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
sessionFailureextension, 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, soretry: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.
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 withnika run --resume <trace> --answer <task>=true(orfalse). 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 thatretry: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.mdwithout/linkpointing 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.execby its program. A harness that asks with a shell string (Codex, Claude Code) or without any command line (Gemini CLI) is admitted only bypermits.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, whateverpermits.execgrants (permits.exec: trueincluded), with the command and Codex’s reason in the question. - An ask for network access is never answered automatically, whatever
permits.execgrants: a fetch, or a command the harness says needs the network (Codex: an http or https URL, a SOCKS target such assocks5Tcp 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 CodedangerouslyDisableSandbox) pauses for you: no grant covers it,permits.exec: trueincluded. - An MCP tool call the harness asks you to approve always pauses for you,
because
permits.toolsnames your workflow’s MCP tools, not the harness’s: no grant covers it,permits.exec: trueandpermits.toolsincluded. 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.fsandpermits.netdo 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.
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 harnessagent: task journals, in order:
- one
permit_checkedwith planeagent, gatesession mode · <mode>and decisionconfine(the measured mode was selected) orunmeasured(the harness’s own mode was only observed), whosewhystates what the harness still does without asking; - one
permit_checkedper ask, with decisionallow,deny(your answer) orescalate(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 (itswhynames the command the widening is for),sandbox · commandfor a Codex command that asks to leave its sandbox (the command and the reason in itswhy, 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 itsworkflow_pausedframe; - one
permit_checkedwith decisiondenyfor each file or terminal method the harness called and Nika refused; - one
agent_image_observedper image the seat sent (see below): the digest and size of the received bytes, the path the harness reported (never opened) and, underimages: { save: }, thesaved_pathNika wrote.
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 anagent: turn.
Declare images: on the task to use them in the workflow:
.outputbecomes{ message, images }.messageholds what.outputwould hold withoutimages:. Each image carriesindex(from 0),path,sha256,size_bytes,mime_type,widthandheight.nika checktypes it that way, so.output.images[0].pathand.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_imagesthat 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; declaresave:. - With
save:, Nika writes the received bytes at that path throughnika:write, insidepermits.fs.write(NIKA-SEC-004outside it). A second image lands atbanner-2.png, a third atbanner-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 checkjudges all four for a literal path (grant the directory, or the four files--infer-permitslists), refuses a computedsave:path whenpermits.fs.writegrants no path at all, and the run judges the received ones before the first write. A task whose session delivered no image failsNIKA-AGENT-007instead of succeeding with no file.nika servelists 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_eachtask that saves one path, or a save beside anika:writeof one of its files are refused (NIKA-SEC-012). Order them withafter:or give each its own path. - Nika recognises the image tool of Codex. On another application,
images: { save: }receives nothing:nika checkdoes not block it, and the run failsNIKA-AGENT-007. images:belongs to a task on a seat. On an API or local model,nika checknames it as a blocker and the run refuses it: there,nika:image_generatemakes the images.
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 failsNIKA-INFER-002. Each repair is one more prompt on your subscription.vision:files, sent as ACP image blocks when the application declares image prompts atinitialize(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, avision:file is a read insidepermits.fs.read:nika checkjudges a literal path, and the run judges every path on its resolved form before the session starts (NIKA-SEC-004outside the grant,NIKA-AUTH-006at check with nopermits:block). A granted file that cannot be read fails with the read’s own code (NIKA-BUILTIN-READ-001when no file is there,NIKA-BUILTIN-READ-002otherwise) (Verbs).
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:
--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:
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 saysaccess: 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.