Harness Runtime
Zuno assembles an agent from a native harness profile. A profile is a set of bundles, and each bundle contributes typed components to one scoped runtime.
Runtime model
Componentis the lifecycle unit.prepareis side-effect-free: it stages typed services, requirements, and deferred effects in aPrepareContext.- An effect starts only after the complete candidate composition has prepared. Its start returns the exact asynchronous disposer that must prove quiescence.
ProfileBundlegroups components that are installed and replaced together.HarnessProfileis the complete composition selected for a session.HarnessRuntimeownsProfile,Session,Agent, andTurnscopes. A child scope inherits services and may override them locally.AgentDriverowns the turn-driving policy. The default driver wraps the standard agent loop; benchmark, workflow, remote, and evaluation harnesses can install another driver without modifying that loop.ToolManifestis the profile's model-visible tool surface. The registry filters all built-ins, including automatically assembled file tools, through this manifest.ToolContributionscarries nativeToolimplementations owned by the profile. Contributions are assembled after built-ins and before MCP tools, pass through the same visibility rules, and may intentionally replace a built-in by wire id.- Native executable values remain typed services. A coordinated named plane projects stable keys, schema contracts, provenance, owner, generation, and availability for dynamic consumers without putting executable Rust values in a string map. Tool contributions publish their provider-visible schema digest;
orchestration_capabilities_bundlepublishes the typed immutable snapshot plus Agent Profile, Workflow Template, and source-isolated Skill descriptors.
Profile activation is transactional and exclusive-resource safe. Candidate components prepare against a staging service view, duplicate identifiers and missing requirements fail before any effect starts, and no candidate service is visible outside the transaction. Replacement first withdraws local services and stops the old composition in reverse order. Only a proven-clean stop permits the candidate effects to start and their services to publish atomically. Candidate startup failure cleans the partial candidate and restores the previous definition through a fresh prepare/start cycle.
Cleanup failure or timeout is never reported as success. The runtime becomes Failed or Uncertain, retains typed lifecycle diagnostics, and refuses a second composition that could overlap the unresolved resource. Repeated shutdown preserves that terminal outcome. Parent shutdown closes child scopes first; parent recomposition rejects a still-live child consumer rather than silently leaving it bound to stale services.
RuntimeSnapshot and ComponentSnapshot expose lifecycle state, effect ids, provided/required service types, and scrubbed diagnostics without coupling a client to the runtime implementation. The TUI projects this inventory today; the same value is available to future server, ACP, and GUI surfaces.
Agent and prompt contracts
Agent prompts define role ownership, negative boundaries, a small amount of role-specific method, and an output contract. They do not repeat the runtime manual. Shared execution policy is generated by the host from the final provider-visible tool set after request hooks have been constrained to a subset. The generated developer instructions use stable ids and sources:
| section | purpose | presence |
|---|---|---|
runtime.intent | Follow the current user request or delegated objective without inventing broader authority. | Always. |
runtime.execution | Choose the smallest coherent workflow, batch independent reads, avoid unchanged re-reads or repeated checks, keep tool-driven work visible, and stop using tools once the outcome and evidence are complete. | Always; tool communication and termination guidance are added only when tools exist, and Plan guidance only when plan_update exists. |
runtime.sandbox | State that Shell is using host authority, including requested/effective mode and the typed reason that confinement was unavailable. | Only while a trusted unavailable-sandbox fallback is active. |
runtime.editing | Preserve unrelated changes, edit the owning abstraction, and inspect uncertain side effects before retry. | Only when an effective edit/write surface or workspace-writing Shell exists. |
runtime.verification | Require observed evidence and disclose blockers or unverified claims. | Always, with wording adjusted when no tools are available. |
runtime.delegation | Require bounded non-overlapping delegation and durable result reconciliation. | Only when task and at least one valid target are effective. |
runtime.persistence | Treat Goal, Plan, Todo, inbox, and Job state as authoritative continuation state. | When durable work state is active or its tools are effective. |
Each section is recorded with source zuno-runtime:<section-id>, exact content, estimated tokens, and a SHA-256 digest. A prompt cannot describe an editor, delegation target, or durable-state tool that was removed by role policy, allowlists, permission visibility, a provider capability, or a request hook.
After request hooks, runtime context, replayed history, attachments, and the final tool schemas have been applied, the engine estimates the complete provider-visible input. When the resolved model has a known context limit and that aggregate estimate exceeds it, the turn fails with a typed prompt-assembly error before provider I/O. Zuno does not make the request fit by truncating instructions, history, selected Skills, or tool schemas. An unknown model context limit leaves this final enforcement to the provider.
The built-in role prompts remain intentionally small:
orchestratorhandles one clear action directly and constructs a dependency graph only when bounded specialization or parallelism has value. It owns integration, conflict resolution, and the final audit.buildowns one end-to-end implementation lane and cannot delegate.deepowns reproduction, ranked hypotheses, causal tracing, root repair, and recovery verification without recursive delegation.planis read-only and produces a decision-complete plan from observed facts, necessary decisions, implementation design, and acceptance evidence.- Specialists use concise natural Markdown and may use
Outcome,Evidence,Inspected/Changed, andRisks/Blocker; the model is not required to invent a JSON or XML report protocol.
Durable planning is host policy rather than model ceremony. Before the first provider request for a user or resolved-command input, the host applies one deterministic classifier shared by CLI, TUI, ACP, server, and child turns. An active Plan is retained. A completed Plan does not suppress later work: a new multi-stage user objective appends a new epoch while preserving prior completed steps. Child reports, steering, and retry continuations never manufacture a new Plan.
A direct answer, one bounded read, or one short commit of already-prepared changes may proceed atomically. Other ordinary engineering work receives an Agent-specific seed Plan before the model sees the request. Typed image, resource, selection, or branch-diff context also selects the planned path, as does sufficiently large multi-block text. This makes research → modification → verification visible by default and guarantees a Plan for cross-component work, delegation, multiple gates, or work that may need compaction or restart recovery.
The model may refine the seed through plan_update; it does not decide whether the request receives durable execution state. Todo items are optional concrete detail beneath Plan steps for ownership, dependency, or recovery tracking. They must not mechanically mirror every Plan step. Refinement preserves existing step ids and completed states while updating titles/statuses or appending new steps, so concurrent clients and recovery snapshots retain stable identities.
Plan and Work are also typed collaboration contracts. collaboration.mode is a runtime-trust prompt block, separate from the native kernel, agent role, project instructions, work state, and user input. Plan tells the model to inspect and update durable planning state without product mutation; Work tells it to execute against the durable Goal, Plan, Todo, Job, and queue projections. Neither block grants capabilities or authorizes a mode transition by itself.
Static tool descriptions live in dedicated text files and are byte-pinned by the prompt golden test. Prompt changes are therefore reviewed as model-visible behavior, while schemas, permission policy, replay policy, and execution remain independently testable.
An Agent has no implicit turn-step ceiling. agents.<name>.steps may explicitly set a positive maximum number of tool-capable provider steps. Reaching that limit closes tool authority and permits exactly one additional text-only provider request with a host instruction to summarize completed work, remaining work, evidence, and blockers. The finalization request and instruction digest are persisted in session.provider.request.1.stepLimitFinalization. If that single finalization still cannot terminate cleanly, the turn ends with a typed step-limit failure.
Zuno does not inject a convergence instruction after an arbitrary number of tool calls. Tool-capable prompts instead require a short preamble before a substantial batch, concise progress updates at meaningful milestones, and a specific evidence gap before more tool work. Once the outcome and evidence are complete, the Agent must stop calling tools and answer. The provider-driven loop continues while the model requests follow-up work, subject to interruption, context management, durable goal state, and an optional operator-configured step ceiling.
Extension packages and executable plugin hosts
Zuno exposes one validated package protocol for agents, slash-command workflows, skills, and runtime tools. It adapts DSH's lifecycle outcome without loading the Cordis/JavaScript ABI or a Rust dynamic library:
extension_definerecords an immutable package in the current process and worktree scope.extension_runvalidates the desired package set and stages a pending revision.extension_stopstages removal of contributions while retaining the definition.extension_undefineremoves an inactive definition immediately or stages removal of a running definition.extension_inspectprojects static and process-local package state.
Staging never changes the committed catalog. Every live host owns a CompositionLease for one workspace-local revision. A transition can reserve the pending revision only after all old leases are gone; reservation blocks late old consumers. The candidate host then starts against the desired catalog and commits the exact transaction. Only that commit publishes Running and advances the active revision.
The TUI performs this transition as an in-process remount. The server serializes host acquisition with transition reservation and lets the last old request host publish the candidate. Both paths rebuild the agent catalog, command registry, skill catalog, prompt provenance, permissions, and tool definitions together. Clean candidate preparation/start failure explicitly aborts the transaction and restores the prior registry state. A cleanup result that cannot prove quiescence marks the workspace composition Uncertain and prevents further mutation until the process is restarted.
Process-local definitions are held only by StartupEnvironment's shared ExtensionRegistry; a new process starts with an empty registry. Static packages live at .zuno/extensions/<id>/extension.json or ~/.config/zuno/extensions/<id>/extension.json, are loaded at composition startup, and require the directory name to match the package id. Dynamic and static packages use the same zuno.extension/v1 schema and contribution merger. Duplicate package ids or duplicate agent/workflow/skill/tool names across active extension packages fail instead of silently choosing a winner. An agent contribution cannot rename its map identity or mark itself disabled.
Static packages may declare one executable runtime:
kind: "wasi"loads a Component Model artifact through Wasmtime. Workspace read/write, sockets, and exact environment names are explicit grants. Fuel, linear memory, instance resources, wall time, cancellation, and shutdown are bounded.kind: "process"starts a contained executable speaking Zuno's bounded line-delimited JSON-RPC protocol. It must declarehost.full, because an OS process cannot enforce narrower host authority.
Here “contained” means that the lifecycle host owns and attempts to reap the child process tree; it is not an OS security boundary. A host.full package is fully trusted and must itself be sandboxed at the deployment boundary when its code is not trusted.
All package hosts initialize before their tool routing is published. Unload withdraws routing and stops hosts in reverse order. Timeout, protocol loss, or a cleanup result that cannot prove quiescence becomes Uncertain and is never replayed. Process-local definitions reject executable runtimes; install persistent packages with zuno plugin add|update|remove|list.
Extension tools use the native effect, strict-authorization, replay, concurrency, and UI-intent pipeline. Version 1 keeps runtime calls exclusive, defaults effect to side-effecting and replay to never, and permits safe replay only when a WASI capability envelope itself excludes network and workspace writes. Process tools are always side-effecting and non-replayable because host.full cannot enforce a read-only claim. A runtime with no tool consumer is rejected.
Configured and extension agents are not prompt-only aliases. Agents whose mode is subagent or all join the exact task target roster and retain their configured model, variant, prompt, and permissions in the child turn. File, network, and environment access comes through the same native tools and permission rules as built-in agents: read/glob/grep/lsp, edit, webfetch/web_search, and shell. shell inherits the Zuno process environment and host visibility and therefore remains a side-effecting, approval-governed capability. A workflow that requires one custom agent explicitly calls task with that agent and a complete typed delegation contract.
Providers, drivers, approvals, and arbitrary typed services remain trusted compiled Rust Component implementations mounted through a HarnessProfile. See plugins, custom agents, and workflows for manifests, capability tables, protocols, and runnable examples.
Native agents
The built-in catalog separates primary modes, delegable specialists, and hidden engine agents:
| agent | role |
|---|---|
orchestrator | Default multi-agent delivery owner and the only native agent that may delegate. |
build | Direct end-to-end implementation in one lane, with all subagent tools withheld. |
plan | Read-only repository research and implementation-ready planning. |
deep | Directly selectable or delegable deep debugging and cross-cutting implementation, without recursive delegation. |
fixer | Focused local implementation with minimal change and regression scope. |
general | Bounded miscellaneous execution when no narrower specialist owns the work. |
explorer | Read-only repository structure, definition, caller, and impact discovery. |
librarian | Current external documentation, release, and upstream research. |
oracle | Read-only architecture review, root-cause analysis, and explicit trade-off advice. |
looker | Visual artifact inspection when a vision-capable model is available. |
compaction, title, and summary are hidden engine agents. A user-defined agent may be declared under agents.<name> or as Markdown under .zuno/agent/**/*.md; it enters the same resolution, permission, prompt, and provenance pipeline as a native agent.
Agents have no fixed provider-step ceiling by default. A user who needs a deployment guard may set agents.<name>.steps to a positive integer:
{
"agents": {
"orchestrator": {
"steps": 200
}
}
}2
3
4
5
6
7
The configured number limits tool-capable provider iterations, not the total lifetime of a goal. If the final permitted iteration still requests continuation, the engine issues exactly one additional request with an empty tool list and a volatile developer instruction to report what completed, what remains, and any evidence or blocker. The session.provider.request event persists that exact instruction and its digest under stepLimitFinalization, so replay can reconstruct why the request was text-only. A provider that still emits tool calls cannot extend that turn; the protocol failure becomes a typed StepLimit recovery.
See agent orchestration and model routing for the exact delegate roster, per-Agent and preset model routes, reasoning precedence, background report delivery, configured workflow DAGs, and Council.
User-facing agents answer in natural Markdown. Zuno does not require XML-like reply envelopes unless a typed runtime consumer exists for that exact structure. The built-in prompts emphasize intent matching, deliberate tool use, scoped changes, proportional verification, and concise outcome-first reporting. In particular, self-contained reasoning or writing does not justify a shell call or a throwaway file.
Delegated session projection and approval routing
A native delegation runs in its own child TurnHost; the parent tool waits for the foreground result but does not own the child's event channel. Interactive composition installs a ChildTurnObserver that first receives the child's durable replay and resolved identity, then every live TurnEvent. The TUI folds those records into a per-session read model and attaches views to that model. Switching the visible session never remounts or aborts the parent or a sibling host.
An attached native child is also an independent input target. The TUI sends its durable session id with the submission instead of routing the text through the parent transcript. The child inbox commits the text before delivery. A running child receives a soft steer; an idle or completed child acquires a run lease and reopens a TurnHost with the resolved Agent, model, effort, and inherited orchestration identity captured for that child. The SessionWakeCoordinator closes the active-to-idle race, so input that misses the running turn remains pending and starts the next child turn. Delivery belongs to the workspace supervisor and is cancelled with that lifecycle. A direct child continuation updates only the child session; it does not fabricate a report or another input for the parent.
Permission attribution comes from immutable coordinates captured by ToolContext, not from a broker-wide current-session slot. A root or child request therefore carries the session, assistant message, and provider call that raised it through every rule and human approval layer. The foreground TUI broker serializes all such asks, scopes standing grants to one session, and fails closed by rejecting pending requests when its last surface or wake channel closes.
ACP consumes the same child observer without creating another child loop. The stable projection is always the task tool card. A client that directly negotiates the draft subagents capability additionally receives foreground child replay and live events on the durable child session id, with spawn and terminal state on the direct parent route. The parent prompt response waits for the child projection queue to drain. Historical children replay as disconnected; background children remain durable jobs.
Child permissions and questions retain their immutable origin. A negotiated native ACP client receives them on the child route. A compatibility client receives them on the declared root route with the child id in typed metadata. Session-level permission grants are owned by that root and survive host replacement, but session/close clears them and cancels and joins only background jobs owned by the closing root.
Child capability authority and Skill loading
A child resolves through the same configuration, model catalog, MCP catalog, Skill discovery roots, permission ceiling, and sandbox configuration as its parent composition. In the absence of a per-Agent or preset route, the child inherits the parent session model and reasoning choice. The model-facing task surface cannot override model, effort, category, MCP, Skill, or sandbox policy; configured host workflows use the same validated routing layer without adding those fields to the model contract.
A native child does not recompute an independent tool superset from the current configuration. The parent Attempt persists the exact provider-visible tool schemas used for its model request, and child resolution treats that frozen set of ToolSchemaIdentity values as its authority ceiling. Matching only a wire id is insufficient: a same-named tool whose provider-visible schema changed is not inherited from that Attempt.
The target Agent role, its extension-tool policy, the configured exact tools allowlist, and the effective global, user, and Agent permission rules are then intersected with that ceiling. Each layer may hide or deny more tools; no later allow or permission.mode: "allow_all" can restore a tool schema that the parent model did not receive. allow_all affects HITL prompting, not capability construction.
prepare_request hooks run after the registry snapshot is locked. They may remove or reorder tool schemas, but the engine rejects any added, replaced, or duplicated schema before provider dispatch. The durable Attempt therefore records the exact post-hook set without allowing the hook seam to widen registered authority.
MCP and extension tools therefore do not flow unconditionally into every child. An exact schema must be present in the parent Attempt and no later allowlist or explicit deny may remove it. Work-capable native roles may opt into automatic extension inheritance. Read-only roles deliberately do not inherit arbitrary dynamic tools automatically; an operator may still grant one audited tool id with an exact per-Agent permission rule. This avoids treating every unknown MCP operation as read-only merely because the receiving Agent is read-only, without making a safe repository query impossible to authorize.
Skill loading is separate from tool authority. Each initial or resumed child host independently discovers the Skill catalog from its own working directory, configuration layers, mounted profile, and active extensions. A Skill body already loaded by the parent is not copied into the child prompt. Instead, agents.<name>.requiredSkills names instruction sets that must resolve, after Agent and profile visibility filtering, to exactly one source. Before each provider-bound input, Zuno ensures those exact sources are loaded and de-duplicates sources already present in the durable prompt. A missing name or multiple visible sources with the same name fails child startup; Zuno never silently picks the first discovery result.
For example, a code-retrieval Agent may declare requiredSkills: ["codegraph"] so every child turn receives the CodeGraph operating instructions. That declaration grants no executable capability. CodeGraph MCP tools remain available only when their exact schemas are inside the parent Attempt ceiling, the role either inherits extension tools or has exact per-Agent grants, its tools allowlist retains them, and no effective permission rule denies them.
This authority model is informed by Codex's child-from-parent-effective-capability design. Codex is a design source, not a compatibility target: Zuno does not promise Codex configuration, role, MCP, Skill, wire, or runtime semantics.
Typed delegation contract
The model-facing task tool no longer accepts loose description, prompt, or load_skills arguments. Its required work agreement is:
{
"objective": "Locate the prompt receipt ownership gap",
"deliverable": "A call path and minimal affected-file set",
"instructions": "Inspect only; use structural code navigation.",
"success_evidence": "Name the owning symbols and distinguish facts from inference.",
"scope": {
"include": ["crates/zuno-engine", "crates/zuno-cli"],
"exclude": ["credential stores"]
},
"constraints": {
"must": ["Preserve unrelated changes"],
"must_not": ["Edit files"]
},
"dependencies": ["The CodeGraph index is current"],
"agent": "explorer",
"background": true,
"reportDelivery": "nextStep"
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scope, constraints, and dependencies are optional. agent selects one member of the effective delegate roster. task_id resumes an existing child session owned by the same parent. Unknown fields and removed loose arguments, including description, prompt, subagent_type, category, model, effort, and load_skills, fail validation; there is no compatibility translation.
Prompt provenance
Prompt assembly is ordered data, not string concatenation spread across the CLI. Every section has a stable identifier, source, exact content, and SHA-256 digest. Present sections are sorted into stable semantic lanes:
- kernel, when a profile contributes one;
- native or configured Agent role;
- typed Plan or Work collaboration mode;
- capability-derived runtime policy;
- global instructions;
- project, configured, and nearby instructions;
- Goal and other model-visible work state;
- extension and routing policy;
- selected Skill bodies;
- the bounded Skill metadata index;
- memory.
The trigger policy makes a named or clearly matching skill a pre-action requirement. The base prompt carries bounded name, description, and source metadata; descriptions are shortened before a source identity is omitted. skills.maxContextTokens overrides the default two-percent catalog budget, while skills.includeInstructions: false disables catalog injection.
Fully selected Skill bodies have a separate aggregate budget. By default it is ten percent of a known model context, with a 2,000-token floor and a 32,000-token ceiling; an unknown context uses 8,000 approximate tokens. skills.maxSelectedContextTokens overrides the derived value while retaining the ceiling. Loading or restoring a body that would exceed the aggregate budget fails before the provider request; Zuno does not silently omit part of a selected Skill or reuse the metadata budget for full instructions.
The skill tool pages the complete catalog with list, searches it with search, reads a selected body with load, and resolves relative text with read_resource. Same-named sources remain distinct and require the advertised source locator. Reads use content-bound cursors and must continue to completion; disk bodies are read after selection rather than retained for the process lifetime.
Skill discovery is Zuno-owned. It advertises project .zuno, .agents, and .claude roots before Zuno's user-global config and user-global Agent Skills, then configured paths and pulled URL caches. .agents precedes .claude within the same scope. Zuno does not scan OpenCode directories. Canonical paths are de-duplicated, including symlink aliases, but same-named files from distinct sources remain separate identities that require source-qualified selection. Discovery order controls presentation and provenance; it does not silently choose a same-name winner.
A visible Skill whose name is unique across sources and does not collide with a real command is also advertised as /<skill-name>. A bare invocation loads the complete body, emits the loaded projection, and does not create a model turn. Supplying arguments loads the Skill first and then admits the exact canonical slash text as user input. Real commands always win; ambiguous names remain available through /skills and source-qualified skill operations.
Reusable workflows belong in Skills by default. A Markdown command remains useful for a literal prompt template or a short argument-expansion macro, but it has no resource bundle, implicit trigger, or authority of its own. The first-party ui-design workflow is a Skill and therefore receives a direct slash entry while its name remains unambiguous. Named organization-specific review and release policy is user owned: users may define dual-review, auto-release, or other named Skills in global or project Skill roots, but Zuno does not compile those policy bodies into the binary. The generic balanced-review council remains a reusable synthesis primitive and does not prescribe either workflow.
Repository instruction initialization
The command registry seeds two Zuno instruction workflows:
/init [focus...]creates or improves the repository-rootAGENTS.md. It is the compact choice for a repository whose guidance does not need scoped overrides./init-deep [--create-new] [--max-depth=N] [focus...]maps the repository with CodeGraph first, then creates or updates the root file and adds scopedAGENTS.mdfiles only at real responsibility, build, language, or deployment boundaries. A scoped file contains only rules that differ from its parent; it must not duplicate inherited guidance.
Both workflows preserve accurate existing content and treat remaining arguments as user priorities. By default /init-deep may improve existing files and create missing ones. --create-new leaves every existing AGENTS.md unchanged and only creates missing files. --max-depth=N counts the repository root as depth zero and prevents inspection or scoped-file creation below N; the root file remains in scope.
Before each provider request, the loop persists session.prompt.assembled.1. The event records the ordered sections, semantic role, trust, priority, source, byte and token estimates, content digest, provider system/developer projection, and the actual post-hook projection when it differs. Each session.provider.request.1 started event points to the exact receipt through promptReceiptID. Identical final projections reuse their receipt id within the turn, including an A -> B -> A sequence.
Prompt and Agent diagnostics
Inspect the receipt used by the latest provider request in one session:
zuno debug prompt --session <session-id>
zuno debug prompt --session <session-id> --step <non-zero-step>2
Without --session, the command prints the latest prompt receipt in the database. With a session, it first resolves the latest or selected session.provider.request.1, then follows promptReceiptID; it does not guess from a matching step stored in another receipt. Prompt bodies, system/developer projections, and the post-hook system prompt are redacted by default while section ids, sources, sizes, roles, digests, session ids, and event ids remain visible. --show-sensitive reveals exact AGENTS, Skill, memory, runtime, and hook-transformed model input and must not be pasted into an issue or log without review.
Inspect current configuration-time resolution for one Agent:
zuno debug agent deepThe command uses the real TurnPlan and McpRuntime resolvers without creating a session or contacting the model provider. It connects every enabled MCP server, records lifecycle state, discovery status, connected servers, exact current tool schema identities, warnings, and cleanup warnings, then closes every transport before returning. Discovery failure, cancellation, or timeout also runs bounded transport cleanup. Discovered tools are evaluated against the current role rules and Agent allowlist. A root diagnostic has no parent Attempt authority to invent; delegated historical authority must be read from that Attempt's persisted orchestration snapshot. If no MCP runtime exists, inheritance remains not-connected rather than being tested with fabricated ids.
The output also reports effective model, reasoning and selected variant, policy-visible and unavailable tools, delegates, sandbox readiness, and Skill catalog counts, metadata/body budgets, bounded preview coverage, and ambiguous names. Interactive question still requires a client asker, and a later request hook may only narrow the final tool set.
Provider request routing context
Foreground requests carry a private, typed ProviderRequestContext beside the model-visible request. A root turn uses MainTurn with its durable session id; a delegated child uses ChildTurn with the child's own durable session id. Every continuation in one tool loop reuses that same context, and resuming the durable session after a process restart reconstructs the same identity.
Title generation, lifecycle summaries, compaction, memory reflection, and Council synthesis use explicit isolated purposes with no foreground-session identity. This prevents lifecycle work from joining either the root or a child provider conversation.
Only an OpenAI Responses wire surface projects the typed identity, as metadata.zuno_session_id. Both the official OpenAI adapter and the compatible adapter used by a custom OpenAI baseURL implement the same projection. Chat Completions and Anthropic Messages do not receive a fabricated equivalent. Unrelated object-shaped metadata is preserved, while metadata.zuno_session_id is reserved and cannot be replaced through provider options or request parameters. The routing context remains private to CompletionRequest, so ordinary request hooks cannot mutate it or move it into prompts, headers, or tool definitions.
Each foreground session.provider.request event records requestPurpose, affinityAttached, and, when attached, affinitySource: "durable-session". It does not persist the raw routing identity, credentials, or upstream account and conversation identifiers.
Provider timeout and retry boundaries
An active provider request and recovery after a failed request are separate lifecycles. The retry recovery budget starts when the first retryable provider failure is observed. It bounds rollback emission, backoff, and admission of a later replay, but never interrupts the first request or an already-running replay. Cancellation continues to interrupt either operation through the turn's control signal.
OpenAI-compatible error frames retain structured stream and protocol codes. upstream_stream_error, upstream_stream_incomplete, upstream_stream_idle_timeout, and request_deadline_exceeded are typed replacement-safe stream failures. They may discard partial text, reasoning, and unfinished tool calls before replaying the unchanged request. Protocol codes such as upstream_protocol_error, invalid_upstream_reasoning, and invalid_upstream_tool_call are terminal. The legacy generic upstream_error remains terminal because it mixed both recovery classes. An opaque transient error after partial output is also terminal for that request; only the structured stream variant authorizes replacement.
Every provider call in the bounded recovery sequence has a durable session.provider.attempt.1 lifecycle. Its started and terminal events share attemptID and requestID and record the attempt number, maximum attempts, terminal status, whether partial output existed, the typed provider code when present, and whether that code permits partial-output replacement. The enclosing session.provider.request.1 remains the logical step lifecycle. Replays clone the original CompletionRequest, including its private durable-session affinity, and the engine never adds the failed partial assistant output to history.
Tool execution begins only after the successful assistant checkpoint, so a failed streamed tool call cannot dispatch a side effect. ACP is append-only and cannot retract an already published text chunk. Its live projector therefore holds provider text, reasoning, and pending tool updates until AssistantCheckpointed; RetryRollback clears the provisional attempt and only the replacement attempt is published. Other clients may continue consuming the engine's lossless live events directly.
OpenAI-compatible transports resolve three independent provider options:
timeout: a whole-request deadline in milliseconds, orfalsefor none;headerTimeout: the maximum wait for HTTP response headers in milliseconds, orfalsefor none;chunkTimeout: the maximum silent gap between streamed body chunks in milliseconds.
The whole-request deadline spans headers and body. Header timeout ends after the response arrives; chunk timeout restarts after every received chunk. When more than one deadline applies, the earliest one wins and produces a typed transient provider error naming the phase. OpenAI-compatible providers default to a 330-second response-header timeout and a 120-second streamed-chunk idle timeout; headerTimeout: false explicitly disables the former. Provider-specific gateways should set their own upstream deadline below Zuno's matching phase deadline so their typed error reaches Zuno before the client cancels the connection.
Auditable memory and reflection
Resident memory has one mutation boundary: memory_propose. Foreground agents and the isolated post-delivery reflection fork both use that tool, which validates the requested add/replace/remove operation and inserts a durable MemoryCandidate; it never edits the resident file directly. Candidates retain scope, action, reason, confidence, source session/message, timestamps, diagnostics, and exact before/after snapshots.
The default promotion policy is review. high_confidence applies candidates at or above the configured threshold, while automatic applies every validated candidate. All policies use the same durable state machine:
pending -> applying -> applied -> undoing -> undone
\-> rejected
\-> failed / uncertain2
3
applying and undoing are written before touching the file. After process loss, the runtime compares the resident file with both stored snapshots and marks the observed result; it never replays the write or undo. Any third state becomes uncertain and requires user inspection.
Reflection runs only after a final response was delivered and uses an explicitly configured reachable small_model. Zuno persists the exact review prompt, replayed durable turn transcript, current resident-memory snapshot, tool schema, model identity, digest, and terminal outcome as memory.reflection.request and memory.reflection.outcome. Stream truncation, malformed arguments, denied tools, and proposal failures are durable failed outcomes. The fork can call only memory_propose; it cannot reach shell, files, normal tools, or foreground conversation state.
Periodic cadence is admitted from a durable per-session delivery sequence rather than a process-local counter. The source assistant message is counted once across host rebuilds and restarts. A selected review owns a leased durable job; process loss changes an expired job to uncertain and never replays its model request. The reviewer compares the supplied resident snapshot before proposing changes, prefers replacement to duplicate additions, and can organize memory only through the same audited add/replace/remove candidate workflow.
Candidate validation rejects prompt injection, credential literals, ambiguous locators, over-budget results, and external file drift. Automatic learning is limited to durable user facts, explicit corrections, repository rules, and verified reusable recovery knowledge. It cannot rewrite code, prompts, agents, extensions, or skills. /memory is the user-owned review and correction surface. See auditable memory and reflection.
Durable inputs
Every model-visible external input is admitted to the session event log and durable inbox in one SQLite transaction before execution is attempted. The inbox is the source of truth across active turns, idle sessions, process restarts, and competing drivers.
Every multi-statement write transaction reserves SQLite's writer with BEGIN IMMEDIATE, including transactions opened through a caller-owned turn connection. This lets the configured busy timeout serialize concurrent parent and child writers before any read snapshot is taken; Zuno does not use a deferred read-then-write upgrade that can fail with SQLITE_BUSY_SNAPSHOT without invoking SQLite's busy handler.
An interactive SessionChoice::New is prepared without inserting a session row. The process-local identity is stable across model, agent, MCP, and theme changes, but opening, browsing, or leaving the welcome screen creates no durable session. The first model-bound submission inserts the session and its user message in one transaction, then emits session.materialized for clients. Existing and continued sessions still hydrate immediately. The TUI /new command selects another prepared SessionChoice::New in the same terminal activation. It opens an empty conversation shell directly instead of returning to the launch welcome surface, and it does not bypass this lazy materialization boundary.
Drivers promote inputs in FIFO order. Promotion is transactional and can target one input identifier for a live soft interrupt. A malformed input records a session error and does not strand later queue entries.
User prompts and subagent reports share this protocol:
- An active parent receives a soft interrupt and promotes the report at the next tool-safe point.
- If the report misses the final safe point, the wake coordinator waits for the active lease to end and starts another turn while the input is still pending.
- An idle parent is claimed and driven immediately.
- A restarted process recovers pending reports from the durable inbox.
Interactive TUI input uses the same durable boundary. When idle, Enter starts a turn. During an active turn, Enter admits a FIFO queue item for the next turn; Ctrl+Enter is the explicit steer override and requests a soft interrupt at the nearest safe step boundary. Shift+Enter, Alt+Enter, and Ctrl+J insert a newline. The UI reports an item as queued only after SQLite commits it, and pending items can be edited or cancelled by revision and survive a process restart. Submission transport is a typed envelope with independent payload, delivery, and origin fields. The command palette's immediate-send action and input sent to a running child session also produce steer; ordinary busy input produces queue. Only text and typed rich-content payloads may steer. Commands, Skills, Council requests, and host commands are queued even if their UI gesture requested immediate delivery. The HTTP prompt API follows the same rule: omitting delivery means queue, while steer must be explicit.
User input is typed rich content, not only a rendered string. A local image is persisted before execution as a durable file part carrying filename, MIME type, data URL, and base64 payload, then reconstructed as a provider-neutral image block on replay. Root sessions, attached child sessions, direct sends, queued inputs, and steering use the same content path. The visible [Image #N] token is draft presentation state and is never treated as attachment identity. Bounded UTF-8 @file and zuno run --file inputs become explicit text context; supported images remain typed. See images and file references.
A provider stream or provider-retry delay is wakeable for explicit steering: Zuno checkpoints any partial assistant output with finish: steer, promotes the durable input, and starts the next model step without emitting TurnInterrupted. An executing tool is not abandoned merely to steer; its result reaches the next tool-safe point first. Commands and ordinary active-turn submissions target the FIFO queue. If the turn ends before a steer is consumed, the already admitted item remains pending and is promoted in FIFO order on the next turn; it is never lost or duplicated. The bounded in-process prompt channel is only a wakeup and handoff path, not the queue of record. A steer never fires the hard-interrupt signal and therefore cannot cancel a foreground task.
Tool-owned human input is projected separately from execution. A permission prompt reports awaiting approval; a structured question reports awaiting answer. Both surfaces replace the composer region rather than becoming another transcript card. Permission choices support Left/Right, the existing Up/Down aliases, Enter, and mouse selection; explicit expansion moves the prompt to the larger overlay. Questions show Question i/n, the remaining unanswered count, numbered choices, and a numbered Other input. They support Up/Down and j/k within a question, Left/Right and h/l across questions, number-key selection, Enter, Space for multi-select, and mouse selection. Per-question cursors and custom drafts survive navigation. Cancelling either interaction resolves the tool as a typed denial and never fabricates an answer.
The conversation surface separates reply identity from transient work state. The identity row contains the resolved agent, catalog model display name, and configured reasoning effort. It follows the bottom of a short assistant reply; once transcript content fills the available viewport, the same row becomes sticky immediately above the composer. The final row also repeats the current agent/model/effort as a neutral, prompt-adjacent badge so the next-turn selection remains visible while a turn is running; Tab updates that badge immediately while host replacement remains deferred to the turn boundary. It does not invent cost or speed multipliers when no authoritative runtime metadata exists.
The frame's final row is the live control surface. During a turn it shows one animation-clock-driven pulse, the resolved interrupt key, latest provider-prompt occupancy, command-list key, and current neutral agent/model badge. The first interrupt press changes that same row to its confirmation state, so the transcript and composer do not reflow. Permission and question waits replace the pulse with their explicit reason. When idle, the row returns to directory, context, and command discovery. Transient working rows are not inserted into the transcript; durable activity, errors, interruption markers, and assistant content remain reconstructable from session events.
Context occupancy is the most recent complete provider prompt divided by the catalog context limit. It is replaced on each provider report rather than accumulated across the session; cumulative disjoint token buckets remain available in the usage projection and sidebar.
Plan and Work transitions
/plan is the interactive mode switch. In Work mode it opens a confirmation in the same keyboard- and mouse-capable dialog system used by other TUI choices. In Plan mode it becomes the handoff path to implementation: a durable plan must exist, and the confirmation names its title, revision, and completed-step count. /start-plan enters Plan mode directly; /start-work performs the same durable plan check and confirmation without first toggling through /plan.
ACP publishes the same three names as native session commands. Because sending the slash prompt is already an explicit client action, ACP performs the transactional mode replacement directly, never sends the command to the model, and reports the result through standard current_mode_update and config_option_update notifications. Returning to Work still requires a durable plan.
Plan is enforced below the prompt by a deny-by-default capability overlay. It allows repository inspection, read-only LSP and search, questions, Skills, and typed Goal/Plan/Todo operations, while shell and file mutation remain denied. The model can recommend Start Work but cannot select it for the user. A confirmed selection is persisted as the session agent, so explicit resume and in-process session switching restore the collaboration mode without restarting the TUI.
Native session commands, compaction, and hard interruption
The typed SessionCommand registry is shared by client surfaces and currently contains /compact, /goal, /plan, /start-plan, and /start-work. Native discovery resolves before Markdown commands and Skills, so a same-named user workflow cannot shadow a runtime control.
/compact and /goal invoke shared live-TurnHost handlers in both the TUI and ACP. Compact runs the hidden compaction agent; Goal exposes show/history/create/edit/pause/resume/block/complete/cancel against the durable goal store. Neither surface sends the slash text to the model or synthesizes a private client-only result. Goal output is a typed session-command output event, not reasoning.
The command acquires the session's exclusive run ownership and emits typed started, output, completed, or failed lifecycle events. TUI and ACP consume those events directly; the HTTP event service commits their stable session.command.{started,output,completed,failed} projections before live delivery. The summary, retained tail, marker, prompt provenance, and usage are durable. Proactive compaction uses the validated compaction.threshold_percent of the usable model window and can be disabled with compaction.auto: false. A provider-confirmed context-limit failure retains its bounded recovery compaction, while a manual command is always eligible.
Compaction changes the provider transcript boundary; it does not delete the durable Goal, Plan, Todo/WorkItem, Job, inbox, event log, or prompt receipts. The active Goal is regenerated from SQLite for every relevant provider request. Selected Skill bodies remain in the mounted resolver and are restored from the latest durable prompt receipt when a host is reopened. Pending subagent reports remain durable inbox inputs and are recovered from the same row after restart. Every relevant provider request also regenerates a bounded runtime.work_state developer instruction from SQLite. It includes the current Plan revision and steps, Todo/WorkItem identities and dependencies, active or uncertain Jobs, terminal Jobs with an unconsumed nextStep report, pending report identities and states, and the latest prior prompt receipt id. One deferred SQLite transaction reads all of those tables from the same snapshot. Each collection is capped at 64 entries and the complete rendered section is capped at 16 KiB. Verbose text is UTF-8-safely shortened before whole tail entries are omitted; omitted counts remain explicit and authoritative identity fields are retained. Assembly fails closed if even those identity fields cannot fit. Typed tools remain the only mutation and full-detail query interface.
Historical image bytes are excluded from the compaction model request. The summary input keeps a stable human label such as [Attached diagram.png (image/png)], while the original durable file part remains unchanged for authoritative replay.
A hard interruption is a typed HardInterruptRequest carrying both source and reason. Sources distinguish TUI, ACP, HTTP API, and lifecycle teardown; reasons distinguish user cancellation, request cancellation, exit, shutdown, and session close. It is session-scoped and linearizable across turn handoff. If the previous run guard has dropped but an already admitted follow-up has not yet acquired its guard, the registry arms that next guard instead of discarding the interrupt. The first accepted request remains authoritative, so later shutdown cannot overwrite an earlier user action. The turn starts with its interrupt signal set, emits the normal terminal interruption event, and issues no provider request.
Within one mounted TUI session, model, agent, effort, and MCP changes may replace the TurnHost, but they must reuse the mounted session's SessionRunRegistry and rebind its SessionTitleSink. Cancellation controls therefore continue to target the current host generation, and generated title updates continue to reach the sidebar. Only a real session remount creates a new continuity scope.
After the TUI confirms a hard interruption, it keeps the stopping state visible and suppresses late provider or tool presentation until TurnInterrupted or TurnCompleted establishes the terminal boundary. Durable persistence and diagnostics still run; the client merely refuses to present post-cancel work as continued conversation. A side effect that completed before cancellation remains an observed result and is never mechanically replayed. A running tool receives a two-second cooperative cleanup window. Settling in that window produces a typed cooperative cancellation and preserves the tool's terminal report; expiry force-aborts the invocation and records forced plus uncertain, requiring authoritative-state inspection before retry. Post-tool hooks may add diagnostics but cannot rewrite either cancellation outcome as an ordinary failure. TurnInterrupted adds a separate session-owned Conversation interrupted by user. row to the live transcript. The source and reason are persisted on turn.interrupted and, when an assistant checkpoint exists, inside its typed abort error. TUI and ACP replay reconstruct cancellation as session state rather than assistant prose or a normal task failure.
Assistant checkpoints reconcile message usage and the session usage projection in the same transaction. Repeated checkpoints subtract the previous message snapshot before adding the new one. Provider accounting is persisted with each snapshot so cache tokens are counted exactly once; stored assistant rows without a reliable accounting mode remain explicitly unavailable instead of being reported as zero. The projection stores cumulative disjoint token buckets, the latest whole prompt, the context limit, and the latest accounting mode.
Durable goal recovery
An active goal uses two recovery layers. The provider request layer retries a bounded sequence in place and rolls back unpublished partial output before another request. Its in-place backoff is interruptible by both hard cancellation and durable live steering; waking it does not replay the stale provider request. If that sequence still ends in a recoverable error, the goal controller writes a goal_retry row before waiting and starts a fresh agent turn when its persisted deadline arrives. There is no cross-turn retry-count ceiling for recoverable failures: the delay grows exponentially, reaches the configured cap, and the goal remains active until it completes, is paused, reaches its token budget, or encounters a permanent failure.
The retry row is tied to the exact goal_id and stores the attempt, typed reason, selected delay, schedule time, and next eligible time. Reopening the same session reconstructs the wait from SQLite. Queued user input has priority over an automatic turn, and long waits are split by poll_interval_ms so an interactive surface can notice that input promptly.
Local delays use exponential backoff with symmetric jitter and never collapse to zero. A valid provider Retry-After value is never shortened by jitter; it is clamped to the configured ceiling rather than replaced by an earlier local delay.
{
"goal": {
"retry": {
"initial_delay_ms": 2000,
"max_delay_ms": 300000,
"jitter_percent": 20,
"poll_interval_ms": 250
}
}
}2
3
4
5
6
7
8
9
10
Recovery is selected from typed errors, never rendered messages:
- Transport failures, rate limits, incomplete streams, SQLite writer contention, and empty assistant messages schedule another goal turn.
- An explicit Agent
stepslimit normally produces a text-only finalization.StepLimitrecovery is reserved for a provider that attempts to continue with tools after that finalization boundary. - Context-limit failures compact retained history before retrying. Successful compaction is persisted as its own retry phase so a restart does not compact the same history twice.
- Authentication failures, user interruption, and a closed event consumer pause the goal for human action.
- Invalid provider protocol, unsupported typed input such as an image sent to a text-only model, unavailable agent/model configuration, corrupt durable state, and other permanent failures block the goal.
OpenAI and Compatible Responses decoders treat response.failed as a typed provider failure, not as an ordinary assistant MessageEnd(Error). When the event carries a structured error body, its type, code, and message remain in the error source chain for diagnostics while recovery still follows the typed ProviderError variant.
Tool effects and strict authorization
Authorization, replay, and concurrency are independent declarations. Every tool classifies each invocation as ReadOnly, UserMediated, Delegating, or SideEffecting; the default is SideEffecting, so an unknown harness or MCP tool fails closed. Mixed tools may classify from validated arguments: bg inspection is read-only and bg cancel is side-effecting. execute is delegating, and each child call passes through the same permission context with its own effect.
{
"permission": {
"mode": "strict",
"rules": {}
}
}2
3
4
5
6
Strict mode is off by default. When enabled, an explicit deny is evaluated first, then every side-effecting invocation requires a fresh attached-user approval even when a normal rule or plugin says allow. The ask cannot be satisfied by a standing grant or automatic approval and offers no "always" choice. TUI --auto yields to the human broker; headless surfaces deny the call. Approval covers the same tool's internal resource checks for that invocation only, while a later explicit resource deny still wins. Explicit danger-full-access changes the effective permission mode to allow_all; in that trusted native-execution mode no Zuno approval request is emitted, even if the authored permission mode is strict.
The shell's destructive-command gate is independent of strict mode. A protected target is denied, while a bounded deletion, a dynamic destructive target, or a redirect that would replace an existing path marks the ordinary shell permission request as human-only. Effective allow_all suppresses that confirmable request, while the gate's catastrophic outcome remains a direct denial. Permission rules still evaluate first, so an explicit deny remains terminal; a model-authored argument cannot approve its own operation. A new static redirect target inside the working directory or the OS temporary directory is creation rather than overwrite and does not receive this extra risk prompt. An exact, non-recursive forced removal of a statically named path that is currently absent below the OS temporary directory is likewise a no-op cleanup. This filesystem probe is advisory risk classification; actual confinement comes from the separately selected sandbox mode and backend.
Refusal is a typed lifecycle outcome rather than an execution failure. Malformed or unsafe arguments, unavailable tools, and permission denials emit ToolDispatchBlocked with invalid_arguments, unavailable, or denied before the model-visible error result is appended. Durable tool state retains outcome: "blocked" and blockKind, so clients can use warning treatment and state that the requested effect never ran. Process, transport, and tool implementation failures remain error outcomes.
Hard turn interruption is observed during tool hooks, permission waiting, and execution. Cancelling before permission resolves drops the pending approval future and guarantees that the tool body never starts; cancelling a running tool joins its cancelled task or process tree before the dispatch returns. A foreground task delegation carries the same interrupt through ChildTurnHost, converts it to the child runner's cancellation token, aborts the live child turn, and waits for event drain plus host shutdown before returning.
Tool execution is at-most-once by default. ToolReplayPolicy::Never is inherited by every tool unless the implementation explicitly declares Safe; current safe tools are read-only or idempotent inspection operations such as file reads, glob, grep, skill lookup, session search, job status, LSP inspection, goal status, and web search/fetch.
The loop never mechanically replays a call. It persists the failed tool result and gives it to the model in the next step, including timeouts that might have completed an external side effect before their response was lost. A later recovery turn receives a hidden, SQL-derived notice naming the retry attempt. A Safe failure may be attempted again after backoff; a Never failure requires authoritative inspection of the worktree or external state before the model decides whether another mutation is appropriate.
Tool overlap is a separate declaration from replay safety. ToolConcurrencyPolicy::Exclusive is the default; only implementations that declare ParallelSafe or IsolatedBackground may overlap. The dispatcher still resolves tools, validates arguments, runs hooks, and asks permissions in model order. It then executes consecutive non-exclusive calls under the configured bound and persists results in original call order, regardless of physical completion order. Shell, writes, unknown extension tools, and MCP tools without an explicit safety declaration remain exclusive.
The configured tool_calls bound is applied even when one parallel-safe group contains more calls than the limit. Exclusive is a two-sided barrier: every earlier ParallelSafe or IsolatedBackground call settles before it starts, and no later call starts until it settles. Physical overlap is bounded while durable results and client events remain in model order.
MCP lifecycle operations use the same bounded pattern across different servers, while operations for one server remain generation-serialized. LSP startup and requests may overlap across servers under one global semaphore; protocol ordering inside one client remains unchanged. Setting any bound to 1 restores serial behavior.
Native child sessions, workflow nodes, Council seats, and Codex/Claude Code ProductAgent instances share one process-local delegation budget for a workspace. The budget survives turn-host replacement within that process, so a background agent started by an earlier turn still consumes capacity. Reloading configuration adjusts the bound without cancelling active work: a lower bound waits for enough active delegations to finish, while a higher bound admits queued work. The queue is explicit and fair FIFO; later calls cannot barge ahead of existing waiters. Workflow maxParallel remains an additional per-workflow ceiling. Separate Zuno processes do not yet share a durable quota lease. Within that ceiling, the workflow scheduler is work-conserving: whenever one node settles, the next ready node is admitted in template order without waiting for slower siblings from the same wave. Durable/model-facing results remain in template order. If parent cancellation and final node completion become ready in the same scheduler tick, cancellation wins and the workflow cannot publish a false completed outcome.
Background native and product-agent jobs commit queued before waiting for a permit, then atomically transition to running immediately before invoking the runner. The TUI reports both states separately. On restart, queued jobs settle as cancelled because execution never began; running jobs settle as uncertain and are not replayed.
/council is a TUI launcher over that same native execution path, not another scheduler. The current Agent must expose council_run; otherwise no Council preset is advertised. Zuno persists the user's original slash message and adds a one-turn routing.council prompt block to the cloned resolver. That block asks the Agent to invoke council_run exactly once with background execution and nextStep delivery, while the base resolver remains byte-identical for later turns. A launch entered while another turn is active waits in the durable input queue instead of steering the in-flight model generation.
{
"concurrency": {
"tool_calls": 8,
"delegations": 8,
"mcp_connections": 8,
"lsp_requests": 4
}
}2
3
4
5
6
7
8
Every value is validated in 1..=64.
Native search and shell isolation
glob and grep use the official rg executable as their only search engine. Zuno contributes a thin adapter for typed arguments, cancellation, bounded JSON decoding, stable ordering, and result shaping; it does not maintain a second ripgrep-compatible walker. rg major version 14 or newer must be available on PATH (or packaged beside Zuno by a distributor). Missing or unsupported ripgrep is a startup error for the tool runtime, never a silent fallback.
The Shell tool is admitted through tree-sitter command analysis, the deterministic destructive-command gate, and permission checks, then compiled by the selected sandbox backend before process-tree containment starts. Existing redirect targets and other confirmable destructive operations require a fresh attached-user decision unless the effective permission mode is allow_all; catastrophic targets remain hard-denied. Static creation under the working directory or OS temporary directory and exact non-recursive rm -f cleanup of an absent OS-temporary path do not require a decision. Strict authorization adds HITL to every side-effecting shell call; neither mechanism adds confinement.
config.shell is passed into the same resolver used by non-interactive command execution. Selection is explicit configuration, then the operating-system account shell, then inherited SHELL, then the platform fallback. An invalid explicit value is a configuration error rather than a silent fallback.
The submitted command and the interpreter identity remain separate throughout the runtime. Durable tool-output titles contain the exact command text, such as git diff --check, so a client can display or copy it without inventing a different invocation. The resolved interpreter remains available as typed shell metadata (zsh, pwsh, and so on), and ACP publishes it as _meta.zuno.interpreter. POSIX execution still invokes the resolved executable with -lc; PowerShell uses its non-interactive -Command form. A client must not flatten that relationship into zsh git diff --check: that text is neither the submitted command nor an argv-equivalent representation of zsh -lc 'git diff --check'.
Terminal selection and model-command admission intentionally diverge after path resolution. A PTY may start any executable login shell. A model command may start only a POSIX shell or PowerShell because those are the syntax families the permission and risk gates understand. Fish, Nushell, unknown interpreters, and cmd.exe fail closed; they are not treated as Bash-compatible aliases.
On Linux, the backend resolves trusted system bubblewrap outside the workspace, probes namespace support, mounts the host root read-only, overlays exact writable roots, reapplies protected descendants, drops capabilities, sets NoNewPrivs, and installs seccomp in a first-party helper. Network is denied by default. The process layer accepts only PreparedCommand; it cannot spawn a confinement-required Shell call from raw argv.
zuno debug sandbox --mode workspace-write --network deny --check verifies the same deployment path. It rejects a non-root-owned, writable, special-bit, or file-capability launcher; checks every launcher ancestor; revalidates device and inode before preparation; probes required namespaces; and executes /usr/bin/true through bubblewrap, capability dropping, PR_SET_NO_NEW_PRIVS, and seccomp. A metadata-only probe is not reported as deployment readiness. --check remains strict: it fails when the requested confinement is not deployable even if a trusted runtime fallback would be eligible.
The public modes are read-only, workspace-write, and danger-full-access. workspace-write is the default. Trusted global, explicit, managed, environment, and CLI sources define the maximum; project configuration may only narrow it. An Agent's capability contract is intersected with that maximum, so a read-only Agent remains read-only under a wider invocation.
read-only and workspace-write require a proved OS backend and fail closed by default. The trusted sandbox.onUnavailable setting accepts deny (default) or run-unconfined; the same value can be supplied by --sandbox-on-unavailable or ZUNO_SANDBOX_ON_UNAVAILABLE. Project configuration may set only deny. Global, explicit, environment, CLI, and managed layers may enable fallback, and managed policy has final precedence.
The SandboxResolver completes discovery, capability checks, and a real verify_deployment before publishing Shell. Only unsupported platforms, a missing trusted launcher, missing required launcher capabilities, and typed namespace/container-policy unavailability may activate fallback. Untrusted launchers, invalid policy or paths, seccomp/helper/internal errors, generic process errors, and command preparation/execution errors remain terminal. Read-only Agent contracts never fall back.
An unavailable fallback uses the existing native backend and the same PreparedCommand, permission review, catastrophic-command denial, background, timeout, cancellation, and process-tree lifecycle. It preserves the original permission mode; unlike explicit danger-full-access, it does not imply allow_all. Requested network denial, writable roots, and protected paths cannot be OS-enforced while the effective authority is the host process user's authority. The host emits one warning, and every model request receives the durable runtime.sandbox section while fallback remains active.
Explicit danger-full-access skips restricted-backend discovery entirely, retains host filesystem, process, credential, and network authority, and sets the effective permission mode to allow_all. Explicit permission denies and catastrophic Shell denials remain terminal.
Every path produces a PreparedCommand and persists execution-authority schema version 3: mode and network are effective authority, while requestedMode, requestedNetwork, resolutionKind, and fallbackReason record resolution. Version-2 background records read as requested equals effective with legacy resolution, so in-flight state remains recoverable. Tool output mirrors requested/effective authority and fallback metadata.
Confined macOS and Windows modes currently report unsupported. They remain fail-closed unless a trusted run-unconfined policy is active for a write-capable Agent; explicit danger-full-access remains available independently. The invariants and E2E matrix are recorded in Shell sandbox roadmap.
Resident process containment
Local MCP and LSP servers, process extensions, product agents, PTY sessions, and background commands share zuno-process, but their ownership shapes are explicit. Local stdio MCP commands are direct process-group leaders, matching Codex's ordinary MCP topology. They terminate with bounded SIGTERM to SIGKILL escalation. Zuno inserts no __zuno_child_guard process in front of or beside MCP.
Other resident and interactive hosts retain dedicated guards where a surviving per-tree owner or terminal foreground transfer is required. The direct child returned by guarded_argv is the guard, not the payload; owners request shutdown through request_contained_process_shutdown and reap it only after the contained group settles. On Linux the guard blocks on signals and uses the parent-death signal. Direct MCP relies on owner close/Drop and therefore does not promise descendant cleanup after an uncatchable owner SIGKILL. The pinned Codex comparison and the split ownership decision are recorded in Resident process containment.
Background command execution
shell registers a command with the process-owned BackgroundExecutionService before spawning it. Explicit background mode and a foreground attention timeout therefore retain one execution identity and one process tree; neither path adopts a detached task or starts a second command. An ordinary foreground command is ephemeral: while it runs, its complete output is spooled so the normal output policy can inspect it, but its state is hidden from /ps and both the in-memory row and spool file are removed as soon as the caller consumes the terminal result. A command is made durable only when background: true was requested or the foreground attention deadline promotes the still-running process.
Durable commands keep a bounded 2 MiB live tail, persist complete output separately, and record status under .zuno/background. The service retains at most 32 terminal commands per workspace and removes the oldest row together with its .status.json and .output files. Running commands are never evicted. Consequently ordinary shell calls no longer accumulate files, while /ps, bg, and restart reconciliation keep the state they actually require. Other tools such as read, grep, glob, and web search never use this directory. Because the product is still pre-release, terminal rows written by the old always-durable format are discarded on first open; an old running row is conservatively rewritten as uncertain and is never replayed.
The bg tool supports list, output, wait, and cancel for executions owned by the current session. The complete tool has ToolReplayPolicy::Never because one action cancels a process tree. Cancellation reaches descendants through the shared process containment layer. A hard process ceiling records failure; a process restart converts a previously running row to uncertain and never replays it.
StartupEnvironment shares one service per workspace across parent sessions, child turns, and in-process session switches. Client projections and /ps use that same service rather than maintaining a second process list. The TUI subscribes to created and settled execution events, refreshes from the authoritative service after lag, and updates the right-sidebar Background section even while a model turn is active. Each row carries status, command, pid, elapsed time, and failure context; the section advertises /ps for the scrollable output view.
Background subagents and product agents
Foreground task runs remain attached to the parent turn's hard interrupt. A cancelled parent waits until the child runner has acknowledged cancellation and shut down; it cannot return a successful child result from the same cancellation tick. Every native task, foreground or background, is first admitted as a durable internal job_* while retaining a separate child session identifier for conversational continuation. A foreground Child Turn is owned by an independent supervisor: dropping or force-aborting the outer TaskTool future only cancels its token and cannot destroy Job settlement. If the child has not stopped ten seconds after cancellation, the supervisor force-aborts it and settles the Job as uncertain; it never leaves an owned Job permanently running. A foreground Job remains attached to the current tool call and uses quiet delivery; a background Job follows the selected reportDelivery.
Background Jobs are independent after admission. Steering or hard-cancelling the parent turn does not stop them. They terminate only through an explicit Job cancellation, closure of their owning session, or process lifecycle shutdown; their supervisor then records the authoritative terminal status before report delivery.
Enabled productAgent instances register independent static tools backed by a host-installed Codex or Claude Code process. A product invocation has a one-shot run_* id and, in background mode, a separate job_* id. It does not create a Zuno child session and cannot be resumed as one.
reportDelivery supports:
nextStep(default): settle the job and admit the report to the parent inbox atomically, then wake the parent.quiet: settle the job without admitting a parent input.
For every native child, the host generates TaskReportMetadata; the child model supplies only final prose. The metadata records schema version, optional job id, child and parent session ids, Agent, terminal status, final text, usage, typed-written paths, typed verification records, uncertain side effects, and evidence collection errors. Changed paths and verification records are derived only from durable tool metadata, never parsed from prose or arbitrary Shell output. Each Job persists an evidence_start_rowid captured at admission, so a resumed child contributes only typed evidence created by that delegation rather than evidence from earlier child turns. A background result is stored in agent_job.result; a nextStep report carries the same value under subagentReport.metadata, while quiet leaves it available through the durable Job. Foreground task returns the same schema under its subagent report metadata with its internal Job id. When a report is admitted to the parent transcript, the same value is stored on the canonical user message as message.data.taskReport. ACP history replay restores it as _meta.zuno.kind = "task_report" plus _meta.zuno.taskReport; clients do not have to fall back to raw tool JSON. TUI database replay carries the same object as non-rendered replay data, allowing the subagent view to restore status, final text, changed paths, verification records, uncertain side effects, and evidence errors without parsing presentation strings.
Job settlement and nextStep inbox admission share one SQLite transaction. Wake occurs only after commit. If a process exits after settlement or after an input was promoted, restart recovery reuses the original report row and returns it to its admitted lane; it does not create another report or rerun the child. Queued jobs that never started reconcile to cancelled; running jobs lost with the process reconcile to uncertain and are not replayed. Concurrent process-local wake attempts for one (session_id, input_id) are coalesced by an in-flight lease. A failed wake releases that lease and may be retried against the same durable input, so the guarantee is one logical report and effective delivery, not a ban on retries across a crash. Normal settlement and restart recovery use the same bounded wake helper: at most three attempts, beginning at 10 ms and exponentially capped at 100 ms.
Goal completion uses the same transactional barrier for the model tool and /goal complete. Completion is rejected while any Plan step or WorkItem remains unfinished, any Job is queued or running, or any terminal nextStep report is still queued, steering, or promoted. Consuming that report releases the Job block. Any uncertain Job blocks regardless of delivery or report consumption until typed authoritative reconciliation changes its state. A terminal quiet completed, failed, or cancelled Job has no parent input and does not block completion.
The job tool reads durable status for jobs owned by the current parent session. JobSubject distinguishes child sessions, product agents, and workflow runs; status is queued, running, completed, failed, cancelled, or uncertain, together with delivery policy, result, error, and subject identity. Council execution remains stored through the workflow service, while the frontend-neutral projection types council:<preset> as a Council and attaches its durable child WorkItems. The same child projection represents ordinary workflow nodes and Council seats, including owner, status, elapsed time, and usage.
The right-sidebar Jobs section and /subagent consume that same projection rather than reconstructing state from tool output. The sidebar shows compact node or seat progress and the user's current session_child_first key binding, with /subagent as the command fallback. The detailed view keeps workflows and Councils as jobs, never fabricates child sessions, and shows each durable node or seat plus report-delivery and safety diagnostics.
job_cancel verifies parent ownership and requests cancellation from the live supervisor. It never pre-settles a job and has ToolReplayPolicy::Never; the executor records cancelled only after the child session or complete product process tree has stopped. Product protocol or process loss after work may have begun records uncertain. A restart reconciles still-running product jobs to uncertain and never replays them.
Every delegation also carries a host-derived logical_key over the child Agent and typed DelegationContract. A parent cannot dispatch the same logical task again while its prior Job is queued, running, uncertain, or owns an unconsumed nextStep report, whether the requested execution is foreground or background. For a new child, session creation and Job admission use one SQLite transaction; a duplicate or failed admission rolls back both and cannot leave an orphan child session. A terminal Job also remains duplicate-blocking for the provider Attempt that created it, so serial tool execution cannot run two identical foreground calls from one model response after the first call settles.
job_reconcile is the only model-facing path that can release an uncertain Job. It is non-replayable, verifies parent ownership, accepts only completed/failed/cancelled, and requires both an authoritative source and concrete evidence. It never reruns the original operation. Reconciliation and replacement of any unconsumed uncertain report are atomic; a nextStep resolution produces one replacement report, while quiet remains non-waking.
Codex and Claude Code retain ownership of their native login, configuration, and model choice. Zuno inherits the session directory and proxy environment but never copies product tokens into AuthStore. See Codex and Claude Code product agents.
Concurrent web search
web_search accepts only queries: string[]. The consumer deduplicates queries by first occurrence, runs the remaining requests concurrently through a single-query WebSearchProvider, and combines cancellation with the turn interrupt.
The first failed query cancels its siblings and waits for every request to settle before returning. Successful output is deterministic regardless of completion order: query content follows input order, sources are merged by rank round-robin, duplicate URLs are removed, and profile-owned query, result, and timeout limits are applied.
Provider adapters normalize transport output into SearchResult and SearchSource; they do not own batch scheduling or model-facing presentation.
Network egress
zuno-network owns the outbound HTTP construction policy shared by providers, authentication, catalogs, remote instructions, remote MCP, and web tools. Session traffic uses ProxyPolicy::Environment, which resolves the standard HTTP, HTTPS, all-proxy, and no-proxy environment variables when a connection pool is constructed. A capability that constructs reqwest directly bypasses this product contract and is incomplete.
ProxyPolicy::Direct is an explicit security boundary, not a fallback. It is used only for local control-plane probes and cloud metadata endpoints. Bedrock therefore has two transports with separate lifecycles: runtime and SSO traffic is proxy-aware, while IMDS and approved local ECS credential endpoints are direct. Remote HTTPS container credential endpoints remain on the proxy-aware transport.
Child processes inherit the process proxy environment unless their typed configuration deliberately overrides a variable. The agent loop does not rewrite process globals per session; deployment-specific proxy choices belong in the environment that launches the Zuno process.
Prompt workflow V2 acceptance
The repeatable user-facing acceptance procedure covers four scenarios with each real target model: one atomic implementation, one deep root-cause task, one parallel delegated task, and one Plan-only task. Each run records provider requests, native tool calls, repeated reads/checks, Plan updates, child Jobs, report admission and wake behavior, prompt receipts, and final evidence. It must use a real authorized provider response; an unavailable account, model, or service is reported as a blocker rather than replaced with a mock.
See 提示词与工作流 V2 用户指南 for the commands, expected observations, and current implementation boundaries.
Building a harness
Use zuno_harness::profile_with_tools to combine an AgentDriver, ToolManifest, and native ToolContributions, then add more ProfileBundle values for typed capability providers:
let profile = zuno_harness::profile_with_tools(
"review",
Arc::new(ReviewDriver::new()),
ToolManifest::new([BuiltinSlot::Read, BuiltinSlot::Grep, BuiltinSlot::Task])?,
ToolContributions::new([Arc::new(ReviewSummaryTool::new())])?,
)
.with_bundle(zuno_harness::orchestration_capabilities_bundle(
Arc::clone(&capability_snapshot),
));
runtime.activate_profile(profile).await?;2
3
4
5
6
7
8
9
10
11
Registrations are effects: a component registers each acquisition in PrepareContext, and the runtime owns the returned disposer. Tokio tasks are cancelled and joined, process trees are terminated and reaped, protocol sessions are closed before transports disappear, and registration handles remove exactly what they added. Drop is only a last-resort safety net and does not prove a successful unload. Deployment choices belong in profile configuration rather than hardcoded branches in the agent loop.
Client surfaces
The TUI, headless CLI, server, ACP adapter, and future GUI consume the same commands, durable events, inbox, and frontend-neutral projections. ActivityProjection, WorkStateProjection, SessionUsage, and BackgroundExecutionProjection prevent clients from rebuilding agent-loop state privately. Cursor replay closes gaps after disconnects; live delivery is only a wake/latency path. See client interface architecture.
The design sources and explicit adopt/adapt/reject decisions are recorded in the harness comparison.