OpenHands / OpenHands/software-agent-sdk

Spec: Support the Agent Plugins (agent-plugins.org) portable package format

Open
#4,405 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement mcp Needs Design plugins skills
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Background

Agent Plugins is a new open, vendor-neutral standard (v1.0.0, Working Draft) for packaging reusable components that extend AI agents into portable plugins. Its initial Technical Steering Committee includes Core Maintainers from Amazon, Cursor, Microsoft, OpenAI, and Vercel. Compatible clients today include VS Code, Cursor, GitHub Copilot, ChatGPT & Codex, and Kiro — OpenHands is not yet in that list.

The standard defines a small interoperability floor on top of two existing specs we already consume:

So the heavy lift is mostly about the package/manifest layout and discovery contract, not new component technology.

The portable package

An Agent Plugin is a directory with a required manifest and optional components in fixed locations:

my-plugin/
├── plugin.json            # required manifest (root-level, not nested)
├── skills/                # immediate child dirs each containing SKILL.md
│   └── summarize/
│       ├── SKILL.md
│       ├── scripts/
│       └── references/
├── mcp.json               # root-level MCP config (NOT .mcp.json)
└── com.example.client/    # reverse-domain client extension namespace
    └── hooks/
How this differs from our current (Claude Code-style) layout

OpenHands currently follows the Claude Code plugin structure (openhands.sdk.plugin):

Concern OpenHands today Agent Plugins v1.0.0
Manifest location .claude-plugin/plugin.json or .plugin/plugin.json (nested) plugin.json at plugin root
Manifest schema Open extra="allow", our own fields Closed schema; only $schema, name, version, description, author, homepage, repository, license, keywords, extensions; unknown top-level fields reported-and-ignored (non-fatal)
MCP config file .mcp.json (dotfile, root) mcp.json (no dot, root)
MCP schema { "mcpServers": {...} } (FastMCP shape) { "$schema", "mcpServers": {...} } with type field (stdio / streamable-http / sse), literal headers, plugin variables ${PLUGIN_ROOT} / ${PLUGIN_DATA}
Skills skills/<name>/SKILL.md ✅ (same) skills/<name>/SKILL.md ✅ (same)
Client extensions N/A (we have commands/, agents/, hooks/) Reverse-domain top-level dirs + extensions.<namespace> manifest data

The skills discovery rule is already identical, which is the good news.

Scope of this issue

This issue is a spec / design task, and the implementation should be split into follow-up issues. The goal is to make OpenHands a conformant Agent Plugins client.

Per the conformance checklist, a conformant client:

  1. Establishes the filesystem-resolved plugin root.
  2. Locates and validates root plugin.json using the locally supported schema selected by $schema (does not retrieve the schema over the network).
  3. Rejects on fatal manifest violations; reports-and-ignores unknown top-level fields and a non-object extensions.
  4. Discovers each supported component type from its fixed location (skills/, mcp.json).
  5. Applies the narrow failure boundary per component type / entry (skip one skill, disable one MCP server, etc. — never nuke the whole plugin for a single bad component).
  6. Applies implemented client-extension namespaces and ignores all others.

A conformant client must support at least one of skills or MCP servers (we want both).

What needs to be specced

1. Dual-format loading & backward compatibility
  • Decide whether to support both the Claude Code layout (.claude-plugin/plugin.json + .mcp.json) and the Agent Plugins layout (plugin.json + mcp.json) during a transition window, or to auto-detect by probing for a root plugin.json first.
  • Per channel discussion, we likely need to maintain backward compatibility with Claude Code plugins for a while. The spec should define the detection/precedence rules (e.g., root plugin.json wins; fall back to .claude-plugin/ / .plugin/).
  • Map PluginManifest to the closed Agent Plugins schema while preserving our extra fields (entry_command, commands, agents) under a client extension namespace (e.g., io.openhands / dev.openhands) per §8.
2. Manifest validation
  • Validate plugin.json against the plugin schema locally (vendored, never fetched at load time).
  • Enforce name constraints (1–64 chars, lowercase ASCII letters/digits/hyphens/periods, no -- or .., alphanumeric start/end).
  • Implement the non-fatal vs. fatal violation split:
    • Non-fatal: unknown top-level field (report + ignore), non-object extensions (report + ignore).
    • Fatal: any other schema violation → reject the whole plugin, do not discover/execute components.
  • Handle $schema: select locally supported validation/interpretation rules; reject if the declared Agent Plugins version is unsupported.
3. Package boundary & path containment (security)
  • All package-resolved paths must remain within the filesystem-resolved plugin root after resolving symlinks/junctions/reparse points.
  • Plugin-relative paths must begin with ./, resolve against the plugin root, and stay inside it.
  • Apply the narrowest failure boundary (§4.1): reject plugin if plugin.json escapes; disable a component type if its fixed location escapes; skip one skill if its SKILL.md escapes; skip one MCP entry if command/cwd escapes; deny other escaping paths.
  • Note: this governs package-supplied files, not subprocess sandboxing — but it must compose cleanly with our existing sandbox/runtime model.
4. MCP config (mcp.json) mapping
  • Read mcp.json (no dot) at the plugin root in addition to .mcp.json.
  • Support transports: stdio (required-ish), streamable-http (required-ish; we should support both), sse (optional).
  • Implement plugin variables ${PLUGIN_ROOT} and ${PLUGIN_DATA}:
    • PLUGIN_ROOT = absolute, filesystem-resolved plugin root.
    • PLUGIN_DATA = dedicated writable data dir, persisted across plugin updates.
    • Textual, single-pass, non-recursive expansion in args, env values, and cwd only — not in command, env keys, remote URLs, or HTTP headers.
    • Client-controlled PLUGIN_ROOT/PLUGIN_DATA set last, overlaying configured env; plugins cannot override them.
  • Remote: validate absolute HTTP(S) URLs (no user-info/fragments); non-loopback must be HTTPS; headers are literal visible package data, no credentials/secrets.
  • Failure isolation: invalid top-level mcp.json disables MCP for the plugin; an invalid/unavailable individual server disables only that entry; continue loading siblings + other component types.
  • Map to our existing MCPServer / FastMCP config without losing the Agent Plugins transport semantics.
5. Skills discovery (already largely aligned)
  • Discover immediate child directories of skills/ whose SKILL.md resolves to a regular file.
  • Do not recursively search deeper descendants for additional skills.
  • Validate SKILL.md against the Agent Skills spec (name must match parent dir, description ≤1024 chars, etc.).
  • Missing skills/ is not an error; wrong filesystem kind disables the skills component type only.
6. Client extensions
  • Recognize our own reverse-domain namespace(s) (e.g., io.openhands) under extensions and in top-level extension directories.
  • Map our Claude-Code-origin concepts that aren't in the portable core — commands/, agents/, hooks/hooks.json, entry_command — into our namespace.
  • Ignore namespaces we don't implement without validating their contents (§8).
7. Discovery, installation & UX (client-owned, out of portable spec)
  • The standard deliberately leaves these to each client. Spec how OpenHands will:
    • Discover plugins (our existing load_user_plugins / load_project_plugins / installed dir + marketplace).
    • Install/enable/update/disable/uninstall with the persisted PLUGIN_DATA guarantee.
    • Surface permissions/trust prompts and sandboxing policy (compose with our confirmation policy + security analyzer).
    • Present skills to users/models.

Proposed deliverables

  • Foundation: extract plugin loading into a PluginFormat strategy (base class + ClaudeCodePluginFormat, detect_format()) so a second format can be added without touching the merge/apply path (OpenHands/software-agent-sdk#4420).
  • ADR / design doc covering detection precedence, manifest mapping, path containment, MCP variable expansion, failure boundaries, and the backward-compat window for Claude Code plugins.
  • Vendored JSON schemas (plugin.schema.json, mcp.schema.json) + local $schema selection logic.
  • Implementation issue: Agent Plugins manifest loader (root plugin.json, closed schema, fatal/non-fatal split). (OpenHands/software-agent-sdk#4450)
  • Implementation issue: mcp.json loader + ${PLUGIN_ROOT}/${PLUGIN_DATA} expansion + PLUGIN_DATA persistence. (OpenHands/software-agent-sdk#4451)
  • Implementation issue: client extension namespace (io.openhands) mapping for commands/agents/hooks/entry_command. (OpenHands/software-agent-sdk#4452)
  • Implementation issue: path-containment enforcement + narrow failure boundaries. (OpenHands/software-agent-sdk#4453)
  • Tests against the official example plugin and the conformance checklist. (OpenHands/software-agent-sdk#5156)
  • Reach out to the Agent Plugins project to get OpenHands listed on the Compatible Clients page once conformance is met. (OpenHands/software-agent-sdk#5159)

Open questions

  1. Do we support sse (deprecated) in addition to stdio + streamable-http?
  2. Exact reverse-domain namespace to claim (io.openhands vs dev.openhands vs com.openhands)?
  3. How long is the Claude Code backward-compat window, and do we eventually deprecate .claude-plugin/ / .mcp.json?
  4. Should Agent Plugins be loaded ambiently (like .skills/project plugins) or require explicit opt-in given the path-containment + subprocess implications?
  5. Where does PLUGIN_DATA live on disk and what owns its lifecycle (creation, persistence across updates, cleanup on uninstall)? Analogous to ~/.openhands/plugins/installed/, but nothing today defines this. Blocks OpenHands/software-agent-sdk#4451.
  6. MCPServer.headers is typed dict[str, SecretStr] (mcp/config.py:512), but Agent Plugins headers must be literal, visible, non-secret package data. Needs a load path that doesn't misrepresent plugin-declared headers as secrets. Blocks OpenHands/software-agent-sdk#4451.

References


This issue was created by an AI agent (OpenHands) on behalf of @VascoSch92.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the PluginFormat strategy and detect_format() foundation, then inspect mcp/config.py and the implementation issues listed in the deliverables. Draft an ADR covering detection precedence, manifest mapping, path containment, MCP variable expansion, failure boundaries, and Claude Code compatibility. Done means the ADR resolves these design questions and records decisions for the follow-up implementation work.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design, security, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.