Repository navigation
[4/5] Add protection to the Codex harness plugin - #1879
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
🤖 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.
fe39c86 to
e227fe1
Compare
5c66c96 to
b560bfb
Compare
b560bfb to
e54b2ed
Compare
8e2d203 to
42eeb93
Compare
2113f87 to
a201b00
Compare
95f8db9 to
f782cbb
Compare
6ae2c45 to
bf3de2a
Compare
fa681f1 to
cc67ae2
Compare
e3e9410 to
ba6de7c
Compare
0e15873 to
68a6bf9
Compare
ba6de7c to
d9db7b4
Compare
- 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]>
8140250 to
6a25293
Compare
0caf2fd to
4564d2b
Compare
|
🤖 On this note from the round-2 review:
Agreed. Native tool outputs reach review as context items. That is correct only because 🤖 Addressed by Claude Code |
|
🤖 On this note from the round-2 review:
Right, and this is deliberate. The protected engine keeps the user's Codex config, instructions, skills and rules, unlike Claude's 🤖 Addressed by Claude Code |
loganj
left a comment
There was a problem hiding this comment.
🤖 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
- Queued messages in
sqlite_homestart 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 pollsPRAGMA data_versionevery 10 s and starts a queuedUserInputas a turn on an idle thread (ext/queue/src/service.rs:96-111,:239). The DB is<sqlite_home>/queue_1.sqlite, andsqlite_homeisBuzz/codex-home, which the companion does not protect (native/engine.pyprocess_protection). The security patch does not touch the queue. Probe (Auto, and Larry reran it on stock 0.166.0): a plainsqlite3insert intoqueued_itemsstartedturn/startedabout 7 s later, and the injected text was in the/v1/responsesbody. 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). Thenunframed()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) whenBUZZ_SECURITY_URLis 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
- The review worker accepts a reply from any turn on its thread (
security-review.ts:148, inline). Filteritem/completedandturn/completedon the turn id thatturn/startreturned. No practical exploit was found (the thread id is unknown beforethread/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. loginruns Codex uncontained in a home whosetmplinks 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-macoslinked to a folder P,codex login statuschanged P from 755 to 700 and left acodex-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 coverlogin. Fix: sign in before the links are made, or in an empty private subfolder.loginmoves the new sign-in to a path resolved before the interactive sign-in (codex.sh:259,$homefrom:69, inline). A worker that swapsBuzz/codex-homefor 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-663puts the failed server inunretiredsynchronously;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-681setsBUZZ_SECURITY_URL/BUZZ_SECURITY_TOKENto empty throughshell_environment_policy.set, traced through Codex6aaac694. This stops inheritance only; the docs state that limit. - B (shared
auth.jsonand cloud config): fixed locally (Auto).-cis global (config_override.rs:29-35). Every auth load checkstokens.account_idagainst 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
~/.codexlinks tocodex-review-home, the nextreviewrelinksauth.jsonand the workspace check fails closed. - D (
unframed()): fixed (Larry).codex/prompt.ts:65unframes agent instructions, team and core memory and repositories;runner.ts:733unframes plugin notes at the source, so Claude, Goose and Codex all get it;unframedis idempotent (plugin-wake.test.ts:74). Tests cover a forged<buzz-event>in a note and in the developer context. loginremoves the shared sign-in link beforecodex 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}`, |
There was a problem hiding this comment.
🤖 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; |
There was a problem hiding this comment.
🤖 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>&- |
There was a problem hiding this comment.
🤖 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" |
There was a problem hiding this comment.
🤖 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
left a comment
There was a problem hiding this comment.
🤖 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.
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]>
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]>
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>  </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]>
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)
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]>
* 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
…-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
* 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]>
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]>
…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
…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]>
…-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
## 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  After  **Light theme, 390px narrow** Before  After  **Dark theme, 1280px wide** Before  After  **Dark theme, 390px narrow** Before  After  **Keyboard focus with 20px text, 390px narrow**   ## 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]>
…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  ### 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.  ### 3. Settings → Plugins → Codex README  ### 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]>
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
CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1) for the protected engine and the review worker. The unprotected spawn is unchanged.-c shell_environment_policy.set.BUZZ_SECURITY_URL="", and the same forBUZZ_SECURITY_TOKEN), so they don't inherit them.setmerges per key, so the user's own shell policy is kept.sqlite_homepinned there, ascodex.sh rundoes. Protected conversations stay out of the user's Codex history, and the Codex app's writer lock can't block them. A newcodex.sh homestep links in the settings and sign-in, asrundoes, and prints the home. The sign-in handover never reads or writes through a link (#1878).~/Library/Application Support/Buzz/codex-review-home, whichcodex.sh reviewmakes. 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_HOMEand the cwd are the review home. The thread config turns off MCP servers, web search, plugin features, memories, project docs and skill instructions.turn/start. The review has no tool handler, so tool calls and approval requests are refused.codex.sh loginstep. It runscodex loginwithCODEX_HOMEset 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.-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.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.~/…, passes the filename as its own argument, and never falls back to an ordinary reader.self_handofftext, 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
config.toml,AGENTS.md, skills and rules. A review that depends on them fails closed.auth.json. A forged one that records the pinned workspace with another workspace's tokens is stopped only if OpenAI's servers bind theaccount_idto the token, which is unverified.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).statusandreviewcommands and itscheckerprocess asenvresolved onPATH([1/5] Add native agent protection and policy boundary #1875 finding A). Thereviewcommand also records approval decisions. The host refuses absolute program paths, and only host processes can declare a hash-checked artifact. So thecheckerprocess could move to one in the companion, while thestatusandreviewcommands need a Buzz host change first. Tracked in squareup/buzz-plugin-security#4.Validation
Restack. Rebased with the stack onto main
066e34f12: now6a2529319(stack tipb85084bff), 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 --libinsrc-tauri(841 passed, 2 ignored), Clippy (-D warnings) andcargo fmt --checkpass. Onesrc-tauritest was left out:window_state::tests::onboarding_snapshot_is_kept_until_next_launch_restorecrashes (SIGSEGV) on main as well (seen at7cb3cb2c1), and this stack doesn't changewindow_state. The round-2 checks below ran on top of8140250ef, 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.
8140250ef, before the restack: the Codex plugin, agent harness and agents suites,tsc --noEmitandbiome checkpass, andbash -nandshellcheckshow no new warnings.codex.sh loginrecords the workspace and hands the sign-in over; it records none for an API key or when Codex saves nothing;reviewprints 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;Round 1 and earlier.
Rebased onto main
61a67d28f: now8140250ef(stack tip80471b41c). 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) andcargo fmt --checkpass. The checks below ran before the rebases.At
f01c81006(stack tip6b514fff4), on main6dc462a2d:tsc --noEmitis 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.cargo test --lib host_process(33) andcargo test -p buzzodz-plugins(64) pass, and Clippy (-D warnings),cargo fmt --checkandbiome checkare clean.codex.sh homeandcodex.sh review, including a review home made again;dispose()closes an unconfirmed server;close()is tried again.config/readshowed 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: falsenow stops that.HOME.Not yet done
-cworkspace pin at bootstrap and reload, whether OpenAI's servers bindaccount_idto the token, andcodex loginwithCODEX_HOMEset to the review home. Also not checked live: remote control reporting disabled with the variable set, the bridge envsetoverride 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.mv -hfolder cases for the handover and the login step) run only on a Mac.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:
Above these: #2088 (exact history records) and #2105 (denial reasons for the owner).
🤖 Generated with Claude Code