# agent-receipts-daemon

Single OS-user process that owns the Ed25519 signing key and the SQLite
receipt store. Emitters (mcp-proxy, OpenClaw, SDK consumers) connect over a
local Unix-domain socket and send fire-and-forget event frames; the daemon
captures the connecting peer's OS-attested credentials, canonicalises the
receipt (RFC 8785), signs it (Ed25519), and persists it.

See [ADR-0010](../docs/adr/0010-daemon-process-separation.md) for design
rationale and [issue #236](https://github.com/agent-receipts/ar/issues/236)
for the work breakdown.

This is **Phase 1** of the daemon roll-out — the foundation slice. It ships
the standalone daemon binary, peer-cred capture, chain-tail resumption, the
file-backed `KeySource`, and the `agent-receipts verify` read CLI (which
works whether the daemon is up or down). Emitter refactor for mcp-proxy /
OpenClaw / SDK ships in later phases.

## Build

```sh
go build ./cmd/agent-receipts-daemon
go test ./...                     # unit tests
go test -tags=integration ./...   # integration tests (real socket, real DB)
```

Build from a clone of the monorepo: the repo-root `go.work` wires the in-tree
`sdk/go` so `go build` from `daemon/` picks up `ReceiptStore.GetChainTail`.

`go install github.com/agent-receipts/ar/daemon/cmd/agent-receipts-daemon@latest`
is **not yet supported**: the daemon depends on `sdk/go.GetChainTail`, which
is not in the latest published `sdk/go` tag (`v0.6.0`). Standalone install
becomes possible once the next `sdk/go` tag is released and a follow-up bumps
the require in `daemon/go.mod`.

### CI coverage

`.github/workflows/daemon.yml` runs `go vet`, builds `./cmd/...`, and
runs the unit + integration test suite with `-race` and
`-tags=integration` on pushes to `main` and on pull requests targeting
`main` whose diff touches `daemon/**` or `sdk/go/**`. The `sdk/go/**`
trigger mirrors `mcp-proxy.yml` so that `sdk/go` changes which break
the daemon are caught in the same PR that introduces them.

## Run

The daemon takes config from flags (preferred) or environment variables. All
fields have sensible per-OS defaults.

```sh
agent-receipts-daemon \
  --socket /run/agentreceipts/events.sock \
  --db    /var/lib/agentreceipts/receipts.db \
  --key   /etc/agentreceipts/signing.key \
  --chain-id default \
  --issuer-id "did:agent-receipts-daemon:$(hostname)" \
  --verification-method "did:agent-receipts-daemon:$(hostname)#k1"
```

| Flag | Env | Default |
|---|---|---|
| `--socket` | `AGENTRECEIPTS_SOCKET` | `/run/agentreceipts/events.sock` (Linux), `$TMPDIR/agentreceipts/events.sock` (macOS) |
| `--db` | `AGENTRECEIPTS_DB` | `~/.agent-receipts/receipts.db` |
| `--key` | `AGENTRECEIPTS_KEY` | `~/.agent-receipts/signing.key` |
| `--chain-id` | `AGENTRECEIPTS_CHAIN_ID` | `default` |
| `--issuer-id` | `AGENTRECEIPTS_ISSUER_ID` | `did:agent-receipts-daemon:local` |
| `--public-key` | `AGENTRECEIPTS_PUBLIC_KEY` | `<--key>.pub` |
| `--verification-method` | `AGENTRECEIPTS_VERIFICATION_METHOD` | `did:agent-receipts-daemon:local#k1` |

The signing key file must be a PKCS#8-encoded Ed25519 private key (the format
`receipt.GenerateKeyPair()` in `sdk/go` produces) with permissions no looser
than owner-only — the daemon rejects any group or world bit (read, write, or
execute), so `0600`, `0400`, etc. are accepted; `0640` and `0644` are not.
The daemon also refuses to start on a non-Ed25519 key, a symlink, or a
non-regular file at this path.

The socket directory is created with mode `0750` if missing; the socket
itself is `0660`. Phase 1 unprivileged installs use the per-user defaults
(`$TMPDIR` on macOS, `$XDG_RUNTIME_DIR` on Linux when set).

On every startup the daemon publishes the matching SPKI public key to
`--public-key` (default `<KeyPath>.pub`, tracking any `--key` override) with
mode `0644`, so independent verifiers — `agent-receipts verify`, audit
scripts, CI checks — can load it without access to the private key path. If
the file already exists with the same contents the publish is a no-op; if
the contents differ the daemon refuses to start (a mismatch means either
the signing key was rotated / restored from backup, or the published file
was tampered with — operator must remove the stale file deliberately). The
daemon also refuses if the path is a symlink, FIFO, device, etc.

The published key file is `0644`, but its parent directory is created at
`0750` to match the receipt-store directory's access policy — non-owners
must be in the daemon user's group to traverse it and reach the public key.
Per-user installs (the MVP path: `~/.agent-receipts/`) are unaffected since
the operator who runs the verify CLI owns the directory. System installs
(`/etc/agentreceipts/`, `/var/lib/agentreceipts/`) are expected to give the
daemon a dedicated `agentreceipts` user and the read-side an
`agentreceipts-read` group whose members traverse the directory; that
ownership/grouping is a packaging concern (Homebrew / launchd / systemd) and
not something the daemon assigns at runtime. If the directory already exists
the daemon does not modify its mode, so operator-managed permissions are
preserved.

## Read interface: `agent-receipts verify`

```sh
agent-receipts verify --chain-id default
# or, with explicit paths:
agent-receipts verify \
  --db /var/lib/agentreceipts/receipts.db \
  --public-key /etc/agentreceipts/signing.key.pub \
  --chain-id default
```

Defaults match the daemon's: a verify run without flags works after
`agent-receipts-daemon` has run at least once with the same per-user paths.

`verify` opens the SQLite store **read-only** via `sdk/go/store.OpenReadOnly`
so it is safe to run while the daemon is the active writer, and it does not
require the daemon socket to be reachable. Independent verifiability is not
gated on daemon availability (issue #236, Section 4).

Exit codes are stable for scripting:

| Code | Meaning |
|---|---|
| `0` | Chain verified |
| `1` | Chain failed verification (output lists per-receipt status) |
| `2` | Usage error (bad flags, missing key file, unreadable DB) |

## Wire protocol

SOCK_STREAM Unix-domain socket (uniform across Linux and macOS — see
*Transport choice* below). Each emitter message is a 4-byte big-endian length
prefix followed by a JSON payload of that many bytes. Maximum payload is
1 MiB; larger frames are dropped with the connection.

The JSON payload is the ADR-0010 emitter schema:

```json
{
  "v": "1",
  "ts_emit": "2026-05-03T00:00:00.000Z",
  "session_id": "uuid-v4",
  "channel": "mcp_proxy",
  "tool": { "server": "github", "name": "list_repos" },
  "input": { "owner": "agent-receipts" },
  "output": [ { "name": "ar" } ],
  "error": "",
  "decision": "allowed"
}
```

`decision` is one of `allowed` / `denied` / `pending`. `input` and `output`
accept any JSON value (object, array, primitive) or `null` / omitted for
events with no payload; the daemon canonicalises (RFC 8785) and stores only
the SHA-256 digest in `action.parameters_hash` and `outcome.response_hash`,
never the raw bytes. The daemon adds `seq`, `prev_hash`, `ts_recv`, peer
attestation, and the receipt id before signing, so emitters never see those
fields and cannot forge them.

## Phase 1 scope and deviations

The following are deliberate Phase 1 choices, all callable out for follow-up:

- **Transport choice.** ADR-0010 specifies `SOCK_SEQPACKET`. macOS does not
  support SEQPACKET on AF_UNIX, so the daemon uses `SOCK_STREAM` uniformly
  on both Linux and macOS with explicit length-prefix framing. Peer-credential
  retrieval works identically on stream sockets, so the trust model is
  unchanged. A follow-up issue should amend ADR-0010 to record the per-OS
  socket type.
- **Peer attestation placement.** ADR-0010 calls for a top-level `peer`
  field on each receipt. Adding that requires a spec change (out of scope
  per AGENTS.md). Phase 1 stashes peer attestation in
  `action.parameters_disclosure` under keys `peer.platform`, `peer.pid`,
  `peer.uid`, `peer.gid`, `peer.exe_path`. The values are still
  signature-protected. The emitter-refactor phase will introduce the
  proper spec field.
- **macOS `peer.exe_path`.** Linux populates this from `/proc/<pid>/exe`;
  macOS uses the `SYS_PROC_INFO(PROC_PIDPATHINFO)` syscall directly
  (the call libproc's `proc_pidpath()` wraps), so the daemon stays
  CGO-free. Failure is non-fatal — `exe_path` is left empty and pid /
  uid / gid are still recorded.
- **Single chain id.** The daemon owns one chain id per process. Multi-chain
  support can grow `chain.State` into a chainID-keyed map without breaking
  callers.
- **No emitter refactor.** mcp-proxy, OpenClaw, and the three SDKs continue
  to sign in-process. They will be migrated to thin emitters in Phase 2+.
- **No drop-counter / `events_dropped` synthetic receipts.** That mechanism
  belongs with the emitter side (`EAGAIN` handling) and ships with the
  emitter refactor.
- **No Homebrew / launchd / systemd packaging.** Operators run the binary
  directly in Phase 1.
- **No Windows port.** Tracked as a separate issue per #236.

## Layout

```
daemon.go                                  # Run() entrypoint and Config; publishes the public key on startup
cmd/agent-receipts-daemon/main.go          # daemon CLI: flag/env parsing, signal handling
cmd/agent-receipts/main.go                 # read CLI: thin shim over internal/verifycli
internal/
  chain/state.go                           # in-memory (seq, prev_hash) owner; sole writer
  keysource/keysource.go                   # KeySource interface (ADR-0015 shape)
  keysource/file.go                        # PEM-on-disk adapter
  socket/listener.go                       # Unix-domain socket + length-prefix framing
  socket/peercred_{linux,darwin,other}.go  # OS-specific peer-credential capture
  pipeline/build.go                        # frame + peer -> AgentReceipt -> sign -> store
  verifycli/verify.go                      # `agent-receipts verify` subcommand
integration_test.go                        # tags: integration. End-to-end concurrency, peer-cred, and verify-CLI fixtures.
```
