# 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](versions/2026-10-04/memories.md#43-content)). 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](versions/2026-10-04/memories.md)), the Harness Memories sub-protocol, optional at every class behind the `memories` capability. 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 a `restricted` node; 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 answer `202` with 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_of` and named snapshots read the past. - `recall` acts 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 in `degraded`; `abstain` says 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 in `metadata.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`, `MemoryConsolidation` and their requests; 23 paths under `/v1/memories` and `/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](versions/2026-09-28/tasks.md#12-selecting-the-harness), [Environments §5](versions/2026-09-28/environments.md#5-attaching-an-environment)). 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](versions/2026-09-28/environments.md#4-builds-and-versions)). 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](versions/2026-09-28/environments.md)), the Environments sub-protocol, optional at every class behind the `environments` capability. 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, the `environment` field on the harness object and `metadata.environment` on a task, what a session sees (the mount, the variables, the instruction, the record), and the errors. - The session object carries `environment` ([Sessions §3](versions/2026-09-28/sessions.md#3-inspecting-a-session)). - 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-url` for 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](versions/2026-09-12/plugins.md)), the Harness Plugins sub-protocol. A plugin is an [Agent Plugins](https://agent-plugins.org/specification) 1.0.0 package, carried as files and installed into a harness through a new `plugins` array on the harness object. The server derives `manifest`, `mcpServers`, `skills` and `skipped` from 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](versions/2026-09-12/plugins.md#4-composition-what-the-agent-gets)). A harness's own `mcpServers` and `skills` keep 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, with `plugins` empty, 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 with `plugin_conflict` rather than resolved silently at run time. - **Export** ([Plugins §5](versions/2026-09-12/plugins.md#5-exporting-a-harness-as-a-plugin)): `GET /v1/harnesses/{id}/plugin` returns 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. - **`stdio` MCP servers, inside plugins** ([Plugins §2.2](versions/2026-09-12/plugins.md#22-what-the-server-derives)): a plugin's `mcp.json` may declare a process (`command`, `args`, `env`, `cwd`), reported on the plugin as a `PluginMcpServer`. The harness's own `mcpServers` list 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 with `unsupported_transport`. - **Discovery**: the `plugins` capability, optional at every class, and `plugin_schemas`, the Agent Plugins manifest schemas the server installs ([Lifecycle §2](versions/2026-09-12/lifecycle.md#2-capability-discovery)). - **Error codes**: `plugin_invalid` (422), `unsupported_plugin_schema` (422), `unsupported_transport` (422), `plugin_conflict` (409), `plugin_not_found` (404) ([Errors §3](versions/2026-09-12/errors.md#3-error-codes)). - **Security §9, plugins are third-party code** ([Security](versions/2026-09-12/security.md#9-plugins-are-third-party-code)): 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 `plugins` capability: 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: false` preserved, 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/files` is accepted and can be sent as task input by id ([Files §1.2](versions/2026-09-12/files.md#12-by-upload)). 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](versions/2026-08-11/sessions.md#6-deleting)): `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 later `GET` is `404`). `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](versions/2026-08-11/sessions.md#5-session-sharing)), closing the four gaps of [#44](https://github.com/HarnessRouter/harnessrouter/issues/44): the share object is schema'd (`SessionShare`: `id` and `url` required, `url` may be base-relative), `DELETE /v1/sessions/{id}/share` is the named revocation endpoint (other forms MAY be kept), a bodyless `POST` publishes, 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](versions/2026-08-11/sessions.md#3-inspecting-a-session)): each item of `GET /v1/sessions/{id}/turns` MUST carry `id` (the response id) and `status`, and SHOULD carry `user` / `assistant` / `tools` / `files`. Previously `additionalProperties: true` and nothing else, which held X-04 at "the endpoint answered 200". ### Conformance - `R-01`/`R-02` validate the share object against the schema and FAIL (no longer skip) when it carries no `url`; `R-06` asserts the named `DELETE` revocation endpoint and FAILs (no longer skips) when it does not work; `X-04` validates every turn item. New defect stub mode: a server whose revocation exists only as a toggle now fails `R-06`. - **`R-08`: a bodyless `POST` publishes.** 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 passes `R-01`…`R-07` on a request §5 does not require anyone to accept, while refusing the one it does. `R-08` is 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 proves `R-08` is the only check that reddens on it. No specification or schema change: this enforces a sentence that is already written. - **`tools` and `include` are reserved and ignored** ([Tasks §1.4](versions/2026-08-11/tasks.md#14-reserved-fields-tools-and-include)). 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](versions/2026-08-11/harnesses.md#41-mcp-servers), 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_fields` is specified** ([Tasks §1.1](versions/2026-08-11/tasks.md#11-request-fields)), 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](https://github.com/HarnessRouter/harnessrouter/issues/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-Version` negotiation, the `GET /v1/uhp` discovery 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 via `metadata.harness_id`, model substitution reporting, idempotency. - **Streaming** — the SSE event vocabulary, `sequence_number` ordering 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-Version` header 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](VERSIONING.md#the-current-versions-known-compromises): 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.