AARM v1.0 — THE SELF-ATTESTATION, IN FULL

AARM Core conformance self-attestation

The per-requirement record our public pages defer to, published here in full — including the requirements we do not satisfy. Self-attested, not third-party certified: it has not been reviewed or endorsed by the Cloud Security Alliance, the CSAI Foundation, or any external conformance body.

PUBLISHING NOTE

The document below is reproduced verbatim from our internal record at containment-ai-agents/docs/aarm-core-conformance-attestation.md, rendered from that markdown at build time. Its per-requirement statuses, tier claims and wording are unedited — the point of publishing it is that it is the same document, gaps included.

Two further internal documents cited below — the AARM conformance evidence ledger and the rules-reconciliation crosswalk — are not published. Their names are kept as plain text rather than as links: a citation that does not resolve is precisely the defect this page exists to close, so we do not link them and we claim no route to them.

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.ai in production with R1 / R3 / R4 / R5 (Core) + R8 (Extended) satisfied at the proxy boundary
  • CONTAINMENT_AUDIT_SIGNING_KEY provisioned (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.ai PR #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.

  1. 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.html and llms.txt — plus a manually copied duplicate of this entire document at www.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.
  2. 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, and containment-ai-agents is 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:181async_pre_call_hook is registered as a LiteLLM CustomGuardrail. 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: block raises before the call leaves, redact mutates the payload before it leaves, detect records 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_OPEN is 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.com directly 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 to lead-engineer:eng-pr-triage), the agent gets in-sandbox bash / git inside the Anthropic-hosted sandbox. Custom tool calls the agent surfaces back to the orchestrator still pass through executeTool (the action chokepoint) and, downstream, the merge-risk-rail and write-class gate — those are unchanged. But a direct side effect executed by sandbox-native bash itself (e.g. an outbound curl, a git push, a file write acted on in-sandbox) does not transit executeTool or 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-41ProxyRequest model carries metadata: Optional[dict[str, Any]], model, messages, litellm_call_id, and created_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.pyIntentClaim dataclass
    • extract_intent_from_request + IntentSessionStore for session-scoped persistence. Captures caller-declared intent from metadata.intent and 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-68PolicyEvaluator._evaluate accepts optional intent: IntentClaim and threads it through 3 intent-aware detectors. Backward-compatible: intent=None preserves 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.pyrecord_violation gains optional intent: IntentClaim | None; the claim is serialized to details.intent BEFORE R5 HMAC signing, so the R5 HMAC and the chained R6 ed25519 signature both cover the intent bytes. All 4 record_violation call sites threaded (pre-call metadata + content paths in guardrail.py; async request + response paths in evaluation/__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 flips verify_event_chain.hmac_valid to False; parameterized per field. Injection defense: a match payload carrying its own intent key 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 implementing PolicyEvaluator._evaluate per containment/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, and record_violation is called per match.
  • proxy.containment.ai/containment/guardrail.py:284-345 — metadata-based policy loop: rate_anomaly and audit_log_integrity evaluators 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_MAP and _ACTION_MAP constants 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.pyGuardrailDenied exception carries an .action attribute that distinguishes block, step_up, and defer so the proxy can return the appropriate HTTP response shape per verb.
  • proxy.containment.ai/containment/guardrail.py:101-134_redact_content implements 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 at details._audit_signature.
  • CONTAINMENT_AUDIT_SIGNING_KEY provisioned to production 2026-05-29T02:13Z (proxy.containment.ai Worker version 3878fa46-fcd2-4303-b690-d737a58b65d0 at 100% traffic).
  • proxy.containment.ai/containment/evaluation/recording.py:86-109 — every policy violation writes an AuditEvent row to Supabase audit_events table. The write is append-only by table design (no update_audit_event API exists).
  • proxy.containment.ai/containment/models.py:78-88AuditEvent model carries id, admin_id (acting user), organization_id, action, resource_type, resource_id, summary, details (including signature), event_timestamp.
  • Each AuditEvent is also accompanied by a paired Alert row per recording.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 ID spiffe://containment-ai/agents/{slug} and an Ed25519 keypair. mintSvid() issues a signed SVID JWT (EdDSA, 1h TTL). Private seeds live in the single SPIFFE_AGENT_KEYS_JSON secret (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 in fetch-handler.ts) publishes, per agent, the current Ed25519 public JWK plus every retired key declared in the key bundle's history, each carrying an explicit nbf/exp validity window (seconds) naming the span of assertion iat values it may have signed. A retired key verifies history only; outside its window the verifier returns key_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).
  • kid identifies the KEY, not the day (ADR 1095 §4): kid = {slug}-{22-char-base64url-SHA-256-thumbprint}. It was {slug}-{YYYY-MM-DD} computed from new 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 with unknown_kid. Measured 2026-08-28 against containment-ai-agents PR #4240: kid=ai-strategy-2026-08-26 against a JWKS publishing only ai-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.ts appends a signed claim block via aarm-identity-claim-block.ts — on BOTH PR-creation paths, github_open_pr and (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-log posts: orchestrator/src/audit-events.ts::postAuditEvent attaches a signed signed_by: spiffe://… field.
    • Gated on SPIFFE_AGENT_KEYS_JSON (hard precondition) AND the default-ON AARM_SIGNING_ENABLED kill switch (graceful-degrade to unsigned).
  • Bound identifiers are IMMUTABLE where one exists (ADR 1095 §6): a github_open_pr claim 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-log claim binds sha256:<hex> of the post text, re-derivable from the posted line via audit-events.ts::unsignedAuditEventText (was triggerId, 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::verifyIdentityClaim re-derives the trusted JWKS and checks: sub parses as spiffe://containment-ai/agents/<slug> inside our trust domain; sub == the claimed spiffeId; the kid's agent slug == the sub'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's expected artifact when one is supplied); exp when present; and the Ed25519 signature against a published key for that agent whose window covers the signed iat. 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.sub against claim.spiffeId — both attacker-supplied, from the same claim object — then resolved the key by kid without 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. verifyIdentityClaim now 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 on origin/main@0a7f10aa, 7 of 7 green after), and aarm-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:

  1. Extract the JSON block between the aarm-identity-claim:v1 markers.
  2. Split signedAssertion into header.payload.signature.
  3. Confirm payload.aarm == "artifact-assertion:v1" and payload.canon == 1.
  4. 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 equals payload.artifact.digest. Confirm the artifact id names the artifact you are holding (for a PR, <repo>@<commit_sha>).
  5. Confirm payload.sub == spiffeId and that both parse as spiffe://containment-ai/agents/<slug>.
  6. Confirm the kid's agent slug equals that <slug>.
  7. Fetch /.well-known/spiffe-jwks.json, select a JWK for that agent whose nbf/exp window (when present) covers payload.iat, and verify the EdDSA signature over header.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-88AuditEvent.admin_id binds the audit row to the human user; organization_id binds 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.

  1. Not yet demonstrated end to end DISCHARGED 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 main at 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.json at 2026-08-31T11:42:22Z (HTTP 200, 1517 bytes) returns 10 keys, of which 10 of 10 carry key-derived thumbprint kids (growth-lead-Gmu43c9QlT26OQGj-AiapE, ai-strategy-cIHFyXL2JVuGXKg29pDyGv, …) and 0 of 10 carry the clock-labelled <slug>-YYYY-MM-DD form. 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.ai PR #616, opened by agent growth-lead at 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] with aarm = "artifact-assertion:v1", canon = 1, iat = 1788026694 (2026-08-29T18:04:54Z), sub = spiffe://containment-ai/agents/growth-lead, kid = growth-lead-Gmu43c9QlT26OQGj-AiapE, and artifact = {type: "pull_request", id: "containment-ai/www.containment.ai@74f163c17e35f26ac5670291f0bea0aebf9e36f2", digest: "25173c2835f1e1c72fe4e1da078550083c0aa6b7cbdf9e06596e7387ce420a19"}. Note for anyone re-checking this: searching the PR body for the string artifact-assertion:v1 returns 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 signedAssertion splits into header.payload.signature PASS — 3 parts
      3 payload.aarm == "artifact-assertion:v1" and payload.canon == 1 PASS
      4 digest recomputed from the documented preimage equals payload.artifact.digest, and the artifact id names the artifact held PASS — recomputed 25173c2835f1e1c7… == signed 25173c2835f1e1c7…
      5 payload.sub == spiffeId, both parse as spiffe://containment-ai/agents/<slug> PASS — both …/growth-lead
      6 the kid's agent slug equals the sub's slug PASS — growth-lead == growth-lead
      7 EdDSA signature over header.payload verifies against a published JWK for that agent whose window covers iat PASS — kid=growth-lead-Gmu43c9QlT26OQGj-AiapE, crv=Ed25519

      Verdict: 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)
      kid re-labelled to another agent (revenue-lead-…) fail at 6 FAIL at step 6 (kidSlug=revenue-lead vs subSlug=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: kid is {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}$).

  2. github_open_pr_from_branch PRs are UNSIGNED. 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 as github_open_pr, bound to the branch's HEAD COMMIT SHA (read back via git/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-body case produces no claim on BOTH paths, which is mirrored deliberately rather than silently diverged.

  3. #audit-log posts are not independently verifiable from Slack alone. formatAuditEvent renders [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 via unsignedAuditEventText, but an external verifier needs the assertion from the event payload, which Slack does not carry.

  4. Forward-only: binding is implemented at the agent-team runtime from ship date; historical artifacts are not retro-signed (RFC 0031 §Scope).

  5. 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.

  6. 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 (parent eec52fa3…, 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.ts posts to /pulls with 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.

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 kid clock-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.ai PR #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 raises NotImplementedError with 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 exposing get_tracer() and the AttrKey.* semantic vocabulary (org_id, user_id, policy_id, enforcement_action, matched_policy_type, severity).
  • proxy.containment.ai/containment/evaluation/recording.py — wraps every record_violation call 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_ENDPOINT receive spans immediately. This is a deployment posture, not a conformance gap.
  • Precision caveat (deployed prod image): the proxy production image (proxy.containment.ai Dockerfile.cloudflare) installs only requirements.txt + requirements.gateway.txt, NOT requirements.otel.txt. Because opentelemetry-api/-sdk are therefore absent at runtime, the wired instrumentation in otel.py/recording.py resolves to its documented no-op shims (_NoOpTracer/_NoOpSpan), so the capability is dormant in the deployed prod image today — it emits no spans even if OTEL_EXPORTER_OTLP_ENDPOINT is 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 add requirements.otel.txt to 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-agents origin/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) and resolveCrossRepoRiskTier (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 fetchVerifyData in orchestrator/src/data-fetcher.ts, the only call sites of the stamping helpers in orchestrator/src/data-provenance.ts (default-ON PROVENANCE_MARKERS_ENABLED kill 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.
  • 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.ai at 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 via propose_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 signed payload.artifact, and verify the EdDSA signature against a windowed public key at /.well-known/spiffe-jwks.json. orchestrator/src/aarm-verifier.ts::verifyIdentityClaim is the reference verifier and orchestrator/src/aarm-artifact-binding.ts the 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 via github_open_pr_from_branch carry no claim block at all.
  • R2 (intent-thread receipt binding) — the intent claim is serialized to details.intent on each signed audit receipt before signing; mutate any intent field on a recorded receipt and verify_event_chain reports hmac_valid: false. The forge-tests in tests/evaluation/test_r2_intent_receipt.py exercise 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)
RELATED RECORDS —
The Trust record is the canonical statement of our assurance posture. The AARM comparison page summarises this attestation requirement by requirement, and Runtime agent governance describes the product the attestation is about.