镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(plugins): let plugins add skills for agents - #2647

Closed
johnmatthewtennant wants to merge 2 commits into
mainfrom
jtennant/martha/plugin-skills
Closed

johnmatthewtennant wants to merge 2 commits into
mainfrom
jtennant/martha/plugin-skills

Conversation

@johnmatthewtennant

@johnmatthewtennant johnmatthewtennant commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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.

  • Each session lists the skills up front in the Buzz tools' server instructions, as plugin/name: description, so the model knows when one applies.
  • The agent reads a skill's body with 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.
  • Skills go with their plugin's scope, like added tools. Disabling, removing or safe mode drops them. A changed list changes addedKey, which replaces cached sessions the same way a changed tool list does. A changed body is read live.
  • Names are lowercase slugs, keyed by plugin, so two plugins can't collide. skill_get is reserved from addTool.

Validation

  • Red/green: the service test "lists plugins' skills up front and reads each through skill_get until its plugin goes" failed on main (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-mcp and the harness and plugin-development bundles pass on Blox. Biome is clean; tsc is clean apart from the existing CanvasDiff module error.
  • Native end to end, on a macOS Blox workstation running a debug build of 9977f11a0. The run used an isolated local relay (block/buzz ab39923 behind TLS at wss://localhost:3443), because staging has no community configured. It used a fresh identity and a dedicated Claude agent (Opus 5.5).
    1. Added a throwaway third-party plugin, e2e-probe, through Settings → Plugins → Add (folder drop). Its plugin.js makes 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..." }).
    2. Asked the agent in its DM "What is the probe handshake?". The Claude transcript shows mcp__buzz__skill_get described as Skills: 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 to skill_get({"name":"e2e-probe/handshake"}). The reply was exactly the token from the skill body.
    3. Disabled e2e-probe in Settings → Plugins and asked again. The next turn's tool delta removed mcp__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

johnmatthewtennant and others added 2 commits November 5, 2026 13:01
`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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant