AARM Core Conformance Self-Attestation
Self-attested — not third-party certified. This document is Containment.AI's own assessment of its production proxy and agent runtime against the AARM v1.0 requirements. It has not been reviewed, certified, or endorsed by the Cloud Security Alliance, the CSAI Foundation, or any external conformance body, and no submission to the AARM Conformance Program has been made on the basis of it.
Published: 2026-06-11.
Scope: Containment.AI proxy (proxy.containment.ai) production deployment + the containment-ai-agents orchestrator runtime (R6 identity layer).
Aligned with: AARM v1.0 (CSA / CSAI Foundation, February 2026).
Attested edition (pinned, 2026-07-11): AARM v1.0 — the original
nine-requirement (R1–R9) numbering. Later editions renumber and re-tier the
controls: the AARM/CS-1 First Edition control catalogue (R1–R11 — the
edition the Third Edition v3.0 working draft's own lineage treats as its
second edition, formally cited there as "AARM/CS 1, First Edition") is
crosswalked in
docs/aarm/conformance-corpus/rules-reconciliation.md
§2, and a Third Edition v3.0 working draft (2026-07-10 — WG review, NOT
adopted) is circulating. Every R-number, tier, and Satisfied/Partial/Gap
verdict in THIS document is frozen to v1.0 semantics and does not
transfer to any later edition without that crosswalk. See § "Edition pin &
Third Edition (working draft) outlook" below.
Architecture pattern: Protocol Gateway (per AARM §reference architectures).
Production state (as attested):
proxy.containment.aiin production with R1 / R3 / R4 / R5 (Core) + R8 (Extended) satisfied at the proxy boundaryCONTAINMENT_AUDIT_SIGNING_KEYprovisioned (HMAC-SHA256 R5 receipts live)- OTel instrumentation wired (export gated on operator setting
OTEL_EXPORTER_OTLP_ENDPOINT) - 5/5 enforcement verbs supported (block / step_up / defer / redact / detect) with distinct audit-trail outcomes
- Per-agent SPIFFE/Ed25519 identity binding live in the agent-runtime layer, with per-artifact content binding implemented 2026-08-28 and demonstrated end to end against a production artifact 2026-08-31 (R6 Satisfied —
www.containment.aiPR #616, all 7 verification steps pass against the live JWKS; transcript in the R6 detail) - Caller-declared intent thread bound into the signed audit receipt (R2)
Every requirement below is verified against specific, named source files in
proxy.containment.ai and containment-ai-agents. We make no tier claim we
cannot defend with these citations.
Summary
| Requirement | Tier | Status | Verified against |
|---|---|---|---|
| R1 — Intercept every action before execution | Core (MUST) | ✅ Satisfied | containment/guardrail.py:181 (async_pre_call_hook) |
| R2 — Maintain context across the action thread | Core (MUST) | ✅ Satisfied | containment/guardrail.py:196-216 (metadata flow); containment/intent.py IntentClaim + session persistence (proxy PR #242, merged 2026-06-06); containment/evaluation/base.py:53-68 intent threaded through PolicyEvaluator + 3 detectors (proxy PR #247, merged 2026-06-08); containment/intent.py::intent_claim_to_receipt_dict + containment/evaluation/recording.py bind the claim to details.intent BEFORE signing — covered by the R5 HMAC and the chained R6 ed25519 signature; per-field forge-tests in tests/evaluation/test_r2_intent_receipt.py (proxy PR #256, merged 2026-06-10) |
| R3 — Evaluate against policy considering action AND intent | Core (MUST) | ✅ Satisfied (both axes) | containment/evaluation/policies/__init__.py (31 detectors, action axis); intent axis via R2 Phases 1-3 — IntentClaim threaded through PolicyEvaluator (proxy PR #247) and bound into the signed receipt (proxy PR #256) |
| R4 — Support 5 decisions (ALLOW / DENY / MODIFY / STEP_UP / DEFER) | Core (MUST) | ✅ Satisfied (5/5 verbs) | recording.py _OUTCOME_MAP + _ACTION_MAP carry all 5; guardrail.py:GuardrailDenied distinguishes block / step_up / defer with .action attr; 28 R4 tests. Shipped proxy PR #213 |
| R5 — Tamper-evident receipt per action | Core (MUST) | ✅ Satisfied | containment/audit_signing.py HMAC-SHA256 over canonical JSON; signature at details._audit_signature; CONTAINMENT_AUDIT_SIGNING_KEY provisioned in production 2026-05-29T02:13Z. 25 R5 tests. Shipped proxy PR #214 |
| R6 — Cryptographically bind receipts to agent identity | Core (MUST) | ✅ Satisfied (2026-08-31 — all three flip conditions met; verification transcript in the R6 detail) | Per-agent Ed25519 SPIFFE SVIDs (containment-ai-agents/orchestrator/src/aarm-identity.ts); identity claims attached to every github_open_pr PR body / RFC filing / #audit-log post (aarm-identity-claim-block.ts, audit-events.ts); verifier (aarm-verifier.ts); key-history JWKS at /.well-known/spiffe-jwks.json. RFC 0031 Phases 1-3 shipped 2026-06-08…2026-06-10; per-artifact content binding shipped 2026-08-28 (ADR 1095) — buildIdentityClaim mints a separate per-artifact assertion carrying artifact={type,id,digest} + canon, and the verifier also binds the kid's agent to the sub's (closing a P0 that let any one agent's key assert any other agent's identity). Flipped to Satisfied 2026-08-31 on the three flip conditions this row previously named as outstanding, each evidenced rather than asserted: (a) the ADR 1095 code is deployed — the live JWKS serves 10/10 key-derived thumbprint kids and 0/10 clock-labelled ones (fetched 2026-08-31T11:42:22Z); (b) a real agent-emitted artifact carries the new shape — www.containment.ai PR #616, agent growth-lead, opened 2026-08-29T18:04:55Z (~24.8h post-deploy), signed payload aarm: "artifact-assertion:v1", canon: 1, artifact.id = containment-ai/www.containment.ai@74f163c1…; (c) steps 1–7 run against that artifact and the live JWKS all pass, with the digest reproduced from the documented preimage and three negative controls failing at steps 4, 6 and 7. Full transcript in the R6 detail below |
| R7 — Track semantic drift | Extended (SHOULD) | Not currently satisfied | behavioral_anomaly_detection.py NotImplementedError stub; deferred per proxy#68 product-lead decision (2026-05-07) |
| R8 — Export action telemetry in OTel-compatible format | Extended (SHOULD) | ✅ Satisfied | containment/otel.py get_tracer() + AttrKey.* vocabulary; recording.py wraps record_violation in OTel spans; 19 R8 tests. Shipped proxy PR #215. OTLP endpoint not yet pointed at a collector but instrumentation is wired |
| R9 — Per-action least-privilege credentials | Extended (SHOULD) | Out of scope | LLM-call boundary doesn't govern downstream tool credentials. This seam belongs to AGT + tool-gateway products. M1 AGT adapter is the integration story (containment-ai-agents PR #1512) |
Tier claim: AARM Core R1 / R2 / R3 / R4 / R5 / R6 all Satisfied, plus Extended R8 Satisfied. R6 was PARTIALLY satisfied from 2026-08-26 to 2026-08-31 and flipped to Satisfied 2026-08-31 once its artifact-content binding was demonstrated end to end against a production artifact (www.containment.ai PR #616; 7-of-7 verification steps, transcript in the R6 detail below) — the flip bar was the demonstration, not the code, and explicitly not a green test suite. The Core requirement set is therefore fully satisfied at the levels recorded in this table, with R6 carrying three stated limitations that do not gate the claim (residuals 3 and 6): the assertion binds the commit that was HEAD at signing time, which a later rebase can move off the PR's head; #audit-log posts are not independently verifiable from Slack alone; and PRs opened by the orchestrator's own cron modules bypass the tool layer and are unsigned. The Extended-tier gaps are R7 (semantic-drift) and R9 (per-action least-privilege credentials), documented below as not-yet-satisfied / out-of-scope-by-architecture. R6 shipped per-agent SPIFFE/Ed25519 identity, identity claims on artifacts, and a published JWKS (RFC 0031 Phases 1-3, 2026-06-08…2026-06-10); the content-binding half shipped 2026-08-28 (ADR 1095) and was demonstrated 2026-08-31. R2 reached Satisfied — and R3's intent axis with it — via caller-declared IntentClaim capture, detector threading, and binding the claim into the tamper-evident signed receipt (RFC 0032 Phases 1-3, the receipt binding shipped in proxy PR #256, merged 2026-06-10).
Public-surface framing: On www.containment.ai and other public marketing surfaces, Containment.AI publishes the conservative form — "Aligned with AARM v1.0", explicitly not a certification — and does not publish a bare "AARM Core conformant" badge. This is a deliberate honesty posture: a self-attestation is not a third-party certification, and the public copy says so.
⚠️ Two facts about the public surface. Both need a www change; neither is fixed by this document.
- The public page DOES reproduce per-requirement status — and as of the 2026-08-31 R6 flip it now AGREES with this document, but only by coincidence.
https://www.containment.ai/products/aarm(HTTP 200, linked from the homepage) states verbatim: "Our current self-attestation covers the connected-tier proxy and records R1–R6 as satisfied at the Core level and R8 as satisfied, with R7 still a gap and R9 out of scope." From 2026-08-26 to 2026-08-31 that sentence overstated R6 against the PARTIALLY-SATISFIED status recorded here; the flip closed the gap by moving this document to meet the page rather than the other way round. The underlying defect is untouched: both surfaces are hand-maintained with no mechanism tying them together, so the next status change diverges again in whichever direction it moves. There are four such undated public surfaces, not one —products/aarm.html,trust.html,compare/aarm.htmlandllms.txt— plus a manually copied duplicate of this entire document atwww.containment.ai:content/aarm-core-conformance-attestation.md, which was byte-identical to this file before the flip and is stale the moment this lands. Fixing that coupling is tracked separately from this document.- The link to this document 404s for the public. That page's "Read the self-attestation" button and its "public attestation document" link both point at
https://github.com/containment-ai/containment-ai-agents/blob/main/docs/aarm-core-conformance-attestation.md, andcontainment-ai-agentsis a private repository — anonymous fetch returns 404 (verified against a public-repo control returning 200). So the page invites a reader to check our work and then denies them the document.ORDERING WAS A HARD CONSTRAINT, AND IS NOW DISCHARGED FOR R6. The rule was: correct the R6 claim (this document, and then the www copy) BEFORE making the attestation reachable, because publishing first would convert an unverifiable overclaim into a published, verifiable one — strictly worse. R6 is now Satisfied on demonstrated evidence and the www sentence is no longer an overclaim, so that specific blocker is gone. The constraint itself still binds any FUTURE status change: never make a surface more reachable than the claim on it is defensible. Note also that the stale www copy of this document must be re-synced before reachability, or a reader following the link gets the pre-flip text. And do not make it reachable by flipping the repository to public: that repo's privacy is currently the sole reason the org has no container/source distribution exposure (export-posture review, 2026-08-26). Publish this single document to a public surface instead.
On external submission: This is a self-attestation only. A formal submission to the AARM Conformance Program at aarm.dev/conformance (a self-attest form with a community-review SLA) is a separate, CEO-owned action and has not been made. Publishing this document does not constitute, and is not a substitute for, that submission.
Per-requirement detail
R1 — Intercept every agent-initiated action before execution
AARM language (paraphrased): A conformant system MUST intercept every agent-initiated action before any side-effectful execution.
Status: SATISFIED.
Evidence:
proxy.containment.ai/containment/guardrail.py:181—async_pre_call_hookis registered as a LiteLLMCustomGuardrail. Every LLM-bound request routed through the proxy passes through this hook before the request leaves the proxy boundary toward OpenAI / Anthropic / Bedrock.proxy.containment.ai/containment/guardrail.py:191-194— docstring explicitly notes the three-verb enforcement model:blockraises before the call leaves,redactmutates the payload before it leaves,detectrecords and allows.proxy.containment.ai/containment/guardrail.py:267-279— fail-closed default: if policy fetch errors and no cached fallback is fresh enough, the proxy raises and the request is rejected.CONTAINMENT_FAIL_OPENis an explicit opt-in escape hatch, not a default.
Caveats:
- "Every agent-initiated action" in our deployment means every LLM-bound
API call routed through the proxy. Actions that bypass the proxy
(e.g. an engineer hitting
api.openai.comdirectly with a personal key) are detected via shadow-AI traffic observation rather than at the pre-call hook. We surface this in the comparison page as a feature (shadow-AI detection) rather than as a gap. - In-sandbox side-effect blind spot (known limitation). For triggers
that run on the Managed Agents path with a repository mount
(
MANAGED_REPO_MOUNT_ENABLED, default-OFF; scoped in code tolead-engineer:eng-pr-triage), the agent gets in-sandboxbash/gitinside the Anthropic-hosted sandbox. Custom tool calls the agent surfaces back to the orchestrator still pass throughexecuteTool(the action chokepoint) and, downstream, the merge-risk-rail and write-class gate — those are unchanged. But a direct side effect executed by sandbox-nativebashitself (e.g. an outboundcurl, agit push, a file write acted on in-sandbox) does not transitexecuteToolor the proxy, so R1's pre-execution interception does not cover it. This is a bounded surface (one read-oriented triage trigger, flag default-OFF) and the orchestrator's own control plane still governs every tool the agent routes through it — but we record it honestly as a gap an application-layer interceptor structurally cannot close. Kernel-/syscall-level observation of the sandbox would be required to intercept it (the seam an eBPF-style agent-observability layer targets); tracked as a roadmap watch item, not claimed as covered today.
R2 — Maintain context across the action thread
AARM language (paraphrased): A conformant system MUST maintain context: agent intent, prior actions, task thread.
Status: SATISFIED (RFC 0032 Phases 1-3 shipped; receipt-binding code merged in proxy PR #256, 2026-06-10).
Evidence (what we have):
proxy.containment.ai/containment/guardrail.py:196-216— request metadata (org_id, user_id, user_api_key_dict) is attached to the request and flows through to the evaluators and the audit-event sink.proxy.containment.ai/containment/models.py:31-41—ProxyRequestmodel carriesmetadata: Optional[dict[str, Any]],model,messages,litellm_call_id, andcreated_at.proxy.containment.ai/containment/evaluation/policies/audit_log_integrity.py— enforces that request metadata carries required organizational fields (department, use-case tag, data classification) for compliance frameworks.- RFC 0032 Phase 1 (proxy PR #242, merged 2026-06-06):
proxy.containment.ai/containment/intent.py—IntentClaimdataclassextract_intent_from_request+IntentSessionStorefor session-scoped persistence. Captures caller-declared intent frommetadata.intentand anchors it against the LiteLLM session.
- RFC 0032 Phase 2 (proxy PR #247, merged 2026-06-08):
proxy.containment.ai/containment/evaluation/base.py:53-68—PolicyEvaluator._evaluateaccepts optionalintent: IntentClaimand threads it through 3 intent-aware detectors. Backward-compatible:intent=Nonepreserves pre-R2 behavior across all 31 evaluators. - RFC 0032 Phase 3 (proxy PR #256, merged 2026-06-10):
proxy.containment.ai/containment/intent.py::intent_claim_to_receipt_dict(canonical 7-field receipt shape) +proxy.containment.ai/containment/evaluation/recording.py—record_violationgains optionalintent: IntentClaim | None; the claim is serialized todetails.intentBEFORE R5 HMAC signing, so the R5 HMAC and the chained R6 ed25519 signature both cover the intent bytes. All 4record_violationcall sites threaded (pre-call metadata + content paths inguardrail.py; async request + response paths inevaluation/__init__.py).tests/evaluation/test_r2_intent_receipt.py— 15 cases including the RFC-required forge-test: mutating ANY of the 7 intent fields (or stripping the claim) in a signed receipt flipsverify_event_chain.hmac_validto False; parameterized per field. Injection defense: a match payload carrying its ownintentkey cannot displace the session claim.
Implementation delta vs. the earlier gap-close sketch (recorded for
auditability): the sketch proposed a receipt_schema_version: "2" bump
with an intent_claim_signed field. The shipped design is additive
instead — the claim lands at details.intent inside the existing
canonical JSON, which the R5/R6 canonicalizers already cover (they strip
only the signature keys). This follows RFC 0032 open question #5's
recommendation (additive, verifier accepts receipts with or without
intent), requires no DB migration, and keeps every historical receipt
verifiable unchanged.
Scope note:
- The "intent thread → drift" linkage (R7, Extended) remains a future
seam, independently scoped under
behavioral_anomaly_detection— explicitly out of R2 scope per RFC 0032 §Scope. - Intent capture is single-session and forward-only; cross-session intent linkage and historical-session backfill are out of scope per RFC 0032 §Scope.
R3 — Evaluate against policy considering action AND intent alignment
AARM language (paraphrased): A conformant system MUST evaluate each action against policies considering both the action and intent alignment.
Status: SATISFIED on both axes (intent axis via R2 / RFC 0032 Phases 1-3, merged 2026-06-10).
Evidence:
proxy.containment.ai/containment/evaluation/policies/__init__.py— 31 policy evaluators registered, each implementingPolicyEvaluator._evaluatepercontainment/evaluation/base.py:53-68.proxy.containment.ai/containment/guardrail.py:366-410— content-based policy loop: every enabled policy for the org is evaluated against the request content, andrecord_violationis called per match.proxy.containment.ai/containment/guardrail.py:284-345— metadata-based policy loop:rate_anomalyandaudit_log_integrityevaluators run before content extraction, enforcing structural / behavioral policies.
Detector inventory (31, source-controlled in
proxy.containment.ai/containment/evaluation/policies/): ai_code_security,
audit_log_integrity, behavioral_anomaly_detection (stub),
bias_decisioning, budget_guardrails, code_detection,
competitive_intelligence, consent_licensing, content_length,
customer_data_exfiltration, document_classification, export_control,
financial_data, infrastructure_leakage, internal_code_leakage,
keyword, legal_privilege, mnpi_detection, phi_detection, pii,
profanity, prompt_injection, rate_anomaly, reading_level,
regulatory_data, secrets, shadow_ai, sql_injection, test,
trade_secret_detection, url_filtering, xss. (32 files in
directory; behavioral_anomaly_detection ships as a stub raising
NotImplementedError per proxy.containment.ai/containment/evaluation/policies/behavioral_anomaly_detection.py:22-91.)
Intent axis (closed 2026-06-10 per RFC 0032): caller-declared
IntentClaim is captured at the proxy entry-point (Phase 1, proxy
PR #242), threaded into PolicyEvaluator._evaluate with 3 intent-aware
detectors branching on declared goal/scope/category (Phase 2, proxy
PR #247), and cryptographically bound into the tamper-evident audit
receipt (Phase 3, proxy PR #256, merged 2026-06-10). Missing intent
degrades to action-axis-only
evaluation (intent=None), which the spec language permits — intent
alignment is evaluated when an intent claim exists. Drift-class intent
evaluation (R7) remains Extended-tier future work and does not gate R3.
R4 — Support five decisions: ALLOW / DENY / MODIFY / STEP_UP / DEFER
AARM language (paraphrased): A conformant system MUST be able to emit five decision verbs: ALLOW, DENY, MODIFY, STEP_UP, DEFER.
Status: SATISFIED — 5 of 5 decision verbs implemented (proxy PR #213).
Evidence:
proxy.containment.ai/containment/evaluation/recording.py— the_OUTCOME_MAPand_ACTION_MAPconstants now enumerate all five AARM enforcement verbs (detect/ALLOW,block/DENY,redact/MODIFY,step_up/STEP_UP,defer/DEFER) with distinct audit-trail outcomes.proxy.containment.ai/containment/guardrail.py—GuardrailDeniedexception carries an.actionattribute that distinguishesblock,step_up, anddeferso the proxy can return the appropriate HTTP response shape per verb.proxy.containment.ai/containment/guardrail.py:101-134—_redact_contentimplements MODIFY by mutating the outbound payload before the request leaves the proxy boundary.- 28 R4 tests cover all five verbs end-to-end (proxy PR #213).
Mapping to AARM verbs (5/5):
| AARM verb | Containment.AI verb | Implementation |
|---|---|---|
| ALLOW | detect (with audit) |
recording.py _OUTCOME_MAP |
| DENY | block (raises) |
GuardrailDenied(action="block") |
| MODIFY | redact (substitute) |
_redact_content, guardrail.py:98-134 |
| STEP_UP | step_up (401 + challenge URL) |
GuardrailDenied(action="step_up") |
| DEFER | defer (per-org approver queue) |
GuardrailDenied(action="defer") |
Caveat (dashboard UX):
- STEP_UP and DEFER are wired at the proxy / audit layer; the dashboard reviewer-queue UX for DEFER and step-up challenge UX for STEP_UP are shipped in minimum form. Polish work remains but does not affect conformance — the verbs are emitted and audited per spec.
Note: This was previously the seam called out in the M1 AGT-adapter PR (README "Contract gaps" #2). With R4 now satisfied, the AGT-adapter MODIFY/STEP_UP/DEFER mapping is unblocked.
R5 — Produce a tamper-evident receipt per action
AARM language (paraphrased): A conformant system MUST produce a tamper-evident receipt of every action decision.
Status: SATISFIED — HMAC-SHA256 signed receipts shipped (proxy PR #214).
Evidence:
proxy.containment.ai/containment/audit_signing.py— HMAC-SHA256 over the canonical JSON serialization of each audit row. Signature stored atdetails._audit_signature.CONTAINMENT_AUDIT_SIGNING_KEYprovisioned to production 2026-05-29T02:13Z (proxy.containment.ai Worker version3878fa46-fcd2-4303-b690-d737a58b65d0at 100% traffic).proxy.containment.ai/containment/evaluation/recording.py:86-109— every policy violation writes anAuditEventrow to Supabaseaudit_eventstable. The write is append-only by table design (noupdate_audit_eventAPI exists).proxy.containment.ai/containment/models.py:78-88—AuditEventmodel carriesid,admin_id(acting user),organization_id,action,resource_type,resource_id,summary,details(including signature),event_timestamp.- Each
AuditEventis also accompanied by a pairedAlertrow perrecording.py:67-82, so two independent records exist per violation. - 25 R5 tests cover signature creation + verification + key-rotation paths (proxy PR #214).
Defense-in-depth roadmap (planned strengthening, not required for R5):
R5 is satisfied today by HMAC-SHA256 over the canonical receipt. A planned hash-chained / Merkle Decision BOM enhancement strengthens the tamper- evidence story further and provides shared audit evidence across our SOC 2 and ISO 27001 efforts. It is a roadmap enhancement on top of an already- satisfied R5, not a gap.
In scope of the planned enhancement:
- Hash-chain audit rows per org per day, with daily root published.
- Public proof endpoint (Merkle path verification).
- Emit our verdict as a child node under AGT's Merkle-verified Decision BOM when running in AGT-adapter mode, so a single audit log captures both layers (M1 / contract-gap #3, M1 AGT adapter PR #1512).
- Same artifact referenced by SOC 2 + ISO 27001 audit evidence.
R6 — Cryptographically bind receipts to agent identity
AARM language (paraphrased): Each receipt MUST be cryptographically bound to the agent identity that initiated the action.
Status: SATISFIED as of 2026-08-31. Artifact-content binding was implemented in code 2026-08-28 (ADR 1095) and has now been demonstrated end to end against a production artifact: the verification procedure below was run against www.containment.ai PR #616 (agent growth-lead) and the live JWKS, and all 7 steps passed. This document recorded R6 as PARTIALLY SATISFIED from 2026-08-26 until that demonstration; the flip conditions it named are each evidenced in the Residual scope note, which retains the transcript. A green test suite was explicitly never one of the flip conditions and was not used as one.
Architecture note (which layer satisfies R6): R6 is satisfied at the
agent-runtime layer (containment-ai-agents orchestrator), not the
proxy receipt layer. The proxy (R5) produces tamper-evident receipts; the
orchestrator now binds every agent-emitted artifact to a cryptographic
agent identity. This is the AARM-correct seam: the agent identity lives
where the agent runs. The proxy's org+user binding (below) remains and is
complementary — R5 proves "this org's key signed this receipt", R6 proves
"this specific agent identity signed this artifact".
Artifact-content binding (ADR 1095, 2026-08-28). The signed assertion is
no longer the agent's SVID. buildIdentityClaim() mints a separate
per-artifact assertion whose signed payload carries
artifact = {type, id, digest} plus a canonicalizer version canon, where
digest is SHA-256 over a length-prefixed canonical descriptor of
(canon, type, id). Two artifacts from the same agent therefore carry
different assertions, and a claim minted for artifact A fails verification
against artifact B.
The binding could not be placed inside the SVID payload: mintSvid caches for
~55 minutes by design, so every artifact in that window would have shared one
assertion — which was the defect. The two token classes now have deliberately
opposite lifetimes and are non-interchangeable in the payload:
| SVID | artifact assertion | |
|---|---|---|
| role | bearer credential | notarization |
| lifetime | 1h exp, ~55-min cache |
iat only, no exp |
| marker | aarm: "svid:v1" |
aarm: "artifact-assertion:v1" |
An expiring assertion makes honest artifacts fail verification purely by
aging, which is what made the pre-ADR-1095 evidence pointer unsatisfiable by
any artifact in existence. The verifier REQUIRES the artifact-assertion
marker, so a bare SVID presented as a claim — the pre-ADR-1095 shape — is
rejected with missing_artifact_binding rather than quietly passing.
What R6 now proves, and what it still does not. The shipped mechanism has
been exercised against a production artifact end to end (transcript in the
Residual scope note), so the claim is no longer test-suite-only. What it still
does not prove: that any artifact signed before the 2026-08-28 deploy is
bound (they are not — binding is forward-only, see the ⚠️ note below); that a
bound commit is still the PR's head at read time (a rebase after signing moves
it — residual 6); or that an #audit-log line is verifiable from Slack alone
(residual 3).
Evidence (agent-identity binding — RFC 0031, shipped):
- Identity issuance (Phase 1, #1885):
containment-ai-agents/orchestrator/src/aarm-identity.ts— each of the 10 agents has a stable SPIFFE IDspiffe://containment-ai/agents/{slug}and an Ed25519 keypair.mintSvid()issues a signed SVID JWT (EdDSA, 1h TTL). Private seeds live in the singleSPIFFE_AGENT_KEYS_JSONsecret (provisioned 2026-06-08), in-process only — never persisted at rest. - Public verification surface — a KEY-HISTORY endpoint:
/.well-known/spiffe-jwks.json(aarm-identity.ts::handleSpiffeJwks, routed infetch-handler.ts) publishes, per agent, the current Ed25519 public JWK plus every retired key declared in the key bundle'shistory, each carrying an explicitnbf/expvalidity window (seconds) naming the span of assertioniatvalues it may have signed. A retired key verifies history only; outside its window the verifier returnskey_window_mismatch. Only PUBLIC halves of retired keys are retained. This is required by the decision to give assertions no expiry: a notarization must stay verifiable, so the key that produced it must stay obtainable (ADR 1095 §3). kididentifies the KEY, not the day (ADR 1095 §4):kid = {slug}-{22-char-base64url-SHA-256-thumbprint}. It was{slug}-{YYYY-MM-DD}computed fromnew Date()at isolate import time — a clock label, so the JWKS advertised a new key id every UTC midnight for unchanged key material and every artifact older than the current day failed the kid-match step below withunknown_kid. Measured 2026-08-28 againstcontainment-ai-agentsPR #4240:kid=ai-strategy-2026-08-26against a JWKS publishing onlyai-strategy-2026-08-28, while the signature verified fine against that published key — the key had never rotated. The thumbprint changes if and only if the key changes.- Artifact signing (Phase 2):
buildIdentityClaim()is wired into the live artifact-creation path —- PR bodies:
orchestrator/src/tools/github-prs-handlers.tsappends a signed claim block viaaarm-identity-claim-block.ts— on BOTH PR-creation paths,github_open_prand (since ADR 1096)github_open_pr_from_branch. - RFC filings:
orchestrator/src/tools/agent-self-handlers.ts(propose_behavior_change) appends the same block. #audit-logposts:orchestrator/src/audit-events.ts::postAuditEventattaches a signedsigned_by: spiffe://…field.- Gated on
SPIFFE_AGENT_KEYS_JSON(hard precondition) AND the default-ONAARM_SIGNING_ENABLEDkill switch (graceful-degrade to unsigned).
- PR bodies:
- Bound identifiers are IMMUTABLE where one exists (ADR 1095 §6): a
github_open_prclaim binds<repo>@<commit_sha>(was<repo>@<branch>— a branch ref moves, so the assertion would keep reading as valid for whatever the branch pointed at next); an#audit-logclaim bindssha256:<hex>of the post text, re-derivable from the posted line viaaudit-events.ts::unsignedAuditEventText(wastriggerId, which every event of one type in a run shared). Hashing a PR body is explicitly rejected: a body is editable and GitHub never exposes prior body text, so an as-of hash over one is unverifiable afterwards and re-signing it would be forgery. - Verifier:
orchestrator/src/aarm-verifier.ts::verifyIdentityClaimre-derives the trusted JWKS and checks:subparses asspiffe://containment-ai/agents/<slug>inside our trust domain;sub== the claimedspiffeId; thekid's agent slug == thesub's agent slug; the artifact binding re-derives from the signed(type, id)and agrees with the claim's visible copy (and with the caller'sexpectedartifact when one is supplied);expwhen present; and the Ed25519 signature against a published key for that agent whose window covers the signediat. Returns stable reason codes (signature_invalid,spiffe_id_mismatch,kid_subject_mismatch,missing_artifact_binding,artifact_binding_mismatch,key_window_mismatch,unsupported_canon,malformed_spiffe_id,expired, …). - P0 closed (ADR 1095 §5): the verifier previously compared
payload.subagainstclaim.spiffeId— both attacker-supplied, from the same claim object — then resolved the key bykidwithout ever checking that the kid's agent matched the sub's. A holder of ANY ONE agent's private key could sign a payload asserting ANY OTHER agent's identity and it verified: sub agreed with spiffeId, the kid named a real published key, and the signature was genuinely valid under it. One compromised agent key was fleet-wide impersonation.verifyIdentityClaimnow binds the key to the subject. - Tests:
aarm-identity.test.ts(issuance, JWKS, kid stability across isolate resets, kid change on rotation, retired-key windows),aarm-verifier.test.ts(tamper / cross-agent forge / expiry / unknown-kid / retired-key in-and-out-of-window / legacy-kid resolution),aarm-artifact-binding.test.ts(byte-exact canonical descriptor, length-prefix collision resistance, trust-domain parsing, windows),aarm-artifact-binding-characterisation.test.ts(the binding defect and the P0, written to fail on the pre-ADR-1095 code — 6 of 7 RED onorigin/main@0a7f10aa, 7 of 7 green after), andaarm-identity-claim-block.test.ts(artifact round-trip: build → embed → extract → verify; tampered block fails; kill-switch off → no block). All use real in-process Ed25519 keypairs — no crypto mocks.
Verification procedure (and what it does NOT yet apply to). A PR opened
by an agent via github_open_pr, or an RFC filed via
propose_behavior_change, carries an <!-- aarm-identity-claim:v1 --> block
in its body. To verify one:
- Extract the JSON block between the
aarm-identity-claim:v1markers. - Split
signedAssertionintoheader.payload.signature. - Confirm
payload.aarm == "artifact-assertion:v1"andpayload.canon == 1. - Recompute the artifact digest from the signed
payload.artifact: SHA-256 of"aarm-artifact-descriptor:v1\ncanon=1\ntype=<utf8ByteLen>:<type>\nid=<utf8ByteLen>:<id>\n", and confirm it equalspayload.artifact.digest. Confirm the artifact id names the artifact you are holding (for a PR,<repo>@<commit_sha>). - Confirm
payload.sub == spiffeIdand that both parse asspiffe://containment-ai/agents/<slug>. - Confirm the
kid's agent slug equals that<slug>. - Fetch
/.well-known/spiffe-jwks.json, select a JWK for that agent whosenbf/expwindow (when present) coverspayload.iat, and verify the EdDSA signature overheader.payload.
orchestrator/src/aarm-verifier.ts::verifyIdentityClaim is the reference
implementation, and orchestrator/src/aarm-artifact-binding.ts is the
canonicalizer in ~270 dependency-free lines so the digest can be
re-implemented independently.
⚠️ This procedure does NOT succeed on any artifact signed before
2026-08-28. Pre-ADR-1095 claims carry the agent's SVID as the assertion:
they have no artifact field (step 3/4 fail), and they expired one hour after
signing. This is stated rather than implied because the previous version of
this paragraph invited an assessor to run a check that could not pass on
anything: measured 2026-08-28 against PR #4240, the kid-match step returned
unknown_kid and the assertion had expired two days earlier. Binding is
forward-only from the ADR 1095 deploy; there is no retro-signing.
Complementary proxy-layer binding (retained from prior attestation):
proxy.containment.ai/containment/models.py:78-88—AuditEvent.admin_idbinds the audit row to the human user;organization_idbinds to the org.proxy.containment.ai/containment/auth.py— API-key authentication ties every request to a verified org + user.
Residual scope note: six residuals, two of which are now closed. None of the remaining four gates the Satisfied claim — residual 1, which did gate it, was discharged 2026-08-31 and retains its verification transcript below. Residuals 3 and 6 are stated limitations of what the binding covers; 4 and 5 are enhancements on top of what shipped.
Not yet demonstrated end to endDISCHARGED 2026-08-31 — this was the gating residual. ADR 1095 shipped the binding in code on 2026-08-28 with the test evidence above; the bar for Satisfied was running the verification procedure against a real production artifact, which has now been done. A green test suite was never one of the flip conditions and was not used as one. All three conditions, with the evidence for each:(a) the ADR 1095 code is deployed to the production Worker — ✅ SATISFIED 2026-08-28, re-verified 2026-08-31. PR #4292 merged to
mainat 17:19Z and the orchestrator auto-deploys on push. Verified by BEHAVIOUR rather than by the merge, since merge is not deploy: a live fetch of/.well-known/spiffe-jwks.jsonat 2026-08-31T11:42:22Z (HTTP 200, 1517 bytes) returns 10 keys, of which 10 of 10 carry key-derived thumbprintkids (growth-lead-Gmu43c9QlT26OQGj-AiapE,ai-strategy-cIHFyXL2JVuGXKg29pDyGv, …) and 0 of 10 carry the clock-labelled<slug>-YYYY-MM-DDform. Only the ADR 1095 code produces that shape.(b) a real agent-emitted artifact carries a claim block whose signed payload has
aarm: "artifact-assertion:v1"— ✅ SATISFIED 2026-08-29.www.containment.aiPR #616, opened by agentgrowth-leadat 2026-08-29T18:04:55Z — ~24.8h after the 17:19Z deploy boundary, so it falls unambiguously on the post-deploy side. Its body carries an<!-- aarm-identity-claim:v1 -->block whose decoded payload has keys[aarm, artifact, aud, canon, iat, iss, jti, sub]withaarm = "artifact-assertion:v1",canon = 1,iat = 1788026694(2026-08-29T18:04:54Z),sub = spiffe://containment-ai/agents/growth-lead,kid = growth-lead-Gmu43c9QlT26OQGj-AiapE, andartifact = {type: "pull_request", id: "containment-ai/www.containment.ai@74f163c17e35f26ac5670291f0bea0aebf9e36f2", digest: "25173c2835f1e1c72fe4e1da078550083c0aa6b7cbdf9e06596e7387ce420a19"}. Note for anyone re-checking this: searching the PR body for the stringartifact-assertion:v1returns zero matches — the marker lives only inside the base64url JWT payload. The field must be DECODED before any conclusion is drawn about its presence; a grep over an encoded field yields a confident false negative.(c) steps 1–7 run against that artifact and the live JWKS and succeed, with the transcript recorded here — ✅ SATISFIED 2026-08-31. Run with an independent re-implementation of the procedure (node builtins only, zero imports from
orchestrator/src), against the live JWKS fetched at 11:42:22Z:step check result 1 claim block extracted from the PR body PASS — keys [spiffeId, signedAssertion, claim_ts, canon, artifact]2 signedAssertionsplits intoheader.payload.signaturePASS — 3 parts 3 payload.aarm == "artifact-assertion:v1"andpayload.canon == 1PASS 4 digest recomputed from the documented preimage equals payload.artifact.digest, and the artifact id names the artifact heldPASS — recomputed 25173c2835f1e1c7…== signed25173c2835f1e1c7…5 payload.sub == spiffeId, both parse asspiffe://containment-ai/agents/<slug>PASS — both …/growth-lead6 the kid's agent slug equals thesub's slugPASS — growth-lead==growth-lead7 EdDSA signature over header.payloadverifies against a published JWK for that agent whose window coversiatPASS — kid=growth-lead-Gmu43c9QlT26OQGj-AiapE,crv=Ed25519Verdict: VERIFIED — 7 of 7 steps pass. The digest was reproduced from the preimage documented in step 4 above (
aarm-artifact-descriptor:v1\ncanon=1\ntype=…\nid=…\n), not read back from the claim, so step 4 is a genuine recomputation.The procedure discriminates — three negative controls, each failing at a DIFFERENT step, which is what distinguishes a real check from one that passes everything:
control expected actual claim replayed onto a different commit id fail at 4 FAIL at step 4 ( namesHeldArtifact=false)kidre-labelled to another agent (revenue-lead-…)fail at 6 FAIL at step 6 ( kidSlug=revenue-leadvssubSlug=growth-lead)one signature byte flipped fail at 7 FAIL at step 7 (EdDSA false)One implementation note for a third-party verifier, because it is an easy way to get step 6 wrong:
kidis{slug}-{22-char base64url thumbprint}and the thumbprint itself contains-, so splitting on the last-mis-parses the slug. Strip exactly the trailing 23 characters (or match^(.+)-[A-Za-z0-9_-]{22}$).
CLOSED 2026-08-28 (ADR 1096). That tool is a separate PR-creation path and had never called the claim builder, so "any PR opened by an agent carries a claim block" was false for PRs opened from an existing branch — independently of ADR 1095, which changed WHAT a claim binds and not WHICH paths sign. It now appends the same claim block asgithub_open_pr_from_branchPRs are UNSIGNED.github_open_pr, bound to the branch's HEAD COMMIT SHA (read back viagit/ref/heads/{branch}, since this handler does not create the commit). If the ref cannot be resolved the PR ships UNSIGNED rather than falling back to{repo}@{branch}: a branch ref moves, so a branch-bound assertion would assert an immutability it does not have (ADR 1096 §3). Signing coverage is now 5 of 5 PR-creation-adjacent paths; the degenerate non-string-bodycase produces no claim on BOTH paths, which is mirrored deliberately rather than silently diverged.#audit-logposts are not independently verifiable from Slack alone.formatAuditEventrenders[signed_by: <spiffeId> binding=<16 hex>]— the spiffeId and a short prefix of the artifact digest, not the assertion JWT (which would balloon every line). The binding is content-addressed and re-derivable viaunsignedAuditEventText, but an external verifier needs the assertion from the event payload, which Slack does not carry.Forward-only: binding is implemented at the agent-team runtime from ship date; historical artifacts are not retro-signed (RFC 0031 §Scope).
AGT convergence: alignment with AGT's SPIFFE identity model (M1 adapter, PR #1512) remains the medium-term integration story so a single combined audit log can carry both layers' identities.
A bound commit can stop being the PR's head, and cron-opened PRs are unsigned — both found while running the 2026-08-31 demonstration, both stated because an assessor will hit them.
- The assertion binds the commit that was HEAD at SIGNING time. On the
PR #616 artifact used above, the bound id is
…@74f163c17e35f26ac5670291f0bea0aebf9e36f2, which is not among the commits the PR lists today (25b1785a…,29ee6510…): the branch was rebased after signing, so an identical tree acquired a new sha. The bound sha is a real, still-retrievable commit in the target repo (parenteec52fa3…, authored 2026-08-29T18:04:53Z, two seconds before the PR was opened), and step 4 verifies against it correctly. This is the intended behaviour of an immutable binding, not a defect — but it means step 4's "the artifact you are holding" is the commit NAMED IN THE ASSERTION, not whatever the PR's head is when you read it. A verifier that fetches the PR head and compares will get a spurious mismatch on any rebased branch. - PRs opened by the orchestrator's own cron modules carry no claim block.
Signing is wired into the tool layer (
github_open_pr,github_open_pr_from_branch,propose_behavior_change); cron modules that call the GitHub API directly bypass it. Measured 2026-08-31:orchestrator/src/sprint-gate-readout-cron.tsposts to/pullswith zero references to the claim builder, and agents PRs #4336 / #4337 (its output, plus the proof-attestation refresh) carry no claim block. #4339 is opened by.github/workflows/adr-index-regen.yml, i.e. by CI rather than by an agent at all. So "every PR opened by an agent carries a claim block" is true of the TOOL paths and not of the cron paths; the Satisfied claim rests on the tool paths, which is where agent-authored artifacts are produced.
- The assertion binds the commit that was HEAD at SIGNING time. On the
PR #616 artifact used above, the bound id is
Operator obligation created by ADR 1095. Because assertions do not expire,
a key rotation MUST move the outgoing public key into the bundle's history
with an nbf/exp window, or every artifact that key signed becomes
unverifiable. Procedure: docs/runbooks/spiffe-provisioning.md § Rotation.
Correction history:
- 2026-06-10 → 2026-08-26: this section recorded R6 as ✅ Satisfied, and
the architecture note claimed R6 proves "this specific agent identity signed
this artifact". The code never supported that sentence, and the gap was
absent from the residual list for the whole period — an undisclosed gap
rather than a stated limitation. Corrected 2026-08-26 (ADR 1076) against
orchestrator/src/aarm-identity.ts:215-238. - 2026-08-28 (ADR 1095): the capability was implemented rather than the
claim weakened further. The mechanism description above is rewritten, the
verification procedure is replaced (the previous one could not pass on any
artifact in existence — measured, not inferred), the
kidclock-label bug and a verifier P0 allowing cross-agent impersonation are fixed, and the residual list gains the three items that now gate the Satisfied claim with explicit flip conditions. The status was NOT flipped: the bar is running the procedure end to end on a real artifact, and this change shipped without a production deploy. - 2026-08-31: status flipped PARTIALLY SATISFIED → SATISFIED on the
three flip conditions ADR 1095 had set, each verified rather than asserted:
the deploy re-confirmed by behaviour against the live JWKS (10/10 thumbprint
kids, 0/10 clock-labelled, fetched 11:42:22Z); a real post-deploy agent-emitted artifact located (www.containment.aiPR #616,growth-lead, ~24.8h post-deploy); and the 7-step procedure run against it with an independent re-implementation, 7 of 7 passing, three negative controls failing at steps 4, 6 and 7. Transcript retained in residual 1 above rather than summarised, so the claim can be re-checked without re-deriving it. One new residual was recorded in the same pass (residual 6), with two parts — a rebase can move a bound commit off the PR head, and cron-opened PRs are unsigned — neither of which gates the Satisfied claim. The status was NOT flipped on the test suite; the suite was already green on 2026-08-28 and that was explicitly excluded as evidence.
R7 — Track semantic drift between proposed actions and original intent (Extended)
AARM language (paraphrased): A conformant Extended system SHOULD track semantic drift between proposed agent actions and original task intent.
Status: NOT CURRENTLY SATISFIED — gap analysis below.
Evidence:
proxy.containment.ai/containment/evaluation/policies/behavioral_anomaly_detection.py:22-91— the file exists, but every method raisesNotImplementedErrorwith the_DEFERRAL_MSG. The intent is clearly scoped but no detector code has shipped.proxy.containment.ai/plans/21-behavioral-anomaly-detection.md— detailed plan for the four drift-class detectors (intra-session semantic drift, tool-call anomaly, policy-compliance drift, exfiltration-pattern drift) with mappings to AARM's intent-drift threat class.
Gap-close plan:
- Land the four detectors per
plans/21-behavioral-anomaly-detection.md. - The plan already cross-references AARM-style threat classes; this is the seam where R2 (intent thread) and R7 (drift) close together.
R8 — Export action telemetry in standard format (Extended)
AARM language (paraphrased): A conformant Extended system SHOULD export action telemetry in a standardized format (the spec references OpenTelemetry).
Status: SATISFIED — OTel instrumentation shipped (proxy PR #215).
Evidence:
proxy.containment.ai/containment/otel.py— OpenTelemetry SDK initialization exposingget_tracer()and theAttrKey.*semantic vocabulary (org_id,user_id,policy_id,enforcement_action,matched_policy_type,severity).proxy.containment.ai/containment/evaluation/recording.py— wraps everyrecord_violationcall in an OTel span carrying the AARM-aligned attribute set.- 19 R8 tests cover span creation, attribute-set completeness, and exporter configuration (proxy PR #215).
- Export is gated on operator setting
OTEL_EXPORTER_OTLP_ENDPOINT— the spec language requires "support for" OTel-compatible export, not that the operator has pointed it at a collector. Instrumentation is wired and provably correct.
Caveat (deployment, not conformance):
- The default production deployment does not yet point at an OTel
collector. Operators who set
OTEL_EXPORTER_OTLP_ENDPOINTreceive spans immediately. This is a deployment posture, not a conformance gap. - Precision caveat (deployed prod image): the proxy production image
(
proxy.containment.aiDockerfile.cloudflare) installs onlyrequirements.txt+requirements.gateway.txt, NOTrequirements.otel.txt. Becauseopentelemetry-api/-sdkare therefore absent at runtime, the wired instrumentation inotel.py/recording.pyresolves to its documented no-op shims (_NoOpTracer/_NoOpSpan), so the capability is dormant in the deployed prod image today — it emits no spans even ifOTEL_EXPORTER_OTLP_ENDPOINTis set, until those OTel deps are installed in the image. R8 remains satisfied at the code/test level (the instrumentation is present and provably correct under test); this notes only that the prod image must addrequirements.otel.txtto actually export spans.
R9 — Per-action least-privilege credentials (Extended)
AARM language (paraphrased): A conformant Extended system SHOULD enforce per-action least-privilege credentials, not session-wide credentials.
Status: NOT CURRENTLY SATISFIED — out of scope for the LLM-call-boundary layer by architecture; addressed by layering a tool-gateway underneath.
Evidence:
- We govern the LLM-call boundary (HTTPS to provider). We do not own or issue the credentials the agent uses for downstream tools (DB connection strings, MCP tokens, OAuth tokens for SaaS APIs).
proxy.containment.ai/gateway/auth.py:147-159— gateway auth handles the incoming request's credential to our proxy, not the agent's credential to its downstream tools.
Why this is out of scope by architecture:
- R9 is the requirement that tool-gateway / action-execution products (e.g. AGT, Runlayer, MintMCP) are built to satisfy. It is the action-execution layer's job, not the LLM-call-content layer's job. Our position in the reference-architecture spectrum (Protocol Gateway at the LLM-call boundary) is structurally not where R9 lives.
- R9 is satisfied by layering a tool-gateway product underneath Containment.AI. The combined deployment addresses R9; Containment.AI alone covers the Core scope R1–R6 at the satisfaction levels recorded in the Tier claim above — not uniformly “satisfied” — plus Extended R8. (Corrected 2026-08-28: this line read “alone satisfies the Core requirements (R1–R6)”, which contradicted the Tier claim's “R6 PARTIALLY satisfied … the Core requirement set is not fully satisfied” ~600 lines earlier. The fix REMOVES the restatement rather than correcting it: a satisfaction status asserted in two places will drift again, and this document is the one artifact whose whole purpose is that it does not.)
Roadmap:
- We do not pursue R9 satisfaction inside the proxy itself.
- Our reference architecture for AARM Extended is the two-layer "Containment.AI + tool-gateway" deployment, where the combined system addresses R9.
Edition pin & Third Edition (working draft) outlook
This attestation is pinned to AARM v1.0 (the original R1–R9 numbering,
February 2026) and is deliberately NOT re-issued against later editions.
Per-control status under the AARM/CS-1 First Edition catalogue (R1–R11 —
the second edition in the Third Edition draft's own lineage, formally
cited there as "AARM/CS 1, First Edition") lives in
docs/aarm-conformance-evidence-ledger.md,
with the numbering crosswalk in
docs/aarm/conformance-corpus/rules-reconciliation.md
§2. Read side by side: this document's "R7 semantic drift (Extended) / R8
telemetry (Extended) / R9 least privilege (Extended)" become, under CS-1,
"R9 behavioral analytics (Extended) / R8 telemetry (Core) / R7 least
privilege (Core)".
Third Edition outlook — informational only, NOT a conformance claim. A consolidated AARM v3.0 working draft ("Third Edition: Working Draft", 2026-07-10) is circulating for working-group review. It has not been adopted by the AARM working group, the CSA, or any standards body, and nothing in this attestation claims against it. If adopted as drafted, the material changes for the surfaces attested here would be:
- R10 (agentic action risk classification) and R11 (source provenance
verification) — proposed promotion to Core MUST. The draft resolves the
CS-1 Table 3 / Table 4 tier contradiction in the MUST direction; the
draft itself flags this resolution as "proposed, not settled" and subject
to explicit WG vote. Honest orchestrator current-state, verified in code
at
containment-ai-agentsorigin/main:- R10-adjacent: the orchestrator runs live, production action risk
classification —
inferToolRiskTier(orchestrator/src/tools/helpers.ts:290; per-tool tiers 1–4, tier ≥ 3 routed to a human approval gate) andresolveCrossRepoRiskTier(orchestrator/src/cross-repo-allowlist.ts; per-repo/per-changeset tiers). These are static per-tool / per-repo classifications, not the severity × source-trust composite per triggering input that the draft describes — under v3-as-drafted this reads as Partial, not Satisfied. - R11-adjacent: the orchestrator machine-stamps provenance headers on
the output-evaluator's pre-fetched verify-data blocks — the #audit-log
and division-channel context assembled by
fetchVerifyDatainorchestrator/src/data-fetcher.ts, the only call sites of the stamping helpers inorchestrator/src/data-provenance.ts(default-ONPROVENANCE_MARKERS_ENABLEDkill switch); other agents' pre-fetched data blocks are not stamped. This tags the provenance of data fed to the evaluator; it does NOT classify the trust of the instruction that triggered an action and implements no least-trusted-wins composition — under v3-as-drafted this reads as a Gap at this surface.
- R10-adjacent: the orchestrator runs live, production action risk
classification —
- New R12–R15 ("Edge & Lifecycle Profile"): signed policy bundles with staged activation / dry-run / rollback / version pinning (R12); disconnected and air-gapped enforcement — last-known-good bundle, bounded autonomy, local tamper-evident ledger, data-diode evidence export (R13); cryptographic agility incl. hybrid classical/post-quantum signing for long-retention evidence (R14); and governance-at-respawn — identity attestation + bundle verification at every instantiation (R15). None of these existed in v1.0 and none is attested here.
If and when a Third Edition is adopted, a new attestation against that edition's numbering is required. This document must never be silently re-read as a claim against v3.0 (working draft or adopted).
External submission status
This is a self-attestation, not a third-party certification. It reflects Containment.AI's own assessment of its production systems against AARM v1.0, verified against the named source files above.
A formal submission to the AARM Conformance Program at
aarm.dev/conformance — a self-attest form with a community-review SLA —
is a separate action and has not been made as of the publication date.
Publishing this document is not, and does not substitute for, that
submission. When and whether to submit is a decision Containment.AI makes
separately.
How to verify the claims in this document
The evidence cited here is checkable end-to-end without trusting our word:
- R1 / R3 / R4 / R5 / R8 — open
proxy.containment.aiat the cited file:line and read the implementation; each Core/Extended requirement is backed by a dedicated test suite (counts noted per requirement). - R6 (agent-identity + artifact binding) — a PR opened via
github_open_pr, or an RFC filed viapropose_behavior_change, carries an<!-- aarm-identity-claim:v1 -->block in its body. Run the 7-step procedure in the R6 section below: extract the block, recompute the artifact digest from the signedpayload.artifact, and verify the EdDSA signature against a windowed public key at/.well-known/spiffe-jwks.json.orchestrator/src/aarm-verifier.ts::verifyIdentityClaimis the reference verifier andorchestrator/src/aarm-artifact-binding.tsthe canonicalizer. Two limits stated up front: artifacts signed before 2026-08-28 carry no artifact binding and expired an hour after signing, so the procedure does not pass on them; and PRs opened viagithub_open_pr_from_branchcarry no claim block at all. - R2 (intent-thread receipt binding) — the intent claim is serialized
to
details.intenton each signed audit receipt before signing; mutate any intent field on a recorded receipt andverify_event_chainreportshmac_valid: false. The forge-tests intests/evaluation/test_r2_intent_receipt.pyexercise this per field.
Related material
- AGT adapter skeleton —
integrations/agt-adapter/(cross-layer integration story for Extended R9) - AARM comparison page —
www.containment.ai/compare/aarm.html(public-surface summary that links here) - RFC 0031 — R6 SPIFFE/DID identity implementation (
rfcs/0031-aarm-r6-spiffe-did-implementation.md) - RFC 0032 — R2 intent-thread modeling implementation (
rfcs/0032-aarm-r2-intent-thread-modeling.md)