# Agent Provenance in Muse ## Overview Muse is built for the agent-first world. Agents and humans are equal first-class cryptographic principals. Every commit carries structured provenance — **who wrote this commit, with which model, using which toolchain, and can that attribution be verified cryptographically?** ## Principals and trust chains ``` Human operator (gabriel) ↓ provisions at spawn time Agent (agentception-abc123) ↓ signs each commit with its own keypair CommitRecord.signature ← Ed25519, embedded public key, offline-verifiable ``` Trust is established at **provisioning time**, not commit time. When an agent is spawned, the operator registers its dedicated keypair with MuseHub via `POST /api/identities/agent`. The server sets `spawned_by = operator.handle` and stores the trust chain. Every subsequent commit signed by the agent's key carries that chain back to the operator. ## Commit-level fields `CommitRecord` in `muse/core/commits.py` carries optional agent provenance fields: | Field | Description | |-------|-------------| | `agent_id` | Stable identifier for the agent or human (`"agentception-abc123"`, `"gabriel"`) | | `model_id` | AI model version used (`"claude-sonnet-4-6"`) | | `toolchain_id` | Agent framework version (`"agentception/v1"`) | | `prompt_hash` | SHA-256 of the system prompt (no raw text stored) | | `signature` | Base64url-encoded Ed25519 signature (86 chars) over the provenance payload | | `signer_public_key` | Base64url-encoded raw Ed25519 public key (43 chars) — embedded for offline verification | | `signer_key_id` | `sha256:<64-hex>` fingerprint of the raw public key bytes — for offline verification and logs | | `format_version` | Must be 8 for a signed commit | ## Signing model (format_version 8) Signatures use **Ed25519** (RFC 8032 / FIPS 186-5). The signed input is a SHA-256 digest binding the commit's identity to its authorship claims: ``` provenance_payload = SHA-256( commit_id \0 author \0 agent_id \0 model_id \0 toolchain_id \0 prompt_hash ) ``` Any post-signing mutation of `author`, `agent_id`, `model_id`, `toolchain_id`, or `prompt_hash` is detected by `muse verify`. The `signer_public_key` field embedded in the commit record allows anyone to verify without access to the private key or any external service (offline verification). ## Key storage ### Human operator key ``` ~/.muse/identity.toml: ["localhost:1337"] type = "human" handle = "gabriel" algorithm = "ed25519" fingerprint = "sha256:abc123..." hd_path = "m/1075233755'/0'/0'/0'/0'/0'" ``` ### Agent-dedicated key (HD-derived, in memory) ``` ~/.muse/identity.toml: ["localhost:1337#agentception-abc123"] # compound key type = "agent" handle = "agentception-abc123" algorithm = "ed25519" fingerprint = "sha256:def456..." hd_path = "m/1075233755'/0'/0'/0'/1'/0'" provisioned_by = "gabriel" capabilities = ["push", "pull"] ``` ### Ephemeral agent key (fd injection) For short-lived agent runs, agentception derives a 64-byte sub-seed and injects it via a kernel pipe file descriptor — never via an environment variable (env vars are visible in `/proc//environ` and process listings): ```python r_fd, w_fd = os.pipe() os.write(w_fd, sub_seed_64_bytes) os.close(w_fd) env = {**os.environ, "MUSE_AGENT_KEY_FD": str(r_fd), "MUSE_AGENT_HANDLE": "agentception-abc123"} subprocess.Popen(["claude-code", ...], env=env, pass_fds=(r_fd,)) os.close(r_fd) ``` `get_signing_identity()` reads `MUSE_AGENT_KEY_FD` first (highest priority), derives the Ed25519 key via SLIP-0010, then falls back to the identity store. ## Provisioning a dedicated agent key ### On-disk agent key (operator workflow) ```bash # 1. Generate the agent's keypair muse auth keygen --hub https://localhost:1337 --agent-id agentception-abc123 # 2. Register with MuseHub (stores trust chain: spawned_by = gabriel) muse auth register --hub https://localhost:1337 \ --agent-id agentception-abc123 \ --handle agentception-abc123 \ --provisioned-by gabriel # 3. Sign commits as the agent muse commit -m "feat: add harmony voice" \ --agent-id agentception-abc123 \ --model-id claude-sonnet-4-6 \ --sign ``` ### Ephemeral agent key (agentception workflow) ```python from agentception.services.agent_identity import register_agent_identity identity = await register_agent_identity( run_id="abc123", model_id="claude-sonnet-4-6", scope=["push:agentception"], ) # Inject into Claude Code subprocess via pipe fd, not env var r_fd, w_fd = os.pipe() os.write(w_fd, identity.sub_seed_bytes) os.close(w_fd) env = {**os.environ, "MUSE_AGENT_KEY_FD": str(r_fd), "MUSE_AGENT_HANDLE": identity.handle} subprocess.Popen(["claude-code", ...], env=env, pass_fds=(r_fd,)) os.close(r_fd) ``` ## Signing resolution order (commit time) `get_signing_identity(repo_root, agent_id=...)` tries in this order: 1. `MUSE_AGENT_KEY_FD` — 64-byte sub-seed via pipe fd (only supported env injection) 2. Agent-specific entry `"hostname#agent_id"` in `~/.muse/identity.toml` 3. Human entry `"hostname"` in `~/.muse/identity.toml` (fallback for operators) ## Signing and verification API `muse/core/provenance.py` provides: ```python from muse.core.provenance import ( sign_commit_ed25519, verify_commit_ed25519, encode_public_key, sign_commit_record, provenance_payload, ) from muse.core._types import public_key_fingerprint # Sign the provenance payload. payload = provenance_payload(commit_id, agent_id="agentception-abc123", author="gabriel") sig = sign_commit_ed25519(payload, private_key) # Embed the public key in the commit for offline verification. raw_bytes, pub_b64 = encode_public_key(private_key) fprint = public_key_fingerprint(raw_bytes) # Verify (offline — no hub required). assert verify_commit_ed25519(payload, sig, raw_bytes) # Convenience: sign a commit in one call. sig, pub_b64, fprint = sign_commit_record( commit_id, "agentception-abc123", private_key, author="gabriel", model_id="claude-sonnet-4-6", ) ``` ## Querying provenance ```bash muse read # human-readable, includes provenance muse read --json # machine-readable full record muse log --format json | jq '.[] | .agent_id' muse shortlog --by agent # group commits by agent muse code lineage "file.py::Symbol" # provenance chain of a specific symbol muse verify # verify all Ed25519 signatures in history ``` ## Related files | File | Role | |------|------| | `muse/core/provenance.py` | Ed25519 signing/verification, `provenance_payload` | | `muse/core/identity.py` | Identity store, compound key support, `resolve_signing_identity` | | `muse/core/keypair.py` | Key derivation (`derive_hd_public_info`), `sign_bytes` | | `muse/cli/config.py` | `get_signing_identity`, `MUSE_AGENT_KEY_FD` fd injection | | `muse/core/commits.py` | `CommitRecord`, `CommitDict` — provenance fields | | `muse/core/verify.py` | `run_verify` — Ed25519 signature verification | | `tests/test_agent_signing.py` | Agent signing unit/integration tests | | `tests/test_provenance.py` | Ed25519 primitives tests | | `tests/test_security_agent_impersonation.py` | Security/tamper-detection tests |