Skip to content

feat(agents): let a bundled agent get its community's media as its owner - #2980

Closed
kazemek21 wants to merge 4 commits into
block:mainfrom
kazemek21:kylez_NEXUS-4300_owner-media-get
Closed

kazemek21 wants to merge 4 commits into
block:mainfrom
kazemek21:kylez_NEXUS-4300_owner-media-get

Conversation

@kazemek21

@kazemek21 kazemek21 commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

Adds owner_media_get, so an agent running in this app on a bundled harness can get one of its community's media blobs signed as its owner, matching the other owner read tools. The bytes stay in native memory. They are saved to a file only after protection has reviewed the answer and the owner binding, sign-in and agent still hold. Agents on remote Codex/Claude targets never see the tool and are refused it before any read.

How it works

  • Stage (app_agent_owner_media_stage, src-tauri/src/app_agents/owner_media.rs)
    • Takes the owner, agent and community from host state, never from the caller.
    • Refuses anything but the agent's own community blob, with the same URL, hash, type and name rules as media_get.
    • Signs the Blossom get auth (kind 24242) only through a purpose-limited IdentityHost::sign_owner_media_get. It does not add a generic signer.
    • Reads into memory:
      • 500 MiB per file and 512 MiB held at once.
      • At most 16 operations.
      • A reservation is made before the body is read. Without a Content-Length the full per-file limit is reserved, and chunks past the reservation are refused. The buffer grows fallibly and never past its reservation.
      • Staged bodies expire lazily after 330 s.
  • Release (owner-access.ts): after the tool-result review, one synchronous segment checks the held binding and the caller check, then invokes native publish.
  • Publish
    • hold_owner keeps the owner signed in.
    • commit_caller takes the existing lock order (IdentityHost → AgentTokens retired → incarnations → FI state → op), re-checks the agent's retirement (including a pending organization sign-out) and incarnation, takes the staged op, and writes the first chunk.
    • The rest is written after the locks drop and persisted to the media path. Nothing named exists before then. The op keeps its memory reservation until the write finishes, so the 512 MiB bound still holds while bytes are being saved.
  • Discard
    • Review denial, a binding change, a cancelled turn or any failure discards the op.
    • A discard that arrives before its stage leaves a bounded tombstone (64), so the late stage is refused.
    • Page reload and window Destroyed clear every read and staged body. A publication already writing its file stays counted until it finishes.
    • No new timer, poll or retry.
  • Remote targets
    • Codex and Claude runtimes strip ownerAccess.media for a remote target.
    • The tool is not listed, and a direct or cached call returns "not available … nothing was read".
    • Pi, Goose, Mesh and kit run locally and keep it. Plugin and external clients still never get ownerAccess.
    • Codex's saved-thread key now includes whether owner_media_get is listed. An existing local owner thread therefore starts fresh once instead of resuming with a stale tool list.

Evidence

  • Rust: 10 new owner_media tests cover:

    • the auth event's pubkey equals the owner;
    • nothing is on disk before publish;
    • another owner is never signed or sent for;
    • foreign URLs and hash mismatches are refused;
    • absent or dishonest lengths are bounded;
    • the shared budget;
    • tombstones that overtake the stage;
    • single take and expiry;
    • no write after deletion, sign-out (including a pending organization sign-out), identity close or a switch to another owner;
    • a publishing op keeps its reservation until the write finishes;
    • the fence order, under controlled interleavings. A test-only pause holds a publication at a chosen point:
      • Held after its review and before the fence, a publication is refused once a deletion, sign-out or closed identity comes first. Nothing is written and the body is dropped.
      • Held inside the fence, a publication makes a sign-out wait. The existing sign-out contention hook observes the sign-out blocked on the retirement lock. The whole file is written, and then the sign-out completes.

    They run against the loopback TLS community fixture, so the host→relay request is real. Result: cargo test --lib filtered to owner_media, identity, relay, federated_identity::agent, app_agents and browser_permissions gave 490 passed, 0 failed, run on Linux Blox. The fence tests passed five repeated runs. Each of three mutants of commit_caller makes one of them fail: dropping the retirement check, dropping the deletion check, and writing after releasing the retirement lock.

  • JS: tests in owner-access.test.ts, owner.test.ts, codex/runtime.test.ts and claude-code/execution.test.ts cover:

    • the release ordering;
    • binding changes before release;
    • cancel during stage;
    • review allow and deny (tool → stage → tool-result → publish/discard);
    • local versus remote discovery and refusal.

    The Claude remote test and the Codex stale-thread test fail without their fixes, as do the Rust pending-sign-out and buffer-capacity cases. Vitest across src/buzz-mcp, src/features/agents, src/features/agent-harness and src/bundled/{codex,claude-code,pi} passed: 2946 tests.

  • Not exercised: the native publish path has not been run in the live app. The fence tests drive the real publish and the real credential locks, not the WebView. A deletion or identity close that arrives inside the fence is ordered by the same locks, but no test observes it waiting.

Overlap

Touches areas also changed by open PRs #2972, #2882, #2336, #2937, #1939, #2950, #2976, #2642, #2700, #1735, #1893, #2953, #2896, #2836 and #2473. Of these, #2882 (streaming protected evidence) is the closest. This PR doesn't stream full bytes to protection; the existing review sees the reply's metadata, path and inline image data only.

🤖 Generated with Claude Code

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

kazemek21 and others added 4 commits November 6, 2026 22:47
owner_media_get reads one media blob of the agent's community signed as
its owner, holds it in native memory, and saves it to the agent's media
folder only once protection has reviewed the answer and the binding,
owner sign-in and agent still hold. Remote Codex and Claude targets do
not list it and are refused it before any read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
- Refuse publication while an organization sign-out's retirement is pending.
- Count a staged or publishing body's capacity until it is dropped, and never
  grow an unknown-length body past its reservation.
- Sign the media read again once the community credential is known.
- Start Codex threads fresh when owner_media_get joins their tools.
- Validate request ids on discard and publish; say nothing was saved when
  saving fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
Replace the ungated lock-order race with two controlled interleavings: a
publication held after review refuses a deletion, sign-out or closed
identity that came first, and a sign-out that arrives while a publication
holds the fence waits for its whole file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
A reload cleared every owner media operation, including a publication still
writing its body, so the next page could reserve a full budget while that
body was live. Keep publishing operations until their publication drops them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
@kazemek21
kazemek21 marked this pull request as ready for review November 7, 2026 09:03
@kazemek21
kazemek21 requested a review from a team as a code owner November 7, 2026 09:03
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 at Kyle's request: MCP work is on hold except channel archive/delete. This change is outside that exception. The branch and existing review/verification evidence are retained; no replacement work is being started.

— Posted by Codex (gpt-6.1-sol) 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