gabriel / muse public
agent-provenance.md markdown
201 lines 7.2 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago

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/store.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 First 16 hex chars of SHA-256(public key bytes) — for logging
format_version Must be 7 for a signed commit

Signing model (format_version 7)

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/keys/localhost_10003.pem       # one key per hub hostname
~/.muse/identity.toml:
    ["localhost:10003"]
    type = "human"
    handle = "gabriel"
    key_path = "/Users/gabriel/.muse/keys/localhost_10003.pem"

Agent-dedicated key (on-disk, long-lived)

~/.muse/keys/localhost_10003__agentception-abc123.pem    # hostname__agent_id
~/.muse/identity.toml:
    ["localhost:10003#agentception-abc123"]               # compound key
    type = "agent"
    handle = "agentception-abc123"
    key_path = "/Users/gabriel/.muse/keys/localhost_10003__agentception-abc123.pem"
    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/<pid>/environ and process listings):

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)

# 1. Generate the agent's keypair
muse auth keygen --hub http://localhost:10003 --agent-id agentception-abc123

# 2. Register with MuseHub (stores trust chain: spawned_by = gabriel)
muse auth register --hub http://localhost:10003 \
  --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)

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:

from muse.core.provenance import (
    sign_commit_ed25519, verify_commit_ed25519,
    public_key_fingerprint, encode_public_key,
    sign_commit_record, provenance_payload,
)

# 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

muse read <ref>                             # human-readable, includes provenance
muse read <ref> --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
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 generation, agent-specific paths, load_private_key_from_pem
muse/cli/config.py get_signing_identity, MUSE_AGENT_KEY_FD fd injection
muse/core/store.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
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 144 days ago