Interlock

08. Compliance mapping: what an Interlock receipt is evidence for

Read this first. Mapping a receipt field to a control says the field can serve as evidence that the control operated. It is not a certification, an attestation, or a legal opinion, and it does not make any system compliant. None of these frameworks mentions agent receipts. Whether a piece of evidence satisfies a control is decided by the assessor (the external auditor, the QSA, the notified body) for a specific system in scope. Every quote below comes from the primary text unless marked otherwise. Sources and how they were read are in docs/09-research-gcp-audit.md section 5. Where a row says “related to”, the control’s text is about something broader or different, and the receipt is at most supporting context for it.

What a receipt is

A receipt bundle is every journal entry for one effect (for example one refund), each chained to the one before it by hash, optionally signed with HMAC-SHA256. interlock.receipts.verify() re-derives the claims from the entries themselves. The exporters in interlock/export/ give every entry the same id everywhere, <effect id>-<entry hash>. What a second export of the same receipt does depends on the destination:

destination a second export evidence
BigQuery merge() adds no rows verified live, results/export_live.md
SIEM files (siem.append) writes no lines local files, results/export_live.md; no real SIEM ingested them
Cloud Logging a query returns each entry once. Google: “there are no guarantees of de-duplication in the export of logs”, so a sink to BigQuery, Pub/Sub or a SIEM may deliver it twice; dedupe on insertId query verified live; sinks NOT VERIFIED LIVE
BigQuery stream() best effort for about a minute; read through DEDUP_VIEW_SQL verified live, results/export_live.md
Cloud Trace via OTLP stores another copy of the span; export once, or dedupe on traceId + spanId verified live, results/export_live.md
entry fields an auditor reads
PROPOSED agent (who proposed), lease (the approval it cites), premises (facts it relied on, e.g. refunded_by_others: 0), effect (e.g. amount: 2000), ts
AUTHORIZED lease: the approval was live when checked
DISPATCHED written to disk before the send; checks.lease_live, checks.lease (the approval row read: max_cents, revoked, granted, or the AP2 mandate check), checks.violations (empty means the premises held), checks.use_problems (AP2 mandates: empty means the mandate was reserved for this effect)
COMMITTED result (the target’s evidence, e.g. Stripe refund id), or via + found after a crash, and rechecked (the checks re-run at recovery)
REFUSED reason (lease not live, stale_premise at recovery, lease already used, …), and the checks that failed
AMBIGUOUS the gate could not tell whether the effect landed, and says so instead of guessing
every entry ts, prev, hash; the bundle may carry signature

Who does what

The approval policy, the software that enforces it, and the authority to spend are held by three different groups. The receipt is how each can check the other two.

role owns touches in Interlock reads in the receipt
Risk and Compliance the approval policy: which actions need an approval, caps, who may approve, when an approval lapses the lease rules (leases.py / the approval store): max_cents, revocation, expiry; which premises must be re-checked refusals and their reasons, AMBIGUOUS effects, whether authorized_when_fired and assumptions_held are true for every send
Platform Engineering the agent framework and its runtime (Temporal, Google ADK, an MCP host) installs the gate once as middleware (the activity body, an ADK plugin, the MCP proxy), the journal, the exporters, the log bucket and its retention, the signing key valid, tamper_evident, export failures (ExportError), gaps in the chain
Finance / line of business operational authority: grants and revokes approvals, owns the money movement grants a lease for a case, revokes it what fired, under whose approval, for how much, with the processor’s id to reconcile against (Stripe refund id)

This split is itself a documented control in NIST AI RMF GOVERN 2.1 (“Roles and responsibilities and lines of communication related to mapping, measuring, and managing AI risks are documented and are clear to individuals and teams throughout the organization”). It is related to the segregation of duties SOC 2 CC6.3 names for access (below). It holds only if the agent’s identity cannot grant its own lease; Interlock records who granted it, it does not enforce who may.

The mapping

Codes are explained with quotes under “Citations”. “Supports” means the field is relevant evidence; “related to” means the control is about something broader, see the note in “Read this first”. The SOC 2 Processing Integrity criteria (PI1.x) apply only when an engagement includes the Processing Integrity category; the Security criteria (CC) are about security events and access. The “limits” column says what the receipt does not show.

The EU AI Act column cites Chapter III articles that do not apply before 2 December 2027 (Annex III systems) or 2 August 2028 (Annex I systems), under Regulation (EU) 2026/1744; see “Citations”.

check receipt evidence SOX ITGC SOC 2 PCI DSS v4.0 req 10 EU AI Act (not yet applicable) NIST AI RMF limits
Proposed: who asked for what, on which facts PROPOSED.agent, .lease, .premises, .effect, .ts supports the “computer operations” and “access to programs and data” ITGC areas (SEC 33-8810 II.A.1.d) for an automated transaction PI1.3 point of focus “Records System Processing Activities” 10.2.2 fields: user identification, type of event, date and time, affected resource (secondary source, NOT VERIFIED against the standard text) Art. 12(1) automatic recording of events; 12(2)(c) monitoring of operation MEASURE 2.8 (transparency and accountability documented) agent is the identity the caller passed; Interlock does not authenticate it
Authorized when fired: the approval was live and covered the amount at the instant of the send AUTHORIZED; DISPATCHED.checks.lease_live, .checks.lease.max_cents, .revoked; COMMITTED.rechecked after a crash; verify().authorized_when_fired “access to programs and data” (33-8810 II.A.1.d). AS 2201 .47 lowers the risk of an automated control only “if relevant information technology general controls are effective”, including program changes, which receipts do not evidence (see “What receipts do not cover”) PI1.3 point of focus “Processes Inputs … as authorized”. Related to CC6.3 and CC6.1, which cover granting and removing access and logical access security, not the approval of one transaction 10.2.2 success or failure indication (secondary, NOT VERIFIED) Art. 14(4)(e): when Finance revokes an approval, that revocation is the intervention by a person; the gate’s refusal of the pending send is how it takes effect MAP 3.5 (human oversight processes); MANAGE 2.4 (mechanisms to supersede, disengage, or deactivate) shows the check the gate ran and the approval row it read; not how the approver was provisioned (CC6.2)
Premises held: the facts the decision relied on were re-read immediately before the send DISPATCHED.checks.violations == []; rechecked.violations; verify().assumptions_held; REFUSED.reason = stale_premise supports the “computer operations” area. It is not evidence the control operated as designed on its own: AS 2201 .47 conditions reliance on effective ITGCs PI1.3 points of focus “Processes Inputs … as authorized” and “Detects and Corrects Production Errors”. Related to CC7.2 only if an assessor treats a changed fact as an anomaly; CC7.2 analyzes anomalies “to determine whether they represent security events” none directly Art. 12(2)(a) recording events relevant to identifying situations that may present a risk MANAGE 4.1 (post-deployment monitoring) only the premises the integration declared are re-checked; a premise nobody declared is not
Executed once: this effect id committed at most once, never sent again while a send was unresolved one COMMITTED; via (retry-idempotent, recovery-query); verify().happened_once “computer operations” (33-8810 II.A.1.d): processing completes once after a failure PI1.3 “Processes Inputs … completely, accurately, and timely as authorized”; PI1.4 point of focus “Distributes Output Completely and Accurately” 10.2.2 identity of affected resource (secondary, NOT VERIFIED) Art. 12(2)(a) MANAGE 4.3 (tracking, responding to, and recovering from incidents and errors followed and documented) happened_once is about one effect id. It is true on an effect that never fired, and it does not show that no other effect id, a hand refund, or another integration did the same thing. The receipt is the gate’s attestation; the independent evidence is the processor’s own record (Stripe’s refund list, checked by experiments/e2e_audit.py)
Recorded: what the target returned COMMITTED.result (Stripe refund id, already_processed) or found from a lookup lets a reviewer reconcile the log to the processor’s records PI1.4 point of focus “Creates and Maintains Records of System Output Activities” 10.2.2 origination and affected resource (secondary, NOT VERIFIED) Art. 12(1) MEASURE 2.8 a refund id is evidence of the processor’s response, not of settlement
Refused, with the reason REFUSED.reason, .checks, resolves evidence the control blocked an unauthorized or stale transaction PI1.3 “Processes Inputs … as authorized” (a refused input is not processed) 10.2.2 failure indication (secondary, NOT VERIFIED) Art. 14(4)(e) only for a refusal that enforces a person’s revocation. A refusal the gate reaches on its own (a stale premise) is an automated safeguard, not human oversight; the person who decides what happens next is the oversight MANAGE 2.4 a refusal stops the send; who follows up is a process outside Interlock
AMBIGUOUS surfaced instead of guessed AMBIGUOUS entry; Cloud Logging severity ERROR; OTLP span status ERROR; CEF severity 9; verify().happened == "unknown" an exception that needs manual resolution is recorded, not hidden PI1.3 point of focus “Detects and Corrects Production Errors” (detected here; the correction is the customer’s). Related to CC7.2 10.7.2 and 10.7.3: failures of critical security control systems detected, alerted, responded to promptly. Whether the gate counts as such a system is the assessor’s call Art. 14(4)(a) detecting and addressing anomalies, dysfunctions and unexpected performance MANAGE 4.3 the alert, the on-call rota and the resolution are the customer’s (a log-based alert on severity=ERROR is one line)
Hash chain and signature: the log was not edited, reordered, or truncated prev, hash on every entry; signature; verify().tamper_evident, .signed supports reliance on the log as evidence; the auditor still tests the log store PI1.5 point of focus “Archives and Protects System Records”. Related to CC6.1: tamper evidence complements access control, it does not replace it 10.3.x protection of audit logs (numbering confirmed by PCI SSC’s Summary of Changes; tamper evidence is in the spirit of it, it does not replace access control or write-once storage) Art. 12(1) GOVERN 1.4 unsigned, the chain proves internal consistency only: whoever controls the journal can rebuild it. The demo receipts are unsigned. A signature binds the entries to the key holder; it does not re-run the checks
Exported and retained the exporters; insertId / entry_hash / externalId ids; Cloud Logging receiveTimestamp evidence retained outside the system that produced it PI1.5 point of focus “Archives and Protects System Records” 10.4.1.1 automated audit log review (the SIEM’s rules over exported events); 10.5.1 12 months of history, three immediately available (secondary, NOT VERIFIED) Art. 19(1) providers and 26(6) deployers keep logs at least six months MANAGE 4.1 retention is the destination’s setting. Cloud Logging’s _Default bucket keeps 30 days unless changed: short of both 12 months and six months. A re-export duplicates spans in Cloud Trace and may duplicate entries in a Logging sink

Citations

SOX (IT general controls)

SOC 2 (2017 Trust Services Criteria, March 2020 revision; text read from a copy of the AICPA PDF, https://arpio.io/wp-content/uploads/2020/08/trust-services-criteria.pdf, since the AICPA download is gated)

PCI DSS v4.0, Requirement 10

EU AI Act, Regulation (EU) 2024/1689 (Official Journal, https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=OJ:L_202401689)

NIST AI RMF 1.0, NIST AI 100-1, January 2023 (https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf). Voluntary.

What receipts do not cover

Where AP2 fits

Google’s Agent Payments Protocol (AP2 v0.2) proves what a user authorized: a signed Payment Mandate, optionally open with constraints such as an amount range and allowed payees. It does not cover refunds or revocation (docs/09-research-ap2.md). An Interlock receipt records a different moment: that at the instant the effect fired, the authorization was still live and within its limits and the facts it relied on still held, and that this effect id committed at most once. With interlock.integrations.ap2, the gate also reserves each closed mandate for one effect, keeps the effects under one open mandate within its amount cap (an open mandate with no amount range authorizes nothing), and reads the payment the target will act on from the processor to check that it is the mandated transaction, payee and instrument (results/adk_mandate_probes.md). For an auditor the two stack: the mandate is the authorization record, the receipt is the execution record, and they share the mandate’s reference hash as the lease id.

Queries each role would run

Against the BigQuery table from interlock.export.bigquery (schema in that module; these are its AUDIT_QUERIES, and experiments/export_live.py ran each one against the live table, see results/export_live.md):

-- Finance: what fired this month, under which approval
SELECT lease, effect_id,
  COALESCE(JSON_VALUE(entry_json, '$.result.refund'), JSON_VALUE(entry_json, '$.found')) AS refund, recorded_at
FROM `P.interlock_audit.receipt_entries`
WHERE kind = 'COMMITTED' AND recorded_at >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), MONTH)

-- Risk and Compliance: every effect that ended refused or ambiguous, and why
SELECT effect_id, state, reason, recorded_at
FROM `P.interlock_audit.receipt_entries`
WHERE kind IN ('REFUSED', 'AMBIGUOUS') ORDER BY recorded_at DESC

-- Platform Engineering / auditor: rebuild a chain and re-run the verifier
SELECT entry_json
FROM `P.interlock_audit.receipt_entries`
WHERE effect_id = @effect ORDER BY entry_index
-- then: interlock.receipts.verify({"effect_id": effect, "entries": [json.loads(r) for r in rows]})

Cloud Logging, for an alert on anything ambiguous:

logName="projects/P/logs/interlock-receipts" AND labels.interlock_kind="AMBIGUOUS"