ADR 0012 - Signed source identity
Status: Accepted
Context
Section titled “Context”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.
Decision
Section titled “Decision”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:codexorsource: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
low—medium/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 initprovisions a default signing identity by default (opt out with--no-identity). This closes the unauthenticated-label bypass where an unsigned--authority high --source source:humanwas believed on the label alone.
Commands
Section titled “Commands”Signed source identity is included in the default CLI build. The secure onboarding path is:
dent8 init --agent codex --install-mcpset -a. .dent8/env. .dent8/identity-codex.envset +adent8 doctor --source source:codex --write-checkFor a custom source id, use:
dent8 init --identity --source source:codexManual commands remain available:
dent8 identity bootstrap --source source:codexdent8 identity status --dir .dent8 --source source:codexdent8 identity repair-env --dir .dent8 --source source:codexdent8 identity rotate-source --dir .dent8 --source source:codex --issuer-key "$DENT8_ISSUER_KEY"dent8 identity grant-verify .dent8/grants/source_codex.grant.jsonbootstrap 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:
DENT8_TRUST=.dent8/trust.jsonDENT8_ACTIVE_GRANTS=.dent8/active-grants.jsonDENT8_REQUIRE_IDENTITY=1DENT8_GRANT=.dent8/grants/source_codex.grant.jsonDENT8_IDENTITY_KEY=.dent8/identities/source_codex.keySecurity properties
Section titled “Security properties”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.
Consequences
Section titled “Consequences”- Multiple agents on one machine can be distinguished if each has a distinct source key and grant.
- Multiple agents can use one globally installed
dent8binary 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.