Trust model
AXIOM assumes the agent is capable but not trusted: it may be wrong, it may be prompt-injected, and the harness it runs in may fail open. The design therefore moves every decision that matters out of the agent’s hands and into artefacts a human, a policy or a CI job can inspect — and makes “could not decide” mean “no”.
flowchart TB
accTitle: AXIOM trust boundaries
accDescr: Which parts are untrusted agent input, which are trusted repository policy and keys, and where AXIOM checks the boundary.
subgraph agent [Untrusted: the agent]
AG[LLM + harness tools]
end
subgraph gate [AXIOM process — trusted code, untrusted inputs]
direction TB
RT[roots allowlist<br/>--root · realpath · no cwd fallback]
CP[compile → manifestDigest]
CK[checks · profile<br/>fail closed]
AP[apply<br/>confirmDigest · TOCTOU · lock]
HK[gate --stdin<br/>PreToolUse, fail closed]
end
subgraph operator [Operator-owned state]
PR[.axiom/axiom.json<br/>gate policy + checks]
TS[.axiom/trust/keys.json<br/>state.json + state.key]
FL[CLI-only switches:<br/>--allow-guards · --allow-net · gc]
end
subgraph proof [Proof that survives]
JN[journal · applied marker]
SG[DSSE signatures]
AT[in-toto attestation → Sigstore]
end
AG -->|Plan| CP --> CK --> AP --> JN
AG -->|raw write tool call| HK
AG -.->|cannot reach| FL
RT --> CP & CK & AP
PR --> CK & HK
TS --> CK
AP -->|"verify ‑‑tree"| AT
CP -->|axiom keys sign, key never in server| SG --> CK
Boundaries, from the outside in
Section titled “Boundaries, from the outside in”1. Roots: an explicit allowlist, no fallback
Section titled “1. Roots: an explicit allowlist, no fallback”The server (axiom mcp --root A --root B) and every CLI verb act only on directories named up
front. Each is realpathed at startup and must be a directory; the set is frozen. A tool’s root
argument is realpathed too and must equal an allowlisted root or lie inside one (case-insensitive
on Windows). There is no cwd fallback, no environment variable, no .git walk-up, and the MCP
client’s roots/list is not consulted (it is untrusted, and deprecated in the 2026-07-28 spec).
What this buys: an agent cannot make AXIOM write outside the repositories an operator chose, no
matter what path it puts in a Plan — .., absolute paths, drive letters, backslashes are rejected
by the RelPath schema before any I/O, and symlinked or junctioned ancestors by the containment
walk (ERR_SYMLINK_IN_PATH, ERR_CONTAINMENT).
The one place cwd is accepted is the PreToolUse hook, where the harness itself spawns the
process in the project directory and puts that directory in the payload — it is the harness’s
declared root, not an accident (hooks.md).
2. The digest a human confirms
Section titled “2. The digest a human confirms”axiom_apply (and axiom plan apply --confirm) refuses unless confirmDigest === manifestDigest.
The digest is stable across machines (invariant 1) and identifies exactly one set of bytes at
exactly one set of paths (invariant 2), so an approval prompt that shows it — codai’s harness
does, and a destructiveHint: true annotation lets any MCP client ask — is approving something
concrete. Between the dry-run a reviewer looked at and the apply, nothing can be swapped: the
bundle is re-canonicalised and re-hashed, every blob is re-hashed, and every pre-image is
re-hashed at commit (pipeline.md).
3. Policy that fails closed
Section titled “3. Policy that fails closed”Checks are the operator’s rules over the whole change set. Two properties make them a boundary rather than advice:
erroris a verdict, and it blocks. An unknown predicate, invalid params, a provider that could not run (no root forrepo.*, guards not enabled,cedar-wasmabsent, a guard that timed out or printed garbage), a driftedpreImage— all yieldverdict: "error", whichapplytreats exactly likefail. There is no configuration that turns a non-answer into a pass.- The dangerous switches are not tools.
guard.externalruns only when the process was started with--allow-guardsand the profile setsfacts.allowGuards; network fetching ofrefsources isaxiom plan compile --allow-neton the CLI only;axiom gchas no MCP tool. An agent talking to the server cannot enable any of them.
The PreToolUse gate applies the same rule to raw harness writes (D-18): malformed payload, a
write tool whose target it cannot determine, an unreadable profile — all deny with a reason.
--fail-open exists for editors that must never be blocked by the gate’s own bugs, and is the
operator’s choice, in the hook config. An override (axiom-override: <reason>) is honoured only
from the call’s explanation/goal/description/reason fields or a trailing shell comment outside
quotes — never from the content being written — and the hard codes axiom setup init writes
(force-push, reset --hard, clean -f, forced worktree removal) cannot be overridden at all.
Policy lives in the repository — since 3.0 in one .axiom/axiom.json (version: 3, gate,
checks, extends); the older .axiom/gate-profile.json and .axiom/profiles/*.json are still
read with a deprecation warning. A version above the running major denies instead of being
half-understood. It is meant to be committed and reviewed like CI configuration
(profiles.md).
4. Signed manifests and anti-rollback
Section titled “4. Signed manifests and anti-rollback”When “the agent compiled it” is not enough provenance, a root can pin Ed25519 public keys in
.axiom/trust/keys.json and require, through manifest.requireSigned, that every bundle it
applies carries a detached DSSE envelope over JCS(manifest) from one of those keys. Because the
signature covers the canonical body, any byte change after signing is signature.bad; a key the
root does not know is signature.unknownKey; the trust store missing is error, not pass.
Two extensions close the replay holes:
- Anti-rollback.
Plan.counteris inside the hash and therefore under the signature; withantiRollback: truethe root rememberslastCounterinstate.json(HMAC-authenticated with a per-rootstate.key) and refusescounter ≤ lastCounter, so an old but genuinely signed bundle cannot undo a newer one. The state advances only onstatus: "applied". - Root binding. A store with a
rootIdaccepts only envelopes signed for that id, so a bundle signed for repo A is not a valid signed bundle for repo B.
Private keys never enter the server: signing is axiom keys sign with AXIOM_SIGNING_KEY or
--key-file, typically in a CI job with a protected secret. Full protocol, key ceremony and the
explicit non-goals (Ed25519 only, no Sigstore keyless, integrity not confidentiality):
signing.md.
5. Attestation: proof that outlives the agent
Section titled “5. Attestation: proof that outlives the agent”axiom plan verify --tree <root> asks whether a real tree matches a manifest — every artifact present
with its digest, every delete absent — and with --attest writes an in-toto Statement
(predicateType: https://dragoscv.github.io/axiom/spec/attestation/apply/v1 —
spec; the legacy https://axiom.dev/attestation/apply/v1 is
still accepted on read — subject = the manifest digest plus one entry per path, no clock). The
GitHub Action uploads it through actions/attest, so months later
gh attestation verify can prove which workflow, on which commit, verified that tree against that
digest (verify-tree.md). This is a different proof from a DSSE
signature: signing says who produced the manifest; attestation says a tree was checked against
it, here, then.
Agent decisions. With record: true in the gate policy, every write/shell call the
gate judged is appended to .axiom/decisions.jsonl — the arguments only as sha256: of their
JCS (RFC 8785) form, so the journal can be shared without the content. axiom journal attest decisions [--sign] emits one in-toto Statement per record with predicateType
https://dragoscv.github.io/axiom/spec/attestation/agent-decision/v0.1
(spec; the legacy
https://auxidus.dev/attestation/agent-decision/v0.1 is accepted on read), the schema proposed in
in-toto/attestation#554 (agent_id,
principal, policy_evaluations, tool_calls[].args_hash, decided_at; the RFC leaves the
argument canonicalization open, AXIOM pins JCS and records args_state as the thread proposes).
The subject is the content-addressed invocation ({name: tool, digest: {sha256: <args hash>}}).
The URI lives in one constant (AGENT_DECISION_V0_1 in @codai/axiom-canon) because #554 intends
an in-toto.io alias once the working group agrees.
OWASP Agent Control Standard
Section titled “OWASP Agent Control Standard”The OWASP Agent Control Standard v0.1 (2026-09-01) defines conformance profiles and spec sections, not numbered controls. Where AXIOM sits against it:
| AXIOM | ACS anchor | Note |
|---|---|---|
axiom gate on PreToolUse (every harness) |
Instrument pillar, steps/toolCallRequest; dispositions allow/deny (§6) |
the deny document carries axiom.verdict in the ACS vocabulary and standard: "owasp-acs/0.1" |
path deny/allow, secret scan, shell scan, steer |
the deterministic policy behind toolCallRequest |
ACS does not define policy content |
fail-closed gate (default), Cursor failClosed, Claude onFailure: block, axiom-hook deny when no gate runs |
§6.4 on_decision_failure: deny |
stricter than the ACS default (proceed, audited) |
journal hash chain (chain.jsonl, axiom journal verify), decision journal |
ACS-Core §8 chain_hash; acs-audit request_hash |
AXIOM hashes request arguments with JCS + SHA-256 like acs-audit |
content-addressed manifest, confirmDigest, pre-image TOCTOU check, atomic apply + rollback |
— (closest: acs-audit request_hash) |
transactional apply has no ACS counterpart |
| DSSE (Ed25519), keyless Sigstore, in-toto attestations | — | ACS-Core uses HMAC envelopes and acs-crypto ML-DSA; DSSE/Sigstore are outside ACS |
AXIOM does not claim ACS conformance (it implements no handshake/hello JSON-RPC session); the
table is a map for reviewers who assess an agent stack against ACS. Against the OWASP Top 10 for
Agentic Applications 2026 the gate and apply path address ASI02 (tool misuse), ASI04 (supply
chain — signed manifests), ASI08 (cascading failures — atomic rollback) and ASI10 (rogue agents —
fail-closed gate, append-only journals).
What the model does not cover
Section titled “What the model does not cover”Be explicit about the edges, because a trust model that overstates itself is worse than none:
- A writer that ignores
.axiom/lock— an editor, agit checkout, another tool — can still race. The TOCTOU check narrows the window to the instant before each rename and detects the change; it cannot prevent it without a snapshotting filesystem. - The shell scan in the gate is a heuristic. It catches
>,tee,rm,sed -i,git reset --hardand friends in a command string; it does not followcd, aliases, scripts,xargs, or writes done from inside an interpreter. The guarantee isPlan → check → apply; the gate is the seatbelt for everything that bypasses it. - Harness timeouts fail open in every harness AXIOM knows. That is the harness’s decision and
the reason the gate is a ~100 ms lazy chunk with no SDK — and why
npxin a hook is banned (install.md). - Key compromise before revocation is trusted;
trust removeandnotBeforeare the tools. - An attacker who can read and write
.axiom/trust/can deletestate.jsonandstate.keyand restart the counter;minCounteron the store bounds that. Protect the trust directory like CI configuration. - Confidentiality is out of scope: manifests, blobs and journals are not encrypted.
The repository’s own threat model, hardening list and disclosure process are in SECURITY.md.
See also
- Invariants — the rules behind each boundary
- Signing — envelopes, trust store, anti-rollback, root binding
- Verify tree — attestation and how to check it later
- Hooks — the fail-closed gate contract