nodwyn.

Reference

How to build, run and set up nodwyn, and what its budgets, approvals and clients do. The README is the short version, the manual goes into the code and the individual features.

See Security boundaries and user choices for project trust, credential transport and migration, and the maintenance review for a behaviour-preserving simplification plan.

Maintainer map

The manual's package map lists the files to read first. This table groups the packages by what they do:

Packages Responsibility
core Own a session, assemble dependencies, coordinate its lifetime and prompts.
budget Account for one turn, enforce ceilings, and request additional soft allowance.
loop Provide the primitives and default Scheme for a turn: the strategy, the plan gate, prompts, compaction, sub-agents, working habits.
event, log Describe front-end events; persist the history from which context and replay are derived.
keychain, binding Hold encrypted credentials, acquire and refresh OAuth2 tokens, and inject them; authorize their destinations, methods, positions and scopes.
policyfile Lock and atomically publish policy-file changes after the caller validates and authorizes them; never interpret policy or grant permission.
scheme, guard, prim Evaluate extensions; authorize host operations; implement those operations.
llm, window, redact, detect/l1 Talk to models (Ollama, OpenAI-compatible, Anthropic) and price their requests; derive their context; redact text; detect credential shapes.
memory, retrieval Make a task's history searchable by words and keep its per-turn cards, as views of the log that the log never records.
schedule, catalogue Compute scheduled tasks' civil-time rules and say them in words; hold what providers publish about their models.
watch, workflow Read what a workflow's source has now (a forge's pull requests, a Gmail search, a command's lines) through the credential check a task's requests meet; keep a workflow's log, its held answers and what it has seen.
startup Write the login item that starts the daemon, only when asked.
termemu, frontend/termvt Give the terminal primitives an emulator behind an interface, with the Charm library only under frontend.
mcp, extract Talk to remote MCP servers (pinned manifests, bound keys); turn attached documents into redacted text, check attached images by their content, and read all a document carries before it is sent.
frontend/workflows Read the daemon's workflows and held answers, say them in words and act on them, for the command line and the TUI alike.
frontend/run Chain planned turns, with optional deadline and turn limits; ask a person to continue, or, unattended, go on only after a turn that marked a plan step done or skipped.
frontend/plain, frontend/tui, frontend/web, frontend/rpc, config, cmd/nodwyn Render and answer events; own local RPC connections and sessions; load settings; wire the application together.

A user message: the front-end calls Session.Send. The session records and redacts the message, and calls the Scheme procedure nodwyn-turn, which plans first when the turn must (a run's turns must) and then applies agent-turn. The default agent-turn runs the plan gate, then asks the model, runs the tools it requested through Scheme and repeats until the model answers without tools; a project may redefine either in .nodwyn/init.scm. The loop's primitives build each window from recorded events and record every result before the next model request. The front-end consumes events from the session.

A credential-bearing tool call: Scheme applies a primitive; the guard reads its metadata and arguments and checks the binding and other policies. Only an allowed call reaches the handler. The handler builds the request and asks the shared credential service to inject the opaque handle's secret at the authorized position. Secret values stay out of Scheme and the log. Jev uses this same path, including when called by the plan gate. The harness's control-plane model call is the deliberate exception described in the spec.

Adding a built-in integration: declare its settings in config, then register its handlers in prim with explicit egress/irreversible metadata and credential, destination and payload roles. Supply its settings during core construction and use host interfaces for credential injection, transport, redaction and recording, as Jev does. Declare fixed request scope with EgressHost, EgressMethod and EgressPosition when the primitive has no corresponding role parameters, and EgressClass when Go supplies the credential (as for google-doc-upload and the judge tools). Do not open the keychain or read secrets in the integration. Test it through Scheme and the guard with a local fake service, bound/unbound destinations and canary credentials. Integration-specific settings should not become getters on Session.

The roadmap extends these boundaries: ADR 21 uses events for RPC clients; ADRs 22–23 bring MCP tools through the primitive/guard path; ADR 24 extends the credential lifecycle to OAuth. Local RPC, workspaces, attachments (text and images), MCP servers (remote, and local ones contained by podman or docker), OAuth2 credentials and documents out (pandoc, Google Docs) are implemented. Keep loop and event separate, and keep credential management shared across integrations.

Build

make build      # bin/nodwyn, and bin/nodwyn-egress (the proxy of contained MCP servers)
make build-webview  # bin/nodwyn whose `nodwyn app` opens its own window (app.md)
make test       # go test -race ./...
make lint       # vet, staticcheck, boundary and ingress lints
make accept     # everything a candidate must pass before promotion
make smoke      # manual two-turn session against the real control plane
make selfcheck  # the candidate proves itself (ADR 13)
make promote    # accept + selfcheck, then ~/.local/bin/nodwyn-stable and nodwyn-egress (webview build; version = commit)

Requires Go 1.26 or newer (ADR 33). make lint builds its staticcheck from the module (go.mod's tool), so nothing else needs installing.

The Scheme interpreter is a separate, public module, github.com/fclaude/brote, which Go fetches like any dependency; no GOPRIVATE or git configuration is needed.

Text search (index-grep) uses the shared guard and produces session-scoped results. The model also searches by words (index-search): the repository, from an index of the files git lists that nodwyn keeps in its data directory and refreshes as files change (index-update), and its own conversation, bringing an earlier event back whole (recall), elided and folded results included, from the log; and, when the person turns it on for a project, its earlier tasks. See memory and retrieval for their limits.

Run

bin/nodwyn daemon               # local sessions, turns and event delivery
bin/nodwyn tui                  # opt-in full-screen terminal client
bin/nodwyn app                  # the whole thing in one program: a window, and a daemon if none runs
bin/nodwyn ui                   # the browser console for a running daemon
bin/nodwyn ui --listen IP:7777  # also for other devices, which pair with `nodwyn devices invite`
make app                      # bin/Nodwyn.app for macOS (to share with people who never open a terminal)
bin/nodwyn                      # chat in the current repository (line mode, over the daemon)
bin/nodwyn --repo ~/src/x       # pin to another repository
bin/nodwyn --show-thinking      # show the model's redacted thinking too
# Exact request replay (new logs with recorded context):
# bin/nodwyn replay SESSION_ID --window REQUEST_SEQ
bin/nodwyn --no-confirm         # non-interactive: every confirm is a no
bin/nodwyn replay <session-id>  # print a session log as a transcript
bin/nodwyn sessions             # id, last activity, state, repo, first ask
bin/nodwyn summary SESSION      # inspect user-authored, revocable summaries
bin/nodwyn sessions --ids       # IDs only (scripts)
bin/nodwyn --resume <session-id> # continue in the recorded repository
bin/nodwyn trust --repo ~/src/x  # review project startup code and approvals
# Then: nodwyn trust --repo ~/src/x --accept REVISION_FROM_THE_REVIEW
bin/nodwyn repl                 # Scheme REPL
bin/nodwyn credential add --oauth2 google --client-id ID  # an OAuth2 credential, authorized in the browser (ADR 24)
bin/nodwyn credential           # list credentials, never a secret

The control model is the [control] table in ~/.config/nodwyn/config.toml: an Ollama server, or any chat-completions server with a key bound in bindings.toml. There is no built-in default; when it is missing, nodwyn says what to add. Model requests honor HTTPS_PROXY, HTTP_PROXY and NO_PROXY, so a sandbox whose only egress is an allowlist proxy can still reach the model. What asks you is [approval] (ADR 35). By default a write inside the repository goes ahead, its diff logged and its previous content kept for undo, and a command no standing approval covers (command prefixes in trusted .nodwyn/allow.toml, ADR 17) goes to the judge first, then to you when it refuses or there is no judge. Standing approvals also check arguments: flags that select programs, output files or another working directory, make variable assignments, unknown flags, and absolute or repository-escaping paths cannot inherit one. When a prefix would otherwise cover such a command, a security confirm asks you. Approved builds and tests still execute project code, write files and may use the network; these checks do not sandbox the process. See executable approvals. A protected path, content that looks like a credential and a command built from tool output always ask. Writes to the session's scratch directory, outside the repository, ask only when they need a security confirm (ADR 28). Session logs live in ~/.local/share/nodwyn/sessions/. Resume restores the conversation and appends to that same log. It reloads current configuration, bindings, and trusted repository approvals and init.scm, so its prompts and tools as they are now apply, which (reload) leaves fixed until then; requests made before still replay under the recipe they used. Session-only approvals and interactive Scheme definitions are reset. An explicit --repo must match the recorded repository. Only one process may append to a session at a time. Interrupted tool calls are never rerun automatically: a call without a durable result is marked "outcome unknown" so the next turn can inspect current state before retrying. sessions reports an unfinished turn as interrupted, not as proof that another process is running. Missing final log lines are repaired on resume; corrupt complete lines are rejected.

Repository startup code and approvals are skipped until you review and accept their revision with nodwyn trust. Trust is stored outside the repository; changing either file requires review again. Review any code the init loads too. Revocation requires restarting existing tasks to discard their loaded code and grants.

Credentials require HTTPS except on loopback by default. For a remote HTTP service you deliberately accept, set [policy] allow_insecure_http = true in the user's config.toml, or Settings > Approvals > Credentials over remote HTTP. The switch is off by default and applies to new/resumed tasks; it preserves binding checks, redirect refusals and certificate verification. See the security reference.

The guarded file tools pin paths to the repository; credential-shaped files are unreadable, network tools on a command line need confirmation, and child processes get a scrubbed environment. Redaction removes recognized credential shapes from user messages, tool results, logs, and frontend events. Responses are buffered until complete so split stream chunks cannot expose a key. Unknown or encoded secrets are not guaranteed to be detected; a classifier for them (the optional L3) is not built. Approved commands run with the user account's privileges; these checks do not sandbox an approved executable.

GitHub Actions runs .github/workflows/ci.yml on every push to main and every pull request. It executes make accept with fresh race tests, the browser checks and govulncheck, then cross-compiles Linux and macOS binaries for amd64 and arm64 with cgo disabled. Only the Linux amd64 binary is executed. The release build also runs a selfcheck against a fake model, so no paid model or real model credentials are needed. Pushes never publish: a release is a manual workflow run on main with a version. Releases says what each one holds and how to check its digest. Publishing is separate from local nodwyn-stable promotion.

Turns also stop when the model repeats an identical tool call four times in a row, retries a denied call three times, gets ten consecutive tool errors, or fails with one tool on one file in six of its last sixteen responses (ADR 18). The reason is shown and logged. Any remaining calls in that response are marked not executed; a new user message starts a fresh turn. A task's own limits stop its turns too, and the bounded run driver (nodwyn run, below) bounds a run.

A task has no limits unless you set some, for that task (ADR 49, amended 2026-10-27): there is no limit in config.toml that every task stops at. Set them on the task's panel in the console (Limits, under the task's monitor: Add a limit, then the value; a chip changes or removes it), or on the command line for the task line mode opens or resumes:

nodwyn --max-tool-calls 200 --budget-wall 1h --budget-cost 5   # a new task
nodwyn --resume ID --budget-cost 0                             # remove its spend limit
Limit Flag Counts
tool calls --max-tool-calls N each turn's tool calls
tokens --budget-tokens N each turn's model tokens: a request's prompt and reply, a prompt token the provider read from its cache as a tenth of one
writes and commands --budget-actions N each turn's authorized irreversible host attempts, every nested write or command inside a Scheme tool included; denials count nothing
time --budget-wall 1h30m each turn's wall time, model waits, tools, planning and waiting for your answers included; it cancels blocked work
spend --budget-cost USD what the whole task's requests cost, every turn's, as far as known

Each is one number: reaching it stops the turn, with nothing asked first. A flag given on a resumed task replaces that limit and keeps the others; 0 removes it. A change applies from the next turn, the task's log records it (session.limits), and resuming the task keeps it. A paired device may add a limit or lower one, never raise or remove one. A [budget] table, or loop.max_tool_calls, that an older config.toml still has loads and sets nothing.

Tokens are the usage each model request reports, the control model's and any other configured model's: a response can overshoot a token limit, but no later operation starts after that usage is observed. Judge requests are not model requests and are not counted against tokens; their usage is recorded and shown, and their cost, priced by [judge] price_input and price_output, counts against a spend limit.

After each reply the front-ends show its tokens, the share read from the provider's prompt cache and its cost, each only when known; a model's price_input, price_cached_input and price_output (USD per million tokens) estimate the cost when the provider does not report it. The TUI's :budget shows the task's limits and the latest turn's spend.

A spend limit counts what the whole task's model requests cost, not one turn's: every turn of a session or a run, a resumed session's history included. At the limit the turn stops (turn.end outcome spend_limit), and no later turn starts: a run ends limited, and a new message stops before it reaches the model. Raise or remove the limit to go on. It counts only known costs, reported by the provider or priced from the model's price_*. A request with neither is not counted, and the first turn that has one says so in a note.

The default loop has two profiles. A session nobody approves (--yolo, --judge, --unattended, a run's auto_approve) runs the lean one: a short system prompt, shell-exec and fs-edit alone, no explorer or planner, and nothing said to the model while it works. A session you answer runs the full one.

[loop]
profile = "auto"   # or "lean" or "full" for every session; NODWYN_PROFILE overrides it

See the manual and docs/scheme.md §9.8.

Local daemon

nodwyn daemon serves a versioned local session protocol over a private Unix socket. Clients can manage sessions, send and abort turns, subscribe to events, and page through durable history after reconnecting. Sessions belong to the daemon and survive client disconnects; shutting down the daemon releases their logs for later resume. A slow subscriber is disconnected without blocking turns. The existing plain frontend stays usable.

Clients can answer approvals and masked credential prompts, explicitly grant standing approval for a pending operation, and evaluate user Scheme. Replies carry the kernel-provided Unix peer identity; subscriptions grant no authority. Credential values stay in the shared host/keychain path and never enter Scheme. The generated wire schema is checked for drift in the normal test suite. See the protocol guide for startup, methods, catch-up cursors, queue limits and ownership.

Terminal client

nodwyn tui --repo /path/to/project starts the full-screen client locally; nodwyn tui --socket /path/to/sock attaches to a daemon. Choose or resume a session, follow the conversation, inspect scrollable approval diffs, and reply to budget or masked credential prompts. Tool calls group their arguments, recorded guard decisions and full results or errors. Ctrl+C aborts busy work; Ctrl+Q quits. Quitting a daemon client detaches without stopping its session; Ctrl+R reconnects and recovers history without resending operations. Plain mode remains the default. Use :tools to list model-facing tools, or :tools NAME for an input schema. :plan shows the latest message’s recorded planning outcome and the steps the model marked done or skipped; :budget shows configured limits, the latest turn’s checkpoint snapshots and its spend. Use :log, :bindings (also :allow), :replay and :friction for read-only inspection; Esc returns to the conversation and r refreshes a snapshot. :model shows the conversation's model and the choices, and :model NAME [EFFORT] moves it from its next turn; :attach PATH adds a file. In a git repository the chooser offers a session in a worktree of its own first, and --worktree opens one directly. Enter sends the whole draft; Alt+Enter or Ctrl+J adds a newline. Bracketed paste preserves line breaks without sending. Prompts keep their own single-line input. See the TUI guide for controls, ownership and the acceptance coverage, and the clients for what each front-end can do.

See session memory for exact request replay, summary creation and revocation, and retrieval.

The console (ADR 21, ADR 30)

The browser console is organized around tasks: Home starts one in a folder and shows what needs an answer, what runs and what finished; a task shows its conversation with the approval inline (the diff, and Y/T/A/N), and beside it the plan with the steps the model marked, this turn's budget, changes with their history, attachments (documents and images), project notes and the workspace. Connections adds and reviews MCP servers and manages credentials (keys, OAuth2 accounts, and where each may go); Workspaces names folders and their mounted folders; Settings, in tabs, sets what a task does without asking you (writes in its folder, and who answers a command: the judge, then you, by default), the limits, the providers with their models, the judges (Jev or an OpenAI API key, set up with the key alone) and the context window, and unlocks the keychain or sets its passphrase (a keychain may have none, which is not recommended). A task chooses its model, effort and judge beside each other, as it opens and between turns. It runs as nodwyn ui for a daemon, as nodwyn app without one (see the app), and on a phone paired by name. See the browser guide.

Background runs

Every task the console starts is a chat; a request that takes long is a message like any other (ADR 35). For scripts and benchmarks, nodwyn run carries a task to its end with no conversation. Start nodwyn daemon, then submit a bounded task:

nodwyn run --task task.txt --repo . --budget-wall 2h --max-turns 20
nodwyn run status SESSION
nodwyn run stop SESSION
nodwyn run resume SESSION

Attach the browser or terminal client to the daemon to answer tool prompts and between-turn checkpoints. Disconnecting the client leaves the run owned by the daemon. A run's deadline (--budget-wall) and turn count (--max-turns) are optional, as its task's limits are (--max-tool-calls, --budget-tokens, --budget-actions, --turn-wall for each turn's time, --budget-cost): without them it goes on until the model reports the task complete, a turn stops it, or you do. Resume preserves the original deadline and consumed turn count; it never replays an interrupted tool or restores temporary approvals. See runs for completion semantics and recovery.

Without a person: --yolo answers a run's ordinary confirms yes (never a security confirm). --judge puts each ordinary confirm to the judge instead, which allows a call only when its probability of "allow" reaches judge.approve_threshold (0.8 by default) and declines security confirms; the decision is made in Go, so a project's Scheme can only refuse more. It needs a judge ([judge] naming a provider whose key is stored; see the judge) and excludes --yolo. --unattended means nobody answers anything: confirms are declined unless --yolo or --judge answers them, and the run goes on after a turn only if that turn marked a plan step done. A resume restores none of these; nodwyn run resume takes them again.