Choose a harness

Use an ACP agent

Configure ACP harnesses and enforce their session controls.

For: plugin authors.

Configure ACP harnesses and enforce their session controls.

Agents that speak ACP

The Agent Client Protocol (ACP) is how editors drive coding agents: JSON-RPC over the agent's stdin and stdout, with the editor as the client. Codex (through codex-acp), Gemini CLI, OpenCode, goose, Cline and others speak it. A harness with protocol: acp runs such an agent as a job's session, with the node runtime as its client. The runtime implements ACP protocol version 1, as schema release 1.24.1 describes it.

Use one

The official plugin harness-acp pins four of them, each to one version, and installs them on the nodes that get the plugin: codex (@agentclientprotocol/codex-acp 2.1.1), gemini (@google/gemini-cli 0.62.0), opencode (1.18.34) and goose (1.53.0, both from their release archives, checked against their SHA-256). Its templates/ folder has an agent for each to copy into your own plugin:

agents/acme-goose-coder.yaml
name: acme-goose-coder
description: Makes small changes to code with goose.
runtime: harness:goose
model:
  use: coding                         # a tenant alias, or openai/<model>
input: text
output: text
instructions: prompts/acme-goose-coder.md
limits:
  max_steps: 40                       # tool calls; the session stops past them
  timeout: 30m
harness:
  tools: [read, search, edit, execute, think]
  requires_approval: [execute]        # a person approves each command the hooks let through

The four agents need a provider of the model API they speak: Codex the OpenAI Responses API, OpenCode and goose the chat completions API, and Gemini CLI Google's generateContent API, which the gateway meters with genai-prices like the others. For Gemini, add the shipped preset and the key, and use google/<model> as the agent's model:

Terminal
oz0 provider set google --preset google
oz0 secret set GEMINI_API_KEY

The plugin installs all four (several hundred megabytes in all); to install fewer, copy it and remove the harnesses you do not want.

The four definitions are pinned and their settings researched against each agent's documentation and code, but the project's tests run a fake ACP agent, not these: they need provider keys. Try one on a test tenant before you rely on it, and tell us what breaks.

What a session gets

  • Its model through the node's LLM proxy. The session's model is the agent's model.primary, or its alias resolved when the job starts, in the first API of the harness's acp.formats that the provider speaks. The harness's settings name it as ${model}, the proxy's address for its provider as ${base_url} and the session's own key as ${api_key}: the key reaches only the proxy's model routes and charges the job, so every call is metered, and budgets, quotas and kill switches apply. No provider key reaches the agent.
  • Pinned settings. A home and configuration directories of the session's own, the harness's acp.env, its session mode (acp.mode, or the agent's harness.mode) and the agent's harness.config settings, all from the plugin's commit. When the agent offers a choice of model, the runtime selects the session's.

The tenant's tools, through the job

Tools of your tool plugins that the agent lists under tools: (such as acme_jira.search) reach the session as one MCP server named oz0, which the agent starts over stdio. Each call goes to the job's workflow and runs there as an agent's tool call does: default-deny, the tenant's tool policy, the routine's tools, the job's hooks and approvals, a shadow run's stubs, and the tool on the job's home node. The agent never gets the tool's program or its secrets. An agent that asks before MCP calls asks with kind other, so list other under harness.tools too.

  • The platform's built-ins. The same server offers oz.ask_colleague, oz.board_write and oz.board_read when the agent lists them, oz.evidence to an agent of a plugin that declares reads: [results], and oz.wait_for_event, whose call registers the wait and tells the agent to end its turn: the session then waits between turns, as for a quota wait, and goes on with the event as its next prompt.
  • Questions to people. An agent with oz.ask_user in its tools: gets ACP's form elicitation: each field of the agent's form becomes a question in the job's approvals inbox, with the field's choices, and the answers fill the form. A URL elicitation is declined, since people answer in the inbox, not in this client.
  • A wait when a quota is spent. When a quota refuses one of the session's model calls, its turn ends, the job waits for a budget decision, and the session goes on with another prompt.
  • The file system and terminals of ACP only inside the sandbox. On a node with sandbox tier 1, the runtime offers the agent ACP's fs/read_text_file, fs/write_text_file and terminal/*: files only inside the session's working directory, opened without following links, regular files only and at most 10 MiB per read, and each command in a sandbox of its own, as tight as the agent's, with the agent's variables set inside it (never OZ0_*, LD_* or DYLD_*).
    A file or terminal call runs without another decision only when an allowed, unfinished tool call of the same kind names that path or runs that command (a terminal that runs it under a shell's -c, or with exactly the words a shell reads in it, each plain or single-quoted); otherwise it is decided as a call of its own.
    Elsewhere it offers neither, and a harness that lists them under acp.needs refuses to start, saying it needs tier 1.

Tools, hooks and approvals

An ACP agent's tools are the protocol's tool kinds: read, edit, delete, move, search, execute, think, fetch, switch_mode and other. Hooks and approvals name them <harness>.<kind>, such as goose.execute, and see the agent's input for the call with its title added as title. An agent that lists no harness.tools gets every kind but switch_mode, since a mode switch can turn off the agent's own asking.

Each session/request_permission goes the way of every harness call: default-deny first, then the job's hooks, then a person. The answer selects one of the agent's options, never an "always" one, which would let later calls skip the decision. Two things differ from JSON-lines harnesses:

  • A hook cannot change a call. ACP's answer only selects an option, so a hook's modify refuses the call, with that reason.

What a repository's own settings can do to a session

A session's MCP servers are the runtime's, and a repository a coding tool works on may hold settings of that tool's. Codex reads a project's .codex/config.toml only for a project its home trusts, and a session's home is its own: that one has been run. OpenCode is told to read no project settings (OPENCODE_DISABLE_PROJECT_CONFIG), which also leaves out the project's instruction files. goose documents settings of the user's alone. Gemini CLI reads a project's .gemini/settings.json; harness-acp holds it to the runtime's server by name (--allowed-mcp-server-names), which limits servers and nothing else in that file.

Gemini CLI, OpenCode and goose rest on their documentation and have not been run this way, so a Gemini CLI session should work only on repositories you would run its settings from: see the integration maturity.

The agent decides when it asks

The harness's settings tell each agent to ask for everything that changes something (Codex's read-only mode, goose's approve mode, OpenCode's ask permissions). A tool call that runs without asking, where the job's policy needed a decision (a kind the agent does not declare, or one a hook or an approval covers), stops the session and fails the job with HarnessUnasked. So does a change to a pinned session mode, and a call that starts before its answer came. A call that runs after it was refused fails the job with HarnessRefusedRan.

These are caught as the call starts, so they bound a breach to one call; they do not undo it.

  • Where every call is decided first, the harness must ask about every call. A routine's fire calls only the tools its routine lists, and a shadow run stubs every call that is not read-only, so each call has to reach the job before it runs. An ACP harness runs such a job only when its acp.asks lists every kind the session may use (or *), with its pinned mode and no harness.config of the agent's; otherwise the job fails before anything runs, with HarnessCannotAsk.
    Of the pinned harnesses of harness-acp, goose (in its approve mode, also pinned as the session mode) and OpenCode (with "*": "ask") list *, from their code at these versions. Codex and Gemini CLI run some calls without asking in every mode (reads, searches and sandboxed commands for Codex; read-only tools for Gemini CLI), so they say why under acp.cannot_ask, and the job fails saying so.

In the live stream (job watch, run) and the job's trace, a session shows its text, its reasoning (…), its plan (☰), its tool calls with their titles and progress (↻), their results, and the context window the agent reports (·). Cancelling the job sends session/cancel, answers the open permission requests with cancelled, waits five seconds for the turn to end and then stops the agent's process group.

Add an ACP agent

Any agent that speaks ACP can be a harness. Pin its program and say how a session reaches its model:

oz0-plugin.yaml
harnesses:
  - name: my-agent
    protocol: acp
    package: npm:@example/[email protected]     # or pypi:<name>==<version>, or binaries:
    command: [my-agent-acp, --acp]               # the package's bin and its arguments
    tools: [read, search, edit, execute]         # the kinds a session gets by default
    acp:
      formats: [openai-chat]                     # the model APIs the agent speaks, preferred first
      auth: api-key                              # an authentication method to select, if the agent needs one
      mode: ask                                  # the session mode to set
      needs: [terminal]                          # client capabilities it cannot work without: fs, terminal
      asks: [edit, delete, move, execute, fetch]  # the kinds it asks about with these settings, or ["*"]
      # cannot_ask: reads without asking in every mode   # or: why it cannot promise more
      env:
        MY_AGENT_BASE_URL: ${base_url}
        MY_AGENT_API_KEY: ${api_key}
        MY_AGENT_MODEL: ${model}

package installs an npm or PyPI package at exactly that version; binaries downloads one archive per platform (linux/amd64, darwin/arm64 and so on, .tar.gz, .tar.bz2 or .zip) and checks its SHA-256, and command[0] is then the program's path inside it. The placeholders ${model}, ${provider}, ${format}, ${base_url}, ${api_key}, ${workdir}, ${state}, ${home}, ${job} and ${agent} work in acp.env and command. Set the agent up to ask before everything that changes something, and to send no telemetry and check for no updates: in a sandbox those requests are refused, and on any node they leave the fleet.

List under acp.asks only the kinds you checked the agent asks about with these settings. plugin lint checks the pins, the kinds and the placeholders. scripts/fakeacp/fakeacp.py in the repository is a small ACP agent to start from.

Section on this page

Copyright © 2026