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.
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 |
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.
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 |
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.
PROPOSED.agent records the model name the integration passed, nothing more. Because AS 2201 .47 ties
reliance on an automated control to effective ITGCs, this gap limits what the other rows support._Default keeps 30 days. PCI’s 12 months (secondary source) and the AI
Act’s six months need a user-defined bucket, a sink, or the BigQuery table. Automated review (10.4.1.1) is the SIEM’s
rules.receiveTimestamp, the time Google
received the entry, which a reviewer can compare.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.
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"