Repository navigation
feat(plugin): map client extensions under the dev.openhands namespace - #4496
Conversation
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
Agent Plugins keeps commands, agents, hooks and entry_command out of its portable core, so they belong in a reverse-domain client extension (spec section 8). Claim dev.openhands and read them there: entry_command from extensions["dev.openhands"] in the manifest, and commands/, agents/ and hooks/hooks.json from the top-level dev.openhands/ directory. Their layout inside that directory is unchanged, so a Claude Code plugin ports by moving directories rather than rewriting them. Namespaces we do not implement are ignored without validating their contents, as the spec requires. Extracting the shared readers surfaced that the plugin format carried a private copy of the exported load_agents_from_dir; drop it in favour of the canonical loader, which additionally skips directories, matches .md case-insensitively and skips both README spellings. The commands predicate is aligned with it so the two stay symmetric. Skills discovery has a similar duplicate that is not substitutable today; filed as #4495. AgentPluginsFormat stays out of _FORMATS until the mcp.json loader lands.
996c839 to
9e269ae
Compare
|
@OpenHands “Agent Plugins” is a spec initiated by OpenAI recently together with some other companies. Please find it, find the spec on the internet and link it in the PR description, as well as in the linked issues, unless it’s there already, so we know what we’re looking at. This PR is part of implementing it. Then take a deep breath, and /codereview this PR. Post your feedback as a review with gh api. |
|
@enyst it looks like you haven't created an OpenHands account yet. Please sign up at OpenHands Cloud and try again. |
|
@enyst it looks like you haven't created an OpenHands account yet. Please sign up at OpenHands Cloud and try again. |
|
@enyst it looks like you haven't created an OpenHands account yet. Please sign up at OpenHands Cloud and try again. |
|
I'm on it! enyst can track my progress at all-hands.dev |
|
🚦 CI is currently failing on this PR's latest commit. Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request This is an automated check - no AI was used to generate this comment. |
|
🚦 CI is currently failing on this PR's latest commit. Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request This is an automated check - no AI was used to generate this comment. |
|
🚦 CI is currently failing on this PR's latest commit. Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request This is an automated check - no AI was used to generate this comment. |
|
@enyst what do you think about this? :-) |
|
🚦 CI is currently failing on this PR's latest commit. Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request This is an automated check - no AI was used to generate this comment. |
|
Hey @jpshackelford and @enyst :-) Can I have a review on this? |
Endpoint auditContract: pinned release artifact
Actionable client-only calls (0)none Actionable server-only operations (18)
Documented non-divergences (13)Client calls intentionally absent from the filtered contract (11)
Reason: Operational Agent Server endpoints intentionally excluded from the filtered public release artifact.
Reason: Client-ahead API stacked on the pending Agent Server meta-profiles implementation.
Reason: Client-ahead API stacked on the pending Agent Server pre-flight LLM validation endpoint. Server operations covered by an exposed browser URL (2)
Reason: RemoteWorkspace.startWorkspaceSession exposes these authenticated URLs for browser iframe and file requests; they are not HttpClient method calls. |
|
Hey @jpshackelford and @enyst a gently reminder :-) |
|
I’m so sorry about dropping the ball on this! Thank you! |
HUMAN:
The PR introduces dev.openhands namespace. Please let me know if you need more clarification on how it works and why we are doing that.
i found some duplicated code that we should unify in the future. I opened an issue (linked to the main issue) for that.
AGENT:
Why
Agent Plugins is an open, vendor-neutral standard. See the canonical specification and the versioned normative v1.0.0 specification. These specification links were added by an OpenHands AI agent on behalf of @enyst.
Agent Plugins deliberately keeps
commands/,agents/,hooks/hooks.jsonandentry_commandout of its portable core — they are not concepts every client shares. The standard's place for them is a client extension (spec §8): a reverse-domain namespace carrying manifest data underextensions.<ns>and files under a top-level<ns>/directory, whose contents only the namespace owner interprets.Until now
AgentPluginsFormatread the manifest and skills and returned empty for everything else, so a ported plugin silently lost its commands, agents, hooks and entry command. This claims our namespace and reads them.The namespace question was the stated blocker on #4452 (#4405's Open Question #2). It was settled in #4405:
dev.openhands— reverse-domain form of a domain we control, with noio.openhands/com.openhandsusage anywhere in the repo to conflict with.Summary
dev.openhandsas our client-extension namespace, and readentry_commandfromextensions["dev.openhands"]pluscommands/,agents/andhooks/hooks.jsonfrom the top-leveldev.openhands/directory. Layout inside that directory is unchanged from the Claude Code format, so a plugin ports by moving directories rather than rewriting them.load_agents_from_dir, which it duplicated; align the commands predicate with it so the two stay symmetric.Issue Number
Closes #4452. Sub-issue of #4405. Skills-loader duplication found on the way out: #4495.
How to Test
1. End-to-end — build a real Agent Plugins package and load it. Save as
/tmp/e2e_demo.py:Run
uv run python /tmp/e2e_demo.py:Every component resolves from the right place:
forecastfrom the portable-coreskills/, the other three fromdev.openhands/, andentry_commandfrom the manifest's extension object through to a working/weather-plugin:nowtrigger. The foreigncom.example.client/directory is untouched — its unparseablehooks.jsonnever reaches a parser and itsspyagent is not loaded.2. Unit tests.
uv run pytest tests/sdk/plugin/test_agent_plugins_format.py -q→ 83 passed.Full suite:
uv run pytest tests/sdk -q→ 5912 passed, 9 skipped, 13 xfailed.Pre-commit on every changed file (ruff format, ruff lint, pycodestyle, pyright, import-dependency rules) → all pass.
3. The tests were mutation-checked, since a green suite proves little on its own. Each mutation was applied to the source, the suite re-run, and the source restored:
dev.openhands/entry_commandguardentry_commandVideo/Screenshots
Not applicable — this is a library-level loader with no UI. The terminal transcript under How to Test is the equivalent evidence.
Type
Notes
Not registered yet, by design.
AgentPluginsFormatstays out of_FORMATS, so none of this executes on a user's plugin until themcp.jsonloader lands (#4451) — registering earlier would claim any directory with a rootplugin.jsonand load it with zero MCP servers.test_not_registered_yetpins that decision.Behaviour change to the Claude Code format. Replacing the plugin format's private agent scanner with the exported
load_agents_from_dirchanges agent loading slightly, in a strictly more permissive direction:Helper.MDnow loads (was silently skipped by an exact.mdmatch), and a directory namedbundle.mdis now skipped silently instead of emitting a misleadingIsADirectoryErrorwarning.README.md/readme.mdare still skipped. I aligned the commands predicate to match so the two adjacent loaders do not diverge — that hunk is easy to drop if you would rather keep this PR strictly scoped to the Agent Plugins format.Deliberately out of scope. Path containment for the extension directory — a symlinked
dev.openhands/pointing outside the plugin root is still followed. That is the pre-existing gap #4453 closes for every component type at once, not a regression from this change.Open design call.
ClaudeCodePluginFormatandAgentPluginsFormatnow each carry three one-line forwarders that differ only in root directory. Acomponent_root()template hook on the base would delete all six, at the cost of makingload_hooks/load_agents/load_commandsconcrete — so a future format would silently inherit the Claude Code layout instead of being forced to declare one. I kept #4420's abstract contract, but I do not feel strongly.🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimnikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:e094fe1-pythonRun
All tags pushed for this build
About Multi-Architecture Support
e094fe1-python) is a multi-arch manifest supporting both amd64 and arm64e094fe1-python-amd64) are also available if needed