Agent Client Protocol Harness
The Agent Client Protocol (ACP) harness adapter connects HarnessAgent to any
harness compatible with ACP version 1. The generic adapter owns the bridge, ACP
client, host-tool relay, event translation, approvals, and lifecycle behavior;
the inline profile describes how to install and configure one ACP runtime.
All you need in addition to the ACP harness adapter is an ACP-compatible implementation that can be installed from NPM or with a trusted Bash command. You can then configure the ACP harness adapter with a few lines of code.
Harness packages are experimental. Expect breaking changes between releases as this early API gets further refined.
Setup
pnpm add @ai-sdk/harness @ai-sdk/harness-acp @ai-sdk/sandbox-vercel
Basic Usage
Define an ACP harness profile, such as one of the
complete implementations below, and
pass it to HarnessAgent:
import { HarnessAgent } from '@ai-sdk/harness/agent';import { createVercelSandbox } from '@ai-sdk/sandbox-vercel';import { codexACPHarness } from './codex-acp-harness';
const agent = new HarnessAgent({ harness: codexACPHarness, sandbox: createVercelSandbox({ runtime: 'node24', ports: [4000], }),});
const session = await agent.createSession();try { const result = await agent.generate({ session, prompt: 'Check the test failures and fix the production code.', }); console.log(result.text);} finally { await session.destroy();}Adapter Settings
createACP accepts:
harnessId: stable kebab-case identity for this profile.version: ACP protocol version. It defaults to and currently supports only'v1'.source: how to acquire the ACP implementation, as a simple NPM package, locked NPM installation, or trusted Bash install command.executable: the bare command name to launch from the acquired implementation.args: optional arguments passed to the executable.forwardEnv: non-credential host environment variable names to forward into the sandbox.credentialEnv: host credential environment variable names. Configure this together withcredentialBrokering.credentialBrokering: function that receives the effective host runtime environment asenvand the environment forwarded to the sandbox assandboxEnv, then returns additive outbound request transformations. Use the credential fromsandboxEnvin exact request header matches and the real credential fromenvin transformed request headers.credentialForwarding: optional synchronous or asynchronous callback that customizes each credential immediately before the harness adapter forwards it into a sandbox process. It receives the credential value that would otherwise be forwarded (either the real credential or a masked value) and the environment variable name used to expose it. This callback only controls the value forwarded into the sandbox process. It does not restrict which credentials the harness adapter can discover, read, or otherwise access in the host process.env: persistent environment values written at bootstrap.builtinTools: optional native tool definitions for static typing and exact name matching.mcpServers: MCP server definitions keyed by server name.hostToolMcpTransport: transport used for the harness-owned MCP server that exposes host tools to the ACP implementation. It defaults to'stdio'. Set this to'http'for implementations that only accept HTTP or SSE MCP servers from the client, which requires the implementation to advertiseagentCapabilities.mcpCapabilities.http.authentication: advertised ACP authentication method, metadata, and client capabilities.auth: downstream provider authentication mode:auto(default): use AI Gateway when Gateway credentials and aproviderAuthenticationconfiguration are available; otherwise use direct authentication.direct: always use direct authentication, even when Gateway credentials are available.ai-gateway: always use AI Gateway and throw when Gateway credentials are unavailable. This requiresproviderAuthenticationconfiguration.- An authentication environment: auto-detect from the supplied record instead of the host process environment.
providerAuthentication: declarative runtime-specific Gateway environment.modelMapping: required static mapping from themodelsupplied toHarnessAgentto the ACP operation used by the implementation. Usesession-config-optionwith its configuration option ID aspath, orsession-modelwith thesession/set_modelrequest property aspath.skillsDirectory: native skills directory relative to the implementation's effective$HOME. It defaults to.agents/skills; override it for runtimes that use a different location.instructionMapping: optional mapping fromHarnessAgentinstructions to an implementation's native system or developer prompt. Usesession-metafor a path below the ACP session request's_metafield,launch-env-jsonfor a path within a JSON launch environment variable, orfilesystemwith a relativepathto write instructions to a file under the implementation's effective$HOME. When omitted, instructions are prepended to the first user prompt.outputSchemaMapping: optional implementation-specific mapping from a structured output JSON Schema to a path below the ACPsession/promptrequest's_metafield. ACP does not standardize structured output, so omit this unless the selected implementation documents that private extension.askUserQuestions: optional translation between an implementation-specific ACP client request for questions and the HarnessaskUserQuestionstool. Configure it only when the ACP implementation exposes a native request that waits for the client's response.permissionModeMapping: mappings from all three Harness permission modes to advertised ACP session modes or configuration options. Set an entry tonullwhen the ACP implementation does not support that mode. When omitted, the adapter appliespermissionModeto ACP permission requests by tool kind.session.meta: serializable implementation-specific metadata for session creation and restoration.port: exposed bridge port override.startupTimeoutMs: bridge startup timeout. The default is 120 seconds.mintBridgeToken: synchronous function that receives the sandbox id and returns the bridge authentication token. By default, the adapter generates a random 32-byte token. Custom implementations must return a suitably secret token.clientApp: optional client attribution withnameandversion. It defaults to the installedai-sdk/harness-acp/<version>identity and is available to Gateway environment placeholders.
Runtime-specific package names, environment variables, modes, and session metadata belong in the inline profile, not in the generic adapter.
Structured Output
ACP version 1 does not define a structured output capability. Profiles for implementations with a private prompt metadata extension can opt in explicitly:
const harness = createACP({ harnessId: 'example-acp', source: { type: 'npm-simple', packageName: '@example/acp-agent', }, executable: 'example-acp', modelMapping: { type: 'session-config-option', path: 'model', }, outputSchemaMapping: { type: 'session-prompt-meta', path: ['outputSchema'], },});For a HarnessAgent configured with output, this writes the generated JSON
Schema to session/prompt.params._meta.outputSchema. A profile without this
mapping throws HarnessCapabilityUnsupportedError instead of assuming that an
arbitrary ACP implementation understands the schema; Codex ACP is unsupported,
while the Grok Build harness includes its verified mapping.
Implementation source
A simple NPM source installs a single package by name:
const source = { type: 'npm-simple', packageName: '@agentclientprotocol/codex-acp', packageVersion: '1.1.4',} as const;packageVersion is optional and must be an exact version when supplied. Omit it
to install the package's latest dist-tag instead. An omitted version also
stays out of the harness identity, so a new upstream release does not
invalidate existing lifecycle state.
A simple source pins only the requested ACP package, while its transitive
dependencies are resolved when the sandbox bootstraps. To freeze the complete
installation, use a locked source and provide the contents of a package.json
and its pnpm-lock.yaml:
const source = { type: 'npm-locked', packageJson: packageJsonContents, pnpmLockYaml: pnpmLockYamlContents, pnpmWorkspaceYaml: pnpmWorkspaceYamlContents,} as const;A locked source installs the supplied files with
pnpm install --frozen-lockfile. pnpmWorkspaceYaml is optional; provide it
when the locked installation needs workspace-level pnpm configuration, such as
an exact-version allowBuilds policy for a required dependency build script.
The manifest, lockfile, and optional workspace configuration are persisted bootstrap artifacts and participate in lifecycle identity. Do not include credentials in these strings.
An install command source runs a trusted Bash command inside the harness's deterministic bootstrap directory:
const cursorSource = { type: 'install-command', command: 'curl https://cursor.com/install -fsS | bash',} as const;
const fxSource = { type: 'install-command', command: 'curl -fsSL https://fx.sh/setup.sh | bash',} as const;The command runs with set -euo pipefail, with its working directory set to
.harness-bootstrap/<harnessId>/implementation and HOME set to the home
directory immediately below it. All persistent installation files must remain
below that HOME. The command must install executable into
$HOME/.local/bin; the adapter launches that deterministic path without a
shell and does not fall back to a command already present in the sandbox.
Install commands do not provide automatic version pinning. A successful bootstrap remains cached, and the exact command participates in bootstrap and lifecycle identity. Treat the command as executable configuration and do not embed credentials in it because bootstrap recipes are persisted.
Authentication
AI Gateway authentication is supported when providerAuthentication defines
how the underlying ACP implementation accepts Gateway configuration. The
adapter reads these environment variables:
VERCEL_OIDC_TOKENAI_GATEWAY_API_KEYAI_GATEWAY_BASE_URL
With the default auth: 'auto', the adapter uses AI Gateway when
AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN is available and otherwise uses
direct authentication.
Applications that resolve credentials at runtime can pass them without
mutating process.env:
const harness = createACP({ // ...implementation settings auth: { AI_GATEWAY_API_KEY: await resolveGatewayToken(), AI_GATEWAY_BASE_URL: 'https://ai-gateway.vercel.sh', },});The supplied record replaces the host environment for authentication
discovery. credentialEnv reads from this record while forwardEnv continues
to read non-authentication runtime configuration from the host. Credential
values are excluded from bootstrap and lifecycle identity.
For direct authentication, configure credentialEnv with the host environment
variable names that the underlying ACP implementation reads, and use
credentialBrokering to map their values to outbound request headers. When the
network sandbox supports additive request transformations, the ACP process sees
the values from sandboxEnv, which are ephemeral placeholders by default. The
real values are injected after requests leave the sandbox, and only when the
request contains the exact expected value. Sandboxes without that capability
retain the legacy behavior of forwarding the real values. Use forwardEnv only
for non-credential runtime configuration.
Gateway environment
Set providerAuthentication.gateway.env to the environment variables that the
underlying ACP implementation uses for its provider endpoint, credentials, and
attribution. Profile values such as gateway-api-key, gateway-base-url,
gateway-authorization, client-app, client-app-name, and
client-app-version are placeholders that the adapter resolves only after
Gateway authentication has been selected. The three client-app placeholders
resolve to the combined name/version identifier, its name, and its version,
respectively. Structured launch environment values are serialized as JSON after
their placeholders are resolved. The resolved Gateway environment overrides
same-name direct values in the environment passed to credentialBrokering, so
one callback works for both authentication modes.
Sandbox
Every ACP harness requires a network sandbox with at least one exposed port,
e.g. @ai-sdk/sandbox-vercel:
const sandbox = createVercelSandbox({ runtime: 'node24', ports: [4000],});By default, the adapter uses the first exposed port. Set port on createACP
to select another exposed port. If neither is available, session startup throws
HarnessCapabilityUnsupportedError with instructions to configure one.
Bridge packages, ACP installations, and replay state live in adapter-owned
directories outside the session project. Skills are written to the configured
native directory below the implementation's $HOME. The session workspace
starts empty.
Tools and Approvals
Optional builtinTools definitions classify native ACP tools only when the
agent supplies a programmatic name matching the tool key or nativeName.
Unmatched native and third-party MCP calls remain valid provider-executed
dynamic tools.
Host-defined AI SDK tools are exposed through a harness-owned MCP server and
execute in the host. Their recursive JSON Schemas are preserved. A tool without
execute pauses for a caller-supplied result through continueStream.
Map all three Harness permission modes when the ACP implementation exposes
native permission controls, and set unsupported modes to null. Selecting a
null mapping throws HarnessCapabilityUnsupportedError. The adapter validates
concrete modes or config options against the session response. When a mapping is
configured, any ACP permission request that still occurs becomes a Harness
approval request. Without a mapping, the adapter automatically selects
allow_once for tool kinds allowed by permissionMode and requests Harness
approval for the rest. A rejection selects reject_once; persistent choices
are never inferred. Host-tool approval remains independent.
Complete ACP Harness Implementations
Unless noted otherwise, these profiles support direct authentication through
the runtime-specific environment variables shown below and AI Gateway
authentication through AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN.
Claude Code ACP
Claude Code accepts the Anthropic-compatible Gateway root URL without /v1.
This profile uses the runtime's supported launch environment:
import { createACP, type ACPPermissionModeMapping } from '@ai-sdk/harness-acp';import { createCredentialRequestTransformation } from '@ai-sdk/harness/utils';
export const claudeCodeACPHarness = createACP({ harnessId: 'acp-claude-code', // Define the runtime's built-in tool names and input schemas to expose // provider-executed calls as typed HarnessAgent tools. // builtinTools: { ... }, source: { type: 'npm-simple', packageName: '@agentclientprotocol/claude-agent-acp', packageVersion: '0.61.0', }, executable: 'claude-agent-acp', modelMapping: { type: 'session-config-option', path: 'model', }, skillsDirectory: '.claude/skills', credentialEnv: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'], credentialBrokering: ({ env, sandboxEnv }) => { const apiKey = env.ANTHROPIC_API_KEY; const authToken = env.ANTHROPIC_AUTH_TOKEN; const sandboxApiKey = sandboxEnv?.ANTHROPIC_API_KEY; const sandboxAuthToken = sandboxEnv?.ANTHROPIC_AUTH_TOKEN; if (!apiKey && !authToken) return [];
const transformations = []; if (apiKey && sandboxApiKey) { transformations.push( createCredentialRequestTransformation({ matchUrl: env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com', matchHeaders: { 'x-api-key': sandboxApiKey }, transformHeaders: { 'x-api-key': apiKey }, }), ); } if (authToken && sandboxAuthToken) { transformations.push( createCredentialRequestTransformation({ matchUrl: env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com', matchHeaders: { Authorization: `Bearer ${sandboxAuthToken}`, }, transformHeaders: { Authorization: `Bearer ${authToken}` }, }), ); } return transformations; }, env: { IS_SANDBOX: '1', }, instructionMapping: { type: 'session-meta', path: ['systemPrompt', 'append'], }, permissionModeMapping: { 'allow-reads': { type: 'session-mode', modeId: 'default' }, 'allow-edits': { type: 'session-mode', modeId: 'acceptEdits' }, 'allow-all': { type: 'session-mode', modeId: 'bypassPermissions' }, } as const satisfies ACPPermissionModeMapping, providerAuthentication: { gateway: { env: { ANTHROPIC_API_KEY: { $source: 'gateway-api-key' }, ANTHROPIC_AUTH_TOKEN: { $source: 'gateway-api-key' }, ANTHROPIC_BASE_URL: { $source: 'gateway-base-url' }, CLAUDE_AGENT_SDK_CLIENT_APP: { $source: 'client-app' }, }, }, },});Without Gateway credentials, this profile uses direct
ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN authentication. The
Gateway environment supplies versioned attribution through Claude's supported
CLAUDE_AGENT_SDK_CLIENT_APP setting. IS_SANDBOX allows Claude Code ACP to
advertise bypassPermissions when the sandbox process runs as root.
Codex ACP
Codex ACP accepts an API key and JSON session configuration through its launch
environment. Its OpenAI-compatible endpoint must end in /v1:
import { createACP, type ACPPermissionModeMapping } from '@ai-sdk/harness-acp';import { createCredentialRequestTransformation } from '@ai-sdk/harness/utils';import { secureJsonParse } from '@ai-sdk/provider-utils';
export const codexACPHarness = createACP({ harnessId: 'acp-codex', // Define the runtime's built-in tool names and input schemas to expose // provider-executed calls as typed HarnessAgent tools. // builtinTools: { ... }, source: { type: 'npm-simple', packageName: '@agentclientprotocol/codex-acp', packageVersion: '1.1.4', }, executable: 'codex-acp', modelMapping: { type: 'session-config-option', path: 'model', }, forwardEnv: ['CODEX_CONFIG'], credentialEnv: ['CODEX_API_KEY', 'OPENAI_API_KEY'], credentialBrokering: ({ env, sandboxEnv }) => { const environmentVariableName = env.CODEX_API_KEY ? 'CODEX_API_KEY' : 'OPENAI_API_KEY'; const credential = env[environmentVariableName]; const sandboxCredential = sandboxEnv?.[environmentVariableName]; if (!credential || !sandboxCredential) return []; const config = env.CODEX_CONFIG == null ? undefined : (secureJsonParse(env.CODEX_CONFIG) as { model_provider?: string; model_providers?: Record<string, { base_url?: string }>; }); const baseUrl = config?.model_providers?.[config.model_provider ?? '']?.base_url ?? 'https://api.openai.com/v1'; return [ createCredentialRequestTransformation({ matchUrl: baseUrl, matchHeaders: { Authorization: `Bearer ${sandboxCredential}`, }, transformHeaders: { Authorization: `Bearer ${credential}` }, }), ]; }, instructionMapping: { type: 'launch-env-json', variable: 'CODEX_CONFIG', path: ['developer_instructions'], }, permissionModeMapping: { 'allow-reads': null, 'allow-edits': null, 'allow-all': { type: 'session-mode', modeId: 'agent-full-access' }, } as const satisfies ACPPermissionModeMapping, authentication: { methodId: 'api-key', }, providerAuthentication: { gateway: { env: { CODEX_API_KEY: { $source: 'gateway-api-key' }, CODEX_CONFIG: { model: 'openai/gpt-5.6-sol', model_provider: 'ai_gateway', model_providers: { ai_gateway: { name: 'AI Gateway', base_url: { $source: 'gateway-base-url', ensureSuffix: '/v1', }, env_key: 'CODEX_API_KEY', wire_api: 'responses', supports_websockets: false, http_headers: { 'User-Agent': { $source: 'client-app' }, 'x-client-app': { $source: 'client-app' }, }, }, }, model_supports_reasoning_summaries: true, preferred_auth_method: 'apikey', }, }, }, },});Codex ACP supports only permissionMode: 'allow-all'. Its restrictive session
modes enable Codex's internal sandbox, which must not run inside the sandbox
already provided to HarnessAgent.
The client-app placeholder resolves to the versioned
ai-sdk/harness-acp/<version> identifier and sends it as both User-Agent and
x-client-app.
Without Gateway credentials, this profile uses direct CODEX_API_KEY or
OPENAI_API_KEY authentication. Gateway env values are added to the
ACP process only when Gateway is selected, so direct runtime configuration
remains unchanged.
Cursor ACP
Cursor exposes ACP through agent acp and installs its CLI with a Bash
installer rather than an NPM package:
import { createACP } from '@ai-sdk/harness-acp';
export const cursorACPHarness = createACP({ harnessId: 'cursor-acp', source: { type: 'install-command', command: 'curl https://cursor.com/install -fsS | bash', }, executable: 'agent', args: ['--disable-auto-update', 'acp'], modelMapping: { type: 'session-config-option', path: 'model', }, clientCapabilities: { _meta: { parameterizedModelPicker: true }, }, credentialEnv: ['CURSOR_API_KEY'], credentialBrokering: ({ env, sandboxEnv }) => { if (!env.CURSOR_API_KEY || !sandboxEnv?.CURSOR_API_KEY) return []; return [ { match: { host: 'api2.cursor.sh', path: { exact: '/auth/exchange_user_api_key' }, method: ['POST'], headers: [ { key: { exact: 'Authorization' }, value: { exact: `Bearer ${sandboxEnv.CURSOR_API_KEY}` }, }, ], }, transform: { headers: { Authorization: `Bearer ${env.CURSOR_API_KEY}`, }, }, }, ]; },});The update flag prevents the cached installation from changing itself at runtime. Credential brokering is limited to Cursor's initial API-key exchange, so subsequent requests keep the short-lived Cursor access token returned by that exchange.
Cursor supports AI Gateway. Configure Cursor's OpenAI API key with an AI
Gateway credential and set Override OpenAI Base URL to
https://ai-gateway.vercel.sh/cursor/v1. The regular Cursor CLI continues to
authenticate to Cursor with CURSOR_API_KEY; that login is separate from the
model provider and routing configured in the Cursor account.
Grok Build ACP
Grok Build exposes ACP directly through grok agent stdio. It does not
advertise ACP session modes for its permission behavior, so this profile omits
permissionModeMapping:
import { createACP } from '@ai-sdk/harness-acp';import { createCredentialRequestTransformation } from '@ai-sdk/harness/utils';
export const grokBuildACPHarness = createACP({ harnessId: 'acp-grok-build', // Define the runtime's built-in tool names and input schemas to expose // provider-executed calls as typed HarnessAgent tools. // builtinTools: { ... }, source: { type: 'npm-simple', packageName: '@xai-official/grok', packageVersion: '0.2.111', }, executable: 'grok', args: ['agent', 'stdio'], modelMapping: { type: 'session-model', path: 'modelId', }, credentialEnv: ['XAI_API_KEY'], credentialBrokering: ({ env, sandboxEnv }) => { if (!env.XAI_API_KEY || !sandboxEnv?.XAI_API_KEY) return []; return [ createCredentialRequestTransformation({ matchUrl: env.GROK_XAI_API_BASE_URL ?? 'https://api.x.ai/v1', matchHeaders: { Authorization: `Bearer ${sandboxEnv.XAI_API_KEY}`, }, transformHeaders: { Authorization: `Bearer ${env.XAI_API_KEY}`, }, }), ]; }, instructionMapping: { type: 'filesystem', path: '.grok/AGENTS.md', }, providerAuthentication: { gateway: { env: { GROK_CLIENT_NAME: { $source: 'client-app-name' }, GROK_CLIENT_VERSION: { $source: 'client-app-version' }, XAI_API_KEY: { $source: 'gateway-api-key' }, GROK_XAI_API_BASE_URL: { $source: 'gateway-base-url', ensureSuffix: '/v1', }, GROK_MODELS_BASE_URL: { $source: 'gateway-base-url', ensureSuffix: '/v1', }, }, }, },});Without Gateway credentials, this profile uses direct XAI_API_KEY
authentication. The Gateway environment supplies client name and version attribution.
Grok Build handles its built-in safe operations internally. For permission
requests sent through ACP, the adapter applies the configured Harness
permissionMode by tool kind and requests explicit approval for the rest.
Known Limitations
- ACP v1 does not expose model-step boundaries or per-step usage. The adapter infers boundaries and reports unknown per-step usage; terminal prompt usage supplies the turn total.
- Standard ACP v1 has no portable manual compaction or mid-turn steering API.
- ACP v1 has no portable built-in tool filtering API. You can still use
activeToolsandinactiveToolsto filter host-executed tools, but filtering ACP built-ins will throw. - A changed host-tool catalog requires the ACP implementation to refresh its MCP tool list. Implementations that retain stale tools fail explicitly.