Skip to main content
Harnesses explains what each loop is. This page is the matrix: what each one can actually do today, read off the catalogue and the loops themselves rather than off the design. Read it before you pick a harness for a team — one of the differences below decides whether an agent can be a coordinator at all.
The values below are the compile-time catalogue. runnable is the one field that varies by environment, so GET /v1/harnesses is authoritative for the deploy you are talking to. Everything else on this page is a property of the loop and is the same everywhere.

The matrix

All four are location: "hosted" and billing: "usage". There is no local harness today; the field keeps a "local" value because the refusal that would catch one is already written. All four capability rows are on the wire, under capabilities on GET /v1/harnesses — nothing on this page has to be hard-coded by a picker.

What each row means

Execution is what an idle session costs, and it is not the same question as who operates the loop. isolate runs the loop in the session object itself — its workspace is rows in that session’s own storage, so a session between turns holds no machine and bills storage only. sandbox runs the agent as a process on a micro-VM, which the session holds across turns. For long-horizon work that spends most of its life waiting, this is usually the deciding field. Runnable answers “will a session on this start, on this deploy”. A harness whose agent is a process in a box also needs a base image built in that environment’s provider account, and staging may have one where production does not. pi and vetta need no image and are runnable wherever the code is deployed. approvals — the loop can hold a tool call open and wait for a person. That covers both parks: a decision on tool confirmations, and an answer to a question the agent asked with ask_operator. A harness with no can run neither, and the session is refused at start rather than silently downgraded. A denied tool at least fails loudly. An unheld question does not: the harness would have to answer the call before anybody had been asked, handing the model an empty answer it cannot tell from a real one — so ask_operator is refused on the same predicate, up front, before any spend. structured_output — the loop can enforce structured_output_required, and can serve a typed delegation. streaming_deltas — the loop emits incremental message.delta events. With no you still get message.completed: the answer arrives whole rather than as it is written. injected_tools — the platform’s tools reach the loop’s model. Every published harness declares yes, by one of two roads, and the road a harness takes changes two things about how the tools behave. It has its own section.

Platform-injected tools

This is the injected_tools capability. Every published harness declares it, and this section is about how — because the two roads are not identical. Vetta contributes these families of tool to a turn, chosen per turn rather than fixed when the agent was made:
  • Team toolssend_to_agent, wait_for_agents, list_agents for delegation, and board_read / board_write for the board.
  • MCP servers declared on the agent.
  • Connections — the third-party integrations an agent acts through.
  • Social publishing and your apps, including the tools an app declares for itself.
  • Vetta’s own built-insweb search and fetch, image and video generation, skill disclosure, publish_file, the managed browser. These reach pi and vetta only (see the second card).

pi and vetta — the loop is ours

We assemble the turn, so one of our tools is simply another entry in the toolset the model is offered. Every family above arrives, built-ins included, and an ask can be held for a decision where the harness declares approvals.

claude_code and hermes — the loop is a CLI

The agent is a binary running as a process in a micro-VM, driven by its command line, carrying its own toolset. It is given a session-scoped tool endpoint at the start of every turn and calls our tools over it, alongside its own. You configure nothing. Every family above except the built-ins travels this road.
Your tools policy governs both roads, by the same names. <server>.<tool> for an MCP server, <connector>.<tool> for a connection, the plain name for a built-in. On the second road a deny is not merely refused when called — the tool is never put in the list the agent is given, so the model cannot ask for something the policy forbids.

Two limits on the second road

Both follow from approvals: false, and both are published rather than discovered at a turn.
A tool whose permission is ask is left out entirely, not gated. Nothing in such a machine can hold a call open while a person decides: the agent’s own prompt is closed, and the call is a single request that has to be answered now. This is why a session whose toolset carries an ask is refused when it starts on a harness declaring approvals: false, naming harness — the guard is what keeps the omission fail-closed instead of silent. The one case you meet without writing ask yourself is social publishing: social.post defaults to ask, so on these two harnesses it is not offered unless you set it to allow explicitly.
wait_for_agents does not pause the turn on these two. A coordinator that fans work out and then calls it is not suspended mid-turn the way it is on pi; it parks once the turn goes idle, and the delegated threads open correctly either way. The cost is a few extra model calls between the fan-out and the park — an inefficiency, not a wrong answer.

Coordinators and members

A coordinator coordinates by calling send_to_agent and board_write, so it needs the team tools. Every published harness can now be one. Members were always unaffected: a member is handed a brief and answers it, and is never given send_to_agent at all — delegation is one level deep. A team may mix harnesses freely, at either level. The refusals that used to enforce this are still in the code and still fail closed; no published harness trips them today. A base whose loop could neither be handed our tools nor call for them would be refused as a coordinator at the agent save, naming multiagent, and refused a non-empty mcp_servers, naming mcp_servers; a coordinator stored on such a base would be refused at session start, naming harness.

Nothing is a quiet best effort

Where a capability is gated, it is gated up front. A session asking for something its harness does not declare is refused when it starts, with a 400 naming harness — the half of the pair you can change: The alternative was worse and is why these exist: an ask toolset that silently became allow would remove a human-in-the-loop gate with no event and no error to notice it by.

Known gaps

Stated here rather than left to be discovered at a turn.
vetta declares structured_output: true and its loop cannot produce one. The catalogue publishes the capability, list_agents reports the member as typed, and every gate — session start, roster save, send_to_agent — admits the pairing. The loop has no submit path, so the promise can only fail at the turn. Until it lands: run structured_output_required on pi, and delegate to a vetta member without an output_schema, asking for the shape you want in the brief instead.
Vetta’s own built-ins stop at the first road. Web search and fetch, image and video generation, skill disclosure, publish_file and the managed browser are handed to a loop we host and are not published over the session-scoped endpoint, so a claude_code or hermes agent is never offered them. It has its CLI’s own web, file and search tools instead. Nothing errors — the tool is simply absent — so a prompt that instructs the agent to call publish_file will read as though the model ignored it. Use pi or vetta when a built-in is load-bearing.
A tool call made over the session-scoped endpoint is not in the event stream on hermes. That harness re-emits one whole answer rather than a running transcript, so tool.started / tool.completed are not published for any tool it calls, ours included. claude_code publishes them normally. Cost is metered on both.
advisor roster entries parse and are refused at save. { "type": "advisor", "model": "…" } is a legal shape on the wire and the CLI accepts the advisor:<model> spelling, but saving a roster that contains one fails with advisor roster entries are not yet served. See Roster entry types.

Reading it at runtime

The catalogue is served, so a picker never has to hard-code this page:
capabilities carries all four booleans. What this page adds is the values in one place, and what each false — and, for injected_tools, each road — costs. Full field reference: GET /v1/harnesses and client.harnesses.

Back to harnesses

What each loop is, and how changing the field versions the agent.