Skip to content

Plugins: the UHP 2026-09-12 sub-protocol in the gateway, runner and console - #175

Merged
richard-epsilla merged 16 commits into
mainfrom
plugins/gateway
Sep 14, 2026
Merged

richard-epsilla merged 16 commits into
mainfrom
plugins/gateway

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

The reference implementation now serves UHP 2026-09-12 beside 2026-08-11 and implements the Plugins chapter end to end. Ten of ten plugin conformance checks pass against it (P-01 to P-10, plus the F-series they lean on), so the next remeasure reads full 2026-09-12.

Gateway. Both versions from one code path, the requested one echoed. A harness accepts plugins on create and update: each package is read the way the Agent Plugins client conformance section says (closed manifest, components from their fixed locations, an invalid component recorded in skipped rather than fatal, a missing or invalid manifest refused), stored out of line, and recorded as manifest, mcpServers, skills and skipped. The harness's own mcpServers and skills are never rewritten by a plugin, which is what keeps every 2026-08-11 client working. Names are unique across the harness and its enabled plugins (409 plugin_conflict); a stdio server on a base whose runner cannot launch one is refused with unsupported_transport. Two endpoints on the org and public surfaces: the package files, and the harness exported as a package with credentials omitted and recorded. Every turn folds enabled plugins in.

Runner. Each package lands whole at .harness/plugins/<name>/ with a writable .harness/plugin-data/<name>/. A stdio server becomes a launcher that exports PLUGIN_ROOT and PLUGIN_DATA and the entry's env, expands the two placeholders once, resolves a ./ command against the root, confines cwd, and execs the command with its args as argv, so every backend writer sees one shape; codex, hermes, cline and omp gain that entry. A package that is also a Claude Code plugin still reaches --plugin-dir.

Console. A Plugins section on the Harness page: install from a folder, see each plugin's tools, Skills and what did not load, enable, remove, and download the Harness's own tools and Skills as a zipped package.

Starter kits. Launch installs a kit's plugin package when the kit ships one (HarnessRouter/starter-kit#23 makes all four ship one), so a launched Harness shows the kit as a named, versioned plugin it can export. A kit baked before that layout still carries its skills directly.

Docs: the self-hosting guide and the console API reference. Tests: gateway test_plugins.py, runner test_uhp_plugins.py. Verified on hr-test with a derived image and the conformance suite; the console at four widths.

🤖 Generated with Claude Code

Verified live on hr-test (derived image plugins-33cabcf on 0.17.1). The full conformance suite against https://20-98-237-6.sslip.io/api/harness, class full, suite 2026.9.12: 74 of 74 passed, none skipped, "CONFORMANT UHP 2026-09-12 (full)". The Slides kit relaunched from the kits-as-plugins layout records harnessrouter-slides 1.0.0 with its one Skill derived and nothing skipped; the package files and the harness export read back; a real hermes task on that harness listed .harness/plugins/harnessrouter_slides/skills/slide-design/SKILL.md and the Skill mounted under the CLI's own skills directory. The console's Plugins section renders the installed kit plugin at 1440, 1024, 768 and 390 with no horizontal overflow.

Review pass (commit 7c5db37). An adversarial review over the branch raised 54 findings; the real ones are fixed in one commit: org-bound package storage with handle ownership checks, validation before any package is stored and deletion of dropped packages, the stdio gate normalising the base, name collisions compared the way the runner keys directories, write-time network policy for plugin servers, full file entry validation, symlink-safe runner writes with stale roots and skills removed per turn, a runnable ./ command, TOML control-character escaping, cloud upload carrying plugins, an immutable base on update, section-local console notices, and a guide example that no longer blanks a harness. Redeployed to hr-test and re-measured: 74 of 74, "CONFORMANT UHP 2026-09-12 (full)", and the Slides kit's Skill again reaches the agent from the plugin root.

@vercel

vercel Bot commented Sep 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
unified-harness-protocol Ready Ready Preview Sep 14, 2026 11:52pm UTC

Request Review

richard-epsilla and others added 6 commits September 13, 2026 19:12
…side 2026-08-11

The gateway now lists both versions, echoes the one asked for, and reports
the plugins capability with the Agent Plugins manifest schema it installs.
A harness accepts plugins on create and update: each package is read the
way the Agent Plugins client conformance section says (closed manifest,
components from their fixed locations, an invalid component recorded in
skipped rather than fatal, a missing or invalid manifest refused), stored
out of line, and recorded as manifest, mcpServers, skills and skipped.
The harness's own mcpServers and skills are never rewritten by a plugin,
which is what keeps every 2026-08-11 client working. Component names are
unique across the harness and its enabled plugins; a collision is a 409
plugin_conflict at write time. A stdio server on a base whose runner
cannot launch one (pi, dsh) is refused with unsupported_transport. Two
endpoints on both the org and the public surface: the package files, and
the harness exported as a package with credentials omitted and recorded.

Every turn folds enabled plugins in: their skills take the ordinary skill
path, their remote servers the ordinary MCP path, and their stdio servers
travel with the whole package so the runner has a root to run them from.

The runner materialises each package at .harness/plugins/<name>/ with a
writable .harness/plugin-data/<name>/, and turns each stdio server into a
launcher script that exports PLUGIN_ROOT and PLUGIN_DATA and the entry's
env, expands the two placeholders once, resolves a ./ command against the
root, confines cwd to the root or the data directory, and execs the
command with its args as argv. Every backend writer therefore sees one
shape, command plus args; codex, hermes, cline and omp gain that entry
beside the URL entry they had. A package that also carries
.claude-plugin/plugin.json still reaches Claude Code through --plugin-dir.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Install a package from a folder (the picker reads every file under it,
paths relative to the folder, and names the row from plugin.json before
the save; the server validates on save and answers with what it derived
and what it skipped), see each installed plugin's tools, Skills and what
did not load, enable or disable it, remove it, and download the Harness's
own tools and Skills as a package zipped in the browser. The harness
type and save body carry `plugins`; fflate zips the export.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lugin

The starter kits now carry their Skills as an Agent Plugins package at
<kit>/plugin/. Launch installs it, so the Harness records the kit as a
named, versioned plugin whose skills derive from the package, and can
export it. A kit baked before that layout still carries its skills
directly through the path that read them before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…API reference

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…der the name

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Gateway. Packages are stored under the org that installed them and a
handle is checked for shape and ownership, so a leaked handle cannot read
another org's package through a PUT. Everything is validated before any
package is stored, and a write that drops a plugin deletes its package;
deleting a harness deletes them all. The stdio gate normalises the base
the way the write does, so no spelling of pi or dsh slips a process past
it. Two plugin names, or a plugin skill and a harness skill, that the
runner would fold into one directory are refused as one name. A remote
server this deployment's network policy refuses is refused at write time
and written into skipped, not dropped with a log line at turn time. File
entries are checked in full: content or content_b64 but not both, base64
that decodes, no duplicate paths, no control characters; a manifest with
no $schema is invalid rather than an unsupported version; env names must
be environment variable names; the frontmatter reader handles block
scalars and trailing comments; the derived record is bounded so the
harness write cannot exceed the property cap. The export never fails on
what an earlier write accepted and strips credentials from URLs. A
disabled own skill no longer suppresses a plugin's skill of that name.
The kit catalog names what a package installs; launch reserves the names
of the hosted entries it attaches; a declared package that is missing is
logged. Cloud upload carries plugins whole when the hosted side reports
the capability and as their skills and remote servers when it does not.
A harness's base is immutable on update, as the spec says.

Runner. The plugin root and data directory are checked as the paths the
session could have replaced with links, before anything is written
through them; roots are rebuilt every turn and stale roots removed; files
are written beside and renamed over so a running server's binary is
replaced rather than truncated; a package's own ./ command is made
runnable; the launcher is written without following links; codex's TOML
escapes control characters; skills this runner wrote on an earlier turn
and does not write now are removed; a package that is also a Claude Code
plugin is not folded into the skills directory a second time.

Console. Errors and notices from installing or downloading a package
appear in the Plugins section; a folder over the package limit or for a
manifest version the server does not install is refused before it is
read into the draft; a save that leaves a plugin with parts not loaded
stays on the page and opens that row; the download is disabled while the
draft is unsaved and reports what the package left out; the open row is
keyed by name; copy and wrapping at phone width.

Docs. The guide's install example is a create, and the update path says
what PUT replaces; the API reference lists the plugins field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the kit shipped one

Launch on an existing kit Harness handled the database tool and the media entry but never the
package, so the three kit Harnesses on hr-oss-test launched before their kits carried packages
stayed without one through a relaunch while a fresh launch installed it (2026-09-14). The
existing path now installs the kit's current package, replaces an older version and leaves a
matching one alone, the kit's loose skills giving way to the package's, as on a fresh launch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD
@richard-epsilla

Copy link
Copy Markdown
Collaborator

Plugin matrix on hr-oss-test, 2026-09-14

Every base, both package shapes, one base at a time with matrix/run-matrix.py against the derived image plugins-7c5db37 (the PR head before today's rebase; hermes and claude-code re-run on plugins-0e3618c, the rebased head plus the launch fix below, same result). Each run installs the fixture package on a fresh harness, runs one real task, checks the probe token in the answer, and deletes the harness.

base Skill from a package stdio MCP server from a package
claude-code pass (6.0 s) pass (13.4 s)
codex pass (10.2 s) pass (13.7 s)
hermes pass (32.7 s) pass (37.1 s)
goose pass (5.0 s) pass (6.1 s)
gemini pass (22.9 s) pass (20.5 s)
qwen pass (9.7 s) pass (20.7 s)
opencode pass (20.0 s) pass (19.7 s)
cline pass (8.8 s) pass (7.4 s)
omp pass (26.1 s) pass (28.0 s)
pi pass (9.5 s) refused as designed (422 unsupported_transport) (0.3 s)
dsh pass (12.2 s) refused as designed (422 unsupported_transport) (0.2 s)

gemini's first attempt hit a connection timeout from this machine and its second found no provider: the box's provider store had been left holding one column's integration after a matrix column earlier in the day (the restore failed on a route to a key only its owner can re-add). With the store restored, gemini passed both shapes.

Starter kits as packages

Relaunch of each kit on hr-oss-test with the four packages from starter-kit PR #23 baked into the image, then one real task per kit asking the agent for the SKILL.md files it can see.

kit plugin installed skills derived skipped task Skill seen by the agent
slides harnessrouter-slides 1.0.0 slide-design [] completed yes
sheets harnessrouter-sheets 1.0.0 sheet-design [] completed yes
dashboard harnessrouter-dashboard 1.0.0 dashboard-design [] completed yes
video harnessrouter-video 1.0.0 video-storyboard [] completed yes

The first relaunch installed a package only for Slides: the three kit Harnesses launched before their kits shipped packages kept no package through a relaunch, because the existing-Harness path of launch_kit handled the database tool and the media entry but never the package. Commit 0e3618c installs the kit's current package on an existing Harness, replaces an older version and leaves a matching one alone (test test_a_kit_harness_from_before_the_package_gets_it_on_the_next_launch). The second relaunch, above, installed all four and a third left them untouched ([kits] <kit>: package ... installed on <id> once each in the gateway log, nothing on the third).

Branch rebased onto main (goose TMPDIR revert included); gateway 291, runner 333 in the 3.12 venv.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD

@richard-epsilla

Copy link
Copy Markdown
Collaborator

The four kits end to end on hr-oss-test (2026-09-14, image plugins-0e3618c, starter-kit #23 merged as 361cae3)

Each kit opened in the console as the console user, created from a prompt in its own app, and the result read off the rendered page with Playwright.

kit Harness record after launch prompt what rendered time
Slides plugin harnessrouter-slides 1.0.0 (slide-design), skills [], no servers a three slide deck about the water cycle 3 slides in the editor: rail, stage, 20 editable objects 161 s
Sheets plugin harnessrouter-sheets 1.0.0 (sheet-design), skills [], no servers three fruits with name, color and a sentence a 3 by 3 grid, every cell filled, Run control present 40 s
Dashboards plugin harnessrouter-dashboard 1.0.0 (dashboard-design), skills [], server database (gateway hosted) revenue by month and quantity by product two chart panels, line and horizontal bar, from the connected PostgreSQL, "data as of just now" 40 s
Videos plugin harnessrouter-video 1.0.0 (video-storyboard), skills [], server media (gateway hosted) one 4 second shot of a calm sea at sunrise one 854x480 clip on the canvas and a 1920x1080 exported film of 4.0 s; the app shows the run's cost, $0.52 410 s

No kit Harness carries loose skills or a package server: the Skills all derive from the package, and the only MCP entries are the two launch provisions (a database connection record and the deployment's media server), which bind this deployment's data and credential and so cannot travel inside a portable package.

Dashboards needed a database: the connection recorded on the box pointed at a container that no longer exists, so a seeded PostgreSQL now runs on the VM (hr-testdb, SELECT-only role) and the kit was relaunched with it through POST /v1/kits/dashboard/launch.

One follow-up for starter-kit, not blocking: every kit SKILL.md points its validator at .claude/skills/<name>/… or .harness/skills/<name>/…; under the plugin layout the Skill lives at .harness/plugins/<package>/skills/<name>/, so the agent in the Sheets and Dashboards runs had to hunt for the script (both recovered). The Skill should reference the script beside its own SKILL.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD

…e version

The version check alone let a kit whose Skills changed without a bump (the 2026-09-14 Skill fix
shipped as 1.0.0 twice) leave launched Harnesses on the old bytes. Launch now compares the stored
package with the one on disk, path by path, and installs when they differ; a matching version and
matching files stay untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD
@richard-epsilla

Copy link
Copy Markdown
Collaborator

Follow-up closed: the kit Skills find their validators (2026-09-14)

Deployed as plugins-ca57223 with the 1.0.1 overlay. Relaunch replaced all four packages ([kits] <kit>: package harnessrouter-<kit> 1.0.1 replaced on <id>, once each). Sheets and Dashboards re-run end to end: a 4 by 4 grid in 43 s and two chart panels in 44 s, and each turn's tool trace shows one Bash call, the new command, no search:

python3 "$(find . -path '*/sheet-design/validate_sheet.py' -not -path '*/node_modules/*' 2>/dev/null | head -1)" sheet.json
python3 "$(find . -path '*/dashboard-design/validate_dashboard.py' -not -path '*/node_modules/*' 2>/dev/null | head -1)" dashboard.json

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD

…, and downloads the plugin

The version rendered as a full-width bordered box (the row's block-span rule outranked the pill),
the details were a wall of text beside floating actions, the header wrapped at 390, and
"Download as plugin" exported the Harness's own tools and Skills, which for a kit Harness is an
empty package. Each installed plugin now has its own Download (the package as installed, zipped
in the browser), an inline version pill, a two-line description, a facts line and a details panel
with Skills, tools, what was not loaded and links. The Harness export moved to a footer that says
what it packages and appears only when the Harness has tools or Skills of its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD
`POST /v1/kits/{kit}/launch {"harness": <id>}` binds the kit to that Harness: the kit's app then
talks to it, it receives the kit's package and launch entries, and the previous kit Harness keeps
its sessions and package but is no longer the one the app finds. A Harness running another kit is
not taken over (409 kit_conflict). This is how a package migrated to another Harness takes the kit
with it, which the plugin round trip needs to be a closed loop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD
@richard-epsilla

Copy link
Copy Markdown
Collaborator

Plugin migration, closed loop, all four kits (2026-09-14, image plugins-fdad224, kits 1.0.2)

For each kit: take the kit Harness's installed package exactly as the server holds it (the bytes the console's Download zips), install it on a fresh Harness of the same base, point the kit at that Harness, drive the kit's own app end to end with Playwright, then point the kit back and delete the copy.

kit files round-trip derivation identical kit rebound to the copy the app, on the copy rebound back
Slides 4 of 4, byte for byte yes yes, package 1.0.2 2 slides rendered, deck.json kept and validated yes
Sheets 3 of 3 yes yes 3 by 3 grid, 9 cells filled yes
Dashboards 3 of 3 yes yes, database entry provisioned on the copy 2 chart panels from the connected PostgreSQL yes
Videos 3 of 3 yes yes, media entry provisioned on the copy 1 shot rendered, storyboard on the canvas yes

Every session the app created landed on the copy, not on the original.

What the first pass found. On the copy, Slides turned the deck into a PowerPoint and deleted deck.json. The package round-tripped, but the rule "deck.json is the deliverable the app reads" lived in the kit Harness's system prompt, which a package does not carry (Plugins chapter 5, by design). The portable home for a rule the app depends on is the Skill: starter-kit #26 moves each kit's operative rules into its SKILL.md and trims the kit prompt to the persona plus "read the Skill first" (packages 1.0.2). With that in place the copy behaves like the original.

New on this branch

  • POST /v1/kits/{kit}/launch {"harness": <id>} (fdad224) runs the kit on a Harness you already have: the kit binding moves, the package and the launch entries (database, media) are provisioned there, and a Harness running another kit is refused with 409 kit_conflict. This is how a migrated package takes its kit with it.
  • Console (90488e4): the Plugins section follows the Tools and Skills row grammar, with an inline version pill, a two-line description, a facts line and a details panel. Each installed plugin has its own Download; the zip of the Dashboards plugin from the console holds plugin.json, skills/dashboard-design/SKILL.md and validate_dashboard.py, 17.6 KB. The Harness export moved to a footer that says what it packages and appears only when the Harness has tools or Skills of its own. Verified at 1440, 1024, 768 and 390 with no sideways scroll.
  • Kit launch installs a changed package byte for byte, not only on a version change (ca57223).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD

…n narrow

Four buttons in one line took the width the description needed. Wide: a two by two block on the
right, the copy keeps the rest. Narrow: one per row under the copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD
… viewport

The settings panel is about 430px wide on a 1024 screen, so a viewport breakpoint left the copy
squeezed to 140px beside four buttons. The list is now a size container: beside the copy, two
per row; under 680px the actions drop below the copy; under 420px one per row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD
…acks on a phone

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD
@richard-epsilla

Copy link
Copy Markdown
Collaborator

Plugin row layout (0e7f441)

The four actions no longer take a whole line from the description. The row follows the width of its list, not the viewport (the settings panel is about 410 px wide on a 1024 screen):

list width layout
wide actions beside the copy, two per row; the copy keeps 428 px at 1440
under 680 px actions below the copy, two per row (768: 424 px of copy; 1024: 366 px)
under 340 px one per row, full width (390)

The export footer's button no longer wraps at any width. Measured with Playwright at 1440, 1024, 768 and 390, no sideways scroll; screenshots in the handoff evidence.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD

…the Harness export footer is gone

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD
The title's flex rule lost to the row's block rule for strong, so the pill touched the name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q96VFkSEJZdTydczxxJHMD

@richard-epsilla richard-epsilla left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Measured on hr-oss-test as plugins-705bb5c: plugin matrix 22 of 22 on every base, the four kits end to end, plugin migration closed loop on all four kits, console Plugins section verified at 1440, 1024, 768 and 390. Tables and evidence in the comments.

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