Trust Anchor Adoption
This guide explains how to add the Flow Agents CI trust anchor to any repository that uses Flow Agents. The anchor is a required CI job that re-runs your canonical verification fresh in a clean environment and reconciles the agent’s claimed passes against the real CI results. It is the external, un-disablable check that closes the loop on agent self-reporting.
See ADR 0017 for the full security model and threat analysis.
What the Trust Anchor Does
-
Re-runs verification fresh. In a clean CI environment the agent does not control, it runs your declared verify command (build + tests + lint). Real exit codes. No agent influence.
-
Reconciles the delivered bundle. If the agent published a canonical
delivery/<task-slug>/trust.bundle(or legacydelivery/trust.bundle) with the PR, the anchor cross-checks every claimed-pass command against CI’s own fresh results. Divergences (claimed pass + CI fail, laundered command, claim with no evidence, checkpoint-only bundle) fail the job with a clear diagnostic. -
Fails closed on compile-only. If no comprehensive verify command is configured, the anchor refuses to pass — preventing a “build only” attestation that misses tests.
-
Makes bundle absence explicit.
missing-bundle-policy: requiredis the stable, fail-closed default for an armed gate. During advisory adoption, setmissing-bundle-policy: advisory: fresh verification must still pass, while a missing current bundle is reported without failing the job. Bundle divergence and failed fresh verification always fail; the legacyfail-on-divergenceinput no longer weakens the gate.
Step 1 — The Agent Publishes a Bundle
Flow Agents’ deliver skill calls publishDelivery, which writes
delivery/<task-slug>/trust.bundle to the repository with git add -f during the
record-release step. This file carries the session’s evidence and claims to CI so the
anchor can reconcile them. The action delegates ownership-aware discovery to the reconciler,
which selects the newest bundle that attests the change across per-session and legacy flat
paths.
You do not need to configure this — it is part of the deliver skill workflow. The bundle is gitignored by default (the deliver skill force-adds it for the PR commit only).
Step 2 — Add the Composite Action
In your repo, create or update a CI workflow file (e.g.
.github/workflows/trust-verify.yml):
name: Trust Verify
on:
pull_request:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: trust-verify-$
cancel-in-progress: true
jobs:
trust-verify:
name: Trust Verify
runs-on: ubuntu-latest
timeout-minutes: 15
# Add id-token: write here if you enable sign: true (Sigstore attestation).
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Ownership checks compare bundle commits through git ancestry.
fetch-depth: 0
- uses: kontourai/flow-agents/.github/actions/trust-verify@<SHA>
with:
# Declare your comprehensive verify command: build + tests + lint.
# The agent must run this same command locally (via trust-reconcile-verify).
verify-command: "npm run build && npm test && npm run lint"
# Use advisory only while the check is not yet required in branch protection.
missing-bundle-policy: required
# bundle: leave empty for ownership-aware discovery across legacy flat and
# per-session delivery paths; set only for a deliberate explicit path
# sign: false (set to true + add id-token: write for Sigstore attestation)
Replace <SHA> with the pinned commit SHA of the kontourai/flow-agents release you
are adopting. Pin to a SHA (not a tag) for supply-chain security.
To find the SHA: look at the
flow-agents releases or pin to
main HEAD after reviewing the CHANGELOG.
Step 3 — Arm It as a Required Status Check
The action reports results but is advisory until you arm it server-side:
- Go to Settings → Branches in your GitHub repository.
- Edit (or create) the branch protection rule for
main. - Under Require status checks to pass before merging, add
Trust Verify. - Check Require branches to be up to date before merging.
- Enable Do not allow bypassing the above settings (the “enforce admins” option).
Once armed, no PR can merge past a Trust Verify failure — including ones pushed by
the agent.
Use missing-bundle-policy: advisory only during a deliberate observation period before
arming the status check. Change it to required when the check becomes required. This
setting controls bundle absence only: a failed fresh verification still fails in either
mode.
Step 4 — Protect the Verify Config
CODEOWNERS prevents the agent from quietly weakening the verify command. Add entries for the files that declare what CI runs:
# Trust anchor config — requires owner review.
# An agent cannot weaken verify-command or the reconcile manifest without a human
# approving the change.
.github/workflows/trust-verify.yml @your-org/owners
package.json @your-org/owners
evals/ci/run-baseline.sh @your-org/owners
package.json covers both trust-reconcile-verify and the trust-reconcile-manifest
declaration (see below); evals/ci/run-baseline.sh covers the manifest registry when you
source the manifest from a run-baseline.sh-shaped registry. Adjust paths and team names
for your repo structure.
Declaring a Manifest (Multiple Commands)
The single trust-reconcile-verify command above is the anchor’s standalone fresh-verify
(and the legacy reconcile path — a manifest of size 1). To reconcile granular,
per-acceptance-criterion evidence — some commands, some session-local/manual/provider
evidence — declare a reconcile manifest: a set of named, individually-re-runnable
commands, each of which must actually run in a required CI lane (see ADR 0020).
Resolution priority (first match wins):
- CLI
--manifest '<json>' TRUST_RECONCILE_MANIFESTenvironment variable (JSON)-
package.json"trust-reconcile-manifest"— either an inline array, or a string command that emits the JSON:{ "trust-reconcile-manifest": [ { "id": "unit", "command": "npm test" }, { "id": "lint", "command": "npm run lint" }, { "id": "types", "command": "npm run typecheck" } ] }Every
id/commandhere must be a command your required CI actually runs. If your repo already has a lane registry that CI invokes, point the key at its machine-readable emit instead of duplicating it (this is what Flow Agents does — see the mirror section):{ "scripts": { "trust-reconcile-manifest": "bash evals/ci/run-baseline.sh --manifest-json" } } evals/ci/run-baseline.sh --manifest-json(auto-detected, if present).
How classification works. A claim reconciles per-command against fresh CI results only
when its evidence is evidenceType: "test_output" and its command matches a manifest
entry. Honest session-local evidence (human_attestation, crawl_observation,
document_citation, policy_rule, source_excerpt, attestation) is never flagged
“not-run” for lacking a command — it is accepted on its Surface-derived verified/assumed
status. A test_output claim whose command is not in the manifest is still a divergence
(you cannot self-label an arbitrary command test_output to dodge the manifest).
Session-local passes are loudly, not quietly, marked. A non-command-backed claim that
re-derives verified (and carries no waiver) is NOT independently re-runnable by CI — the
anchor can only confirm the bundle is internally self-consistent, not that the underlying
attestation is true. It prints a distinct line for every such claim:
[trust-reconcile] ATTESTED (not independently verifiable at L0): '<claimId>' (<claimType>) evidenceType=<type> — accepted on bundle-internal consistency only; see ADR 0020 Residuals
plus a summary count (N attested claim(s) accepted without independent verification). This
does not change the exit code — attestations are not blocked, only disclosed — so review the
ATTESTED lines and count in any bundle before trusting it. See the Residuals section of
ADR 0020 for the full
disclosure (fabricated self-consistent attestations, unauthenticated approved_by, and the
evidenceType-laundering route).
Waiving an accepted gap. For an honestly-accepted gap, record it with both flags:
npm run workflow:sidecar -- record-evidence <artifact-dir> --verdict pass \
--check-json '{"id":"load-test","kind":"external","status":"skip","summary":"sustained-load perf test"}' \
--accepted-gap-reason "load-test env not provisioned this cycle; tracked for the perf milestone" \
--waived-by "your-name"
Both --accepted-gap-reason and --waived-by are required together — an accepted gap
with no justification or approver is refused (no silent waiver). Reuse a separate
record-evidence invocation for waived checks: the flags apply to every check in the
invocation. The anchor prints each waived claim on a distinct, un-suppressible
[trust-reconcile] WAIVED: ... line in the required job’s own log — reviewed and visible,
never silent.
Configuring the Verify Command
The anchor fails closed if it cannot find a comprehensive verify command. Provide it one of three ways (in priority order):
- Action input
verify-command(recommended for the composite action). TRUST_RECONCILE_COMMANDSenvironment variable (comma- or newline-separated).-
package.jsonscripts["trust-reconcile-verify"]— the anchor auto-discovers this key. Add it to yourpackage.json:{ "scripts": { "trust-reconcile-verify": "npm run build && npm test && npm run lint" } }Then you can also run it locally:
npx @kontourai/flow-agents verify
Local Use
The flow-agents verify CLI subcommand runs the same trust-reconcile logic locally:
# Install (or npx):
npm install -D @kontourai/flow-agents
# Re-run verify + reconcile against a delivered bundle explicitly:
npx @kontourai/flow-agents verify \
--commands "npm run build,npm test" \
--bundle delivery/<task-slug>/trust.bundle
# Auto-discover bundle + verify command from package.json:
npx @kontourai/flow-agents verify
# Help:
npx @kontourai/flow-agents verify --help
Exit codes: 0 = clean (fresh verify passed, no divergence); 1 = failed/divergence.
Mirror: Flow Agents’ Own Setup
Flow Agents uses the same pattern in its own repository:
scripts/ci/trust-reconcile.js— the anchor script (runs in.github/workflows/trust-reconcile.yml).package.jsontrust-reconcile-verify—npm run build && npm run eval:static(the standalone fresh-verify / legacy single-command reconcile path).package.jsontrust-reconcile-manifest—bash evals/ci/run-baseline.sh --manifest-json(the reconcile manifest is the liverun-baseline.shLANE_*registry — every entry runs in a requiredci.ymllane by construction).evals/ci/antigaming-suite.sh— the regression suite that proves the gate and anchor work (includingtest_trust_reconcile_manifest.shand the end-to-endtest_trust_reconcile_mixed_bundle.sh); runs in the requiredci.ymllane.- Branch protection on
main—Trust Reconcilerequired,enforce_adminson.
Adoption Checklist
- Deliver skill is configured and publishes
delivery/<task-slug>/trust.bundle. .github/workflows/trust-verify.ymladded and the composite action is pinned.verify-commanddeclares a comprehensive verify (build + tests + lint).missing-bundle-policymatches the adoption phase (advisorywhile observing,requiredbefore branch protection is armed).Trust Verifyadded as a required, no-bypass status check onmain.- CODEOWNERS entry protects
trust-verify.ymlandpackage.json. - (Optional)
scripts["trust-reconcile-verify"]inpackage.jsonfor local use. - (Optional)
sign: true+id-token: writefor Sigstore attestation.
Troubleshooting
“no comprehensive trust-reconcile-verify configured”: Provide verify-command in
the action input, set TRUST_RECONCILE_COMMANDS, or add scripts["trust-reconcile-verify"]
to package.json. The anchor refuses to attest a compile-only check.
“trust divergence: agent claimed X passed; CI fresh run = FAIL”: The agent’s local environment or shell profile produced a false pass. The anchor correctly flagged the mismatch. Fix the underlying test failure.
“trust divergence: command contains exit-code-laundering operator”: A claimed
command used ||, ; true, or ; exit 0. These mask real exit codes. Remove them.
“command is not in the reconcile manifest”: A test_output claim named a command that
is not a manifest entry. Either declare the command in your trust-reconcile-manifest (and
ensure it runs in a required CI lane), or, if the evidence is genuinely session-local
(manual/provider/observation), record it with a non-test_output evidenceType so it is
classified session-local instead of expected to be CI-re-runnable.
“session-local claim … asserts pass … but has no waiver and no Surface-derived
verified/assumed status”: An honest session-local claim must either resolve a real
verified/assumed Surface status from its own evidence, or be explicitly waived with
--accepted-gap-reason + --waived-by. Classification is not a pass bypass.
“checkpoint-only bundle cannot be reconciled per-command”: A delivery/trust.bundle
was expected but only delivery/trust.checkpoint.json was found. The deliver skill
publishes the full bundle; ensure it ran correctly.
“ATTESTED (not independently verifiable at L0): …“: Not an error — the job still
passes. This is the anchor telling you a claim was accepted on bundle-internal consistency
only (no CI command to re-run and no independent, cryptographically-verified attestation).
Review each ATTESTED line and the summary count before trusting the bundle; see
ADR 0020’s Residuals section.