Bind CBCL address atoms to cryptographic identity (design) #10

Open
opened 2026-05-11 13:10:27 +00:00 by hugooconnor · 0 comments
hugooconnor commented 2026-05-11 13:10:27 +00:00 (Migrated from codeberg.org)

Address atoms (@planner, @router, @email-service) are just CBCL symbols today. There is no cryptographic identity tied to them — a producer can address an ask to @planner regardless of who @planner actually is, and a worker can advertise that it speaks dialect X without binding the dialect's @planner references to a verifiable identity. The router resolves recipient atoms by registry lookup, which itself is keyed by self-chosen agent-id at hello time.

Under the threat model "honest workflow easy, dishonest no wire harm," this is the deepest open gap: a malicious worker who connects as agent-id=planner can intercept messages addressed to @planner from any dialect that uses that symbol, with no way for producers or other workers to detect the substitution.

Direction: W3C DIDs

The chosen long-term direction is W3C Decentralized Identifiers for agent identity handles. Address atoms become (or carry) DIDs; resolution returns a DID Document with public keys; the substrate verifies that a connecting agent controls the keys associated with the atom they claim.

Why DIDs:

  • Interoperable — workers, producers, and other CBCL substrates all speak the same identity vocabulary; we don't reinvent.
  • Method-flexibledid:key: (key-as-identifier) for self-contained dev/test, did:web: (DNS-anchored) for hosted services, did:plc: / did:peer: etc. for production scenarios. The substrate doesn't pin a method.
  • Off-the-shelf trust model — DID Documents define rotation, revocation, controller delegation. We don't have to design those primitives.
  • Composable with existing substrate — Ed25519 keys (R4) map directly to verificationMethod entries in a DID Document.

Routing implications

Identity and routing are the same design problem from two angles. With DID atoms, the substrate gains a routing primitive it doesn't have today: precise, cryptographically-attributable addressing alongside the existing capability-based dispatch.

Current routing modes:

Producer says Router does
(lang <dialect> (ask @router "verb" …)) Capability dispatch — select-agent <dialect>, least-loaded
(meta (teach @<agent-id> …)) Proxy delivery by self-chosen agent-id (impersonable)

Routing modes after DIDs land:

Producer says Router does Use case
(lang <dialect> (ask @router "verb" …)) Capability dispatch (unchanged) "anyone competent"
(lang <dialect> (ask @did:key:… "verb" …)) Direct, dialect-validated "this specific worker, in this shape"
(ask @did:key:… "raw" …) Direct, no dialect frame Worker-to-worker private exchange

What direct DID routing enables that capability dispatch cannot:

  • Thread continuation across reconnect. Today's thread-affinity routing uses agent-id, which is impersonable. With DID, "send the follow-up to the same agent that handled the original ask" becomes cryptographically meaningful — the agent must prove control of the DID on reconnect.
  • Worker-to-worker. An agent A handling dialect X needs a sub-task done by a peer who speaks dialect Y. Today A would (ask @router …) and the router picks; with DIDs, A can address a specific known peer (ask @did:key:Y "…"). Still routes through the hub, but the destination is precise.
  • Federation. @did:web:router-a.example/agent/planner is unambiguous across hubs. Today self-chosen agent-ids collide between deployments.
  • Audit semantics on the receipt log. The :thread "rcp-…" chain plus DIDs on each frame gives a cryptographically-attributable transcript. Material for compliance and dispute resolution.
  • Off-substrate handles. A DID for a non-CBCL service (did:web:legacy.example.com). The substrate can't bridge directly, but the addressing is unified — the producer doesn't have to know whether @did:web:planner is a CBCL agent or an HTTPS adapter.

Required substrate changes for routing

  1. Registry index. cbcl-agent-registry grows a did → pid index alongside the existing agent-id → pid. New find-by-did/1 and dispatch-to-did/1 parallel the existing functions.
  2. Connect-time DID proof-of-control. Hello carries the agent's DID + a signature over a router-issued nonce, signed by a key in the resolved DID Document. Replaces today's "trust the agent-id field."
  3. DID Document resolver + cache. Router-side. did:key: is zero-network (entire identity is in the atom). did:web: is HTTPS + DNS. Cache with TTL; respect updated timestamps for rotation.

Open routing question

Should direct DID routing also require dialect-advertisement consistency, or can a producer address a DID that hasn't advertised the dialect?

  • Reject by default — if (lang Y (ask @did:key:X …)) and X doesn't advertise Y, return dialect-not-advertised. Preserves the dialect-as-capability commitment ("advertising is committing"). Producers must consult the registry before direct-addressing.
  • Forward — deliver anyway; X can error if it doesn't speak Y. More flexible, but undermines the speaker-contract guarantee.

Lean: reject by default with an explicit :bypass-capability-check keyword on the ask for the forward case. Captures the trust commitment in the wire form rather than at the router config.

Open design questions

These need a SPEC entry before implementation:

  • Atom syntax. Bare DID in the atom (@did:key:z6Mk…) or namespaced binding (@planner resolves to a DID via a registry)? Bare DIDs are honest but ugly; binding via registry is ergonomic but reintroduces the squatting question (who controls the binding registry, and how).
  • Resolution model. Lazy (resolve on first use) vs eager (pin a DID Document in the hello)? Caching policy. Stale-handling.
  • Which DID method(s) the substrate ships first. did:key: is the lowest-friction starter — entire identity is in the atom, no resolver needed. did:web: is the next step for production-like deploys.
  • Binding to R4. The dialect's :signature clause becomes "signed by a key in this DID's verificationMethod set at sign-time." Verification needs a DID resolver in the cbcl-rs pipeline.
  • Backwards compatibility. Bare-symbol atoms today; the new form coexists how? Possibly: bare symbols are valid only inside cbcl-base (well-known router/dev atoms); user-defined dialects must use DID atoms.
  • Routing-vs-identity separation. Whether to split routing-design into a sibling issue once identity-design is anchored, or treat them as one SPEC entry.

Acceptance (for the design phase)

  • SPEC entry in cbcl-rs documenting the atom-as-DID model, the initial method support set, the resolution / caching contract, and the routing primitives (dispatch-to-did, capability-check policy).
  • Identify which existing primitives change: parser (atom syntax extension), message type, router resolution + registry, hark hello shape.
  • Identify wire-compatibility story and migration plan.
  • Prototype: parse + resolve did:key: atoms; verify R4 signature against the key embedded in the DID; route a direct-addressed ask through the registry's did → pid index.

Out of scope for this issue

  • Implementation beyond the prototype. A separate implementation PR follows the SPEC entry.
  • DID resolver infrastructure for methods beyond did:key: / did:web:.
  • Substrate-side identity rotation protocol (rely on DID Document update semantics for now).
  • P2P / direct worker-to-worker transport (still hub-routed in this design).

See discussion in hark conversation on the dialect-distribution threat model.

Address atoms (`@planner`, `@router`, `@email-service`) are just CBCL symbols today. There is no cryptographic identity tied to them — a producer can address an ask to `@planner` regardless of who `@planner` actually is, and a worker can advertise that it speaks dialect X without binding the dialect's `@planner` references to a verifiable identity. The router resolves recipient atoms by registry lookup, which itself is keyed by self-chosen agent-id at hello time. Under the threat model "honest workflow easy, dishonest no wire harm," this is the deepest open gap: a malicious worker who connects as `agent-id=planner` can intercept messages addressed to `@planner` from any dialect that uses that symbol, with no way for producers or other workers to detect the substitution. ## Direction: W3C DIDs The chosen long-term direction is **W3C Decentralized Identifiers** for agent identity handles. Address atoms become (or carry) DIDs; resolution returns a DID Document with public keys; the substrate verifies that a connecting agent controls the keys associated with the atom they claim. Why DIDs: - **Interoperable** — workers, producers, and other CBCL substrates all speak the same identity vocabulary; we don't reinvent. - **Method-flexible** — `did:key:` (key-as-identifier) for self-contained dev/test, `did:web:` (DNS-anchored) for hosted services, `did:plc:` / `did:peer:` etc. for production scenarios. The substrate doesn't pin a method. - **Off-the-shelf trust model** — DID Documents define rotation, revocation, controller delegation. We don't have to design those primitives. - **Composable with existing substrate** — Ed25519 keys (R4) map directly to `verificationMethod` entries in a DID Document. ## Routing implications Identity and routing are the same design problem from two angles. With DID atoms, the substrate gains a routing primitive it doesn't have today: **precise, cryptographically-attributable addressing** alongside the existing capability-based dispatch. Current routing modes: | Producer says | Router does | |---|---| | `(lang <dialect> (ask @router "verb" …))` | Capability dispatch — `select-agent <dialect>`, least-loaded | | `(meta (teach @<agent-id> …))` | Proxy delivery by self-chosen agent-id (impersonable) | Routing modes after DIDs land: | Producer says | Router does | Use case | |---|---|---| | `(lang <dialect> (ask @router "verb" …))` | Capability dispatch (unchanged) | "anyone competent" | | `(lang <dialect> (ask @did:key:… "verb" …))` | Direct, dialect-validated | "this specific worker, in this shape" | | `(ask @did:key:… "raw" …)` | Direct, no dialect frame | Worker-to-worker private exchange | What direct DID routing enables that capability dispatch cannot: - **Thread continuation across reconnect.** Today's thread-affinity routing uses agent-id, which is impersonable. With DID, "send the follow-up to the same agent that handled the original ask" becomes cryptographically meaningful — the agent must prove control of the DID on reconnect. - **Worker-to-worker.** An agent A handling dialect X needs a sub-task done by a peer who speaks dialect Y. Today A would `(ask @router …)` and the router picks; with DIDs, A can address a specific known peer `(ask @did:key:Y "…")`. Still routes through the hub, but the destination is precise. - **Federation.** `@did:web:router-a.example/agent/planner` is unambiguous across hubs. Today self-chosen agent-ids collide between deployments. - **Audit semantics on the receipt log.** The `:thread "rcp-…"` chain plus DIDs on each frame gives a cryptographically-attributable transcript. Material for compliance and dispute resolution. - **Off-substrate handles.** A DID for a non-CBCL service (`did:web:legacy.example.com`). The substrate can't bridge directly, but the addressing is unified — the producer doesn't have to know whether `@did:web:planner` is a CBCL agent or an HTTPS adapter. ### Required substrate changes for routing 1. **Registry index.** `cbcl-agent-registry` grows a `did → pid` index alongside the existing `agent-id → pid`. New `find-by-did/1` and `dispatch-to-did/1` parallel the existing functions. 2. **Connect-time DID proof-of-control.** Hello carries the agent's DID + a signature over a router-issued nonce, signed by a key in the resolved DID Document. Replaces today's "trust the agent-id field." 3. **DID Document resolver + cache.** Router-side. `did:key:` is zero-network (entire identity is in the atom). `did:web:` is HTTPS + DNS. Cache with TTL; respect `updated` timestamps for rotation. ### Open routing question Should direct DID routing also require dialect-advertisement consistency, or can a producer address a DID that hasn't advertised the dialect? - **Reject by default** — if `(lang Y (ask @did:key:X …))` and X doesn't advertise Y, return `dialect-not-advertised`. Preserves the dialect-as-capability commitment ("advertising is committing"). Producers must consult the registry before direct-addressing. - **Forward** — deliver anyway; X can `error` if it doesn't speak Y. More flexible, but undermines the speaker-contract guarantee. Lean: reject by default with an explicit `:bypass-capability-check` keyword on the ask for the forward case. Captures the trust commitment in the wire form rather than at the router config. ## Open design questions These need a SPEC entry before implementation: - **Atom syntax.** Bare DID in the atom (`@did:key:z6Mk…`) or namespaced binding (`@planner` resolves *to* a DID via a registry)? Bare DIDs are honest but ugly; binding via registry is ergonomic but reintroduces the squatting question (who controls the binding registry, and how). - **Resolution model.** Lazy (resolve on first use) vs eager (pin a DID Document in the hello)? Caching policy. Stale-handling. - **Which DID method(s) the substrate ships first.** `did:key:` is the lowest-friction starter — entire identity is in the atom, no resolver needed. `did:web:` is the next step for production-like deploys. - **Binding to R4.** The dialect's `:signature` clause becomes "signed by *a key in this DID's verificationMethod set at sign-time*." Verification needs a DID resolver in the cbcl-rs pipeline. - **Backwards compatibility.** Bare-symbol atoms today; the new form coexists how? Possibly: bare symbols are valid only inside `cbcl-base` (well-known router/dev atoms); user-defined dialects must use DID atoms. - **Routing-vs-identity separation.** Whether to split routing-design into a sibling issue once identity-design is anchored, or treat them as one SPEC entry. ## Acceptance (for the design phase) - [ ] SPEC entry in cbcl-rs documenting the atom-as-DID model, the initial method support set, the resolution / caching contract, and the routing primitives (`dispatch-to-did`, capability-check policy). - [ ] Identify which existing primitives change: parser (atom syntax extension), message type, router resolution + registry, hark hello shape. - [ ] Identify wire-compatibility story and migration plan. - [ ] Prototype: parse + resolve `did:key:` atoms; verify R4 signature against the key embedded in the DID; route a direct-addressed ask through the registry's `did → pid` index. ## Out of scope for this issue - Implementation beyond the prototype. A separate implementation PR follows the SPEC entry. - DID resolver infrastructure for methods beyond `did:key:` / `did:web:`. - Substrate-side identity rotation protocol (rely on DID Document update semantics for now). - P2P / direct worker-to-worker transport (still hub-routed in this design). See discussion in hark conversation on the dialect-distribution threat model.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
anuna-research/cbcl-rs#10
No description provided.