Skip to main content

Harnesses

A harness is an agent program driven over the Agent Client Protocol (ACP): a coding agent you already know. Four are offered today (28 October 2026): Claude Code, Codex, OpenCode and pi. Four more are described in the catalogue and not offered — Gemini CLI, fast-agent, goose, OpenHands — each with the reason, said in a sentence wherever one is named; and the long tail of ACP agents is listed, not drilled (below). Agent Runtimes drives a harness as it drives its own agents: it launches the program in the runtime, speaks ACP to it over stdio, lends it tools through the MCP gateway, writes the application's skills into its workspace in its own format, and serves it over every protocol the runtime serves — A2A, AG-UI, the session API, the embed. The harness has no face of its own and reaches nothing the runtime did not lend.

This page says what is built today and what is not; the boxes named below (SH-…) are the Shield's work items. The Shield's engine is the datalayer-agent-shield package, documented on its own site, agent-shield.datalayer.tech: the standalone proxy (agent-shield run), the server (agent-shield serve, the sidecar bridge of SH-40), registration with Datalayer, the policy reference and the register.

The spec (built: agentspecs 0.0.100, SH-01)​

A harness is a spec kind of agentspecs, one YAML per harness under agentspecs/harnesses/. An agent spec names one with harness: where it says pydantic-ai or vercel-ai today:

# agentspecs/harnesses/claude-code.yaml
id: claude-code
version: 0.0.1
name: Claude Code
description: Anthropic's coding agent, driven over ACP through the Agent Client Protocol project's adapter.
docs: https://github.com/agentclientprotocol/claude-agent-acp
acp:
command: npx
args: ["-y", "@agentclientprotocol/claude-agent-acp@0.91.0"] # pinned
protocol_version: 1
load_session: true # read from initialize, refused if it lies
mcp: lend # lend | own | none
modes: [default, plan, acceptEdits, bypassPermissions]
requires:
secrets: [ANTHROPIC_API_KEY] # ids of env-vars/, declared as skills declare theirs
binaries: [node, git]
terms:
served_to_third_parties: api_key_only # subscription credentials refused
licence: Apache-2.0 (adapter); Anthropic Commercial Terms of Service (the agent)
skills:
format: skill-md # SKILL.md folders, the agentskills.io specification
dir: .claude/skills
honours:
model: true # takes the agent's `model:`
subagents: false # has its own: `subagents:` is refused
instructions_file: CLAUDE.md
# an agent on it
id: coder
name: Coder
harness: claude-code
model: bedrock:us.anthropic.claude-sonnet-4-6

acp says how it is launched and driven; requires what it needs in the pod (secrets as ids of the env-vars catalogue, executables on the path, one the image installs for it pinned as name@version); honours and skills what of an agent spec it can take; instructions_file where the application's instructions are written for it (CLAUDE.md, AGENTS.md, GEMINI.md, .goosehints); terms the terms it is served under. Nothing in the fields is specific to coding: a framework's agent over ACP is described the same way.

The catalogue offers four — claude-code, codex, opencode, pi — and describes four it does not offer, gemini-cli, fast-agent, goose and openhands, each with enabled: false and an unavailable_because sentence (a harness that is not offered says why, and an offered one says nothing: the spec is refused at load otherwise). Naming one of the three is refused with that sentence by the runtime (harness_agent), by loop --harness, by GET /api/v1/harnesses/{id} and by the Studio's checks; loop harnesses and the reference page list them apart. custom is the one harness with no acp block, whose command an organization's own harness spec names; the shared catalogue refuses it to any agent. The reference page of every harness, generated from the specs, is agentspecs.datalayer.tech/harnesses.

make specs generates the Python and TypeScript catalogues as for every kind: agent_runtimes/specs/harnesses.py (HARNESS_CATALOGUE, get_harness, list_harnesses) and src/specs/harnesses.ts (getHarness, listHarnesses), typed by HarnessSpec in agent_runtimes.types and src/types/agentspecs.ts.

Refused at load​

A spec that asks what its harness cannot do is refused at load, in a sentence, wherever harness: is read — the generated catalogue (generate_agents.py refuses it at generation), Agentspec (a ValidationError as the spec is built), the Studio's instant checks (checkAppspec) and the variant vocabulary (src/runtimes/variants.ts):

  • There is no harness named 'x': one of pydantic-ai, vercel-ai, claude-code, …
  • The harness 'custom' names no command: an organization's own harness spec names it, and is never typed into a form.
  • The harness 'claude-code' has its own subagents: drop subagents:.
  • The harness 'x' does not take a model: drop model:. and The harness 'x' reads no skills: drop skills:. — for a harness whose honours.model is false or whose skills.format is none; none of the catalogue's is, an organization's own may be.
  • The harness 'goose' (goose) is not offered: the catalogue offers four harnesses today — Claude Code, Codex, OpenCode and pi … — at launch, for a harness the catalogue describes and does not offer (offered_refusal); the Studio says it on the agent (unservedHarnessProblem).

harness_problems(harness_id, model=, subagents=, skills=) in agent_runtimes.types says them; unservedHarnessProblem(spec) in src/runtimes/variants.ts is the TypeScript side — it refuses custom and a harness that is none, and says nothing of a served one: a catalogue harness is served by the Datalayer runtime (variantSupportsSpecHarness is true for local-pydanticai and cloud-pydanticai, the runtimes that launch it, and false for browser-vercelai, which has no process to launch; specHarnessOf keeps the catalogue id; isServedCatalogueHarness says which), and checkAppspec passes an application on one (1.5.31, SH-10's Studio side). Nothing runs pydantic-ai in a refused harness's place.

The register (built: SH-04's part in agentspecs)​

agentspecs/harnesses/register.yaml is the register: every harness of the catalogue with its adapter, the version pinned, its licence, the agent's terms, the credential it takes, whether serving it to third parties is permitted, its tier restrictions and the binaries it needs — each with where it was read, on 15 October 2026. A harness without an entry, or whose spec disagrees with its entry, does not load; a harness whose terms are unknown is not in the catalogue. The entries, in short:

HarnessOfferedAdapter, pinnedCredentialThird partiesWhy
Claude Codeyes@agentclientprotocol/claude-agent-acp 0.91.0, Apache-2.0, Node 22ANTHROPIC_API_KEYAPI key onlyAnthropic's legal page: each end user authenticates with their own key; developers may not route requests through Free, Pro or Max plan credentials for their users
Codexyes@agentclientprotocol/codex-acp 2.2.2, Apache-2.0OPENAI_API_KEYAPI key onlyOpenAI's authentication page recommends API keys for automation; a ChatGPT login needs a browser
OpenCodeyesopencode-ai 1.19.0 acp, MITANTHROPIC_API_KEY (the provider it is configured with)permittedOpen source
piyespi-acp 0.0.34, MIT, spawning @earendil-works/pi-coding-agent 1.4.0 (MIT) as published — chosen over @automatalabs/pi-acp, which embeds an older pi in processANTHROPIC_API_KEY (the provider pi is configured with)permittedOpen source on both sides; pi speaks no ACP itself, the adapter does
Gemini CLIno@google/gemini-cli 0.64.0 --experimental-acp, Apache-2.0, Node 20+GEMINI_API_KEY, or a Google login on Code Assist Standard or EnterpriseAPI key onlyThe individuals tier stopped being served on 18 June 2026. Not offered: its API host is outside a runtime pod's boundary, not routed through ai-inference, unfunded (the user's decision, 28 October 2026)
goosenogoose acp, goose 1.54.0, Apache-2.0the provider's key (GOOSE_PROVIDER)permittedOpen source, no vendor terms of its own. Not offered: a binary of its own release, not in the runtime image, not drilled
OpenHandsnoopenhands 1.16.0 acp --override-with-envs, MITLLM_API_KEYpermittedOpen source. Not offered: a Python tool the runtime image does not carry, not drilled
fast-agentnofast-agent-acp 0.10.45 (uvx), Apache-2.0the provider's keypermittedOpen source; a framework, not a coding agent. Not offered: a Python tool the runtime image does not carry, not drilled
customnothe organization's ownthe organization's ownthe organization's ownNot launchable from the shared catalogue

Reviewed by counsel: not yet. The register's engine side — the Shield reading it to refuse a credential kind — is SH-04's part in agent-shield.

What each harness honours (SH-20)​

What the runtime may ask of each harness, read from its spec in the catalogue (agent_runtimes.specs.harnesses, generated from agentspecs): how its connections are given, whether a session is reloaded after a restart, whether it takes the application's model and helpers, where its skills and instructions are written, and the modes it advertises. A spec asking what its harness does not honour is refused at load, in a sentence (above).

HarnessConnectionsReloads a sessionTakes the modelTakes helpersSkills written toInstructions fileModes
Claude Codelentyesyesno, its own.claude/skillsCLAUDE.mddefault, plan, acceptEdits, bypassPermissions
Codexlentyesyesno, its own.agents/skillsAGENTS.mdread-only, workspace-write, agent, agent-full-access
OpenCodelentnoyesno, its own.opencode/skillsAGENTS.md—
pilentyes (session/load, pi's own session files)yesno, its own.pi/skillsAGENTS.md—
Gemini CLI (not offered)lentnoyesno, its own.gemini/skillsGEMINI.mddefault, auto_edit, yolo, plan
custom——————refused in the shared catalogue

The three described and not offered (goose: .goosehints, no reload; OpenHands and fast-agent: AGENTS.md, reload) keep their rows in the catalogue and in the capabilities table for the day they are offered; nothing launches them until then.

Lent is acp.mcp: lend: the gateway's toolsets the application connects, with the run's credential, never a key of the harness's own (transports). A harness that reloads no session is started over after a restart, and says so. A mode such as bypassPermissions changes what the harness asks, never what the Shield decides: the Shield decides every action whether or not the harness asked.

The adapter (built: SH-10's adapter, SH-11, SH-12's runtime side, SH-13, SH-18, SH-19's loop; 15 October 2026)​

agent_runtimes/adapters/acp_agent.py is ACPAgent(BaseAgent), the harness as a Datalayer agent, on the agent-shield package's engine (datalayer-agent-shield 0.1.1, agent_shield.acp, policy, register): the class is the package's ShieldProxy with the runtime as its outer face, so the proxy's own interception — tool calls classed and decided, fs/* and terminal/* answered against the workspace, the limits counted, allow_always a session rule — runs unchanged, and agent-runtimes adds only what needs the runtime.

  • Launch and negotiate. initialize resolves the harness's command from its spec — an npx adapter is taken from ~/.agent-shield/harnesses/<id>@<version> (agent-shield setup --harness <id>) or from the PATH (the image's install, SH-05), never fetched at first run — gives the process its spec's plain variables and its declared secrets by name (a missing one is refused in a sentence), launches it over stdio in the workspace (AGENT_RUNTIMES_WORKSPACE_ROOT, refused when unset), runs initialize (protocol version 1, fs and terminal on) and refuses what the spec asked that the agent did not advertise (loadSession).
  • The events: agent_message_chunk → text; agent_thought_chunk → thought; tool_call → tool_call with kind, status, arguments, awaiting: approval while pending, the policy's sentence; tool_call_update → tool_call (in progress, with its displays) or tool_result (completed or failed, the diff and terminal displays in display); plan → plan; available_commands_update → commands; current_mode_update → mode; the turn's end → done with the stop reason. A malformed update, or an action a policy leaves to the person, ends the turn with a sentence ([Shield] … as text, done with stop_reason: refusal and the sentence).
  • Permissions are Rules (SH-12). ACP's kind is classed by the package's one table (agent_shield.policy.class_of: other → write, fail closed; the table-driven test both ways is test_acp_agent.py). The policies in force are the application's Rules on classes read into a team-scope document (agent_runtimes/guardrails/shield_policy.py; if_asked read as ask me first) above the platform's default. Do it answers allow_once without asking; ask me first raises the approval through the runtime's tool approvals (Waiting for you, ai-agents /ws, GET /tool-approvals/{id}) with the caller's token and answers what the person chose — a note saying "always" picks allow_always, a session rule gone with the session; leave it to me answers reject_once with the sentence; a turn cancelled while it waits answers cancelled. A Rule of the application is never loosened by a session rule.
  • Files and terminals (SH-13): fs/read_text_file, fs/write_text_file and terminal/* are answered by the runtime against the workspace, each a tool row (Read README.md — do it, kind read; Write hello.txt — do it, kind edit, the diff as its display; Run …, kind execute, the terminal as its display, its output and exit kept) and a decision; a path outside the workspace is refused in a sentence before any rule.
  • The record (SH-18): the Shield's audit rows are the application's record — a decision for what a policy decided, a tool_call for what was done (the diff as its hash and size), an approval for what the person answered, the turn and the run's end — kept as record.include names them. The harness's session is kept in the protocol state store; a restarted runtime loads it when the harness advertises loadSession and says in a sentence when the replay and the record differ; otherwise it says it starts over, never pretends to continue. Deleting the agent (a paused deployment stops its runtime) ends the process.
  • Served over ag-ui (transports/agui_stream.py: BaseAgent.stream as AG-UI events — the thinking events, TOOL_CALL_*, TOOL_CALL_RESULT with the displays, datalayer.harness.<plan|commands|mode|tool_call> custom events), acp and a2a; vercel-ai is refused in a sentence (it reads a pydantic-ai agent, SH-16).
  • loop --harness <id> runs a harness on this machine over stdio, the directory loop runs from as its workspace, the platform's default policy, the record in the terminal; a missing adapter, secret or workspace is refused before anything starts, each in a sentence — OpenCode's adapter opencode-ai@1.19.0 is not installed: neither ~/.agent-shield/harnesses/opencode@1.19.0/node_modules/.bin/opencode (agent-shield setup --harness opencode) nor 'opencode' on the PATH … — and so is a harness the catalogue does not offer (The harness 'goose' (goose) is not offered: …). The adapter is installed into the Shield's prefix by agent-shield setup --harness <id> (an npm install --prefix, pinned), never fetched by npx -y at first run. loop harnesses lists the offered harnesses with each one's adapter, credential, third-party serving, licence and capabilities, then the ones not offered with why.
  • The orchestrator's channel (SH-02's agent-runtimes part): orchestration/adapters/acp.py takes the package's channels as its ShieldChannel — stdio:<command line> launches a local agent, ws:// connects — and no longer says no stdio yet; the client it was first built on (transports/clients/acp_client.py) is out from under it.

Tests: test_acp_agent.py (45: every update kind, the permissions, the kinds table both ways, sessions and the record, the resolution of a catalogue harness, Claude Code live as a marked test) and test_acp_agent_transports.py (9: AG-UI, the orchestrator's channel, the runtime started with a harness), all on the ACP SDK's fake agent (tests/fixtures/acp_fake_agent.py, agent-shield's): the only Anthropic key at hand has no credit, so Claude Code's first live turn through the adapter waits on a funded key.

What each harness needs, and how it gets it​

Each harness runs on the vendor's own credential — the person's, never Datalayer's: an API key (Claude Code and Codex refuse a subscription login in their terms; OpenCode and pi take the key of the provider they are configured with). Where that credential lives and how the harness's process gets it depends on the harness and on where it runs:

HarnessCredentialOn Datalayer (a runtime pod)On your machine (loop --harness, Desktop, VS Code)
Claude CodeANTHROPIC_API_KEYthe key stays in the account's secrets; the harness calls ai-inference's Anthropic route with the runtime's own token, and ai-inference uses the key (below)from the environment, or set once for the Shield
CodexOPENAI_API_KEYthe key stays in the account's secrets; codex-acp's gateway authentication onto ai-inference's OpenAI route with the runtime's token (1.5.80)from the environment
OpenCodeANTHROPIC_API_KEY (the provider it is configured with)the key stays in the account's secrets; OpenCode is given its configuration inline (OPENCODE_CONFIG_CONTENT): an anthropic provider on @ai-sdk/anthropic whose baseURL is ai-inference's Anthropic route and whose key and Authorization header are the runtime's token, every other provider off — the user's decision of 28 October 2026 (1.5.88); before it, the account's secret was given to its process by namefrom the environment
piANTHROPIC_API_KEY (the provider pi is configured with)the account's secret, declared by the harness and given to its process by name (the key, from IAM — below); the pod's boundary allows api.anthropic.com. pi-acp spawns pi from the PATH: installed beside it in the Shield's prefix, which the runtime puts first on the process's PATHfrom the environment; pi on your PATH (npm install -g @earendil-works/pi-coding-agent)

What is not offered: a subscription login of any vendor (a browser flow no pod has; and Claude Code's and Codex's terms), a key of Datalayer's own, and a harness the catalogue describes and does not offer.

The key, from IAM (built: the declared secrets, 1.5.32, 15 October 2026)​

An agent on a catalogue harness declares the harness spec's requires.secrets (Claude Code's ANTHROPIC_API_KEY) as the runtime's own (guardrails/declared_secrets.py, the harness claude-code a consumer): the companion fetches them from IAM for configure-from-spec, the runtime's process holds them, and the harness's process is given them by name and nothing else of the runtime's (harness_environment); an account without them is refused in a sentence before the agent is made — … is not set up: the harness claude-code needs ANTHROPIC_API_KEY — never fetched from anywhere else.

On Datalayer, Claude Code's calls go through ai-inference (1.5.65, 20 October 2026)​

A runtime that routes its inference through ai-inference (AGENT_RUNTIMES_INFERENCE_PROVIDER_OVERRIDE=datalayer, as every Datalayer runtime does) is never given the Anthropic key. Claude Code is started with:

VariableValue
ANTHROPIC_BASE_URLai-inference's Anthropic Messages route, <DATALAYER_AI_INFERENCE_URL>/api/ai-inference/v1/anthropic
ANTHROPIC_AUTH_TOKENthe runtime's own ai-inference token (the person's token narrowed to ai-inference), set again as the harness starts

ai-inference relays POST /v1/messages (streamed or not) and /v1/messages/count_tokens to Anthropic on the account's own ANTHROPIC_API_KEY, read from the account's secrets (datalayer secrets), and records each call's tokens on the account's usage. An account with no key is answered 403: This account keeps no ANTHROPIC_API_KEY in its Datalayer secrets…, which Claude Code shows as the turn's error.

A routed harness (Claude Code, Codex, OpenCode) waits for the runtime's ai-inference token before it starts (1.5.90): a kept deployment's first keep on a fresh runtime can start its harness while the platform is still giving the runtime its token. It waits up to 30 seconds (AGENT_RUNTIMES_INFERENCE_TOKEN_WAIT, in seconds); with still no token it is refused in a sentence that says how long it waited, and a token that has already expired is refused at once. The key is not declared for Claude Code there, so the platform does not copy it into the runtime (adapters/harness_inference.py). Elsewhere — a runtime started by hand, loop --harness — Claude Code takes ANTHROPIC_API_KEY from the environment, as above.

In the runtime image (built: SH-05; the four since image 0.0.66, 28 October 2026)​

The runtime image (plane/etc/dockerfiles/agent-runtimes) carries the four offered harnesses' adapters at the versions the register pins, installed at build with npm install --prefix into the Shield's own prefix, AGENT_SHIELD_HOME=/opt/datalayer/agent-shield — harnesses/claude-code@0.91.0/node_modules/.bin/claude-agent-acp, harnesses/codex@2.2.2/node_modules/.bin/codex-acp, harnesses/opencode@1.19.0/node_modules/.bin/opencode (its musl build) and harnesses/pi@0.0.34/node_modules/.bin/pi-acp with pi beside it (image 0.0.67; 0.0.66 carried Gemini CLI in its place) — owned by the runtime's user (nobody, whose HOME is /tmp, which is why the Shield's home is named rather than found under ~), and never fetched by npx -y from the pod: a pod reaches no package index (the boundary refuses npm and PyPI), so an adapter that is not in the image cannot be installed at run time, and launch_command refuses it in a sentence. Images 0.0.44 to 0.0.65 carried Claude Code and Codex alone; OpenCode and Gemini CLI were refused there as not installed; 0.0.66 carried OpenCode and Gemini CLI, 0.0.67 OpenCode and pi. Node is Alpine 3.21's, 22.x (Zed's adapters need 22), so nothing was installed for it. Verified inside the image at build and by hand: agent_shield importable, launch_command resolving the four to the prefix and not the PATH, each adapter answering --version. The harness's workspace is AGENT_RUNTIMES_WORKSPACE_ROOT=/mnt/shared-agent/workspace, a directory of the volume the agent-runtimes container shares with the sandbox (where generated and skills live), made by the entrypoint — the contents the companion mounts land in the sandbox container's /home/datalayer, which the agent-runtimes container does not see (SH-13's open point). Operator 1.0.92 pins the image (1.0.93 pins 0.0.45, the image on 1.5.34); the prepuller and the image pruner's keep-list know the tags.

On r1 (drilled 15 October 2026, runtime image 0.0.44 on agent-runtimes 1.5.33)​

  • The platform path. loop --cloud -a example-harness-claude-code -q … got a fresh 0.0.44 pool pod, and the companion's configure-from-spec, asking the runtime which secrets its specs declare, was told the harness's key; the account holds none, so the runtime was refused in a sentence before the agent was made — example-harness-claude-code is not set up: the harness claude-code needs ANTHROPIC_API_KEY, which this runtime was not given: add it to the account's secrets (or set it before a local run), then configure it again. The runtime was stopped. — and stopped: nothing left in the pool.
  • The turn, inside a pool pod. With the key handed to a runtime started by hand in the pod (AGENT_RUNTIMES_HARNESS=claude-code agent-runtimes serve, the key over stdin, never on a command line), the adapter found the harness in the image's prefix (Harness claude-code started: @agentclientprotocol/claude-agent-acp 0.91.0, shield 0.1.1, workspace /mnt/shared-agent/workspace), the pod's AGENT_RUNTIMES_WORKSPACE_ROOT present and the directory made, session/new answered, the prompt sent, and the AG-UI stream ended with Anthropic's refusal as the Shield's sentence — RUN_ERROR Claude Code refused the turn: Internal error: Credit balance is too low — no traceback; DELETE /agents/default answered. Found on the way: image 0.0.44's ag-ui-protocol was 1.0.0, which dropped the thinking events, so the harness's AG-UI route was never mounted there (capped below 1 in 1.5.34, image 0.0.45); the drill ran with 0.1.19 in that one pod, and the pod was deleted after. Not yet proved there: the harness's child process gone after DELETE (the count was muddied by earlier runs), the keeper's relaunch (SH-18), and a kept application's permissions decided from the page (SH-12).

Examples (SH-23)​

Runnable from the repository's examples/shield, each on the ACP SDK's fake agent (no model, no key), each with its output as printed in the folder's README and run by test_the_shield_examples.py:

ScriptWhat it shows
harness_on_the_runtime.pywhat loop --harness <id> starts — the runtime, the harness made at startup, one AG-UI turn — with the Shield's decisions and the diff; --harness claude-code with the adapter and a funded key; --serve for the web chat
proxy_from_python.pyagent-shield run driven from Python as an editor drives it: an edit done, a push asked and refused, a read outside refused, the audit read back
session_rule.pya rule added to a running session holding from the next action, one that would loosen the organization's refused, the rule taken back
audit_read_back.pythe application's record of a turn: the decisions, the approval, the diff and the terminal kept whole, a key redacted everywhere
delegate_to_a_harness.pyan orchestrator delegating to the harness over A2A and reading the text/x-diff artifact, its hash checked

In the examples app, the Shield group: Shield Harness (the web chat on harness_on_the_runtime.py --serve: diffs, terminals, the approval banner) and Shield Policy (@datalayer/agent-shield's types and describeDecision).

Not built yet​

  • OpenCode and pi drilled on Datalayer: both are in the image (0.0.67) and resolved by the runtime; a funded turn of each on a runtime pod has not been run — Claude Code's and Codex's have (the sessions drills of 21–22 October 2026). Gemini CLI stays unoffered: its API host is outside the pod's boundary and is not routed.
  • plane local running a harness beside the local runtime (SH-19's second half); the Studio's Toolbox offering the catalogue's harnesses (SH-43) — an application names example-harness-claude-code by hand today.
  • Served over every protocol — see that page: MCP lent through the gateway (SH-14) and the A2A face (SH-15) are built; the diff and terminal displays are drawn by the web chat and the embed, the phone's contract and fixture given (SH-16); ACP in, ACP out is proved with the SDK's client, not yet with Zed on r1 (SH-17); skills and the instructions file are written in the harness's format, Claude Code using one end to end waits on the key (SH-21). Still open: the web chat's own transport (vercel-ai) for a harness; a harness's question (elicitation/create) answered from the page — declined today, in a sentence.
  • The Shield around a harness is built — the organization's rules from IAM, the limits soft then hard, the session's rules, the record and the trace: see The Shield. Open there: the Shield report (SH-36), the bypass drills run with a funded harness (SH-37: run with the fake agent).
  • The long tail of ACP agents as a listed tier of the register (SH-57).