Repository navigation
feat(plugins): let plugins add skills for agents - #2647
Closed
johnmatthewtennant wants to merge 2 commits into
Closed
johnmatthewtennant wants to merge 2 commits into
johnmatthewtennant wants to merge 2 commits into
Conversation
`ctx.agents.addSkill({ name, description, body })` adds a Markdown
skill to every agent session. Sessions list each skill's description up
front, keyed plugin/name, and read its body through a `skill_get` tool
on the Buzz tools, so Claude, Codex, Goose and Pi get it the same way
with no harness skill folder written. Skills go with their plugin's
scope, and a changed list replaces sessions through addedKey as tools
do.
Co-authored-by: Martha <73923046810b68ae182000e74ef7c444383a907df20f81f2ba1008870a6f45a1@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@block.xyz>
skill_get's description listed skills in registration order, and addedKey hashes that description, so two plugins starting in a different order replaced saved sessions with nothing changed. List them by key. Co-authored-by: Martha <73923046810b68ae182000e74ef7c444383a907df20f81f2ba1008870a6f45a1@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: John Tennant <jtennant@block.xyz>
kazemek21
added a commit
that referenced
this pull request
Nov 6, 2026
…uzz tools (#2707) ## TL;DR An approved external app (like Nexus) now gets the selected agent's whole Buzz MCP tool catalog and native dispatcher, including tools plugins add later, after one explicit "full access" approval. Before, it got a hand-picked list of six tools with rewritten arguments. Older narrow approvals still connect but grant no tools until the owner approves full access again, so no existing grant widens on its own. Initiative status: https://g2.sqprod.co/apps/kylez-nexus-buzz-plugins-status Fixes NEXUS-4262 ## What changes - **One scope, `agent`.** New links request `scope=agent`, and `agent` can't be combined with other scopes. Rust pairing accepts only `["agent"]` for new approvals (`valid_scopes`). Saved `read`/`post`/`create` approvals still load, renew and resume, but grant nothing. - **No adapter.** `respondExternal` now passes `tools/list` and `tools/call` to `respond` in `rpc.ts`, using the same `sessionTools(added)` catalog as native sessions. Plugin-added tools come from `addedTools.snapshot()` (the source native harnesses use) and are re-read on every message, so enabling or disabling a plugin changes the catalog live. The old policy map, argument rewriting and Bestie injection are removed. - **The agent's own authority.** The connection uses the agent's run handle (`runners.handle(key, undefined, true)`), so it gets device settings, plugin requests, Git access and handoff exactly as a native run does. Plugin installs still go through the owner's confirmation. Memory reads use the agent's `memoryListing`, like the native plugin path. Protected agents are still refused. `plugin_disable` acts at once, as it does natively, and the consent text says so. - **Handoff with true provenance.** `self_handoff` goes through the run's own handoff and `Agents.wakeIn`, but its `from` is `{ external: true }`. `wakeSection` shows it as an `<app-wake>` section that says an external app the owner approved sent it, and that it is not a message, a watch or timer, or another of the agent's conversations. - **Files where the agent reads them.** `send` with `path`/`files` calls a new optional `AgentType.readFile`. Claude, Codex, Pi and goose implement it with the reader their own tools use, in the agent's saved workspace on this device. Relative paths start there; absolute and `~/` paths reach any file the account can read, as they do natively, and the consent text says so. A Claude or Codex agent bound to an execution target refuses, because only a conversation's session opens that target. A type without `readFile` refuses with the same message its tools give natively. Closing the connection cancels a read in progress. - **No implicit conversation.** Calls run with an empty context, so every tool needs explicit channel or message IDs. - **Consent.** The dialog now states full access plainly: future plugin tools, Git access, waking the agent, Nexus workers, settings changes, posting any file the agent can read (including outside its workspace), and immediate plugin disabling. A legacy narrow link shows an update-required notice with Deny only, which sends the normal denial callback. `approve()` refuses non-`agent` requests as a second check. Agent settings flags a saved legacy approval as no longer working. ## Companion-client contract (for the Nexus consumer) | | Behavior | | --- | --- | | Request | `scope=agent` alone. Narrow scopes get the update-required notice and only a denial callback. | | `tools/list` | The full native catalog: 66 built-in tools with settings, plus plugin tools. Each tool is listed as natively, plus `_meta: {"buzz/requiredScopes": ["agent"]}`. Result `_meta`: `buzz/grantedScopes` (`["agent"]`, or `[]` for a legacy approval) and `buzz/supportedScopes: ["agent"]`. Cursors are rejected (single page). | | `tools/call` without the grant | `isError` result, nothing dispatched, `_meta["buzz/error"] = {code: "authorization_required", required_scopes: ["agent"]}`. | | `tools/call` with the grant | Native result and errors, unchanged. | | `self_handoff` | Native arguments. The wake reaches the agent as an `<app-wake>` from the app. | | `send` `path`/`files` | Read by the agent's type in its saved workspace. Agents on an execution target, and types without a reader, return their refusal. | | `channel_create` | Takes native arguments. A private Solo chat with the owner's Bestie must now pass `visibility: "private"`, `solo: true` and `members` explicitly. Buzz no longer fills them in. | An unupdated consumer that accepts only `read`/`post`/`create` will reject `supportedScopes: ["agent"]`. Its upgrade path is to accept `agent` and request `scope=agent`, which takes one owner approval. ## Overlap These open PRs touch nearby files. This PR neither depends on nor replaces any of them: - #2260 and #2685 cover external pairing and identity. - #2647 adds plugin skills to `session-tools`. - #2622 covers Bestie settings. - #1939, #1909, #1854, #2311 and #2070 change `service.ts`/`runner.ts`. - #2610 has no shared files. ## Validation Ran on a Linux Blox box against the current head: - `tsc --noEmit` and `tsc -p tsconfig.design.json`; - Biome on the touched files; - Vitest for `src/buzz-mcp`, the external connection, dialog and settings tests, `AgentsPage.test.tsx`, `service.test.ts`, `plugin-wake.test.ts` and the Claude, Codex, Pi and goose runtime tests. New tests cover the external handoff's trigger and `<app-wake>` wording, file reads through the type's reader (target workspace, cancellation on close, native refusal), the Claude and Codex workspace and execution-target cases, a saved narrow approval that a link asks to resume as `agent`, and memory read from the selected agent. The Rust pairing tests (`host_mcp::pairing`) need the Tauri system libraries, so they are left to PR CI. No browser journeys were added or removed. Independent security reviews traced authorize, approve, resume, renew and call, and found no path that grants tools without the `agent` scope. Not done: - The native desktop consent dialog and a live external connection have not been exercised against this build, including a live `<app-wake>` turn and a file read in a running Claude, Codex, Pi or goose agent. - No human testing yet. - The Nexus side of the contract is a separate change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Kyle Zemek <kylez@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plugins can add agent tools and harness prompts, but not skills: there is no way for a plugin to give agents a recipe they read only when it applies. Switchboard's manual operator recipe, for example, lives in one harness's skill folder on John's Mac, so only that harness on that Mac has it.
What
ctx.agents.addSkill({ name, description, body })adds a Markdown skill to every agent session while its plugin is on.plugin/name: description, so the model knows when one applies.skill_get({ name: "plugin/name" }). It's served by the Buzz tools, so Claude, Codex, Goose and Pi get it the same way, and nothing is written to a harness skill folder that could outlive the plugin.addedKey, which replaces cached sessions the same way a changed tool list does. A changed body is read live.skill_getis reserved fromaddTool.Validation
plugin.agents.addSkill is not a function) and passes now. It covers validation, the reserved name, the up-front listing in the session hint, reading a body, an unknown name, and removal when the plugin's scope ends. "keys the added skills whatever order plugins started in" failed before skills were sorted (expected 'ufd0zn' to be '117p8ib') and passes now.src/features/agents,src/buzz-mcpand the harness and plugin-development bundles pass on Blox. Biome is clean; tsc is clean apart from the existing CanvasDiff module error.9977f11a0. The run used an isolated local relay (block/buzzab39923behind TLS atwss://localhost:3443), because staging has no community configured. It used a fresh identity and a dedicated Claude agent (Opus 5.5).e2e-probe, through Settings → Plugins → Add (folder drop). Itsplugin.jsmakes one call:ctx.agents.addSkill({ name: "handshake", description: "How to answer when the owner asks for the probe handshake.", body: "...reply with exactly this token..." }).mcp__buzz__skill_getdescribed asSkills: e2e-probe/handshake: How to answer when the owner asks for the probe handshake., the same listing in the Buzz server instructions, and one call toskill_get({"name":"e2e-probe/handshake"}). The reply was exactly the token from the skill body.e2e-probein Settings → Plugins and asked again. The next turn's tool delta removedmcp__buzz__skill_get, nothing called it, and the agent answered that it no longer had a skill for the handshake. The token was not in the reply.🤖 Generated with Claude Code