Changelog
All notable changes to the Unified Harness Protocol.
2026-09-28 (patched 2026-09-29)
Additive to 2026-09-12: every request and object valid under the previous version is valid here.
Patch of 2026-09-29, same version. The task-side reference to an environment is
metadata.environment, not a top-level request field: the task surface is a Responses request
and metadata is its extension point, where harness_id already travels
(Tasks §1.2,
Environments §5). The response
reports metadata.environment beside session_id. The harness and session objects keep
environment as an ordinary field. The published chapter had carried the field at the top level
for one day; the schema, the conformance suite (2026.9.28.post1: EN-05 sends it in metadata,
EN-07 asserts the response echo; post2: EN-02 compares the returned bytes, which post1 could not
read) and the reference server (0.26.7) changed together. The reference server passes the full
class, 84 of 84, on post2.
Patch of 2026-09-29, second, same version. A build's record says which step is running
(stage) and carries its log while it runs, and a server MUST write it as steps end, so a client
can show progress (Environments §4).
An optional GET /v1/environments/packages/check answers what the manager's registry says about a
spec before any build (EnvironmentPackageCheck), so a client can refuse a typo where it is
typed. The environment's status is building while a build runs, whichever version is active;
a task keeps reading the active version meanwhile. Suite 2026.9.28.post3: EN-09 exercises the
check on a server that offers it; reference server 0.26.10.
- Environments (Environments), the Environments
sub-protocol, optional at every class behind the
environmentscapability. An environment is a project's files and installed dependencies, built once and mounted read-only at a fixed path in every session that names it, beside the session's own writable working directory. The chapter defines the object (henv_), files by path and whole-project import, builds and versions with an active pointer and rollback, theenvironmentfield on the harness object andmetadata.environmenton a task, what a session sees (the mount, the variables, the instruction, the record), and the errors. - The session object carries
environment(Sessions §3). - New error codes:
environment_not_found,environment_not_ready,environment_busy,environment_exists,environment_invalid,environment_unavailable. - Conformance suite
2026.9.28: checks EN-01 to EN-08 (EN-07 runs one task that reads the environment and tries to write it), skipped on a server without the capability.
Conformance suite 2026.9.12.post4 (2026-09-27)
- The public fixture answers at
https://uhp-fixture.harnessrouter.ai/mcp, the suite's new default; post3 pointed at the container's own address.
Conformance suite 2026.9.12.post3 (2026-09-27)
The protocol is unchanged; the suite that measures it is.
- The plugin checks point a plugin at a server that resolves and answers. A host may refuse an
MCP server it cannot reach at configuration time and record that in
skipped; with the unresolvable placeholder the checks used before, such a host failed P-02, P-04, P-06 and P-07 for refusing the placeholder rather than for anything about plugins (the hosted HarnessRouter, 2026-09-27). The suite ships the server (uhp-conformance-fixture,pip install "uhp-conformance[fixture]"), defaults to a public copy of it, and takes--plugin-mcp-urlfor a copy the host under test can reach. - P-11, new: a plugin's MCP server answers the agent's tool call. The agent is asked to call the fixture's tool with a nonce and the fixture's answer, a function of the nonce, must come back. It is the only plugin check that runs a task, and the only one that shows an agent reaching a plugin's server rather than a server deriving and refusing correctly.
- Measured implementations are re-measured under this suite before their badges are shown again.
2026-09-12
Additive to 2026-08-11: every request and object valid under the previous version is valid
here, and a client written against it keeps working against a server that serves this one. The
previous version stays published at its own address and a server SHOULD keep serving it.
Added
-
Plugins (Plugins), the Harness Plugins sub-protocol. A plugin is an Agent Plugins 1.0.0 package, carried as files and installed into a harness through a new
pluginsarray on the harness object. The server derivesmanifest,mcpServers,skillsandskippedfrom the package on every write; the package's servers and skills join the harness's own for every turn. UHP defines no package format of its own: as Agent Plugins wraps Agent Skills by saying where a skill lives and deferring the skill's format, this chapter wraps Agent Plugins by saying how a package travels, binds and runs, and defers the package's contents. -
The composition rule (Plugins §4). A harness's own
mcpServersandskillskeep exactly the meaning they had: what a client wrote there, never a plugin's components copied in. This is what makes the version additive. A harness configured before this version is byte-for-byte a valid harness now, withpluginsempty, and a client that reads a harness and writes it back cannot install a plugin's servers twice. Component names are unique within a harness across the direct lists and every enabled plugin; a collision is refused at configuration time withplugin_conflictrather than resolved silently at run time. -
Export (Plugins §5):
GET /v1/harnesses/{id}/pluginreturns the harness's own tools and skills as an Agent Plugins package, credentials omitted and each omission recorded, that installs into another harness or any other Agent Plugins client unchanged. The direct fields were always the contents of a plugin without a name; this makes that an operation. -
stdioMCP servers, inside plugins (Plugins §2.2): a plugin'smcp.jsonmay declare a process (command,args,env,cwd), reported on the plugin as aPluginMcpServer. The harness's ownmcpServerslist stays remote-only and byte-for-byte what it was, because a process needs the root and data directory only a package provides. A server whose base cannot run a transport refuses the plugin withunsupported_transport. -
Discovery: the
pluginscapability, optional at every class, andplugin_schemas, the Agent Plugins manifest schemas the server installs (Lifecycle §2). -
Error codes:
plugin_invalid(422),unsupported_plugin_schema(422),unsupported_transport(422),plugin_conflict(409),plugin_not_found(404) (Errors §3). -
Security §9, plugins are third-party code (Security): stdio servers run inside the agent's sandbox and nowhere else, only the two placeholders are ever expanded, plugins are installed by whoever manages the harness and never per request.
-
Schema:
Plugin,PluginManifest,PluginSkill,PluginSkipped,FileList; the skill files endpoint, which the prose named and the OpenAPI document omitted, is now in both.
Conformance
-
P-01 to P-10, class Full, gated on the
pluginscapability: the schema list, package round trip and derivation, refusal of a package without a manifest, refusal of a name collision, survival of an unrelated edit, export that installs again without credentials, skipped components recorded,enabled: falsepreserved, refusal of an unsupported manifest schema, refusal of duplicate plugin names. Each is proven against a defect stub carrying one mistake at a time, the way the R-series is. That made the suite 74 checks. -
X-09, class Extended: a file uploaded through
POST /v1/filesis accepted and can be sent as task input by id (Files §1.2). X-05 sends its file inline and never reaches the upload endpoint, so a server whose uploads all failed passed the chapter (HarnessRouter CE 0.17.3, #198). Proven against a defect stub: an upload that fails outright, one that answers without an id, and one that truncates each fail X-09 alone while X-05 stays green. The suite is now 75 checks.
2026-08-11, additive clarifications
These were published as additive changes to 2026-08-11 before 2026-09-12 and are carried
into it unchanged.
Clarified
-
Session deletion is named (Sessions §6):
DELETE /v1/sessions/{id}is the protocol's session delete, with the semantics the old path always had (transcript, trace and working folder go; the session stops counting toward any allowance; a laterGETis404).DELETE /v1/traces/{id}stays as an alias a server MAY keep, which is what the reference implementation does with one handler behind both paths. -
Session sharing is codified (Sessions §5), closing the four gaps of #44: the share object is schema'd (
SessionShare:idandurlrequired,urlmay be base-relative),DELETE /v1/sessions/{id}/shareis the named revocation endpoint (other forms MAY be kept), a bodylessPOSTpublishes, and revocation MUST reach every link minted. Sharing itself remains a MAY. These are the shapes the reference implementation already demonstrates and the R-series already measures; the suite now asserts them instead of probing for them. -
Turn items have a shape (Sessions §3): each item of
GET /v1/sessions/{id}/turnsMUST carryid(the response id) andstatus, and SHOULD carryuser/assistant/tools/files. PreviouslyadditionalProperties: trueand nothing else, which held X-04 at "the endpoint answered 200".
Conformance
-
R-01/R-02validate the share object against the schema and FAIL (no longer skip) when it carries nourl;R-06asserts the namedDELETErevocation endpoint and FAILs (no longer skips) when it does not work;X-04validates every turn item. New defect stub mode: a server whose revocation exists only as a toggle now failsR-06. -
R-08: a bodylessPOSTpublishes. The sentence was written into §5 above and left unenforced, named as a known gap rather than hidden. The rest of the R-series mints through a helper that retries a 400/422 with{"enabled": true}— the retry is what lets the series measure a toggle-dialect server at all, and it is also what let this sentence go untested: such a server passesR-01…R-07on a request §5 does not require anyone to accept, while refusing the one it does.R-08is the one check that does not retry. New defect stub mode,bodyless_rejected, is what the reference implementation was before this series existed; the matrix provesR-08is the only check that reddens on it. No specification or schema change: this enforces a sentence that is already written. -
toolsandincludeare reserved and ignored (Tasks §1.4). Both arrived with the OpenAI Responses wire shape this version stays compatible with, and neither ever had defined semantics in UHP. A server accepts them and does not act on them.
tools cannot mean what it means in the Responses API, where the client executes tools and
returns the result as input: UHP puts both the call and its result in output, so there is no
input path for the return leg and the loop the field implies cannot be completed. The other
plausible reading — per-request MCP servers — is already covered by
Harnesses §4.1, and would be an escalation
primitive besides: it would let any caller point the agent at an endpoint of their choosing,
executed with the harness owner's credentials and workspace. The rule underneath, stated so it
can be applied again: narrowing is safe, widening is escalation.
-
metadata.ignored_fieldsis specified (Tasks §1.1), and required when a request carries a reserved field. Ignoring has to be observable, for the same reason model substitution is reported in §1.3: a silently dropped field is indistinguishable from an honoured one. -
GOVERNANCE.md records "a declined field is not a pending one" — the general rule these two fields are the worked example of. Due to @aenawi in #42.
Nothing is removed and no client breaks: a request sending either field was already accepted and already had no effect. Removal is a question for the next version.
Conformance
Three checks at class Core, in both directions, where the suite previously sent neither field:
T-08 a request carrying them is accepted, T-09 they are reported in
metadata.ignored_fields, T-10 a request that sent neither is not told one was ignored. Core
goes from 37 checks to 40.
2026-08-11 — first published version
The initial specification, extracted from the shipping HarnessRouter implementation rather than designed in the abstract. Every endpoint and event described here was already running in production before it was specified; the work was to write down the contract precisely, close the gaps that writing it down revealed, and make the result testable.
Defined
- Architecture — client / server / harness roles, three conformance classes (Core, Extended, Full), the six-object model, transport and authentication.
- Lifecycle —
UHP-Versionnegotiation, theGET /v1/uhpdiscovery document, task states (in_progress,completed,failed,incomplete,cancelled), session lifecycle, concurrency rules. - Harnesses — discovery, the harness object, model catalogues, computed
available, and harness management at class Full. - Tasks —
POST /v1/responses, the response object, harness selection viametadata.harness_id, model substitution reporting, idempotency. - Streaming — the SSE event vocabulary,
sequence_numberordering guarantees, reconnection. - Sessions — continuation via
previous_response_id, listing, inspection, cancellation, sharing. - Files — inline and uploaded input, artifact annotations, download, preview, retention.
- Errors — a single error envelope, a closed set of codes, retry rules.
Gaps this closed in the reference implementation
Writing the specification exposed three places where the implementation had no defined behaviour:
- No capability discovery. A client had to guess what the server supported, or discover it from
a 404. Added
GET /v1/uhp. - No protocol version on the wire. Nothing identified which contract a response was written to.
Added the
UHP-Versionheader on every response. - Unstructured errors. Failures returned a bare human-readable string, so a client had to match on prose to decide whether to retry. Added the structured error envelope, with the previous string retained as a deprecated alias so existing clients keep working.
Known compromises
Recorded in VERSIONING.md: mixed field
casing between the task and harness surfaces, and session deletion living at /v1/traces/{id}. Both
are kept for compatibility and marked for a future major version.