Skip to content

feat(agent-command): let local agents discover advertised plugin tools - #2110

Merged
kazemek21 merged 1 commit into
block:mainfrom
kazemek21:kylez_NEXUS-4115_agent-command-discovery
Nov 2, 2026
Merged

kazemek21 merged 1 commit into
block:mainfrom
kazemek21:kylez_NEXUS-4115_agent-command-discovery

Conversation

@kazemek21

@kazemek21 kazemek21 commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

Lets any local agent of the signed-in user (Claude Code, Codex, Pi, Nexus) call a plugin's app-held MCP tools without inheriting a connection from Buzz. A plugin publishes one of its existing host.serveMcp sessions with the bundled buzz-agent-command --advertise. A fresh agent then runs the installed helper by absolute path, and the helper finds that session. There is no pairing, token copy or per-agent MCP configuration.

Contract for plugins

  • Serve a dedicated server: const server = await host.serveMcp(handler).
  • Declare { "id": "agent-access", "program": "buzz-agent-command", "bundled": true, "args": ["--advertise"] } in host.processes (with host.mcp: true). Then call await host.spawn("agent-access", { mcp: server }).
  • The host already wraps this as buzz-agent-command --launch <bundled helper> --advertise. The outer helper claims the server's one session. The inner helper writes that session (loopback URL, bearer and mcp-session-id) to a random *.json in ~/.buzz-foundation/agent-connections (directory 0700, file 0600), then exits.
  • The plugin keeps that server open after the helper exits, and spawns the helper once per server. A second spawn initializes again, and the host then stops the server.
  • Access is same-user by design: every local agent of the user can reach advertised tools, including Buzz-started agents and protected workers. A plugin that must exclude its own protected workers can list the directory in their hiddenPaths.
  • Every same-user agent shares that one session, so the handler must not assume a particular caller.

Contract for agents

  • <installed helper> tools, <installed helper> <tool> --field value and <installed helper> call <tool> '<JSON object>' behave as buzz does inside an agent command.
  • The helper reads every advertisement, newest first, and asks each for tools/list. It merges the catalogs, so the newest server wins a name both offer (also across plugins), and routes each call to the server that offers the tool. A tool call stops asking at the newest server that offers it, so an older server that accepts and never answers does not hold it for the 60 s request timeout; tools still asks every advertisement.
  • On macOS and Linux it reads only files the directory's owner wrote with private permissions. A directory that was once open to others may still hold their files.
  • Another user's process can take a closed server's port, so each request to an advertised session is bound to its own connection. The helper connects, confirms that a process of this account has accepted that same connection, and only then sends HTTP on it; until then the server gets no bytes at all. Each request, including the tool call after discovery, checks its own connection, since the host takes one request per connection.
    • macOS: lsof -a -u <euid> -iTCP@127.0.0.1:<caller port> -sTCP:ESTABLISHED -Fpun must show the server's side (127.0.0.1:<server>-><caller port>), and every listed process must carry that numeric UID. This does not rely on what non-root lsof can see.
    • Linux: the /proc/net/tcp row for the server's side must be established, owned by the euid and attached to a socket (non-zero inode). Before accept, Linux 6.12 shows that row with the overflow UID and inode 0.
    • Checks repeat for about a second while the server accepts. Each lsof run is killed and reaped after 10 s, and the whole request has a 60 s deadline. Missing, partial or failed evidence is a denial.
    • A refused connect or a 404 removes the file. An unconfirmed connection keeps it and reports Could not confirm that this user's Buzz serves an advertised port.
    • Windows reports advertised sessions as unsupported.
    • lsof adds about 0.5 s per request on macOS.
  • Requests now use hyper's HTTP/1 client on that socket. hyper, hyper-util, http-body-util, url and libc were already in Cargo.lock; no versions changed. The helper drops reqwest, whose connector does not expose the socket to check.
  • --help (or no arguments) prints the usage with the path the helper was run as and exits 0, so an agent can discover the commands from the helper itself.
  • An advertisement whose server refuses the connection or answers 404 has a dead server or a reused port. The helper deletes that file. Any other failure, such as the caller's own sandbox denying loopback, leaves the file in place and is reported as Could not reach Buzz: <cause>. The agent's sandbox must allow loopback connections: Codex's read-only and workspace-write modes block them unless network access is enabled.
  • An inherited BUZZ_AGENT_CONNECTION still takes precedence. Buzz-started agents and the existing --launch path behave as before, on every platform, and skip the ownership check.
  • Installed paths: /Applications/Buzz.app/Contents/Resources/bin/buzz-agent-command (macOS; the release bundle name, while some installs use another name such as Buzz Foundation.app), bin\buzz-agent-command.exe (Windows) and /usr/lib/Buzz Foundation/bin/buzz-agent-command (Linux).

The host, FOUNDATION files and the relay buzz CLI are unchanged. No account key is read or written.

Verification

crates/agent-command/tests/discovery.rs runs the real helper binary against a loopback server that copies the host's token, single-session claim and 404 behaviour. It covers:

  • the --launch … --advertise publish path, with private modes;
  • a call with no inherited connection, and an error when nothing is advertised;
  • two concurrent windows, where the newest wins and an older-only tool is still reached, and an older stalled server is never contacted by those calls;
  • pruning of a stopped server, and of a live server that answers 404 for the saved session;
  • skipping a file others could have written, and a server that never accepts the connection, which receives no bytes and keeps its file;
  • on macOS, a caller whose sandbox denies network access gets the cause and leaves the advertisement for others;
  • inherited precedence, and refusal to advertise without an app-held session.

Unit tests cover the lsof and /proc/net/tcp predicates: both sides present, only the caller's side (pre-accept), a wrong or missing UID, a foreign process group next to ours, a non-established state and an unaccepted (inode 0) row.

On the previous head, these mutations failed the tests: reversing the newest-first order, dropping the pruning, dropping the owner and mode check, pruning on any connection error, and asking every advertisement before a call. The tests also passed there inside Codex's workspace-write sandbox with network access.

Real other-user check (one-off Blox boxes). A listener run as nobody through sudo stands in for a foreign process on a reused port. A second run as the current user is the control. The same script also applied each mutation and re-ran the crate tests. Kyle's account was not touched.

Linux 6.12 (uid 2000) and macOS (ec2-macos) nobody listener own-user control crate tests
this head denied, 0 bytes received, file kept answered pass
check omitted received the request (250 bytes) answered integration test fails
server/caller direction swapped received the request answered integration test fails
UID compare ignored Linux: received the request; macOS: still denied by -u answered unit test fails

On macOS, the nobody listener was still denied with both the -u selection and the parser's UID check removed: non-root lsof there did not list another user's socket. So the macOS real negative proves the end-to-end denial, and the unit test is what guards the UID checks.

Locally on macOS, cargo clippy (deny warnings), the crate tests (including the sandbox case) and scripts/test-agent-command.mjs pass.

Outside CI:

  • Real host round trip on this head (Linux Blox box). A scratch host_mcp test, not committed, drove the debug helper against the real HostMcpServers. It covered --launch <helper> --advertise, a fresh tools, and a tool call (tools/list, then tools/call with its arguments), all through the new client and ownership check.
  • Real host lifetime (Linux Blox box, previous head). A scratch test, not committed, used the release helper against the real HostMcpServers. It served two windows' servers, each advertised with --launch <helper> --advertise. A fresh env -i call reached the newest server, including four concurrent calls. After close on the newest server, calls fell back to the older one and the stale file was removed. After stop_all (page reload or app exit), calls failed with "No running Buzz…" and no files remained. A re-served server advertised and answered again. A second spawn on a claimed server stopped that server.
  • Fresh external agent (macOS, previous head). codex exec (gpt-6-luna, low) ran with no inherited connection and was told only the helper's installed-layout path. It ran --help, listed the tools, created a Solo chat and listed chats, all exiting 0, against a loopback fixture with the host's token and session rules.

Not covered here

  • No plugin advertises yet. Publishing from buzz-nexus and its Solo tools is a separate change.
  • Windows does not add .exe to a bundled program name, so a plugin must declare buzz-agent-command.exe there. Calls through advertisements fail as unsupported on Windows. Neither has been exercised there.
  • App-background lifetime (window closed while the app keeps running) is not exercised.
  • Any process of this account that accepts the connection is trusted, as any same-user agent already is. The check does not identify Buzz itself.

Fixes NEXUS-4115

🤖 Generated with Claude Code

@kazemek21
kazemek21 force-pushed the kylez_NEXUS-4115_agent-command-discovery branch from 03b14fe to b5f0f7f Compare November 1, 2026 15:38
@kazemek21
kazemek21 marked this pull request as ready for review November 1, 2026 15:59
@kazemek21
kazemek21 requested a review from a team as a code owner November 1, 2026 15:59
@kazemek21
kazemek21 force-pushed the kylez_NEXUS-4115_agent-command-discovery branch from b5f0f7f to 23550f7 Compare November 1, 2026 16:02

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.

Reviewed head 23550f7c664792983741d53a868618453ffdef31 against base ca836a3109be30a7fd46db97bdcceaf32ebabf65 (merge base 923a80f4aeed4ab7a671c4df62f10b3a81bf15bc).

[P2] Remove the internal coordination link from the public PR description. The “Initiative context” section links an internal status application and names its internal task. This repository and PR are public. Please remove that link/task context or replace it with a public-safe summary; I have deliberately not repeated the address here. This concerns public PR material, not contributor attribution.

The inline findings cover stale advertisements trusting a replacement loopback listener (P2) and external helper help directing callers to a different executable (P3). Optional follow-ups: use a shorter discovery-specific timeout so one stalled plugin does not delay every healthy command by up to 60 seconds; add a cross-link to the new advertisement contract from the plugin architecture guide. These are not required changes.

Source-only: no helper, tests, builds or app workflows were executed by this review. The new Rust integration tests add no browser cases and use the appropriate native boundary; their fixture does not itself enforce second-initialize shutdown, so the PR’s separately reported host/agent exercises remain author-reported evidence. Windows behavior, packaged/plugin lifetime and runtime latency remain unverified here. The one CI snapshot had Rust/tool integration successful, two WebKit shards still running and Windows validation skipped. The linked issue could not be read, so intent was assessed from the PR description and repository docs. No attached images were present in the description. This COMMENT is not an approval or merge recommendation.

Comment thread crates/agent-command/src/main.rs Resolved
Comment thread crates/agent-command/src/main.rs Resolved
@kazemek21
kazemek21 force-pushed the kylez_NEXUS-4115_agent-command-discovery branch from 23550f7 to d7bd3dd Compare November 1, 2026 16:37
@kazemek21

Copy link
Copy Markdown
Contributor Author

On review 5729028348's body:

  • [P2] Internal coordination link. Removed. The description no longer has the "Initiative context" section; the verified body is at head d7bd3dd.
  • Optional: shorter discovery timeout. Not taken in this PR. A shorter cap would turn a busy but healthy plugin into a false Could not reach Buzz, and only a server that accepts the connection and then stalls hits the 60 s cap. Refused or closed ports fail at once.
  • Optional: architecture-guide cross-link. Not taken. docs/agents.md ("Tools for the user's other local agents") is the single home for the contract.

Reopen if you'd like either optional item taken here.

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

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage.

Reviewed head d7bd3dda6f8869eb6174be3fda278b1f13719007 against base ca836a3109be30a7fd46db97bdcceaf32ebabf65, comparing the fix with previously reviewed head 23550f7c664792983741d53a868618453ffdef31.

[P2] The existing listener-authentication finding remains partially unresolved: #2110 (comment). In crates/agent-command/src/main.rs:258–262, ownership is sampled once before sequential catalog requests. A server can close and its port change owners while discovery awaits another server; the cached port still passes :274–276, and :293 sends the saved headers to the replacement. The host closes connections after each request (src-tauri/src/host_mcp.rs:264–267), so the later tool call (main.rs:169–175) also needs a current authenticated connection. Bind server/peer identity verification to the actual connection before sending saved credentials or tool arguments; refreshing the initial snapshot only narrows the race. Add coverage for a listener changing ownership after discovery starts—the new wildcard-listener case covers only the state before the snapshot. This is follow-up evidence on the existing finding, not a second issue.

The prior external --help finding is repaired in source, and the public description’s internal coordination link has been removed. No additional actionable findings. Optional: preserve the underlying connection error when the ownership check cannot run, so sandbox failures remain easier to diagnose.

Source-only: no tests, builds, helper or app workflows executed. The native integration tests are the appropriate layer; this follow-up adds no browser cases. Runtime behavior, Windows safety and the reported macOS latency remain unverified here. The one CI snapshot still had Rust, JavaScript and browser work running; Windows validation was skipped. The referenced issue could not be read (403); intent was assessed from the PR and repository docs. No attached images were present. This COMMENT is not an approval or merge recommendation.

@kazemek21
kazemek21 force-pushed the kylez_NEXUS-4115_agent-command-discovery branch from d7bd3dd to e8da62f Compare November 1, 2026 16:50

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source follow-up via Wes’s account.

Reviewed head e8da62f68152949d310cbde3b5285576b50692f4 against base ca836a3109be30a7fd46db97bdcceaf32ebabf65, comparing with previously reviewed head d7bd3dda6f8869eb6174be3fda278b1f13719007.

This revision only clarifies the installed macOS app name in docs/agents.md and docs/buzz-help.md (3 added lines, 2 removed). Implementation and tests are unchanged. The documented helper location matches the configured resource layout; no new actionable findings in this documentation follow-up.

The existing P2 listener-authentication finding remains unresolved because its implementation is unchanged: previous follow-up and repair guidance. No duplicate inline finding added.

Solo follow-up under the limited-change exception; the preceding implementation review had Rocket, Mantis and Groot coverage. Source-only: no tests, builds, helper or app workflows executed. No browser cases added or removed and no focus behavior changed. Public description inspected; no attached images. The CI snapshot shows Chromium shard 8/9 and the required aggregate failing, with Windows skipped; failure cause and runtime behavior were not investigated in this documentation review. This COMMENT is not an approval or merge recommendation.

@kazemek21
kazemek21 force-pushed the kylez_NEXUS-4115_agent-command-discovery branch from e8da62f to 5d0c8a7 Compare November 1, 2026 17:16

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage.

Reviewed head 5d0c8a7a3bce604286826800f663db1dfb3d23be against base ca836a3109be30a7fd46db97bdcceaf32ebabf65, comparing endpoint trees with previously reviewed e8da62f68152949d310cbde3b5285576b50692f4.

The fix now checks listener ownership before each advertised request, including the later tool call, and reports failures to run the check. That repairs the discovery-length stale snapshot and missing call-time check. No new actionable findings.

The existing P2 listener-authentication finding remains partially unresolved: original finding. In crates/agent-command/src/main.rs:81–96, the ownership observation still precedes a separate connection and send. If the original server closes and another OS user obtains the port between them, tool arguments can reach that replacement and its output can be trusted. The PR now accurately discloses this narrower race; disclosure does not establish that the peer is authenticated. The remaining repair is connection-bound server identity verification before sending credentials or arguments, potentially requiring a host/transport change as the author notes. This updates the existing finding rather than adding a duplicate. The final scope/risk decision belongs to the human maintainers.

Source-only: no tests, builds, helper or app workflows executed. Tests are unchanged; the wildcard-listener case checks ownership before discovery, not a transition between discovery and a call. Native integration is the appropriate regression layer; zero browser cases added or removed, and no UI focus surface changed. The one CI snapshot had Rust/tool integration successful, browser shards still running and Windows validation skipped. Platform behavior and the author-reported macOS latency remain unverified here. Public description and commit inspected; no attached images or new public-material findings. NEXUS-4115 was not independently consulted this cycle; intent was assessed from the PR and repository docs. This COMMENT is not an approval or merge recommendation.

@kazemek21
kazemek21 requested a review from a team November 2, 2026 20:30
Comment thread crates/agent-command/src/main.rs Resolved
@kazemek21
kazemek21 force-pushed the kylez_NEXUS-4115_agent-command-discovery branch from 5d0c8a7 to 69fa4c5 Compare November 2, 2026 21:17
A plugin can spawn the bundled helper with `--advertise` inside its existing
app-held MCP connection; the helper saves that session to a same-user-private
file. A local agent Buzz did not start (Claude Code, Codex, Pi, Nexus) runs the
installed helper with no inherited connection and calls the newest advertised
session that still answers, dropping stale ones. Before sending an advertised
session anything, the helper confirms on macOS and Linux that a process of this
account accepted that same connection; Windows reports advertised sessions as
unsupported. Inherited connections keep precedence.

Fixes NEXUS-4115

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kyle Zemek <kylez@squareup.com>
@kazemek21
kazemek21 force-pushed the kylez_NEXUS-4115_agent-command-discovery branch from 69fa4c5 to 0f0526d Compare November 2, 2026 22:24
@kazemek21
kazemek21 merged commit 0cdafb1 into block:main Nov 2, 2026
75 of 77 checks passed
dr0uleau added a commit that referenced this pull request Nov 2, 2026
Co-authored-by: Codex <noreply@openai.com>

* origin/main: (26 commits)
  AIDA-2336: Show when worktree folders are required (#2165)
  AIDA-2326: Reveal full emoji completion names (#2172)
  feat(agent-command): let local agents discover advertised plugin tools (#2110)
  feat(bestie): a Bestie tab on your Bestie's agent page (#2034)
  fix(relay): notify verified profile changes once per replay (#2209)
  feat(shortcuts): open Shortcuts settings with Mod+/ (#2022)
  Re-send live channel subscriptions on access grants, not every minute (#2207)
  perf(profiles): count verified namesakes once per publish (#2159)
  feat(claude-code): start sessions with the agent's recent notes (#2040)
  fix(relay): skip status, emoji, read-state and reminder re-reads a continuous replay covers (#2204)
  Support agent credentials through the macOS debug helper (#2174)
  refactor: centralize relay search debounce and require two characters (#2200)
  fix(relay): drop unused profile exemptions from config and agent reads (#2202)
  fix(relay): skip full profile and roster re-reads after a continuous reconnect (#2203)
  Notify when Solo chat agents finish (#2011)
  feat(mcp): start a Solo chat with channel_create (#2128)
  fix(relay): keep profiles when channel access ends (#2187)
  Wait for thread readiness before keyboard input (#2181)
  fix(builderlab): unify the development sign-in (#2109)
  feat(host): add byte requests, private saves and POST dispatch protection (#2084)
  ...

Signed-off-by: Daniel Rouleau <drouleau@squareup.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants