The workflow frame
A frame is what the embedder says this step is, as a typed value.
It carries what has already been established, what must hold here, what the step owes before it may end, and — the enforcing part — strictly the operations admitted here.
Set once, at launch
In this release a frame is set when the run starts, by --frame <FILE>, and holds for the whole
run. frame.set is refused on every adapter with UNSUPPORTED_CONTROL. It is the command that
would replace the frame mid-run, and a replaced frame needs its text delivered between turns and
its operations enforced per call, both or neither; no adapter delivers the first. Read it with
metaharness capabilities <kind>; crates/metaharness-claude/src/seam.rs
(frame_set_is_refused_because_it_is_not_partially_deliverable) asserts it on Claude Code, and
crates/metaharness-cli/tests/website_claims.rs on every kind.
A malformed frame.set is refused MALFORMED before that: a line that does not decode
(Run::send_line), or a frame whose digest no longer describes it. Well-formedness is checked
before the adapter's support (apply in crates/metaharness/src/run.rs), so the code sends the
caller to the input they got wrong; a_frame_mutated_after_it_was_sealed_is_refused_as_malformed in
crates/metaharness/tests/run_loop.rs reads it on Claude Code.
The embedder's per-state tool set changes at every transition. An embedder that needs a per-step guarantee starts each step as its own run, with its own frame; a frame held across steps turns a per-step guarantee into a per-session one.
A step is the embedder's unit of work. A turn is the vendor's. Conflating them is exactly how a per-step guarantee quietly becomes a per-session one.
What a frame holds
| Field | What it is |
|---|---|
workflow | Which workflow, at which pinned version. |
node | The state the run is in. |
step | Workflow, state, index, and which attempt — counting from 1. |
prior | What has already been established, one line each, each with the document that established it. |
obligations | What must hold while here. Verbatim, never summarised. |
reaching | What does not hold yet on a way out, prefixed with where it goes. |
next | The nodes reachable from here. |
handoff | What this step must produce before it may end. |
operations | Strictly the operations admitted here. |
entities | The enumerated set a routing step chooses from, when there is one. |
digest | SHA-256 over the canonical form of everything above. |
Why reaching exists
A recorded failure: a run in which the model was never told what the next state wanted, wrote neither, and was refused for work already paid for.
Why lines are verbatim
A driver that summarised a requirement would be the only place that summary existed.
Why handoff is explicit even when empty
handoff: none says a step owes nothing, deliberately. A step whose handoff is unstated is a
step nobody can fail.
The operation vocabulary is small and closed
The frame names neutral operations. The adapter renders them into vendor tool names and never re-decides what an admission implies.
file.read file.write file.edit dir.list search
shell web.read skill.load subagent.spawn
task.todo mcp.call{server, tool}
That closure is load-bearing: a second harness that decided file.write admitted a shell would be
a weakening the protocol had no way to notice.
subagent.spawn is not admitted by default on any adapter — a subagent's tool set is derived
by nothing in these decisions, so it would be a route around the per-step admission.
Sealing, and why the digest is on the document
metaharness run claude --hermetic --frame step.frame.json -p "…"
The on-disk form is one JSON object tagged metaharness.frame/1. Loading it checks, in order:
- The format tag — a document is self-describing or it is refused.
- The shape.
- The digest — a document whose digest does not describe its contents is refused.
A frame is refused by name when it is unreadable, untagged, misshapen, or edited after sealing. Writing a frame out does not reseal it: a producer that mutated a sealed frame should be refused by every consumer rather than silently repaired.
The tag lives on the document itself, not on a file extension or a handshake, so a copied or truncated file is still self-describing.
Canonical ordering is a wire fact
Operations serialize sorted by their op name (then by MCP server and tool). The sealed digest is
over that serialization. This is deliberately a rule an external producer can follow without
reading the Rust enum — a digest that depended on variant declaration order would make the
canonical form depend on one file in one repository.
Two independent ways a frame reaches the model
| The text | The frame is rendered as instruction text. It says what the step is. |
| The control seam | It is what makes the text true. |
Text alone is not enforcement. The rendering function lives in the protocol crate and is shared by every adapter, so two harnesses cannot describe the same frame differently.
This is the integration seam
An external driver writes the frame as a file and never links this workspace. That is the whole interface: a sealed document on disk, verified on load, enforced per call from the first turn.