docs: agent broker specification (#6467 interface half) - #6790
Conversation
A standalone specification of the keyless-agent broker protocol from #6467, written for someone implementing a host or a client who will never open the Rust crate: system model, six invariants, the nine operations with their wire shapes, the result and error model, the HTTP binding, and conformance checklists for both sides. Draft status until approved; the implementation in #6742 is held until then. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> Signed-off-by: ss-core-02 <cca2c695c61d409321106cc399009c2cc674d72cc8900b2999a68fe54577a411@buzz.block.builderlab.xyz>
Six members in the request envelope, not five; requestId is never trimmed and admits no whitespace; cursors admit no spaces; reaction and updatedFields are described as intent plus what is actually checked; the three response examples now use values that parse. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> Signed-off-by: ss-core-02 <cca2c695c61d409321106cc399009c2cc674d72cc8900b2999a68fe54577a411@buzz.block.builderlab.xyz>
Drop the cross-references to the originating issue and implementation pull requests; keep the rationale each one carried inline. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e28589733d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - Records, per `requestId`, a digest of the received bytes and the verdict | ||
| reached; replays the recorded verdict with `replayed: true` on a matching | ||
| digest and answers `request_id_conflict` on a differing one (I3). |
There was a problem hiding this comment.
Scope idempotency records to the authenticated principal
When two authenticated sessions choose the same caller-controlled requestId, this requires a shared lookup keyed only by that value. If their request bytes match, the second agent receives the first agent's stored verdict without its own operation running; if they differ, it gets a cross-agent request_id_conflict. This can suppress another principal's operation and expose its outcome, so the durable key must include the session-derived agent/conversation authority scope in addition to requestId.
Useful? React with 👍 / 👎.
| The outcome is addressing material only: | ||
|
|
||
| ```json | ||
| { "authorPubkey": "…", "kind": 30078, "dTag": "…" } |
There was a problem hiding this comment.
Return the NIP-AE event kind from storage.address
This returns the NIP-78 application-data kind, but Buzz defines NIP-AE engrams as kind:30174 in both docs/nips/NIP-AE.md and KIND_AGENT_ENGRAM. A client using this addressing result will query or publish at the wrong coordinate and will not find the agent's actual encrypted memory; return 30174 here.
AGENTS.md reference: AGENTS.md:L176-L178
Useful? React with 👍 / 👎.
| This document specifies the **contract between an agent and a broker host**: | ||
| the request and response envelopes, the nine operations and their argument and | ||
| outcome shapes, the result and error model, and the HTTP binding. It |
There was a problem hiding this comment.
Specify every successful operation outcome
The document claims to define all outcome shapes and later requires strict parsing, but it never defines successful outcomes for message.post, reaction.add, or profile.set; only request arguments are given before the next operation begins. Independent hosts and clients therefore cannot agree on required fields or reject unknown fields as the conformance rules demand, so each operation needs an explicit outcome schema.
Useful? React with 👍 / 👎.
| - Accepts only the request envelope in §Request with exactly its six | ||
| members; rejects an unknown member anywhere, a `null` anywhere, a repeated | ||
| key, an unknown `protocolVersion`, an unknown action, or an unsupported | ||
| `actionVersion` — each as `failed` with the matching code. |
There was a problem hiding this comment.
Define an error form for requests without a valid requestId
For a malformed envelope whose requestId is missing, null, or repeated, this requires the host to return a failed verdict even though every result envelope requires one unambiguous requestId and clients reject responses that do not correlate to the request. The host cannot produce a conforming response for those inputs; define a pre-correlation error/transport response or permit an uncorrelated failure shape for envelope errors.
Useful? React with 👍 / 👎.
Chessing234
left a comment
There was a problem hiding this comment.
this reads well and the discipline of arguing six invariants from the rules — rather than listing them — is what makes it reviewable. three things i'd want settled before it leaves draft, all in the spec rather than in code.
I6 has a second spelling of absence and the document doesn't rule on it. "null is never a legal value anywhere in this protocol" is unambiguous, but arrays aren't. channel.read's outcome shows "tags": [], and message.post takes "mentions": ["<pubkey>", …]. is "mentions": [] legal, and if so does it mean the same as omitting mentions? if both are legal and equivalent, absence has exactly the two spellings I6 exists to forbid, and a host validating "which members a status admits" has to treat them as one — which is the slip the invariant predicts. the same question applies to empty strings for optional string members. one sentence ("an optional array member is omitted when empty; a present empty array is malformed", or the opposite) closes it.
requestId scoping isn't stated, and it's the one place a caller-chosen value becomes a lookup key. the request section says it's "the caller-chosen idempotency key, unique per logical operation" and that it "becomes part of a durable idempotency record". uniqueness is asserted of the caller, but nothing says the record is scoped to the authenticated session. if a host keys idempotency on requestId alone, agent B picking a string agent A already used gets A's cached verdict — a cross-agent read of someone else's outcome, arrived at without any authorization failure. that would also quietly violate I5, since the verdict B receives correlates on a requestId it really did send. §Scope and Non-Goals defers "how a host stores idempotency records" to the host, which is right for the mechanism, but the scope of the key is part of the contract, not the implementation. i'd state it as a rule: the idempotency record is keyed on (authenticated principal, requestId).
I4's completeness caveat lands hardest on the wake path, and the spec doesn't say so. the invariant is honest that A trusts H for completeness. but mentionsOnly is described as "the wake path for a keyless agent, which polls this instead of holding a subscription", and cursors are opaque, host-issued, and "never parsed, compared, or synthesized" by A. so an agent has no gap detection available to it at all: it cannot compare created_at across pages against a window it chose, cannot notice a skipped id, and cannot even tell a short page from a filtered one, because it "learns to stop from nextCursor's absence, never by comparing a page length against a limit". a host that drops every mention from one person is indistinguishable from a quiet channel, forever. that may be an accepted consequence of trusting the key holder — the host has the key, so it can also just not post — but silent selective censorship of reads is a different capability from refusing to act, and the document should say which side of the trust boundary it puts that on rather than leaving it to the reader.
smaller: §Operations says length limits "on names, prompts, and similar fields count characters", while the pr description says the storage slug bound is in bytes. both may be right, but "characters" needs a definition in a document this precise — unicode scalar values, grapheme clusters, and UTF-16 units give three different answers for the same emoji, and a client in TypeScript and a host in Go will pick different ones by default. naming the unit (and NFC or not) would prevent a class of interop bug that only shows up on non-ASCII names.
on the pr note itself: checking every limit against the code at 7c853e0fb rather than against #6742's description, and reporting the two places the description was wrong, is exactly how a spec-before-implementation should be written up. worth keeping that commit sha in the document's own header rather than only in the pr body, since the pr body isn't what a future reader has.
- Key the idempotency record on (authenticated principal, requestId), never requestId alone (I3, C2) - Rule empty arrays a second spelling of absence: optional arrays are omitted when empty, a present [] is malformed (I6) - State the shared publish outcome (eventId/kind/createdAt) once at message.post - Define the no-answer treatment for a request whose requestId is itself unusable - Name the unit behind 'characters' (Unicode scalar values) - Correct the storage.address example kind to the agent-memory kind - Say out loud that read completeness on the wake path is trusted, not verifiable: silent mention withholding is indistinguishable from quiet Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
Review on the contract asked the pubkey type to preserve the public-key invariant rather than accept any 64 hex characters; the contract is being tightened to match, so the spec states the rule. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
A documentation-minimalism pass, no code changes. The English spec (docs/agent-broker.md, #6790) is the canonical home for design rationale; doc comments now state each invariant once, where an implementer will read it, instead of retelling the same stories (no-null, second wire door, identity spelling, validate-then-execute trap) across module docs, type docs, and helper docs. Also names memory read/write in the deferred-operations list, per review: v1's storage.address only addresses a record, so intent-level memory operations are a declared follow-up alongside presence/typing. Net -404 lines, all comments. Validation: cargo test -p buzz-sdk 301/301, clippy --all-targets clean, RUSTDOCFLAGS="-D warnings" cargo doc clean, fmt clean. The diff contains no non-comment lines (verified by filtering the diff for changes outside //-prefixed lines). Signed-off-by: ss-core-02 <cca2c695c61d409321106cc399009c2cc674d72cc8900b2999a68fe54577a411@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
This is the design behind the broker stack, written up as a standalone specification so it can be approved before any of the implementation merges. It is the interface half of #6467 — how an agent that holds no secret key reads its channels, posts, reacts, keeps its profile, addresses its memory, and creates agents by asking a host to act for it.
It sits at
docs/agent-broker.md, besidedocs/remote-agents.md, because it is the same kind of document: a Buzz-defined protocol that is not a NIP because the relay never sees it. It is written for someone building a host in Go or a client in TypeScript who will never open the Rust crate — the vocabulary is wire names and JSON examples, and there are no code identifiers in it.The shape: a short system model with two axioms (identity is public-key-only; the body never asserts authority), six invariants argued from the rules, the nine operations with their argument and outcome shapes and limits, the three-way succeeded / failed / indeterminate result with the full code-to-status table, the HTTP binding, and conformance checklists for the client side and the host side. It closes with what is deliberately out of scope and four open decisions.
Status is
draftuntil a non-author approves it. #6742 (the contract crate) is on hold until then; later PRs in the stack that change the wire amend this document in the same change.One thing to know when reading: every limit and rule in here was checked against the code at
7c853e0fbrather than against #6742's description, which got two details slightly wrong — string trimming is not uniform (content and cursors are left exactly as sent), and the storage slug bound is in bytes, not characters. Both are stated correctly here.