Skip to content

[4/5] Add protection to the Codex harness plugin - #1879

Merged
shellz-n-stuff merged 4 commits into
mainfrom
codex/security-codex-integration
Nov 4, 2026
Merged

shellz-n-stuff merged 4 commits into
mainfrom
codex/security-codex-integration

Conversation

@shellz-n-stuff

@shellz-n-stuff shellz-n-stuff commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Based on #1878. Companion plugin: squareup/buzz-plugin-security#2.

Protected Codex agents run the Security plugin's verified native engine through the host's authenticated protection bridge. Buzz binds the agent's identity, community and policy independently of model content. It checks prepared model input and exact tool arguments, and retires protected sessions before a policy changes. Security reviews that use a Codex model run on the same pinned engine, with no tools, in a home of their own, pinned to the ChatGPT workspace signed in through Buzz.

What this changes

  • Protected sessions. Prepared model input and final tool arguments go to the policy, and tool results return through the next input check. Restored history, dynamic instructions, memory, tool results and unknown evidence stay inspectable.
    • Resuming a saved thread or compacting an inactive one needs the same protection state, in both directions. Protected title generation is off.
    • Final delivery state belongs to the captured request, so overlapping sends and steering can't erase another request's failure or mark a later success failed.
    • Buzz skips its own check of a Buzz tool call that the native gate already reviewed with identical arguments. A call with a captured file body is still checked.
    • Remote control is off (CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1) for the protected engine and the review worker. The unprotected spawn is unchanged.
    • Tool shells get an empty protection bridge address and token (-c shell_environment_policy.set.BUZZ_SECURITY_URL="", and the same for BUZZ_SECURITY_TOKEN), so they don't inherit them. set merges per key, so the user's own shell policy is kept.
  • Buzz's Codex home. The protected engine runs in Buzz's own Codex home, with sqlite_home pinned there, as codex.sh run does. Protected conversations stay out of the user's Codex history, and the Codex app's writer lock can't block them. A new codex.sh home step links in the settings and sign-in, as run does, and prints the home. The sign-in handover never reads or writes through a link (#1878).
  • Retirement. A retirement resolves only once a pending start has settled and the process has exited.
    • No protected start or preview runs for an agent while it retires, or after a failed retirement until a later one succeeds.
    • A failed close is retried by the next retirement.
    • A connection that fails before the process exits (for example, an oversized frame) keeps the server tracked and closes it with the same shutdown. Starts for that agent are refused until the old process exits.
  • Security reviews. There is one review worker per protection provider and workspace, with a short-lived thread for each review, cancelled by interrupt.
    • The worker runs in its own home, ~/Library/Application Support/Buzz/codex-review-home, which codex.sh review makes. The home holds a link to the shared sign-in, the recorded workspace, and four links to a separate state folder for the names Codex writes at startup. The Security plugin makes the home read-only to contained workers.
    • HOME, CODEX_HOME and the cwd are the review home. The thread config turns off MCP servers, web search, plugin features, memories, project docs and skill instructions.
    • Trusted instructions and evidence go in separate fields, and the output schema goes to turn/start. The review has no tool handler, so tool calls and approval requests are refused.
  • Review sign-in. Signing in through Buzz's Codex setup, including the new Sign in again in Settings → Agent setup → Harness defaults → Codex, runs a new codex.sh login step. It runs codex login with CODEX_HOME set to the review home, records the sign-in's ChatGPT workspace there, then hands the sign-in to the shared one. The move into Buzz's home uses the no-follow flag on macOS, as the handover does.
    • Every review re-reads the pin and starts Codex with -c forced_chatgpt_workspace_id="<id>" -c forced_login_method="chatgpt", so Codex refuses an API key or a sign-in recorded for another workspace before it fetches any workspace's cloud config. A changed pin replaces the worker.
    • The sign-in itself stays linked, because Codex rotates refresh tokens and a copy would stop working.
    • Before codex login, Buzz removes the review home's link, so Codex's sign-out before a sign-in revokes nothing and a cancelled sign-in leaves the shared one working. The replaced sign-in is left to expire. The dialog keeps the sign-in's output after it ends.
    • Without a matching pin, reviews fail closed before any worker starts, with a message saying where to sign in again.
  • File reads. Protected Buzz file tools use the app-bundled reader under the captured policy. It accepts ~/…, passes the filename as its own argument, and never falls back to an ordinary reader.
  • Text Buzz doesn't own. Some text Buzz doesn't own goes in a turn in place of its event: plugin-wake and self_handoff text, attention prompts and Solo Canvas text. Plugin notes are escaped where they are stored (runner.note), for every runtime, and Codex's whole turn context (instructions, team, core memory, repositories) is escaped before it becomes developer text. A line in any of these that starts with <buzz-event> or <new-message-arrived-while-you-were-working> is escaped, so the text can't pose as the turn's event record. Codex's own user-role text, such as the workspace AGENTS.md, is handled by the companion, which now splits a Codex block only when it is Buzz's own turn input (squareup/buzz-plugin-security@5b56d68). #2088 adds the tests for the prompt that Claude, Pi and Goose share (claude-code/prompt.ts).

Known gaps and tradeoffs

  • Reviews ignore a keyring sign-in, a custom model provider and the user's config.toml, AGENTS.md, skills and rules. A review that depends on them fails closed.
  • Existing users must use Sign in again once before Codex reviews work. Until then, and while the shared sign-in is an API key or names another workspace, Codex reviews fail closed with a message saying where to sign in. API key and keyring sign-ins get no Codex reviews.
  • A contained worker can still replace the shared auth.json. A forged one that records the pinned workspace with another workspace's tokens is stopped only if OpenAI's servers bind the account_id to the token, which is unverified.
  • A review that starts during a Buzz sign-in waits on the review-home lock until the 15 s step timeout, then fails closed. Reviews work again once the sign-in finishes.
  • The protected engine still reads the shared config.toml, AGENTS.md, skills and rules through links, and its rollouts in Buzz's Codex home are worker-writable. With injection inspection on, resumed history is reviewed as context, not trusted (same as [3/5] Add protection to the Claude harness plugin #1878).
  • Tool shells no longer inherit the bridge token, but a same-user command that reads the engine's own environment can still end the bridge session. Later turns then fail closed: an availability gap, not a protection one.
  • Buzz's stock Codex lists the review models, not the pinned 0.164.0 reviewer. A model only the newer build knows fails at the review turn, closed.
  • A provider replacement closes the old generation's server, and a failure of that close is swallowed. The provider's abort kills that generation's process, which bounds it.
  • The companion still declares its status and review commands and its checker process as env resolved on PATH ([1/5] Add native agent protection and policy boundary #1875 finding A). The review command also records approval decisions. The host refuses absolute program paths, and only host processes can declare a hash-checked artifact. So the checker process could move to one in the companion, while the status and review commands need a Buzz host change first. Tracked in squareup/buzz-plugin-security#4.
  • Protected agents need a release build of the macOS desktop app. In dev builds and on other platforms they fail closed.

Validation

Restack. Rebased with the stack onto main 066e34f12: now 6a2529319 (stack tip b85084bff), still one commit on #1878 (4564d2b68). No conflicts; main's saved Codex session prompts (#1855) sit beside this commit's changes. At the stack tip (b85084bff): pnpm check (Biome, tsc, design checks), the integration suite (248 passed, 1 skipped), the full Vitest run (16,072 passed, 22 skipped), cargo test -p buzzodz-plugins (64), cargo test --lib in src-tauri (841 passed, 2 ignored), Clippy (-D warnings) and cargo fmt --check pass. One src-tauri test was left out: window_state::tests::onboarding_snapshot_is_kept_until_next_launch_restore crashes (SIGSEGV) on main as well (seen at 7cb3cb2c1), and this stack doesn't change window_state. The round-2 checks below ran on top of 8140250ef, before the restack.

Round 2. This round fixes loganj's four round-2 must-fix items (A: connection failure, B: review sign-in, C: handover through links, fixed in #1878, D: forged event openings), turns off remote control, and keeps tool shells from inheriting the bridge.

  • Each fix was checked on top of 8140250ef, before the restack: the Codex plugin, agent harness and agents suites, tsc --noEmit and biome check pass, and bash -n and shellcheck show no new warnings.
  • New tests, each seen failing on the previous code:
    • a protected server whose connection failed stays tracked until its process exits; a failed close of a failed connection is repeated by the next retirement;
    • the protected engine sets the remote-control variable;
    • a plugin note is stored escaped; forged core memory and instructions are escaped in the Codex turn context;
    • codex.sh login records the workspace and hands the sign-in over; it records none for an API key or when Codex saves nothing; review prints no pin once the shared sign-in names another workspace; a cancelled sign-in keeps the shared one; a review waits for a sign-in in progress;
    • the review worker refuses a pin that could escape its config string;
    • after a ready-state sign-in fails, its output stays visible in the dialog.
  • Other new tests: an idle protected retirement logs no clean-up error through its closing connection; the review worker also sets the remote-control variable; the protected engine's exact arguments, including the bridge env. The macOS-only "never moves a review sign-in into a folder the shared sign-in links to" was added with the restack.
  • Agent review: each fix had an independent review. Blockers fixed: Sign in again revoked the shared sign-in before the browser opened, and a ready-state sign-in's failure output disappeared when it ended.

Round 1 and earlier.

Rebased onto main 61a67d28f: now 8140250ef (stack tip 80471b41c). At the new tip, the agent, Buzz MCP, relay, Builderlab, Inbox, Channels and scripts suites (6,882 passed, 22 skipped), tsc --noEmit, cargo test --lib host_process (33), cargo test -p buzzodz-plugins (64), Clippy (-D warnings) and cargo fmt --check pass. The checks below ran before the rebases.

At f01c81006 (stack tip 6b514fff4), on main 6dc462a2d:

  • At this head, the Codex plugin, agent harness and agent protection: 405 passed, 2 skipped, and tsc --noEmit is clean. At the stack tip, the Claude, Codex, Goose and Pi plugins, agent harness, agent protection, agents, Buzz MCP, Builderlab and scripts: 3,000 passed, 18 skipped.
  • At the stack tip: cargo test --lib host_process (33) and cargo test -p buzzodz-plugins (64) pass, and Clippy (-D warnings), cargo fmt --check and biome check are clean.
  • New tests, each seen failing on the previous code:
    • the protected engine starts in Buzz's Codex home; when the home can't be found, the start fails closed and a later one looks again;
    • codex.sh home and codex.sh review, including a review home made again;
    • the review worker's env, cwd and pins; when the review home can't be made, no worker starts and a later review tries again;
    • a retirement stays pending while its server starts;
    • a preview is refused while the previous server closes;
    • no server starts for a standalone turn whose agent was retired while it waited;
    • after a failed retirement, previews are refused and the next retirement repeats the close until the process exits;
    • dispose() closes an unconfirmed server;
    • a failed close() is tried again.
  • Other new tests: wake, attention and Canvas text can't open an event section.
  • Live engine check: I ran the pinned 0.164.0 engine with the review environment. config/read showed no provider, model providers, MCP servers or developer instructions, and no instruction sources. The run also found that skills from a git repository above the review home reached the model; skills.include_instructions: false now stops that.
  • Agent review: each fix had an independent review. Two blockers in the review home were fixed: project docs above the home, and HOME.

Not yet done

  • No packaged-app run.
  • Not checked live (no real Codex account): the -c workspace pin at bootstrap and reload, whether OpenAI's servers bind account_id to the token, and codex login with CODEX_HOME set to the review home. Also not checked live: remote control reporting disabled with the variable set, the bridge env set override against the protected binary, Codex token refresh in the review home, and whether 0.164.0 unloads an ephemeral review thread after its last unsubscribe.
  • Sign in again has not been tried in the native app.
  • CI gap: hosted Vitest runs on Linux, so the macOS-only codex-sh cases (the mv -h folder cases for the handover and the login step) run only on a Mac.
  • Human testing.

Code flow

[4/5] Add protection to the Codex harness plugin — illustrative code flow

Codex gates prepared model input and final tool arguments. Tool results return through the next input gate. The Security plugin supplies its protected engine.

Illustrative flow; checks shown enabled. Source: runtime.ts, native-bridge.ts.

Review and merge bottom to top:

  1. [1/5] Keep protected launches on the startup Agents root (merged)
  2. [2/5] Pi harness integration (merged)
  3. [3/5] Claude harness integration
  4. [4/5] Codex harness integration (this PR)
  5. [5/5] Goose harness integration

Above these: #2088 (exact history records) and #2105 (denial reasons for the owner).

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T01:20:42.461619Z 179ec25 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c66c961bf

ℹ️ 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".

id: TYPE_ID,
hooks: { events: CODEX_HOOK_EVENTS },
retire: (pubkey) => runtime.retire(pubkey),
protection: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Discover models from the protected engine

When a protection provider supplies a pinned Codex engine whose catalog differs from the ordinary bundled binary, model selection still runs through codexRun → runtime.preview(), which calls this.open() without the agent handle and therefore lists models from the unprotected shared server. The picker can consequently accept a model that the protected server later rejects in prepare() as unavailable, preventing protected turns from starting. Route protected-agent previews through that agent's protected server and scope the preview cache to its protection key.

AGENTS.md reference: AGENTS.md:L81-L85

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in e560712. preview() now opens the server through the agent's handle, so a protected agent's models come from its protected engine. It refuses when a protected agent has no live policy binding.

@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-claude-integration branch from fe39c86 to e227fe1 Compare October 29, 2026 14:25
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-codex-integration branch from 5c66c96 to b560bfb Compare October 29, 2026 14:25
@shellz-n-stuff
shellz-n-stuff marked this pull request as draft October 29, 2026 14:27
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-codex-integration branch from b560bfb to e54b2ed Compare October 29, 2026 15:23
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-claude-integration branch 2 times, most recently from 8e2d203 to 42eeb93 Compare October 29, 2026 17:29
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-codex-integration branch 2 times, most recently from 2113f87 to a201b00 Compare October 29, 2026 18:02
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-claude-integration branch 2 times, most recently from 95f8db9 to f782cbb Compare October 29, 2026 20:04
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-codex-integration branch 4 times, most recently from 6ae2c45 to bf3de2a Compare October 30, 2026 02:37
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-claude-integration branch from fa681f1 to cc67ae2 Compare October 30, 2026 16:15
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-codex-integration branch 2 times, most recently from e3e9410 to ba6de7c Compare October 30, 2026 16:23
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-claude-integration branch 2 times, most recently from 0e15873 to 68a6bf9 Compare October 30, 2026 17:37
@shellz-n-stuff
shellz-n-stuff force-pushed the codex/security-codex-integration branch from ba6de7c to d9db7b4 Compare October 30, 2026 17:37
- Connect the Codex harness plugin to protected plugin engines
- Preserve failed Codex deliveries and security review deadlines
- Contain Codex delegated file reads with its active policy
- fix(codex): recover protected workers across provider changes
- fix(codex): review a natively gated Buzz tool call once
- perf(codex): keep one review worker per protection provider
- fix(codex): do not resume saved threads across protection states
- fix(codex): run protected Codex in Buzz's Codex home
- fix(codex): run Codex reviews in a read-only review home
- fix(codex): retire only once a pending start settles and the process exits
- fix(agents): escape event openings in wake, attention and Canvas text
- fix(codex): retire protected servers on a closed connection
- fix(codex): turn off remote control for protected Codex and reviews
- fix(agents): escape event openings in plugin notes
- fix(codex): pin reviews to the ChatGPT workspace signed in through Buzz
- fix(codex): keep tool shells from inheriting the protection bridge

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: Codex <[email protected]>
Signed-off-by: Alex Rosenzweig <[email protected]>
@shellz-n-stuff

Copy link
Copy Markdown
Collaborator Author

🤖 On this note from the round-2 review:

Note: companion 46e5fc5 sends native tool-result items through context. That works while the policy keeps the context and tool-result flags tied.

Agreed. Native tool outputs reach review as context items. That is correct only because policy.mjs:37 ties context and tool-result to the same injection setting. If those phases ever become independent, the companion will need to classify tool-output items as tool-result in the same change. Noted for that change; nothing to change now.

🤖 Addressed by Claude Code

@shellz-n-stuff

Copy link
Copy Markdown
Collaborator Author

🤖 On this note from the round-2 review:

Note: the protected engine still reads the shared config.toml, AGENTS.md, skills and rules through links. [...] Rollouts in Buzz/codex-home are worker-writable, so a resume can load planted history (same as #1878 E).

Right, and this is deliberate. The protected engine keeps the user's Codex config, instructions, skills and rules, unlike Claude's --setting-sources "". Its rollouts are in Buzz/codex-home because the engine is itself a contained worker and has to write them. Making them read-only would mean writing history from outside containment, which is a larger change than this PR. Until then, when injection inspection is on, every model request's full input, resumed history included, is reviewed as context before the model sees it, and, when action review is on, every tool call is reviewed with the conversation. So when injection inspection is on, planted history is reviewed, not trusted. It is the same disclosed gap, with the same answer, as #1878 E.

🤖 Addressed by Claude Code

@loganj loganj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Larry, Auto and Cerveau: round-3 review of 6a25293190f866335f49a8e83ca44e975c28ed23 (one commit on #1878 4564d2b68), companion squareup/buzz-plugin-security at fe00293. Scope: the round-2 must-fix items and defects that the fixes added. Other areas were reopened only on new evidence. CI is green at this exact head.

Result: round-2 A, B and D are fixed. C is unchanged by this PR (tracked as #1878 round-3 item 2). One new must-fix: a contained worker can start turns in a protected Codex thread through the queue DB.

Must-fix

  1. Queued messages in sqlite_home start protected turns that Buzz did not assemble (runtime.ts:676, inline). The Codex 0.164.0 app-server always installs its durable queue when a state DB exists (app-server/src/message_processor.rs:314-319, extensions.rs:82-84). It polls PRAGMA data_version every 10 s and starts a queued UserInput as a turn on an idle thread (ext/queue/src/service.rs:96-111, :239). The DB is <sqlite_home>/queue_1.sqlite, and sqlite_home is Buzz/codex-home, which the companion does not protect (native/engine.py process_protection). The security patch does not touch the queue. Probe (Auto, and Larry reran it on stock 0.166.0): a plain sqlite3 insert into queued_items started turn/started about 7 s later, and the injected text was in the /v1/responses body. An ephemeral thread behaves the same way. A worker can lay the text out like a Buzz turn (<context>, then a <buzz-event> from the owner). Then unframed() never sees it, and the companion splits it into the same 2 sources as a real owner turn, so the tool checks judge actions against a forged owner request. This bypasses the round-2 D fix. Fix: in the Codex patch, do not install the queue (queue_store = None) when BUZZ_SECURITY_URL is set or the process is review-only, or refuse turns whose trigger is the queue. Add a test that inserts a row and expects no turn.

Advice

  1. The review worker accepts a reply from any turn on its thread (security-review.ts:148, inline). Filter item/completed and turn/completed on the turn id that turn/start returned. No practical exploit was found (the thread id is unknown before thread/start, and a queued row runs only when the thread is idle), but the filter is cheap, and item 1's patch change should also cover reviews.
  2. login runs Codex uncontained in a home whose tmp links into worker-writable state (codex.sh:249, links at :226-231, inline). At startup Codex creates <tmp>/arg0-macos, sets it to 0700 and creates a directory in it, following links (arg0/src/lib.rs:333-380). Probe: with $state/tmp/arg0-macos linked to a folder P, codex login status changed P from 755 to 700 and left a codex-arg0* folder in it. Low impact (no worker-chosen bytes), but the round-2 reason for the links ("the reviewer runs no tools") does not cover login. Fix: sign in before the links are made, or in an empty private subfolder.
  3. login moves the new sign-in to a path resolved before the interactive sign-in (codex.sh:259, $home from :69, inline). A worker that swaps Buzz/codex-home for a link while the browser is open sends the user's fresh sign-in to <target>/auth.json, outside containment. Fix: resolve the folder again just before the move and refuse a link (same family as #1878 round-3 item 2). Negligible: lock "$dir-home.lock" (:221) follows a link at that name and can create an empty file.

Fixed or fine

  • A (RPC failure dropped retirement tracking): fixed (Cerveau). runtime.ts:655-663 puts the failed server in unretired synchronously; shutdown (:535-544) keeps it there until a close succeeds; assertRetired (:501-503) refuses a replacement. The original over-4 MiB probe now keeps retirement pending until exit. A replacement with failed EOF and kill keeps the barrier.
  • Tool-shell bridge token: fine (Cerveau). runtime.ts:677-681 sets BUZZ_SECURITY_URL/BUZZ_SECURITY_TOKEN to empty through shell_environment_policy.set, traced through Codex 6aaac694. This stops inheritance only; the docs state that limit.
  • B (shared auth.json and cloud config): fixed locally (Auto). -c is global (config_override.rs:29-35). Every auth load checks tokens.account_id against the pinned workspace (login/src/auth/manager.rs:1262-1281, :1308-1349), cloud config uses only auth that passed it (cloud-config/src/service.rs:200-227), API-key and Bedrock sign-ins are refused, and remote control starts disabled (cli/src/main.rs:1045,1306-1316). A forged file that names the pinned workspace but carries another workspace's tokens depends on the backend and is not re-reported. The cloud-config cache HMAC key is public, but the review home is read-only to workers and the cache is removed before each review.
  • C: unchanged here. No handover changes in this PR. If ~/.codex links to codex-review-home, the next review relinks auth.json and the workspace check fails closed.
  • D (unframed()): fixed (Larry). codex/prompt.ts:65 unframes agent instructions, team and core memory and repositories; runner.ts:733 unframes plugin notes at the source, so Claude, Goose and Codex all get it; unframed is idempotent (plugin-wake.test.ts:74). Tests cover a forged <buzz-event> in a note and in the developer context.
  • login removes the shared sign-in link before codex login, so a cancelled sign-in cannot revoke the shared one; the new sign-in is parsed inside the protected home before the move; the workspace id is checked against [0-9A-Za-z_-] before it goes into -c.
  • Tests: Codex suite 359 passed, 2 skipped (Cerveau, frozen-lockfile install). The Codex tests have no darwin-only skips. Docs state the review home, the workspace pin, the unverified server-side workspace/token match and the bridge-token limit.

Not checked: a packaged containment run with the patched 0.164.0 binary (probes used stock 0.166.0; the queue code is not in the patch), and live inference.

args: [
...(options?.args ?? []),
"-c",
`sqlite_home=${home}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Must-fix. sqlite_home is Buzz/codex-home, which contained workers can write. Codex 0.164.0 always installs its durable queue there (queue_1.sqlite) and starts a queued UserInput on an idle thread within about 10 s (ext/queue/src/service.rs:96-111). A sqlite3 insert into queued_items for the protected thread id started a turn that Buzz did not assemble, so unframed() never sees it and a forged owner <buzz-event> reaches the checks as an owner request. Fix in the Codex patch: no queue store when BUZZ_SECURITY_URL is set or the process is review-only, or refuse queue-triggered turns. Add a test that inserts a row and expects no turn.

| undefined;
if (wire.method === "connection/closed")
fail(new Error("The security model connection closed"));
if (!threadId || params?.threadId !== threadId) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Advice: this accepts item/completed and turn/completed from any turn on the review thread. Filter on the turn id that turn/start (:203) returned. No practical exploit found, but it is cheap hardening.

# new sign-in is written here, where no contained worker can change it
# before its workspace is read. The browser it opens must not keep the lock.
rm -f "$review/auth.json"
CODEX_HOME="$review" "$dir/$version/bin/codex" login 9>&-

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Advice: this codex login runs uncontained with CODEX_HOME=$review, whose tmp/.tmp link into $state (:226-231). Codex creates <tmp>/arg0-macos, chmods it 0700 and creates a folder in it, following links (arg0/src/lib.rs:333-380). Probe: a link at $state/tmp/arg0-macos to folder P changed P to 700 and left a codex-arg0* folder in it. Sign in before the links are made, or in an empty private subfolder.

echo "Codex security reviews can't use this sign-in: it names no ChatGPT workspace." >&2
fi
# As a sign-in made in Buzz's home, which the next step hands to the shared one.
mv -f $nofollow "$review/auth.json" "$home/auth.json"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Advice: $home was resolved at :69, before the interactive sign-in. A worker that swaps Buzz/codex-home for a link while the browser is open sends the fresh sign-in to <target>/auth.json, outside containment. Resolve the folder again just before this move and refuse a link (same family as #1878 round-3 item 2).

@loganj loganj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Round 3 at 6a2529319. Approving: no major blockers.

Logan's direction for this stack (2026-11-04): the reviewer is meant to catch attempts to get around it, so breakout vectors are not blockers, and this prototype needs to land so development can continue. This round is a check for major blockers only. A major blocker is a defect that breaks the reviewer gate itself or breaks agents that are not protected. I read the round-3 change against round 2 and checked CI at this exact head. I did not re-probe each round-2 item.

Round-3 change: the round-2 must-fix A path is addressed. On connection/closed, a protected server whose process has not exited is passed to the new shutdown(), unless a retirement already owns its close (runtime.ts). Tool shells no longer inherit BUZZ_SECURITY_URL/BUZZ_SECURITY_TOKEN, and remote control is turned off for the protected engine. The unprotected Codex path is unchanged apart from the session-prompt records that came from main.

Optional, recorded breakout vectors (out of scope per Logan): round-2 B (review sign-in swap), C (uncontained codex.sh writes through a worker-made link; the ~/.codex link resolved by pwd -P at codex.sh:52 is still open, as noted on #1878) and D (unframed() coverage).

Evidence: CI run 41708338770 is green at this head.

shellz-n-stuff added a commit that referenced this pull request Nov 4, 2026
Based on main. The base fix
[#2044](#2044) and Pi's adapter
[#1877](#1877) have merged.
Companion plugin:
[squareup/buzz-plugin-security#2](squareup/buzz-plugin-security#2).

Protected Claude agents now pause native input and tool calls for the
agent's bound policy. Security reviews that use a Claude model run
tool-free and return a structured verdict. Both run Claude's pinned
release, and the host runs its own verified copy of it. Security reviews
also sign in apart from `~/.claude`, with their own Claude folder and
token.

### What this changes

- **Pinned, host-verified runtime.** The plugin declares Claude 2.1.305
as two macOS artifact processes, `claude-protected-arm64` and
`claude-protected-x64`.
- Each has the release's SHA-256 and the path `~/Library/Application
Support/Buzz/claude-code/2.1.305/claude`. The host expands `~/` under
the user's home.
- For a contained spawn, the host hashes the bytes from one no-follow
descriptor and copies them into its write-denied artifact store, then
runs that copy. A changed file in the download folder doesn't start, and
swapping it after the check changes nothing.
- The plugin picks the process from `uname -sm`. A wrong answer only
names a pin whose checksum then fails.
- Setup checks the pin by starting the host's copy contained, with the
same read-only Claude root as the real launch, and then stopping it. It
never runs the download folder's file outside the sandbox. A pruned
2.1.305 folder doesn't trigger a download while the store copy is valid.
- If the host refuses its copy, setup installs the pinned release
through `claude.sh` and checks again the same way. A failed install step
reports "could not be installed"; a copy the host still refuses surfaces
the host's own reason. The host, not `claude.sh`, verifies and starts
protected Claude. `claude.sh` only installs the pinned release on
request, checking its SHA-256, and keeps 2.1.305 when it prunes.
- A caller waiting for that download stops waiting when its own signal
aborts, and the shared download carries on for later callers.
- **Protected sessions.** Hooks pause native input, tool actions and raw
output for the policy. The native watchdog (350 s) stops a stalled
callback before the CLI's 600 s hook timeout.
- Exact tool arguments and unknown evidence are kept, and prompt and
history representations map to stable message sources.
  - Sessions retire before protection changes.
- An entry made before `sync`, by `session`, `run` or `runOnce`, is
already marked protected, so the "no live policy binding" backstop
applies to it.
- `AgentSessions` tracks every process it starts, from the request to
its exit, including a spare it took. `shutdown()` kills each one and
waits for it, even one still starting. A pool that has shut down starts
nothing.
- A session subscriber that throws is logged and can't stop the others,
so it can no longer break a shutdown before every process is killed and
waited for.
- A failed retirement is dropped rather than kept, so the agent can
start again.
- When the host's hook guard stops a worker for an invalid or oversized
frame, it writes the reason to stderr, so the turn's error names it.
- After a guarded worker exits, the host still delivers its last output,
such as its final `result` or error. Only the guard's timers stop, and
input to an exited worker is still refused.
- **Security reviews.** Reviews run the pinned copy, contained and with
no tools. Trusted instructions are kept apart from evidence. The output
schema goes to native `--json-schema`, and the adapter requires
`structured_output`, so formatted prose can't pass as a verdict.
- Reviews, like protected agents, pass `--setting-sources ""`, so Claude
reads no user, project or local settings file. A contained worker can't
redirect reviews through `env` (such as `ANTHROPIC_BASE_URL`) or
`apiKeyHelper` in `~/.claude/settings.json`.
- **Own folder and sign-in** (Alex's idea of giving reviewers
credentials that contained workers can't reach). Reviews run with
`CLAUDE_CONFIG_DIR` set to `~/Library/Application
Support/Buzz/claude-code-review-home`, and sign in with a `claude
setup-token` token in `CLAUDE_CODE_OAUTH_TOKEN`. So a review reads
nothing from `~/.claude`: no `.credentials.json` and no cached
`remote-settings.json`. The companion creates that folder at 0700 and
lists it in `hiddenPaths`, so contained workers can neither read nor
write it.
- **Setting it up.** The person runs `claude setup-token` in Terminal
and pastes the token under **Sign in for reviews** in **Settings → Agent
setup → Harness defaults → Claude → Edit configuration**. A new
`claude.sh review-token` step reads it from stdin and prints only fixed
lines, so the token never appears in argv, step output or the setup log.
It refuses a folder that is a link or not a folder, and writes `token`
at 0600 through an exclusive temp file and `mv -h`. A `review` step
reads the folder and token back for the reviewer, skips a token that is
a link, and off macOS fails with "Claude security reviews need macOS".
Closing the dialog during a save cancels it, as Cancel does.
- **Fail closed.** With no saved token, a review fails before the
download or any model starts. Clear auth failures
(`authentication_error`, or an expired or revoked OAuth token) map to
one message that also says a Claude subscription is needed.
- **Env hygiene.** The review env nulls `ANTHROPIC_AUTH_TOKEN`,
`ANTHROPIC_BASE_URL`, `CLAUDE_CODE_API_BASE_URL`, `CLAUDE_CODE_SIMPLE`,
`ANTHROPIC_UNIX_SOCKET`, `CLAUDE_CODE_REMOTE_SESSION_ID` and the
`CLAUDE_CODE_USE_*` routes.
- A review's 60 s budget now covers reading its sign-in and waiting for
the pinned release's first download. A longer download fails the review
closed and carries on for later reviews.
- **File reads.** Buzz file tools and the transcript paths a worker
chooses use the app-bundled reader under the captured policy. It accepts
`~/…`, passes the filename as its own argument, and never falls back to
an ordinary reader.
- **Codex launcher fixes that are reachable from here.** `codex.sh`
`run` and `adopt` already run uncontained at this point in the stack, so
two link-following fixes land here rather than in #1879:
- The sign-in handover moves Buzz's sign-in file onto the shared
`auth.json` in one rename (`mv -f -h` on macOS). It no longer reads or
writes through a link, so a worker can't use it to overwrite or copy out
a protected file. A user's own link at `~/.codex/auth.json` is now
replaced by a regular file, as `codex login` does; the Codex README says
so.
- `adopt` skips a saved thread that is a link, creates its copy as a new
file (`set -C`, keeping the time with `touch -r`), and on macOS
publishes it with `ln -P`. A link planted at the temporary name makes
adopt stop without touching the target, and the thread starts new.
- **CI.** `install-protected-claude-test.mjs` checks that both pins
exist in the plugin manifest and match the release manifest before it
checks the download.

### Known gaps and tradeoffs

- Protected agents and Claude-model reviews need a release build of the
macOS desktop app. In dev builds and on other platforms they fail
closed.
- Claude-model reviews sign in only with their own `claude setup-token`
token. That needs a Claude Pro, Max, Team or Enterprise subscription and
lasts a year. API-key, gateway, Bedrock and Vertex setups can't run
Claude-model reviews; choose another review model.
- **Existing users must sign in for reviews once.** Until the review
sign-in is saved, and after it expires or is revoked, Claude-model
reviews fail closed.
- **Release coupling.** Ship the companion change
(squareup/buzz-plugin-security#2) in the same Buzz release that carries
both this PR and [#839](#839). The
companion lists protected paths that may not exist yet, and a host
without #839 refuses a protected launch when a listed path is missing.
Without the companion change, the review folder is not hidden from
contained workers.
- **Process environment.** While a review runs, another process of the
user can read the review token from that review's process environment.
Whether contained agents can read the pinned binary's environment this
way (`KERN_PROCARGS2`) has not been checked. Closing this needs a new
host channel for the token, so it is documented instead.
- **Protected runs still use `~/.claude`.** The review folder applies
only to security reviews. Another contained worker can plant a sign-in,
or cached managed settings that carry hooks and env, in `~/.claude`, and
they reach a protected run's checks. Transcripts in `~/.claude/projects`
can also be swapped between a run's checks or between the CLI's
`--resume` load and Buzz's read for the review. Moving protected runs to
a Buzz-owned folder would also move their sign-in, cached managed
settings and session history, so it is a separate change. Both gaps are
in `agent-protection.md`.
- The review env's null list was read from 2.1.312's strings and is not
yet checked against 2.1.305.
- Review model choices come from the Claude in use, but reviews run
2.1.305. An alias such as `opus` resolves inside 2.1.305, and a model
newer than it may be unknown to it.
- A symlinked home directory, or any symlinked component of the artifact
path, fails closed. Reinstalling doesn't help, because the install
writes to the same path.
- A spare still being handed over when a pool shuts down is killed as
soon as it arrives, but not waited for. Spares serve only unprotected
launches.
- The first protected launch copies the release into the artifact store:
about 273 MB on Apple silicon, 284 MB on Intel. Each review still hashes
the release twice (the contained check, then the launch).
- Older Buzz builds prune 2.1.305 from the shared download folder, so
switching between builds can download it again.
- Pi's protected runtime does not verify its runtime bytes at launch: it
runs the shared host file and Node, made read-only for the worker,
without hashing or copying them as this PR now does for Claude. Fixing
it is a follow-up.
- Planting a program on `PATH` stays outside the documented boundary.
- A tool result Claude echoes over the 4 MiB frame cap, such as a large
PDF or image, stops the turn closed, now with a reason.
- Codex `adopt` keeps small residuals that bash can't close: a rollout
swapped for a link between the check and the read can still be copied,
linked folders along the path can place a new `rollout-*.jsonl`
elsewhere, and noclobber still writes through a link to a FIFO or
device. The handover guards only the last path part, so a linked
`~/.codex` still receives `auth.json` in the folder it points to.
- **Size.** The Claude review sign-in is about 195 production lines,
against a checkpoint budget of about 55. The author chose to keep it as
is.
- Follow-ups not in this PR, tracked in [the follow-up
issue](#2334) with the
protected-run `CLAUDE_CONFIG_DIR` and Pi's runtime bytes:
- a per-hook TypeScript deadline below the native 350 s, which would
make a stalled review's error clearer;
- removing the duplication in `protectedEvents`, and the O(n) source
checks per hook;
- a wall-clock hook deadline. Tokio's `Instant` stops during macOS
sleep. If the CLI's hook timer doesn't stop, a laptop that sleeps
through a pending `PostToolUse` review could let the CLI time out first,
and that hook fails open;
- caching the artifact snapshot's validation in the host, so a review
doesn't hash the release twice;
- carrying the Buzz destination explicitly in natively gated tool
reviews, across Claude, Codex and Goose;
- the shared 256 KiB action cap, which a long protected Claude
conversation can reach.

### Validation

Restacked onto main `066e34f12` with the round-2 fixes: now `4564d2b68`
(stack tip `b85084bff`). Main's saved session prompts
([#1855](#1855)) overlapped the
Claude runtime's session store: both are kept, the store keyed by
protection state and main's `started` prompt save. At the stack tip
(`b85084bff`): `pnpm check` (Biome, `tsc`, design checks), the
integration suite (248 passed, 1 skipped), the full Vitest run (16,072
passed, 22 skipped), `cargo test -p buzzodz-plugins` (64), `cargo test
--lib` in `src-tauri` (841 passed, 2 ignored), Clippy (`-D warnings`)
and `cargo fmt --check` pass. One `src-tauri` test was left out:
`window_state::tests::onboarding_snapshot_is_kept_until_next_launch_restore`
crashes (SIGSEGV) on main as well (seen at `7cb3cb2c1`), and this stack
doesn't change `window_state`. The round-2 checks below ran on the
round-2 changes over `0caf2fd59`, before the restack.

Round 2, on the changes over `0caf2fd59`:

- `vitest run src/bundled/claude-code`: 316 passed, 4 skipped. `vitest
run src/bundled/codex`: 297 passed, 2 skipped. `tsc --noEmit` and `biome
check` on the changed files are clean. `bash -n` and `shellcheck -s
bash` are clean for `claude.sh`; `codex.sh` has only the existing
SC2174.
- `cargo test --lib host_process`: 34 passed. Clippy (`-D warnings`) and
`cargo fmt --check` are clean.
- **New tests, each seen failing on the previous code:**
- the pinned check starts the copy contained, never the download
folder's file;
- a cancelled wait for the pinned copy ends, during the check or the
download, while one download serves the next caller;
  - a review's 60 s budget fires during a held install;
  - a failed install step is reported without checking the copy again;
- a subscriber that throws during retirement no longer stops the worker
being killed;
- a guarded worker's last frame reaches the page after it exits, and the
guard drains an exited worker's output but refuses its input;
- a review without its own sign-in fails closed before any model; a
review signs in with its own folder and token, over other sign-ins; only
a refused sign-in maps to the sign-in message; the save step gets the
token on stdin alone;
- the review sign-in field saves through stdin, keeps its result,
cancels a save from the footer and when the dialog closes, and keeps the
field and Save focusable while a save is pending;
- `claude.sh` saves the review sign-in privately, refuses a linked or
non-folder home and a non-Claude token without printing it, and reads no
sign-in through a link;
- `codex.sh` never writes a handed-over sign-in through a link to a file
or into a linked folder; adopt never copies a linked thread, never
writes through a planted temporary link, and never hard-links what a
swapped-in link points to.
- **CI gap.** The `claude-sh` and `codex-sh` shell tests that depend on
macOS are skipped elsewhere. CI's Vitest job runs on Ubuntu, so CI does
not run them; they ran only locally on macOS.
- **UI check.** Before the last polish, the real Claude configuration
dialog body ran in Chromium through the browser test server: both
themes, 390 px and 1280 px, root text at 200%, keyboard and pointer.
That check found focus loss while a save was pending, and a dialog close
that didn't cancel a save. Both are fixed and covered by the tests
above, but the fixed version was not rendered again.
- **Agent review:** independent reviews of the round-2 changes scored
them 9 or higher with no blockers.

Earlier rounds, at `29828e405` (stack tip `6b514fff4`), on main
`6dc462a2d`, with later rebases onto main `61a67d28f`:

- At this head, `vitest run src/bundled/claude-code`: 281 passed, 4
skipped, and `tsc --noEmit` is clean. At the stack tip, the Claude,
Codex, Goose and Pi plugins, agent harness, agent protection, agents,
Buzz MCP, Builderlab and scripts: 3,000 passed, 18 skipped.
- At the stack tip: `cargo test --lib host_process`: 33 passed. `cargo
test -p buzzodz-plugins`: 64 passed. Clippy (`-D warnings`) and `cargo
fmt --check` are clean.
- `biome check` and `tsc --noEmit` are clean at the stack tip.
- After the rebase onto `61a67d28f`, one conflict: main's recent-notes
read ([#2040](#2040)) now runs
after the protection check at session launch; in protected sessions the
notes reach the policy with the rest of the system prompt. The agent,
Buzz MCP, relay, Builderlab, Inbox, Channels and scripts suites (6,882
passed, 22 skipped), `tsc --noEmit`, `cargo test --lib host_process`
(33), `cargo test -p buzzodz-plugins` (64), Clippy (`-D warnings`) and
`cargo fmt --check` passed there.
- **New tests, each seen failing on the previous code:**
  - a review runs the pinned copy, contained;
- a review installs the pin first, and fails without starting a worker
when that install fails;
- shutdown waits for processes still starting and for a spare it took,
and starts nothing for a conversation still launching;
  - the backstop refuses a protected entry made before `sync`;
  - an agent runs again after its retirement failed.
- **Other new tests:** a review passes `--setting-sources ""`; the hook
guard's stderr reason and its quiet ordinary exit; `~/` artifact and
protection paths; the `uname` mapping for both Mac architectures,
refusing Linux; the manifest pins matching `PROTECTED_CLAUDE_ROOT` and
`PROTECTED_CLAUDE_VERSION`; `claude.sh` keeping 2.1.305 when it switches
past it.
- **Agent review:** an independent review of the earlier round's fixes
found no blockers. An independent review of the settings change scored
it 9/10 with no blockers.

### Not yet done

- No packaged-app run of the sandbox and the artifact copy. Dev builds
refuse contained spawns.
- The CI pin check runs only in hosted CI. I didn't download the release
locally.
- **Deferred live checks.** No pinned Claude 2.1.305 binary or real
Claude account was run for the review sign-in. Not yet checked:
- that `CLAUDE_SECURESTORAGE_CONFIG_DIR` and the keychain don't supply
another sign-in;
- a review with a fresh, empty `CLAUDE_CONFIG_DIR` (trust and onboarding
skip, and Claude writing into that folder);
- `CLAUDE_CODE_OAUTH_TOKEN` taking precedence over stored sign-ins, and
its 401 behavior;
- the real `claude setup-token` output, and the real error texts for an
expired, revoked or non-subscription token;
- whether remote managed settings are fetched into the review folder
under an env token.
- Not checked live: a Claude-model review under the flag with hostile
settings files.
- The companion creating and hiding the review folder was not run
against this build in the app.
- The Codex launcher fixes were checked only in fake homes, not against
a real Codex.
- The review sign-in field in the native app: both themes, narrow and
wide widths, enlarged text, keyboard and pointer.
- Human testing.

<!-- buzz-security-flow:start -->
### Code flow

![[3/5] Add protection to the Claude harness plugin — illustrative code
flow](https://raw.githubusercontent.com/block/buzz-app/f36b5077959f8c565487457947a479525ff4d05f/docs/assets/agent-protection/claude.png)

Claude holds native input, tool actions and raw output. Its 350-second
native watchdog stops stalled callbacks before the 600-second CLI
timeout.

Illustrative flow; checks shown enabled. Source:
[claude.ts](https://github.com/block/buzz-app/blob/4564d2b68/src/bundled/claude-code/claude.ts),
[claude_hooks.rs](https://github.com/block/buzz-app/blob/4564d2b68/src-tauri/src/host_process/claude_hooks.rs).
<!-- buzz-security-flow:end -->

Review and merge bottom to top:

1. [[1/5] Keep protected launches on the startup Agents
root](#2044) (merged)
2. [[2/5] Pi harness
integration](#1877) (merged)
3. [[3/5] Claude harness
integration](#1878) (this PR)
4. [[4/5] Codex harness
integration](#1879)
5. [[5/5] Goose harness
integration](#839)

Above these: [#2088](#2088) (exact
history records) and
[#2105](#2105) (denial reasons for
the owner).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Alex Rosenzweig <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Codex <[email protected]>
Base automatically changed from codex/security-claude-integration to main November 4, 2026 16:55
shellz-n-stuff and others added 2 commits November 4, 2026 10:57
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Alex Rosenzweig <[email protected]>
Co-authored-by: Codex <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Alex Rosenzweig <[email protected]>
@shellz-n-stuff
shellz-n-stuff merged commit 4832bd2 into main Nov 4, 2026
77 of 79 checks passed
@shellz-n-stuff
shellz-n-stuff deleted the codex/security-codex-integration branch November 4, 2026 19:01
shellz-n-stuff added a commit that referenced this pull request Nov 4, 2026
Based on [#1879](#1879). Companion
plugin:
[squareup/buzz-plugin-security#2](squareup/buzz-plugin-security#2).

Protected Goose sessions run the Security plugin's verified native
engine through the host's authenticated protection bridge. Buzz captures
the agent's identity, community and policy independently of model
content. Goose inspects native input, restored history and tool results
before inference, and checks exact tool arguments, with the preceding
conversation, before dispatch.

### What this changes

- **Protected sessions.** Each protected agent gets its own contained
`goose-acp` worker; ordinary agents keep the shared process.
- Native history export maps Buzz conversation keys to the actual Goose
sessions, for ordinary and standalone turns. It reads from the agent's
own worker, with the standard request timeout, and refuses if the agent
has none.
- Protected workers turn off automatic session naming, so a request
isn't inspected twice.
- Buzz skips its own check of a Buzz tool call that the native gate
already reviewed with identical arguments.
- **Retirement.** A protection change needs an acknowledged ACP
retirement.
- `AgentSessions.shutdown()` waits for every session still opening and
every close not yet acknowledged. It then closes each session strictly,
and fails if one could not be closed.
- Callers of `AcpProcess.close()` share a close in flight, and a failed
close stays failed while goose runs.
- A failed retirement leaves the agent stopped. The next retirement
repeats the work until the sessions close.
- A session that goose opened but Buzz failed to set up is closed
through the same tracked strict close, so retirement waits for it.
- A start that fails initialize or the security-hooks check kills its
process and rejects once it has exited, or after 5 s. The shared goose
keeps that process until it exits, so a failed kill is repeated by the
next retirement, and a second `get()` meanwhile starts no second goose.
  - The shared goose stops only after the last agent's retirement.
- Retirement, session changes and disposal cancel pending clients, so a
late approval can't resume work under an old policy.
- **Security reviews.** Configured Goose models give short-lived review
inference with no tools, in one fresh contained worker per review, and
the native engine keeps the credentials. Requests match the strict
native schema, and the reviewer is told the reply schema. The Security
plugin parses the decision.
- The companion lists `config.yaml`, `secrets.yaml`, `custom_providers`
and `config.tmp` (plus the `.tmp` beside the target when `config.yaml`
is a link) read-only whether or not they exist, so a worker can't plant
a provider or redirect a review. When the Goose folder is missing, it
creates the folder (mode 0700) and nothing inside it.
- Goose reviews refuse `github_copilot`, whose token cache sets its API
endpoint, and the `codex` and `cursor-agent` command-line providers. A
review with no provider reads Goose's default in the review worker
itself and is refused when that is missing or refused. The review-model
list hides all three.
- Review models list every provider that answers. One failing provider
no longer empties the list.
- **Host protection paths.** A `readOnlyPaths` entry may now lack its
last component, under an existing parent other than `/`; the host then
denies creating, renaming, hard-linking or symlinking anything at that
name. Hidden paths and controls must still exist. This is a small change
to the shared protection contract in `protection.rs`, documented in
`plugin-architecture.md`.
- **Companion engine patch.** The protected engine also refuses
session-bound and agent-category providers (`codex`, `cursor-agent`),
for agents and reviews alike, not only providers flagged as running
their own tool loop.
- **File reads.** Protected Buzz file tools use the app-bundled reader
under the captured policy; ordinary Goose reads keep the native host
path. Cancelling a held approval checks the captured tool content before
retirement and rejects a late approval. The fixed command accepts `~/…`,
passes the filename as its own argument, and never falls back to an
ordinary reader.

### Release order

- The companion change must ship with or after this PR's host change.
The companion now lists Goose paths that normally don't exist yet (such
as `config.tmp`), so on an older host every protected launch fails with
"Protection path is unavailable".
- `goose-acp` must be rebuilt from the companion's new patch, with its
provenance regenerated, before release. The patch change has not been
compiled yet.

### Size

Round 2 adds about 50 net production lines, not counting comments (Goose
plugin, agent harness and `protection.rs`), against a checkpoint budget
of about 35. Tests and docs are extra.

### Known gaps and tradeoffs

- Sign-in token folders stay writable, so OAuth refresh works. That is
why Goose reviews refuse GitHub Copilot, whose token cache sets its
endpoint.
- Removing the Goose folder makes every protected launch fail closed
until the folder exists again, for example when the plugin restarts and
recreates it. A listed path left as a dangling link does so until the
link is fixed or removed.
- Protected Goose agents can't write `config.yaml` either. I haven't yet
confirmed live that one never needs to.
- The protected `goose-acp` is built separately from the bundled one
that stored the keychain keys, so macOS may ask for keychain access on
first use and after each rebuild. Denied access leaves Goose with
`secrets.yaml`, which contained workers can't change. How often a prompt
appears, and how an unanswered prompt behaves, are not yet checked live.
- `codex` and `cursor-agent` are refused because, going by their source,
contained workers may be able to change the settings their agents run
on. This is not checked live.
- The protected engine pin (`12283408`) is behind Buzz's bundled Goose
1.57.0, so protected agents lag unprotected ones until the pin moves.
The review found no sessions-schema change in the gap.
- The bundled 1.57.0 build lists the review models. A model only that
build knows fails closed in a review.
- Each review cold-starts a contained engine. Concurrency is capped by
the Security plugin (16 assessments in flight) and by each bridge (16
pending checks).
- For an ordinary agent, a failed close stays failed until goose exits.
- A failed start waits at most 5 s for its process to exit. The shared
goose keeps that process until it exits, so while a host never reports
the exit, the shared goose's next start fails with the same error. The
desktop host kills after a 3 s grace.
- Follow-ups, not fixed here: two provider changes before the first
replaced worker exits overwrite the single `stopping` slot, which can
leave an untracked worker; and `protectionContext` calls
`entry.worker.get()` directly, so a tool call during the old worker's
kill grace can start the new worker early. Both are tracked in [the
follow-up issue](#2334), with
the Goose pin plan and the cold-start measurement.
- The capabilities probe proves the patch is present, not that gating is
on. Making it report `enabled` needs a companion engine change.
- The companion still declares its `status` and `review` commands and
its `checker` process as `env` resolved on `PATH` (#1875 finding A). The
`review` command also records approval decisions. The host refuses
absolute program paths, and only host processes can declare a
hash-checked artifact. So the `checker` process could move to one in the
companion, while the `status` and `review` commands need a Buzz host
change first. Tracked in squareup/buzz-plugin-security#4.

### Validation

**Restack.** Rebased with the stack onto main `066e34f12`: now
`cef48c69c`, still one commit on #1879 (`6a2529319`). One conflict, with
main's saved session prompts
([#1855](#1855)): an opened
session records its prompt first, then is closed if the harness was
removed meanwhile. At the stack tip (`b85084bff`): `pnpm check` (Biome,
`tsc`, design checks), the integration suite (248 passed, 1 skipped),
the full Vitest run (16,072 passed, 22 skipped), `cargo test -p
buzzodz-plugins` (64), `cargo test --lib` in `src-tauri` (841 passed, 2
ignored), Clippy (`-D warnings`) and `cargo fmt --check` pass. One
`src-tauri` test was left out:
`window_state::tests::onboarding_snapshot_is_kept_until_next_launch_restore`
crashes (SIGSEGV) on main as well (seen at `7cb3cb2c1`), and this stack
doesn't change `window_state`.

**Round 2**, checked in the fix worktrees before the restack:

- Start-wait fixes (c07d40f, 6044e8a): agent harness ACP and Goose
plugin tests: 314 passed, 8 skipped. `tsc --noEmit` is clean. Their
three tests fail on the unbounded waits, and the shared-worker one also
fails when the worker is forgotten.
- Goose plugin tests in the config fix worktree: 286 passed, 8 skipped.
Goose plugin and agent harness tests in the lifecycle fix worktree: 301
passed, 8 skipped. `tsc --noEmit` and `biome check` on the changed files
are clean in both.
- `cargo test --lib host_process` (35, including the new protection
tests), Clippy (`-D warnings`) and `cargo fmt --check` pass.
- **New tests, each seen failing on the previous code:**
- a read-only path with a missing last component is accepted under an
existing non-root parent, and the macOS kernel test shows a contained
process can't create, rename into, hard-link or symlink at it;
- reviews on `github_copilot`, `codex` and `cursor-agent` are refused
before a worker starts, an unnamed provider uses Goose's default read in
the review worker, and the review-model list hides the refused
providers;
  - a failed kill is repeated by the next retirement;
- a worker whose initialize failed stays owned until it has exited,
though its start rejects at 5 s, and a second `get()` meanwhile starts
no second goose;
- a start whose harness ignores the kill rejects at 5 s and not before,
and its caller is given the harness;
- a protected start whose goose lacks the security hooks and ignores the
kill rejects at 5 s and not before;
- retirement doesn't finish past a failed open's unacknowledged session
close.
- Coverage tests for the review's cancel and deadline paths: a cancel
kills the worker and rejects only after it has exited; a 65 s timeout
kills it and rejects.
- **Companion** at
[`5b56d68`](squareup/buzz-plugin-security@5b56d68):
126 Node tests pass. The Python engine tests ran inside a sandbox that
blocks the real plugin storage; the 4 that failed there need `/bin/ps`,
a nested sandbox or that storage, and the 3 engine tests among them pass
outside it. The relocated-bundle install test hasn't run outside the
sandbox. The engine patch has not been compiled.
- **Agent review:** each round-2 fix had an independent review, and the
blockers it found were fixed.

The checks below ran before round 2.

Rebased onto main `61a67d28f`: now `1b4cd9990` (stack tip `80471b41c`).
At the new tip, the agent, Buzz MCP, relay, Builderlab, Inbox, Channels
and scripts suites (6,882 passed, 22 skipped), `tsc --noEmit`, `cargo
test --lib host_process` (33), `cargo test -p buzzodz-plugins` (64),
Clippy (`-D warnings`) and `cargo fmt --check` pass. The checks below
ran before the rebases.

At `47b80eb6f` (stack tip `6b514fff4`), on main `6dc462a2d`:

- At this head, the Goose plugin and agent harness: 320 passed, 8
skipped, and `tsc --noEmit` is clean. At the stack tip, the Claude,
Codex, Goose and Pi plugins, agent harness, agent protection, agents,
Buzz MCP, Builderlab and scripts: 3,000 passed, 18 skipped.
- At the stack tip: `cargo test --lib host_process` (33) and `cargo test
-p buzzodz-plugins` (64) pass, and Clippy (`-D warnings`), `cargo fmt
--check` and `biome check` are clean.
- **New tests, each seen failing on the previous code:**
- shutdown waits for a session still opening (a conversation, a one-off
turn, and a one-off turn given up);
- shutdown waits for every close, rejects with the one that failed, and
waits for a close that an idle session's eviction started;
- shutdown fails again on retry while a close stays failed, and passes
once goose has exited;
- a close waits for one already in flight, strict and non-strict, and a
failed close stays failed for a strict caller while goose runs;
- an ordinary agent stays stopped after a failed retirement, and
retirement repeats until its sessions close;
- when stopping the worker also fails, the retirement's own error is
reported;
  - the shared goose stops only after the last retirement;
  - the protected-worker retry runs shutdown again;
- a protected tool's session read that Goose doesn't answer is stopped
at the timeout;
  - models still list when one provider fails.
- **Companion** at
[`46e5fc5`](squareup/buzz-plugin-security@46e5fc5):
117 Node and 38 Python engine tests pass, including the new protection
tests. I didn't run the install tests locally, because they would touch
the real plugin storage.
- **Agent review:** each fix had an independent review, and the blockers
they found were fixed.

### Not yet done

- No packaged-app or native end-to-end run.
- The companion's engine patch is not compiled, and `goose-acp` is not
rebuilt from it or its provenance regenerated.
- Not checked live: the keychain prompt for protected `goose-acp`, and
what a denied or unanswered prompt does; protected Goose never writing
`config.yaml`; a protected launch with the missing Goose paths listed
(it should start, and a worker should be unable to create them); the
Copilot, `codex` and `cursor-agent` refusals against real providers; and
review cancellation and deadlines against a real model.
- The review cold-start time is not measured.
- Human testing.

<!-- buzz-security-flow:start -->
### Code flow

![[5/5] Add protection to the Goose harness plugin — illustrative code
flow](https://raw.githubusercontent.com/block/buzz-app/f36b5077959f8c565487457947a479525ff4d05f/docs/assets/agent-protection/goose.png)

Goose gates native conversation input, including results, and checks
tool arguments with history before dispatch. Policy changes require
strict ACP retirement.

Illustrative flow; checks shown enabled. Source:
[runtime.ts](https://github.com/block/buzz-app/blob/1b4cd9990e7c6259dbb3afe4f0dce0a38e632a02/src/bundled/goose/runtime.ts),
[agent-sessions.ts](https://github.com/block/buzz-app/blob/1b4cd9990e7c6259dbb3afe4f0dce0a38e632a02/src/features/agent-harness/acp/agent-sessions.ts).

<details>
<summary>Ownership across the stack</summary>

![Harness plugin, Buzz host and Security plugin
ownership](https://raw.githubusercontent.com/block/buzz-app/f36b5077959f8c565487457947a479525ff4d05f/docs/assets/agent-protection/overview.png)

</details>
<!-- buzz-security-flow:end -->

<details>
<summary>Approved media from earlier snapshots</summary>

These recordings and screenshots predate the current fixes. The
two-agent demo used host `831386161d561678c92c9ce59883162e7261a68c` and
plugin `6d1528a`; the earlier single-agent clips used the snapshots
named below. They illustrate those flows and do not validate this head.

**Independent Claude/Codex approvals; approving Codex leaves Claude
paused.**


https://github.com/user-attachments/assets/fd5d55e8-71a3-490d-a3ac-b6c5c8fab8e3

**Codex continues to its requested summary.**


https://github.com/user-attachments/assets/f690fb96-e8ca-4b8c-866a-098f1254f510

<img width="1200" alt="Test Agent using Claude and Testing Codex each
have a separate pending approval"
src="https://github.com/user-attachments/assets/945dec79-f38f-47bd-a061-c16c5bcabf48"
/>
<img width="1200" alt="After Codex approval, Claude remains paused on
its independent review"
src="https://github.com/user-attachments/assets/7d4acfbb-d677-43ff-9aab-2a95daaf116f"
/>
<img width="1200" alt="Testing Codex has posted the requested summary
after approval"
src="https://github.com/user-attachments/assets/88023e56-6933-4d24-a17f-8ad59d063bbe"
/>

**Earlier single-agent Ready reply (`7c3dba764`).**


https://github.com/user-attachments/assets/a872f726-51c8-4cc4-80a0-7917658ccdd0

**Earlier quoted-injection denial (`9ed4ebda2`).**


https://github.com/user-attachments/assets/4f6ec552-62f1-4a4c-8090-91c02ab7d28d

<img width="1200" alt="Security Worker completes the benign request with
Ready"
src="https://github.com/user-attachments/assets/c1f8e290-5014-42ae-b957-7901090db93a"
/>
<img width="1200" alt="Buzz identifies Security Worker and pauses the
quoted injection for approval"
src="https://github.com/user-attachments/assets/b70c3eb3-7e53-4c6e-b04d-a6c9c8494687"
/>

[Original approved demo
post](#839 (comment)).

</details>

Review and merge bottom to top:

1. [[1/5] Keep protected launches on the startup Agents
root](#2044) (merged)
2. [[2/5] Pi harness
integration](#1877) (merged)
3. [[3/5] Claude harness
integration](#1878)
4. [[4/5] Codex harness
integration](#1879)
5. [[5/5] Goose harness
integration](#839) (this PR)

Above these: [#2088](#2088) (exact
history records) and
[#2105](#2105) (denial reasons for
the owner).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Alex Rosenzweig <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Codex <[email protected]>
johnmatthewtennant added a commit that referenced this pull request Nov 4, 2026
Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Damon <8a08290d889f1c68921542bfe9631d4cd671be9bea5f1114951f386819e4c10a@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* origin/main:
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  feat(search): show member counts on channel results (#2333)
  Fix the mute icon flashing when selecting a channel (#2287)
  feat(agents): open a working agent's transcript from its activity (#2276)
  test(agents): refuse a real relay workflow mention in the harness kit (#2444)
  fix(agents): catch up every event watch on the kinds it watches (#1852)
  Keep local artifact collections current (#1507)
  feat(members): add several people to a chat in one write (#2010)
bradseiler added a commit that referenced this pull request Nov 4, 2026
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>

* origin/main:
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  feat(search): show member counts on channel results (#2333)
  Fix the mute icon flashing when selecting a channel (#2287)
  feat(agents): open a working agent's transcript from its activity (#2276)
  test(agents): refuse a real relay workflow mention in the harness kit (#2444)
  fix(agents): catch up every event watch on the kinds it watches (#1852)
  Keep local artifact collections current (#1507)
  feat(members): add several people to a chat in one write (#2010)

Signed-off-by: Brad Seiler <[email protected]>
michaelneale added a commit that referenced this pull request Nov 4, 2026
* origin/main:
  Open any Inbox filter view by route (#2394)
  fix(bestie): deleting an agent saves no Bestie record where there was none (#2420)
  fix(unread): never treat agents-audience messages as unread (#1552)
  feat(focus): add an optional Focus plugin that quiets everything except Solo chats (#2277)
  [5/5] Add protection to the Goose harness plugin (#839)
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  feat(search): show member counts on channel results (#2333)
  Fix the mute icon flashing when selecting a channel (#2287)
  feat(agents): open a working agent's transcript from its activity (#2276)

Signed-off-by: Michael Neale <[email protected]>

# Conflicts:
#	crates/plugin-manager/tests/management.rs
#	src/app/pages.integration.test.mjs
#	src/features/agent-harness/acp/process.ts
wpfleger96 added a commit that referenced this pull request Nov 4, 2026
…-service

* origin/main: (73 commits)
  fix(bestie): deleting an agent saves no Bestie record where there was none (#2420)
  fix(unread): never treat agents-audience messages as unread (#1552)
  feat(focus): add an optional Focus plugin that quiets everything except Solo chats (#2277)
  [5/5] Add protection to the Goose harness plugin (#839)
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  feat(search): show member counts on channel results (#2333)
  Fix the mute icon flashing when selecting a channel (#2287)
  feat(agents): open a working agent's transcript from its activity (#2276)
  test(agents): refuse a real relay workflow mention in the harness kit (#2444)
  fix(agents): catch up every event watch on the kinds it watches (#1852)
  Keep local artifact collections current (#1507)
  feat(members): add several people to a chat in one write (#2010)
  feat(solo): let viewers join read-only spaces (#2301)
  [3/5] Add protection to the Claude harness plugin (#1878)
  fix(settings): keep the bottom padding on long sections (#2361)
  fix(mermaid): cap diagram output size (#2231)
  feat(agents): give plugin agent types the Buzz side of a turn (#2364)
  feat(plugins): pin buzz://plugin-install links to a path and commit (#2315)
  feat(plugins): tint currentColor SVG page icons like host icons (#2314)
  ...

# Conflicts:
#	src-tauri/src/host_process.rs
#	src-tauri/src/lib.rs
#	src/bundled/claude-code/claude.ts
#	src/bundled/codex/rpc.ts
#	src/bundled/goose/goose.ts
#	src/bundled/goose/runtime.ts
#	src/bundled/goose/sessions.test.ts
#	src/bundled/goose/sessions.ts
#	src/features/agent-harness/acp/process.ts
gilgtm added a commit that referenced this pull request Nov 4, 2026
* origin/main: (22 commits)
  feat(mentions): quieter mention menu that marks agents and their owners (#2328)
  feat: add a Canvas View tab, History diffs and a 256 KiB limit (#2318)
  feat(plugins): let local agents write and reload plugins (#2435)
  feat(onboarding): offer Create a community after public Builderlab sign-in (#2397)
  Open any Inbox filter view by route (#2394)
  fix(bestie): deleting an agent saves no Bestie record where there was none (#2420)
  fix(unread): never treat agents-audience messages as unread (#1552)
  feat(focus): add an optional Focus plugin that quiets everything except Solo chats (#2277)
  [5/5] Add protection to the Goose harness plugin (#839)
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  feat(search): show member counts on channel results (#2333)
  Fix the mute icon flashing when selecting a channel (#2287)
  feat(agents): open a working agent's transcript from its activity (#2276)
  test(agents): refuse a real relay workflow mention in the harness kit (#2444)
  fix(agents): catch up every event watch on the kinds it watches (#1852)
  Keep local artifact collections current (#1507)
  feat(members): add several people to a chat in one write (#2010)
  feat(solo): let viewers join read-only spaces (#2301)
  [3/5] Add protection to the Claude harness plugin (#1878)
  ...

Signed-off-by: Gil Birman <[email protected]>
Co-authored-by: Claude <[email protected]>
shellz-n-stuff added a commit that referenced this pull request Nov 4, 2026
Stacked on [#839](#839). Companion
plugin change:
[squareup/buzz-plugin-security@3fdf502](squareup/buzz-plugin-security@3fdf502),
in
[squareup/buzz-plugin-security#2](squareup/buzz-plugin-security#2),
with the round-2 wake guard in
[`5b56d68`](squareup/buzz-plugin-security@5b56d68).

Protected Claude, Pi and Goose agents re-reviewed a thread's root
message as part of every reply's combined prompt. If that root was
flagged, the whole thread conversation stayed tainted. Codex didn't have
this problem: it sends thread context and the triggering event as exact
JSON records, and the Security plugin checks and remembers each record
on its own.

This PR gives Claude, Pi and Goose the same turn format:

- **Thread context comes first.** It is `<thread-context>` /
`<conversation-context>` JSON holding up to 12 `{id, pubkey, created_at,
content}` records. Each content is cut to 4,000 characters.
- **Routing context and the project section come next.**
- **The event comes last.** It is `<buzz-event>` with the exact event
JSON and a quoted `Request:` line.
- **Steers rename the event section** to
`<new-message-arrived-while-you-were-working>`, so the event stays a
separate record. A turn without exactly one event section, such as a
wake, is wrapped whole as before.

`quoted`, `eventBody` and `readEventBody` now live in
`agent-harness/turn-records.ts`, shared with Codex. The Claude, Pi and
Goose transcript readers and usage titles read the record form first and
still accept the earlier From/Content form, so existing sessions keep
rendering. Claude's history lookup ignores fields quoted inside thread
context, so it finds a session by its own context. `session-debug`,
`export-claude-session` and the session-viewing doc match the new form.

The plugin commit splits these runtimes' messages into those records. It
removes each runtime's transport bookkeeping:

- Pi: `timestamp`.
- Goose: the `msg_` UUIDv4 `id`, `created`, and `metadata.steer`.

An approved message now keeps its fingerprint in later turns and in
other sessions.

Each record, the event included, is checked on its own, as for Codex.
That is what lets an approval be reused per message, and a flagged
record is checked again wherever it appears. The cost: an injection
split across messages that each look harmless isn't seen as one prompt.

### Model-visible change

The human author approved this format change. It also applies to
unprotected Claude, Pi and Goose agents, which now see JSON records with
pubkeys: no display names, no ISO times and no `Parsed:` line. The base
prompt now says so: messages carry pubkeys, a mention takes the pubkey,
and the agent looks a name up with `users` only when it needs to write
one.

### Review fixes, round 1

- **Wake text can't pose as the event record.** The escape lands at the
shared owner in [#1879](#1879): a
line in plugin-wake, `self_handoff`, attention or Canvas text that
starts with `<buzz-event>` or
`<new-message-arrived-while-you-were-working>` is escaped. Here, a
Claude prompt test checks a plugin wake and a self-handoff, and fails
without the escape.
- **Records with U+2028 or U+2029 read back.** `turn-records.ts` and
`export-claude-session.mjs` match each serialized line with `[^\n]*`, as
the companion does; `quoted()` is unchanged.
- **A channel name stays on one line** in the Claude prompt, so it can't
open an event section.
- The Claude prompt header and doc comment describe the shared record
format, and `NAMES_TIMEOUT_MS * 2` is now `CONVERSATION_READ_TIMEOUT_MS`
(4 s) in the Claude, Pi and Goose runtimes.

### Review fixes, round 2

- **A plugin note can't pose as the event record.**
[#1879](#1879) now escapes plugin
notes where they are taken (`runner.note`), with the same escape as wake
text. That closes the steer path and the approval-cache bypass for
Claude plain and steered wakes, Goose and Codex; Pi takes no notes. The
regression test sits at that intake (`agents/service.test.ts` in #1879),
because the runtime tests stub the notes. The companion also lifts no
current event from a turn whose text holds a `<plugin-wake>` or
`<self-handoff>` section; its history is still split.
- **agent-protection.md says a refused input comes back.** Codex records
a turn's input before it is checked, so a refused input is checked again
on each later turn in that thread, and a saved denial refuses each of
them until the owner starts a new session there or has the plugin forget
its saved denials. An input too large to check is refused the same way,
and forgetting doesn't help; Codex keeps refusing it in that thread, and
Claude retries in a new session (corrected in `6c17cd1b9`, see Limits).
- **Tracked as follow-ups, not fixed here:** budget thread context by
encoded bytes in the shared `turnPrompt`, and Pi taking plugin notes (or
agent types declaring whether they do).

### Validation

**Docs correction.** `6c17cd1b9`, on `06bb739ba`, changes only
agent-protection.md: an oversized Claude turn is retried in a new
session rather than staying denied (see Limits). The new stack tip
`a23697c4d` differs from `b85084bff`, where the checks below ran, only
in that file.

Restacked onto main `066e34f12`: now `06bb739ba` (stack tip
`b85084bff`). Main's saved session prompts
([#1855](#1855)) overlapped the
Claude and Pi transcript readers and the Activity section titles: the
readers keep main's prompt display and add this PR's author names, and
Activity keeps main's titles beside the steer tags. At the stack tip
(`b85084bff`): `pnpm check` (Biome, `tsc`, design checks), the
integration suite (248 passed, 1 skipped), the full Vitest run (16,072
passed, 22 skipped), `cargo test -p buzzodz-plugins` (64), `cargo test
--lib` in `src-tauri` (841 passed, 2 ignored), Clippy (`-D warnings`)
and `cargo fmt --check` pass. One `src-tauri` test was left out:
`window_state::tests::onboarding_snapshot_is_kept_until_next_launch_restore`
crashes (SIGSEGV) on main as well (seen at `7cb3cb2c1`), and this stack
doesn't change `window_state`.

Earlier, rebased onto main `61a67d28f`: `d457a35ea` (stack tip
`80471b41c`). One test conflict: the session-start test keeps main's
recent-notes reads and, as before, expects no profile reads. At the new
tip, the agent, Buzz MCP, relay, Builderlab, Inbox, Channels and scripts
suites (6,882 passed, 22 skipped), `tsc --noEmit`, `cargo test --lib
host_process` (33), `cargo test -p buzzodz-plugins` (64), Clippy (`-D
warnings`) and `cargo fmt --check` pass. The checks below ran before the
rebases.

At `9d20d7ff1` (stack tip `6b514fff4`), on main `6dc462a2d`:

- **App:**
- At this head, the Claude, Pi, Goose and Codex plugins and agent
harness: 1,073 passed, 17 skipped, and `tsc --noEmit` is clean.
- At the stack tip, the Claude, Codex, Goose and Pi plugins, agent
harness, agent protection, agents, Buzz MCP, Builderlab and scripts:
3,000 passed, 18 skipped. `biome check` is clean.
  - Mandatory push checks passed at the stack tip.
- **New tests in this round:** `turn-records.test.ts` round-trips
records holding U+2028 and U+2029, and the export test finds those
requests. A channel name with a newline and forged event and steer
openings stays one line. A wake's text can't forge an event record.
- **Cross-repo, earlier round:** a one-off check (not committed) ran the
real `turnPrompt` / `steerPrompt` output through the plugin's
`inspectionSources` for a root, a thread reply, a steer and a later
turn. Every message produced the same check source in the channel
session, the thread session and later turns, and the check failed
against the previous plugin.
- **Plugin** at
[`5b56d68`](squareup/buzz-plugin-security@5b56d68):
126 Node tests pass. For Codex and Claude shapes, a plugin-wake or
self-handoff turn holding an event opening, escaped or not, stays one
source. (At
[`46e5fc5`](squareup/buzz-plugin-security@46e5fc5),
117 passed and an unescaped opening was still lifted.)
- **Agent review:** an independent review of the round-1 fixes found no
blockers. The round-2 changes this PR relies on (the #1879 note escape
and the companion wake guard) were also reviewed by an agent, with no
blockers left.

No browser cases were added or removed.

### Limits and deferred checks

- This has not been run in a packaged app, and there is no human
acceptance yet.
- Pi's live message shape is unverified; the fixtures follow pi-ai's
`UserMessage`.
- Above the plugin's limits (more than 50 messages, or one over 4,000
characters), a message is checked whole, which is the previous behavior.
- A turn over the host's 256 KiB check limit is denied with "Protected
action is too large". Forget doesn't help, because it is a size error.
The next turn resumes the saved Claude session and is denied again on
that prompt. In Claude 2.1.202 and 2.1.312 that happens before Claude
reports its start, so Buzz retries in a new session shown the last 12
messages, each cut to 4,000 characters, as starting a new session would.
An oversized current message fails once. Oversized context fails each
turn until it leaves the last 12, and an oversized Canvas until it
shrinks. `6c17cd1b9` corrects agent-protection.md, which said the
session stays denied until a new one. A tool action whose combined
check, with the whole transcript, exceeds the limit is refused after
Claude starts, with no retry, until a new session. This fails closed and
predates this PR, but thread context makes it easier to reach: each
escaped `<` or `>` costs 7 bytes in the check request, so about 9
context messages of 4,000 `<` are enough. Budgeting thread context by
encoded bytes in the shared `turnPrompt` is a follow-up
([issue](#2334)); it won't cover
a large current message or Canvas. Not checked on the pinned protected
Claude, Pi or Goose.
- Pi has never taken plugin notes, so a note left for a Pi agent is
never delivered and stays in `runner.notes`, though `note()` reports it
as left. Nothing in the app calls `Agents.note` today. This predates
this stack and is a follow-up.
- Claude's own user-role text, such as system reminders, skill bodies
and compaction summaries, is checked whole. The companion now splits a
Claude user message only when it starts with Buzz's turn input, as it
does for Codex (squareup/buzz-plugin-security@fe00293).
- The companion still declares its `status` and `review` commands and
its `checker` process as `env` resolved on `PATH` (#1875 finding A). The
`review` command also records approval decisions. The host refuses
absolute program paths, and only host processes can declare a
hash-checked artifact. So the `checker` process could move to one in the
companion, while the `status` and `review` commands need a Buzz host
change first. Tracked in squareup/buzz-plugin-security#4.
- Claude, Pi and Goose records take `pubkey` and `content` from raw
events (the signer and raw content); Codex takes them from folded rows.
Runtime is in the fingerprint, so approvals don't cross harnesses.
- Codex's own record readers on main (`codex/usage.ts`,
`codex/transcript.ts`, `export-codex-session.mjs`) still reject U+2028
and U+2029. That fix is outside this stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Alex Rosenzweig <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Codex <[email protected]>
wpfleger96 added a commit that referenced this pull request Nov 4, 2026
…r-load-perf

* origin/main:
  Open any Inbox filter view by route (#2394)
  fix(bestie): deleting an agent saves no Bestie record where there was none (#2420)
  fix(unread): never treat agents-audience messages as unread (#1552)
  feat(focus): add an optional Focus plugin that quiets everything except Solo chats (#2277)
  [5/5] Add protection to the Goose harness plugin (#839)
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  feat(search): show member counts on channel results (#2333)
  Fix the mute icon flashing when selecting a channel (#2287)
  feat(agents): open a working agent's transcript from its activity (#2276)

Signed-off-by: Will Pfleger <[email protected]>

# Conflicts:
#	src/features/agent-harness/acp/agent-sessions.ts
shellz-n-stuff added a commit that referenced this pull request Nov 4, 2026
…ed input (#2105)

Stacked on [#2088](#2088).
Companion plugin change:
[squareup/buzz-plugin-security#2](squareup/buzz-plugin-security#2),
which sends the reviewer's reason.

When Auto-decide denied model input to a protected Codex turn, the
thread said only "Codex could not finish", and the owner had nothing to
act on. This lets a protection provider explain a denial to the owner.
The agent's denial and the thread stay generic.

- **Provider contract.** A provider's `check()` may now deny with
exactly `{allowed: false, reason}`. Any other value except `true` still
denies, with no reason.
- Buzz reduces the reason to one line of at most 300 characters.
Control, format and separator characters become spaces.
- It then throws `ProtectionDenied`. That error's message is all the
agent sees, and it stays generic.
  - A cancelled or timed-out check never carries a reason.
- A reply that throws while Buzz reads it, such as a throwing getter or
Proxy, also denies, with no reason.
- **Native bridge.** The bridge keeps the reason for a session's denied
model input (`context` actions only), for up to 16 sessions, until the
runtime takes it. Its reply to the runtime stays `{allowed: false}`.
- It numbers checks as they start. A turn takes only a reason from a
check it started itself.
- A check from an earlier turn, such as an interrupted one, may settle
late. It neither explains the new turn nor replaces that turn's own
reason.
- **Codex.** A turn starts taking reasons when it binds its thread. When
the turn fails, the thread still gets only "Codex could not finish: …".
The reason goes into that session's "Needs attention" detail on the
agent's Activity tab, as plain text, in memory, on the owner's device.
It is never published to the relay, so neither the denied agent nor
other agents read it.
- The reason shows in the failed conversation's own "This
thread/channel" block, the path an owner takes from that conversation,
as well as in Recent. The shared `OpenedConversation` block takes an
optional detail for this; other agent types pass none. That block now
shows the same detail as Recent rows, including the collapsed "Latest
activity" while Working.
- New session clears the conversation's old status and reason once its
binding is deleted. If that write fails, both stay.
- The reason is the provider's text, often written by a reviewer model
that may quote or follow the denied input. Keeping it off the relay
means that text can't reach other agents under the protected agent's
key, or come back to Codex through its thread history.
- **Docs.**
- `docs/agent-protection.md` documents the contract and where the reason
shows.
- `docs/buzz-help.md`, which the `buzz_help` tool bundles, now covers
agent protection. It says only refusing what a protected Codex agent
reads gives a reason, that the reason shows on the Activity tab on this
device, and that people should treat it as information, not
instructions.

Only Codex shows the reason; Claude, Pi and Goose denials are unchanged.

### Known limitations

- **Later turns show less of the reason.** On a later turn that hits a
remembered denial, the plugin puts its own explanation first, so only
about 113 characters of the reviewer's reason fit before the
300-character cut. That wording lives in the plugin.
- **A same-user command can still end the bridge session.** Since #1879
(`6a2529319`), protected Codex gives its tool shells an empty
`BUZZ_SECURITY_URL` and `BUZZ_SECURITY_TOKEN` through
`shell_environment_policy.set`, so they no longer inherit the bridge.
That follows Codex 0.164.0's source and hasn't been run against the
protected binary. A same-user command that reads the engine's own
environment can still find the token. It can't submit checks with it:
the token admits one MCP session, which the app-server claims first. But
a second client stops the bridge, and later turns fail closed. This
affects availability, not protection, and predates this PR.
- **The companion's commands resolve on `PATH`.** The companion still
declares its `status` and `review` commands and its `checker` process as
`env` resolved on `PATH` (#1875 finding A). The `review` command also
records approval decisions. The host refuses absolute program paths, and
only host processes can declare a hash-checked artifact. So the
`checker` process could move to one in the companion, while the `status`
and `review` commands need a Buzz host change first. Tracked in
squareup/buzz-plugin-security#4.

### Validation

**Restack.** Rebased onto #2088's docs correction (`6c17cd1b9`): now
`a23697c4d`, unchanged apart from that agent-protection.md paragraph, so
the checks below at `b85084bff` still apply. Before that, rebased with
the stack onto main `066e34f12`: `b85084bff`, still one commit on #2088
(`06bb739ba`). One docs conflict: the help guide keeps main's
image-upload bullet next to the agent-protection one. The rest of the
patch is unchanged apart from context lines. At the stack tip
(`b85084bff`): `pnpm check` (Biome, `tsc`, design checks), the
integration suite (248 passed, 1 skipped), the full Vitest run (16,072
passed, 22 skipped), `cargo test -p buzzodz-plugins` (64), `cargo test
--lib` in `src-tauri` (841 passed, 2 ignored), Clippy (`-D warnings`)
and `cargo fmt --check` pass. One `src-tauri` test was left out:
`window_state::tests::onboarding_snapshot_is_kept_until_next_launch_restore`
crashes (SIGSEGV) on main as well (seen at `7cb3cb2c1`), and this stack
doesn't change `window_state`. The round-2 checks below ran at
`1728b6399`, before this rebase.

**Round 2, at `1728b6399`.** This round fixes loganj's round-2 must-fix:
the failed conversation's own Activity block showed no reason. It also
takes the three optional items: a reply that throws while read, New
session clearing the view, and the comment nit.
- Vitest: 347 passed across 8 files. Those are the Codex, Claude, Goose
and Pi tabs (`OpenedConversation` is shared), `OpenedConversation`, the
agent-protection service and bridge, and the Codex runtime.
- `tsc --noEmit` and `biome check` pass. Pre-commit and pre-push hooks
pass: types and related unit tests, design-system, Clippy.
- **New tests, each failing on `80471b41c`:**
- A mounted Activity tab with a saved current conversation and a denial
detail containing backticks and a Markdown link. The "This channel"
region shows it as plain text, with no link and no disclosure. Before
the fix, the text was not found.
- A throwing `reason` getter and a Proxy whose `ownKeys` throws each
give a generic `ProtectionDenied`. Before the fix, `check()` rejected
with the provider's own error.
- New session leaves no view after a successful write, and keeps it
after a failed one. Before the fix, the old view remained.
  - An existing test now also checks the block's Working disclosure.
- **Rendered check.** A temporary fixture page, not committed, mounted
the real Codex Activity tab. It ran in Chromium and WebKit, light and
dark, at 360 and 1280 px, at normal and 150% text. The reason wraps as
plain text with no horizontal overflow. The row-to-reason gap matches
Recent's: 8 px, or 12 px at 150%. The Working disclosure is reached by
keyboard and opens with Enter.
- **Agent review.** An Opus review of correctness, design system, tests
and necessity, with each finding adversarially verified, found no
blockers. Taken: the spacing match and the Working assertion. Left out:
- A found-but-unsaved current session can show its detail twice. This
needs another build's rollout and is cosmetic.
- New session on a conversation with no binding keeps its unbound row.
That predates this PR and is a product decision.

**Round 1 and earlier.**

Rebased onto main `61a67d28f`: now `80471b41c`. One docs conflict: the
help guide keeps main's new agent-command bullet next to the
agent-protection one. At the new tip, the agent, Buzz MCP, relay,
Builderlab, Inbox, Channels and scripts suites (6,882 passed, 22
skipped), `tsc --noEmit`, `cargo test --lib host_process` (33), `cargo
test -p buzzodz-plugins` (64), Clippy (`-D warnings`) and `cargo fmt
--check` pass. The checks below ran before the rebases.

At `6b514fff4`, on main `6dc462a2d` (rebased; the patch matches
`73b3b89fd` apart from test context lines):

- Vitest over the Claude, Codex, Goose and Pi plugins, agent harness,
agent protection, agents, Buzz MCP, Builderlab and scripts: 3,000
passed, 18 skipped.
- `biome check` and `tsc --noEmit` are clean, and the Rust host-process
and plugin-manager tests, Clippy and `cargo fmt --check` pass.
- **New tests:**
- A provider's explanation and its normalization, including malformed
replies that deny without a reason.
- A cancelled check whose provider explained its denial. This test fails
if the abort check moves after the denial.
- The bridge keeps reasons per session and only for model input, and
caps them at 16.
- A turn takes only a reason from a check it started. A check from an
earlier turn that settles late neither explains the turn nor replaces
its reason. The tests fail when either guard is removed.
- A denied turn publishes only the generic message, and its Activity
status carries the reason. This test fails on the previous code, which
published the reason.
- **Agent review:** two earlier independent reviews, and one of this
round's change (the reason shows only to the owner), found no blockers.

### Not yet done

- Not exercised in the native app. That needs a build with the plugin
change, a protected Codex agent on Auto-decide, and input the reviewer
denies. The round-2 UI was checked only on the fixture page above.
- Human testing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Alex Rosenzweig <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Codex <[email protected]>
wpfleger96 added a commit that referenced this pull request Nov 4, 2026
…-nip-fi

* origin/main: (23 commits)
  fix(inbox): show Solo responses under All, read live replies in the open conversation, describe toolbar badges (#2275)
  AIDA-2434: Show saved teams in mentions (#2339)
  AIDA-2455: Restore channel scrollbar dragging beside threads (#2436)
  feat(agent-protection): show the owner why a protection provider denied input (#2105)
  fix(search): show slow lookups beside their section headings (#2442)
  Align Bestie DM layout and unread state; speed up thinking shimmer (#2418)
  Send Claude, Pi and Goose history as exact records (#2088)
  fix(agent-activity): align the stopped note and show it after the agent's reply (#2441)
  fix(agents): stop orphaned Attention commands and prevent duplicate runs (#2448)
  feat(mentions): quieter mention menu that marks agents and their owners (#2328)
  feat: add a Canvas View tab, History diffs and a 256 KiB limit (#2318)
  feat(plugins): let local agents write and reload plugins (#2435)
  feat(onboarding): offer Create a community after public Builderlab sign-in (#2397)
  Open any Inbox filter view by route (#2394)
  fix(bestie): deleting an agent saves no Bestie record where there was none (#2420)
  fix(unread): never treat agents-audience messages as unread (#1552)
  feat(focus): add an optional Focus plugin that quiets everything except Solo chats (#2277)
  [5/5] Add protection to the Goose harness plugin (#839)
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  ...

# Conflicts:
#	src/features/identity/builderlab-hosted.test.ts
#	src/features/identity/builderlab-hosted.ts
kazemek21 added a commit that referenced this pull request Nov 5, 2026
## TL;DR
Buzz's Create agent dialog now offers provider-owned Thinking choices
for Claude and Codex before the first config is saved. Explicit
supported efforts persist; Default continues to inherit provider
behavior.

## What
- Add a type-owned Thinking control to the existing create settings
group.
- Use each provider's current effort catalog and loading state for new
agents; preserve saved Thinking on unrelated agent updates. Remote Codex
update reviews keep Thinking read-only.
- Leave unsupported agent types, routing, and global defaults unchanged.

## Risk
The change affects initial agent creation and the two bundled provider
controls. Update reviews preserve saved effort unless the user
explicitly changes Thinking or Model. Routing and global defaults are
unchanged.

## Testing
Synthetic before/after captures use Buzz shell components and fictional
fixtures in Chromium at 1280px and 390px. The create-control fixture
passed in Chromium and WebKit; the 390px WebKit captures below show the
dialog with 20px root text (125% of 16px) and keyboard focus on
Thinking. Native desktop, live-provider execution, and persistence after
an actual created agent remain unverified.

**Light theme, 1280px wide**
Before

![before-light-wide](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/before-light-wide.png)
After

![after-light-wide](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/after-light-wide.png)

**Light theme, 390px narrow**
Before

![before-light-narrow](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/before-light-narrow.png)
After

![after-light-narrow](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/after-light-narrow.png)

**Dark theme, 1280px wide**
Before

![before-dark-wide](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/before-dark-wide.png)
After

![after-dark-wide](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/after-dark-wide.png)

**Dark theme, 390px narrow**
Before

![before-dark-narrow](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/before-dark-narrow.png)
After

![after-dark-narrow](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/after-dark-narrow.png)

**Keyboard focus with 20px text, 390px narrow**

![light-keyboard](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/light-keyboard.png)

![dark-keyboard](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz-app/prs/2409/dark-keyboard.png)

## Related PRs
This PR is based on main and stays independent of adjacent settings
changes in #2105, #1879, and #1878.

## Tracking
Fixes NEXUS-4165

Generated with Codex — posted by Codex (gpt-6.1-sol) on behalf of kylez

Signed-off-by: Kyle Zemek <[email protected]>
Co-authored-by: Codex (gpt-6.1-sol) <[email protected]>
matt2e added a commit that referenced this pull request Nov 10, 2026
…ults (#3188)

Codex's harness defaults (Settings → Agent setup → Harness defaults →
Codex → Edit configuration) showed **"Security reviews need a ChatGPT
sign-in made in Buzz."** and a **Sign in again** button to everyone once
Codex was ready. That happened whether or not a protection provider was
installed, and whether or not a Buzz sign-in had already recorded the
review workspace. After a successful sign-in the dialog looked the same
as before, so it seemed to have failed. Reopening the dialog asked
again.

Public builds ship no protection provider, so this cuts the prompt at
the UI level and leaves the protection runtime alone.

## Before and after

Before is `409b6fa47` (merge base with `main`); after is this branch.
Every surface except the last two is rendered in Chromium from the real
components (`ProviderDialog` with Codex's `ProviderDialogBody`, and
`PluginReadme` with the bundled Codex README). Codex itself is a
simulated launcher (`codex --version`, `login status`, `login`, `review`
and the app server).

### 1. Harness defaults → Codex, ready and signed in outside Buzz

![Harness defaults dialog, Codex ready: before shows the security-review
prompt and Sign in again; after shows
neither](https://github.com/user-attachments/assets/48e65dde-e82d-4e2d-a81f-2ad11a92b79e)

### 2. Harness defaults → Codex, after the first-run Sign in finishes

The dialog while it waits for the browser is unchanged. Once the sign-in
finishes, before still asks for a sign-in; after does not.

![First-run sign-in finished: before shows the security-review prompt
and Sign in again; after shows
neither](https://github.com/user-attachments/assets/8c18ee8c-ba82-4031-a48c-b0b91a9bb94e)

### 3. Settings → Plugins → Codex README

![Codex README paragraph: before mentions Sign in again in its harness
defaults; after drops that
sentence](https://github.com/user-attachments/assets/c8552f52-25dc-4673-9384-bbc98edac0f0)

### 4. Codex security review error (not rendered here)

This error is raised when a protected agent's Codex security review
finds no Buzz sign-in. Only a protection provider plugin shows it, in
that plugin's own surfaces, and none ships with Buzz, so it isn't
rendered here.

| | Text |
| --- | --- |
| Before | Codex security reviews need a ChatGPT sign-in made in Buzz.
Use Sign in again in Settings → Agent setup → Harness defaults → Codex.
API key and keyring sign-ins can't run reviews. |
| After | Codex security reviews need a ChatGPT sign-in made in Buzz,
which Settings → Agent setup → Harness defaults → Codex offers while
Codex is signed out. API key and keyring sign-ins can't run reviews. |

### 5. Buzz help for agents (`docs/buzz-help.md`, not a UI surface)

Agents read this through the Buzz MCP help tool.

| | Text |
| --- | --- |
| Before | …or while Codex's sign-in is for another workspace; once
signed in elsewhere, use Codex's **Sign in again**. API key and keyring
sign-ins can't run them. |
| After | …or while Codex's sign-in is for another workspace. Codex's
harness defaults offer that sign-in while Codex is signed out. API key
and keyring sign-ins can't run them. |

`docs/agent-protection.md` gets the same wording change; it is a
developer doc and isn't shown in the app.

## What changes

- `src/bundled/codex/tabs.tsx`: removes the prompt, its button and the
`signingIn` state. The setup output area goes back to
`hidden={state.readiness === "ready"}`, as it was before #1879. This
block was added only by #1879; the only later change to that part of
`tabs.tsx` was #3165's install copy, which this leaves as is.
- The two tests for the removed button are removed.
- The Codex review error (`security-review.ts`) and the three docs above
no longer point at **Sign in again**.

## What stays the same

- The first-run **Sign in** still runs `codex.sh login`, records the
review workspace and hands the sign-in to the shared one.
- Agent protection, the review home and the review worker are unchanged,
and do nothing without a provider.

## Follow-up

People who use a protection provider and are already signed in to Codex
outside Buzz no longer have a button for the Buzz sign-in that Codex
reviews need. They can sign out of Codex and then sign in from the
dialog. A later change can bring the button back, shown only when a
protection provider is registered and saying whether a Buzz sign-in is
already recorded.

## Validation

- `vitest run src/bundled/codex/ src/bundled/readmes.test.ts`: 461
passed, 2 skipped. `tsc --noEmit` and `biome check` are clean (at
546ae61).
- The screenshots above, from the real components against a simulated
Codex.
- Not yet exercised in the native app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Matt Toohey <[email protected]>
Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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.

2 participants