|
| 1 | +# Signed Decision Receipts for ABCA Agent Runs |
| 2 | + |
| 3 | +**Add cryptographically verifiable evidence of every tool call an ABCA agent makes, so PR reviewers and regulators can verify the agent's behavior without trusting the orchestrator.** |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Why this matters for ABCA |
| 8 | + |
| 9 | +ABCA agents run autonomously in isolated cloud environments, clone repos, |
| 10 | +write code, run tests, and open pull requests. Each step is a tool call |
| 11 | +(`git clone`, `npm install`, `npm test`, shell commands, file edits). By |
| 12 | +default, the evidence that each step happened and was authorized lives in |
| 13 | +CloudWatch logs, which are operator-controlled and trust the orchestrator |
| 14 | +not to redact or edit entries after the fact. |
| 15 | + |
| 16 | +For regulated environments (financial services, healthcare, public sector) |
| 17 | +or for teams that want to hand a PR to a regulator or counterparty, a |
| 18 | +stronger evidence model is needed: each policy decision, signed at the |
| 19 | +moment it is made, with a tamper-evident chain across the full agent run. |
| 20 | + |
| 21 | +Signed decision receipts provide this. A receipt is a small JSON object, |
| 22 | +signed with Ed25519, JCS-canonicalized, and hash-chained to its |
| 23 | +predecessor. Anyone with the public key can verify the whole chain offline. |
| 24 | + |
| 25 | +## What this adds to the default ABCA pipeline |
| 26 | + |
| 27 | +The ABCA agent runs inside the Dockerfile in `agent/`. Three additions: |
| 28 | + |
| 29 | +1. **PreToolUse hook**: before every tool call, evaluate the call against a |
| 30 | + Cedar policy. Cedar denial blocks the tool call. |
| 31 | +2. **PostToolUse hook**: after every tool call, sign an Ed25519 receipt |
| 32 | + describing the decision, the policy digest, the inputs, and a hash link |
| 33 | + to the previous receipt. |
| 34 | +3. **Receipt artifact**: the receipt directory (`/tmp/receipts/`) is |
| 35 | + uploaded as a build artifact alongside the PR, and the PR body includes |
| 36 | + a one-line verification instruction. |
| 37 | + |
| 38 | +Result: a reviewer or auditor running `npx @veritasacta/verify |
| 39 | +./receipts/*.json` gets an offline proof that every step of the agent run |
| 40 | +was authorized under the declared policy, with exit code 0 for valid, 1 for |
| 41 | +tampered, or 2 for malformed. |
| 42 | + |
| 43 | +## The pattern |
| 44 | + |
| 45 | +``` |
| 46 | +┌──────────────────────────────────────────────────────────────────────────┐ |
| 47 | +│ ABCA Autonomous Agent Runtime │ |
| 48 | +│ │ |
| 49 | +│ ┌────────────────┐ │ |
| 50 | +│ │ Orchestrator │ │ |
| 51 | +│ └───────┬────────┘ │ |
| 52 | +│ │ invokes │ |
| 53 | +│ ▼ │ |
| 54 | +│ ┌────────────────┐ PreToolUse hook ┌───────────────────────────┐ │ |
| 55 | +│ │ Agent Runtime │ ───────────────────▶ │ Cedar Policy Evaluator │ │ |
| 56 | +│ │ (Claude Code / │ (before each call) │ ./protect.cedar │ │ |
| 57 | +│ │ Amazon Q / │ │ allow → continue │ │ |
| 58 | +│ │ similar) │ │ deny → exit 2, block │ │ |
| 59 | +│ └───────┬────────┘ └───────────────────────────┘ │ |
| 60 | +│ │ post-execution │ |
| 61 | +│ ▼ │ |
| 62 | +│ ┌────────────────┐ PostToolUse hook ┌──────────────────────────┐ │ |
| 63 | +│ │ Tool output │ ────────────────────▶ │ Ed25519 Receipt Signer │ │ |
| 64 | +│ └────────────────┘ (after each call) │ JCS canonical + chain │ │ |
| 65 | +│ │ /tmp/receipts/*.json │ │ |
| 66 | +│ └──────────────────────────┘ │ |
| 67 | +└──────────────────────────────────────────────────────────────────────────┘ |
| 68 | +``` |
| 69 | + |
| 70 | +## Setup |
| 71 | + |
| 72 | +### 1. Add protect-mcp to the agent container |
| 73 | + |
| 74 | +In `agent/Dockerfile`, ensure Node.js 18+ is installed (for `npx`): |
| 75 | + |
| 76 | +```dockerfile |
| 77 | +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ |
| 78 | + && apt-get install -y nodejs |
| 79 | +``` |
| 80 | + |
| 81 | +### 2. Ship a Cedar policy with the agent image |
| 82 | + |
| 83 | +Create `agent/protect.cedar` with allow/deny rules appropriate to the agent's |
| 84 | +scope. Example for a coding agent that runs in an isolated environment: |
| 85 | + |
| 86 | +```cedar |
| 87 | +// Allow read-oriented tools on source files. |
| 88 | +permit ( |
| 89 | + principal, |
| 90 | + action in [Action::"Read", Action::"Glob", Action::"Grep"], |
| 91 | + resource |
| 92 | +); |
| 93 | +
|
| 94 | +// Allow the build/test commands the agent needs. |
| 95 | +permit ( |
| 96 | + principal, |
| 97 | + action == Action::"Bash", |
| 98 | + resource |
| 99 | +) when { |
| 100 | + context.command_pattern in [ |
| 101 | + "git", "npm", "pnpm", "yarn", "uv", "python", |
| 102 | + "pytest", "cargo", "go", "make" |
| 103 | + ] |
| 104 | +}; |
| 105 | +
|
| 106 | +// Deny destructive commands even in the isolated environment. |
| 107 | +forbid ( |
| 108 | + principal, |
| 109 | + action == Action::"Bash", |
| 110 | + resource |
| 111 | +) when { |
| 112 | + context.command_pattern in ["rm -rf", "dd", "mkfs", "shred"] |
| 113 | +}; |
| 114 | +
|
| 115 | +// Writes only to the agent's working directory. |
| 116 | +permit ( |
| 117 | + principal, |
| 118 | + action in [Action::"Write", Action::"Edit"], |
| 119 | + resource |
| 120 | +) when { |
| 121 | + context.path_starts_with == "/workspace/" |
| 122 | +}; |
| 123 | +``` |
| 124 | + |
| 125 | +Policy authoring tips: |
| 126 | + |
| 127 | +- **`forbid` is authoritative.** Destructive rules cannot be bypassed by a |
| 128 | + later permissive rule. Always write `forbid` for genuinely dangerous |
| 129 | + patterns. |
| 130 | +- **Restrict writes by path prefix.** Pin the agent to its working directory |
| 131 | + so it cannot accidentally modify CI config or credentials. |
| 132 | +- **Allow-list commands, do not deny-list.** The `Bash` permit rule above |
| 133 | + lists exactly the commands the agent is allowed to run. Any unknown |
| 134 | + command (e.g., a prompt-injected `curl malicious-url`) falls through to |
| 135 | + an implicit deny. |
| 136 | + |
| 137 | +### 3. Configure Claude Code hooks |
| 138 | + |
| 139 | +If the ABCA agent uses Claude Code, drop `.claude/settings.json` into the |
| 140 | +working directory before invoking: |
| 141 | + |
| 142 | +```json |
| 143 | +{ |
| 144 | + "hooks": { |
| 145 | + "PreToolUse": [ |
| 146 | + { |
| 147 | + "matcher": ".*", |
| 148 | + "hook": { |
| 149 | + "type": "command", |
| 150 | + "command": "npx protect-mcp@latest evaluate --policy /agent/protect.cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\" --fail-on-missing-policy false" |
| 151 | + } |
| 152 | + } |
| 153 | + ], |
| 154 | + "PostToolUse": [ |
| 155 | + { |
| 156 | + "matcher": ".*", |
| 157 | + "hook": { |
| 158 | + "type": "command", |
| 159 | + "command": "npx protect-mcp@latest sign --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\" --output \"$TOOL_OUTPUT\" --receipts /tmp/receipts/ --key /agent/protect-mcp.key" |
| 160 | + } |
| 161 | + } |
| 162 | + ] |
| 163 | + } |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +The signing key (`/agent/protect-mcp.key`) is generated on first run. |
| 168 | +Persist it to AWS Secrets Manager for long-lived agents, or regenerate per |
| 169 | +run for ephemeral ones. Keep the **public** key fingerprint (visible in |
| 170 | +every receipt's `public_key` field) alongside the agent definition for |
| 171 | +verifiers. |
| 172 | + |
| 173 | +### 4. Upload receipts as a PR artifact |
| 174 | + |
| 175 | +In the agent's PR-opening step (`agent/src/...`), after the agent finishes: |
| 176 | + |
| 177 | +```python |
| 178 | +import shutil, subprocess |
| 179 | +from pathlib import Path |
| 180 | + |
| 181 | +# Archive receipts alongside the PR |
| 182 | +receipts_dir = Path("/tmp/receipts") |
| 183 | +if receipts_dir.exists(): |
| 184 | + tarball = Path("/workspace/decision-receipts.tar.gz") |
| 185 | + subprocess.run( |
| 186 | + ["tar", "-czf", str(tarball), "-C", str(receipts_dir), "."], |
| 187 | + check=True, |
| 188 | + ) |
| 189 | + |
| 190 | + # Upload to S3 for long-term retention (bucket from environment) |
| 191 | + subprocess.run( |
| 192 | + ["aws", "s3", "cp", str(tarball), |
| 193 | + f"s3://abca-receipts/{task_id}/decision-receipts.tar.gz"], |
| 194 | + check=True, |
| 195 | + ) |
| 196 | + |
| 197 | + # Reference from the PR body |
| 198 | + pr_body += "\n\n## Decision Receipts\n" |
| 199 | + pr_body += f"This PR was produced by an autonomous agent. Decision receipts " |
| 200 | + pr_body += f"for every tool call are archived at " |
| 201 | + pr_body += f"`s3://abca-receipts/{task_id}/decision-receipts.tar.gz`.\n\n" |
| 202 | + pr_body += "Verify offline:\n\n" |
| 203 | + pr_body += "```bash\n" |
| 204 | + pr_body += f"aws s3 cp s3://abca-receipts/{task_id}/decision-receipts.tar.gz - | tar xz\n" |
| 205 | + pr_body += "npx @veritasacta/verify receipts/*.json\n" |
| 206 | + pr_body += "```\n" |
| 207 | +``` |
| 208 | + |
| 209 | +Reviewers see a link in the PR body; any stakeholder can run two commands |
| 210 | +to confirm the chain is intact. |
| 211 | + |
| 212 | +## Receipt format |
| 213 | + |
| 214 | +A single receipt: |
| 215 | + |
| 216 | +```json |
| 217 | +{ |
| 218 | + "receipt_id": "rcpt-a8f3c9d2", |
| 219 | + "receipt_version": "1.0", |
| 220 | + "issuer_id": "abca-agent-protect-mcp", |
| 221 | + "event_time": "2026-04-17T12:34:56.123Z", |
| 222 | + "tool_name": "Bash", |
| 223 | + "input_hash": "sha256:a3f8c9d2e1b7465f...", |
| 224 | + "decision": "allow", |
| 225 | + "policy_id": "protect.cedar", |
| 226 | + "policy_digest": "sha256:b7e2f4a6c8d0e1f3...", |
| 227 | + "parent_receipt_id": "rcpt-3d1ab7c2", |
| 228 | + "public_key": "4437ca56815c0516...", |
| 229 | + "signature": "4cde814b7889e987..." |
| 230 | +} |
| 231 | +``` |
| 232 | + |
| 233 | +Three invariants make this verifiable offline across any conformant |
| 234 | +implementation: |
| 235 | + |
| 236 | +- **JCS canonicalization (RFC 8785)** before signing |
| 237 | +- **Ed25519 signatures (RFC 8032)** over the canonical bytes |
| 238 | +- **Hash chain linkage** via `parent_receipt_id` |
| 239 | + |
| 240 | +Full wire-format spec: |
| 241 | +[draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/). |
| 242 | + |
| 243 | +## Why Cedar for ABCA specifically |
| 244 | + |
| 245 | +ABCA already uses AWS CDK for infrastructure. Cedar is AWS's open |
| 246 | +authorization language (used in Amazon Verified Permissions, AWS IAM |
| 247 | +Access Analyzer, and the CDK policy constructs). Using Cedar for agent |
| 248 | +authorization means: |
| 249 | + |
| 250 | +- Same policy engine your AWS infrastructure teams already know |
| 251 | +- WASM bindings available ([cedar-policy/cedar-for-agents](https://github.com/cedar-policy/cedar-for-agents)) |
| 252 | + so policy evaluation does not require a Rust toolchain in the container |
| 253 | +- AWS IAM analyzer can audit the policy file for logical errors |
| 254 | +- Policy changes are diffable in Git alongside the rest of the IaC |
| 255 | + |
| 256 | +## Composition with SLSA provenance |
| 257 | + |
| 258 | +When an ABCA agent produces a PR, the commit is the subject of a SLSA |
| 259 | +Provenance v1 attestation. The receipt chain rides as a ResourceDescriptor |
| 260 | +byproduct in that provenance, following the |
| 261 | +[agent-commit build type](https://refs.arewm.com/agent-commit/v0.2): |
| 262 | + |
| 263 | +```json |
| 264 | +{ |
| 265 | + "name": "decision-receipts", |
| 266 | + "digest": { "sha256": "..." }, |
| 267 | + "uri": "s3://abca-receipts/<task_id>/decision-receipts.tar.gz", |
| 268 | + "annotations": { |
| 269 | + "predicateType": "https://veritasacta.com/attestation/decision-receipt/v0.1", |
| 270 | + "signerRole": "supervisor-hook" |
| 271 | + } |
| 272 | +} |
| 273 | +``` |
| 274 | + |
| 275 | +The SLSA provenance (signed by ABCA orchestrator identity) references the |
| 276 | +receipt chain (signed by the agent's supervisor-hook identity). Two trust |
| 277 | +domains, cross-referenced at the byproduct layer. See |
| 278 | +[slsa-framework/slsa#1594](https://github.com/slsa-framework/slsa/issues/1594) |
| 279 | +for the composition discussion. |
| 280 | + |
| 281 | +## Verifying a PR from outside AWS |
| 282 | + |
| 283 | +A reviewer without AWS account access can still verify an ABCA agent's run |
| 284 | +if the receipts are published in a publicly readable location: |
| 285 | + |
| 286 | +```bash |
| 287 | +# 1. Download the archive |
| 288 | +curl -sL https://example.com/abca-receipts/task-123.tar.gz | tar xz |
| 289 | + |
| 290 | +# 2. Verify the chain offline |
| 291 | +npx @veritasacta/verify receipts/*.json |
| 292 | +# Exit 0 = chain valid, 1 = tampered, 2 = malformed |
| 293 | + |
| 294 | +# 3. Inspect any specific receipt |
| 295 | +jq '.' receipts/rcpt-a8f3c9d2.json |
| 296 | +``` |
| 297 | + |
| 298 | +No AWS credentials, no ABCA runtime, no trust in the orchestrator required. |
| 299 | + |
| 300 | +## Cross-implementation interoperability |
| 301 | + |
| 302 | +The receipt format is implemented by four independent codebases today: |
| 303 | + |
| 304 | +| Implementation | Language | Use case | |
| 305 | +|----------------|----------|----------| |
| 306 | +| [protect-mcp](https://www.npmjs.com/package/protect-mcp) | TypeScript | Claude Code, Cursor | |
| 307 | +| [protect-mcp-adk](https://pypi.org/project/protect-mcp-adk/) | Python | Google ADK | |
| 308 | +| [sb-runtime](https://github.com/ScopeBlind/sb-runtime) | Rust | OS-level sandbox | |
| 309 | +| APS governance hook | Python | CrewAI, LangChain | |
| 310 | + |
| 311 | +A chain produced in any of them verifies with any conformant verifier. The |
| 312 | +format is the contract. |
| 313 | + |
| 314 | +## What this guide does not cover |
| 315 | + |
| 316 | +- **Policy authoring at scale.** A production ABCA deployment likely needs |
| 317 | + multiple policies (per environment, per task risk tier). Cedar supports |
| 318 | + policy composition with explicit precedence rules; start simple and |
| 319 | + iterate. |
| 320 | +- **Key management.** The example above generates a key per run. Production |
| 321 | + deployments should use AWS Secrets Manager, AWS KMS, or a hardware |
| 322 | + security module (CloudHSM). For the strongest guarantee, bind the signing |
| 323 | + key to an ATECC608B secure element outside the agent's trust boundary. |
| 324 | +- **Transparency log anchoring.** Receipts can be anchored in Sigstore |
| 325 | + Rekor for cross-org verification with inclusion proofs. See |
| 326 | + [sigstore/rekor#2798](https://github.com/sigstore/rekor/issues/2798). |
| 327 | + |
| 328 | +## References |
| 329 | + |
| 330 | +- [`draft-farley-acta-signed-receipts`](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) — IETF draft, receipt wire format |
| 331 | +- [RFC 8032](https://datatracker.ietf.org/doc/html/rfc8032) — Ed25519 |
| 332 | +- [RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785) — JCS |
| 333 | +- [Cedar policy language](https://docs.cedarpolicy.com/) |
| 334 | +- [cedar-policy/cedar-for-agents](https://github.com/cedar-policy/cedar-for-agents) — WASM bindings |
| 335 | +- [protect-mcp on npm](https://www.npmjs.com/package/protect-mcp) |
| 336 | +- [@veritasacta/verify on npm](https://www.npmjs.com/package/@veritasacta/verify) |
| 337 | +- [in-toto/attestation#549](https://github.com/in-toto/attestation/pull/549) — Decision Receipt predicate proposal |
| 338 | +- [agent-commit build type](https://refs.arewm.com/agent-commit/v0.2) — SLSA provenance for agent-produced commits |
0 commit comments