---
title: "Use an ACP agent"
description: "Configure ACP harnesses and enforce their session controls."
canonical_url: "https://docs.oz0.ai/build/parts/harnesses/acp"
---
# 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](https://agentclientprotocol.com) (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:

```yaml [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:

```bash [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.

<note>

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.

</note>

### 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`](https://docs.oz0.ai/agents/colleagues) when the agent lists them, [`oz.evidence`](https://docs.oz0.ai/improve/evidence-for-agents) to an agent of a plugin that declares `reads: [results]`, and [`oz.wait_for_event`](https://docs.oz0.ai/agents/waiting-for-events), 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](https://docs.oz0.ai/spend/budgets-and-quotas), 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](https://docs.oz0.ai/concepts/security/sandbox-tiers#sandboxes), 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_*`).<br />

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.<br />

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](https://docs.oz0.ai/reference/compatibility#integrations-awaiting-live-validation).

#### 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`.<br />

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:

```yaml [oz0-plugin.yaml]
harnesses:
  - name: my-agent
    protocol: acp
    package: npm:@example/my-agent-acp@1.4.2     # 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.

<span id="what-a-session-gets-1">

Section on this page

</span>


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
