> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Bring a custom harness

> The ACP stdio contract a custom harness implements, walked section by section against the dependency-free reference server.

Any container image that speaks the documented ACP stdio contract can register as a [HarnessRelease](/concepts/harnesses-and-models), pass conformance, and run Sessions — no Checkfu code review, no privileged path. This guide teaches that contract by walking a complete, runnable implementation: `examples/custom-acp/server.mjs` in the Checkfu repository, an ACP v1 server in about three hundred lines of plain Node with no dependencies at all.

Two companion pages carry the normative truth this guide deliberately does not restate. [Harness wire extensions](/concepts/harness-extensions) is the contract page — its capability matrix and slot inventory are machine-generated from the same code the platform runs, so they cannot drift. The [`acp-v1` extension schema](/harness-acp-v1-extension.schema.json) and [golden wire vectors](/harness-acp-v1-extension-vectors.json) are the machine-readable wire artifacts. When this guide and those artifacts ever disagree, the artifacts win.

Every `server.mjs` block below is excerpted from that file, not retyped, and the repository's documentation checks fail if one drifts from the source. The CLI invocations later on are ordinary examples, not quotes — check them against `checkfu harness --help` if a flag ever looks wrong.

## The wire: JSON-RPC over stdio

A harness is a process. The Runner launches it inside the sandbox, writes newline-delimited JSON-RPC 2.0 to its stdin, and reads the same framing from its stdout. Stderr is yours for logging. The entire transport layer of the reference server is one line:

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`)
```

and one read loop, with a drain rule worth copying: when stdin closes, finish the requests already in flight, flush stdout, and exit zero. The platform reads settlement from the durable event log, not from your process staying alive.

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
const input = createInterface({ input: process.stdin })
input.on("line", (line) => {
	if (line.trim().length === 0) return
	const message = JSON.parse(line)
	activeRequests += 1
	void handle(message).then(
		(outcome) => completeRequest(message, outcome === TURN_FAILED),
		() => completeRequest(message, true),
	)
})
input.on("close", () => {
	inputClosed = true
	exitAfterInput()
})
```

## `initialize`: declare only what you serve

The first request is `initialize`. The response names your protocol version and capabilities:

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
	if (message.method === "initialize") {
		send({
			jsonrpc: "2.0",
			id: message.id,
			result: {
				protocolVersion: 1,
				agentCapabilities: { loadSession: false, mcpCapabilities: { http: true } },
				authMethods: [],
			},
		})
		return TURN_HANDLED
	}
```

`mcpCapabilities: { http: true }` is load-bearing: it tells the platform this harness can accept HTTP MCP servers in `session/new`, which is how the platform tool plane arrives (below). Richer harnesses additionally declare Checkfu's namespaced extensions — native steering, cooperative interrupt, observed subagents — in the `initialize` response `_meta`; the exact admission rules are in [the declaration handshake](/concepts/harness-extensions#the-declaration-handshake).

Declare honestly, and only what you serve. A declaration is never evidence: conformance actively exercises every declared surface, and only server-issued, signed conformance evidence realizes a capability. Declaring an axis you do not serve is worse than not declaring it — the platform will drive the declared surface and the failure modes are honest ones, up to failing the Run.

## Session and turn lifecycle

`session/new` opens a session and hands over the MCP server list; the reference server keeps it and answers with a session id:

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
	if (message.method === "session/new") {
		sessionMcpServers = message.params.mcpServers ?? []
		send({ jsonrpc: "2.0", id: message.id, result: { sessionId: "custom-acp-session" } })
		return TURN_HANDLED
	}
```

`session/prompt` is a turn. While it runs, the harness streams `session/update` notifications — agent message chunks, tool calls, tool results — and settles the turn by answering the request with a stop reason:

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
		send({
			jsonrpc: "2.0",
			method: "session/update",
			params: {
				sessionId: message.params.sessionId,
				update: {
					sessionUpdate: "agent_message_chunk",
					messageId: "custom-acp-message",
					content: { type: "text", text: JSON.stringify(evidence) },
				},
			},
		})
		send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } })
```

The conformance grammar holds every harness to the same shape — exactly one ready handshake before any output, exactly one terminal observation per turn, unique correlated tool-use ids. The full rule table is in [the conformance grammar](/concepts/harness-extensions#the-conformance-grammar).

## The model plane arrives in the environment

A harness never holds provider credentials. The sandbox environment carries `CHECKFU_MODEL_BASE_URL` and `CHECKFU_MODEL_TOKEN` — the platform-neutral names, and the ones to prefer in new code — pointing at the governed model gateway. The same values are also exported under the vendor-shaped aliases `OPENAI_BASE_URL`/`OPENAI_API_KEY` and `ANTHROPIC_BASE_URL`/`ANTHROPIC_API_KEY`, so a harness built against an existing SDK works unmodified; note that `ANTHROPIC_BASE_URL` is the origin without the `/v1` suffix its OpenAI-shaped sibling carries. Routing, budget, and usage accounting stay with the platform, and the model name you send is rewritten to the admitted route. The reference server reads the OpenAI alias:

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
		const prompt = promptText(message.params)
		const modelBaseUrl = process.env.OPENAI_BASE_URL ?? ""
		const modelToken = process.env.OPENAI_API_KEY ?? ""
		const modelResponse = await fetch(`${modelBaseUrl}/responses`, {
			method: "POST",
			headers: {
				accept: "application/json",
				authorization: `Bearer ${modelToken}`,
				"content-type": "application/json",
			},
			body: JSON.stringify({ model: "harness-requested-model", input: "relay probe" }),
		})
		if (!modelResponse.ok) {
			await modelResponse.body?.cancel()
			return TURN_FAILED
		}
```

Bake no credentials into the image, and fail closed when the model gateway refuses a model call. The model gateway forwards only the request paths of the wire dialects your image declares — the reference server also probes a non-model path and expects rejection. Which dialects reach which providers is generated data in [harness and model-provider reachability](/concepts/harness-extensions#which-harnesses-can-reach-which-model-providers).

## The platform tool plane: MCP handoff

Granted platform tools arrive as ordinary HTTP MCP servers in `session/new` — the handoff your `initialize` response opted into. The reference server looks up the control server by name when its turn needs a governed capability:

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
			const controlServer = sessionMcpServers.find((server) => server.name === "checkfu-control")
			if (controlServer === undefined) return TURN_FAILED
```

and then speaks stock MCP to it — initialize, `tools/list`, `tools/call`:

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
	const called = await mcpRequest(server, "control-call", "tools/call", {
		name: CONTROL_TOOL_NAME,
		arguments: { name: "external_lookup", arguments: { query: "canonical" } },
	})
```

While a tool runs, emit `tool_call` and `tool_call_update` session updates so the activity is observed rather than narrated — the platform's own served-call record, not your narration, is the authority on whether tool delivery happened.

## The image contract

Your harness runs under a container posture the **sandbox** imposes, not one your image chooses: the process runs as user `65532:65532`, the root filesystem is read-only, and exactly `/workspace`, `/tmp`, and `/home/checkfu` are writable. Build for it — a harness that expects to write elsewhere, or to be root, fails at runtime rather than at registration.

Be precise about what enforces this. The sandbox provider applies the posture at materialization; the values above are the same ones the catalog's image contract records for first-party entries, and a custom image registered through `harness add oci` has no catalog entry at all. Conformance does not check container posture — its case set covers the ACP handshake, tool correlation, and the declared control axes, and contains no uid, read-only-root, or writable-path case. So the reference server does what any harness author should do while developing: it proves each property from inside and reports it as ordinary evidence, which is a self-report, not platform attestation.

```js path=examples/custom-acp/server.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
			uid: process.getuid?.(),
			gid: process.getgid?.(),
			workspace: writableProof("/workspace/proof"),
			workspace_symlink_target: workspaceSymlinkProof(),
			tmp: writableProof("/tmp/proof"),
			home: writableProof("/home/checkfu/proof"),
			root_read_only: mountIsReadOnly("/"),
			capabilities_dropped: /^0+$/.test(processStatus("CapEff")),
			no_new_privileges: processStatus("NoNewPrivs") === "1",
```

Non-secret configuration reaches the harness as visible JSON in `CHECKFU_HARNESS_CONFIGURATION`, validated against the configuration schema registered with the release. Credentials never belong there — provider access rides the model plane above, and tool authority rides the MCP handoff.

## Register, verify, run

Registration is the same public arc for every custom harness: build and push an immutable image, adopt the digest as a HarnessProfile, request server-signed conformance, then create and publish an Agent on the verified profile.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
checkfu harness test oci \
  --name my-harness \
  --image ghcr.io/acme/my-harness@sha256:<64-hex-digest> \
  --entrypoint /opt/acme/bin/my-harness \
  --protocol acp-v1 \
  --working-directory /workspace

checkfu harness add oci \
  --name my-harness \
  --image ghcr.io/acme/my-harness@sha256:<64-hex-digest> \
  --entrypoint /opt/acme/bin/my-harness \
  --working-directory /workspace \
  --platform linux/amd64

checkfu harness verify my-harness --sandbox standard --model primary --wait
```

Only the digest-pinned `name@sha256:...` form registers; a mutable tag fails before anything persists, and a new image is always a new release. The walked five-step transcript — including the Agent creation, PermissionAssignments, and Session chat — is the [Custom ACP harness example](/examples/custom-acp).

### Upgrade and roll back one logical harness

Keep the HarnessProfile name stable after initial adoption. Test and publish a
new immutable digest, then create the release, advance the profile with
optimistic concurrency, and verify the exact resulting version in one CLI
workflow:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
checkfu harness update my-harness oci \
  --image ghcr.io/acme/my-harness@sha256:<new-64-hex-digest> \
  --entrypoint /opt/acme/bin/my-harness \
  --working-directory /workspace \
  --platform linux/amd64 \
  --expected-version 1 \
  --verify \
  --sandbox standard \
  --model primary
```

`--expected-version` is checked before the CLI creates a release, then the API
guards the patch with the same current version. The command prints a recovery
command as soon as an OCI release exists, so a failed or interrupted profile
patch never strands an undiscoverable artifact. After a successful patch it
prints the exact rollback command. Rollback is an ordinary forward version:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
checkfu harness update my-harness release \
  --release hrel_<previous-release-id> \
  --driver checkfu:acp-v1@1 \
  --expected-version 2 \
  --verify \
  --sandbox standard \
  --model primary
```

The earlier HarnessRelease and every HarnessProfileRevision remain immutable.
An in-flight Run keeps its resolved pins; later admission resolves the new
profile version. Moving any release, driver, or configuration member
invalidates the old conformance tier, which is why `--verify` targets the
returned profile version directly instead of resolving the logical name a
second time. Non-secret configuration is retained unless `--configuration`
is supplied, and the CLI validates the resulting configuration against the
target release before patching.

For an OCI-to-OCI update, omitted process, platform, configuration-schema,
model-wire, and MCP options inherit from the current immutable release. This
keeps an image-only rebuild exact without silently dropping a declared
capability; pass an option only when the new binary intentionally changes that
part of the release contract.

### Or hand us the repository instead

If you would rather not run a container build at all, `POST /v1/organizations/{organization_id}/harness-builds` takes a GitHub repository URL, an optional ref, and an explicit platform build pool or Workspace/RunnerPool target. Checkfu builds the image and mints the Organization-owned, digest-pinned release for you. Ownership never implies execution authority. The ref is resolved to an exact commit at submission and only that commit is stored, so the immutability rule above is unchanged rather than relaxed: no branch or tag is ever stored on the result, and re-registering a repository is always a new build. Submission is accepted with `202`; poll `GET /v1/organizations/{organization_id}/harness-builds/{id}` for settlement.

The CLI drives that same public resource. It can stop at the ordinary
release-profile handoff, or compose adoption and signed verification without
introducing a privileged build-only profile path:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
checkfu harness build github \
  --repository https://github.com/acme/my-harness \
	--ref refs/heads/main \
	--runner-pool rpool_<runner-pool-id> \
  --entrypoint /opt/acme/bin/my-harness \
  --arg=--stdio \
	--platform linux/amd64 \
	--profile-name my-harness \
	--adopt \
	--verify \
	--sandbox standard \
	--model primary

# If the waiting process exits, resume without resubmitting the build.
checkfu harness build status hbld_<build-id>
```

Without `--adopt`, success prints `checkfu harness add release --name ... --release hrel_... --driver checkfu:acp-v1@1`. With `--adopt`, the CLI submits
that same public profile-creation payload; `--verify` additionally waits for
signed conformance and prints status. Building an image grants no conformance
or admission privilege.

For an existing profile, leave `--adopt` off and pass the resulting release to
`checkfu harness update <profile> release --release <hrel_...>` instead. OCI
releases select the canonical protocol driver automatically; a non-OCI release
requires the explicit `--driver` value. The update path then follows the same
fresh-verification rule above.

<Warning>
  This path is `alpha` and has real edges worth knowing before you depend on it. A build settles only where a build-capable Runner is available to your Workspace — with none, the build stays **pending** rather than failing, and nothing is minted. A private repository needs the Checkfu GitHub App installed and bound to that exact Workspace and repository; without it, submission fails without distinguishing private from missing. And a build whose image digest was already registered reuses that existing release, which carries no build provenance — a success guarantees you a release, not a stamped one.
</Warning>

## Where to go next

* **To understand the boundary**, stay with `examples/custom-acp` — minimal on purpose, every line of the contract visible.
* **To adapt an SDK-authored agent**, use the [OpenAI Agents SDK over ACP example](/examples/openai-agents-acp) — it pins the official SDK, routes its fixed model through Checkfu's gateway, disables the SDK's separate trace exporter, and declares no MCP handoff.
* **To build a real harness inside an authorized Checkfu source checkout**, start from `apps/harness` — pi's agent loop behind the same ACP server contract, with durable turn journals, checkpoints, completion evidence, and the extension declarations this page only gestures at. Its README carries the same export → modify → `checkfu harness add oci` → `checkfu harness verify` arc. The package and standalone export are private qualification artifacts today; no public repository or npm package is claimed.
* **For the contract itself**, [Harness wire extensions](/concepts/harness-extensions) and the schema and vector artifacts remain the normative source.
