Repository navigation
Conversation
…ir owner Agents lose the `dm` tool, and `send` and `reply_in_subthread` refuse a DM channel. Plugin agents' keys no longer sign DM opens (41010). A Solo chat an agent starts always adds its owner. The shared posting conventions and the Claude Code, Goose, Pi and Codex DM turn context tell an agent to answer a DM by starting a Solo chat for the request, with the sender and its owner. AGNTOPS-780 Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Joah Gerstenberg <[email protected]>
A DM whose only members are the agent and its owner, such as Bestie's pinned chat, stays open to the agent: send accepts it, and the Claude Code and Codex turns keep the ordinary DM routing there instead of sending the answer to a Solo chat. Every other DM stays closed. AGNTOPS-780 Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Joah Gerstenberg <[email protected]>
kazemek21
left a comment
There was a problem hiding this comment.
Kyle reviewed the PR and the Nexus review-tracker notes. Approved with the non-blocking suggestion below.
Posted by Codex (gpt-6.1-sol) on behalf of kylez
| : thread | ||
| ? "IMPORTANT: `send` replies in this thread by default, so the conversation stays threaded. If the human explicitly asks for a channel-root, top-level, or broadcast post, pass `channel`. If the requested destination is ambiguous, ask before sending." | ||
| : "IMPORTANT: This is a new top-level message. `send` replies in its thread by default — the triggering message is the thread root. Do NOT reply into any other (older) thread. If the human explicitly asks for a channel-root, top-level, or broadcast post, pass `channel`."; | ||
| : channel.dm && !channel.ownerDm |
There was a problem hiding this comment.
[P2] Preserve the owner-DM exception in every shared-prompt caller
Goose, Pi and the harness kit still construct channel with only dm, so ownerDm is undefined here even in the owner’s own DM. Those turns now explicitly tell the agent to start a Solo chat instead of answering in Bestie. Could these callers pass the owner-only status as the Claude runtime does?
Posted by Codex (gpt-6.1-sol) on behalf of kylez
There was a problem hiding this comment.
🤖 Good catch, fixed in 245e265. Goose, Pi and the harness kit now pass ownerDm the same way the Claude Code runtime does: the kit stores it at admission from the channel's members, and Goose and Pi compute it from the channel summary. A new kit test checks that an owner-only DM keeps the normal DM routing and that a DM with a third member, or with unknown members, gets the Solo chat instruction.
… turns Goose, Pi and the harness kit built the turn's channel with only `dm`, so in a DM with only the owner they still told the agent to start a Solo chat. Each now passes `ownerDm` the way the Claude Code runtime does. AGNTOPS-780 Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Joah Gerstenberg <[email protected]>
* origin/main: (38 commits) Fix reminder counts with compact dock corner tabs (block#3126) Add conditional undo for explicit reads (block#3291) fix(composer): edit channel links as atomic chips (block#3312) fix(agents): show remote instruction limit (block#3325) NEXUS-4033: Offer session skills in the core task composer (block#2790) feat(github): name pasted repository and commit links (block#3268) Let plugins give standing instructions to every agent turn (block#3285) fix(relay): read a channel's creation once, not per metadata version (block#3244) fix(messages): remove unconfirmed sends from this device (block#3317) fix: expose proven non-delivery for artifacts (block#3300) feat(plugins): let plugins mark a person's name in @mention suggestions (block#3260) fix(mcp): strip metadata from images agents attach (block#3311) fix(identity): name a linked key that isn't here, and keep Restore after a failed backup check (block#3238) feat(changesets): show the files a local agent changed on its reply (block#3214) fix(identity): show a backed-up key as backed up, and drop dead retries (block#3223) fix(browser): open Builderlab account connections in the system browser (block#3287) Expose exact captured-message reads (block#3264) docs(agents): keep internal discussion out of public repo content (block#3258) fix(identity): clear a copied private key, name restore failures, fix storage copy (block#3236) fix(channels): list workspaces that load after `#` opens (block#3277) ...
AGNTOPS-780 Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Joah Gerstenberg <[email protected]>
## Summary Agents stop writing in direct messages, except a DM with only their owner. This is the relay-side backstop for block/buzz-app#3261, which takes away the app agents' DM tools. An agent holding its own key could still sign DM events directly, and only the relay can stop that. A key counts as an agent when it has a stored owner (`users.agent_owner_pubkey`, written when the agent authenticates with a NIP-OA owner attestation). For those keys: - **Writes into a DM channel** are refused with `restricted: agents cannot post in direct messages; start a Solo chat that includes your owner instead`. The check sits in `check_channel_membership` for `ChannelWrite::Post`, so it covers stream messages, reactions, deletions, typing and other channel ephemerals, artifact moves and edit validation. Non-DM channels skip it at no extra cost. - **Workflow `send_message`** (which posts as the workflow owner and checks `may_post` directly) applies the same rule before persisting, so an agent can't route a DM post through a workflow. - **Put-user (9000) and edit-metadata (9002)** skip the generic gate, so they get their own check in a DM: an agent never adds DM members with 9000 (an addition to an owner-only DM always widens it), and edits topic or purpose only in its owner-only DM (the edit posts a system message with the text). - **Huddle admission** checks `may_post` directly, so it applies the same rule to the huddle channel and its lifecycle parent. - **DM open (41010) and DM add-member (41011)** are refused unless the resulting participant set is exactly the agent and its owner. - **Exemption:** a DM whose live members are exactly the agent and its owner stays open (for example, a personal assistant's home chat). - Lookup failures fail closed. Confirmed agent owners are cached per community (`agent_owner_cache`, 5 min TTL). The link is first-write-wins and never changes once set. A missing link is never cached, because another relay process may write it later. `link_agent_owner` primes the cache. Humans are unaffected: they can still open DMs that include an agent and post in them. The agent answers elsewhere. Not covered: - A key that never presented an owner attestation has no stored owner and looks human. - The huddle join commit re-check (4e) doesn't repeat the DM rule; the pre-join admission check enforces it. - Agents also can't delete their own earlier messages in a non-owner DM via kind 5, since deletions pass the same `Post` gate. ### Related issue Companion to block/buzz-app#3261. AGNTOPS-780. ### Testing New Postgres ingest tests (`handlers::ingest::postgres_tests::agent_*`), run locally: - An agent is refused in a 1:1 DM with someone else and in a group DM with its owner; the humans in both still post. - An agent posts in a DM with only its owner. - An agent can open a DM only with its owner; a human can still open a DM with an agent. - An agent can't add members to its owner DM, through 41011 or 9000. - An agent can't set the topic in a DM with a stranger. - An agent is refused admission to a huddle in a DM with a stranger and admitted in its owner-only DM; a human still joins. - An agent's workflow `send_message` is refused in a DM with a stranger and allowed in its owner-only DM. - An owner linked after a lookup miss is seen on the next lookup (no negative caching). `cargo clippy -p buzz-relay --all-targets -D warnings` is clean. The other ingest and command-executor tests pass locally (214), except two that fail only because of my local database: one needs the desired-state FTS schema, and the other fails creating its restricted role in setup. Neither reaches DM code. I pushed without the local pre-push `just test-unit` lane because a `buzz-cli` unit test hung there for about 30 minutes; CI runs that lane. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Joah Gerstenberg <[email protected]> Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
* origin/main: (133 commits) Expose ordinary chats without workspace headings (block#3530) [2/2] feat(agents): offer Automatic (Recommended) in Create agent and Identity (block#3345) fix(solo): preserve and resume unsent chat drafts (block#3525) fix(builderlab): give refused app sign-ins and Identity links a route to Organization sign-in (block#3078) fix(agents): say why Import an agent can't import here, and link Manage plugins (block#3073) [1/2] feat(agents): add a recommended agent setup (core) (block#3344) test(browser): survive the live-stream restart a page-clock jump causes (block#3442) chore(renovate): hold rusqlite below 0.41 for mesh-llm (block#3438) test(browser): serve the diffs and CLI reload fixtures outside the navigation budget (block#3445) test(browser): play the draft clip at rate 0 instead of looping it before the Play click (block#3446) fix(status): reopen a status whose preset deadlines coincide as Today (block#3449) fix(messages): tolerate a cancelled reaction-name chunk load (block#3450) fix(pairing): tolerate /proc/net/tcp rows printed twice or missed (block#3451) test(browser): wait on the signal each test asserts in local-agent-activity and message-navigation (block#3444) Expose native channel headings to Solo sources (block#3523) test(browser-host): run the 100,000-channel broker scale check alone in the measurements lane (block#3447) fix(solo): keep every contributed destination action (block#3516) fix(identity): report an issuer that keeps rate-limiting an agent as a limit (block#2365) fix(identity): let a stuck at-launch sign-in copy its link (block#2453) fix(profiles): open avatar previews on hover in compact density (block#3510) ... Signed-off-by: Joah Gerstenberg <[email protected]> # Conflicts: # src/bundled/codex/prompt.test.ts # src/bundled/goose/runtime.ts
Agents can no longer DM people or each other. The one exception is a DM whose only members are the agent and its owner, such as the pinned Bestie conversation. For any other private conversation, an agent starts a Solo chat, and that Solo chat always includes its owner. When anyone else DMs an agent, the agent answers by starting a Solo chat for that request.
Changes
dmis removed from the Buzz MCP tools.sendandreply_in_subthreadrefuse a channel whose metadata type isdm, unless its roster (39002) holds only the agent and its owner. The error tells the agent to start a Solo chat instead.crates/agent-controllerapp_agentKINDS, so the app won't sign a DM open with a plugin agent's key. Bestie's DM is opened by the owner's app, so it doesn't need this.channel_createwithsolo: trueadds the agent's owner (client.owner) to the members. If the host doesn't know the owner, it refuses. The node client now exposesownerfrom its NIP-OA auth tag. The owner counts toward the relay's 32-member limit at create.prompt.md(shared posting conventions) drops "In a DM,sendreplies at the top level." It now says to answer a DM from anyone but the owner by starting a Solo chat with the sender (the owner is added), and to keep using that Solo chat for follow-ups on the same request. The newDM_PROMPTreplaces the "passreply" routing line in those DM turns for Claude Code, Goose, Pi, the harness kit (which shareturnPrompt) and Codex.ownerDminfeatures/agents/reads.tsidentifies owner-only DMs from the channel's members. Every one of those callers passes it, so owner-only DM turns keep the ordinary DM routing.Not covered here
reply()posts, like the failed-turn notice that tags the owner, still go to the conversation, and that includes a DM.Testing
vitest run src/buzz-mcp/tools.test.ts: pass. Tests cover:dmtoolsendandreply_in_subthreadrefused in a DM that has another person in it, allowed in a DM with only the owner, and refused there when the host doesn't know the ownersrc/features/agent-harness/kit.test.ts: an owner-only DM turn gets noDM_PROMPT. A DM with a third member, or with unknown members, gets it.src/features/agents/reads.test.ts:ownerDmis true only for a DM whose members are just the agent and its owner. It's false for unknown members, an absent owner, or any third member.replyrouting. The Codex prompt tests cover the DM turn instruction.cargo test -p buzz-agent-controller: pass, including 41010 now refused.tsc --noEmit, biome and rustfmt hooks: clean.src/buzz-mcp,src/bundled/{claude-code,codex},src/features/agentsandsrc/features/agent-harness: every failure is in a file this PR doesn't touch, and each one either passes when rerun alone or also fails on unmodifiedmain(codex/tabseffort refresh).🤖 Generated with Claude Code