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 > 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. 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 > Runners > Add a Runner, then on the host run
rost 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.
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.
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. - Inspect:
rost runner list --json/runner.list/rost_list_runnersshows heartbeat state plus execution readiness. A runner is execute-ready only when it has a live heartbeat, a claimable local runtime, and recent claim/result evidence or active queue processing. 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 runner status/list 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.rost runner status/runner.status/rost_runner_statusreads one runner with the same execution detail, due queue count, claimed/running counts, and recent result timestamps. Both surfaces also 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. - 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. 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.