zirvDocs

DocsSupervision (zirv ctx)

Native runtime

README.md, lines 3469–4528, 5631–6148View source

Runtime backends#

Every supervised session picks a RuntimeKind: harness (the default — an AgentAdapter spawns the real Claude Code/Codex/etc. binary through the supervise::spawn_tapped chokepoint) or native (zirv conducts the model/tool conversation itself over a direct provider route, with no vendor CLI in the loop, on the native-runtime roadmap, issue #469). Native mode is always explicit and opt-in: nothing detects it, and an unrecognised runtime name is an error rather than a silent fallback to harness.

The session registry (sessions::Record), the orchestrator seat (seat::Seat), and the .conversation marker each persist which RuntimeKind they were started under, defaulting to harness so every record written by an older binary still parses. A resume matches agent, session id, and runtime before reusing a recorded conversation, so a session can never silently cross from a harness-backed conversation to a native one (or back).

The in-process wire shapes a RuntimeBackend speaks — commands, events (EventEnvelope's wire keys are version, revision, session_id, generation, event), replies, and a structured ErrorCode (runtime::RuntimeError's Unsupported/UnknownSession/Busy/StaleGeneration map 1:1 to it; any other backend error maps to ErrorCode::Backend) — are versioned from day one as protocol v1 (src/commands/ctx/runtime/protocol.rs), with frozen example payloads under tests/fixtures/runtime/v1/ so a later change can't silently break what v1 meant. No daemon or socket exists yet; everything is in-process.

Native sessions use an authoritative schema-v1 SQLite/WAL journal at <state>/native-journal.sqlite. It records acknowledged inputs, complete typed assistant blocks, exact tool calls, execution-state receipts, usage, task receipts, portable checkpoints, and monotonic sequence/generation IDs. Bounded streaming frames are transient until a completed-message barrier commits them; incomplete tool arguments never enter the executable event log. An execution interrupted after it starts becomes outcome_unknown and must be reconciled rather than blindly replayed. Provider continuation envelopes live outside portable replay and are readable only with the same route, provider, endpoint, account, protocol, vendor, and model identity. The journal projects committed facts into the existing NormalizedEvent scoring vocabulary, so the pure rot engine is unchanged. The storage/recovery contract is documented in docs/design/2026-09-11-native-conversation-journal.md.

The journal does not replace the session registry, seat, task, mailbox, workflow, or policy stores. Those remain the single authorities for their own state and the journal records only their stable identifiers and receipts. Existing harness transcripts are untouched, and older Zirv builds simply ignore the additional private database.

Native effects are admitted through one Zirv-owned execution broker before any tool implementation can touch the machine. The broker reloads the current canonical policy, fences the persisted native seat generation, resolves paths through symlinks/junctions, requires the writer permit for the exact linked worktree, protects Zirv/provider credential paths, and strips credential-bearing environment variables from subprocesses. Interactive approvals are signed and bound to the exact action arguments, canonical path targets, task/session/role/generation, resource claims, and current policy; they cannot be reused after any of those facts changes, and a headless ask is an explicit refusal.

Arbitrary native processes require a verified OS isolation launcher. Linux uses bubblewrap when bwrap is installed, macOS uses the built-in Seatbelt launcher, and Windows requires Zirv's restricted-token/AppContainer helper; a Job Object is cleanup, not containment. A missing mechanism is reported as unsupported and never falls back to an unsandboxed spawn. Consequently Zirv does not advertise native coding support on a host until that host's enforcement probe passes. The contract and current platform evidence are in docs/design/2026-09-11-native-execution-enforcement.md.

The native coding tool service now exposes a closed, schema-described registry for file ranges, directory/glob/text search, atomic writes, exact-content patches, process start/poll/wait/input/termination, and stored output retrieval. JSON is fully decoded into a typed request before the N04 broker sees an action; unknown fields, incomplete payloads, empty handles, and oversized arguments are refused without executing anything. File writes require idempotency keys and existing files require SHA-256 preconditions. Patches additionally require exact occurrence counts. UTF-8 BOM, UTF-16 endianness, existing permissions, and consistent line endings survive an atomic replacement; binary and image reads report their media type and hash instead of corrupting bytes through a text decoder.

Processes use explicit argv by default. Shell mode is a separate shell_script shape and receives conservative git/destructive effects. Requested network, outside-write, and git-metadata access can only add broker checks and sandbox bindings; omitting them leaves those effects unavailable. Non-interactive commands use pipes, interactive commands alone use PTY/ConPTY, and every live command has an opaque handle for bounded polling, waiting, input, and process-tree termination. Raw stdout/stderr streams once into the existing output store, so large and non-UTF-8 failures return a bounded summary plus output_id rather than filling model context. Process handles are intentionally machine-process-local and are not claimed to survive a runtime crash. N03 journal states make an interrupted effect outcome_unknown; receipts mark each tool safe, reconcile, or never_after_start, preventing blind replay of remote mutations. The exact contract is documented in docs/design/2026-09-11-native-coding-tools.md.

Native context is compiled independently from provider delivery. Zirv emits typed instruction and data messages with source/trust provenance, a stable methodology prefix, an output-token reservation, and explicit records for every included, truncated, referenced, or excluded source. Operator and Zirv methodology are instructions; repository prompts, canonical context, shared memory, and repository skills remain untrusted data and cannot grant authority. Only the active workflow step's skills and bounded relevant memory are selected. Required task, workflow, and evidence handles either fit or the compile fails—optional prose can never silently crowd them out.

The native registry also exposes memory_recall, memory_remember, memory_forget, and zero-model context_search. They reuse the existing locked memory/search stores and opaque output_read evidence handles; shared memory writes still cross the repository policy and writer-lease boundary. No Claude/Codex prompt file, helper process, or AgentAdapter participates in native compilation. The contract is documented in docs/design/2026-09-12-native-context-compiler.md.

The first direct-model adapter speaks Anthropic's Messages API over raw HTTPS/SSE behind a provider-neutral transport contract. It sends only Zirv's compiled system/data messages and public tool schemas, reassembles interleaved text, tool-use, thinking signatures, and redacted-thinking blocks, and keeps all tool execution in Zirv. Exact model IDs and model-specific thinking/effort controls are checked locally; authentication, entitlement, model access, context limits, rate limits, overloads, timeouts, cancellation, refusals, and usage/cache classes remain typed. Opaque continuation material is replayed verbatim but excluded from diagnostics. Versioned fixtures and an ignored, credential-gated live contract test (cargo test --ignored live_anthropic_messages_contract, needing ANTHROPIC_API_KEY and ZIRV_ANTHROPIC_LIVE_MODEL — an exact, entitled model id) cover the provider boundary; N09 owns wiring this adapter into the durable agent loop. The transport contract is in docs/design/2026-09-12-native-anthropic-provider.md.

The second direct-model adapter speaks OpenAI's Responses API the same way -- raw HTTPS/SSE to POST /v1/responses, with no Codex binary, Codex SDK, or Codex App Server in the path. Zirv owns the conversation: every request sends store: false with the full local history, and previous_response_id is never used as durable state. Reasoning items keep their id and encrypted content verbatim for replay and are refused when they reach a provider that cannot carry them; function calls commit only from a completed output item whose arguments match the streamed ones, an incomplete response drops every function call and records why, and a stream without a terminal event is a typed error rather than a completion. Only a Platform API key is accepted: a ChatGPT/Codex subscription login is refused with an entitlement error instead of being billed as API usage. Controls are validated against exact API model ids -- a Codex harness model id is not assumed to be a Responses model. The routes are fixture-verified, with live validation available through an ignored, credential-gated test (cargo test --ignored live_openai_responses_contract, needing OPENAI_API_KEY and ZIRV_OPENAI_LIVE_MODEL); the contract is in docs/design/2026-09-12-native-openai-provider.md.

The third direct-model adapter speaks Google's Gemini generateContent/ streamGenerateContent API over the same raw HTTPS/SSE transport, behind two explicitly versioned protocol profiles: the Gemini Developer API (API key, generativelanguage.googleapis.com, v1beta, where thinking and function-calling controls live) and Vertex AI (an OAuth access-token credential plus an explicit project and location, addressed at .../v1/projects/{project}/locations/{location}/publishers/google/models/ {model}). Token acquisition for the Vertex profile is a separate credential-class concern from payload transformation. Parallel function calls and thoughtSignature continuation metadata -- including signature-only reasoning with no visible thought text -- are preserved verbatim as opaque data and rejected before transport if they carry another provider's shape. Safety blocks (SAFETY/RECITATION/PROHIBITED_CONTENT/...) commit as a typed refusal outcome, never prose; quota errors, invalid project/location, authentication, model access, transport and context-overflow failures map onto the same typed failure classes the other two providers use. A Gemini CLI OAuth login (~/.gemini) is refused outright, by path and by credential shape, with an actionable Entitlement failure -- the same posture as OpenAI's subscription refusal. The Vertex profile is declared but only just promoted from Support::Planned to Support::Native in N02's capability table; both routes are fixture-verified, with live validation of the Developer API profile available through an ignored, credential-gated test (cargo test --ignored live_google_generative_ai_contract, needing GEMINI_API_KEY and ZIRV_GOOGLE_LIVE_MODEL). The contract, and how to add or retire a protocol profile, is in docs/design/2026-09-13-native-google-provider.md.

The native agent loop#

Coming soon: native execution is unavailable in this release.

zirv ctx exec --runtime native runs a whole session without a coding harness installed:

text
zirv ctx exec --runtime native --prompt "fix the failing test"
zirv ctx exec --runtime native --route work-sonnet --role worker -- fix the failing test
zirv ctx exec --runtime native --resume 9d2f… --prompt "now update the docs"

Flags: --runtime harness|native (default configured — the operator's own ~/.zirv/ctx.toml [runtime] table decides; unconfigured resolves to harness — see Choosing the default), --route <id> (a [route] from the operator's native provider configuration; defaults to the [roles] entry for --role), --role <role> (default worker; selects that default route and the repository-write posture its tools run under), --resume <session> (continue a stored native session — see below; a resume needs no fresh prompt). --budget-tokens, --max-tool-calls and --timeout-secs apply as the loop's own ceilings (issue #637: --budget-tokens checkpoints once at agent::BUDGET_SOFT_FRACTION of the ceiling and stops at it, status:"limit_reached", limit:"tokens", the same exit code the harness path's own budget ceiling uses). The ceiling counts input + cache-creation + cache-read + output tokens, the same four classes the harness path's own --budget-tokens sums (harness parity); the final status's own reconciliation.billable_tokens counts only input + output, so the two figures differ whenever cache tokens are in play. --agent, --transcript, --session-id and --max-restarts are harness-runtime flags and are refused here, not ignored: a native session supervises no external process, has no transcript to score and nothing to restart. The command prints one structured JSON final status (schema_version, status, the actual route/provider/endpoint/account, the configured and served model, turn/request/tool counts, usage, evidence, and any incomplete or outcome-unknown tools) and exits on the same supervisor exit codes. --view json|plain (default json, contract unchanged) — plain additionally renders the finished session's transcript, after that same JSON status, through the dashboard native pane's own non-ratatui renderer (issue #480): streaming text/markdown, tool calls, diffs, test outcomes, artifact links and errors, readable in a piped log or a terminal too small for the dashboard, with no ratatui required.

--resume <session> does the four things a continuation owes before it may run, in this order: it reads the stored session (a session this journal has never heard of is an error, never an invented one), checks it was started in this repository (issue #639 — the canonical repository root is recorded once at session start and never rewritten; --resume from any other root is refused, naming the recorded origin, before anything else below runs or mutates the journal at all — resume from the same repository is unaffected), converts every execution whose last durable state is started to outcome_unknown — an effect that began and never reported is never silently retried — and then advances the generation, which fences the previous one out of both the journal and the execution broker. Anything still holding the old generation (a half-dead process, a stale handle) is refused from that point on.

Two operator-only flags make a whole native session runnable with no provider configured at all: --provider fixture:<path> replays a deterministic provider script instead of calling a model, and --fixture-tools <path> supplies the tool receipts it runs against (without it every tool call reports a fixture failure rather than touching the machine). Both are command-line flags and nothing else — no configuration layer, least of all a repository's, can set them. This is the same fixture machinery the loop's own tests use, so zirv ctx exec --runtime native --provider fixture:… exercises the shipped code path end to end without a credential, a network call or an installed harness.

Inside, an explicit session/turn/request/tool state machine drives the cycle. The assistant message and its complete tool calls are committed to the journal before any tool preflight, so a truncated argument stream can never become an effect. Independent (read-only) calls may be scheduled ahead of mutating ones, but results always go back in the provider's own declared order, keyed by call id. Every input is durably acknowledged the moment it is accepted and joins the conversation at an explicit delivery boundary — between requests in a turn, or between turns, never mid-stream or mid-tool; anything not yet delivered is reported as queued_input rather than dropped. An interrupt cancels the in-flight stream, every unstarted tool and the remaining turns, but never an effect already in progress: that becomes outcome_unknown and must be reconciled before any retry. Response retries (re-sending a request that committed nothing) are budgeted separately from tool-effect retries, and only a tool whose own contract says safe is ever re-run. A model's finish token cannot produce completed while an execution is incomplete, an outcome is unknown, an acknowledged input is undelivered, or a lifecycle gate objects.

Every lifecycle decision the loop makes — before-tool admission, after-tool result disposition, prompt notes, stop, notification classification, owed verification — comes from ctx::lifecycle, the shared service hook.rs now translates its harness payloads into. The native path calls it directly: no hook process, no harness binary, no PATH probe. Deterministic fixture provider/tool scripts under tests/fixtures/runtime/native/ prove loop correctness for both primary provider shapes without a paid call. The contract is in docs/design/2026-09-13-native-agent-loop.md.

The native conversation pane#

Coming soon: native execution is unavailable in this release.

The native conversation pane is coming soon. zirv native, including zirv native --help, only prints the coming-soon notice and exits without loading configuration, opening a dashboard, or starting a session. Native flags such as zirv chat --runtime native fail with the same explanation. zirv commands --json lists zirv native as an informational, non-mutating placeholder with no activation flags. zirv chat remains the existing harness.

The following describes the implementation retained for a future release.

zirv chat takes no --route or --view flag (those are zirv ctx exec --runtime native's own) — the native pane spends the orchestrator role's route ([runtime.roles].orchestrator, or [roles].orchestrator in ~/.zirv/native.toml with no per-role runtime override), unless the harness proxy is active and decided a model that resolves to a configured, policy-allowed route, directly by name or by catalogue model — see Harness proxy. --runtime native refuses every wrapped-harness-only flag (--agent, --simple, --resume, --pin-harness, a trailing extra argv) rather than silently ignoring them, and refuses without an interactive terminal on both stdin and stdout. The transcript renders Claude Code-style: assistant text and tool calls as ⏺ bullets, each tool result as an indented ⎿ line ("(ctrl+r to expand)" while collapsed), user turns as > lines, and diffs with an old/new line-number gutter. While a turn runs, an activity line (spinner, a rotating verb, real elapsed time, a running token count, "esc to interrupt") appears at the tail of the transcript. The bottom status line shows the actual model, route, runtime and billing class, one of seven states (generating, executing, waiting, blocked, cancelled, failed, or completed with an unread result), an estimated "context left N%" (recorded usage against the route's declared context window — not the compaction budget's own accounting), the repo path and, when known, the checked-out git branch.

Composer key contract: Enter submits (or, mid-turn, steers — written straight to the journal and picked up at the next turn boundary, the same mechanism NativeLoop::queued_input already re-polls between turns; a blocked/not-yet-ready submission is queued instead, and queued input is never treated as an approval answer). Shift+Enter/Alt+Enter insert a newline. Up/Down browse submit history only at the first/last line of the draft. Esc interrupts the current turn without quitting. Ctrl+C no longer interrupts by itself — one press arms a quit confirmation, and a second Ctrl+C within 2 seconds of the first quits; Ctrl+Q still quits immediately (persisting the draft first). Ctrl+R toggles the most recently rendered tool call's expanded state from either region. Shift+Tab cycles a composer mode label (default/accept-edits/plan) shown on the hint line — decorative only: no submit path reads it back to change approval or write behaviour yet. Tab swaps focus between the composer and the transcript, where Up/Down/PageUp/PageDown/Home/End scroll (scrolling up disengages auto-follow; it re-engages at the bottom) and e/Enter expands or collapses the most recent tool call. A /-prefixed submission is a pane-local command, never a turn: /clear drops the queued backlog, /help and /status report the key contract and the live session facts, /compact is an honest inert stub. /agents lists the resolved agent roster; /agent <manifest-id> <task> compiles and shows an explicit one-seat plan (a dry-run preview through the same capability/team-role/route checks --seat uses, never persisted); /team shows the plan stored for this repository and /team plan <objective> compiles and persists a new one — all three render through the identical functions the headless zirv workflow agent list/team show|plan commands print through, so the two surfaces cannot drift (issue #541 chunk C). /workflows lists the workflow registry and /workflow <id> shows that pack's definition (or, given trailing text as a task, starts it) and /workflow status [id] shows a running workflow's status -- all three rendered through the exact same workflow::engine writer functions the headless --json/text CLI uses (issue #542 chunk 3b), so the pane and zirv workflow list/show/status can never disagree about the same state. /settings opens a searchable, keyboard-only list of every configuration key (name, effective value, winning layer, scopes User/Project/Session, reload timing) with a detail footer that also shows the built-in default ((unset) for a key with no default, false and 0 shown as such); type to filter, Up/Down move, Tab/Shift+Tab (or PageDown/PageUp) jump to the next/previous top-level section, Enter edits the focused row, Tab inside the editor cycles its scope, Ctrl+R resets it to the inherited value (or removes a session override), Esc cancels and then closes. /settings <query> opens the list filtered, /settings get <key>, /settings set <key> <value> [--scope session|user|project] and /settings reset <key> [--scope ...] print one notice instead. It is zirv control input: never journaled, never sent to the model. User-scope writes go through the exact validated, comment-preserving, atomic edit zirv ctx config set uses (and are refused when the file changed since it was read); project scope writes <repo>/.zirv/ctx.toml only for keys a repository may set and only when the value narrows, refusing before anything is written; Credential-like keys render as (redacted) and are never accepted. Session scope is an in-memory override held by that pane: it is journaled as a control event (key and non-secret value, never a user turn), replayed from the journal when the pane resumes, and never written to a file. Only keys the pane re-reads on every record refresh (fallback.enabled, fallback.health.enabled, fallback.unknown_headroom_pct, which feed the pane's pool view) offer it and say pane view only; delegations and turns: next session: the override changes what that pane displays, while delegations, turns and the run loop load their own config and pick the key up next session. Every other key says next session and a session edit of one is refused with that reason. A saved (user or project) write is never live, because the dashboard loads its config once. zirv ctx config get <key> [--json] prints the same effective value, default, source and reload timing as /settings get for the same key (project layer from the current directory), and zirv ctx config show [key] --json prints the stored operator file as JSON; there is no session scope on the command line. @ file references and a !-prefixed shell line are not wired into this loop yet.

--runtime native is not a separate dashboard any more: the native conversation opens as the first pane of the ordinary dashboard, so one process holds wrapped and native panes together. A pane carries its kind (dash::pane::PaneKind); everything around it — the header, the sidebar roster, the spawn-request channel, the mail sweep, attention, the budget sweep, the footer spend and the restore roster — is the dashboard's own and applies to both kinds unchanged. Only four things differ by kind: the pane renders as a vt100 grid or as the native frame, a keystroke reaches the pty writer or the native composer/router (a native control is never offered on a wrapped pane, and vice versa), mail is delivered as a visible pty injection or through the native submit path (so it is subject to the same rollover/generation guard the operator's own Enter is), and ending it sends a harness quit sequence or shuts the session down. zirv agent <role> --runtime native from inside the dashboard opens a native worker pane the same way; such a request is refused, rather than accepted and silently unaccounted, when it asks for a work group or a token/time ceiling, which are accounted from a harness transcript a native session does not have. The restore roster records each pane's kind, and a native pane comes back through the same attachment decision a fresh one makes — so a dashboard restarting while the persistent runtime still holds the conversation re-attaches to it rather than opening a second supervisor over it. The view model (the reducer from the journal to a transcript, the composer, the renderers) is dash::native_pane, covered by deterministic tests with no terminal required; see docs/design/2026-09-13-native-pane.md.

The pane's own surfaces (issue #490). Beside the conversation the pane draws an agent & task overview built from the coordinator graph, the delegation receipts and the seat records — role, the actual model and backend with its provenance, ownership and worktree, and one of running / blocked / approval-needed / done-unread / failed / queued / draining, each with its own glyph as well as its own colour. Rows are navigable with Up/Down and clickable; Enter opens that worker's bounded manifest — its diff, test and artifact evidence — and never its transcript, and f sends a bounded follow-up to it (queued automatically if that worker is itself blocked, and retried at its next idle boundary). Underneath sits the usage and health provenance strip: measured, estimated and unknown are labelled and never summed, a route that is excluded keeps its reason verbatim, and an unknown figure is never rendered as a zero. Compaction, rollover and reconnect appear as ordinary notices that scroll with the conversation, each announced once; drafts, selection, focus, scrollback and acknowledged input survive all three, and a submission is refused outright when the logical seat has moved on underneath the pane. An approval is rendered as a numbered dialog carrying the exact scope, answered only with 1–3, the arrows, Enter or Esc — never from the composer, which queues while blocked. Answering "Yes" is real for an in-process session: its execution broker runs in interactive approval mode, raises a typed request carrying the exact tool, scope and actor, and BLOCKS that tool call until the dialog answers. 1 runs it once; 2 runs it and stops asking for that exact tool+scope for the rest of this session only (never persisted, never widened to a directory, and only offered when the scope can be stated exactly); 3 refuses the call with the operator's guidance, which is also committed as steering so the running loop picks it up. Esc is 3. Interrupting the turn cancels whatever is blocked on the dialog: the call fails closed and a later answer releases nothing. A headless session is unchanged — it is shown the deny option only, rather than a "Yes" that would quietly fail — and repository-owned configuration can only narrow this, never grant it. ? lists every binding, Tab moves focus, Esc interrupts (or closes a dialog first), and a double Ctrl+C quits. The composer itself is a bordered box with a > marker and a hint line naming what Enter does right now, and /, @ and ! open the slash-command list, the worktree-restricted file picker and a shell line that runs through the pane's own process tool under the same policy as any other tool call.

Where the conversation lives. With [session] persistent on, something listening, a runtime that serves native conversations, and a live native seat for this repository, the pane attaches to that session over protocol v1 instead of opening its own — submit, steer, interrupt and the approval decision all go out as session.send_input / session.interrupt / session.approve, closing the window is a session.detach rather than a kill, and the status line reads native · runtime. Anything missing falls back to an in-process session, which is a working mode rather than a failure. An attached pane's model and route show as –: the runtime publishes no route identity for a session, and a guess would be worse than an honest placeholder. See docs/design/2026-09-13-native-ux.md and the mock in docs/design/mocks/2026-09-13-native-pane.html.

Native workers, shared ownership and delegation receipts#

Coming soon: native execution is unavailable in this release.

zirv agent --runtime native (equally zirv ctx agent --runtime native) delegates one task to a native worker -- no coding harness installed, no child process, no PTY:

text
zirv agent native "read src/main.rs and report the entry point" --runtime native
zirv agent work-sonnet "run the failing test and report" --runtime native --task task-12 --json
zirv agent claude "review this diff" --mode read-only          # unchanged: the harness fork

Flags: --runtime harness|native (default configured, resolved the same way zirv ctx exec's does — see Choosing the default) and --route <id>, the same two flags and the same two values zirv ctx exec already takes. Without --runtime native nothing changes -- the harness delegation is reached by the same code, in the same order, with the same arguments. With it, the positional <name> names the provider route rather than a harness (a native worker has no harness to name), and the reserved value native defers to the [roles] entry for --role; --route overrides it. --max-restarts and a trailing -- <flags> passthrough are refused, not ignored, for the same reason zirv ctx exec --runtime native refuses them.

Everything else about the delegation is identical, because it is literally the same code: --workdir/--worktree, --task, --group, --mode, --path-scope/--no-network/--depth, --result-schema/--result-kind, --budget-tokens/--max-tool-calls and --json all behave exactly as they do for a harness worker. A native worker takes the same ownership a legacy one does and is therefore mutually exclusive with it:

Exclusive claimMechanismEffect
Task cardzirv ctx task claim (task::claim_locked)one live claimant per card, whichever runtime asked
Checkout writewriter permit (permit::acquire_writer)one writer per tree; a native worker's permit is handed to its execution broker, which refuses any repository write not backed by a permit for that exact tree
Provider tokensper-provider reservation ledgerone machine-wide outstanding total, settled from the run's real usage

Each delegation gets a stable handle -- minted by zirv, independent of any provider conversation id a resume would change -- and a durable record at <state>/delegations/<repo-slug>/<handle>.json holding the launch receipt (written before anything runs), the ownership taken, every attempt, and every delivery already published or consumed. Terminal outcomes are persisted first and notified second, over ordinary mail, carrying a delivery identity <handle>:<attempt>:<revision>. Mail is at-least-once: a consuming zirv ctx inbox drops an exact repeat of an identity it has already consumed (and still shows anything it cannot account for), so one completion is never acted on twice and never lost. A message that arrives while its target has an approval or other attention latch open is queued durably and retried at the next idle boundary -- never typed at the dialog (the same rule the dashboard's own pane sweep applies).

A native session drives all of this with seven typed tools in its own registry -- delegate, send, wait, result, follow_up, interrupt, close -- each a validated argument shape in front of the same service method the CLI verb calls. result returns a bounded manifest (outcome, delivery identities, report reference, unknown tool outcomes, whether the summary was cut), never a transcript. follow_up is addressed to the delegation handle: directed mail while the worker is live, a journal resume for a finished native worker (the id it returns is what zirv ctx exec --runtime native --resume takes), otherwise an explicit replacement checkpoint that says it has none of the original's hidden context -- there is no "most recent session" fallback, and an unknown handle is an error. close releases the reservation and the write claim while preserving every receipt and every outcome_unknown effect. The contract is in docs/design/2026-09-13-native-workers.md.

Trust boundary: every delegation tool crosses the native execution broker as an ExecutionAction::Delegate, so a native session cannot delegate around the seat fence and policy its other tools run behind; the delegation handle is validated as [A-Za-z0-9_-]{1,128} at the argument boundary, so provider output can never name a file outside its own repository's delegation directory; and a nested worker gets its own principal and a delegation_depth one hop shorter than its parent's, so it inherits none of the parent's session authority.

zirv verify --builtin's ZCHK-RUNTIME-INVENTORY check keeps docs/design/native-runtime-inventory.md honest against the real command surface and source tree: every command verb and every model-calling call site in src/ has a named implementation owner, checked on every run. Its sibling ZCHK-NATIVE-PARITY keeps docs/design/native-parity.md -- the release-blocking parity matrix -- honest in turn: every capability the inventory knows about has a row naming the native path, the legacy path, the provider/platform requirement, the evidence, and the rung that says how strong the claim is (unit, integration, ci-matrix, live-validated, legacy-only). A missing row, a cited test that exists nowhere in src/, a CI step that is not in ci.yaml, an uncommitted docs/benchmarks/ pointer, a live-validated claim with no recording, an evidence-free row that is not named as a release blocker, or a legacy-only row that does not say why -- each fails the build, so no row can claim more than its evidence. What that record is worth, what is deliberately not verified, the pre-declared non-regression targets and the release decision (the default stays the harness; native is opt-in) are in docs/design/2026-09-14-native-release-evidence.md. The architecture decision behind all of this is recorded in docs/design/2026-09-11-native-runtime-contracts.md.

Native workflows, verification and helper calls#

Coming soon: native execution is unavailable in this release.

Migrating the chat loop alone would leave hidden vendor-CLI dependencies in everything around it, so every zirv model call that is not the main conversation runs natively too: handoff distillation, zirv ctx ask, zirv ctx optimize's judgment pass, the agent loop's objective judge, the memory harvest and consolidation, the independent code reviewer, the frontend visual reviewer and the built-in agent seats.

They all go through one helper service (ctx::helper) rather than per-call-site provider code: one bounded conversation, a typed answer, typed failures. There is no new configuration key. A helper runs natively exactly when your own native provider configuration names a route for that helper's role — distiller, ask, optimize or seat under [roles] in ~/.zirv/native.toml — and otherwise keeps its existing harness path unchanged. A native attempt that fails still falls back to the harness rather than failing the caller.

Every helper call, native or harness, appends one row to logs/delegations.jsonl as agent helper (principal helper/<role>/<harness>, with the model, wall time and, for native calls, the tokens), so zirv ctx spend shows the overhead. The model scorecard skips these rows. Codex rows written before 4.51.0 logged cached tokens as plain input; readers re-split them from the pinned rollout's final token count (recorded in logs/delegations-codex-split.json), so spend prices them at the cache rate.

zirv ctx compile --measure [--role orchestrator|sub-orchestrator|worker|single] reports every layer a seat of that role receives, including the adapter layer, the supervisor line and the workflow step; the rows add up to the total. Tokens stay a bytes/4 estimate.

The seats that are real delegated workers take --runtime native instead, so they reuse zirv agent end to end:

bash
zirv workflow review run <id> --agent fast-route --runtime native
zirv workflow agents dispatch reviewer --adapter fast-route --runtime native
zirv workflow frontend review --runtime native

Read-only stays read-only by mechanism, not by prompt: a native helper and a --mode read-only native worker hold no writer permit at all, so the execution broker refuses every repository write, outside write, write-effect process and shared-scope knowledge write at effect time — and a headless session cannot approve its way past that. A writable seat is therefore refused by the native seat dispatcher rather than quietly downgraded; run it as a delegated worker (zirv agent --runtime native --mode writing), which takes a real permit.

A native session drives the workflow itself with four typed tools — workflow_status, workflow_context, workflow_advance, workflow_approve — each a thin adaptor over the same workflow::engine function the CLI verb calls, over the same durable state. The two that mutate the workflow declare the write capability, so a read-only reviewer can read the workflow it is reviewing and cannot move it.

Methodology and workflow adoption are automatic. The native context compiler puts the engineering standard, the role methodology, the model profile, your own and the repository's instruction files and the active workflow's current step into every request the session makes; nobody hand-seeds a prompt. Verification freshness is not advisory either: the engine's completion gate is re-read at every completion attempt, so a session that reaches the Test step after it started is gated on the evidence that exists then, and its "I am finished" token cannot outrank it. That gate is keyed by the workflow's own recorded branch, so a workflow started in the main checkout accepts its worker worktree's evidence for the same change set (see Linked worktrees) and rejects a different one.

The per-command parity table — every shipped command and helper, its native implementation and the test that pins it — is docs/design/native-parity.md; the decisions behind this step are in docs/design/2026-09-13-native-workflows.md.

Instructions in native sessions#

Coming soon: native execution is unavailable in this release.

A native session's instruction layer (SourceKind::NativeInstructions, data, never a provider instruction message) is built the same ZIRV.md precedence the wrapped harness reports, in a fixed order: operator-global ~/.zirv/ZIRV.md first, then the unchanged repo .zirv/system-prompt.md layer, then the resolved ZIRV.md/AGENTS.md/ CLAUDE.md/AGENT.md winners for the repo root and the active scope's ancestor directory chain. Scope rule: only the repo root plus directories that contain a path this session has actually touched (a tool call's read/write/edit target) load their nested instruction files — a large monorepo's unrelated crates are never pulled in. A shadowed, duplicate or excluded chunk A decision still gets a provenance entry (Excluded, naming the exact reason) rather than vanishing, and the whole layer is capped by context.instructions_max_bytes (default 32 KiB) on top of the existing per-file cap. Recompile on change: at the start of a turn, the resolved instruction file list (paths and content hashes) is recomputed for the session's touched-path scope; when it differs from what shaped the current system/preamble — a new nested file entered scope, a file changed on disk, a file was removed — the whole standing context recompiles and replaces it before that turn is sent, without restarting the session. Recompiling only ever touches the instruction layer: tools, policy, permissions and the route are untouched. Every compile/recompile is journaled (context_version = the compiled context's stable-prefix hash, plus the per-source path/scope/trust/decision), and the native /context (alias /instructions) pane command surfaces the same facts live.

Native teams and the coordinating seat#

Coming soon: native execution is unavailable in this release.

Zirv can run the coordinator itself. A native session seated as --role coordinator (or sub-orchestrator) plans the work, staffs it and delegates it across native and wrapped workers, over the same shared task cards, work groups, objective and workflow every other surface uses.

Seven team roles are recognised — coordinator, sub-orchestrator, researcher, planner, implementer, reviewer, tester — and each one spends the route you configured for it under [roles] in ~/.zirv/native.toml:

toml
[roles]
coordinator   = "sonnet-seat"
implementer   = "sonnet-seat"
reviewer      = "haiku-metered"
tester        = "haiku-metered"

A role with no entry has no native path at all: the refusal names the role and lists the roles that are configured, rather than quietly spending another role's route. Nothing is inferred, and nothing changes provider on its own — a route a model asks for is admitted only if it clears your policy and keeps the same billing posture you seated that role on.

bash
zirv ctx exec --runtime native --role coordinator --prompt "ship issue #123"

Eight typed tools give that seat the board: task_create, task_claim, task_list, group_create, group_status, objective_status, team_status and team_plan. Each is a thin adaptor over the same zirv ctx task|group|objective/zirv workflow team service the CLI verb calls, so there is one definition of "this card is claimed" and one of "this group is full". The four that change shared state need a writer permit; the four that read do not.

Who may do what comes from the seat, not from the request. A reviewer or tester is read-only by identity, however the delegation was spelled; a coordinator that is itself read-only can still dispatch a writing implementer; a reviewer seat may not delegate at all. Delegation depth, task ownership, checkout ownership, group admission and the provider token ceiling are the same limits the rest of zirv already enforces — there is no second counter, and a delegation that fails any of them starts nothing and leaves no receipt behind.

A delegation is checked against its manifest and the team plan (issue #541 chunk C). delegate accepts manifest: <id> (defaulting to the requesting role's own built-in manifest — implementer for implementer, reviewer for reviewer, and so on); an unknown manifest, or one whose own team role disagrees with the requested role, or one that may write for a role that is read-only by identity, is refused before any receipt exists. team_plan (coordinator/sub-orchestrator seats only) compiles the SAME proportional team zirv workflow team plan compiles — classification, execution profile, then compile/compile_explicit for seat: <manifest id> — and stores it: the active workflow owns the plan when one exists for the repository, and the coordinator's own record does otherwise. Once a plan exists, a delegate call must name (as its task) an unfilled seat whose manifest and role match one in it, or it is refused with not in the team plan; run team_plan again or pass override: true; override: true bypasses the match but is honoured only for the coordinator seat, and is recorded on the launch receipt either way. Seats are marked filled the moment they are dispatched (the SAME graph node task_create/team_status already key by task id); a retry after a failure re-fills the identical seat rather than creating a second one, and two seats whose claimed paths overlap can never both be dispatched at once — a wrapped-harness worker seat goes through these exact checks too, whichever runtime the coordinator itself runs on.

A coordinator survives a restart. Its task graph, decisions, evidence references and your standing constraints are durable. On restart it consumes every worker receipt published while it was away, exactly once, and never re-dispatches work that already settled. A worker's outcome is a bounded manifest and a reference to its result, never a replay of its transcript, and until the receipt is actually read team_status reports it as pending rather than assuming it:

bash
zirv ctx objective set "ship issue #123 without touching the release branch"
zirv ctx objective close      # stops further dispatch; work already running still reports

Setting an objective is how you steer — it redirects the coordinator and lifts a previous stop. Closing it stops further dispatch while leaving work already delegated answerable, so its results are still collected. objective close refuses (exit 1) unless a fresh, passing final verification for the repository already exists — completion is asserted only by a passing verification gate, never by prose.

The decisions behind this step are in docs/design/2026-09-13-native-orchestrator.md.

Native configured capabilities#

Coming soon: native execution is unavailable in this release.

A native session inherits nothing from a coding harness, so the non-shell capabilities a workflow needs — MCP servers, a browser, web search, language diagnostics, artifact presentation — are configured, never assumed. Zirv does not claim a raw model API provides any of them.

bash
zirv ctx capabilities                       # the report, one row per integration
zirv ctx capabilities --probe               # also contact each configured MCP server
zirv ctx capabilities --require browser     # exit non-zero unless it would admit a step

Every row is exactly one of three states. available means zirv found the backend. unavailable means it did not, and the row names the missing binary, credential or config key. unverified means it is configured but has not been contacted this run — discovery reads configuration, PATH and the repository tree and contacts nothing, so a configured MCP server nobody spoke to is not evidence that it answers. --probe is what turns an unverified MCP row into a verified one. The workflow engine reads the same report and refuses to enter a step whose required integration is unavailable, quoting the diagnosis, instead of failing halfway through it.

Zirv speaks MCP itself, over a local stdio server or a remote Streamable HTTP endpoint with a bearer credential from the same store the direct providers use. It negotiates the 2025-11-25 revision (refusing an unknown one rather than guessing), discovers tools and resources, calls them, cancels with notifications/cancelled, and reconnects with re-discovery. A reconnect that changed or removed a tool invalidates it: a call naming that tool is refused until its current schema is described again, so a stale call can never execute a different tool. Server descriptions and results are untrusted data — bounded, redacted, never executed, and streamed into the existing output store when large. A catalogue at or below capabilities.max_inline_mcp_tools is exposed as ordinary tools, namespaced mcp__<server>__<tool> so a server can never shadow a built-in name; a larger one is reachable only through a compact index (mcp_list), an on-demand schema (mcp_describe) and mcp_call, so a big toolset never enters every model request.

MCP invocation crosses the same execution broker, canonical policy, tool receipts and output limits as every built-in tool, with the effects an operator declared for that server — never the server's own claim about itself. An MCP call is never blindly replayed, and a cancelled one is reported as an unknown outcome.

Web results always carry the source URL they came from, and a row that cannot name one is dropped. Browser captures return the on-disk evidence path they actually wrote, and a capture that produced no readable file is an error rather than a success. A capability with no configured backend returns a typed unavailable result naming what is missing; there is no code path that returns an empty success. Outbound requests pass one interception seam (EgressGuard), which is where issue #466's on-device obfuscation belongs rather than a second subsystem beside it.

Configuration lives under [capabilities] in ~/.zirv/ctx.toml and is off by default:

toml
[capabilities]
enabled = true
max_inline_mcp_tools = 24        # above this, only the index plus describe

[capabilities.web]
search_endpoint = "https://search.example/api?q={query}"
search_credential = "env:SEARCH_TOKEN"   # env:NAME, store:<item> or file:<path>
fetch_enabled = true
allow_hosts = ["docs.rs", ".rust-lang.org"]   # empty means nothing is reachable

[capabilities.browser]
enabled = true
# binary = "chromium"            # discovered on PATH, or the standard
                                  # per-platform install locations, when
                                  # unset: macOS's /Applications and
                                  # $HOME/Applications app bundles, and
                                  # Windows's Program Files, Program Files
                                  # (x86), and %LocalAppData% installs

[[capabilities.mcp]]
name = "docs"
enabled = true
transport = { mode = "stdio", command = "mcp-docs", args = [] }
effects = { network = true }     # what this server's tools may do, per the operator

[[capabilities.mcp]]
name = "remote"
enabled = true
transport = { mode = "http", url = "https://mcp.example/rpc", credential = "env:MCP_TOKEN" }

The whole [capabilities] table is operator-only (see Trust boundary): every key names a command zirv spawns, an endpoint it authenticates to, a credential, or a browser it launches, so there is no narrowing half a repository checkout could legitimately set. The full contract is documented in docs/design/2026-09-13-native-capabilities.md.

Runtime protocol v1 (zirv ctx api)#

The RuntimeBackend wire above is in-process. Protocol v1 is the public, versioned surface around it: the one a durable subscriber, an alternate client or a future daemon is allowed to depend on. The CLI wrappers remain the normal automation interface — raw protocol access exists for clients zirv does not ship.

bash
zirv ctx api schema          # the contract, for a human
zirv ctx api schema --json   # the same contract as a JSON Schema document
zirv ctx api serve           # bind the endpoint for a bounded time
zirv ctx api call session.snapshot

Both schema renderings are generated from the binary's own types — the method table, the real enum variants, and a test that pins the published frame field lists against what serde actually writes — so the documentation cannot drift from the wire.

Transport. A unix domain socket at <state>/s/api.sock on unix, a named pipe on Windows, carrying NDJSON in both directions. The same transport also backs one other owner-only, per-session endpoint outside this protocol: the Jev session relay at <state>/s/jev-<hash-of-session-id> (see [jev]'s "Session relay" above). The server writes one hello frame per connection before reading anything; a client intersects the capabilities that frame advertises with its own and disables locally whatever is missing, which is how an older client connects to a newer server and vice versa. Requests carry a caller-chosen id; replies echo it and carry the server revision; mutations accept an optional idempotency_key, and a retry with the same key returns the first attempt's result instead of repeating the work. Unknown fields are ignored everywhere, and every published enum vocabulary ends with an unknown fallback.

Methods (v1 is deliberately narrow): server.ping, server.capabilities, session.snapshot|list|get, session.start|stop, session.read|send_input, session.wait, session.report_status, session.attach|detach|takeover|resize|screen, session.interrupt|approve|task_result|history|journal, events.subscribe. Mail, memory, work-group, workflow, layout and plugin methods are added only when a concrete client needs them. The five attachment methods need a server that owns terminals, so they sit behind their own session.attach capability; the five native methods need one that owns native conversations, so they sit behind session.native. A server without either does not advertise it, and a client disables that surface locally rather than calling it and being refused.

Native sessions on the protocol. A native conversation has a journal instead of a pseudoterminal, so it reaches the same endpoint through the same methods with a different half of the surface. Input is session.send_input's ordinary submit/steer modes — there is no second way to hand a session text. session.interrupt cancels the turn in flight and leaves the session alive (session.stop is still the only verb that ends one); session.approve decides one pending approval; session.task_result records a delegated task's outcome; session.history reads the conversation; session.journal pages its durable event stream by cursor. session.screen, session.resize and mode=raw are refused by name: there is no terminal to act on.

session.history is the one method in v1 that publishes conversation text. It is capability-gated, seat-checked, and its tool entries carry a tool's name and never its arguments or results. Session facts, snapshots, lists and event frames still carry none of it.

Seats and durable retries. Once any client is attached to a native session, every mutation must name a client_id holding the controller seat — omitting it is refused too, so an observer cannot mutate by leaving the field out. A session nobody has attached to is driven by whoever can reach the owner-only endpoint, which is the rule a headless zirv ctx exec needs. For a native session an idempotency_key becomes the journal's own message id, so a retry after a reconnect — or after the runtime restarted, which loses every in-memory cache — is deduplicated on disk and answered with duplicate: true and the original message_id; no second turn is queued. session.journal answers gap: true when a caller's cursor can no longer be continued from, which is the durable counterpart of the live revision gap rule.

Events and gaps. The server-wide revision advances by exactly one per emitted event, so a subscriber that sees a revision other than last + 1 has missed something and refreshes session.snapshot rather than drifting. Waits pin the session generation resolved at call time: a session replaced while a wait is running fails that wait with stale_generation instead of letting the replacement satisfy it.

What is shared and what is not. Server state holds runtime facts only — which sessions exist, their stable ids, generations and lifecycle state, and the event log. Layout, colour, sidebar selection, mouse state and modals stay in whichever client draws them; no method can read or write any of it. Session ids are stable across panes, tabs, worktrees and client attachment: surface is the only axis a client change moves.

Security. The endpoint path is always derived from the operator-owned state directory — there is no flag, config key or environment variable that points the server or a client at an arbitrary path, so a checkout cannot name or redirect it (see Trust boundary). On unix the socket lives in a 0700 directory, is chmod'ed 0600, and the server verifies the peer uid (SO_PEERCRED/getpeereid) against its own; on Windows the pipe is created with an explicit owner-only DACL, because the default named-pipe descriptor grants read access to Everyone and the anonymous account. Snapshots publish a redacted session shape by construction: no transcript path or body, no prompt, no mail body, no terminal history, no credential and no absolute repository path — only the sanitised repo slug. Mutations go through the same narrowing-only trust model as everything else.

What v1 is not. The reference server (zirv ctx api serve, or the duration of one zirv ctx api call) runs in-process, owns no terminals and holds no processes: it serves every read method off the session registry, refuses session.start|stop|send_input with a structured unsupported, and does not advertise the attachment capability at all. The daemon that does own terminals is zirv session serve — the same protocol, the same endpoint, one extra capability. Frozen request/response/event fixtures under tests/fixtures/protocol/v1/ are replayed against the server on every test run, so the wire cannot change by accident. The full contract and its trade-offs are recorded in docs/design/2026-09-12-runtime-protocol-v1.md.

Persistent runtime (zirv session)#

Experimental, operator-only, and off by default. A local runtime service owns the PTY/ConPTY processes, their supervisors and their registry records, and every UI is a client of it. Closing the window, killing the client or losing the terminal then costs a repaint, not a session.

bash
zirv session serve                 # run the runtime (owns the terminals)
zirv session list                  # what it holds, and who is attached
zirv session attach [name|id]      # attach this terminal (Ctrl+A d detaches)
zirv session detach [name|id]      # release clients; the agent keeps running
zirv session stop [name|id]        # end a session (this one asks first)
zirv session stop --runtime        # stop the service; sessions keep running

Turn it on in ~/.zirv/ctx.toml (a repository cannot):

toml
[session]
persistent = true       # default false
history = false         # default false -- see the warning below
scrollback_rows = 2000  # in memory, per session
stale_after_secs = 120  # secondary staleness signal only

With persistent = true and a real terminal on both stdin and stdout, zirv chat attaches to this repository's runtime session (starting the runtime and the session if they are not there yet) instead of owning a PTY in its own process. zirv chat --no-session, a piped stdin and a redirected stdout all keep the previous behaviour exactly.

Clients. Any number of observers may watch a session; at most one client holds the keyboard. A second controller is refused with busy rather than silently displacing the first — zirv session attach --takeover takes the seat on purpose, and every client sees the controller_changed event. Ctrl+A d detaches (Ctrl+A Ctrl+A sends a literal Ctrl+A to the agent), the same prefix the dashboard uses.

Detach is not stop. detach moves an entry in the runtime's attachment table and touches nothing else: no confirmation, and the agent, its PTY, its supervisor and its registry record are all still there afterwards. stop puts the child through the existing termination ladder and asks first — --yes is required when stdin is not a terminal.

What actually survives, honestly:

TierEventGuarantee
1A client detaches, crashes or closesThe original PTY and process keep running. Reattaching restores the current rendered screen and live input from the runtime's own in-memory parser. Nothing is relaunched.
2The runtime itself restartsThe processes are gone. Topology (which sessions, which directories, what size) is restored, and a session is resumed only when it carries a verified harness conversation reference; everything else is reported as not resumed. A restored session is a new session that records its predecessor — never the old identity revived.
3Rendered terminal history across a runtime restartOff by default. history = true writes rendered terminal output — including anything an agent printed, such as API keys, tokens and file contents — to disk, and says so every time the runtime starts.
4Replacing the runtime binary under live sessionsNot supported. Stop the runtime, upgrade, start it again.

A detached session keeps every Zirv guarantee that reads the registry, because the runtime files the same registry record a dashboard pane does and holds it for the life of the session: usage pacing, budgets, rot scoring, mail addressing, writer permits and workflow policy all keep working with nobody watching, and the harness's turn signals keep being observed. Mail is delivered while nobody is watching too — the service types it into the session on its own heartbeat, through the dashboard's own sweep, instead of leaving it queued until a client attaches.

Native conversations live here too. The same runtime owns native sessions (runtime = "native"), publishes them in the same session list and serves them over the same endpoint — one service with two hosts, not two daemons. A native conversation's durable state is its journal, so a runtime restart brings it back by reading it: every tool execution that was merely started becomes outcome-unknown and is named on startup rather than retried, the generation advances to fence out any straggler, and nothing is re-submitted. No process is ever described as having survived.

The dashboard is a client, not a second owner. With the gate on and a runtime listening, zirv dash refuses to open a second terminal over a session the runtime already holds and points at zirv session attach; two supervisors on one conversation is what the runtime exists to prevent. With the gate off, or with nothing listening, the dashboard owns its own terminals exactly as before.

Identity. Each namespace publishes a record under <state>/runtime/ with its owner, version, endpoint, creation time and last-client time. Staleness is decided by process start identity, not by pid alone: a live pid whose start time does not match the record is a recycled pid and the namespace is free, while a record nothing can verify is left alone rather than seized. Every service start mints a fresh instance id, so a crashed runtime's session identities can never be republished by its successor.

The native integration — the two hosts, the seat rule, durable idempotency, journal cursors and restart reconciliation — is recorded in docs/design/2026-09-13-native-runtime-integration.md. See docs/design/2026-09-13-persistent-runtime.md for the design, the measurements and what is deferred.

Native provider routes#

Coming soon: native execution is unavailable in this release.

Native routing is opt-in through a separate ~/.zirv/native.toml; older zirv binaries ignore this file and continue reading the unchanged ctx.toml. Start with zirv ctx provider init, inspect the offline inventory with zirv ctx provider list [--json], and validate role access with zirv ctx provider check [--live] [--role <role>] [--json]. Live checks are off by default and only call the configured model-list endpoint. Store a store: credential without accepting its value as a zirv argument with zirv ctx provider credential set <account>.

toml
schema = 1

[endpoint.local]
provider = "openai-compatible"
base_url = "http://127.0.0.1:11434"
vendor = "ollama"

[account.work]
provider = "anthropic"
credential = "env:ANTHROPIC_API_KEY_WORK"
billing = "api"
pool = "work"

[route.work-sonnet]
account = "work"
endpoint = "anthropic"
model = "claude-sonnet-5"

[roles]
orchestrator = "work-sonnet"

[policy]
allowed_routes = ["work-sonnet"]

Providers with a default URL have an implicit endpoint named after the provider. openai-compatible and aws-bedrock instead require a vendor, and a base_url unless the vendor's route profile has a documented one. Account pools default to the account id; two accounts may deliberately share one pool when they share quota. Every API-billed account except openai-compatible must declare a credential reference. Subscription-billed accounts may omit one. A direct API route on such an account stops at configured with an entitlement problem; an explicit provider-owned execution route uses the official harness login, as described below. Credential references are env:NAME, store:<item>, or file:<path> (~ expands; Unix files must be mode 0600 or stricter). Claude Code/Codex harness login tokens are refused: Claude.ai and ChatGPT subscriptions are entitlements for the harness backend, not native API credentials, and credential set refuses those store refs before reading a secret. Every route must declare a model that remains non-empty after an optional matching vendor/ prefix. Model ids and aliases resolve by exact case-insensitive match. A matching vendor/ prefix is accepted, but @date and :suffix decorations are never stripped or silently substituted.

The evidence ladder is recognized → configured → credentialed → reachable → authenticated → validated. Catalogue recognition never claims account access, and authenticated specifically means a credential was accepted. A credential-less compatible route stops at configured offline and can reach only reachable during a live check. Direct API offline commands stop at credentialed; official-harness checks may attest a signed-in account without a model call (model access remains unverified). For direct API routes, --live can establish reachability/authentication, while validated remains unavailable until the native model transports record a real validation. Credentials are withheld from plaintext HTTP on non-loopback hosts and that live probe is skipped; loopback HTTP remains available for local runtimes.

The optional repository layer <repo>/.zirv/native.toml may contain only schema, [policy].allowed_routes, [policy].compaction and [policy].context_editing. Its routes are intersected with the operator's set, so a checkout can narrow access but cannot add accounts, endpoints, routes, role bindings, credentials, or permissions.

Provider-owned execution in the native UI#

Coming soon: native execution is unavailable in this release.

A route may use an official provider harness while retaining Zirv's native conversation UI, tasks, approvals and tool broker. This is distinct from direct API execution: the official process owns the model connection, agent loop and local conversation. Provider identity, execution backend, authentication owner and billing are separate route facts. Initially the adapter registry supports claude-code; adding another provider requires an execution adapter and an independent review of that provider's rules, not a subscription-token transport.

toml
# ~/.zirv/native.toml (operator configuration only)
schema = 1

[account.personal-claude]
provider = "anthropic"
billing = "subscription" # Use "api" for Console/API/cloud billing.
# No credential. Claude Code owns authentication.

[route.personal-claude]
account = "personal-claude"
model = "sonnet"
execution = { adapter = "claude-code" }
# Optional absolute path; spaces are supported:
# execution = { adapter = "claude-code", program = "/opt/Claude Code/claude" }

[roles]
orchestrator = "personal-claude"
worker = "personal-claude"

Run zirv ctx provider login personal-claude to hand the terminal directly to official Claude Code's login, then zirv ctx provider status personal-claude. Zirv does not read, import, store or refresh subscription credentials, login URLs or authorization codes. A missing binary is never silently installed. Removing this route from native.toml leaves Claude Code signed in. To log out of the official installation, run claude auth logout yourself; that affects Claude Code sessions outside Zirv too. All official authentication methods and subscription plans are accepted. Pass login options after --, for example zirv ctx provider login personal-claude -- --console or -- --sso. API keys, cloud credentials and profiles configured in the launching user's environment remain available to Claude Code. Its public user settings retain apiKeyHelper, awsAuthRefresh, awsCredentialExport, login restrictions and authentication environment variables. CLAUDE_CONFIG_DIR may select an absolute user configuration directory outside the repository. Zirv never copies inherited authentication secrets to its settings, journal or diagnostics.

zirv chat --runtime native uses this route for the orchestrator. Native workers use their configured role routes and retain the existing task, mail, delegation, writer-lease and generation fences. The UI identifies the backend and selected billing. Provider status and headless final status report the detected official authentication method, upstream provider and billing, using unknown when the public status cannot establish them. Headless final status schema 3 includes an execution object with backend/version/auth owner, estimated API-equivalent cost, and separate nullable billed-spend and remaining-allowance fields.

Initial capability boundary. Requires a local, unmodified native Claude Code 2.1.248+ executable in the 2.1 series, its documented restricted/settings/tool/MCP flags, and a public status response attesting authentication. Windows/WSL (managed-policy verification pending), npm shell shims, managed startup policy, unauthenticated status, and an explicit token-budget ceiling are rejected before a model turn. Turn/time limits, streaming, follow-up and exact-session continuation are supported. Steering waits until the next turn; it is never reported as immediate. No live installation/platform compatibility is implied by fixture tests.

Claude Code starts in a private Zirv directory with project settings excluded, only authentication settings carried from public user configuration, hooks disabled, and only the explicit Zirv MCP server. Managed startup configuration that could override this boundary is rejected. Claude built-in tools and internal subagents are unavailable on this route; coding, shell, external services and independently scheduled workers use Zirv MCP tools. Every such effect passes the existing native broker, sandbox, exact-action approval, repository narrowing and generation checks. A denied approval produces a tool error, never an automatic permission-mode escalation. Streamed tool observations are not executed again. Tool subprocesses receive no provider credentials.

Billing. Claude Code selects authentication using its own precedence. The account's billing declares scheduling intent; it does not force a login method. Keep it aligned with the method you select in Claude Code. Detected billing is reported separately and is never hardcoded to subscription. No authentication failure or exhausted allowance causes an automatic API fallback. All aliases of the local official login share one anthropic capacity pool. Unknown allowance is unknown, not unlimited. The official result's dollar figure and zirv ctx spend are API-equivalent estimates, not invoices. Billed spend remains unknown. Paid usage credits can permit additional upstream charges: disable them in Claude's Settings → Usage if desired; Zirv cannot verify that switch or promise zero additional charges.

A successful turn persists an exact Claude session reference bound to its Zirv seat, route and workspace. Follow-up resumes only that reference. Cancellation before any tool effect can resume; a crash or interrupted effect with uncertain delivery leaves a reconciliation block. Inspect effects and use a new session with a portable checkpoint before retrying; Zirv never blindly replays the task. A provider-owned continuation cannot become an API conversation. Cross-runtime handoff transfers Zirv task/artifact/approval state, with the existing exclusive writer and worker/mail disposition rules.

Policy evidence, checked 2026-09-15. This design relies on Anthropic's explicit allowance for running the unmodified binary under the applicable terms and conditions, including the product operator agreeing to Commercial Terms and each user authenticating and being billed under their own agreement. Zirv does not resell usage or claim Anthropic approval. Written confirmation for this exact integration has not been obtained; this is not a guarantee of policy compliance. It does not offer Zirv-owned Claude login or claim endorsement. The June 15 pause means print-mode usage currently draws subscription limits; this is not a guarantee of permanent eligibility. The SDK documentation retains restrictive third-party-login language; extending this design beyond the unmodified-binary allowance requires clarification from Anthropic. See legal and compliance, the current subscription notice, programmatic execution, and the decision and validation note. This replaces #644's Claude-specific token-import proposal; it grants no entitlement for other providers. Direct Anthropic API routes still work without Claude Code installed and continue rejecting harness-login secrets before HTTP.

Opt-in live smoke (uses your own allowance). Record claude --version and the sanitized provider status output. In an authorized disposable worktree, open zirv chat --runtime native, ask for a bounded file edit and its verification, and check streamed text, one tool receipt per effect, and the backend/billing label. Send a follow-up and confirm the same external session in the journal's provider_execution checkpoint. Interrupt a response before tool work, then resume with a follow-up. For an interrupted effect, verify the reconciliation block instead of replaying it. Finally restart/reattach the Zirv session and check task/mail state. Never copy raw auth output, MCP configuration or login codes into a report. Fixture results and actual live versions/platforms must be reported separately.

Native setup, diagnosis and rollback#

Coming soon: native execution is unavailable in this release.

Native setup is coming soon. Provider commands and migration to native are blocked in this release; zirv ctx doctor reports coming-soon without probing providers or credentials. The commands below document the future setup flow:

bash
zirv ctx provider init                       # write ~/.zirv/native.toml
$EDITOR ~/.zirv/native.toml                  # declare account, route, roles
zirv ctx provider credential set work        # store the secret out of band
zirv ctx doctor                              # verify, class by class
zirv ctx exec --runtime native -- "…"        # first native run

zirv ctx doctor [--role <role>] [--live] [--json] is the readiness command. For every role it prints which backend an unflagged session would get and which authority decided that (flag, runtime.roles, runtime.default, built-in), which route it would spend, and how far that route got up the evidence ladder. Every problem it finds is sorted into exactly one class, because the operator's next action is different for each:

ClassWhat it meansWhat to do
missing-auth-materialNo API key resolved for the account, or the one that resolved was rejectedSet the env:/store:/file: reference, or zirv ctx provider credential set <account>
inaccessible-modelAuth material works; this model is not one this account may callPick a model the account is entitled to, or fix the alias
missing-toolA binary, MCP server or configured integration is absent — including a native adapter zirv has not shipped yetInstall/configure it; a missing adapter is a zirv gap with a tracking issue, never an entitlement excuse
unsupported-isolationNo verified process containment on this platformInstall bwrap (Linux); on Windows there is no verified backend yet, and sandboxed invocations are refused rather than run unconfined
service-failureThe endpoint is configured and credentialed but did not answerCheck the endpoint URL, the network, and the provider's status
upstream-entitlementA genuine upstream limitation, not a zirv gap: a subscription-billed account, or a vendor surface that exists only inside that vendor's CLIUse an API-billed account, or keep that surface on the harness backend

For provider-owned routes, doctor checks the official CLI's non-model version/auth interfaces and may create its private startup directory. It exits 1 only when a role that would run natively has no usable route. Its output is redacted the way zirv ctx snapshot is — every line is screened for credential shapes, high-entropy and opaque runs, and any line that opens like a conversation turn is replaced outright — so a doctor dump is safe to paste into a bug report. It carries no transcript text and no continuation data by construction.

Billing. A route's billing is api or subscription. Direct provider execution requires API billing; explicit provider-owned execution can use its official harness login under the boundary above. Subscription tokens remain refused as native API credentials (credential set refuses those store refs before it reads a secret). Accounts that share quota share a pool, and the usage windows (zirv ctx usage) aggregate per pool, so two accounts on one plan are not double-counted. zirv ctx spend --by groups the delegation ledger by harness, model, task-class, or worker instead -- it has no pool dimension of its own. A cached Jev answer made no request, so its row is not counted as a run.

Choosing the default. ~/.zirv/ctx.toml's [runtime] table decides which backend a session gets when the command line does not say:

toml
# ~/.zirv/ctx.toml
[runtime]
default = "native"        # or "harness"; absent means "harness"

[runtime.roles]
reviewer = "native"       # per-role override, outranks `default`
worker = "harness"

An explicit --runtime harness|native always wins over both. zirv ctx exec and zirv ctx agent default that flag to configured, which is exactly "ask this table"; zirv chat with no --runtime resolves the same way at the orchestrator role. A value this build does not recognise degrades to the harness with a one-line note rather than failing the command — zirv ctx doctor is where it is reported. The whole [runtime] table is REPO_FORBIDDEN (see Trust boundary).

[runtime] prompt_cache_ttl = "1h" (or "5m"; ZIRV_CTX_RUNTIME_PROMPT_CACHE_TTL) opts native Anthropic sessions into prompt caching: "1h" puts a one-hour cache_control breakpoint on the stable prefix (system prompt, else last tool definition) and never on the moving tail, "5m" uses the default cache TTL. Unset (the default) leaves native requests uncached; Bedrock, Google and OpenAI routes are unchanged. It is independent of [headless].prompt_cache_ttl.

Migration and rollback. zirv ctx config migrate [--to harness|native] [--dry-run] brings ~/.zirv/ctx.toml to schema 2 — the [runtime] table above — backing the previous document up to ~/.zirv/ctx.toml.pre-schema-2.bak and recording the schema in a sidecar ~/.zirv/ctx.migration.toml. The marker is deliberately not a key inside ctx.toml: that file is parsed with unknown keys rejected, so an older zirv binary would refuse the whole configuration rather than ignore one key. Running the migration twice writes nothing the second time and says so, so a re-run can never overwrite the real pre-migration backup.

zirv ctx config migrate --downgrade restores that backup byte for byte (or, with no backup, removes the [runtime] table), and deletes both markers. It is the supported way back to an older zirv: install the older binary after downgrading, since an older binary cannot parse [runtime] either. Nothing else is part of the transaction — ~/.zirv/native.toml, native journals, and the harness conversation references those journals carry are all outside the file being migrated and survive a round trip in either direction. Both modes can run concurrently on one machine throughout: a native and a wrapped session share the state directory, session registry, mail, task cards and cost ledger.

Entitlement limitations versus implementation gaps#

These are not the same thing and zirv never conflates them. An entitlement limitation is something the upstream vendor does not sell zirv access to; an implementation gap is work zirv has not done, and every one of them has a tracking issue. zirv ctx doctor classifies the first as upstream-entitlement and the second as missing-tool, and docs/design/native-parity.md lists both exhaustively. The current lists:

Genuine upstream entitlement limitations:

  • Subscription plans are not API entitlements. A Claude.ai or ChatGPT subscription cannot be spent through a direct provider API call. Native direct API routes on a subscription-billed account stop at configured with that problem named. Explicit official execution routes use the vendor harness while retaining the native Zirv seat.
  • Harness login tokens are refused as credentials. Reusing the vendor CLI's stored login for direct API calls is outside what that token is issued for; credential set refuses those store refs before reading a secret.
  • Vendor-CLI-only surfaces. zirv ctx wrap supervises a vendor TUI by definition, and zirv ctx handover swaps one vendor CLI for another; a native session has neither. Changing a native seat's model is [roles] configuration, not a handover.
  • Model entitlement per account. A model absent from an account's own model list is that account's entitlement, reported as inaccessible-model; zirv cannot grant it.

Implementation gaps (zirv's own work, each tracked):

  • Route profiles with no adapter yet are reported as missing-tool with their tracking issue in the message, explicitly as "a zirv gap, not an upstream entitlement limit".
  • Windows process isolation. There is no verified restricted-token/ AppContainer helper shipped, so PlatformIsolation::detect reports unavailable on Windows and sandboxed invocations are refused rather than run unconfined. Linux (bwrap) and macOS (sandbox-exec) are supported.
  • Live-provider validation. Every parity row is fixture- or service-level; the validated rung of the evidence ladder is not reachable until the N19 validation pass records a real one.

Native compaction#

Coming soon: native execution is unavailable in this release.

A native session compacts itself rather than calling a harness /compact. Committed journal events and the provider's own measured input footprint (fresh input plus cache writes plus cache reads) are projected into zirv's existing rot scoring engine, with the token gate sized from the route model's declared context window less the run's output reservation — not from the harness-transcript token constants. An unknown context window falls back to the absolute score.token_floor/score.token_ceiling defaults rather than a guess.

Four typed triggers are reported: context_overflow (the provider refused the request), token_pressure (measured input reached the derived ceiling), repeated_identical_errors, and loss_of_progress. The first two force a compaction; the others are advice until the rot gate escalates.

A compaction writes a versioned portable checkpoint — objective, hard constraints, task/workflow refs, every acknowledged input, task claims, completed-action receipts, outstanding tool calls, and evidence by SHA-256 — as an atomic export under <state>/native-checkpoints/ plus one journal event, which is the commit point. The original history and every stored artifact are retained: the summary only replaces what the next provider request sends, and the cacheable system prefix is never rewritten. The summary boundary never crosses a tool call whose effect has not settled, so compaction cannot mark a pending action complete, and acknowledged input that has not been answered is repeated verbatim. Distillation runs through the session's own native route with a bounded output budget and no tool schemas at all, and falls back to a deterministic structural summary whenever no model capacity, credential or valid reply is available — so it works with no coding-harness binary installed.

A same-route resume keeps the provider's opaque continuation envelope. A route, model, endpoint, account or protocol change discards it and rebuilds a portable semantic history from the checkpoint and the journal: text and refusals only, with no hidden reasoning, no provider signature, and no synthesized tool outcome — an unknown outcome is carried as unknown.

[policy].compaction is automatic (the default) or advisory (zirv reports the pressure and never compacts on its own). A repository layer may narrow it to advisory and can never widen it back — see Trust boundary. zirv ctx status prints one line per native session that has compacted or resumed, with the newest reason, and zirv ctx exec --runtime native reports the same facts in its final-status JSON.

Native context editing#

The first-party Anthropic Messages adapter (anthropic-messages routes only — not Bedrock, Vertex, or any other protocol) requests server-side context editing (clear_tool_uses_20250919, beta context-management-2025-06-27) on every request by default, so a long tool-heavy native session sheds stale tool results before it exhausts context instead of relying solely on compaction or a restart. The request asks Anthropic to clear once a turn crosses 100,000 input tokens, keeping the 3 most recent tool_use/tool_result pairs intact and requiring at least 5,000 tokens be reclaimed for a clear to fire at all (these mirror Anthropic's own documented defaults; see Context editing). If Anthropic ever rejects the beta or field with a 400 naming it, zirv retries that one request without it and disables it for the rest of the process, with a single diagnostic line — it never turns a request that would have succeeded into a failure. When the server reports context_management.applied_edits, zirv surfaces it as a provider stream event.

[policy].context_editing is true (the default) or false (never request it). A repository layer may narrow it to false and can never widen it back — see Trust boundary.

Route profiles#

Every configured route binds to a route profile: the versioned record of one vendor route's documented base URL, request path, credential class, capability caveats and permitted provider-native options. zirv ctx provider list prints the whole registry and the profile each route bound to. A route whose vendor has no profile is refused at load time rather than sent to a guessed endpoint.

The primary families keep their own transports (anthropic, openai, google, google-vertex). Everything else speaks one of two:

FamilyProviderVendorProtocol
DeepSeek, xAI, Qwen (DashScope), Moonshot/Kimi, Mistral, Zhipu/GLM, MiniMax, Meta (Llama API)openai-compatiblethe vendor slugchat completions
Ollama, LM Studio, vLLMopenai-compatibleollama / lmstudio / vllmchat completions
Azure OpenAIazure-openai(fixed)chat completions
Amazon Nova and every Bedrock-hosted familyaws-bedrockthe vendor slugBedrock Converse

A vendor with a documented base URL does not need one configured:

toml
[endpoint.deepseek]
provider = "openai-compatible"
vendor = "deepseek"

[account.deepseek]
provider = "openai-compatible"
credential = "env:DEEPSEEK_API_KEY"

[route.reason]
account = "deepseek"
endpoint = "deepseek"
model = "deepseek-v4-pro"

[route.reason.extensions]
temperature = 0.2

[route.<id>.extensions] carries provider-native request options and is validated against the profile's typed allow-list: an unknown key, a wrong type or an out-of-range value is a load error that names the accepted keys. There is no free-form passthrough, and an extension can never overwrite a protocol-owned field such as messages or tools.

A compatible endpoint is not a feature superset of OpenAI's. Capabilities are declared per profile, and a request asking for something the profile does not declare -- tools, a reasoning-effort control, a thinking configuration, prompt caching -- is refused with a typed failure instead of being silently stripped. Reasoning text a chat-completions endpoint emits (reasoning_content) is streamed for display but never replayed as continuation state, because it carries no signature.

Local models#

ollama, lmstudio and vllm routes default to their runtime's own loopback address (127.0.0.1:11434, :1234, :8000). Their credential class is "none" or "optional": no key is fabricated for them, and declaring one on a key-less local route is an error rather than a secret sent to a local server. Plain http:// is accepted only for a loopback or private address (a literal one -- a hostname is never assumed local); a credential is still never sent in the clear to a non-loopback host.

Cloud routes#

aws-bedrock requires account.<id>.region and signs every request with SigV4; its credential is a JSON object rather than a bare key, because a signature needs a key pair:

toml
[endpoint.bedrock]
provider = "aws-bedrock"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
vendor = "anthropic"

[account.aws]
provider = "aws-bedrock"
credential = "store:aws-bedrock"   # {"access_key_id":"…","secret_access_key":"…"}
region = "us-east-1"

[route.bedrock-sonnet]
account = "aws"
endpoint = "bedrock"
model = "claude-sonnet-5"

azure-openai requires account.<id>.api_version and a per-route deployment; the request is addressed at {base}/openai/deployments/{deployment}/chat/completions?api-version=… with an api-key header, and carries no model id, because on Azure the deployment names the model. Each identity field is required by exactly one provider and forbidden on every other, so a Vertex project, a Bedrock region and an Azure api_version can never be read as one another.

Broker subscriptions#

GitHub Copilot and Factory/Droid resell models under their own subscription identity, with no documented, separately authorized direct API. Their profiles are legacy-only with that reason recorded, configuring one as a native route is refused, and they remain available through their harness backend. zirv ctx provider list distinguishes this from planned, which means zirv has not written an adapter yet -- the two are never conflated.