Local runner guide
How local agent sessions and runner surfaces should operate through ROST without bypassing Charters or audit.
The local runner is for human-controlled local agent work. It should retrieve ROST context, use narrow tokens, and report work back through audited MCP or CLI commands.
Runner sequence
1. Create a scoped token. 2. Fetch the agent-reference-map. 3. Fetch the seat or onboarding context. 4. Draft changes before activation. 5. Record work, Friction, handoffs, and escalations. 6. Revoke access when the session ends.
Install a local Runner
Use Local Runner when a Seat should run on a machine you control. Install it from the public CLI — no repository checkout and no desktop app are required. A signed desktop app is planned; until verified installers ship, Settings > Connections > Connected machines (/settings?section=runners) shows no desktop download link.
Recommended paths:
- Always-on host:
rost runner install-service --name <runner-name> --executeinstalls a background service (macOS launchd user agent) that keeps the runner online.rost runner setupinstalls and verifies that same service for you as part of the resumable setup lifecycle below. Manage it withrost runner start|stop|restart|status|logs|uninstall --name <runner-name>. On macOS the CLI tries bothuser/<uid>andgui/<uid>launchd domains because session behavior differs across console and SSH contexts;runner status --name <runner-name>prints the loaded domain, the installed plistapp_url, and whether the development/preview URL-policy opt-in is missing or stale. - Manual smoke:
rost runner serve --onceheartbeats once and exits;rost runner serve --executeclaims and executes runner work orders while it runs.
Two ways to authenticate and pair — both first-class, an agent can drive either entirely from the terminal:
- CLI-native (agent):
rost login --deviceprints a code for one human OAuth approval; after that the session is stored locally and the agent runs unattended ("approve once, then autonomous"). Thenrost runner serve/install-servicepairs against your active company — an authenticated CLI can also self-issue a pairing code withrunner.pairing.startand claim it. - Web-guided (human): issue a pairing code from Settings > Connections > Connected machines (
/settings?section=runners) > Add a runner, then on the host runrost runner serve --user-code <code>. An explicit user code always claims that code, even if the host has an older local runner state file, so the browser and terminal do not accidentally point at different runner ids. The host stores only the runner bearer secret in an owner-only file and never logs in.
Headless and service accounts are supported first-class: when no usable OS keychain exists (for example a service or SSH account), the CLI stores the session in an owner-only file (directory 0700, file 0600) automatically — no development flag. That file holds only the CLI session token, never tenant secrets, API keys, or vault refs.
Resumable setup lifecycle
Runner setup is tracked as ONE server-side resumable lifecycle per owner: runner_setup.start creates (or reuses — a duplicate start returns the same setup id, never a second record) the setup session with the selected runtimes (claude, codex, or both), and runner_setup.status / runner_setup.resume report and advance it through the stages: pairing, provider consent, service install, runtime validation, heartbeat wait, ready. Each stage records checkpoints; a blocked setup carries a typed blocker code, and runner_setup.resume renews an expired human action or rewinds to the earliest unproven stage under the same setup id. runner_setup.cancel terminally ends the setup record (human-gated, owner-only) without touching an already-paired runner — revoking a runner stays runner.revoke.
These commands are owner-authorized before any detail is disclosed. Device pairing approval and Claude/Codex provider consent remain human-only: a CLI agent may start setup, poll it, and present the safe action link or code to a human, but the response to an agent is always a human action_required envelope — never pairing authority. Raw pairing codes are never stored or re-displayed by the server (the code's hash lives only on the pairing session record), and setup state never contains provider credentials, runner bearer secrets, or raw provider output. Per-runtime readiness is one shared fail-closed contract: a runtime counts as ready only when it is installed, its isolated credential cell proves auth, its sandbox check passed, and a current server-issued validation receipt exists — installed-but-unauthenticated, stale or unknown proof, an expired or foreign receipt, sandbox failure, and runtime mismatch all read as not ready. Selected runtimes never silently fall back to another runtime.
The ergonomic rost runner setup orchestrator drives this lifecycle over the local primitives for you: rost runner setup start --user-code <code> (runtime defaults to claude; pass --runtime to be explicit) binds the runtime selection to the setup session before any pairing code is consumed, claims the web pairing code, reuses any already-paired runner state, and runs the isolated Claude consent ceremony against the runner's own credential cell. rost runner setup status reports the stage, pending human action, and local checkpoints; rost runner setup resume revalidates every recorded checkpoint against live local facts and rewinds to the earliest unproven stage before continuing under the same setup id — an interrupted consent resumes against the same credential cell, and an expired pairing action is renewed, never re-created. A --runtime passed on resume must match the bound selection or the command errors; it is never silently dropped or replaced. Agent-driven invocations (--agent, or a non-interactive shell) surface the server's human action_required and poll until it clears or a bounded timeout; tune the poll timeout with --timeout-ms <milliseconds> on start or resume. rost runner setup cancel terminally cancels the server session (paired runners are untouched). Cancel requires interactive human approval; every verb emits a single JSON envelope on stdout with --json — all human copy stays on stderr, and the envelope never carries secrets or user codes. The same three driving verbs — start, status, resume — also run on an implementation-bootstrap credential, which is how the external implementation Skill sets a runner up without ever holding an owner session: a present bounded credential takes precedence over a co-resident login, an expired one is refused rather than silently swapped for the owner, and the tenant every claimed runner is checked against comes from the credential itself. cancel is deliberately not on that surface — it is owner-only and human-gated, so a bounded caller is refused before any local state is touched, with the same JSON envelope every other refusal uses. The orchestrator's local progress lives in a private 0600 sidecar under ~/.rost/runner/setup/; the raw verbs (rost runner_setup start --selected-runtimes claude --json and siblings, or the raw invoker rost command runner_setup.start --json '{"selected_runtimes":["claude"]}') remain available for driving the lifecycle directly.
Setup owns the background service, and the truth about closing your terminal. On macOS the service-install stage writes the launchd user agent for the paired runner, pins it to the setup's own state file, loads it, and records a checkpoint; rost runner setup resume revalidates that checkpoint against the installed plist and the loaded launchd job, so a second run reuses the service instead of installing another. Two collisions are refused rather than resolved silently: a service already installed under the same runner name but bound to a different state file, and a differently named service already pinned to the same state file — so one machine's runners can neither boot each other out nor share one credential. When a collision lands, an implementing agent must not call tenant-wide runner.list (denied to the bootstrap principal): use the local rost runner inspect-state-path --json verb instead. It reports whether the durable setup state path is claimed, by which service label, and a tenant-anonymised relation (same / other / unknown / absent) against the active implementation credential — never foreign tenant ids or machine inventories. Setup picks a durable state file of its own by default rather than the temporary one a foreground rost runner serve uses, because a service that comes back from a reboot with no bearer secret is not an always-on service; an explicit --state-file always wins and is never substituted. Every install precondition — supported service manager, a durable state file, no other service already using it — is checked before a pairing code is consumed or provider consent runs, so a host that cannot end up with a working service never spends a single-use code. The service is installed with work execution enabled — the validation stage that follows needs it to claim a validation job — and the CLI says so as it installs. On any other service manager the stage stops with a typed manual_service_install action and the unsupported_service_manager blocker, and the setup is deliberately not ready: run rost runner serve --execute under your own supervisor (for example systemd, pm2, or nssm) instead. Setup then waits for that service to come up and, only then, says the terminal may be closed. That claim is deliberately narrow: it asserts what the machine can prove — the launchd job this setup installed is loaded, running, and pinned to this setup's durable state file — and nothing more. It does not claim ROST has observed this particular service, because that is not establishable today: a foreground rost runner serve on the same state file authenticates as the *same runner id*, so a heartbeat cannot be attributed to the service. Setup still reports the runner's current health, explicitly labeled as not attributed to the service, as information rather than proof; a transient failure to read it never fails a service that is demonstrably running. Server-observed, service-attributed readiness arrives with the service-identity validation receipt (tracked separately); until then setup does not claim it. If the job does not come up within --timeout-ms, setup reports a blocker and says nothing about closing the terminal.
The installed service claims and runs work orders by default, so the validation stage that follows it can do its job. Setup says so before it installs, and --no-execute installs a heartbeat-only service instead. That choice is remembered: once you decline, a later rost runner setup resume keeps the service heartbeat-only rather than quietly handing execution back, and --execute is how you deliberately turn it on again.
Runner transport defaults to the canonical production host https://rost.elevatez.ai. The CLI rewrites the old https://rost-pink.vercel.app alias to the canonical host with a visible warning. External http:// app URLs and non-canonical hosts are refused before pairing, heartbeat, claim, MCP, or result traffic starts; only http://localhost, http://127.0.0.1, or the explicit development/preview escape ROST_ALLOW_INSECURE_APP_URL=1 are allowed. rost doctor and local runner status --name <runner-name> inspect the installed macOS launchd plist and flag when a preview/development host is missing that opt-in, or when the opt-in is still set after the service has returned to the canonical host.
Choose the local runtime with --runtime auto|claude|codex (default auto). The same short-lived seat-scoped MCP token and server-side guard apply regardless of runtime; results are tagged claude-cli or codex-cli.
Claude-lane turns authenticate from a dedicated per-runner Claude credential cell, separate from the operator source store. After pairing, run rost runner login on the runner host once (if the runner was installed as a service, pass --name <runner-name> — with no flags it uses the only paired runner when exactly one exists): it runs claude auth login scoped to the runner's private credential cell (~/.rost/runner/claude-home/, keyed on the runner id) and verifies the credential reads back in the exact shape turns use. The credential lives as a file inside that cell — the same shape on macOS and on Linux. runner login performs an advisory comparison with your own Claude Code login and discloses a match or an unreadable/null probe; a local comparison cannot prove a runner-only account, so complete the morning second-account proof before production use. Your own Claude credential is unreachable from a turn: the sandbox denies the operator's config dir and the macOS Keychain, so a turn cannot reach it even by re-pointing Claude's credential environment itself. The cell survives restarts, so re-login is only needed after it is deliberately removed. A turn that finds no usable runner credential (never logged in, or the file credential lost) prints a one-line rost runner login reminder to the runner's stderr or log; the turn still reports the auth failure.
For always-on execution, runner serve --execute heartbeats on its normal interval and also checks for claimable work between heartbeats. The default interactive claim cadence is about one second; set RUNNER_INTERACTIVE_CLAIM_MS or --interactive-claim-ms to tune it for a host. Set how many turns the runner may run in parallel with --max-sessions <n> or RUNNER_MAX_SESSIONS; left unset, the runner estimates a value from host CPU and memory. The scheduler honors the declared value (clamped to a small ceiling) so a runner never claims more concurrent work than the capacity it reports. Claude-backed turns reuse a per-seat Claude Code resume session when Claude reports a session id, so later turns can run with claude --resume <session-id> under the local subscription account. The runner stores only the opaque resume id in its owner-only state file; it does not use API-billed managed sessions to make subscription runner turns faster. A resident long-lived Claude/Codex process is still prototype-gated, so do not assume a fixed latency SLA from this guide alone.
Claude claims require truthful dedicated-cell evidence: the runner emits claude.auth_state_contract_version: 1 only when its cell probe reports installed: true and auth_state: "ready". The server requires that marker for interactive Claude turns and Claude-backed generic work orders; missing, malformed, or older markers leave work queued while the runner continues heartbeating. A configured Codex-only runner (Claude absent, Codex installed) remains eligible for generic work when it also advertises execution-mode contract v1.
Each claimed work order carries a server-persisted execution_mode of production_manual or production_scheduled. Current runners validate that field before starting and forward it unchanged into the model context. The server withholds mode-required work from an older runner that does not advertise execution-mode contract v1; upgrade and restart rost runner serve to make those queued orders claimable. A new CLI also waits for heartbeat contract v3 before making any claim or resuming an orphaned claim, so rolling against an old or malformed heartbeat route remains heartbeat-only.
The runner bearer secret is stored locally and is never displayed. Each claimed turn receives a short-lived seat-scoped MCP token from ROST, and server-side guards still decide what tools the Seat may use.
Interactive AICOS runner turns
AICOS runner chat uses the same runner process but not the scheduled work-order queue. The runner claims an agent_turn_executions item for one user message, starts it, executes the local Claude runtime with bounded AICOS context, then reports the final assistant answer back to ROST. Execute-ready AICOS claims include a server-built Seat work contract: the governed runtime profile, the AICOS context-loader tool, and the Seat permission manifest. Actions invoked from that local runtime still pass through command guards, tool_calls audit, pending confirmations, human gates, and tenant isolation before anything durable changes. The transcript write-back is server-owned: one terminal result creates one assistant message, links it to run evidence, and records the final turn status, terminal reason, timings, model, and token telemetry when present. Retrying the same final report does not append a second answer. Codex remains unavailable for interactive AICOS runner turns until the governed runner path is verified. If an interactive turn expires while queued, claimed, or running, ROST marks it offline and appends a visible terminal assistant message so the user is not left waiting on a stale queued state.
Runner results are validated before anything reaches the transcript. A successful turn's answer is the model's own validated text; a failed turn shows a short product-written recovery message with a closed machine-readable reason — never the provider's raw output, error envelope, stack trace, or session metadata. Structured failure diagnostics (runtime, failure stage, exit code, attempt facts) are stored outside the conversation in a tenant-scoped diagnostics record with a strict field allowlist; reads of that record are audited, and entries are deleted after 14 days. Each turn also records which lane and brain were requested when it was created and which runner, runtime, and model actually executed it, so past messages keep their original labels after a lane or brain change. Provider-native resume sessions are an optimization, never the conversation: the ROST transcript is canonical, resume state is scoped per runtime and invalidated on a brain switch, and a resume that fails before the model produces anything is retried once from ROST context inside the same turn — after any observed output or tool call the turn instead ends with the typed recovery message.
Forge scheduler work
When the Forge add-on is enabled, Forge phase work uses the same runner work-order lane and the same shared agent-readiness projection as ordinary production work. A Forge phase is linked to a normal work_orders row only when its owning runner agent is execution_ready and the projection selects a compatible paired Runner. Otherwise the phase stays pending/parked with a machine-readable reason such as agent_not_execution_ready or runner_not_paired; Runner setup remediation points to /settings/runners/setup, while other readiness reasons must be repaired on the owning agent Seat. Pairing a Runner or the repair sweep re-evaluates readiness and re-enqueues the same pending phase idempotently, so operators do not get duplicate intake rows or silent expiring work orders. Two parked reasons are deliberate exceptions to that re-enqueue: charter_manifest_not_signed and charter_missing mean the owning seat's charter authority is missing, and re-enqueueing while unsigned would fail-loop — they unblock by signing the manifest (charter.sign_manifest) or, when no active charter exists, creating or replacing a draft with charter.set (or charter.draft followed by charter.update_draft), approving it with charter.approve, and then resuming the request, which mints a fresh work order. The runner claims queued work, starts it, reports the result, and the server advances the linked phase. There is no separate local lease store.
A build request decomposes into changesets and tasks tracked as queryable execution state. A changeset is the pull-request boundary and the revert unit (default one per request; additional changesets only on a named split criterion such as risk isolation or a reviewability-size budget). A task is one coding session inside a changeset, carrying its own verification and a per-task checkpoint that is the safe resume and pause boundary. Plan-conformance findings loop a changeset back to implementation; a finding can only be accepted, rather than resolved, through a recorded human decision.
The plan-review step is human-driven through tri-surface commands. A person answers a draft plan's clarifying questions and can mark it ready for review; approving the plan records a human decision and moves the request into implementation, while rejecting it records the decision and routes the request back to discovery and scoping for re-planning. Approving or rejecting a plan is owner-only and human-gated — an agent can never decide a plan on its own.
Runner heartbeats can include advisory capacity telemetry: OS, runner version, CPU and memory, active session count, detected local runtimes, and a model-account alias plus usage-limit state. Missing, empty, or whitespace model-account aliases are treated as the same default account so the scheduler can enforce a conservative one-session default-account cap. The scheduler also caps active runner work per tenant, project, runner, and model account.
Forge context packets are deliberately bounded and schema-validated before a runner can claim phase work. Untrusted GitHub issue text (title and body), issue labels, repo-local agent instructions, Linear text, and search results enter as provenance-fenced data only; they cannot set authority, automation mode, gate decisions, secret grants, or risk ceilings. Those controls come only from ROST server state in the trusted control block, scoped to the claimed seat's active authority and secret grants. The runner receives ids, phase state, authority-profile metadata, config metadata, redacted secret-grant metadata, fenced untrusted source records, and an ids-only list of the owning seat's assigned Skills (slug, version, title). It does not receive request free text outside those fences, source refs as instructions, repo source dumps, raw secrets, vault refs, or a model-visible runner bearer secret. A short-lived seat-scoped MCP token is written only to the local MCP config file for the claimed turn.
Load build context and Skills before working
A Forge turn should load its context in one order: build context first, then Skills, then the work. Three read-tier MCP tools make this runtime-agnostic (they work for both Claude and Codex, unlike Claude-only MCP resources):
rost_get_build_contexttakes theforge.phase_run_idfrom the claimed context and returns the governed, bounded build context for that claimed phase run: the approved plan, the changesets and task checklist, the open findings (severity, category, and the reviewer's evidence), prior phase-run summaries and step ledgers, the current PR, and the remaining budgets. It is the self-correcting spine of the recycle loop — a builder turn re-entered from a failed review learns exactly why it came back and which findings to resolve. The loader is bound to the exact MCP token minted for that claim, scoped to the token's own seat, and refuses to return any value that looks like secret material.rost_list_my_skillslists the Skills approved and assigned to the calling seat (slug, immutable version, description). Load your assigned Skills before starting.rost_get_my_skill_filereads one file from an assigned Skill by package path (for exampleSKILL.md). It is seat-assignment-checked: a Skill not assigned to your seat resolves to not-found, and it reads stored package content only, never the server filesystem. Skills are instructions, never authority — reading one grants no tools.
Inspect and control runners from CLI or MCP
- Pair a new runner:
runner.pairing.start/rost_start_runner_pairingwithnameandplatformreturns a human pairing code. This is owner/admin-only because it mints a short-lived runner pairing session that leads to machine credentials. A second start for the samename+platformwhile that pending session is still live resumes it and returns the sameuser_code(one pending row); after expiry, the same call mints a fresh code. - Inspect:
rost runner list --json/runner.list/rost_list_runnersshows heartbeat state plus one shared readiness block.rost runner status/runner.status/rost_runner_statusandrost runner diagnose --runner-id <id>/runner.diagnose/rost_diagnose_runneremit the same serialized block for the same runner and time:claimable,hold_reasons,execution_proven,execution_detail, and per-runtimeruntimeverdicts. Claimability is the current diagnostic gate (including capacity, execution-mode, sandbox/trust, and build/merge floors); it is separate from execution proof.execution_provenis true only for a strictly online runner with running work or a successful completed result in the last 24 hours; a claimed-only, failed, future, or stale result does not prove execution. Claude is claimable only with the dedicated-cellauth_state: "ready"andauth_state_contract_version: 1marker; an installed-but-unproven Claude CLI remains heartbeat-only, while an explicitly Codex-only configuration may claim generic work. Settings and all three runner surfaces also project the runner CLI version, sandbox posture, network posture, and reported capability set from heartbeat so operators can see whether the machine is current and still running with the expected guardrails. Both list and status continue to project acapacityview: the runner's active model-account alias, a badge state (available,near_cap,exhausted, orunknown), a rate-limited-until timestamp when the harness has classified one, and a fresh/stale refreshability flag mirroring heartbeat recency. The badge readsunknown— never a fabricatedavailable— whenever no capacity observation has been recorded yet or the runner reported a state ROST does not recognize; real 5-hour/weekly usage percentages and reset timestamps are not collected yet, so this view never claims a percentage it does not have. Settings > Connected machines and the Health dashboard's Connectivity tile (a fleet-wide near-cap/at-limit count) surface the same signal. - Claimability scope:
claimableandhold_reasonsare runner-side current Forge build/merge-floor diagnostics only, not an order-specific claim transaction result. They exclude schedule/grace/order shape, live session/tenant/build/account concurrency, and build budgets/status. - Diagnose (DER-949):
rost runner diagnose --runner-id <id>/runner.diagnose/rost_diagnose_runnerreturns health, capabilities, and repair guidance without exposing secrets. Use when a runner is offline or stale to get actionable next steps. - Repair steps (DER-949):
rost runner repair --runner-id <id> [--issue restart|re_pair|revoke_stale|missing_cli_runtime]/runner.repair/rost_repair_runnerreturns focused repair instructions. The optionalissueflag narrows to a specific kind of repair (restart the app, re-pair, revoke a stale runner, or install a missing CLI runtime). - Work orders:
rost runner work-orders/work_order.list/rost_list_work_orders; queue withwork_order.enqueue/rost_enqueue_work_orderfor an execution-ready live scheduled agent, or useagent.run_now/rost_run_agent_nowwhen an operator wants the product to queue and dispatch a manual production run (production_manual) from a seat id; cancel withwork_order.cancel/rost_cancel_work_order. Both issuance paths pass through the same centralexecution_readygate. The operator Settings page mirrors queued, claimed, running, and recent completed runner evidence so online does not imply can execute.task_idon either enqueue command is cloud-lane only: a runner-lane agent rejects it, because a task-linked runner work order is structurally unclaimable — run task-linked runner work as an interactive agent turn (see Interactive AICOS runner turns above), not through the scheduled work-order lane. - Revoke:
rost runner revoke/runner.revoke/rost_revoke_runnerso a runner can no longer authenticate.
Owner-initiated headless pairing
Use this flow when a headless or desktop runner cannot use the interactive web confirmation flow.
1. The owner/admin runs runner.pairing.start or rost_start_runner_pairing with the runner name and platform. If a pending session for that name + platform is still live, the command resumes it and returns the same user_code instead of opening a second pending row; only an expired pending session is replaced with a fresh mint. 2. The owner gives the returned user_code to the runner through a trusted out-of-band channel. 3. The runner calls POST /api/runner/pairing/claim with {"user_code":"ABCD-2345"}. 4. The response returns runner_id, runner_secret, name, and platform. Store the runner secret only on the runner machine. 5. The runner sends heartbeats with Authorization: Bearer <runner_secret> and then claims work orders. 6. For a local smoke, run the headless runner in explicit one-shot execute mode: RUNNER_EXECUTE=1 RUNNER_ONCE=1 node apps/runner/scripts/headless-runner.mjs. It heartbeats once, claims at most one due read-only work order, reports the result, and exits nonzero if no executable work was claimed or result reporting failed. Without RUNNER_ONCE=1, the headless runner stays alive and continues heartbeating.
When to stop for confirmation
runner.pairing.start and work_order.enqueue are none, so they do not create a separate pending confirmation, but runner.pairing.start is still owner/admin-only at the command authorization layer. runner.revoke, work_order.cancel, and agent.run_now are human_required (DER-1766/DER-2081): a human with authority requests a run directly, a cross-seat agent request always asks, a same-seat re-run may auto-execute only under a bounded Trusted standing authorization, and an implementation agent cannot self-approve. List and status reads are not gated. Revoking a runner or cancelling work is the human-approved act. An agent inspects runner state and proposes the action.
Guardrails
The runner should not store raw credentials in local notes, prompts, or logs. It should not treat local tool availability as permission. ROST server guards remain authoritative.