Changelog
All notable changes to the Unified Harness Protocol.
Conformance suite 2026.10.4.post9 (2026-10-06)
No change to the protocol, the schema or any verdict a conformant server gets.
A check that quotes the server's answer in its failure message could not read it. Eighteen messages,
reached by eight of the nine Environments checks and all ten Memories checks (in the check itself or
in a helper they share), were built from r.text, and a client result had body, json and
header() but no text. A message is
evaluated only when its assertion fails, so nothing showed against a conformant server. Against a
server that really refused the call, the check raised AttributeError, the suite filed it as ERROR,
"the check itself broke", and the server's own sentence about why it refused never reached the
report: a real non-conformance read as a defect in the suite. A result now has text, the body
decoded and never raising, and those checks FAIL with the status and the server's words. Reported
and fixed by @Jiaqiiii523 (#411), with a test that reads the checks and refuses any attribute a
result does not have; it now also follows an answer that comes back from a helper and one read
straight off the call, which between them are a third of the answers the checks read.
A report names the suite that produced it. suite_version was a second copy of the version and
had not moved since 2026.9.12.post4, so every report written by 2026.10.4 through
2026.10.4.post8 carried that older name. The version is now written once, in the package, and
the build reads it from there. No report published under reports/ was written in that window.
2026-10-04 (patched the same day)
Additive to 2026-09-28: every request and object valid under the previous version is valid here.
Patch of 2026-10-04, same version. A record's content is an ordered list of parts, always, on
every read (Memories §4.3). The chapter as first
published that morning said content was text or one file reference, which could not hold a
picture beside what it shows, or a conversation turn with a file in it. Two kinds of part are
defined, text and file; a modality is a media type of a file part, not a kind of part, so a new
one needs no change here, and any other kind is x.-prefixed and carried unchanged. A file part
may carry the words that stand for it (text, with text_source), which is what recall by words
finds; a part may carry the role that said it, so a turn is observed in this same shape. A string
remains shorthand for one text part on a write. A file part's bytes are read at
GET /v1/memories/{id}/records/{rid}/content/{index} (was /content). A provider's capability
document says what it keeps in content (media, bytes, describes), replacing the files
flag, and a write of a media type the provider does not keep is refused with memory_unsupported,
never stored without its file. Schema: MemoryContentPart; MemoryRecord.content is an array.
Suite 2026.10.4.post1: ME-02 expects the list, ME-09 exercises parts, order, refusals, and a file
part kept with its description and read back byte for byte, or refused, as the provider declares.
No implementation had shipped against the morning's text.
Second patch of 2026-10-04, same version. A memory's id is opaque. The schema had required
the hmem_ prefix, which a server whose memories are a tree it already keeps (its own workspaces,
its own folders) could meet only with a second id space and a lookup between the two. hmem_
remains the prefix of a server that mints its own ids. Schema: the pattern is gone from Memory.id,
the path parameter and a harness's entry. Suite 2026.10.4.post2: ME-01 no longer asserts the prefix.
Third patch of 2026-10-04, same version. recall searches the memory and everything below it
the caller may read; the morning's text had it act on one memory. A search is how a caller finds
where something is kept, so each result now carries memory (id, name): the place to walk
from. It never searches an ancestor. depth narrows it (0 is the memory alone; it used to widen,
from a default of one memory). A server MAY cap the memories one question covers and says so in
degraded. list, get, history and the queries still act on one memory. The provider
capability recall.max_depth is gone: the subtree is the server's to compose. Schema:
MemoryRecall.results[].memory is required. Suite 2026.10.4.post3: ME-03 rewritten.
Fourth patch of 2026-10-04, same version. One vocabulary for who acts on a memory. A grant's
principal was "an opaque typed identifier" whose kinds each server chose, and a record's
written_by named its kinds only by example (harness, consolidator); two implementations had
already stamped a person as member and as user. Now four kinds serve both places: member,
harness, group (holds grants, never writes) and provider (writes, never holds a grant); a
server's own kind is x.-prefixed. consolidator is gone: a consolidation's records are written
by the provider, with consolidation_id beside it, and a record derived from an observation
carries on_behalf_of. A credential is not a kind. Schema: MemoryWriter, and a pattern on a
grant's principal. Suite 2026.10.4.post4: ME-02 checks the writer's kind.
Fifth patch of 2026-10-04, same version. A person and an agent are the same kind, and access
has one mechanism. The fourth patch listed member (a person) and harness (an agent) as two
kinds; they are one, member, and which of the two a member is, is its type (human, agent),
a property no rule depends on. With that, a harness no longer carries a list of attached memories
with an access that narrowed a grant: an agent reaches what it was granted, on the memory, as a
person does. /v1/harnesses/{id}/memories reads the agent's principal and what it was granted, and
sets two things: default_memory_id and observe. An agent joins a group only when someone adds
it. Kinds are now member, group and, for a writer only, provider. Schema: HarnessMemories
reshaped, HarnessMemorySettings added, HarnessMemoryEntry removed. Suite 2026.10.4.post5.
Sixth patch of 2026-10-04, same version. One graph. Entities and the relationships between
them are not a second structure beside records: an entity is a record (new core type entity), a
reference is an edge, and a relationship with something to say is a fact that references its
subject and its object, so it has a writer, a time, a history and a source like any record.
Five reference names are defined (derived_from, part_of, about, subject, object). One
read, POST /v1/memories/{id}/graph, returns records as nodes and references as edges around a
record or for a memory, the same for every provider; a provider declares where its entities come
from in graph.entities (derived, stated, none). Also: an agent's membership of a group
follows the rule a person's does (the fifth patch forbade a server to place an agent in a group on
its own, which is not equal standing where every new person joins one). Schema: MemoryGraph,
MemoryGraphRequest, MemoryRecordRef. Suite 2026.10.4.post6: ME-10.
The chapter's opening, §1, the list of an agent's tools (memory_graph) and §14 were then brought
in line with the six patches above; no rule changed.
Seventh patch of 2026-10-04, same version. A record MAY carry a title: one line that names
it, apart from what it says (a document's heading, an entity's name, a fact's statement). It is
optional, at most 300 characters, matched by a search together with the content, and revised like
any other field. A record with a title MAY have empty content. An entity's name is its title.
Schema: MemoryRecord.title. Suite 2026.10.4.post7: ME-02 checks that a title reads back apart
from the content and may stand alone.
The graph read without around starts from the records of the memory and of what is below it,
the scope a search has; the sixth patch's text said the memory alone, which returned nothing for a
memory whose records are all kept in its children.
A record names at most one subject and at most one object; a second of either is refused with
memory_invalid (422). Stated because two implementations had to agree on it: the reference
gateway kept such a record and a graph engine refused it.
Two things the first server built from this chapter by another team read differently from the
reference gateway, both the chapter's fault, both now said once: supersedes is the NUMBER of the
version a version replaced (the example showed a record id, and the schema allowed either); and a
reference the reader may not follow is { record_id, available: false } with no memory_id (the
chapter returned the memory's id, which tells the reader that the memory exists).
Records that follow a source (§5.4), found by the second server. A server that shows a
document as one record per section cannot let a section be revised or forgotten through the memory:
the next save of the document would undo it. The chapter had no word for that, so the server's
refusal was undefined and a kept file was caught by the same refusal. Now: such a record carries
follows (kind, id, name of its source), revise and forget on it are refused with
memory_unsupported in words that say where the source is changed, and a client offers neither.
A file someone kept is NOT such a record: its title and its line are its writer's statement, and
it is revised and forgotten like any other; forgetting it takes the file out of reads, erasing it
removes the bytes. Schema: MemoryRecord.follows, optional.
What a caller was given (GET /v1/memories?granted=true), found by the second server. The
listing without parent says where a caller enters the tree, and nothing said where below that it
may do more. A server that ran an agent whose memories another server kept built the agent's tools
from the entry listing, and an agent that read a memory and wrote one of its children was offered
no tool to write with. granted=true lists every memory the caller may read whose parent it
cannot see, and every memory below those on which what it may do differs from the parent, with
its privileges on each. It is stated in what the caller may do, not in how access is kept, so a
server that lets a member in by a role answers it as one that keeps a grant per memory does. §9
names it as what an agent's tools are built from. Conformance: ME-01 asks it.
- Memories (Memories), the Harness Memories sub-protocol,
optional at every class behind the
memoriescapability. A memory is a named node in a tree: it holds records and may have child memories. A grant gives a principal privileges (read,write,create,delete) on a node and the node's descendants inherit it, up to arestrictednode; what a caller may not read is answered exactly as what does not exist. - Records are written two ways (
observe: raw episodes, the provider decides what to derive, and may answer202with a job;remember: one record as stated) and removed two ways (forget: closed, trace kept;erase: content gone, with a list of what could not be reached). A change appends a version;history,as_ofand named snapshots read the past. recallacts on one memory, by meaning, words and fields, and answers with the parent and children the caller may read, so an agent walks the tree itself, up or down. What a provider did not do is said indegraded;abstainsays that nothing returned answers the question. Named queries, and free queries where a provider offers them, run in the provider's own language, confined to what the caller may read by a means the statement cannot undo.- A server keeps no memory of its own: records are kept by a provider the server is connected
to, behind one contract, and each provider says what it does in a capability document
(
GET /v1/memories/providers): isolation, derivation, signals, history, erase, queries, types. - A harness is attached to memories at
PUT /v1/harnesses/{id}/memories; attaching is granting. A task names one more inmetadata.memory. A session reaches memory at four moments: priming, the agent's tools, observation after a turn, and consolidation, whose runs are visible, bounded and reversible. - Extension record types carry their own schema and operations, for resources that are more than text.
- New objects in the schema:
Memory,MemoryRecord,MemoryRecall,MemoryGrant,MemoryProvider,MemoryConsolidationand their requests; 23 paths under/v1/memoriesand/v1/harnesses/{id}/memories. New error codes:memory_not_found,memory_forbidden,memory_record_not_found,memories_not_attached,memory_invalid,memory_unsupported,memory_busy,memory_unavailable. - Conformance suite
2026.10.4: ME-01 to ME-08 drive the tree, records, recall and the provider's capability document on a server that reports the capability and has a provider connected; a server without the capability skips them. They pass, 8 of 8, on a build of the reference server with a hosted memory service as the provider. The suite runs with one credential, so isolation between principals is not yet checked by it.
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.