Skip to main content

The Data Plane daemon — running a session against your own tests

The Data Plane daemon (cmd/kiwidaemon, pkg/daemon) is the only component that touches your source code and your model key. It is a pull-model worker: nothing connects to it; it polls the Control Plane, does the work, and reports back.

The pull model

On first boot the daemon generates its keypairs and registers with the Control Plane using a single-use join token (mint one with POST /api/v1/daemon/join-token, or from the dashboard's Fleets page). After registration its persisted identity key is sufficient, and the token can be omitted on restart.

It then heartbeat-polls the Control Plane over HTTPS. Each poll can return a leased task plus that org's sealed credentials. Because the daemon only ever makes outbound HTTPS connections, it runs cleanly inside a customer VPC with no inbound firewall holes — the foundation of the BYOC model.

Workspace provisioning

Rather than clone a repo for every task, the daemon keeps a cached bare clone per repository (pkg/gitcache) and provisions each task an instant workspace with git worktree. One job maps to one branch. The git cache keeps at most -max-cached-repos bare clones (default 20), evicting the least-frequently-used; 0 disables the bound.

The daemon also infers the test command when none is given, and works out the runtime image itself — a task submitted through the API or dashboard needs only a description and a repo. See Sandbox & isolation for how the image and test command are inferred and self-corrected.

Getting the repository ready

Before the session starts, the daemon works out which image the repo needs and installs its dependencies in a networked phase with no credentials in it. Both are inferred from what the repository already declares — you never pass an image or an install command. That two-phase split, and what it buys, is on Sandbox & isolation.

The Architect/Implementer session

The session (pkg/session) is the only execution loop — there is no separate fast path for bounded edits. Two roles, one task:

  • The Architect is persistent across the whole task. It writes the opening spec, reviews each round's diff against its own acceptance criteria, and decides when the task is done — approve, revise (try again with a new brief), proceed (the first round), or abandon (the task cannot be done, or the premise is false). By default it uses read_file/grep to explore the repository before writing a spec, rather than reasoning from a bare filename listing; a provider that cannot hold a tool conversation falls back to a single completion, silently.
  • The Implementer does the actual work with real tools — read_file, grep, edit_file, write_file, run. It starts a fresh context every round: rather than carrying a live, growing transcript (which becomes a lossy, stale cache of the filesystem the longer a session runs), each round is briefed from durable state — the Architect's spec for the round, the task, a compact digest of what earlier rounds did, and the most recent verification output.

Key behaviors, straight from the code:

  • The task is the goal; the test is a guard. The task description is the objective; the test command proves the change broke nothing, not that the work is done. A run that changes nothing is reported as a failure — that is what makes additive work ("add an example to the docs") an ordinary job rather than a silent no-op on an already-green suite.
  • It will not edit a failing test. While the suite is red, must_not_change in the Architect's spec is where the anti-gaming rule lives — the Architect, having actually read the repository, can name the specific file whose weakening would fake the fix, rather than a blanket rule that also blocks "add tests for the parser."
  • Open questions get answered, not just asked. A spec's open_questions come back from the Implementer as structured answers, or a new_question when the Implementer could not resolve one on its own — both appear in the next review under their own heading, not folded into a single freeform note where nothing downstream can check whether they were actually addressed.
  • A reviewer that repeats itself is caught. Two consecutive specs asking for the same change (by objective and target files) halt the session as looping, rather than burning the remaining round budget on a request the Implementer already couldn't satisfy.
  • A stall is distinct from a budget cap. The same diff and verification output twice in a row halts the session as "not making progress" — a different, more useful message than "ran out of rounds."

Opening the PR

When the guard test passes and the worktree actually changed, the daemon commits and opens a pull request using the org's git token (unsealed in memory, never written into the sandbox). A SUCCEEDED task always corresponds to a real PR — the daemon treats "green test but no PR" as a failure rather than reporting a misleading success. It then reports completion (with the PR URL) back to the Control Plane, which transitions the task to SUCCEEDED, and the run is sealed into a verified execution record.

A review comment left on the resulting PR resumes the task: a new round starts carrying context from the parent task's outcome, rather than requiring a full re-run or being silently ignored.

Running a daemon

kiwidaemon -api-url https://api.runkiwi.dev \
-key-path ~/.kiwi/daemon.key -cache-dir /tmp/kiwi-cache \
-poll-interval 5s -max-cached-repos 20 -max-rounds 4 -session-budget 5.00 \
-join-token "$KIWI_JOIN_TOKEN"

-max-steps and -max-budget are still accepted as deprecated aliases/no-ops so an existing launcher does not fail on an unknown flag, but -max-rounds and -session-budget are the live knobs.

For the shared Free tier, pass -sandbox-runtime runsc (or set KIWI_SANDBOX_RUNTIME=runsc) so the test command runs under gVisor. See Sandbox & isolation.