protocol/CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# Changelog
All notable changes to the Unified Harness Protocol.
## Conformance suite 2026.10.19.post6 (2026-11-04)
B-01…B-06 check the addition below on a server that reports `session_branching`: the first
response's file bytes after three turns, absence of later files, original-session isolation,
inspection provenance, the `branch_unavailable` refusal for a running response, the `400` for a
branch with no previous response (and no task run in the session it named), and a continuation in a
branch that names no origin. Missing model-created fixtures remain explicit skips; refusing a valid
fresh fixture's branch fails a server that reports the capability. A server without the capability
skips all six, so no verdict it had changes.
## 2026-10-19, addition: explicit session branching (capability `session_branching`, #477)
`branch: true` beside `previous_response_id` creates a new session from that response's
committed user files and transcript, leaving the original untouched. Responses identify the
origin with `metadata.branched_from`; session inspection identifies the source session too.
Only the response that branched carries `branched_from`; the session's detail keeps the origin,
with the `turn` it started from. Missing or unreachable checkpoints return `409 branch_unavailable`.
The request is opt-in; a non-boolean `branch`, `true` without a previous response, and `branch`
with an `input_response` are `400 invalid_input`, never run some other way. Under
[VERSIONING](VERSIONING.md) a new capability and an optional request field fit within a version:
branching is optional at every class, a server without the capability may ignore `branch` as every
server did before, and a client reads the capability before sending it. Earlier versions served
from the same implementation share the additive behavior. The unprefixed code is part of this
accepted UEP's canonical specification, following the existing `input_not_pending` precedent,
rather than an independently invented vendor error.
## Conformance suite 2026.10.19.post5 (2026-11-03)
T-13 and T-14 check the addition below on a server that reports `input_requests`. T-13 asks the
harness, through the `ask_user` tool §3.2 names, for a choice between two options, and requires the
task to end `incomplete` with reason `input_required`, the question as an `input_request` item and
in the last message's text; then an answer outside the options is `422 input_response_invalid`, an
answer to a request the response does not hold is `409 input_not_pending`, the answer that fits
continues the task to a terminal status that is not `failed`, and the same answer again is `409`.
T-14 asks for a `secret`, answers with a nonce, and requires the nonce in nothing a client reads:
the response to the answer, both responses read back, and the task's `input_items`. A model that
never asked measured nothing, so both skip; a server without the capability skips, so no verdict it
had changes. The packaged schema is regenerated from the OpenAPI document, which now carries
`harness_idempotency` as the hand-edited copy already did, and `incomplete_details` is typed.
## 2026-10-19, addition: a task can end by asking the person (capability `input_requests`)
A harness that needs one answer — a choice, a confirmation, a value it must not guess, a secret it
must not invent — ends its turn on the question ([Tasks §3.2](versions/2026-10-19/tasks.md#32-asking-the-person)).
The response ends `incomplete` with `incomplete_details.reason` `input_required`, the question is
its last `input_request` item (`kind` is `choice`, `confirm`, `free_text` or `secret`; `options[]`
carry `{id, label}`; `expires_at` is optional) and its last message's text, and the answer is an
`input_response` item on the next task, chained on the response that asked — a normal continuation,
the way a `function_call_output` answers a `function_call`. Nothing waits: the turn is over, and
the question stands until it is answered, passed over by another task (named in
`metadata.voided_input_requests`) or expires. Three refusals, one per remedy: `409
input_not_pending`, `410 input_request_expired`, `422 input_response_invalid`, after which the
request stays pending. A `secret` answer reaches the harness outside the conversation and appears
nowhere a client reads. A base without a question tool of its own is given one named `ask_user`.
`incomplete` widens from "a budget stopped it" to "stopped short, with a reason"
([Lifecycle §3](versions/2026-10-19/lifecycle.md#3-task-lifecycle)), and `incomplete_details` joins
the response's field table it was missing from. Under [VERSIONING](VERSIONING.md) a new capability,
a new output item type a client already tolerates, and an input item a server without the
capability never sees fit within a version; the three codes are unprefixed on the precedent of
`idempotency_key_reused`, as codes this specification now covers. Issue #476, with #479's typed
request.
## Conformance suite 2026.10.19.post4 (2026-10-29)
F-10 checks the addition below on a server that reports `harness_idempotency`: a repeat of a
create's key answers with the first harness, a repeat carrying a different request is refused `409`
`idempotency_key_reused`, and once that harness is deleted a repeat creates one. It deletes every
harness it made. A server without the capability skips F-10, so no verdict it had changes.
## 2026-10-19, addition: a create's Idempotency-Key (capability `harness_idempotency`)
`POST /v1/harnesses` ([Harnesses §5.1](versions/2026-10-19/harnesses.md#51-create)) takes an optional
`Idempotency-Key`, as a task does. Without one, a client whose create answer was lost cannot tell
whether the harness exists, and its retry makes a second. A server that reports the new capability
keeps a create's key for at least 24 hours in the caller's scope. A repeat answers with the harness
the first request created, waits for it while it is still being created, and is refused `409
idempotency_key_reused` when it carries a different request. Once that harness is deleted the key
names none, and a repeat creates one. Under [VERSIONING](VERSIONING.md) an optional request header
and a new capability fit within a version: a server without the capability ignores the header, as
every server did before.
## 2026-10-19, deprecation: the harness models view's `fallback`
`fallback` in `GET /v1/harnesses/{harness_id}/models` ([Harnesses §3](versions/2026-10-19/harnesses.md#3-models))
is deprecated. It names the model a server would run in place of one it cannot serve, which
[Tasks §1.3](versions/2026-10-19/tasks.md#13-model-selection-and-substitution) makes the harness's
`default`, so it only ever repeats `default`; a server that refuses an unavailable model (`422
model_unavailable`), as both maintained servers do, runs none in its place. Nothing changes on the
wire: the field keeps its type, and servers keep sending it. Under the
[deprecation rules](VERSIONING.md#deprecation) it is removed no earlier than the second version
after this one. A client reads `default` and each model's `available`.
## Conformance suite 2026.10.19.post3 (2026-10-19)
No change to the protocol, the schema or any verdict a conformant server gets.
T-12 measures the step budget (Tasks §1.1). A task sent with `max_step: 1` whose work needs three
tool calls must end `incomplete`, never `completed` or `failed`. Nothing in the suite sent
`max_step` before this, so a server that ignored the budget scored as well as one that honoured
it. The fixture gains a tool, `uhp_steps`, whose answer asks for the next step, and the check
counts the rounds from the response's own items: the tool call and the answer to it are two items
the server produced, so a model that only claims a call cannot make the count rise. A tool named
with a server's own MCP prefix (`mcp__<server>__uhp_steps`) counts as the fixture's tool.
- A server that ran the chain past the budget and reported `completed` or `failed` fails.
- A server that names `max_step` in `metadata.ignored_fields`, the answer §1.1 gives a server that
cannot bound a base, passes.
- `incomplete_details.reason` is reported in the result and never failed: §1.1 requires the status
and not the reason, and a UEP is what would make the reason a rule.
- A chain that stayed inside the budget, or never began, says nothing about the rule and is
skipped.
T-12 needs the fixture's `uhp_steps`, so the public copy of the fixture is rebuilt from this commit;
until it is, T-12 skips against the default `--plugin-mcp-url`.
## Conformance suite 2026.10.19.post2 (2026-10-19)
The packaged copy of the 2026-10-19 schema marks `HarnessModels.fallback` deprecated. No check or
verdict changes.
## Conformance suite 2026.10.19.post1 (2026-10-19)
No change to the protocol, the schema or any verdict a conformant server gets.
T-11 reads a background task back while it is still running and requires the lifecycle table's name
for the state, `in_progress`. No check looked at a task before it reached a terminal status, so a
server could answer every background POST and every poll with a name of its own and stay green.
HarnessRouter answered `running`, which the table does not define, while its own stream emitted
`in_progress` for the same turn: a client written to the specification waited for `in_progress`,
or treated the unknown status as an error. The check skips rather than fails when the task finishes
before it can be read mid-turn, since there is then nothing left to observe.
## 2026-10-19, addition: when each turn began and ended
Additive. Each item of a session's `turns` ([Sessions §3](versions/2026-10-19/sessions.md#3-inspecting-a-session))
SHOULD carry `created_at`, when the turn began, and `completed_at`, when it ended, both in Unix
seconds as every other time in the protocol. `completed_at` is `null` while the turn is running and
when the server does not know when it ended. An imported turn carries the time it was imported for
both. A client no longer reads each turn's response to learn its time. `TurnItem` gains both fields.
## 2026-10-19, addition: earlier turns (capability `input_history`)
Additive. A request without `previous_response_id` may carry the conversation so far as `user` and
`assistant` messages before the last `user` message ([Tasks §2.1](versions/2026-10-19/tasks.md#21-earlier-turns)).
A server that reports the new `input_history` capability gives them to the harness as its history,
treats only the last `user` message as the task's own, keeps them as the session's first turns marked
`imported: true` in `/turns` (completed, no model ran, no cost). Beyond its bounds it imports none
of them and reads the request as without the capability, never refusing it. A server without the
capability, or one that reports it `false`, is unchanged.
## 2026-10-19
Against `2026-10-04`, one rule changes and the rest is additive.
**Retiring a harness may retire its sessions with it, under a stated restore window**
([Harnesses §5.3](versions/2026-10-19/harnesses.md#53-delete)). Every earlier version said a server
MUST NOT delete the sessions or responses that used a deleted harness. That stays the rule for a
server that reports the new `harness_retirement` capability `false`, or says nothing. A server that
reports it `true` MAY remove them with the harness, under three conditions:
- it states the window when the harness is retired: the delete response carries
`sessions: "retired"` and `restorableUntil` (Unix seconds), and its own interface tells the person
the window's length before they confirm;
- within the window, a restore brings back the harness and its sessions together;
- only after the window may it remove them for good. The one exception is a person's own request
to erase their data, which may remove their retired sessions sooner;
- it never retires a harness mid-task: it cancels the task first, or refuses the delete with `409`
and the new code `harness_busy`, saying why.
A server may withhold them from lists and reads while the window runs. The delete response gains
`sessions` (`kept` or `retired`; absent means kept) and `restorableUntil`. This is the one change
that is not additive. [Versioning](VERSIONING.md#the-current-versions-known-compromises) records what
it means for a server that also serves an earlier version.
**Harness writes refuse what they cannot store**, now stated in
[Harnesses §5.1](versions/2026-10-19/harnesses.md#51-create): a field the harness does not have and
a `default_model` its base does not run are `422`, the latter with `model_not_available`, now in
the error table beside the task-time `model_unavailable`. Both maintained servers have done this
since 2026-10-10.
**Conformance suite 2026.10.19.** New check F-09 (class Full) creates a harness, runs a task on it
and deletes the harness. A server whose `harness_retirement` is `false` or absent passes when the
session still reads afterwards and the response says nothing else. One that reports it `true` passes
when the delete response says `sessions: "retired"` with a future `restorableUntil`.
## Conformance suite 2026.10.4.post11 (2026-10-15)
No change to the protocol, the schema or any verdict a conformant server gets. ME-01 assumed the
memory the suite creates is a root of the server's tree and asked its child for exactly one
ancestor. A server whose memories are its own workspaces places a new memory inside the caller's,
which the chapter allows, and would have failed for that alone. The child's ancestors are now
expected to be the memory's own ancestors and then the memory, root first.
## 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.
**Facts that change (§4.4), standing records and observed conversations, found while preparing
three public memory benchmarks for the second server.** A memory that keeps facts has to tell an update
("I moved to Porto") from an addition ("I got a fourth bike") and from a disagreement ("I have
never written a route", after describing one), keep the time something was said apart from the
time it became true, and give an agent its standing instructions without being asked. The chapter
had a word for none of these, so every writer and every server would have invented its own. Now:
- A fact's attributes have defined keys: `predicate`, `value`, `holds` (`one` or `many`),
`stated_at`, `as_said`, `precision`, and `standing` (on any record). Without `time.valid_from`, a
fact became true when it was said. A server that declares `holds` places a `one` fact among the
others on its subject and predicate by their world time: what was true at T ends at T (a version
of the server's own, `on_behalf_of` the writer), a different value from the same instant is
linked with `contradicts` and both stay, and a fact about the past ends where the next value
began. The answer to the write says what it did, in `closed` and `conflicts`, never stored.
- `contradicts` is a defined `rel`, kept on both records. A recall result on one side brings the
other, after the ranked results, with `score` null and `why` `["contradicts"]`, outside `limit`.
- `GET /v1/memories/{id}/prime` reads a memory's standing records, newest first, within a token
budget, from that memory alone unless `depth` widens it; a server priming an agent reads each
memory it was granted at depth 0. The capability `prime` says whether a provider answers it.
- An observed episode carries `time.valid_from`, a content part may carry `name` beside `role`, and
a conversation is observed one session to an episode, in order. A job says `queued`, `running`,
`completed` or `failed` (`pending` stays for a provider that cannot tell the first two apart),
with what it wrote, `closed`, `conflicts` and `usage`.
- A revision no longer changes the world time of the version it replaces: that version gets only
its belief-time stamp, so `as_of` reads what the memory believed then. Stated because the second
server's store did otherwise; its existing histories needed no repair.
Additive: every request and object valid before is valid now. Schema: `MemoryContentPart.name`, the
episode's `time`, `MemoryJob` (`memory_id`, `closed`, `conflicts`, `usage`, times; `queued` and
`running`), `MemoryRecord.closed` and `.conflicts` on a write's answer, `MemoryProvider.prime` and
`.holds`, `MemoryPrime` and its path. Suite `2026.10.4.post10`: ME-11 (five facts on one subject:
ended, linked, placed in the past, placed inside an earlier value), ME-12 (both sides of a
disagreement from a recall of one), ME-13 (the prime read: only standing records, newest first, a
budget, the memory alone).
- **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.