@kodel-ai/runner
v0.1.0
Published
Kodel self-hosted pull-runner daemon (block-7)
Readme
@kodel-ai/runner
Self-hosted pull-runner daemon for Kodel pipelines (block-7).
The runner is a passive executor: it declares labels (what is installed on this VM) and
opens an outbound WebSocket to the Kodel server (works through NAT). The server matches
a pipeline node to this runner and sends a runner.claim carrying a structured
AgentTask (engine + prompt + env + optional git/shell). The daemon cleans
work/<project>/, optionally git clones, then runs the task with the engine adapter
for task.engine, streams stdout/stderr back, and reports the final exit code.
Formula: the server says WHAT (intent), the runner's adapter does HOW. Built-in adapters:
agentapi(claude-code / opencode via coder/agentapi) andshell(escape hatch that runstask.shell.script). There are no server-side profiles/recipes.
Install
npm i -g @kodel-ai/runnerThis installs the kodel-runner daemon.
Configure
Create a registration on the server first:
kodelctl runner create backend-1 --kind self-hosted --label claude --label agentapi --project PLT
# → prints the backing service-account PAT (token) — copy it once
# labels are assigned server-side (the daemon does not send them).Then on the VM, ~/.kodel/runner.yaml:
server: https://kodel.example.com
token: pat_xxxxxxxxxxxxxxxx # backing SA PAT (resolves the runner; never logged)
slug: backend-1 # optional; server resolves by token
workdir: ./work # cleaned per claim under work/<project>/
# concurrency: 1 # fixed at 1 in MVP (sequential)
# labels are NOT set here — they are assigned server-side at `runner create --label`.Every field can be overridden by env (KODEL_RUNNER_SERVER, KODEL_RUNNER_TOKEN,
KODEL_RUNNER_WORKDIR, KODEL_RUNNER_SLUG) or CLI flags.
Precedence (later wins): defaults < file < env < flags.
Run
kodel-runner start --server https://kodel.example.com --token pat_xxx
# or, with the config file in place:
kodel-runner startThe daemon registers, heartbeats every 30s, and waits for claims. Reconnects with exponential backoff (1s → 30s cap). Ctrl-C to stop.
WebSocket transport
The runner uses the native ws client (NOT socket.io). This matches the Kodel server,
whose gateways are native-ws (WebSocket.Server({ noServer: true }) + an HTTP upgrade
handler on a fixed path — see tui.gateway.ts). The daemon connects to <server>/ws/runners
(http→ws, https→wss).
Auth is register-first: immediately after the socket opens, the daemon sends a
runner.register message carrying the PAT (token) + slug + labels as its first frame;
the server resolves the runner by token+slug and replies with runner.registered. The
token is sent only inside that frame (never as a query param / header) and is never logged.
Protocol messages are defined in @kodel-ai/shared ("Self-Hosted Runner Protocol"):
server → runner runner.claim / runner.cancel; runner → server runner.register /
runner.heartbeat / runner.step / runner.node.complete / runner.node.error.
Credentials
The daemon does not store any long-lived engine credential. Per-claim env (including
e.g. ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN minted by the server under the runner's
service account) arrives inside AgentTask.env and is used only for that task.
Engine adapters
The daemon picks an adapter by task.engine:
- agentapi (
engine: claude-code | opencode) — drives theagentapiHTTP wrapper: spawnsagentapi server -- <agent>in the checkout dir, POSTs the prompt, polls/statusuntilstable, streams the answer.agentapi(and the engine binary, e.g.claude) must be pre-installed on the VM; declare the matching labels (agentapi,claude) on the runner. - shell (
engine: shell) — escape hatch: runstask.shell.script[]sequentially, stopping at the first non-zero exit.
Development
bun test # unit tests (config + executor)
bun run start # run the daemon locally