daemon

package module
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 19, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

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 for design rationale and issue #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

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.

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

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:

{
  "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.

Documentation

Overview

Package daemon assembles the agent-receipts-daemon's components — chain state, key source, receipt store, frame socket — into a single Run entrypoint. cmd/agent-receipts-daemon/main.go wraps Run with flag/env parsing and signal handling.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultDBPath

func DefaultDBPath() string

DefaultDBPath returns the per-user SQLite path used when AGENTRECEIPTS_DB is not set. Uses XDG_DATA_HOME (defaults to ~/.local/share on Linux/macOS).

func DefaultKeyPath

func DefaultKeyPath() string

DefaultKeyPath returns the per-user signing-key path used when AGENTRECEIPTS_KEY is not set. Uses XDG_DATA_HOME (defaults to ~/.local/share on Linux/macOS).

func DefaultPublicKeyPath

func DefaultPublicKeyPath(keyPath string) string

DefaultPublicKeyPath returns the default published public-key path: the same directory as keyPath with the suffix ".pub". Empty when keyPath is empty so cmd/main.go can surface a clearer "Config.KeyPath is required" error from validateConfig instead of a less-helpful PublicKeyPath one.

func DefaultSocketPath

func DefaultSocketPath() string

DefaultSocketPath returns the per-OS default socket path. Phase 1 resolves Q1 of issue #236:

  • macOS: $TMPDIR/agentreceipts/events.sock — per-user, unprivileged.
  • Linux with $XDG_RUNTIME_DIR set: $XDG_RUNTIME_DIR/agentreceipts/ events.sock — per-user, unprivileged.
  • Linux fallback (no $XDG_RUNTIME_DIR): /run/agentreceipts/events.sock — this is the system-install path and requires privileged directory creation/write. Unprivileged users on systems without $XDG_RUNTIME_DIR should set AGENTRECEIPTS_SOCKET explicitly.
  • Other platforms: empty string (the daemon refuses to start outside Linux/macOS, see Run).

func GenerateKey

func GenerateKey(keyPath, publicKeyPath string) error

GenerateKey creates a new Ed25519 key pair and saves the private key to keyPath (mode 0600) and public key to publicKeyPath (mode 0644). Refuses to overwrite an existing file at either path, and refuses to follow a symlink at either path. Use this explicitly via --init; never call it as a side-effect of starting the daemon — silently regenerating a missing key would invalidate every receipt previously signed by the operator's real key.

Atomicity: both files are created with O_CREATE|O_EXCL|O_NOFOLLOW so an attacker who plants a symlink (or any other dirent) at either path between the directory creation and the file open trips O_EXCL — we never write through the symlink target. If the public-key write fails after the private-key write succeeded, the private-key file is removed so the caller doesn't end up with a half-initialised on-disk state.

The mode passed to OpenFile may be narrowed by the process umask; an explicit fchmod after open ensures the on-disk mode matches what the caller asked for.

func Run

func Run(ctx context.Context, cfg Config) error

Run starts the daemon and blocks until ctx is cancelled. It returns the first fatal error or nil on graceful shutdown.

Types

type Config

type Config struct {
	// SocketPath is the Unix-domain socket the daemon listens on.
	SocketPath string

	// DBPath is the SQLite receipt-store path.
	DBPath string

	// KeyPath is the PEM-encoded Ed25519 private key path. Mode must be 0600.
	KeyPath string

	// PublicKeyPath is where the daemon publishes the matching SPKI public
	// key in PEM form, mode 0644, on every startup. Read-side tools
	// (`agent-receipts verify`) load it without needing access to KeyPath or
	// the daemon's signing surface. Defaults to KeyPath + ".pub" when empty.
	PublicKeyPath string

	// ChainID is the chain id all incoming frames are written under. Phase 1
	// supports one chain per daemon process.
	ChainID string

	// IssuerID is embedded in receipts as issuer.id, e.g.
	// "did:agent-receipts-daemon:<host>".
	IssuerID string

	// VerificationMethodID goes into proof.verificationMethod.
	VerificationMethodID string

	// Logger receives daemon log lines. Defaults to log.Default().
	Logger *log.Logger

	// TraceLog optionally receives daemon trace lines for test debugging.
	// When nil, tracing is silent. Tests can pass a buffer to inspect
	// what frames were received, receipts signed, etc.
	TraceLog io.Writer

	// ParameterDisclosure controls whether plaintext tool input and output are
	// included in the parameters_disclosure receipt field. Disabled by default.
	// WARNING: stores unredacted payloads — see issue #280 for the encrypted
	// design that supersedes this flag.
	ParameterDisclosure bool

	// RedactPatternsPath is an optional path to a YAML file of additional
	// redaction patterns applied to receipt body fields after hashing. When
	// empty, only the built-in patterns are used. File format:
	//
	//   patterns:
	//     - name: my-secret
	//       pattern: 'MY_SECRET_[A-Z0-9]+'
	RedactPatternsPath string
}

Config is the daemon's startup configuration. Resolve from flags/env in cmd/agent-receipts-daemon/main.go and pass to Run.

Directories

Path Synopsis
cmd
agent-receipts command
Command agent-receipts is the daemon's read-side companion CLI.
Command agent-receipts is the daemon's read-side companion CLI.
agent-receipts-daemon command
Command agent-receipts-daemon runs the receipts daemon: a single OS-user process that owns the Ed25519 signing key and the SQLite receipt store, and receives fire-and-forget event frames from emitters over a Unix-domain socket.
Command agent-receipts-daemon runs the receipts daemon: a single OS-user process that owns the Ed25519 signing key and the SQLite receipt store, and receives fire-and-forget event frames from emitters over a Unix-domain socket.
internal
chain
Package chain owns the daemon's in-memory chain state — the next sequence number and the previous-receipt-hash for each chain id.
Package chain owns the daemon's in-memory chain state — the next sequence number and the previous-receipt-hash for each chain id.
keysource
Package keysource defines the interface the daemon uses to sign receipts.
Package keysource defines the interface the daemon uses to sign receipts.
listcli
Package listcli implements the `agent-receipts list` subcommand: query recent receipts from a daemon-written SQLite store and print them in tabular or JSON form.
Package listcli implements the `agent-receipts list` subcommand: query recent receipts from a daemon-written SQLite store and print them in tabular or JSON form.
pipeline
Package pipeline maps an emitter frame plus an OS-attested peer credential into a signed AgentReceipt and persists it to the store.
Package pipeline maps an emitter frame plus an OS-attested peer credential into a signed AgentReceipt and persists it to the store.
socket
Package socket owns the daemon's Unix-domain-socket listener, the per-OS peer-credential capture, and the length-prefix message framing.
Package socket owns the daemon's Unix-domain-socket listener, the per-OS peer-credential capture, and the length-prefix message framing.
sockettest
Package sockettest provides shared helpers for AF_UNIX socket tests.
Package sockettest provides shared helpers for AF_UNIX socket tests.
verifycli
Package verifycli implements the `agent-receipts verify` subcommand: validate a stored chain's signatures and hash links using a daemon-written SQLite store and the daemon-published public key.
Package verifycli implements the `agent-receipts verify` subcommand: validate a stored chain's signatures and hash links using a daemon-written SQLite store and the daemon-published public key.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL