Skip to content

ADR 0012 - Signed source identity

Status: Accepted

dent8 authority already provides a source -> authority ceiling: once an authority registry exists, a source cannot fact above its registered maximum. That is authz. It does not prove authn: a caller can still pass --source source:owner unless the write boundary verifies that the caller holds the key for that source.

A signed grant by itself is not enough. If the grant is a bearer token, anyone who can copy it can impersonate the source. The grant must bind the source id to a source public key, and the caller must prove possession of the matching private key on each write.

Add an opt-in signed source identity layer at the CLI/MCP write boundary.

  • An issuer key is the owner/admin authority. The operator keeps it outside the project/agent workspace and configures dent8 to trust its public key.
  • A source key belongs to one agent/source, for example source:codex or source:claude-code.
  • A signed source grant binds:
    • source id
    • source public key
    • maximum authority
    • issuer name
    • optional subject scope (* or exact <kind>:<key>)
    • optional expiration as Unix milliseconds
  • Every write first checks the existing source->authority ceiling, then, if identity trust is configured, verifies the signed grant and source-key possession before the candidate event reaches the firewall. Updated by ADR 0013: the original ephemeral per-write payload signature is replaced by a persisted signed write attestation in provenance.attestation, signed over the whole event at the append boundary and re-verifiable offline.
  • Signed identity’s configured-enforcement is opt-in like the authority registry: if a trust registry exists or DENT8_REQUIRE_IDENTITY=1, missing/invalid grant/key material fails closed.
  • Update (signing-required-above-agent, BREAKING): independently of that opt-in, a write whose effective authority is above the agent tier (strictly greater than lowmedium/high/ canonical) now always requires a valid signed identity; when unconfigured (or the grant is insufficient) such a write is rejected, not trusted. Agent-tier and below stay permissive with no signing. To keep the honest path working, dent8 init provisions a default signing identity by default (opt out with --no-identity). This closes the unauthenticated-label bypass where an unsigned --authority high --source source:human was believed on the label alone.

Signed source identity is included in the default CLI build. The secure onboarding path is:

Terminal window
dent8 init --agent codex --install-mcp
set -a
. .dent8/env
. .dent8/identity-codex.env
set +a
dent8 doctor --source source:codex --write-check

For a custom source id, use:

Terminal window
dent8 init --identity --source source:codex

Manual commands remain available:

Terminal window
dent8 identity bootstrap --source source:codex
dent8 identity status --dir .dent8 --source source:codex
dent8 identity repair-env --dir .dent8 --source source:codex
dent8 identity rotate-source --dir .dent8 --source source:codex --issuer-key "$DENT8_ISSUER_KEY"
dent8 identity grant-verify .dent8/grants/source_codex.grant.json

bootstrap creates or reuses an operator issuer key outside the project bundle (--issuer-key, DENT8_ISSUER_KEY, $XDG_CONFIG_HOME/dent8/issuer.key, or $HOME/.config/dent8/issuer.key), then writes the project-local trust registry, active-grant registry, source key, grant, and .dent8/identity-<source>.env (for example .dent8/identity-codex.env). It refuses to place the issuer private key inside the project bundle. status reports bundle/trust/active-grant/grant/key/expiry health. repair-env rewrites generated .dent8/identity-<source>.env and restores a missing active-grant entry from the current signed grant after verifying trust, grant, and source key consistency; it does not rotate keys or issue a new grant. rotate-source generates a replacement source key and grant at the same active paths, updates .dent8/active-grants.json, and removes the previous private source-key backup after a successful rotation, so existing MCP config can keep pointing at .dent8/identity-<source>.env while old grant+key pairs are rejected at the write boundary. The lower-level issuer-keygen, agent-keygen, trust-add, and grant-issue commands remain available for custom layouts.

The default issuer key is scoped to the OS user, not the project: bootstrapping several projects with the default path reuses one owner/admin key. That is acceptable for the v0 local operator model because agents receive source keys and grants, not the issuer key. It also means the issuer key is a cross-project root of trust; operators who want project-level blast-radius isolation should pass a distinct --issuer-key outside each project workspace.

Agent runtime configuration:

Terminal window
DENT8_TRUST=.dent8/trust.json
DENT8_ACTIVE_GRANTS=.dent8/active-grants.json
DENT8_REQUIRE_IDENTITY=1
DENT8_GRANT=.dent8/grants/source_codex.grant.json
DENT8_IDENTITY_KEY=.dent8/identities/source_codex.key

This defends against:

  • an unregistered or low-trust agent posing as a higher-authority source;
  • copying a signed grant without also holding the source private key;
  • raising the requested authority above the grant ceiling;
  • using a grant outside its optional subject scope or after expiration;
  • tampering with the write payload before the boundary check.

This does not defend against:

  • a compromised source private key or malware running as the same OS user;
  • a malicious process that can read another agent’s private key file;
  • direct Postgres writes or a process calling the store adapter without the CLI/MCP boundary;
  • a compromised dent8 binary;
  • a bad grant issued by a trusted issuer;
  • history rewrites after append (that remains the witness layer).

Therefore v0 requires private signing keys to be owner-only (0600) on Unix. Stronger isolation later should use separate OS users, hardware-backed keys, macOS Keychain, 1Password/secret-store integration, or an external signer.

  • Multiple agents on one machine can be distinguished if each has a distinct source key and grant.
  • Multiple agents can use one globally installed dent8 binary and one shared operational store, but stdio MCP clients normally launch separate server subprocesses. That is fine: provenance comes from each subprocess’s grant/key env, and shared memory comes from the backend URL.
  • Teams decide grant levels explicitly. dent8 does not infer trust from agent brand names.
  • The local source->authority registry remains useful as a simple authz layer and a dev-mode bootstrap path; signed identity is the production authn layer above it.
  • MCP deployments should run one dent8 stdio server process per agent identity if they need per-agent source separation. A shared stdio MCP process can only prove the identity whose key it holds. The local Unix-socket daemon adds a per-connection challenge, but each daemon process still proves the single source key it holds; run separate daemon instances for distinct local identities. Future remote HTTP transport needs per-request source authentication without service-held user source keys.