@fbuildai/runner
v0.1.54
Published
egos runner — app-managed machine agent for fbuild. TERMINAL `connect` IS DEPRECATED: the fbuild desktop app starts and supervises the runner (this package remains as the app's managed entrypoint; headless automation may set FBUILD_APP_MANAGED=1).
Downloads
4,593
Maintainers
Readme
@fbuildai/runner
The fbuild runner: connect a machine (your laptop, a VM, a CI box) to an fbuild team hub. The runner dials out over a WebSocket (no inbound ports), registers the machine, and runs tasks the workspace assigns — using that machine's own Claude Code login (or the egos spine). Zero dependencies (Node 22+).
Part of fbuild: console (UI) · hub (Cloudflare Worker) · runner (this).
Quick start
# 1) authorize Claude on this machine once (reused by the runner)
claude setup-token
# 2) connect to your team hub (token from the console → "Add a machine")
cd ~ && npx @fbuildai/runner@latest connect --hub https://<team>.fbuild.ai --token <TOKEN>The machine now shows online in Soldiers. Leave it running (auto-reconnects on network drops). Run from any directory except the runner's own source folder (npx would resolve the local unlinked package).
Mapping repos
# map a repo to a local checkout, scoped to a workspace
npx @fbuildai/runner@latest add-project <owner>/<repo> \
--dir ~/Developer/<repo> --hub https://<team>.fbuild.ai --org <workspace>
npx @fbuildai/runner@latest status --hub https://<team>.fbuild.aiMapped repos appear in Projects with live git status (branch, dirty, ahead/ behind, last commit), and New task dispatches runs into them.
Routing & discovery. A repo is resolved to its workspace deterministically
from the checkout's git remote plus the hub's D1 workspace map — no owner-name
inference guessing (0.1.27). Discovery also descends into umbrella / nested
checkouts (0.1.29) and treats the repos it finds as first-class projects
(they show up in runner.hello alongside explicitly-mapped ones), so you don't
have to add-project every inner repo by hand.
Team machine (recommended)
One physical machine can serve every workspace in a team with a single connect:
# add repos per workspace first
npx @fbuildai/runner@latest add-project egosorg/fbuild-hub \
--dir ~/Developer/aceware/egosorg/hub --hub https://aceware.fbuild.ai --org fbuild
# then connect once (no --org) — fans out to every workspace in config
npx @fbuildai/runner@latest connect --hub https://aceware.fbuild.ai --token <TOKEN>What a run does
By default a run routes through the egos deterministic spine (a gated
f-run.sh in a forked git worktree) — see --executor egos
below. There is no silent fallback to Claude: a failed or empty egos run
surfaces an honest run.failed with a concrete reason.
Under the explicit --executor claude path, a run instead spawns Claude Code
headless (claude -p … --output-format stream-json --include-partial-messages)
in the project dir and streams structured events to the hub: assistant text, tool
calls + results, extended thinking, and a short auto-generated chat title.
The Claude session id is captured so a reply continues the same context
(--resume) — even after a runner restart.
Executors
The runner picks how a task runs from --executor (saved to config on connect):
| Executor | What it does |
| --- | --- |
| egos (default) | Runs the task through the egos deterministic spine — see below. |
| claude | Explicit opt-in (no longer a silent fallback): Claude Code headless in the project dir; rich streamed tool timeline. Choose it with --executor claude. |
| shell | Runs task.commands verbatim. For testing. |
| demo | Prints machine output, edits nothing. Proves the loop safely. |
As of 0.1.32 the silent egos→claude fallback was removed (#45/#46): a failed, empty, or un-startable egos run reports
run.failed+ arun.artifact status:"failed"with a concrete reason — it is never silently re-served through Claude. Executor choice is unchanged; an explicit--executor claudeis still honored, and egos can also run Claude wrapped insidef-runas a worker backend (--backend claude-code).
--executor egos
npx @fbuildai/runner@latest connect --hub https://<team>.fbuild.ai --token <T> \
--executor egos [--egos-path <egos checkout or bindir>]This is now the default executor. It routes each assigned task through egos' gated pipeline instead of calling Claude directly. For every order the runner:
- Builds a shape-valid egos Order from the hub task (synthesizes the fields
egos requires: a sanitized id,
successCriteria, acode_patchartifact, and a benign read-only command) and writes it to a temp file — never the repo. - Spawns egos'
f-run.shwith the claude-code backend, which forks a git worktree off the project dir, runs Claude headless inside egos' Tool Police (command allow/deny + budget gate), and writes artifacts/events to a runner-owned workdir under~/.egos/runtime/(kept out of your repo). - Streams
f-run's stdout+stderr verbatim to the task's Terminal tab.
Follow-up replies (order.reply) on an egos-served task route back through
the egos spine. egos has no session --resume, so instead of resuming a session
the runner builds a fresh follow-up Order carrying the reply text as the task
instruction and drives it through egos' runEgosRun — a fresh gated f-run
(same --worktree --budget-gate Tool Police as the original run) in its own git
worktree forked off the project dir, auto-falling back to Claude only on an
empty/failed artifact. (A claude-served run — native or an egos→claude fallback —
instead resumes its real Claude session via claude --resume.) Reply context
carries via the follow-up order rather than a live session id.
Prerequisites (on the runner machine): egos installed (its install.sh puts
f-run.sh on PATH) or reachable via --egos-path / $EGOS_HOME; plus claude
(the backend rides ambient Claude auth) and jq on PATH. The project must be a
real git checkout (egos forks a worktree from it).
Scope — be aware (MVP): the Terminal tab shows egos' flow/governance
progress (worktree created, command allow/deny, budget gate, manifest, gate
verdict when a PR is attached) — not Claude's per-tool stream. Parsing
egos' events.log into structured timeline rows is not yet implemented
(tracked as runner#7 / egos#338).
Degrade: if egos, claude, or jq is missing, or the project dir isn't a git
work tree, the run fails cleanly with an actionable reason in the Terminal tab —
the runner never crashes and the task never gets stuck running.
A smoke test for this wiring lives at test/executor-egos.smoke.mjs
(npm test) — it drives the real runner against a fake hub with a stubbed
f-run.sh, no real LLM required.
Keeping it alive
Foreground — run
connectand leave the terminal open. Simplest.Background —
nohup npx @fbuildai/runner@latest connect --hub … --token … &.Service (recommended on a VM) — keep it up across reboots:
macOS (launchd) —
~/Library/LaunchAgents/ai.fbuild.runner.plist:<?xml version="1.0" encoding="UTF-8"?> <plist version="1.0"><dict> <key>Label</key><string>ai.fbuild.runner</string> <key>ProgramArguments</key> <array><string>/usr/local/bin/npx</string><string>@fbuildai/runner@latest</string><string>connect</string><string>--hub</string><string>https://aceware.fbuild.ai</string></array> <key>RunAtLoad</key><true/><key>KeepAlive</key><true/> </dict></plist>launchctl load ~/Library/LaunchAgents/ai.fbuild.runner.plistLinux (systemd) —
/etc/systemd/system/fbuild-runner.service:[Unit] Description=fbuild runner After=network-online.target [Service] ExecStart=/usr/bin/npx @fbuildai/runner@latest connect --hub https://aceware.fbuild.ai Restart=always [Install] WantedBy=multi-user.targetsudo systemctl enable --now fbuild-runner
Config
Per-hub files under ~/.egos/:
runner.json— the default (aceware.fbuild.ai)runner.<host>.json— one per additional hub (e.g.runner.carna.fbuild.ai.json)
Each holds { hub, token, name, machineId, executor, projects }. The token is
saved on first connect, so the service form needs no --token. Executor
defaults to egos (routes through the egos spine — see
--executor egos), with claude as the fallback (Claude
Code headless, available until egos is verified — see egos#344); shell and demo
exist for testing.
Commands
| Command | Description |
| --- | --- |
| connect --hub <url> [--token <t>] [--name <n>] [--executor claude\|egos\|shell\|demo] [--egos-path <dir>] | join a team hub, run assigned tasks |
| add-project <owner>/<repo> --dir <path> [--hub <url>] [--org <workspace>] | map a repo to a local checkout |
| status [--hub <url>] | print the resolved config |
| ui login\|orders\|prompt\|new\|init-soc | drive the console from the terminal |
CLI aliases: fbuild-runner and egos-runner (legacy) both resolve to the same binary.
What the runner reports (runner.hello)
On connect (and re-registration) the runner sends a runner.hello frame carrying
name, os, cores, idleSlots, detected backends, and a projectInfo
array — one entry per known project (explicitly mapped and discovered):
{
"repo": "<owner>/<repo>",
"discovered": false, // true = found by discovery, not add-project'd
"hasSoc": true, // a soc/onboarding CLAUDE.md is present
"lifecycle": "continue", // compact egos verdict (new|continue|idle); null for discovered repos
"exists": true, "branch": "main", "dirty": 0, "ahead": 0, "behind": 0,
"lastCommit": "…", "lastWhen": "2 hours ago", "remote": "…", // live git status
"files": ["…"] // gitignore-aware @-mention index; empty for discovered repos
}Only presence/name booleans, git status, and file paths cross the wire — never file contents or secrets.
Order signing (per-order Ed25519 verification — WARN mode)
Since 0.1.32 the runner verifies a per-order Ed25519 signature on incoming
order-control frames (ADR 0014 Phase B). The check runs at a chokepoint before
any order.* branch acts on the frame, using a pinned public signing-root
key committed at bin/signing-root.pub.jwk (the runner never holds a private key).
It runs in WARN mode: verdicts are emitted as telemetry
(unsigned-order-accepted / invalid-order-signature) and never block an
order — until a workspace is latched by a root-signed signing-required policy, at
which point unsigned/invalid orders for that workspace are refused (no downgrade).
Fleet lane-config & LiteLLM proxy (fleet.* — ADR 0007 Phase 2)
Behind allowlisted verbs, the runner terminates the console → hub → DO fleet control path:
fleet.lane.config— routes a lane config op to the credential-blind f-lane ledger and (when the LiteLLM proxy is enabled) reconciles the machine-local proxy config. A mutating op mints a lane virtual key that resolves to a secret-manager name only — no key value ever crosses the wire.fleet.proxy.{apply,up,down,health}— lifecycle for a machine-local LiteLLM proxy (docker/config standup, teardown, loopback health probe). The handler self-repliesfleet.proxy.<verb>.resultand never throws.fleet.observe.query→fleet.observe.result— a read-only fleet-ledger snapshot (one observation per machine) so the hub's fleet view has real data.
The lane-config reconcile + key mint and the observe health/money enrichment
are flag-gated and default OFF — dormant until EGOS_LITELLM_ENABLE is
truthy on the runner; when OFF the runner touches no docker and mints no
key. The operator fleet.proxy.{up,down} control path is deliberately not
flag-gated — an authenticated console up/down is the enablement. Dispatch /
acquire / failover stay founder-gated and are not wired here.
Workspace lifecycle probe (workspace.open)
The console's config / onboarding screen asks a machine, on demand, "what's
the setup state of this workspace?" The runner answers by running the
read-only egos open <dir> --json probe on the granted checkout and streaming
a shaped verdict back. Zero-authority: it mutates nothing, reads no secrets, and
emits only presence/name booleans — never file contents.
Request (hub → runner):
{ "type": "workspace.open", "repo": "<owner>/<repo>", "requestId": "<id>" }repo is resolved to a local dir via the machine's granted projects config
(projects.find(p => p.repo === repo).dir). requestId correlates the reply.
Response (runner → hub) — always type: "workspace.lifecycle", echoing
requestId and repo:
// repo NOT granted / dir missing on this machine — fail-clean, never throws
{ "type": "workspace.lifecycle", "requestId": "<id>", "repo": "<owner>/<repo>",
"lifecycle": null, "reason": "repo not granted on this machine" }
// granted — shaped verdict + full first-setup checklist
{ "type": "workspace.lifecycle", "requestId": "<id>", "repo": "<owner>/<repo>",
"lifecycle": {
"verdict": "maintain", // maintain | first-setup
"onboarding": {
"state": "configured", // raw egos onboarding verdict
"socPresent": true,
"designSystemPresent": false
} | null,
"firstSetup": {
"allPass": false,
"checklist": [ // presence/name booleans ONLY
{ "name": "soc present", "pass": true },
{ "name": "design system present", "pass": false }
]
} | null
}
}If the probe can't run (no egos on the machine, missing dir, timeout, bad JSON),
lifecycle is null — the runner never throws and never fabricates a verdict.
Verdict vocabulary. The on-demand probe normalizes to the onboarding-guard
two-word vocabulary — maintain | first-setup: egos onboarding first-setup
→ first-setup; configured or an absent onboarding verdict → maintain.
(The compact lifecycle field carried in the runner.hello registration frame
uses egos' raw top-level verdict instead — new | continue | idle — falling
back to it only when there's no onboarding verdict; the two shapes are
intentionally distinct.)
First-setup checklist. Each checklist entry is { name, pass } — name is
the human label (or id) of an egos open first-setup item, pass is true only
when that item's status === "pass". firstSetup.allPass mirrors egos' own
all-pass flag. Only booleans and labels cross the wire; no file contents leave the
machine.
Sibling messages workspace.create / workspace.addFeature / workspace.switch
drive the platform CLI (egos create/add/switch) and reply on
workspace.output / workspace.result; workspace.open is the read-only
sibling that reports state without changing it.
© Carna · fbuild
