Skip to content

Stage the agents that plugin-published skills depend on - #48

Merged
josephschorr merged 2 commits into
mainfrom
skill-plugin-agents
Nov 5, 2026
Merged

josephschorr merged 2 commits into
mainfrom
skill-plugin-agents

Conversation

@josephschorr

Copy link
Copy Markdown
Member

Summary

Many skills are published as part of a Claude Code plugin, and their instructions hand work off to that plugin's own agents. Until now only the skill itself reached the sandbox, so those agents were missing and the skill quietly ran without them. This affects reviewbot's two review skills, among others.

With this change, a skill discovered inside a plugin brings the plugin's agent definitions with it, and Claude Code in the sandbox loads the skill as part of its plugin. The agents then show up under the names the skill already uses.

  • Works for every SkillSource and ClusterSkillSource; nothing is specific to one agent or repository.
  • Only agent definitions travel. Plugin hooks, MCP servers, commands and settings are never staged, so a plugin cannot get around the sandbox's tool controls.
  • Skills that are not part of a plugin behave exactly as before.
  • Set disablePluginAgents: true on a source to stage its skills on their own instead.

Testing

  • Unit, integration and e2e suites pass.
  • Checked end to end against reviewbot's two upstream skill repositories: Claude Code in the staged environment lists every agent both skills dispatch.

A skill published inside a Claude Code plugin often hands work to the
plugin's own agents. Those agents were never staged, so a sandboxed
Claude Code following the skill had nothing to dispatch to.

Discovery now records the enclosing plugin's name and agent definitions on
the Skill, staging carries them into the sandbox, and the Claude Code
launcher loads each such skill as part of its plugin. Only agent
definitions travel; hooks, MCP servers, commands and settings never do.
A source can opt out with disablePluginAgents.
@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
openagentprimitives Ready Ready Preview Oct 30, 2026 8:28pm UTC

Request Review

@samkim samkim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review notes (code review plus a data-security pass, each finding re-checked against the head commit and against the PR description).

Should fix before merge

1. No cap on the number of captured agents, but the CRD allows at most 64 (pkg/controllers/skillsource/discover.go, the agent loop in discoverPlugin; Agents has MaxItems=64 in skill_types.go)
Discovery caps agents by size (64 KiB each, 256 KiB total) but not by count. A plugin with 65 small agents/*.md files makes the apiserver reject the Skill write. upsertSkill errors, the source goes Ready=False, and every skill after this one in the same source stops syncing, including skills that are not in a plugin. That breaks the description's "Skills that are not part of a plugin behave exactly as before", and it can be triggered by an upstream repo change alone.
Suggested fix: cap at 64 the same way the byte cap works (keep the alphabetical prefix) and report the drop as a discovery problem.

2. Plugins from unrelated skills are merged when they share a name (pkg/tools/toolchain/claude/launcher.go, assemblePlugin / BridgeStagedSkills)
The rebuilt plugin lives at ~/.oap/claude-plugins/<manifest name>, and nothing namespaces that name. If one AgentClass enables two skills from different sources that both call their plugin review, their agents are pooled. On an agent-name collision, the first skill in ReadDir order wins, and that order is set by the skill's own name, which its author controls. So skill A can replace an agent prompt that skill B's workflow dispatches (review:bug-hunter). Provenance and authority allowlists cover canonical skill names, not plugin names, and the only signal is a stderr warning. The description says agents "show up under the names the skill already uses"; with a collision they can be someone else's.
Suggested fix: key the rebuilt directory by plugin name plus source identity (canonical repo authority, or a digest of spec.plugin), or refuse to merge across sources and fall back to standalone. Either keeps the intended same-plugin sharing.

3. Every shim run wipes the shared plugins directory without a lock (launcher.go, os.RemoveAll(pluginsRoot) at the top of BridgeStagedSkills)
The runner runs a turn's tool calls in parallel, and the reviewbot prompt suggests separate codelike_claude calls for review-pr and differential-review. One invocation's RemoveAll can delete the directory while another's claude is loading --plugin-dir, or land in the middle of its assemblePlugin so that skill falls back to standalone without its agents. Confirmed by reading the code, not reproduced live.
Suggested fix: build into a per-invocation temp dir and rename into place atomically, or use a per-invocation plugins root.

Minor

  • Frontmatter stripping can be bypassed (pkg/controllers/agentsession/skills_plugin.go, stripAgentFrontmatter): content is passed through unchanged unless it starts with exactly ---\n. A leading UTF-8 BOM or a trailing space on the fence keeps hooks/mcpServers/permissionMode in place (confirmed with a probe). The PR body doesn't promise this, and Claude Code reportedly ignores those fields on plugin agents, but the code comment does promise it. Strip a BOM and trim fence whitespace, or skip the agent when a fence-like first line doesn't parse.
  • Empty frontmatter drops the agent: ---\n---\nbody fails with frontmatter has no closing "---" fence because the search for \n--- misses the fence at offset 0.
  • Plugin files can push a near-limit skill over the 1 MiB ConfigMap ceiling (skills.go), so the whole skill goes off-disk instead of just losing its plugin context. Consider retrying without plugin files on overflow.
  • The frontmatter splitter duplicates skillmd.Parse's; sharing one would fix the two items above in one place.

Nits

  • len(manifest.Agents) > 0 is true for "agents": null and [], so a spurious discovery problem is reported every sync.
  • The reviewbot prompt (examples/reviewbot/manifests/agentclass.yaml) still describes the skills as linked into Claude Code's skill-discovery path. Also worth confirming that a symlinked skill dir inside a plugin is loaded; the e2e check in the description shows the agents load, not the skill itself.

Checked and clean

No path traversal or zip-slip (names are regex-validated at every layer, untarGz keeps regular files only, bundles are digest-verified). Upstream plugin.json hooks and MCP servers don't ride along (manifest re-marshaled with the name only). No RBAC change; logs carry paths and parse errors, not content. Default-on capture with disablePluginAgents as the opt-out matches the stated design.

…ree bridge

- Discovery caps captured agents at the CRD's 64-item limit (alphabetical
  prefix, reported as a problem) so an oversized plugin cannot fail the
  source's sync; an empty or null manifest "agents" is no longer a problem.
- The Claude Code launcher shares a rebuilt plugin only between skills whose
  agents are identical. Skills naming the same plugin with different agents
  are all linked standalone and the conflict is reported, so one source can
  no longer supply the agents another source's skill dispatches.
- Rebuilt plugins live in content-addressed directories built in a temp dir
  and renamed into place, and standalone links are replaced atomically, so
  parallel claude invocations never delete or half-build each other's plugin.
- Frontmatter splitting is shared via skillmd.Split, which tolerates a BOM,
  trailing whitespace on fences and empty frontmatter.
- A skill whose plugin context pushes it past the ConfigMap ceiling is staged
  standalone rather than dropped.
@josephschorr

Copy link
Copy Markdown
Member Author

Addressed in a80fbf4. Agents are capped at 64. Skills naming the same plugin with different agents now all load standalone, and the conflict is reported. Plugin dirs are content-addressed and renamed into place, so parallel runs can't wipe each other's plugin. The minor items and nits are fixed too: shared skillmd.Split (BOM, fence whitespace, empty frontmatter), a standalone fallback on ConfigMap overflow, agents: null, and the prompt wording. I also confirmed locally that a symlinked skill inside a --plugin-dir loads, as <plugin>:<skill>.

@josephschorr
josephschorr merged commit f5914b7 into main Nov 5, 2026
15 of 17 checks passed
@josephschorr
josephschorr deleted the skill-plugin-agents branch November 5, 2026 16:19

This branch was successfully deployed

1 active deployment
Preview — a80fbf4b Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants