Skip to main content
Experimental. Engine ships the adapters in the default CLI build (default = ["access-harness"] in nika-cli at v0.123.0). You still need the harness installed and signed in, and a model: that seat can serve. nika doctor listing a CLI is detection, not a completed generation. Retired ACP wrapper ids (claude-agent-acp · codex-acp) are not seats and refuse with NIKA-1802.
model: picks the intelligence. Access picks the path the run takes to reach it — an API key, a local server, the mock, or a harness you already pay for. The same infer: / agent: task is admitted onto that path with a witness. Unsatisfied pins refuse (NIKA-1801 · NIKA-1803); they never silently fall back to a metered key.

What you need

The harness, signed in the way IT expects. Nika never holds its credential: a harness seat runs on the account signed into that application, so check that provider’s terms before running one on a shared server. nika doctor prints one runtime row per seat (detected, signed in or not) and the fix for each one that is not ready. The seats in : Every seat runs agent: tasks over the Agent Client Protocol. An infer: task needs a one-shot, tool-free answer, which only the four seats marked yes are admitted for. With --access <seat>, every model in the file goes to that seat, and only the run proves the seat can serve it. Classes also pin: local · mock · harness · oauth · api.

Check, then run

nika check --access judges the same access plan the run uses, so a seat that is missing, signed out or unfit for an infer: task is named before anything runs. On every agent: task a seat will run, nika check also prints a [harness] hint: what permits: does not cover on that seat (nika explain harness).

What permits: covers on a harness seat

On a harness seat, an agent: task runs the application’s own tools: its file edits, its shell, its fetches. Nika does not execute them, so it cannot judge each one the way it judges an exec: task or a nika:* builtin. It holds the session with two levers. Before the prompt (journaled as permit_checked, plane agent):
  • The session works in the project the run was admitted for, never in the directory the Nika process runs from.
  • Nika selects the harness’s measured asking mode and reads it back from the harness’s answer (Gemini CLI only acknowledges the switch), or refuses to start. It never selects a mode that approves actions on its own (an auto-reviewer, accept-edits, full access, bypass, yolo). A mode change the harness reports during the turn ends the session; Gemini CLI reports one only as message text, which Nika does not read.
  • Nika offers the harness no file or terminal methods. A harness that calls one anyway gets an error, never a fake success, and the refusal is journaled.
  • Nika asks the harness to report a turn it could not complete as a failure (the sessionFailure extension, which codex-acp and claude-agent-acp implement). A rejected model or an exhausted retry limit then fails the task with the harness’s own error, instead of succeeding with that error as its answer: transient (NIKA-1804, so retry: may replay the task) when the harness offers a retry, an expired sign-in when it asks you to sign in again, a refusal (NIKA-1805) otherwise. A harness without that report still returns its error as text, and Nika does not read answer text to guess a failure.
On every permission the harness asks (journaled as permit_checked):
  • Inside your permits: grants, the ask is allowed once, never “allow always”.
  • Outside them, the run pauses with the harness’s question, followed by the command it names (unless its title shows it) and the reason it gives (NIKA-1806). The question is one line the harness cannot steer: control characters become spaces, invisible ones (direction overrides, zero-width characters) are removed, and a title or command over 2,000 bytes keeps its beginning and end around the number of bytes left out and their SHA-256. Answer with nika run --resume <trace> --answer <task>=true (or false). The harness is shut down at the pause; the resumed run starts the task again in a new session, and your answer decides its first ask outside the grants, once for the task: an attempt that retry: replays never reuses it, so an ask outside the grants there pauses again.
  • Paths are judged in your project’s terms (./out/** matches the absolute path the harness names) and where the operating system will land: each path is walked from the root, component by component, through every symlink (a dangling one included) and every ... out/link/../notes.md with out/link pointing outside the project is judged outside, beside the link’s target. A path that cannot be resolved (a symlink loop, an unreadable directory) pauses for you.
  • A command is matched against permits.exec by its program. A harness that asks with a shell string (Codex, Claude Code) or without any command line (Gemini CLI) is admitted only by permits.exec: true; otherwise the ask pauses for you.
  • In read-only, Codex runs ordinary commands in its sandbox without asking, so a command it asks about asks to leave that sandbox, and Codex would run it with no sandbox at all. Such an ask always pauses for you, whatever permits.exec grants (permits.exec: true included), with the command and Codex’s reason in the question.
  • An ask for network access is never answered automatically, whatever permits.exec grants: a fetch, or a command the harness says needs the network (Codex: an http or https URL, a SOCKS target such as socks5Tcp network access to <host>, or a network permission). It pauses for you with the target in view.
  • A command that asks the harness to widen its own sandbox (Codex additionalPermissions, Claude Code dangerouslyDisableSandbox) pauses for you: no grant covers it, permits.exec: true included.
  • An MCP tool call the harness asks you to approve always pauses for you, because permits.tools names your workflow’s MCP tools, not the harness’s: no grant covers it, permits.exec: true and permits.tools included. Codex asks it as a command with no command line; Claude Code and Gemini CLI ask theirs as another kind of ask, which pauses too.

What permits: does not cover

What the selected mode lets the harness do without asking never reaches Nika, so permits: does not bound it: On every seat:
  • An action Nika allows runs under the harness’s own sandbox, not Nika’s exec sandbox, so permits.fs and permits.net do not confine it. Nika judges the network access a harness asks for, not the connections an allowed command opens by itself: Codex keeps its sandboxed commands offline unless they ask, while a command Claude Code or Gemini CLI runs has the network the machine has.
  • The harness’s own configuration still applies: its permission rules, its hooks and the MCP servers it configures act without Nika’s judgment unless they ask.
If a task must not write anything, or must not read outside the project, do not rely on permits: alone on a harness seat. Run it on an API or local model, where Nika’s own agent loop judges every tool call, or in a container or a throwaway checkout.

Reading the trace

Each harness agent: task journals, in order:
  • one permit_checked with plane agent, gate session mode · <mode> and decision confine (the measured mode was selected) or unmeasured (the harness’s own mode was only observed), whose why states what the harness still does without asking;
  • one permit_checked per ask, with decision allow, deny (your answer) or escalate (paused for you). Its gate names what was judged: mcp · <server or tool call> for an MCP tool approval, network · <target> for a network ask, sandbox · <widening> for a sandbox widening (its why names the command the widening is for), sandbox · command for a Codex command that asks to leave its sandbox (the command and the reason in its why, whole up to 16 KiB, then its beginning and end around the bytes left out and their SHA-256), otherwise the ask’s kind with its program or paths (an unresolvable path reads <unresolved>). A run paused on that question still journals the task’s decisions, just before its workflow_paused frame;
  • one permit_checked with decision deny for each file or terminal method the harness called and Nika refused;
  • one agent_image_observed per image the seat sent (see below): the digest and size of the received bytes, the path the harness reported (never opened) and, under images: { save: }, the saved_path Nika wrote.
A turn the harness reports as failed ends the task with task_failed and its code (NIKA-1804 or NIKA-1805), never a task_completed whose output is the error text.

Images and structured answers on a seat

An application such as Codex can generate images during an agent: turn. Declare images: on the task to use them in the workflow:
  • .output becomes { message, images }. message holds what .output would hold without images:. Each image carries index (from 0), path, sha256, size_bytes, mime_type, width and height. nika check types it that way, so .output.images[0].path and .output.message.<key> resolve, and a schema key read at the top is refused.
  • Only the bytes the application sent on this task’s own session count. The path it reports for its own copy (Codex: a folder under ~/.codex/generated_images that every session on the machine shares) is never opened, so a task cannot pick up another session’s image. Do not ask the application to copy its image into the project; declare save:.
  • With save:, Nika writes the received bytes at that path through nika:write, inside permits.fs.write (NIKA-SEC-004 outside it). A second image lands at banner-2.png, a third at banner-3.png, up to four: only images whose bytes arrived count, and a save that received more than four is refused before any file is written. Every path is judged before any file is written: nika check judges all four for a literal path (grant the directory, or the four files --infer-permits lists), refuses a computed save: path when permits.fs.write grants no path at all, and the run judges the received ones before the first write. A task whose session delivered no image fails NIKA-AGENT-007 instead of succeeding with no file. nika serve lists the saved file among the run’s artifacts with the received digest.
  • A save is a write: two tasks that can run at the same time and save on one path, a for_each task that saves one path, or a save beside a nika:write of one of its files are refused (NIKA-SEC-012). Order them with after: or give each its own path.
  • Nika recognises the image tool of Codex. On another application, images: { save: } receives nothing: nika check does not block it, and the run fails NIKA-AGENT-007.
  • images: belongs to a task on a seat. On an API or local model, nika check names it as a blocker and the run refuses it: there, nika:image_generate makes the images.
A schema: on a seated agent: task is validated by Nika on the final message, and the validated object becomes the output (message under images:). There is no repair turn on a seat, because a new session would replay the agent’s work: a final message that does not validate fails NIKA-INFER-002. An infer: task over ACP (run.access.protocol: acp) runs one tool-free turn and carries:
  • schema:: the prompt carries the schema and Nika validates the answer. An answer that does not validate is never the output: Nika asks again in a new one-turn session that receives the earlier answer and the errors, up to the repair budget, then fails NIKA-INFER-002. Each repair is one more prompt on your subscription.
  • vision: files, sent as ACP image blocks when the application declares image prompts at initialize (codex-acp and claude-agent-acp do). An application that does not is refused before the session starts, and so is a file whose bytes do not start with a PNG, JPEG, GIF or WebP signature. As on an API route, a vision: file is a read inside permits.fs.read: nika check judges a literal path, and the run judges every path on its resolved form before the session starts (NIKA-SEC-004 outside the grant, NIKA-AUTH-006 at check with no permits: block). A granted file that cannot be read fails with the read’s own code (NIKA-BUILTIN-READ-001 when no file is there, NIKA-BUILTIN-READ-002 otherwise) (Verbs).
Over ACP, a seat cannot carry temperature, max_tokens, an enabled thinking budget or a vision: URL on an infer: task (Nika does not fetch it): nika check names them as blockers instead of reporting the file run-ready, and the run refuses them before anything is sent. On every seat, a tools: whitelist on an agent: task is a blocker too, and check stops suggesting max_tokens or max_tokens_total on a seated task. nika run --dry-run refuses what the run’s seat refuses, with the lines nika check blocks run ready on, and exits 2. Without run.access.protocol: acp, an infer: task on a seat runs as the application’s direct one-shot command (codex exec, claude -p, and the one-shots of Grok Build and GitHub Copilot CLI). That command carries the prompt, the system prompt, the model and a schema: (except on Copilot), never an image, so a task with vision: is refused instead of running without its images: nika check blocks run ready with NIKA-432, and the run refuses the task before the seat starts, with nothing sent. The run still judges and reads the vision: files under permits.fs.read first. The check and the run name a route that carries the files: run.access: { via: <seat>, protocol: acp } when that seat’s ACP one-shot also carries the rest of the task, and an API route first when the task sets what the ACP session refuses (temperature, max_tokens, an enabled thinking budget, a vision: URL), with those fields named:
Captured offline from nika 0.124.1 with --access codex, a scripted codex standing in for the application. That task sets max_tokens, so the API route comes first. Without max_tokens, the same line ends on the ACP route:
The direct route still neither sends nor refuses temperature, max_tokens and thinking, and nika check does not flag them: declare an API route on a task that needs them.

The honest receipt

A harness run’s trace row says access: harness and billing: unknown — because your plan’s quota is the harness’s business until the harness itself reports it. What you will never see is a fabricated $0.00: the ledger records the lane as subscription_quota, priced when evidence exists, absent when it doesn’t.

Kill-switch

One adapter refusing to behave? Take it off the machine without touching the rest:
A disabled adapter is no candidate at all — a pin naming it refuses in plain words.