Skip to content

feat(agents): let app-hosted agents manage their owner's channels - #3006

Closed
kazemek21 wants to merge 1 commit into
block:mainfrom
kazemek21:kylez_owner-channel-management
Closed

kazemek21 wants to merge 1 commit into
block:mainfrom
kazemek21:kylez_owner-channel-management

Conversation

@kazemek21

@kazemek21 kazemek21 commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

Adds five owner tools that let an app-hosted agent create, edit, archive, unarchive and delete its signed-in owner's channels and Solo chats. Agents can already read with their owner's access; this adds the matching channel management. The tools reuse the session's existing channel writers and signing/publication admission, and never give the model a signer.

What changes

  • Tools: owner_channel_create, owner_channel_update, owner_channel_archive, owner_channel_unarchive and owner_channel_delete (src/buzz-mcp/owner.ts). They are listed only where the existing owner read tools are: bundled-harness runs in communities the BUZZ_OWNER_MCP_COMMUNITIES build flag names. Plugins, installed harness plugins, plugin tools and external apps still get no owner access.

  • Writers: src/features/agents/owner-channels.ts drives the owner's own session services: the outbox and channelWrites for create, channelDetails.save for edits, and channelLifecycle.run for archive, unarchive and delete. Each checks active() before signing and again before publication. Details and lifecycle retain their own uncertain-publication state, even if the session closes after dispatch.

  • Admission: ownerMutationActive (owner-access.ts) is that active(). It is true only while all of these hold:

    • the calling turn (the host's own turn signal and check, via appClient);
    • the binding the write was admitted on (same session, viewer and community);
    • the agent's run lifetime on this host (runner.controller.signal, captured in Runner.handleFor);
    • the agent's recorded owner.

    A harness that cannot report whether its turn is still running gets no writes.

  • Create: takes a caller-chosen UUID.

    • A call with no operation is a new create. If Buzz already has a queued create for that ID, the call resumes it; if the relay's signed metadata already has the channel, the call confirms it.
    • To resume, the caller passes the operation an earlier answer returned. That call only resumes the journaled command or confirms the channel from the relay's metadata. It never signs another create. If Buzz no longer holds the operation and the relay has no channel yet, the answer is unconfirmed, with that operation and a pointer to owner_channel_get.
    • The same ID with other details, a system-channel command, additional initial members, or another create's operation is a conflict.
    • The owner is Owner; the agent is not added.
  • Update: takes the version that owner_channel_get now returns (the metadata event ID). Editable settings and version come from the same fresh metadata. It never saves over a newer version.

  • Lifecycle: exact IDs only, limited to archive, unarchive and delete. DMs and system channels are refused.

  • Outcomes: confirmed (read back), rejected (nothing published; reason is one of busy, conflict, cancelled, denied, refused or failed) or unconfirmed (may have been published). Anything that reached the relay transport is never reported as rejected.

Tests

  • owner-channels.test.ts drives a real relay session (outbox, channel writes, details and lifecycle) against a fixture relay that applies 9007/9002/9008 to its own signed records. It covers:
    • confirmed create, Solo create, repeat and conflicting IDs, and a lost acknowledgment that a call with the returned operation resumes without re-signing;
    • a separate call with the operation after a possible send, once the receipt is cleared and the relay shows no channel: it answers unconfirmed and signs nothing, then confirms once the channel appears;
    • relay refusal and refusal to resume broader queued system/member-bearing creates;
    • Stop, turn end, account switch and agent retirement before queueing and while signing, each with nothing published;
    • versioned edits, stale discovery values, stale versions, and unconfirmed edits that later settle or stay busy;
    • the full create → inspect → update → archive → archived discovery → unarchive → delete sequence; busy; unconfirmed lifecycle; system-channel refusal;
    • caller retirement during authorization before signing for update, archive, unarchive and delete, plus session closure after an update reaches transport;
    • the admission predicate on its own.
  • Mutation checks confirmed these tests fail when admission always passes, the journal resume is removed, the "may have been sent" classification is removed, the version check is removed, or the no-replacement guard for a resumed operation is removed.
  • owner.test.ts, app-client.test.ts and service.test.ts cover argument validation (including operation), exact forwarding, the write-aware release message, host-supplied caller wiring, and refusal without a host caller.

Not covered

  • Exercised only in automated tests: not yet driven in the running app or against a live relay.
  • If a caller retries without operation after the receipt is gone and the relay shows no channel, the call is treated as a new create. The tool description tells agents to pass operation when they resume.

🤖 Generated with Claude Code

🤖 Updated by Codex (gpt-6.1-sol) on behalf of kylez.

@kazemek21
kazemek21 force-pushed the kylez_owner-channel-management branch 3 times, most recently from 06a644f to 9bf4a41 Compare November 7, 2026 23:51
Add owner channel creation, versioned editing and lifecycle tools through
the existing session services. Recheck the caller before signing and
publication; preserve uncertain outcomes after dispatch. Resume creates
only with matching intent and original custody, and return fresh editable
settings with their version.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex (gpt-6.1-sol) <noreply@openai.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
@kazemek21
kazemek21 force-pushed the kylez_owner-channel-management branch from 9bf4a41 to 0ad48eb Compare November 8, 2026 01:17
kazemek21 added a commit that referenced this pull request Nov 9, 2026
## TL;DR

Removes the six owner-session read tools that let a Buzz-hosted agent
read with its owner's own access: channel, member and DM lists, channel
details, messages and canvases. An agent's own reads and the channel
archive, unarchive and delete changes are unchanged. The tools are gone
from discovery on every harness, and a cached or direct call to one is
refused before anything is read with the owner's session.

Fixes NEXUS-4348 · Initiative:
https://blockcell.sqprod.co/sites/kylez-buzz-mcp-initiative/

## Scope

Kyle's request was to remove "the channel/member/dm list/get" endpoints
and leave the archive/delete channel changes for now. Removing
`owner_read` and the two Canvas tools as well is the initiative lead's
interpretation: they read through the same owner session and handle, and
leaving them would keep the owner-read capability this removes. If that
is wrong, the #2881 half can be restored on its own.

### Removed

From #2745 (owner channels and members):
- `owner_channel_list`, `owner_channel_get`, `owner_member_list`, their
hint text and discovery on Claude Code, Codex, Pi, the session RPC and
the harness kit.
- `src/features/agents/owner-access.ts`, the run handle's `ownerAccess`
field and the runner's host hook that built it for bundled-revision
runs.
- The `BUZZ_OWNER_MCP_COMMUNITIES` build setting (`vite.config.ts`). A
distributor build that still sets it now gets nothing from it;
`docs/configuration.md` lists it with the other retired inputs.
- The owner-tool sections in `docs/agents.md`, `docs/configuration.md`,
`docs/buzz-help.md` and `src/buzz-mcp/AGENTS.md`.

From #2881 (owner messages and canvases):
- `owner_read`, `owner_canvas_get`, `owner_canvas_history`,
`src/buzz-mcp/owner.ts` and the `Owner*` client types.
- The `docs/relay-queries.md` paragraph, and the exports of `tools.ts`
helpers that only `owner.ts` used. `edited()` keeps its author-only edit
selection, minus the owner parameter.

### Kept

- From #2745: the Stop and cancellation fences in the Claude Code, Goose
and ACP sessions (`workLifetime`, `current()`), which every Buzz tool
call now depends on, and the channel-lifecycle changes
(`channel-lifecycle.ts`, including its `records` parameter).
- `addTool` still refuses plugin tool names starting with `owner_`, so a
stale call to a retired name can never reach plugin code. There is no
other owner-prefixed restriction and no new permission layer.
- Ordinary agent-scoped reads, Codex per-thread/turn tool validation,
plugin and external isolation.

## Alternate routes checked

Nothing else reached these tools: no Rust, IPC, sidecar or RPC path, and
Mesh and external agents use the same run handle that no longer carries
owner access. Codex thread tool keys no longer carry the `:owner`
suffix, so threads created with owner tools are replaced instead of
reused with a stale tool list.

## Tests

The new tests list the six names as a fixed fixture rather than reading
them from the catalog:
- `session-tools.test.ts`: `sessionTools()` and `sessionHint()`, which
every harness lists from, name none of the six.
- `service.test.ts`: a real bundled run handle has no `ownerAccess`.
With the owner's session stubbed to return an `OWNER_ONLY` message for
an owner-only channel, each retired name is refused with `No tool
<name>` before any relay query. An ordinary `read` of that channel then
runs with the agent's own key, filtered to that channel, and returns `No
messages.`. The same client's `read` of the agent's own channel returns
its message, and no read through the owner's session ever names the
owner-only channel. External agents see none of the names and are
refused the same way.
- `codex/runtime.test.ts`: `dynamicTools` lists none of the names, each
call fails Codex's validation, a planted owner-access spy is never
called, and the thread's own `read` works.
- `app-client.test.ts`: a handle that still carries an owner capability
doesn't pass it to the tool client.
- The Claude Code and Goose Stop/timer tests now hold an ordinary
`mem_get` read instead of an owner tool.

On a Blox box with these commits applied to main: `tsc` passes, and the
12 affected Vitest files pass (claude, codex, goose, pi runtimes,
app-client, tools, session-tools, service, kit, ACP client,
channel-lifecycle). Full CI covers the rest. `git push --no-verify`
skipped the pre-push lanes (types-and-related-unit-tests, design-system,
rust-clippy); CI's `pnpm check`, sharded Vitest and clippy jobs repeat
all three.

Fail-before check: the removal tests in `service.test.ts` and
`codex/runtime.test.ts` were run against the pre-removal source
(0a5c190). Three of them fail on real assertions:
- Codex `dynamicTools` contains `owner_channel_list`.
- An external agent's call returns "Owner tools are not available to
this agent here; nothing was read." instead of `No tool
owner_channel_list`.
- The bundled handle has `ownerAccess`.

All three pass on this branch. Vitest strips types, so this check
doesn't cover `tsc`. The `session-tools.test.ts` discovery test also
passes on the old source, because that source only lists the tools when
the owner setting is on, so it doesn't count as a fail-before check.

## Not covered

- No native app run or live relay check. The removal is exercised
through the service, runner and harness tests above.
- A Buzz process already running a build with these tools keeps them
until it updates. This doesn't revoke anything remotely.

## Related open PRs

- Human work in overlapping files keeps its behavior but may need a
rebase: #2700, #2974, #3081, #2972 (cancel added tools on native Stop),
#2935, #1634, #2622, #3095, #2882.
- #3004, #3005, #3006, #3008 and #2980 build on the removed owner access
and are not part of this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
(claude-opus-5-5) on behalf of kylez

---------

Signed-off-by: Kyle Zemek <kylez@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@kazemek21

Copy link
Copy Markdown
Contributor Author

Closing without merging. Every owner-channel action in this PR, including archive, unarchive and delete, runs through the owner-access layer, which #3123 removed. Bringing it back would mean reintroducing owner authority, and that work is stopped for now.

Agents' own channel_archive, channel_unarchive and channel_delete tools already exist on main and are unaffected. The earlier cancellation check this PR adds to the shared lifecycle has no current caller on main, so it isn't worth landing alone. The branch is kept as is.

— Posted by Claude (claude-opus-5-5) on behalf of kylez

@kazemek21 kazemek21 closed this Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant