Skip to content

fix(inbox): claim pending messages atomically to stop duplicate delivery #2015

fix(inbox): claim pending messages atomically to stop duplicate delivery

fix(inbox): claim pending messages atomically to stop duplicate delivery #2015

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
security-events: write
jobs:
test:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --all-extras --dev
- name: Run unit tests with coverage
run: |
# CI intentionally includes integration-marked tests except for the Kiro
# provider test, which requires an authenticated external CLI.
uv run pytest test/ \
--ignore=test/providers/test_kiro_cli_integration.py \
--ignore=test/e2e \
-m "not e2e" \
--cov=src/cli_agent_orchestrator \
--cov-report=xml \
--cov-report=term-missing \
-v
- name: Validate Markdown links
run: uv run python scripts/validate_markdown_links.py
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
web-build:
name: Web UI Build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Install dependencies
run: npm ci
working-directory: web
- name: Type check
run: npx tsc --noEmit
working-directory: web
- name: Run tests
run: npm test
working-directory: web
- name: Build
run: npm run build
working-directory: web
aidlc-portfolio-example:
name: AI-DLC Portfolio Example
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.10"
- name: Install dependencies
run: bun install --frozen-lockfile
working-directory: examples/aidlc-portfolio
- name: Type check and test
run: bun run check
working-directory: examples/aidlc-portfolio
cao-mcp-apps:
name: CAO MCP Apps
runs-on: ubuntu-latest
# Per-tier test timing budgets (Requirement 20.3), surfaced as env so the
# vitest tiers (Phase II+) and CI can enforce them:
# Unit < 5s, Component < 8s, Integration < 15s, E2E < 60s.
env:
CAO_TIER_TIMEOUT_UNIT_MS: "5000"
CAO_TIER_TIMEOUT_COMPONENT_MS: "8000"
CAO_TIER_TIMEOUT_INTEGRATION_MS: "15000"
CAO_TIER_TIMEOUT_E2E_MS: "60000"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Set up Python
run: uv python install 3.12
- name: Install Python deps (for the HTTP-only guard + coverage ratchet)
run: uv sync --all-extras --dev
- name: Install MCP-apps deps
run: npm install
working-directory: cao_mcp_apps
- name: Type check
working-directory: cao_mcp_apps
run: |
if ls src/**/*.ts src/**/*.tsx >/dev/null 2>&1; then
npm run typecheck
else
echo "no TypeScript sources yet (Phase 0) — skipping tsc"
fi
- name: Unit tests
working-directory: cao_mcp_apps
# --coverage emits coverage/coverage-summary.json for the ratchet floor.
run: npx vitest run --coverage --passWithNoTests
- name: Build single-file bundles
working-directory: cao_mcp_apps
run: npm run build:all
- name: JIT-free deny-list scan
working-directory: cao_mcp_apps
run: npm run scan:jit
- name: Bundle-size budget
working-directory: cao_mcp_apps
run: npm run check:size
- name: HTTP-only MCP boundary guard
run: uv run pytest test/test_http_only_boundary.py -q
- name: Backend coverage (for the ratchet floor)
# Produces coverage.json (python floor) consumed by coverage:ratchet.
run: |
uv run pytest test/ \
--ignore=test/providers/test_kiro_cli_integration.py \
--ignore=test/e2e \
-m "not e2e" \
--cov=src/cli_agent_orchestrator \
--cov-report=json \
-q
- name: Coverage ratchet
working-directory: cao_mcp_apps
run: npm run coverage:ratchet
cao-mcp-apps-e2e:
name: CAO MCP Apps E2E (Playwright)
runs-on: ubuntu-latest
# E2E tier budget (Requirement 20.3): 60s per test (enforced in
# playwright.config.ts via `timeout: 60_000`).
env:
CAO_TIER_TIMEOUT_E2E_MS: "60000"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install MCP-apps deps
run: npm install
working-directory: cao_mcp_apps
- name: Install Playwright browser (chromium)
run: npm run test:e2e:install
working-directory: cao_mcp_apps
- name: Run Playwright E2E
# playwright.config.ts builds the single-file bundles and starts the
# harness server (e2e/server.mjs) on 127.0.0.1:9889 before the run.
run: npm run test:e2e
working-directory: cao_mcp_apps
lint:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --all-extras --dev
- name: Check code formatting with black
run: uv run black --check src/ test/
- name: Check import sorting with isort
run: uv run isort --check-only src/ test/
- name: Run type checker with mypy
run: uv run mypy src/
# Pre-existing repo-wide mypy errors (agent_scaffold/profile/
# memory_service — unrelated to AG-UI) are tolerated as non-blocking on
# main; this PR introduces none. Mirror main's policy so Code Quality
# stays green rather than silently making mypy a hard gate here.
continue-on-error: true
ag-ui-demo:
name: AG-UI demo (shift-left recording)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (cao-server for the live AG-UI surface)
run: uv sync --all-extras --dev
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install ffmpeg (GIF export)
run: sudo apt-get update && sudo apt-get install -y ffmpeg
- name: Install recorder deps
run: npm install
working-directory: examples/ag-ui/ag-ui-eventsource-viewer/tools
- name: Install Playwright browser (chromium)
run: npm run playwright:install
working-directory: examples/ag-ui/ag-ui-eventsource-viewer/tools
- name: Record demo (renders the six components + refusal; gates on drift)
# The recorder boots a CAO_AGUI_ENABLED cao-server + a static server,
# drives the live generative-UI flow, ASSERTS every component renders and
# the off-list component is refused (non-zero exit on drift — shift-left),
# and exports docs/media/ag-ui-eventsource-viewer-demo.gif.
run: npm run record
working-directory: examples/ag-ui/ag-ui-eventsource-viewer/tools
- name: Upload demo GIF artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: ag-ui-eventsource-viewer-demo
path: docs/media/ag-ui-eventsource-viewer-demo.gif
if-no-files-found: warn
continue-on-error: true
ag-ui-construct-demos:
name: AG-UI construct demos (shift-left recordings)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (constructs + examples run under uv)
run: uv sync --all-extras --dev
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install recorder deps
# ffmpeg is provided by the ffmpeg-static npm package (gif-capable), so
# no apt install is needed here.
run: npm install
working-directory: examples/ag-ui/ag-ui-construct-demos/tools
- name: Install Playwright browser (chromium)
run: npm run playwright:install
working-directory: examples/ag-ui/ag-ui-construct-demos/tools
- name: Record per-feature demos (asserts each construct; gates on drift)
# For each of the four L2 constructs, runs its asserting example in
# offline/synthetic mode and exports docs/media/ag-ui-<slug>-demo.gif.
# A non-zero example exit (a construct regressed) fails this job —
# this is the shift-left gate.
run: npm run record
working-directory: examples/ag-ui/ag-ui-construct-demos/tools
- name: Upload construct demo GIFs artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: ag-ui-construct-demos
path: docs/media/ag-ui-*-demo.gif
if-no-files-found: warn
continue-on-error: true
ag-ui-stock-client-live:
name: AG-UI stock-client live (AC3)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (cao-server + agui run plane)
run: uv sync --all-extras --dev
- name: Set up Node.js (for the @ag-ui/client stock SDK)
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Run stock-client live demo (asserts >=1 post-connect frame)
# Boots a CAO_AGUI_ENABLED cao-server with a keyless mock_cli fleet,
# POSTs a RunAgentInput to /agui/v1/run with the pinned @ag-ui/client
# HttpAgent, and asserts a live frame is received (AC3 / spec task 17.2).
# Non-zero exit fails the job; the script cleans up the server on exit.
run: ./examples/ag-ui/ag-ui-stock-client-live/run.sh
# =======================================================================================
# RUST TUI (issue #321, unit `rust-ci`). THE FIRST JOB IN ci.yml THAT INSPECTS THE RUST
# CRATE AT ALL — measured before this change: `cargo` appeared in only
# publish-to-pypi.yml, never here.
#
# So until this job existed, the crate's 46 tests across 6 targets were green ONLY because
# they had been run by hand. Nothing in CI would have noticed a regression, a clippy
# warning, a cargo-deny advisory, or an FFI dependency appearing in the graph.
#
# ALSO THIS FILE'S FIRST NON-LINUX RUNNER. `wheel-matrix` added macOS/Windows runners to
# the publish workflow; every `runs-on:` in ci.yml was ubuntu-latest.
# =======================================================================================
rust:
name: Rust TUI (${{ matrix.label }})
runs-on: ${{ matrix.os }}
# TS-2 and team.md: every job running a pty test carries an explicit `timeout-minutes`.
# ci.yml had ZERO before this job (the 6 in the repo are all wheel-matrix's, in the
# publish workflow), so a deadlocked pty test would have burned the 6-hour default with
# no diagnosis.
#
# 20 minutes is chosen against measurement, not by feel. Locally, from a COLD target dir:
# 9.6s to build all six test targets, 8.0s to build-and-run the suite, and the pty target
# itself takes 2.12s. A cold CI runner with no cargo cache is slower — toolchain install,
# crate downloads, and a debug build of 39 crates — but that is a minutes-scale budget,
# not a tens-of-minutes one. 20 minutes leaves roughly an order of magnitude of headroom
# over the measured cost while still failing a genuine hang fast.
#
# Deliberately NOT `timeout(1)` around the test command: that coreutils binary is absent
# on some developer machines (hit three times while building this unit) and cannot be
# assumed on a runner image either. `timeout-minutes` is the harness-level bound, and the
# pty harness carries its own per-read deadline as the inner one. (#321)
timeout-minutes: 20
strategy:
# TS-1 / interview Q4: pty buffering, EOF semantics, and drain ordering differ between
# Darwin and Linux, and macOS arm64 is the development platform — so LINUX IS THE
# UNTESTED HALF and this job is what finally exercises it.
#
# `fail-fast: false` for the same reason wheel-matrix uses it: knowing WHICH platform
# fails is the diagnostic value of a matrix whose second leg has never run.
fail-fast: false
matrix:
include:
- os: macos-latest
label: macOS arm64
- os: ubuntu-latest
label: Linux x86_64
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# SHA-pinned, not `@stable` (SR-5, interview Q10): `stable` is a mutable BRANCH on this
# action's repo, so `@stable` re-resolves on every run and is unreviewable supply chain.
# The toolchain channel is selected by the `toolchain` input, which is what `@stable`
# actually did. Same pin wheel-matrix resolved, reused rather than re-resolved.
#
# `components: rustfmt, clippy` is REQUIRED, not decorative: this action installs with
# `--profile minimal`, which ships neither. Without them `cargo fmt`/`cargo clippy`
# would fail as unknown subcommands — a red job for the wrong reason.
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable @ 2026-07-16
with:
toolchain: stable
components: rustfmt, clippy
# The no-FFI check below is a Python script, so Python is provisioned EXPLICITLY rather
# than relying on whatever the runner image happens to preinstall. That is the same
# class of mistake as assuming `timeout(1)` exists — a tool that is present on one
# machine and absent on another, discovered only when the gate fails for the wrong
# reason. GitHub's images do ship Python, but the version differs across the macOS and
# Ubuntu images and is not contractual.
#
# Same SHA pin `wheel-matrix` resolved for the smoke-test job, reused rather than
# re-resolved (SR-5). (#321)
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'
# ---------------------------------------------------------------------------------
# BLOCKING GATE 1 — formatting. Mirrors black/isort's `--check` posture for Python.
# ---------------------------------------------------------------------------------
- name: cargo fmt --check
working-directory: tui
run: cargo fmt --check
# ---------------------------------------------------------------------------------
# BLOCKING GATE 2 — clippy.
#
# `--all-targets` IS LOAD-BEARING AND MUST NOT BE "SIMPLIFIED" AWAY.
#
# team.md affirms `cargo clippy -- -D warnings`. The bare form lints only the default
# target selection and SKIPS TEST TARGETS — so every one of this crate's integration
# tests (tests/pty.rs, tests/endpoint_contract.rs, tests/hermeticity_tripwire.rs,
# tests/no_backend_attach_call.rs, tests/binary_exits_zero.rs) would escape the gate
# entirely. That was discovered while linting a new integration test in
# `skeleton-crate`: the bare command passed on code the `--all-targets` form rejected.
#
# This is a deliberate CORRECTION to an affirmed practice, not an embellishment of it.
# Do not drop `--all-targets` to match the letter of team.md — the affirmed form is
# insufficient, and the test code is where a pty harness's unwrap-heavy mistakes live.
#
# `--locked` (TS-2, interview Q7) so a clippy run cannot quietly re-resolve the graph.
# The escape hatch for a genuine false positive stays per-site: `#[allow(lint_name)]`
# plus an issue-number comment (SR-3) — never a global relaxation here. (#321)
# ---------------------------------------------------------------------------------
- name: cargo clippy (--all-targets so TEST code is linted too)
working-directory: tui
run: cargo clippy --locked --all-targets -- -D warnings
# ---------------------------------------------------------------------------------
# THE TESTS — 46 across 6 targets, including the real-pty harness.
#
# `--locked` so the build FAILS if Cargo.lock would change (TS-2). Cargo.lock is
# committed, and without --locked a silent re-resolution would mean CI tested a
# different graph than the one under review.
#
# This is the step whose Linux half has never run. The pty tests assert real terminal
# behaviour — isatty, TIOCSWINSZ resize, non-UTF-8 byte passthrough, SIGTERM->SIGKILL
# escalation, and a concurrent drain that must not deadlock — and every one of those
# differs in detail between Darwin and Linux. (#321)
# ---------------------------------------------------------------------------------
- name: Assert no Python-FFI crate in the dependency graph (FR-5.2)
# Ordered BEFORE `cargo test` and marked `if: always()` deliberately. It used to run
# AFTER the tests with no condition, so a test failure -- which happened on EVERY PR,
# since no cao-server was started -- meant FR-5.2's no-FFI gate NEVER EXECUTED. A red
# job is loud; a gate that never ran is not. Found by the ss12a reviewer for
# skeleton-endpoint-verify. (#321)
if: always()
run: python scripts/assert_no_ffi.py --manifest-path tui/Cargo.toml
# The three live-contract tests in tui/tests/endpoint_contract.rs call a REAL cao-server;
# they are the deliberate hermeticity exemption. Without a server they fail on
# `Connection refused` on every PR (verified: with CAO_API_PORT=19999 they FAIL rather than
# skip, which is correct BR-7 behaviour and is why this gap was visible at all).
#
# `uv sync` is required because this job installs Python but NOT the project — it had
# `setup-python` and no `uv sync`, so `cao-server` did not exist here at all. (#321)
# This job runs `uv` but had NO `setup-uv` — it was the only uv-using job in the workflow
# without it, so `uv sync` would fail with `uv: command not found`. Caught by comparing every
# job's setup-uv presence against its uv usage rather than assuming the toolchain was there.
# NOTE: `@v4` is a MUTABLE tag, matching the three pre-existing setup-uv call sites in this
# file. The affirmed SHA-pinning rule covers actions THIS INTENT adds; pinning here while the
# other three stay on @v4 would be inconsistent without pinning all four. Flagged in
# ci-config.md as a follow-up rather than half-applied. (#321)
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install the Python project (provides cao-server for the live contract test)
run: uv sync --all-extras --dev
- name: Start cao-server and wait for readiness
# Poll /health rather than `sleep N`: a fixed sleep is either flaky or slow, and /health is
# the same endpoint the TUI itself probes. 30 x 1s matches the readiness cap the TUI uses.
# `timeout(1)` is NOT used — it does not exist on macOS runners by default. (#321)
run: |
# The PID IS recorded and IS read — by the teardown step below. An earlier revision
# wrote /tmp/cao-server.pid that nothing read, and the revision after that dropped the
# file entirely on the reasoning that "the runner is ephemeral and torn down with the
# job, so a cleanup step would be ceremony."
#
# THAT REASONING WAS WRONG, and the first CI run is what proved it: `setup-uv`'s
# POST-JOB step runs before the runner is torn down, and on macOS it hung for exactly
# 300s in `uv cache prune --ci` and then failed the job with exit 2 — after all 8 pty
# tests had PASSED. The job's own teardown line names the culprit: "Terminate orphan
# process: pid (12288) (Python)". The server was still running while the post step tried
# to prune the cache underneath it.
#
# Linux passed this step only by luck: its uv cache RESTORED, and `setup-uv` skips the
# prune-and-save path entirely on a cache hit. macOS missed ("Failed to restore: Cache
# service responded with 400"), took the prune path, and hit the contention. So this is
# a cache-miss-only failure on BOTH platforms, not a Darwin quirk — which is why the fix
# is an explicit shutdown rather than a platform condition. (#321)
uv run cao-server &
echo "$!" > "${RUNNER_TEMP}/cao-server.pid"
for i in $(seq 1 30); do
if curl -sf "http://127.0.0.1:9889/health" >/dev/null 2>&1; then
echo "cao-server ready after ${i}s"
exit 0
fi
sleep 1
done
echo "cao-server did not become ready within 30s" >&2
exit 1
- name: cargo test --locked
working-directory: tui
run: cargo test --locked
# `if: always()` so the server is stopped even when the tests FAIL. Without it a red test
# run would leave the server up and turn one honest failure into two — the test failure plus
# a post-job cache error that has nothing to do with the change under review.
#
# Kills the process GROUP (`-PID`), not just the recorded pid: `uv run cao-server` is a
# parent `uv` process that execs the server as a child, and the Linux teardown listed BOTH
# ("pid (3510) (uv)", "pid (3517) (cao-server)"). Killing only the recorded pid would leave
# the other alive and holding the cache — the exact condition this step exists to remove.
#
# Never fails the job: `|| true` on every kill and `exit 0` at the end. A cleanup step that
# can fail is a second way for a green build to go red, and "the server had already exited"
# is not a defect. (#321)
- name: Stop cao-server before the post-job cache steps run
if: always()
run: |
pidfile="${RUNNER_TEMP}/cao-server.pid"
if [ ! -f "$pidfile" ]; then
echo "no pidfile at $pidfile; cao-server was never started"
exit 0
fi
pid="$(cat "$pidfile")"
# SIGTERM the group, give it a moment, then SIGKILL whatever ignored it. Mirrors the
# pty harness's own escalation rather than inventing a second shutdown policy.
kill -TERM "-${pid}" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
for _ in 1 2 3 4 5; do
kill -0 "$pid" 2>/dev/null || break
sleep 1
done
kill -KILL "-${pid}" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
rm -f "$pidfile"
echo "cao-server stopped"
exit 0
# ---------------------------------------------------------------------------------
# THE NO-FFI GATE (FR-5.2, SR-4, T-10) — hard gate, on BOTH platforms.
#
# T-10 forbids linking the TUI against the Python runtime: the boundary is subprocess
# for CLI invocation and HTTP for server reads. The affirmed rule requires this be a
# DETERMINISTIC dependency-graph check rather than a code-review convention, because an
# FFI crate does not arrive as a reviewable line in Cargo.toml — it arrives several
# levels down a transitive chain in a diff that shows only Cargo.lock churn.
#
# PROVEN ABLE TO FAIL, not assumed: `pyo3 = "0.22"` was planted in tui/Cargo.toml, the
# graph re-resolved (39 -> 53 crates), and the check rejected it — naming both `pyo3`
# and the transitively-pulled `pyo3-ffi` — before Cargo.toml/Cargo.lock were restored
# byte-identically. test/test_no_ffi_guard.py keeps that property asserted in the Python
# suite, since the live proof mutates the lockfile and cannot run in CI.
#
# Runs on both platforms deliberately: cargo-deny's own [bans] section carries an
# independent implementation of the same ban, but that job is a Docker action and
# therefore Linux-only. This step is the macOS coverage. (#321)
# ---------------------------------------------------------------------------------
# `python`, not `python3`: actions/setup-python puts `python` on PATH on every platform,
# and this matches how publish-to-pypi.yml already invokes scripts/assert_wheel_matrix.py.
# No `working-directory` — the script takes a repo-root-relative --manifest-path.
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Generate requirements.txt for scanning
run: |
uv export --format requirements-txt > requirements.txt
# SR-4 (interview Q1): `exit-code: 1` makes this job FAIL on a CRITICAL/HIGH finding.
# Before this, the step had no exit-code, so it uploaded SARIF and continued — meaning
# "Security Scan passed" attested that a scan COMPLETED, not that it was clean. The
# rest of the repo already treats scanners as gates: secret-scan.yml passes
# `--exit-code 1` to gitleaks at both :68 and :82. This closes the inconsistency
# rather than introducing a new policy. (#321)
#
# SARIF is still produced and uploaded (see the next step's `if: always()`), so the
# Security tab keeps its history even on a failing run.
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.69.3
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
exit-code: '1'
# FR-8: the SARIF step above writes findings to a FILE, so a failing scan
# logs an exit code and names nothing — diagnosing #568 took an out-of-band
# `gh api .../code-scanning/alerts` call to learn what had failed. This step
# re-runs the same scan in table format so the finding appears in the log.
#
# `if: failure()` — runs only when the gate above fails, so a green run pays
# nothing. `exit-code: '0'` — this step must not be the one that fails the
# job; the gate above already did, and a second failure would obscure which
# step is authoritative.
#
# No `severity:` input, matching the effective gate: trivy-action's
# entrypoint.sh unsets TRIVY_SEVERITY whenever format is sarif, so the step
# above scans at ALL severities. Passing CRITICAL,HIGH here would print a
# narrower set than the one that failed the build — and could print NOTHING
# while the job is red.
- name: Show Trivy findings in the log
if: failure()
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
scan-type: 'fs'
scan-ref: '.'
format: 'table'
ignore-unfixed: true
exit-code: '0'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
# `always()` is load-bearing now that Trivy can fail: without it, a CRITICAL finding
# would fail the scan step and SKIP the upload, hiding the very finding that failed
# the build from the Security tab.
if: always()
with:
sarif_file: 'trivy-results.sarif'
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.repository == 'awslabs/cli-agent-orchestrator'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0
# NOTE: CodeQL runs via GitHub's "default setup" (visible as the `Analyze (...)`
# checks on every PR). Adding a workflow-based CodeQL job here would conflict
# with default setup ("CodeQL analyses from advanced configurations cannot be
# processed when the default setup is enabled") and fail uploads. To widen the
# query suite beyond the default, toggle default setup off in
# Settings → Code security → CodeQL default setup and re-add a workflow job.
#
# ---------------------------------------------------------------------------------------
# RUST SAST IS AN ACCEPTED, NAMED GAP — considered and declined, not forgotten (SR-6,
# interview Q6; issue #321).
#
# There is deliberately NO Rust static-analysis/scanning job in this file. The reason is the
# constraint recorded immediately above: CodeQL here runs via default setup, and a
# workflow-based scanning job that uploads SARIF conflicts with it and fails the upload. So
# adding one would not buy Rust SAST — it would break the analysis that already runs.
#
# What covers the Rust code instead, and it is genuinely less than SAST:
# - `cargo clippy --locked --all-targets -- -D warnings` (the `rust` job) — a hard gate,
# linting TEST targets as well as the binary. The per-site escape hatch is
# `#[allow(lint_name)]` plus an issue-number comment (SR-3), never a global relaxation.
# - `#![forbid(unsafe_code)]` at tui/src/main.rs line 1 — memory-safety classes that SAST
# would hunt for cannot be expressed in this crate at all. It is a compile-time refusal,
# not a warning.
# - `cargo-deny` (.github/workflows/cargo-deny.yml) — RustSec advisories over the whole
# resolved graph, weekly as well as per-PR. That is dependency CVE coverage, which is a
# different axis from first-party static analysis.
#
# WHAT REMAINS UNCOVERED: first-party Rust logic flaws — taint flow into a subprocess argv,
# an injection path through a constructed command, a TOCTOU on a file the TUI writes. clippy
# is a lint pass, not a dataflow analysis, and will not find those. Unit `rust-ci` treats this
# as an accepted gap rather than papering over it; to close it, toggle CodeQL default setup off
# (Rust is supported by CodeQL) and add a single advanced-configuration job covering Python and
# Rust together.
# ---------------------------------------------------------------------------------------