Skip to content

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 in vault/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:

  1. Who is this agent?
  2. Who owns or authorized it?
  3. What human, org, tenant, or workflow is it acting for?
  4. What is it allowed to do, for how long, under what approval gates?
  5. Which key, workload, model/runtime, version, policy, or environment proves it is really that agent?
  6. 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 piecesAgentManifest, 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 form agent:secondbrain/<slug>@<version>.
  • brain/agent_identity/principal.pyPrincipal and principal-chain helpers for users, services, agents, and automations.
  • brain/agent_identity/scopes.py — initial scope vocabulary plus deterministic scope normalization, narrowing, and permission:<name> mapping for legacy ToolSpec.requires_permission strings.
  • brain/agent_identity/claim.py — short-lived signed IdentityClaim (sbid/1) with iss/sub/aud/nbf/exp/kid/sig aliases, claim hashes, HMAC-SHA256 signatures, expiry checks, and child-claim narrowing.
  • brain/agent_identity/context.py — trace-safe summaries and agent_identity_* event-log metadata.
  • brain/agent_identity/audit.py — shared helper for writing consistent actor, session_id, trace_id, and agent_identity_* event metadata from either a RunContext/RunIdentity or 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, and 0600 permissions where supported.
  • brain/agent_identity/runtime.py — helpers to infer run subjects and principals from RunContext, mint a run claim, and attach it.
  • brain/agent_builder/models.py — optional OwnershipSpec, AgentLifecycle, and identity_scopes manifest fields plus the computed canonical AgentManifest.agent_urn. Manifest-aware minting refuses revoked manifests 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, and runtime.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.pyA2AEnvelope now carries normalized on_behalf_of principal-chain metadata and an optional serialized identity_claim for delegation provenance.
  • brain/a2a/models.py, brain/agent_builder/agent.py, and brain/agent_builder/runtime.py — A2A cards carry agentUrn and 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 on a2a_send_message event-log rows when present.
  • brain/a2a/grpc_server.py — gRPC SendMessage and SendStreamingMessage remain backward-compatible for unsigned local callers, but verify any supplied identity_claim, require a2a:send on signed callers, reject mismatched tenant metadata, and can be configured with require_identity_claim=True for 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.pyRunIdentity, the single run-local identity value object. Boundary hints such as user_sid and actor are normalized into a principal chain instead of stored as parallel authority.
  • brain/kernel/run_context.py — stores one RunIdentity, exposes derived actor/user_sid compatibility accessors, serializes identity under a single identity key with active_actor/root_user_id, and delegates claim scope/audit helpers to RunIdentity.
  • brain/kernel/tooling/permissions.py — opt-in ToolPolicy(..., 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, log agent_identity_* metadata, and execute with identity_required=True.
  • brain/agent_sdk/runner.pySecondBrainRunner can 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 for fork_session=True.
  • brain/cli/operator_identity.py — top-level sb agent-identity operations for local revocation, key rotation, and trace lookup: revoke, rotate-key, and trace.

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:129AgentManifest{id, name, slug, version}; id defaults to agent-<uuid8>, slug derived from name.
  • brain/a2a/models.py:392A2AAgentCard{agent_id, name, version, provider, supported_interfaces, …}; doc-comment calls agent_id "a local registry key."
  • brain/a2a/models.py:575A2AAgentCard.local() synthesises endpoint as internal://a2a/{agent_id} when none is given.
  • brain/kernel/run_context.py:168RunContext.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 of meeting_copilot produced a result.

Q2: Who owns or authorized it?

Where answered:

  • brain/agent_builder/models.py:141AgentManifest.author: str | None — a freeform string.
  • brain/a2a/models.py:366A2AAgentProvider{organization, url} on the card.

Gaps / partials:

  • OwnershipSpec now models owner_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 or local-operator.
  • Manifest-aware identity minting refuses revoked manifests, 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 only RunContext still do not consult a manifest lifecycle unless they have a manifest context.
  • A2A card has signatures but 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.pyRunContext.identity: RunIdentity, workspace: str | None, intent: str. Per-run.
  • brain/a2a/models.py:186A2APushNotificationConfig.tenant; several A2A request models carry tenant: str | None.
  • brain/agent_builder/models.pyA2AEnvelope{from_agent_id, to_agent_id, conversation_id, on_behalf_of, identity_claim}.

Gaps / partials:

  • Principal and principal-chain helpers now exist in brain/agent_identity/principal.py; RunContext now stores identity through RunIdentity, with no top-level user_sid/actor authority fields. Runtime minting prefers the identity chain before falling back to automation/user/service inference for non-RunContext callers.
  • Local A2AEnvelope instances preserve on_behalf_of and identity_claim metadata when callers provide it through request or message metadata. AgentRuntime now auto-mints a child claim when a verified parent claim is present. SecondBrainRunner.fork_session also 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 nested identity key. Compatibility accessors remain in process, but new serialized consumers should read identity.active_actor and identity.root_user_id instead of top-level actor/user_sid.

Q4: What is it allowed to do, for how long, under what approval gates?

Where answered:

  • brain/agent_builder/models.py:102ToolsSpec{mcp_servers, tool_allowlist, tool_policies} on the manifest.
  • brain/agent_builder/models.py:94RuntimeSpec{max_turns, token_budget, timeout_s}.
  • brain/kernel/run_context.py:52RunBudget{max_tool_calls, max_cost_usd, max_latency_ms, thinking_budget_tokens} enforced atomically (claim_tool_call()).
  • brain/kernel/run_context.py:33SafetyMode{SAFE, NORMAL, STRICT}; is_safe_mode() toggles classes of tools.
  • brain/kernel/tooling/permissions.py + executor.py — central ToolPolicy / ToolExecutor evaluating before each call.
  • tests/agents/test_research_brief_agent.py:34 — contracts already carry an approval_tier ("gate" vs auto) and a tool_allowlist.
  • brain/policies/injection.py:46identity_override rule 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. ToolPolicy can require attached claim scopes with identity_required=True, and tool safety classes map to tools:read, tools:write, tools:network, or tools:destructive.
  • IdentityClaim now has nbf and exp; 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:send on 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:1A2AAgentCardSigner signs cards with HMAC-SHA256 (JWS-style), key stored at <state>/a2a_card_signing_key.json. Verification via verify_card.
  • brain/a2a/models.py:426A2AAgentCard.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-183RunContext.provider, model are 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 IdentityClaim now exists and includes run_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_key namespace (a.b.c), evidence_refs, support.research.brief style intents.
  • EventLog.log_action(action_type, description, input_data, output_data) used everywhere (incl. the new research_deep).
  • RunContext.to_dict() emits a full snapshot including budget, working_set, IDs, and a single nested identity object.
  • brain/automation_tokens.py supports 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 revoke is 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 trace reconstructs 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:

  1. Added brain/agent_identity/urn.py — canonical AgentURN parser/formatter: agent:secondbrain/<slug>@<version> (e.g. agent:secondbrain/meeting-copilot@1.1.0). Pydantic validator and from_manifest() helper.
  2. Added OwnershipSpec, AgentLifecycle, and identity_scopes to AgentManifest. identity_scopes normalize deterministically and manifest-aware minting refuses revoked manifests.
  3. Added computed AgentManifest.agent_urn and wired it into A2A card creation/sync.
  4. New AgentBuilder-created manifests receive default OwnershipSpec ownership from author or local-operator.

Remaining:

  1. Decide when ownership becomes required for hand-authored manifests loaded from disk.
    class Ownership(BaseModel):
        owner_id: str               # required when set
        owner_kind: Literal["user", "team", "service"] = "user"
        tenant_id: str | None = None
        signing_key_id: str | None = None
        lifecycle: Literal["active", "deprecated", "revoked"] = "active"
        created_by: str | None = None  # agent_urn or user_sid
    
  2. 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:

  1. Principal Pydantic model:
    @dataclass(frozen=True)
    class Principal:
        kind: Literal["user", "service", "agent", "automation"]
        id: str                       # user_sid / service_id / agent_urn
        tenant_id: str | None = None
        display: str = ""
    
  2. Principal-chain helpers (principal_chain_from_dicts, extend_principal_chain, principal_chain_for_context) preserve ordered, oldest-first chains and enforce depth ceilings.
  3. RunIdentity stores the normalized ordered chain and optional signed claim as one coherent identity object. RunContext owns exactly one RunIdentity; mismatched explicit chains and signed claims fail visibly.
  4. A2AEnvelope.on_behalf_of and A2AEnvelope.identity_claim preserve delegation provenance through local A2A queue payloads.
  5. AgentRuntime auto-mints a verified child claim for local A2A dispatch when a parent claim includes agent:spawn; if a manifest has no explicit identity_scopes, the child inherits parent scopes except agent:spawn.
  6. SecondBrainRunner.fork_session mints a fresh child claim from a verified parent claim, removes agent:spawn from 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:

  1. Keep replacing remaining call sites that pass user_sid/actor boundary hints with explicit Principal values where the caller already knows the actor type.
  2. 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:

  1. Scope vocabulary as an enum:
    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"
        # ...
    
    Kernel scope requirements currently derive from ToolSpec.safety_class, ToolSpec.requires_permission, and optional metadata["required_identity_scopes"].
  2. IdentityClaim:
    class 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: str
    
    Implemented as IdentityClaim with JWT-like aliases and HMAC-SHA256 integrity. RunContext can attach a serialized claim and ToolExecutor uses it for observe-only audit metadata or opt-in scope enforcement.
  3. Local key material and mint helpers: LocalIdentityKeyStore, local_identity_issuer, mint_run_identity, and mint_run_identity_from_key_store.
  4. gRPC A2A ingress verifies supplied claims against the local identity key, checks expiry/signature through IdentityClaim.verify, requires a2a:send, rejects on_behalf_of/claim-chain mismatches, refuses tenant mismatches, and has a strict require_identity_claim mode.

Remaining:

  1. Automatic minting at run start for more selected entrypoints. n8n inbound, local A2A dispatch, and SecondBrainRunner are the first wired surfaces.
  2. Decide which A2A callers must supply claims instead of accepting backward-compatible unsigned requests.
  3. 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:

  1. Operator revocation: sb agent-identity revoke <agent> --reason "..." flips manifest lifecycle to revoked, archives the registry status, deletes the local A2A card, and emits an agent_identity.revoked event.
  2. 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 emits agent_identity.key_rotated.
  3. Event-log uniformity: identity_audit_log_kwargs(...) writes consistent actor, session_id, trace_id, and agent_identity_* metadata. Kernel tool calls, A2A dispatch, n8n inbound execution, and SDK run logging use the helper or its underlying metadata contract.
  4. Attribution CLI: sb agent-identity trace reads local event/tool-call metadata and filters by agent URN, trace id, or session id.

Remaining:

  1. Per-claim/key revocation list for compromised keys or claims that must be refused before natural expiry.
  2. Trusted-previous-key deprecation schedule after rotation.
  3. 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:

  1. What signing_key_id format should we adopt — workspace-scoped UUID, or owner-rooted (tenant:<id>:key:<n>)?
  2. Should the IdentityClaim be embeddable in MCP responses (so an MCP consumer can verify "this answer came from a SecondBrain agent under this policy") or kept internal?
  3. How do we represent delegation chains crossing organizations — do we lock to single-tenant until federation is on the roadmap?
  4. 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 @ ".