Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions daemon/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@ All notable changes to `agent-receipts-daemon` are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.10.0] - 2026-05-17

### Added

- **Receipt pipeline redaction** ([#426](https://github.com/agent-receipts/ar/pull/426),
closes [#423](https://github.com/agent-receipts/ar/issues/423)):
The daemon now redacts secrets from receipt body fields before persistence.
Built-in patterns cover GitHub PATs, OpenAI/Anthropic keys, AWS access key IDs,
bearer tokens, Slack tokens, PEM private keys, and URL query-string tokens.
JSON-aware key redaction additionally covers `password`, `token`, `api_key`,
`secret`, `authorization`, `private_key`, `jwt`, and 20+ other sensitive key names.
Redaction runs after hashing — `parameters_hash` and `response_hash` commit to
the raw canonical bytes; only the stored text fields (`outcome.error`,
`parameters_disclosure` when enabled) are sanitised.
Custom patterns can be added via `--redact-patterns <file.yaml>`
(env: `AGENTRECEIPTS_REDACT_PATTERNS`).

- **`agent-receipts list` companion CLI command** ([#420](https://github.com/agent-receipts/ar/pull/420),
closes [#410](https://github.com/agent-receipts/ar/issues/410)):
`agent-receipts list` prints recent receipts from the daemon store in tabular
or JSON form. Flags: `--limit N` (default 50), `--json`, `--db`/`AGENTRECEIPTS_DB`.
Newest-first by default.

### Changed

- **mcp-proxy is now a thin emitter** ([#421](https://github.com/agent-receipts/ar/pull/421),
closes [#416](https://github.com/agent-receipts/ar/issues/416)):
The mcp-proxy no longer maintains its own `receipts.db` or signs receipts.
It forwards raw tool-call events to the daemon over the Unix socket
(the same pattern as `agent-receipts-hook`). The daemon is the sole receipt
writer. **Breaking change for mcp-proxy:** the `-receipt-db`, `-key`, `-chain`,
`-issuer*`, `-operator*`, `-principal`, `-taxonomy`, and `-bundled-taxonomies`
flags have been removed. The `mcp-proxy list`, `inspect`, `verify`, `export`,
and `stats` subcommands now print a deprecation notice pointing to
`agent-receipts list` / `agent-receipts verify`.

### Dependencies

- Bump `github.com/agent-receipts/ar/sdk/go` to `v0.9.1`
(DESC ordering and no silent 10k row cap in `QueryReceipts`).

## [0.9.1] - 2026-05-16

### Dependencies
Expand Down
4 changes: 4 additions & 0 deletions site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ export default defineConfig({
label: "Blog",
items: [
{ label: "All Posts", slug: "blog" },
{
label: "Why Your Agent Can't Audit Itself",
slug: "blog/daemon-process-separation",
},
{
label: "OpenClaw Plugin: How It Works",
slug: "blog/openclaw-plugin-deep-dive",
Expand Down
117 changes: 117 additions & 0 deletions site/src/content/docs/blog/daemon-process-separation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: "The audit boundary belongs outside the agent"
description: "The architectural problem with in-process receipt signing, and how daemon process separation restores the audit property."
---

**Series: Auditing AI Agents** · Part 1 of 3 · Next: [One Chain, Two Channels, Zero Secrets →](/blog/unified-chain-redaction-demo/)

---

An agent that signs its own audit trail isn't being audited.

---

## The original design

The first version of Agent Receipts was deliberately small: each MCP proxy instance loaded an Ed25519 signing key, maintained its own SQLite database, and signed and stored a receipt for every tool call it intercepted — all in the same process as the agent.

The cryptography worked. The audit property didn't. When the signing process and the audited process are the same, the auditor is holding its own signing key. It can sign what it likes, drop what it doesn't, and nothing outside the process can disagree.

A second limitation came from running more than one MCP proxy at once — the usual case when an agent talks to several MCP servers. Each proxy kept its own chain, with no shared sequence between them. Correlating a GitHub tool call with a filesystem tool call meant application-level join logic instead of one query.

---

## The decision

[ADR-0010](https://github.com/agent-receipts/ar/blob/main/docs/adr/0010-daemon-process-separation.md) split every integration into two roles.

**Thin emitter** — the plugin, proxy, or SDK serialises the tool call, writes it to a local Unix socket, and moves on. Signing, storage, and chaining are the daemon's concern; the agent is never blocked waiting for the audit layer. If the daemon is running but backpressured, the emitter increments a local drop counter and flushes it on the next successful send, so the daemon can record the gap as an `events_dropped` receipt in the chain. If the daemon isn't running at all, events drop without trace — that case is detectable from outside the chain, via service-manager status and the absence of fresh receipts.

**agent-receipts-daemon** — a separate process running as its own OS user, sole owner of the signing keys and the SQLite database. Receives events, captures peer credentials at the kernel level, canonicalises inputs (RFC 8785), hash-chains, signs (Ed25519), and persists.

```mermaid
graph LR
subgraph agent["Agent process"]
MP[mcp-proxy]
Hook[agent-receipts-hook]
end
subgraph daemon["agent-receipts-daemon"]
PC[peer credential capture]
CH[hash chain]
KS[signing key]
DB[(receipts.db)]
end

MP-. fire-and-forget .->PC
Hook-. fire-and-forget .->PC
PC --> CH --> KS --> DB
```

The agent process can't reach the signing keys or the database. The separation is structural, not policy.

---

## Peer credential capture

The daemon doesn't take the emitter's word for who it is. The moment a connection lands on the socket, it asks the kernel directly:

- **macOS**: `LOCAL_PEERCRED` for uid/gid, `LOCAL_PEEREPID` for pid, then `proc_pidpath` for the executable path
- **Linux**: `SO_PEERCRED` for uid, gid, pid, then `/proc/<pid>/exe` for the executable path

These come straight from the kernel. The connecting process can lie about anything in its payload, but not about who it is. The daemon stamps every receipt with that identity, so an auditor can answer not just *what* was called but *which process* called it — by uid, pid, and executable path.

---

## One chain, all channels

Because every emitter connects to the same daemon socket, all receipts land in one chain with a single monotonic sequence. An agent session that talks to three MCP servers and fires native tool calls through the Claude Code hook produces one chain — not four.

Cross-channel correlation collapses to one query: every receipt for a session carries the same `session_id`, regardless of which emitter captured the call.

---

## The tradeoff

The daemon is a system service. That's friction the old `npm install` flow didn't have. It requires installation, a signing key, and a service manager entry (launchd on macOS, systemd on Linux).

That's the cost of an audit boundary that means something. In-process signing is simpler to install; it just isn't the audit boundary.

Homebrew handles most of it:

```sh
brew install agent-receipts/tap/agent-receipts-daemon
brew services start agent-receipts-daemon
```

---

## What it looks like in practice

Receipts are W3C Verifiable Credentials, Ed25519-signed, hash-chained. Here's a real one from this session, signature and hashes abbreviated:

```json
{
"action": {
"type": "mcp.github.pull_request_read",
"parameters_hash": "sha256:2f9911eb..."
},
"credentialSubject": {
"chain": { "sequence": 2046, "chain_id": "default" }
},
"proof": {
"type": "Ed25519Signature2020",
"verificationMethod": "did:agent-receipts-daemon:local#k1",
"proofValue": "uZ7aqJ4Zfq..."
}
}
```

That signing key — `did:agent-receipts-daemon:local#k1` — lives only in the daemon. The agent process that triggered this call has never seen it.

---

## What comes next

The daemon is the trust anchor for everything that follows: secret redaction before storage (already shipped), asymmetric payload encryption so only a forensic key holder can read stored inputs, centralised storage for cross-machine audit trails. All of it depends on having one trusted process that owns the keys and the chain.

The next post is the demo — the unified chain and secret redaction working end to end, on real receipts.
Loading