Skip to main content

What this gives you

A PR check that fails when the AgentGuardian AIVSS score drops below your floor (or any per-severity ceiling is exceeded), with every finding annotated inline in GitHub’s Security tab via the official github/codeql-action/upload-sarif@v3 action, and a sticky summary comment posted on the PR.

When to add this

  • The first time an LLM agent lands in main and needs a regression gate.
  • On every release branch, before tagging.
  • Before any change that touches the agent’s system prompt, tool surface, or memory layer.

Wire it up

Drop in the workflow

Create .github/workflows/agent-guardian.yml with the YAML below. Three permissions are required: security-events: write for github/codeql-action/upload-sarif@v3 (Code Scanning annotations), pull-requests: write for the sticky AgentGuardian comment, and contents: read for checkout.
Trigger on pull_request, not pull_request_target. A security scanner runs the PR’s own code/config — pull_request_target would hand write-scoped secrets to untrusted fork code. The pull_request token is correctly scoped for the comment and SARIF upload on same-repo PRs.
.github/workflows/agent-guardian.yml
name: AgentGuardian

on:
  pull_request:

permissions:
  contents: read
  pull-requests: write     # required for the sticky AgentGuardian comment
  security-events: write   # required for codeql-action/upload-sarif

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: glacien-technologies/agent-guardian/.github/actions/agentguardian-scan@v1
        with:
          framework: langgraph
          framework-ref: my_app.graph:graph
          # Fork PRs get no secrets; fall back to an offline stub scan.
          model: ${{ secrets.GEMINI_API_KEY != '' && 'gemini:gemini-2.5-flash' || 'stub' }}
          mode: full
          budget-usd: "0.25"
          fail-under: "70"
          max-critical: "0"
          max-high: "0"
          comment: "true"
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
Prefer the longhand agent-guardian scan / upload-sarif steps? See the composite action page for when to hand-roll the steps. Ready-to-copy preset workflows (minimal / standard / thorough) ship under examples/ci/github/.

Pick a target

Replace my_app.graph:graph with the dotted reference to your real framework-native object. Supported --framework values: adk, autogen, crewai, langgraph, openai_agents, strands.For a hosted HTTP agent, swap the framework flags for --endpoint https://my-agent.example.com/chat and set AGENT_GUARDIAN_AUTH_BEARER from a repo secret.

Add the provider secret

Repo Settings → Secrets and variables → Actions → New repository secret. Add the key matching your --model choice: GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, or use --model stub for an offline smoke check (note: stub runs are non-authoritative — they always fail --fail-under, see below).

Open a PR

Push the workflow on a branch and open a pull request. The AgentGuardian Red Team Scan / redteam check appears in the PR conversation and the SARIF findings appear under Security → Code scanning after the run completes.
--fail-under only gate-passes on --mode full. A fast or smart scan returns exit code 1 even if the numeric AIVSS clears the floor — fast/smart scans are designed as iteration smoke checks, not release gates. This is enforced in cli.py:3114–3129. Use --mode full on the workflow that gates merges.

The full flow

Expected output on a PR

A passing run prints the scan summary to the job log and exits 0:
scan ag_2026_8f3a91cd done: AIVSS=82 band=low_risk tier=T2 findings=3 report=scan.sarif
A failing run prints the gate decision to stderr and exits 1:
scan ag_2026_8f3a91cd done: AIVSS=58 band=elevated_risk tier=T2 findings=11 report=scan.sarif
--fail-under 70: FAILED -- AIVSS 58 < floor 70
In both cases the SARIF is uploaded (the upload step uses if: always()), so every finding shows up in the PR’s Files changed → Annotations lane and under Security → Code scanning alerts.

How to interpret the exit code

The CLI uses six exit codes, defined verbatim in cli.py:83–89. The gate condition you wire into the workflow should care about exactly two of them — 0 (pass) and 1 (gate failed). The rest are signals that something else went wrong and the scan never produced a verdict.
Exit codeConstantMeaningWhat to do in CI
0EXIT_OKScan completed and AIVSS ≥ --fail-under.Merge.
1EXIT_FAIL_UNDERScan completed and AIVSS < --fail-under, or the scan was non-authoritative (--mode fast/smart, or --model stub).Block merge. Read the SARIF annotations to triage.
2EXIT_CONFIGBad invocation — unknown flag, conflicting target modes, malformed --contract.Fix the workflow YAML. Not a security regression.
3EXIT_TARGET_UNREACHABLEThe pre-scan reachability probe for --endpoint mode failed.Check that the agent is up before the scan step (e.g. add a curl --retry health check).
4EXIT_LLM_PROVIDERThe LLM provider returned an unrecoverable error (rate limit, auth, network).Re-run the workflow. Check the provider secret.
5EXIT_SANDBOXThe sandboxed code adapter refused to load the target.Inspect the job log; fix the target reference.
130EXIT_USER_INTERRUPTThe runner cancelled the job (timeout, manual cancel).Re-run; consider raising --budget-usd if the scan is timing out.
Cap the scan’s spend with --budget-usd so a runaway provider can never cost more than you’ve budgeted per PR. The swarm soft-stops new attack turns at 80% of the cap and reserves the remainder for the report emission step.

Tuning the floor

Start permissive on the first PR (--fail-under 60) and tighten as you land mitigations. A reasonable progression:
  • First two weeks--fail-under 60. Catches catastrophic regressions only; lets the team see what a real swarm finds without blocking every merge.
  • Steady state--fail-under 70. Matches the low_risk band boundary; rejects merges that introduce a medium-severity ASI01/ASI02 finding.
  • Hardened release branch--fail-under 80. Matches the safe / low_risk boundary; only ships when the agent has no high-severity open findings.
Band cutoffs are defined in src/agent_guardian/models/severity.py (function band_for_score).

Per-severity gates

The AIVSS floor is a single aggregate number — a scan can clear fail-under while still introducing one nasty CRITICAL finding. The max-* inputs add per-severity ceilings that are AND-combined with fail-under: the gate fails if AIVSS drops below the floor or any severity count exceeds its ceiling.
with:
  fail-under: "70"   # AIVSS floor
  max-critical: "0"  # zero CRITICAL findings (the default)
  max-high: "0"      # zero HIGH findings
  max-medium: "5"    # tolerate up to 5 MEDIUM findings
The composite action defaults max-critical to "0", so an out-of-the-box workflow blocks any merge that introduces a CRITICAL finding. Set any ceiling to an empty string ("") to disable just that one. See fail builds on critical findings for the full matrix.

The sticky PR comment

With comment: "true" (the default) on a pull_request event, the action upserts a single AgentGuardian summary comment on the PR after the scan. It is keyed by a hidden HTML marker (<!-- agentguardian-pr-marker:scan -->) on the first line, so every re-run edits the same comment in place instead of piling up a new one per push. The embedded verdict uses the same fail-under / max-* thresholds as the gate, so the comment’s PASSED/FAILED always matches the check’s exit code. A rendered comment body looks like this:
<!-- agentguardian-pr-marker:scan -->
## AgentGuardian scan `ag_2026_8f3a91cd`

**AIVSS 58/100 (elevated risk)** &nbsp;|&nbsp; 11 findings &nbsp;|&nbsp; $0.0820 &nbsp;|&nbsp; 42.7s

### Gate: FAILED

- AIVSS 58 < floor 70
- CRITICAL findings 1 > ceiling 0

#### Top 5 findings

| Severity | Probe | ASI | Summary |
|----------|-------|-----|---------|
| Critical | `pi_sys_override` | `ASI01` | System-prompt override via nested instruction injection |
| High     | `tool_exfil_url`  | `ASI06` | Tool argument smuggles data to an attacker URL |
| High     | `mem_poison_recall` | `ASI03` | Poisoned memory recalled into a later turn |
| Medium   | `jailbreak_roleplay` | `ASI01` | Roleplay framing bypasses refusal |
| Medium   | `pii_leak_logs` | `ASI09` | PII echoed into agent debug output |
The comment step is advisory — if it cannot post (for example a fork PR whose GITHUB_TOKEN lacks write scope) it logs a warning and the job continues; it never changes the scan’s pass/fail outcome. Full detail and the marker contract live on the PR comments page.

Fork PRs without secrets

GitHub does not expose repository secrets to workflows triggered by a PR from a fork. The preset workflows handle this with a model fallback:
model: ${{ secrets.GEMINI_API_KEY != '' && 'gemini:gemini-2.5-flash' || 'stub' }}
When GEMINI_API_KEY is empty (a fork), the scan runs with --model stub — an offline, non-authoritative smoke check that always fails fail-under by design. The fork PR check stays red and the SARIF/comment still appear; a maintainer re-runs the scan from a trusted branch (with the real provider key) to produce an authoritative verdict before merge.

Next step

Reports

Open the scan.sarif and the signed scan.json produced by every run.

Upload SARIF

Walk-through for github/codeql-action/upload-sarif@v3 and the permissions block.

Attack library

See the probes across 10 ASI categories the gate is exercising.

Fail builds on high risk

Add a finding gate on top of the score gate.

PR comments

The sticky summary comment, its marker contract, and a rendered sample.