Identity and ACL design¶
Per-agent identity and ACL enforcement grew out of a design target for
deployments that need more than shared-token mode and the caller-name model.
The core of it is now implemented (see the next section): deny-by-default ACL
enforcement is opt-in, the production waiter opts into a name-ownership lease,
and installations with cryptography use a zero-config trust-on-first-use
machine key. Core-only installations retain the unsigned compatibility path.
What remains a design target is listed at the end of that section. A secured
hub still admits a connection with a shared token; the layers below decide what
the admitted connection may claim to be and to do.
The goal is to bind every durable actor to an identity-bound credential, then evaluate whether that identity may perform a requested action before the hub mutates state or exposes scoped data. This does not replace per-message authentication, does not replace signed events, and does not sandbox agents.
Implemented (TOFU, shadow tools, and opt-in enforcement)¶
The ACL model and its evaluation are implemented in
:mod:synapse_channel.core.identity, :mod:synapse_channel.core.acl, and
:mod:synapse_channel.core.acl_enforcement:
synapse identity audit --identities <file>loads a declared identity inventory and reports the ambiguities that would block an enforcement rollout: duplicate audit subjects, missing credentials, and seats that run more than one agent id.synapse acl shadow --policy <file> --requests <file>evaluates candidate accesses against a deny-by-default ACL policy and records the would-allow / would-deny decision each would receive — with the matching rule and reason — without ever blocking a frame. Target patterns are structured (a kind plus a glob value), scoped to a project namespace, across the permission vocabulary below.synapse hub --acl-policy <file> --require-aclturns the same deny-by-default evaluation into runtime enforcement: a mutating frame (chat, claim, release, task update, handoff, checkpoint, board, finding, guard-denial evidence) is mapped to the structured accesses it needs and refused with an error before routing if the authenticated sender's identity is not allowed. Authentication stays the per-message authentication layer; this is the authorisation layer. Enforcement is opt-in and off by default, ungated verbs and read surfaces still pass, and a missing policy or shared-token local hub is unchanged.- Hub-authoritative name-ownership lease (
core/name_ownership.py): a name has exactly one owner across reconnects, not merely per socket. A registration that declareslease: trueon a free name is granted an opaqueowner_leasetoken in a directedlease_grantedframe (the hub stores only a SHA-256 digest); while the lease is live — its holder connected, or offline for less than--lease-offline-ttlseconds (default 3600) — any claim on the name must present the token or it is refused with close code4016(name owned), takeover flag or not. A claim presenting the token passes and still crosses the takeover damping (cooldown, oscillation quarantine). The waiter (synapse wait/synapse arm) opts in end to end: it persists the token per connect name under~/synapse/owner-lease/and presents it on every re-arm, so a re-arm re-takes its own-rxidentity and a stranger cannot squat it in the gap. Clients that never opt in keep classic first-come semantics, and a pre-lease hub ignores the fields — a mixed fleet keeps working. A lapsed or lost token self-heals: past the offline window the name returns to first-come-first-owned. - Zero-config trust-on-first-use identity (
machine_identity.py+core/identity_pins.py+ the trust-on-first-use posture incore/hub_identity_gate.py): with no operator input at all, the first connect provisions a per-machine Ed25519 keypair under$XDG_DATA_HOME/synapse/identity/, the registration is signed with it and carriesidentity_public_key, and the hub pins the name to the first key that proves it — persisted in--identity-pins(default~/synapse/identity-pins.json) so the pin survives hub restarts. A pinned name refuses a missing signature or any other key with close code4013(identity pin mismatch) and names the observed key id plus the governed recovery command; unsigned names keep classic semantics;--require-identity-bindingtakes precedence and keeps its fail-closed operator-bundle behaviour. Together with the ownership lease this closes the name-squatting class: the lease covers reconnect gaps with a bearer token, the pin covers restarts with a proof of key possession. - Every client verb signs uniformly:
SynapseAgentpresents the machine identity by default, so any verb that connects — send, listen, arm, queries, the bridges — proves the same key. Before this default onlyarmandwaitsigned, and arming a name locked its holder out of every other verb under that name (refusedsignature missing— the 2026-07-10 incident class). An explicitidentity_key_pathwins over the default,machine_identity=Falseopts a deliberately unsigned agent out, and a core-only installation degrades to the unsigned path with the module's one-time warning. - Governed stale-pin reclaim:
synapse identity reclaim <agent> --operator <identity> --expected-key-id <key> --reason <text>asks the live hub to remove one exact TOFU pin. The handler always enforces anidentity-pin-reclaimACL grant on target kindagent, even when general--require-aclenforcement is off; the requester must itself be pinned or operator-bundle-bound, and a hub without--dbrefuses because it cannot write the mandatory audit event. The safe path accepts an offline name whose ownership lease has lapsed under--lease-offline-ttl. It also accepts a socket-up holder after it has lacked both a recent reaction and a live waiter for that TTL after the recipient-liveness window elapsed; keepalive heartbeats neither refresh that clock nor erase its dead-letter signal. A fresh reaction or waiter blocks normal recovery. An operator may add--break-glassto evict any other live or still-leased holder, but that override, the previous key id, the operator, and the reason are recorded in a write-aheadidentity_pin_reclaimaudit trail and broadcast as a system notice. Removal is compare-and-swap on--expected-key-id; it never rotates or installs a replacement key. The next valid proof establishes a fresh first-use pin.
The identity namespace is taken from the resolved sender (project/agent). The
first credential format is now the zero-config machine key above (operator
bundles remain the multi-tenant graduation). General credential rotation and
revocation tooling, owner recovery beyond the governed stale-pin path, and
read-surface ACLs (metrics, dashboard, event-query) remain design targets.
Recovering a changed machine key — worked example¶
When a machine's Ed25519 key is regenerated (a fresh install, a wiped identity directory), every name this hub pinned to the old key is refused on reconnect with the identity-pin mismatch — the correct trust-on-first-use posture, but it needs a governed way back in that does not wipe the whole pin store. The governed reclaim above is that path; here is the end-to-end recipe. Which operator holds the grant and how widely it is scoped are the owner's ratification, not a default.
- Provision the reclaim grant (owner) in an ACL policy file the owner controls, scoped to a dedicated recovery-operator project namespace so only that operator — never a live seat self-granting — may reclaim:
{
"rules": [
{
"permission": "identity-pin-reclaim",
"target_kind": "agent",
"target_pattern": "*",
"namespace": "recovery-ops",
"reason": "designated recovery operator may reclaim stale machine-key pins"
}
]
}
-
Start the hub reading that grant:
synapse hub --db <path> --acl-policy <file>. The--dbis required — the reclaim writes a mandatory durable audit event. Theidentity-pin-reclaimverb is always enforced against this grant, even when general--require-aclenforcement is off. -
Connect the recovery operator under a FRESH identity in the granted namespace (
recovery-ops/<agent>). Because that name is unpinned it trust-on-first-use-pins to the current machine key and binds, sidestepping the bootstrap paradox that an operator whose own name is stale cannot bind to run the reclaim. -
Reclaim each stale name, giving the exact key id observed in the pin file (the compare-and-swap guard) and a reason for the audit trail:
synapse identity reclaim <stale-name> \
--operator recovery-ops/<agent> \
--expected-key-id <old-key-id> \
--reason "machine key rotated after reinstall"
The safe path accepts an offline name whose ownership lease has lapsed under
--lease-offline-ttl, or a socket-up holder that has gone unreactive for that
TTL. A live, reacting holder requires an explicit --break-glass, recorded in
the audit event alongside the operator, the previous key id, and the reason.
- Reconnect the recovered name with its new key. Removal is compare-and-swap only and never installs a replacement, so the next valid signed registration trust-on-first-use-pins the name to the new machine key and normal operation resumes.
A genuinely different public key stays operator-gated at every step: nothing here performs a silent key rotation or lets a name self-reclaim. This recovers a lost or rotated key, not a compromised one — a real credential leak is out of scope and must be handled by revocation, not reclaim.
Identity model¶
An identity record should separate the stable actor from the terminal, process, or provider session currently using it:
- Agent id: the stable per-agent identity used as the audit subject on claims, releases, messages, progress notes, capability cards, and receipt evidence.
- Seat id: the local workstation, tmux session, service unit, or operator seat that launched the agent. A seat can run one or more agent ids, but it should not silently borrow another agent's credential.
- Project namespace: the project or organisation prefix, such as
SYNAPSE-CHANNEL, that scopes identity names, claim paths, private channels, metrics views, and future federation policy. - Credential id: the key, certificate, or token handle that proves the caller is allowed to act as the agent id.
- Audit subject: the canonical identity string recorded by the hub after verification, not a self-reported display name.
The shipped credential formats are the local machine Ed25519 key and the operator-managed Ed25519 trust bundle. A future managed credential lifecycle may add other key or certificate handles, but any addition must support key-id lookup, rotation, revocation, owner recovery, and diagnostics that explain which credential failed without leaking secret material.
ACL model¶
ACLs should deny by default. An admitted identity may only perform the allowed verb on a matching target pattern inside its project namespace. The initial permission vocabulary should stay small and auditable:
| Permission | Scope |
|---|---|
message |
Send chat, direct messages, handoffs, and checkpoints to allowed targets. |
claim |
Create or update claims for allowed path target patterns. |
release |
Release claims and publish release receipts; this is the release permission. |
board |
Create and update blackboard tasks or findings. |
metrics |
Read live or historical operational metrics; this is the metrics permission. |
dashboard |
Read dashboard snapshots; this is the dashboard permission. |
a2a |
Serve or consume Agent-to-Agent bridge data; this is the A2A permission. |
namespace |
Administer project namespace membership and trust material; this is the namespace permission. |
observe |
Receive directed messages the identity is not a party to (when private directed routing is on). Target kind agent. |
mailbox |
Replay another identity's directed backlog via a mailbox heartbeat (mailbox_for). Target kind agent. Self and -rx sidecars do not need a grant. |
role-claim |
Bind a role on the heartbeat when --require-role-claim is on. Target kind role (<project>/<role>). Complements the role-grant store. |
identity-pin-reclaim |
Remove one exact stale TOFU pin after the liveness, expected-key, requester-binding, and durable-audit gates pass. Target kind agent. Always enforced for this verb. |
evidence |
Append authenticated, content-minimized enforcement evidence. The shipped guard_denial target is evidence:guard-denial; the handler additionally requires connect-token provenance and a durable journal. |
recall |
Pull the hub's global chat history and cursor-based resume backlog (history_request / resume_request). Target kind history; the shipped target is history:global. Consulted only under --require-acl — without enforcement the recall reads stay open, matching the proportionate-to-exposure posture. |
Each rule should include an allowed verb, a target pattern, an optional channel or project namespace constraint, and a decision reason suitable for receipts and postmortems. Target patterns must be structured data, not ad hoc substring checks, so a future policy engine can compare path claims, channel ids, agent ids, and A2A endpoints consistently.
Enforcement path¶
The hub should evaluate identity and ACLs at the point where a frame would change state or reveal scoped state:
- Resolve the connection credential to an audit subject.
- Canonicalise the requested verb and target pattern.
- Evaluate the identity's ACL rules with deny by default semantics.
- Record the verification result, rule id, and reason in the event log.
- Reject unauthorized frames before state mutation or scoped data disclosure.
Read paths need the same treatment as write paths. Metrics permission, dashboard
permission, private-channel history, global-history recall (the recall
permission, covering history_request and resume_request), A2A permission, and
event-query access all
need explicit rules because they can expose sensitive coordination metadata even
when no payload is changed.
Migration from shared-token mode¶
Migration should be gradual because local solo use must stay simple:
- Inventory current sender names,
synidentities, worker-session identities, service units, and shell-hook waiters. - Issue identity-bound credentials for known agent ids and seat ids.
- Run a shadow mode that records which ACL rule would have matched while still accepting shared-token mode traffic.
- Warn on ambiguous names, missing credentials, unscoped target patterns, and overbroad namespace permissions.
- Enable enforcement per project namespace only after the operator has recovery credentials and rollback instructions.
Rollback should restore shared-token mode without deleting audit history. The event log must keep both the asserted sender and the resolved audit subject so a postmortem can explain what changed during migration.
Relationship to other designs¶
- Per-message authentication authenticates selected frames after WebSocket connect authentication. Identity and ACLs decide whether the authenticated caller may perform the requested verb.
- Signed events and mTLS make selected durable records tamper-evident and authenticate configured peers. Identity and ACLs decide who is authorized before those records are created or relayed.
- Signed capability cards make route-relevant advertisements tamper-evident. Identity and ACLs decide who may advertise, update, project, or revoke those cards.
- Private channels scope the intended audience. Identity and ACLs decide who may join, leave, read history, or publish to that channel.
- Differential-privacy blackboard projections can redact or perturb shared board reports. Identity and ACLs decide who may create board data or request those projections.
- Agent trust graph evidence can explain routing and policy inputs. Identity and ACLs still decide who may act on that evidence.
- Policy engine can later consume identity decisions, release receipts, and event-log evidence, but it should not be the first layer that authenticates a caller.
Boundaries¶
The identity-binding, trust-on-first-use, role-claim, mailbox, private-directed, and mutating-frame ACL controls described above are implemented. They do not encrypt payloads, replace per-message authentication, replace signed events, replace TLS, sandbox agents, or make arbitrary provider code safe to run.
The local-first tradeoff is administrative complexity. A single-owner loopback hub should still work with shared-token mode. Exposed deployments need explicit credentials, broader credential rotation and revocation, deny by default ACLs, diagnostics, and operator procedures before this opt-in runtime can be treated as a complete multi-tenant IAM system. The reclaim verb is a recovery primitive, not a complete credential lifecycle.