Agent Identity in SecondBrain — Design¶
Status: implementation in progress, 2026-05-17. Companion to
agent_identity_checklist.md. This doc audits the current identity surface, tracks landed pieces, and keeps the remaining phased implementation grounded in code. Industry context lives invault/05_playbooks/research/2026-05-16-agent-identity.md.
1. Working definition (reproduced)¶
Agent identity is the verifiable identity of a software or AI agent as an actor, distinct from the human user, organization, app, or runtime it uses. A useful agent identity must answer:
- Who is this agent?
- Who owns or authorized it?
- What human, org, tenant, or workflow is it acting for?
- What is it allowed to do, for how long, under what approval gates?
- Which key, workload, model/runtime, version, policy, or environment proves it is really that agent?
- How are its actions audited, revoked, rotated, and attributed?
2. Why now¶
- SB is repositioning as durable memory + grounded-citations substrate ([[substrate_pivot]]). Substrate consumers (other agents, MCP clients, remote A2A peers) need to verify they're really talking to a SecondBrain agent and that an answer was produced by the policy they expect.
- The codebase already has the pieces —
AgentManifest,A2AAgentCard,A2AEnvelope,A2ACardSigner,RunContext,automation_tokens,decisions/,policies/injection.py— but no integrated story. Each layer carries part of the answer and they don't share a vocabulary. - The deep-research note shows the industry is converging on the shape (unique per-agent identity, scoped tokens, signed claims, runtime attestation, traceable owners) but not on a single standard. We need an internally coherent model that we can later adapt to Microsoft Entra Agent ID, MCP/OAuth, SPIFFE/SPIRE, etc.
2.1 Landed implementation¶
The first implementation lives in brain/agent_identity/, not
brain/identity/. The existing brain/identity/ package is semantic
memory identity and lineage; agent identity is security/governance identity.
Shipped pieces:
brain/agent_identity/urn.py— canonical agent URNs in the formagent:secondbrain/<slug>@<version>.brain/agent_identity/principal.py—Principaland principal-chain helpers for users, services, agents, and automations.brain/agent_identity/scopes.py— initial scope vocabulary plus deterministic scope normalization, narrowing, andpermission:<name>mapping for legacyToolSpec.requires_permissionstrings.brain/agent_identity/claim.py— short-lived signedIdentityClaim(sbid/1) withiss/sub/aud/nbf/exp/kid/sigaliases, claim hashes, HMAC-SHA256 signatures, expiry checks, and child-claim narrowing.brain/agent_identity/context.py— trace-safe summaries andagent_identity_*event-log metadata.brain/agent_identity/audit.py— shared helper for writing consistentactor,session_id,trace_id, andagent_identity_*event metadata from either aRunContext/RunIdentityor a signed claim.brain/agent_identity/keys.py— local file-backed HMAC key material, create/load/rotate behavior, trusted previous-key verification for short-lived claims, and0600permissions where supported.brain/agent_identity/runtime.py— helpers to infer run subjects and principals fromRunContext, mint a run claim, and attach it.brain/agent_builder/models.py— optionalOwnershipSpec,AgentLifecycle, andidentity_scopesmanifest fields plus the computed canonicalAgentManifest.agent_urn. Manifest-aware minting refusesrevokedmanifests and narrows requested scopes to the manifest ceiling when one is declared.brain/agent_builder/agent.py— newly generated AgentBuilder manifests receive default ownership from the requested author or local operator while older manifests remain loadable without ownership.brain/agent_builder/registry.py,validation.py, andruntime.py— revoked manifests remain inspectable by id, but are filtered from active listing/intent discovery, rejected as subagent dependencies, skipped by registry-to-card sync, removed from the local A2A card catalog, and refused by local A2A dispatch/execution.brain/agent_builder/models.py—A2AEnvelopenow carries normalizedon_behalf_ofprincipal-chain metadata and an optional serializedidentity_claimfor delegation provenance.brain/a2a/models.py,brain/agent_builder/agent.py, andbrain/agent_builder/runtime.py— A2A cards carryagentUrnand identity metadata; both direct AgentBuilder creation and registry-to-card sync now derive the card URN from the manifest.brain/agent_builder/runtime.py— local A2A dispatch propagates identity metadata from request/message metadata into the persisted envelope, auto-mints a child identity claim when a verified parent claim is supplied, narrows child scopes, and includes claim hash/subject ona2a_send_messageevent-log rows when present.brain/a2a/grpc_server.py— gRPCSendMessageandSendStreamingMessageremain backward-compatible for unsigned local callers, but verify any suppliedidentity_claim, requirea2a:sendon signed callers, reject mismatched tenant metadata, and can be configured withrequire_identity_claim=Truefor strict ingress.brain/a2a/card_signing.py— card signing now supports active/trusted key rotation while preserving verification for cards signed with the previous key.brain/agent_identity/run_identity.py—RunIdentity, the single run-local identity value object. Boundary hints such asuser_sidandactorare normalized into a principal chain instead of stored as parallel authority.brain/kernel/run_context.py— stores oneRunIdentity, exposes derivedactor/user_sidcompatibility accessors, serializes identity under a singleidentitykey withactive_actor/root_user_id, and delegates claim scope/audit helpers toRunIdentity.brain/kernel/tooling/permissions.py— opt-inToolPolicy(..., identity_required=True)scope enforcement.brain/kernel/tooling/executor.py— passes attached identity scopes into policy evaluation and includes identity metadata in tool-call audit rows.brain/serve/routers/n8n.py— governed n8n inbound tool calls now mint a local run identity claim, logagent_identity_*metadata, and execute withidentity_required=True.brain/agent_sdk/runner.py—SecondBrainRunnercan mint or accept a signed local identity claim, inject a trace-safe identity block into the model prompt, expose the serialized claim through subprocess environment variables, and mint a narrower child claim forfork_session=True.brain/cli/operator_identity.py— top-levelsb agent-identityoperations for local revocation, key rotation, and trace lookup:revoke,rotate-key, andtrace.
Most runtime behavior remains unchanged: identity is attached and enforced only where a caller opts in. The first opted-in ingress is n8n inbound tool execution because it is already allowlisted, approval-gated, and externally reachable.
3. Current state — per question¶
Q1: Who is this agent?¶
Where answered:
brain/agent_builder/models.py:129—AgentManifest{id, name, slug, version};iddefaults toagent-<uuid8>,slugderived from name.brain/a2a/models.py:392—A2AAgentCard{agent_id, name, version, provider, supported_interfaces, …}; doc-comment callsagent_id"a local registry key."brain/a2a/models.py:575—A2AAgentCard.local()synthesises endpoint asinternal://a2a/{agent_id}when none is given.brain/kernel/run_context.py:168—RunContext.agent_name(string) is the runtime handle.
What works: stable slug + version + manifest id; canonical URN helper;
protocol-aligned card with agentUrn and identity metadata.
Gaps / partials:
- Canonical URNs now exist and registry-to-card sync uses them, but older entrypoints still accept freeform strings for backward compatibility.
- Version is on the manifest and the card but not threaded into
RunContext.agent_name, so the event log can't tell which version ofmeeting_copilotproduced a result.
Q2: Who owns or authorized it?¶
Where answered:
brain/agent_builder/models.py:141—AgentManifest.author: str | None— a freeform string.brain/a2a/models.py:366—A2AAgentProvider{organization, url}on the card.
Gaps / partials:
OwnershipSpecnow modelsowner_id,owner_kind,tenant_id,created_by,signing_key_id, and lifecycle. It is optional for backward compatibility, so older manifests can still be loaded. New AgentBuilder output defaults ownership from the requested author orlocal-operator.- Manifest-aware identity minting refuses
revokedmanifests, and AgentBuilder registry/runtime paths filter or reject revoked agents. Operator revocation archives the manifest and removes the local A2A card. Runtime entrypoints that mint from onlyRunContextstill do not consult a manifest lifecycle unless they have a manifest context. - A2A card has
signaturesbut no formal binding of "this signer attests to ownership."
Q3: What human/org/tenant/workflow is it acting for?¶
Where answered:
brain/kernel/run_context.py—RunContext.identity: RunIdentity,workspace: str | None,intent: str. Per-run.brain/a2a/models.py:186—A2APushNotificationConfig.tenant; several A2A request models carrytenant: str | None.brain/agent_builder/models.py—A2AEnvelope{from_agent_id, to_agent_id, conversation_id, on_behalf_of, identity_claim}.
Gaps / partials:
Principaland principal-chain helpers now exist inbrain/agent_identity/principal.py;RunContextnow stores identity throughRunIdentity, with no top-leveluser_sid/actorauthority fields. Runtime minting prefers the identity chain before falling back to automation/user/service inference for non-RunContext callers.- Local
A2AEnvelopeinstances preserveon_behalf_ofandidentity_claimmetadata when callers provide it through request or message metadata.AgentRuntimenow auto-mints a child claim when a verified parent claim is present.SecondBrainRunner.fork_sessionalso mints a narrower child claim when given a parent claim and local key store. Other non-SDK fork/session paths still need the same treatment. - Tenants are sprinkled across A2A request models but not unified in a single place enforced by the runtime.
- Serialized
RunContext.to_dict()intentionally emits identity under one nestedidentitykey. Compatibility accessors remain in process, but new serialized consumers should readidentity.active_actorandidentity.root_user_idinstead of top-levelactor/user_sid.
Q4: What is it allowed to do, for how long, under what approval gates?¶
Where answered:
brain/agent_builder/models.py:102—ToolsSpec{mcp_servers, tool_allowlist, tool_policies}on the manifest.brain/agent_builder/models.py:94—RuntimeSpec{max_turns, token_budget, timeout_s}.brain/kernel/run_context.py:52—RunBudget{max_tool_calls, max_cost_usd, max_latency_ms, thinking_budget_tokens}enforced atomically (claim_tool_call()).brain/kernel/run_context.py:33—SafetyMode{SAFE, NORMAL, STRICT};is_safe_mode()toggles classes of tools.brain/kernel/tooling/permissions.py+executor.py— centralToolPolicy/ToolExecutorevaluating before each call.tests/agents/test_research_brief_agent.py:34— contracts already carry anapproval_tier("gate"vs auto) and atool_allowlist.brain/policies/injection.py:46—identity_overriderule treats identity as a defended boundary.
What works: strong runtime budget + safety-mode story; manifest-level allowlist; per-intent approval tier on contracts.
Gaps / partials:
- A formal initial scope vocabulary now exists.
ToolPolicycan require attached claim scopes withidentity_required=True, and tool safety classes map totools:read,tools:write,tools:network, ortools:destructive. IdentityClaimnow hasnbfandexp; default TTL is five minutes.- Enforcement is currently opt-in in kernel tooling. n8n inbound tool
execution opts in and mints a local claim from the serve state directory.
gRPC A2A ingress verifies caller claims when they are supplied, requires
a2a:sendon signed callers, and can be configured to require claims for every caller.
Q5: Which key/workload/model/runtime/version/policy/environment proves it is really that agent?¶
Where answered:
brain/a2a/card_signing.py:1—A2AAgentCardSignersigns cards with HMAC-SHA256 (JWS-style), key stored at<state>/a2a_card_signing_key.json. Verification viaverify_card.brain/a2a/models.py:426—A2AAgentCard.signatures: list[dict]holds the signed envelope.brain/automation_tokens.py— hashed invoke-tokens for automations (per-automation, plaintext returned once, SHA-256 stored).brain/kernel/run_context.py:182-183—RunContext.provider,modelare present but not signed into anything.
What works: card signing is real, verifiable, and key-rotatable with an active key plus trusted previous keys.
Gaps / partials:
- Per-run
IdentityClaimnow exists and includesrun_id, subject, principal chain, scopes, provider/model, approval mode, permission mode, policy profile, expiry, key id, claim hash, and signature. - The signing key is shared per workspace; there's no agent-specific signing key bound to the manifest.
- Local identity key material is currently workspace-scoped and file-backed. Rotation preserves trusted previous keys for verification of short-lived claims, but there is not yet a manifest ownership key binding.
Q6: How are actions audited, revoked, rotated, and attributed?¶
Where answered:
brain/decisions/—decision_keynamespace (a.b.c),evidence_refs,support.research.briefstyle intents.EventLog.log_action(action_type, description, input_data, output_data)used everywhere (incl. the newresearch_deep).RunContext.to_dict()emits a full snapshot including budget, working_set, IDs, and a single nestedidentityobject.brain/automation_tokens.pysupports rotation by re-issuing a token and invalidating the old hash.
Gaps / partials:
- Manifest lifecycle revocation is enforced for AgentBuilder registry
discovery, subagent validation, local A2A dispatch/execution, card sync,
and manifest-aware claim minting.
sb agent-identity revokeis the local operator path; a separate revocation list for compromised individual keys/claims is still future work. - Local identity-key and card-signing key rotation are exposed through
sb agent-identity rotate-key; scheduled deprecation of trusted previous keys is still future work. - Kernel tool-call audit rows, A2A sends, n8n inbound calls, and SDK run
events include
agent_identity_*metadata when a claim is attached. Other event-log call sites still need incremental wiring. sb agent-identity tracereconstructs identity-bearing event/tool-call rows from local metadata. It is intentionally event-log scoped; richer decision/provenance graph reconstruction is still future work.
4. Gap summary¶
| # | Gap | Severity | Phase |
|---|---|---|---|
| A | Canonical agent URN wired into manifests/cards; older freeform entrypoints remain | Low | 1 |
| B | Manifest ownership defaults for new AgentBuilder output; legacy/manual manifests can still omit it | Med | 1 |
| C | Principal exists; A2A and SDK fork propagation landed; other fork/session paths remain |
Low | 2 |
| D | A2AEnvelope.on_behalf_of exists; gRPC signed ingress enforces claims/scopes when supplied |
Low | 2 |
| E | Scope vocabulary exists; broad runtime adoption remains opt-in | Med | 3 |
| F | Signed IdentityClaim exists; n8n, A2A runtime, and SDK runner minting landed; broader entrypoints remain |
Low | 3 |
| G | Manifest lifecycle revocation and operator CLI landed; no per-claim/key revocation list yet | Low | 4 |
| H | Key rotation CLI landed; trusted-key deprecation policy pending | Low | 4 |
| I | Event log carries identity for kernel tools, A2A, n8n, and SDK runs; remaining call sites are incremental | Med | 4 |
5. Proposed implementation¶
Designed to land in four phases, each independently shippable and behind-the-existing-APIs additive (no breakage of MeetingCopilot, A2A queue, or AgentBuilder).
Phase 1 — Naming + ownership (gaps A, B)¶
Files: brain/agent_builder/models.py, brain/agent_identity/.
Done:
- Added
brain/agent_identity/urn.py— canonicalAgentURNparser/formatter:agent:secondbrain/<slug>@<version>(e.g.agent:secondbrain/meeting-copilot@1.1.0). Pydantic validator andfrom_manifest()helper. - Added
OwnershipSpec,AgentLifecycle, andidentity_scopestoAgentManifest.identity_scopesnormalize deterministically and manifest-aware minting refusesrevokedmanifests. - Added computed
AgentManifest.agent_urnand wired it into A2A card creation/sync. - New AgentBuilder-created manifests receive default
OwnershipSpecownership fromauthororlocal-operator.
Remaining:
- Decide when ownership becomes required for hand-authored manifests loaded from disk.
- Continue replacing freeform runtime strings at newer call boundaries with canonical agent URNs.
Phase 2 — Principal + delegation chain (gaps C, D)¶
Files: brain/agent_identity/principal.py, brain/kernel/run_context.py,
brain/agent_builder/models.py, brain/a2a/models.py.
Done:
PrincipalPydantic model:- Principal-chain helpers (
principal_chain_from_dicts,extend_principal_chain,principal_chain_for_context) preserve ordered, oldest-first chains and enforce depth ceilings. RunIdentitystores the normalized ordered chain and optional signed claim as one coherent identity object.RunContextowns exactly oneRunIdentity; mismatched explicit chains and signed claims fail visibly.A2AEnvelope.on_behalf_ofandA2AEnvelope.identity_claimpreserve delegation provenance through local A2A queue payloads.AgentRuntimeauto-mints a verified child claim for local A2A dispatch when a parent claim includesagent:spawn; if a manifest has no explicitidentity_scopes, the child inherits parent scopes exceptagent:spawn.SecondBrainRunner.fork_sessionmints a fresh child claim from a verified parent claim, removesagent:spawnfrom the child's scope set, injects trace-safe identity metadata into the prompt, and exposes the serialized claim to the spawned process via environment variables.
Remaining:
- Keep replacing remaining call sites that pass
user_sid/actorboundary hints with explicitPrincipalvalues where the caller already knows the actor type. - Tests: tenant boundary refusal beyond gRPC ingress and any non-SDK fork path that later gains claim propagation.
Phase 3 — Scopes + per-run IdentityClaim (gaps E, F)¶
Files: brain/agent_identity/scopes.py, brain/agent_identity/claim.py,
brain/agent_builder/models.py, brain/kernel/tooling/permissions.py,
brain/a2a/grpc_server.py.
Done:
- Scope vocabulary as an enum:
Kernel scope requirements currently derive from
class Scope(str, Enum): MEMORY_READ = "memory:read" MEMORY_WRITE = "memory:write" WEB_FETCH = "web:fetch" WEB_SEARCH = "web:search" EMAIL_SEND = "email:send" DECISION_RECORD = "decision:record" AGENT_FORK = "agent:fork" SPEND_USD_1 = "spend:usd<=1" # ...ToolSpec.safety_class,ToolSpec.requires_permission, and optionalmetadata["required_identity_scopes"]. IdentityClaim:Implemented asclass IdentityClaim(BaseModel): version: Literal["sbid/1"] = "sbid/1" agent_urn: str # who principal_chain: list[Principal] # for whom scopes: list[Scope] # what not_before: str # iso8601 not_after: str # iso8601 (≤ run timeout + slack) run_id: str provider: str | None model: str | None policy_profile: str | None claim_hash: str # sha256(canonical_json) signature: str # signed by ownership.signing_key_id key_id: strIdentityClaimwith JWT-like aliases and HMAC-SHA256 integrity.RunContextcan attach a serialized claim andToolExecutoruses it for observe-only audit metadata or opt-in scope enforcement.- Local key material and mint helpers:
LocalIdentityKeyStore,local_identity_issuer,mint_run_identity, andmint_run_identity_from_key_store. - gRPC A2A ingress verifies supplied claims against the local identity key,
checks expiry/signature through
IdentityClaim.verify, requiresa2a:send, rejectson_behalf_of/claim-chain mismatches, refuses tenant mismatches, and has a strictrequire_identity_claimmode.
Remaining:
- Automatic minting at run start for more selected entrypoints. n8n inbound,
local A2A dispatch, and
SecondBrainRunnerare the first wired surfaces. - Decide which A2A callers must supply claims instead of accepting backward-compatible unsigned requests.
- Tests: scope-narrowing on future non-SDK fork/session paths outside AgentRuntime.
Phase 4 — Revocation, rotation, attribution loop (gaps G, H, I)¶
Files: brain/agent_identity/audit.py, brain/a2a/card_signing.py,
brain/state/event_log.py, brain/cli/operator_identity.py.
Done:
- Operator revocation:
sb agent-identity revoke <agent> --reason "..."flips manifest lifecycle torevoked, archives the registry status, deletes the local A2A card, and emits anagent_identity.revokedevent. - Operator rotation:
sb agent-identity rotate-key [--a2a-card]rotates the local identity signing key, optionally rotates the A2A card-signing key, preserves trusted previous keys locally, and emitsagent_identity.key_rotated. - Event-log uniformity:
identity_audit_log_kwargs(...)writes consistentactor,session_id,trace_id, andagent_identity_*metadata. Kernel tool calls, A2A dispatch, n8n inbound execution, and SDK run logging use the helper or its underlying metadata contract. - Attribution CLI:
sb agent-identity tracereads local event/tool-call metadata and filters by agent URN, trace id, or session id.
Remaining:
- Per-claim/key revocation list for compromised keys or claims that must be refused before natural expiry.
- Trusted-previous-key deprecation schedule after rotation.
- Incremental adoption of the audit helper by event-log call sites outside kernel tooling, A2A, n8n, and SDK runs.
6. What we are not doing yet¶
- Asymmetric (Ed25519 / X.509) signing. HMAC is enough for a single
workspace; multi-organization deployments will need a swap-in but the
surface (
A2ACardSigner,IdentityClaim.signature) is the same. - Federation with external identity providers (Entra Agent ID,
SPIFFE/SPIRE, MCP OAuth). The
agent_urn+ scope vocabulary + claim shape is intentionally close enough to map later. - UI for browsing identities. The CLI surface in Phase 4 is enough for an operator workflow; a UI can come once the data is stable.
7. Risk register¶
| Risk | Mitigation |
|---|---|
| Adding required fields breaks existing manifests | All new fields default to None/empty; loader emits warnings, not errors, until a deprecation pass |
| Per-run claim minting adds latency | Signing is HMAC-SHA256 and expected to be negligible; keep automatic minting targeted until entrypoints are audited |
| Scope vocabulary explodes | Start with the ~10 enum values listed. Adding scopes is cheap; removing them is not — review additions like any public API |
| Delegation chain becomes a leak channel | Principal.tenant_id propagates; A2A ingress refuses cross-tenant chains unless explicitly allowed by manifest |
| Card-signing key compromise | Local rotation is implemented; external surfaces still need operator CLI/docs and deprecation policy |
8. Open questions¶
Mirror of the deep-research note's open questions, scoped to SB:
- What
signing_key_idformat should we adopt — workspace-scoped UUID, or owner-rooted (tenant:<id>:key:<n>)? - Should the
IdentityClaimbe embeddable in MCP responses (so an MCP consumer can verify "this answer came from a SecondBrain agent under this policy") or kept internal? - How do we represent delegation chains crossing organizations — do we lock to single-tenant until federation is on the roadmap?
- Which subsystems should enable
ToolPolicy(identity_required=True)first (likely: external connectors,email, web fetch to allow-listed domains,decision.record) and which should remain claim-optional in dev (likely: local read-only memory/vault reads)?
9. Tracking¶
- Checklist:
agent_identity_checklist.md. - Memory:
[[agent-identity-definition]](user's canonical framing),[[deep-research-2026-05-16]](industry context source). - Vault:
vault/05_playbooks/research/2026-05-16-agent-identity.md. - This doc updates as phases land; each phase should ship with a section
here moving "Proposed" → "Done @
".