`homeboy runner`
Synopsis
homeboy runner <COMMAND>runner manages durable execution backends. SSH runners are a capability on a homeboy server record, so the common Lab flow uses one ID for the machine and its runner. Local runners remain standalone because they describe this machine rather than an SSH server. Both storage shapes share the same runner capability contract: workspace root, settings, environment, secret references, resources, and policy.
Runner configuration separates printable environment from secrets:
envis for non-secret values that are useful in diagnostics, such asHOMEBOY_PUBLIC_ARTIFACT_BASE_URL.secret_envis for execution-time secret references like{ "env": "NAME" }or{ "file": "~/.config/homeboy/secrets/name" }.- Command output redacts sensitive names in
envand prints onlysecret_envreferences, never resolved secret values.
Subcommands
lifecycle
Inspect runner workspace lifecycle and finalization readiness.
add
homeboy runner add <id> --workspace-root <path>
homeboy runner add <server-id> --server <server-id> --workspace-root <path>
homeboy runner add --json <spec>Options:
--kind local|ssh: explicit runner kind. Defaults tosshwhen--serveris set, otherwiselocal.--server <server-id>: existinghomeboy serverrecord for SSH runners. For SSH runners,<id>must match<server-id>.--workspace-root <path>: workspace root on the runner machine.--homeboy-path <path>: Homeboy binary path on the runner machine.--daemon: marks the runner as daemon-preferred for future commands.--concurrency-limit <n>: maximum concurrent workflows this runner should accept.--artifact-policy <label>: artifact policy label reserved for future execution commands.
enable
homeboy runner enable <server-id> --workspace-root <path>
homeboy runner enable <server-id> --workspace-root <path> --concurrency-limit 4 --artifact-policy copyEnables runner capability on an existing SSH server. This is the recommended onboarding path for any machine that should accept Homeboy runner work:
homeboy server create <runner-id> --host <host> --user <user> --port 22
homeboy runner enable <runner-id> --workspace-root <workspace-root> --concurrency-limit 4 --artifact-policy copy
homeboy runner connect <runner-id>After this, <runner-id> is both the server ID and the runner ID.
Commands that are both resource-policy hot and portable for Lab offload (full lint, test, bench run, fuzz run, and trace) auto-select a default runner when --runner is omitted. Selection is conservative:
--runner <id>always wins.--placement localkeeps a portable hot command local even when a default Lab runner is available, unless a command-specific host policy denies local execution. For benchmarks,homeboy config set /bench/local_execution '"denied"'makes localhomeboy benchexecution fail closed until the global config is changed back.lab.preferred_runneris used when it names an SSH runner, even if that runner is not connected yet.- Without
lab.preferred_runner, Homeboy auto-selects only when exactly one SSH runner is configured or exactly one SSH runner is already connected. - With a preferred or uniquely configured Lab runner,
homeboy bench <component>routes to Lab directly;--runner <id>is only needed to override an ambiguous or non-default runner selection. - Local runners are never auto-selected.
- If the auto-selected runner is disconnected, Homeboy attempts a short bounded
runner connectbefore execution. Connection failure prints the reason and falls back to local execution. - Explicit
--runner <id>also attempts to connect a disconnected runner, but connection failure remains a command error instead of falling back silently.
Observation metadata records the routing decision under metadata.lab_offload when an observed run is created. The stable contract is schema: "homeboy/lab-offload/v1": source is automatic or explicit; status is offloaded, skipped, or fallback; successful offloads include runner_id plus remote_workspace; local fallback records the runner and fallback_reason; skipped local execution records why no automatic offload was used, such as placement_local_override or no_default_runner. The same object also carries plan_id and plan-derived phase fields including sync_mode, capability_preflight, extension_parity, and patch_captured.
Commands launched through non-local homeboy runner exec run with HOMEBOY_RUNNER_HOSTED_EXEC=1 and the private HOMEBOY_RUNNER_PLACEMENT_RESOLVED=1 marker. Nested runner commands such as homeboy agent-task cook pass the non-interactive resource preflight without a public placement override because the work is already intentionally hosted on the selected runner.
For reviewer-facing output, treat runner exec stdout as operator context and
record durable artifacts through the run/artifact loop. Pass --run-id <id> plus
--artifact <path> for files/directories and --summary <path> for compact
summary evidence produced by the runner command. JSON summary files are echoed in
the structured runner.exec response under structured_summaries, with the run
id, runner id, command, declared path, and promoted artifact id/path so LLM
workflows can consume the compact result without spelunking remote stdout or
artifact directories. See
Artifact loop for runner and matrix workflows
for static HTML and matrix examples, plus proposed attach/promote command shapes
for successful runner jobs that produced files but registered zero artifacts.
Lab offload support is intentionally command-specific:
| Command | Auto offload | Explicit --runner | Decision |
|---|---|---|---|
agent-task cook / agent-task run-plan | Yes | Yes | Portable agent-task execution when the command has deterministic gates where required. |
agent-task retry --run | Yes | Yes | Retry plus execution follows the portable agent-task run path. |
agent-task run / status / logs / artifacts / review / providers | No | Yes | Runner-resident inspection and queue interaction. |
agent-task auth status | No | Yes | Runner-resident auth diagnostics. |
bench run / default bench run | Yes | Yes | Safe single-workspace replay; local baseline/ratchet writes are treated as mutation flags. |
build | No | No | Local quality gate; extension-owned build commands are not represented in the portable Lab contract yet. |
fuzz run / default fuzz run | Yes | Yes | Safe single-workspace replay with fuzz-specific case/result artifacts. |
lint full workspace | Yes | Yes | Safe single-workspace replay; --fix is treated as a mutation flag. |
lint --changed-since / lint --changed-only | No | No | Runs locally for now because changed-file scopes are not represented in the Lab portability contract yet. The Lab plan records the skipped local-only decision. |
review | Yes | Yes | Scoped lint/test umbrella using the same portable quality gates. |
rig check | No | Yes | Runner-side rig validation without starting local services. |
test full workspace | Yes | Yes | Safe single-workspace replay with runner extension parity preflight. |
test --changed-since | No | No | Runs locally for now because changed-since test selection depends on git base refs that Lab sync may not have fetched. The Lab plan records the skipped local-only decision. |
trace | Yes | Yes | Safe single-workspace replay with Playwright/browser capability gate. |
tunnel preview-consumer run | No | Yes | Runner-hosted preview consumer execution. |
tunnel service expose / tunnel service start | No | Yes | Runner-hosted private service tunnel lifecycle. |
rig up | No | No | Stays local because rig pipelines manage local services, leases, ports, and declared filesystem paths that the current single-workspace snapshot cannot safely mirror. |
Local-only resource-pressure commands still get resource-policy warnings, but those warnings explain why Lab offload is unavailable instead of suggesting --runner.
Stale daemon diagnostics and local recovery
When a connected SSH runner has a non-fresh daemon admission session, normal
runner exec remains fenced: it does not rotate or submit work through the
stale daemon. Use the diagnostic transport for bounded direct SSH inspection:
homeboy runner exec <runner-id> --ssh -- homeboy daemon statusDiagnostic SSH is available only when the runner is disconnected or its daemon
admission is non-fresh. It bypasses daemon job admission, does not materialize
extensions or rotate the tunnel, and preserves the runner id, command, and an
optional --run-id in the normal runner execution evidence.
If runner disconnect reports endpoint_identity_mismatch, it retains local
state and never sends a lifecycle mutation to the foreign endpoint. After the
read-only SSH status probe proves an authoritative zero active-job count, retire
only the controller-local state with:
homeboy runner disconnect <runner-id> --local-recoveryThis operation does not claim the remote daemon was stopped or torn down.
Configure a preferred runner with:
homeboy config set /lab/preferred_runner '"<runner-id>"'doctor
homeboy runner doctor local
homeboy runner doctor <runner-id>
homeboy runner doctor <runner-id> --path <component-path> --extension rust
homeboy runner doctor <runner-id> --require-tool zip --require-tool unzip
homeboy runner doctor <runner-id> --scope lab-offload
homeboy runner doctor <runner-id> --scope lab-offload --repairDiagnoses a local or configured SSH runner without mutating it. A connected
session is read once from controller state and checked through a bounded
/health request; doctor does not reconcile the session, create an exec job,
replace the session, or tear down its tunnel. Use local,
localhost, or self to inspect this machine without creating a runner record.
The JSON payload uses command: "runner.doctor" and includes runner_id,
status, capabilities, and warning/error details when a capability probe fails.
status: "error" means the runner is not ready and exits with status 1; ok
and non-blocking warn reports exit 0. The command has no report-only mode.
Use doctor before connect when you need to know whether Homeboy, Git, SSH,
and the configured workspace root are usable on the target machine.
preflight
homeboy runner preflight --request '{"schema":"homeboy/placement-readiness/v2","runner_id":"homeboy-lab","invocation":{"kind":"agent_task_cook","provider":"openai","source_path":"/workspace","selector":"openai.default","model":"gpt-5"}}'
homeboy runner preflight --request '{"schema":"homeboy/placement-readiness/v2","runner_id":"homeboy-lab","invocation":{"kind":"capability_audit","source_path":"/workspace","capability_id":"browser"}}'preflight creates no run, rig lease, runner job, workspace, or connection. Its
homeboy/placement-readiness/v2 response is ready or blocked with exact
predicates and typed executable recovery actions (including action ID, argv,
safety, required confirmations, and evidence). A full runner is blocked, not
queued. Execution revalidates live admission
facts through the same compiled admission plan before reserving a daemon admission, so this result is evidence, not a
reservation. Wrapper order is preflight, durable run persistence, then setup and
materialization.
Safety manifest metadata marks runner connect as an explicit operator
lifecycle action. It does not currently expose a dry-run contract; use
runner doctor for preflight diagnostics before changing runner lifecycle state.
Use --scope lab-offload before serious Lab evidence runs. It adds checks for
the configured runner Homeboy command, bare homeboy PATH resolution, preferred
runner binaries, connected daemon health readiness, and Sample Runtime runner-path
freshness signals. When Homeboy can identify a safe exact recovery command, the
check includes that remediation instead of leaving the operator to infer it from
logs.
Use --scope lab-offload --repair for the narrow self-healing path. Today this
reconnects a failing direct Lab runner daemon and reruns the daemon exec probe.
It does not upgrade binaries, rewrite runner paths, or refresh Sample Runtime caches;
those remain explicit operator actions because they can be expensive or depend
on environment-specific paths.
runner status includes generic selected_lab_runner.executable_requirements
from declared agent runtime manifests. Each entry names the runtime, requirement
id, non-secret env vars, candidate executable names/paths, optional version
arguments, and install hint. Homeboy owns the generic semantics: runtimes declare
the executable they need; runner diagnostics expose the declaration without
embedding product-specific checks in core.
runner status also includes selected_lab_runner.selected_runtime_runtime for the
legacy runner job environment projection. That block shows the configured WP
Sandbox CLI, managed cache source/binary, expected
@automattic/sample-runtime-playground and @automattic/sample-runtime-core paths, a
runner-side probe command that prints the exact effective runtime and git SHA,
and warnings when configured paths mix a stale checkout with the managed cache.
refresh-homeboy
homeboy runner refresh-homeboy <runner-id> --reconnect
homeboy runner refresh-homeboy <runner-id> --ref v0.434.30 --dry-run
homeboy runner refresh-homeboy <runner-id> --select /path/to/homeboy --reconnecthomeboy runner connect <runner-id> is the normal way to update a runner: it
brings the runner’s binary to the controller’s release
(see Binary Convergence),
and stale-runner diagnostics recommend it. refresh-homeboy is for explicit
refs (--ref), selecting an existing binary (--select), downgrades and
rollbacks (--allow-downgrade), and forced reconnects; unreleased builds use
runner dev-sync.
Installs the controller’s release tag by default. Materialize mode uses the
existing verified release installer in stage-only mode, then publishes the
binary into the content-addressed slot
<workspace_root>/_homeboy_binaries/homeboy-<sha256>/homeboy and updates
homeboy_path. The runner must have a workspace_root. An explicit --ref
must be a release tag (vMAJOR.MINOR.PATCH); branches, commits, alternate
source repositories, and alternate target directories are not supported. Select
mode probes the exact binary passed to --select before updating
homeboy_path.
The downgrade guard compares semantic versions with the controller, active
daemon, and configured binary. An intentional rollback requires
--allow-downgrade. No Git history or ancestry proof is fetched.
Unreleased builds reach Lab only through runner dev-sync; a source-built
controller no longer automatically converges Lab to its commit.
Use --reconnect when the active daemon/session should be refreshed immediately.
Without it, the JSON output includes follow-up disconnect, connect, and
status commands so operators can restart a session at the right time. runner status also reports controller, configured executable, active daemon version,
build identity, drift signals, and refresh commands under
selected_lab_runner.runner_homeboy.
Reconnect rollback restores the exact previously selected binary and publishes
its session. If any rollback phase fails, Homeboy retains
runner-sessions/<runner-id>/refresh-partial.json; the error names that path and
one refresh-homeboy --select <previous-path> --reconnect continuation command.
Do not delete the receipt as a recovery step.
When a configured SSH runner is disconnected, refresh-homeboy automatically
uses its configured SSH server transport to run the same release install/select
script with a bounded deadline. The selected binary and runner configuration are
updated only after that script reports a clean identity matching the requested
release version. The fallback is restricted to configured SSH runners; local
runners retain their existing direct execution path.
Refreshing back to a release changes only the selected homeboy_path; existing
runner environment and resource provenance remain intact.
By default, stdout is a bounded runner.refresh_homeboy operator projection.
It includes the exit state, selected binary, phase summary, failure/readiness
state, and durable refresh artifact refs without streaming the generated script
or build transcript. Pass --full to emit the complete refresh result on stdout.
Global --output <path> always writes the lossless command-result envelope,
regardless of --full; use it when automation needs every response field.
While a refresh is running, stderr emits bounded noninteractive progress lines
with the HOMEBOY_REFRESH_PROGRESS prefix. The JSON payload includes the
durable run_id, coarse phase: "refresh", requested_mode (materialize or
select), and state; heartbeat lines additionally include heartbeat: true.
These lines are operator progress only and never contain child stdout/stderr or
secrets. The final stdout envelope and persisted artifacts use the same run ID.
Every refresh attempt creates one terminal observation run. Its redacted
materialization script and complete build/select transcript are persisted for
both successful and failed execution; planning or execution errors persist their
redacted error record. The default projection returns the run id and artifact
URIs. Retrieve them with homeboy runs artifacts <run-id> or inspect the run
with homeboy runs evidence <run-id> --full. Persisted artifacts follow the
standard terminal-run retention window and can be inspected or cleaned through
homeboy runs artifact cleanup-persisted and homeboy cleanup --include persisted-run-artifacts.
dev-sync
homeboy runner dev-sync <runner-id> --homeboy-source /path/to/homeboy --reconnect
homeboy runner dev-sync <runner-id> --homeboy-binary /path/to/target/release/homeboy
homeboy runner dev-sync <runner-id> --homeboy-source . --extensions sample-runtime=/path/to/runtimerunner dev-sync is the fast-loop handoff for iterating against a Lab runner
without cutting a release. It builds the controller-local Homeboy source checkout
unless --homeboy-binary points at a prebuilt local binary, hashes that binary,
uploads it to a runner-managed content-addressed slot under
<workspace_root>/_homeboy_binaries/dev/<hash>/homeboy, probes it with
self identity, and selects that exact runner-side binary using the same
selection machinery as runner refresh-homeboy --select.
The selected dev binary becomes authoritative for both daemon and child executor
processes: Homeboy updates runner.homeboy_path, prepends the selected binary
directory to the runner PATH, and sets HOMEBOY_COMMAND to the selected
absolute binary path. This avoids stale homeboy binaries shadowing runner jobs
through inherited PATH state.
The command records dev provenance under the runner resources.dev_sync block:
binary SHA-256, content hash, runner-side path, source revision, dirty flag, and
deferred extension specs. runner show exposes the full runner record and
runner status surfaces the same block under
selected_lab_runner.runner_homeboy.dev_sync so operators can see dev mode at a
glance.
--extensions id=path is accepted and recorded in the dev-sync provenance. The
binary handoff is first-class in this release; extension source relink/parity is
deferred and remains visible in extensions_deferred output rather than silently
pretending extension parity was changed.
The JSON output includes next_actions, including a ready-to-copy
runner status command and the release/ref refresh command that reverts the
runner from dev mode.
Pass one or more --require-tool <command> values when a provider or job path
knows it needs additional runner-side commands before starting expensive work.
Doctor resolves each command on the runner PATH and reports missing requested
tools as tool.required.<command> errors with install/setup remediation. This is
generic: provider layers declare their own tools; Homeboy core only checks command
availability.
Programmatic runner execution can use the same generic boundary through
RunnerCapabilityPreflight.required_commands. Those commands are checked before
remote execution starts, alongside existing required tools, components, and
environment variables.
Runner-managed dependency sources
Workspace state location
Homeboy may add only homeboy.json to a repository. Every other byte it
produces lives outside the checkout. For a runner workspace at <checkout>,
runner-owned state lives in the sibling <checkout>-homeboy-state/ directory:
workspace metadata, @file scratch, the prepared-source-ready marker,
lab-source-evidence.json, gate-receipts/<uuid>.json, and lab-stacks/
checkouts. Run artifacts (provider evidence inputs, rig registries) live in
<checkout>-homeboy-artifacts/. Each location has one shared resolver in
homeboy_lab_contract::lab::handoff, and workspace removal and pruning clean
these siblings. Source snapshots carry no .homeboy exclude: a .homeboy
directory in a checkout is ordinary user content.
Portable Lab/evidence runs should declare runner dependency sources instead of
smuggling local checkout paths through workflow-specific environment variables.
Homeboy’s generic contract is the extension/component setting
validation_dependencies: each entry is a component id or explicit checkout
path that the runner workspace sync treats as managed source input.
For each declared dependency, homeboy runner workspace sync:
- resolves a sibling checkout, registered component, or deterministic clone;
- rejects dirty, stale, divergent, missing-upstream, or ambiguous Git state;
- runs the dependency install/build lifecycle in a prepared copy;
- materializes the prepared dependency beside the primary runner workspace; and
The JSON output includes
validation_dependencies, with each dependency’sid,role, controllerlocal_path, runnerremote_path, andevidence_path. Theevidence_pathpoints into the dependency’s sibling<remote_path>-homeboy-state/lab-source-evidence.json; dependency lifecycle records land in the artifact store under<artifact-root>/dependency-lifecycle/<run-id>/. Bench, trace, eval, and provider layers can consume that generic output to populate their own path settings without knowing Lab filesystem layouts or dependency-specific environment variable names.
Example manifest fragment:
{
"extensions": {
"provider-id": {
"settings": {
"validation_dependencies": ["runtime-component"]
}
}
}
}Pass one or more --extension <id> values to validate extension parity before
Lab offload. Doctor runs the same homeboy extension show <id> contract on the
target runner that test offload uses at execution time. --path sets the probe
working directory when the extension should resolve from a specific component
checkout. Missing extensions are reported as extension.parity errors with an
install command such as homeboy extension install <source> --id rust.
Lab offload for portable resource-pressure commands uses the same capability vocabulary before running on
an explicit --runner. Homeboy currently gates lint, test,
bench, and trace against the source worktree’s lightweight tool signals:
package.jsonrequiresnodeandnpm.pnpm-lock.yamlrequiresnodeandpnpm.composer.jsonrequiresphpandcomposer.- Docker/Compose files require
docker. tracerequires Playwright plus browser binaries.
When an explicit runner is missing required tools, the command fails before
workspace sync with a runner_capabilities validation error and remediation.
The same central policy returns a local fallback reason for future automatic
Lab offload selection.
refresh-plan
homeboy runner refresh-plan --runner <runner-id> --workspace . --runner-cwd /runner/workspaces/app --run-id matrix-refresh-1 --produces artifacts/review --summary artifacts/review/summary.json -- npm testPlans a runner-backed refresh loop before dispatching matrix-style work, without
executing the workload. Its RunnerExecutionEnvelope owns typed dispatch,
workspace mapping, secret and lifecycle policy, and declared artifacts. The v2
handoff contains only the planned execution record and Homeboy binary provenance
(controller CLI vs. runner configured binary vs. active daemon, with
version/build drift diagnostics). Ordered next_commands verify the runner,
sync the workspace, run the refresh, and inspect the produced evidence.
Source and fixture paths passed with --source/--fixture must exist before the
plan is emitted; --sync-mode accepts snapshot, snapshot-git, or git.
Declared produced evidence uses --produces and --summary. --produces was
spelled --output through 0.321.0, which collided with the process-wide global
--output <PATH> ("write the JSON result envelope to this file") and left both
meanings unreachable in a clap-defined order (#10566). On this command --output
now unambiguously means the global envelope path; use --produces to declare a
produced artifact.
connect
homeboy runner connect <runner-id>
homeboy runner connect <runner-id> --adopt-live-lease <lease-id> --expected-live-pid <pid>Starts a loopback-only Homeboy daemon on the runner and opens an SSH tunnel to
it. Connect dials the configured server host and does not discover a new LAN
address. SSH exit 255 with Host is down is transport_unreachable for that
recorded host, not a missing Homeboy install.
A plain runner connect (no recovery flags) first converges the runner’s
configured Homeboy binary on the controller’s release and reports the outcome in
binary_convergence (converged, already_current, pending_active_jobs with
blocking_job_ids, skipped_dev_build, skipped_remote_newer,
skipped_unverifiable, or failed with detail). It never downgrades, never
interrupts active jobs, and a failed install still connects with the existing
binary. See Binary Convergence. A numeric DHCP address is not
stable: set a stable hostname or a DHCP reservation with homeboy server set,
then reconnect. Host key verification stays on. See
Unreachable recorded host.
This is the preferred Lab execution path because later runner exec calls
can use the daemon session instead of ad-hoc SSH command execution. The JSON
payload uses command: "runner.connect" and reports connection state such as
the runner ID, tunnel endpoint, daemon endpoint, and persisted session metadata.
Set HOMEBOY_CONTROLLER_PROXY to an explicit http, https, socks5, or
socks5h controller-local proxy URL to expose it to a direct SSH runner. Connect
then owns a runner-loopback reverse forward and injects its credential-free
loopback URL as HTTP_PROXY, HTTPS_PROXY, and ALL_PROXY for daemon jobs that
do not already set those values. Session status records the forward PID and URL;
rotation preserves it and disconnect tears it down. With the variable unset,
runner connections and job environments are unchanged.
Reconnect distinguishes a healthy endpoint from a lost local tunnel, a live exact-owner PID whose recorded listener no longer answers, a dead PID with a retained lease, and an endpoint whose lease/PID identity differs from the controller record. Only the exact lease, PID, loopback address, configured build, and authoritatively zero durable jobs permit automatic unhealthy-listener replacement. The lease-bound stop rechecks the startup ownership token and job store before every signal. Active work, PID/lease drift, and ambiguous listener ownership remain fail-closed.
When a remote daemon is dead, or its state record is missing, runner connect
starts its successor. The successor’s owner lock refuses a still-live holder,
and its startup recovery reconciles every job the dead generation left in its
scope (see homeboy daemon startup recovery),
so durable work never blocks the start. Jobs that recorded no child identity
are left for homeboy runner reconcile <runner-id> --attest-absent. Live,
ambiguous, and lease-mismatched daemon records remain protected.
Every daemon returned by a replacement operation is journaled before tunnel health and endpoint identity checks. A retry authenticates and publishes those exact coordinates. If the recorded PID has died, a newer lease has superseded it, or the exact idle replacement listener is unhealthy, the stale journal is retired under the runner lock so it cannot pin a dead endpoint.
Broker authentication and pairing
Every authenticated /runner/* daemon route (/runner/staging/capabilities
and /runner/staging/direct) requires a scoped bearer token (#2990). Lab
runner file transfer (@file arguments, provider evidence, structured-output
download) does not use the daemon: it always runs over direct SSH to the
runner’s server_id, and every upload is verified by remote SHA-256 and size
before it succeeds. The trust model is:
- Pairing. On the controller, mint a credential with
homeboy runner broker pair <id> --runner-id <runner-id>. Unless--no-installis given, the hash-only store is installed on the SSH runner host. Pairing prints a one-time bearer token and stores only its SHA-256 hash in~/.config/homeboy/broker_auth.json(0600). The plaintext token is shown once and never re-displayed or logged. - Scope. Pairing grants the
submitscope, which authorizes staged submission. Credentials stored with the retiredworkscope still load but grant no route. - Runner-id binding. Requests carry a
runner_id; the presented token must belong to a credential paired to that exactrunner_id. - Revocation.
homeboy runner broker revoke <id>disables a credential.homeboy runner broker listshows non-secret credential metadata (never tokens or hashes). - Secure by default. With no credentials configured, the broker rejects all
/runner/*traffic with a structuredbroker.auth_denied(401) error. To keep an existing loopback-only smoke setup working without auth, set"allow_unauthenticated_loopback": trueinbroker_auth.json; this is only honored when the broker is bound to loopback.
Controllers read their submit token from HOMEBOY_BROKER_TOKEN, falling back
to the newest active submit credential for the runner in broker_auth.json. Tokens travel as both X-Homeboy-Broker-Token and
Authorization: Bearer <token> so they survive proxies that strip one form.
Unauthenticated, mismatched-runner, or wrong-scope requests are rejected with a
structured broker.auth_denied error and never expose the stored secret.
Daemon and broker HTTP responses use one canonical envelope. The outer response
reports transport success and the endpoint payload always lives under
data.body; runner clients require that shape and do not parse legacy direct
data payloads.
lifecycle
Runner lifecycle output uses generic contracts over the direct daemon tunnel. JSON consumers should prefer these contracts:
RunnerSession: persisted connection/session ownership, including mode, role, controller/broker references, process IDs, version identity, and heartbeat.RunnerJob: active or inspected job state with lifecycle owner, claim lease, command, status, durable run linkage, and artifact refs.RunnerWorkspaceLease: controller-owned local-to-runner workspace materialization with source ref/commit/dirty metadata.RunnerWorkspaceLeaseSet: named workspace leases for jobs that materialize more than one tree.primarynames the default execution workspace; entries inleasesbind names such assource,rig, orruntimeto their ownRunnerWorkspaceLease. Existing single-workspace output keepsworkspace_leasewhile migrations addworkspace_leases.RunnerResult: terminal command result with exit status, stream sizes, mirrored run linkage, and artifact refs.RunnerArtifactRef: stable artifact identity plus path/URL/hash/transport metadata for later retrieval.RunnerMutationArtifacts: transport-independent mutation return shape. When a runner command captures source changes,mutation_artifacts.patch_refpoints at the patch artifact; future file-bundle and operation-log returns usefile_bundle_refandoperation_log_refwithout changing caller logic.LabRunnerHandoff: the controller-to-runner handoff envelope tying transport, owner, job, workspace lease, and result together.
Compatibility fields such as active_jobs, job, job_id, job_events, and
workspace_lease remain present for existing callers. New integrations should
read active_runner_jobs, runner_job, workspace_leases, runner_result,
mutation_artifacts, artifact_ref, and handoff so transport details stay
hidden behind the same lifecycle vocabulary.
Connected daemon status also reports stale runtime-path signals. When the daemon
started with configured HOMEBOY_*_COMPONENT_PATH, HOMEBOY_*_PLUGIN_PATH,
HOMEBOY_*_PROVIDER_PATH, or HOMEBOY_*_RUNTIME_PATH values, /version
captures start-time fingerprints and compares them with the current on-disk
paths. homeboy runner status <runner-id> surfaces those differences in
stale_daemon.stale_runtime_paths, and surfaces runner config path changes in
stale_daemon.changed_runtime_paths. This is intentionally read-only: refresh a
development runner with homeboy runner disconnect <runner-id> followed by
homeboy runner connect <runner-id> after rebuilding runner-side runtime code.
Daemon compatibility is compared or unverified
stale_daemon.verification names which question the report answered:
compared— the runner’s configured identity was read and compared. The version and build-identity fields are observations, and a mismatch fences admission.probe_failed— the identity probe itself failed. Nothing was compared;stale_daemon.probe_errorcarries the verbatim failure andcurrent_homeboy_versionis the sentinelunverifiedrather than a substituted value. A runner that has a probe and could not answer it is still fenced. Reports with theprobe_failedverdict carryseverity: unknownand a read-onlyhomeboy runner doctor <runner-id> --scope lab-offloadrecovery: replacing a binary is not the answer to "I could not look at it".
Lab contract compatibility
Lab admission requires equal lab_contract_version and capabilities covering the
workload, not equal Homeboy builds. A legacy runner without a contract version
is accepted only when its capabilities cover the workload. Contract mismatch
reports lab_contract_version_mismatch with runner-refresh recovery.
require_exact_homeboy_version opts into exact-build fencing. Independently,
a daemon that differs from its own configured binary or is unreachable still
blocks admission. Controller build drift alone does not make a daemon stale.
{
"success": true,
"data": {
"status": 200,
"endpoint": "runner.jobs.submit",
"body": {
"job": {},
"poll": {}
}
}
}broker
homeboy runner broker pair ctrl-1 --runner-id homeboy-lab
homeboy runner broker pair ctrl-2 --runner-id homeboy-lab --no-install
homeboy runner broker revoke lab-1
homeboy runner broker listManage broker authentication and pairing from the controller.
pair mints a one-time submit-scoped bearer token (printed once; only its SHA-256 hash
is stored under ~/.config/homeboy/broker_auth.json). revoke disables a
credential by id, and list reports non-secret credential metadata. See
Broker authentication and pairing for the
full trust model.
service
homeboy runner service install homeboy-lab
homeboy runner service status homeboy-labRun the runner daemon as a systemd user unit that the runner owns. install
requires an idle runner. It writes the unit, hands the controller-started
daemon over to it, stops idle daemons left by earlier rotation, and marks the runner
service_managed. After that, connect only attaches, disconnect only
detaches, and refresh-homeboy --reconnect repoints the unit’s binary and
restarts it instead of replacing the daemon. status reports the unit
and the daemon lease it holds. See
Runner-Owned Service.
job
homeboy runner job list <runner-id>
homeboy runner job list <runner-id> --all
homeboy runner job list <runner-id> --json
homeboy runner job logs <runner-id> <job-id>
homeboy runner job logs <runner-id> <job-id> --follow --poll-ms 1000
homeboy runner job logs <runner-id> <job-id> --follow --cursor 65028
homeboy runner job cancel <runner-id> <job-id>Inspects or cancels durable runner daemon jobs after runner exec or Lab offload
has submitted work to a connected runner. logs fetches the persisted job plus
its event stream; --follow keeps polling until the job reaches a terminal state
and prints newly observed events as they arrive. Use this when a controller exits
after dispatching runner work and you need to inspect the already-started job.
job list defaults to a compact table of live daemon jobs. Use --all to
include observed terminal jobs, and --json for the structured response,
including the live job count.
cancel requests cancellation for a queued or running durable runner daemon job
through the connected runner daemon.
Follow tracks the highest displayed daemon event sequence. A controller tunnel
loss opens a new ephemeral SSH tunnel to the runner’s current session daemon,
and verifies that daemon’s lease and PID through /health before
reading. It never reuses or probes the recorded local tunnel port. Replayed
events are sorted and emitted only when their sequence exceeds the cursor. The
final JSON output includes next_cursor and resume_command; Ctrl-C prints the
same continuation command without cancelling remote work. A job absent after a
successful authoritative recovery is reported as retention/eviction rather than
a transient connection failure. cancel uses the same owner recovery path when
its current controller tunnel has disappeared.
status
homeboy runner status <runner-id>
homeboy runner inventory <runner-id>Shows the persisted tunnel/session state for a runner. Use this to determine
whether runner exec will use a connected daemon or needs an explicit fallback.
inventory (homeboy runner inventory <runner-id>) inspects the connected daemon without occupying a
job slot. It scans only managed workspaces, cargo targets, artifacts, runtime
scratch (including isolated gate homes), managed Rust package caches, and
Homeboy’s browser cache. A shared two-second scan budget bounds filesystem work;
results are reused for 60 seconds and concurrent scans are coalesced. complete: false marks a partial lower-bound total, not an empty root. Symlinks are not
followed. A blocked filesystem syscall can exceed the cooperative scan deadline;
the controller request also has a three-second transport timeout.
Admission and status carry CPU/IO/memory pressure in hundredths of a percent.
Linux PSI uses CPU some and IO/memory full, all on the sustained avg300
window. Without CPU PSI, loadavg per logical CPU is used; unavailable IO/memory
readings remain null. runner.settings.pressure_thresholds configures integer
percentage limits (cpu: 10, io: 10, memory: 5, load_per_cpu: 100 by
default). Limits block only new admissions; existing work and the concurrency
ceiling are unchanged. Placement avoids pressured runners and reports pressure
when none are eligible.
The JSON payload uses command: "runner.status" and reports whether a saved
session exists, whether the tunnel still appears live, and the recorded endpoint
details.
Status output also carries the former Lab readiness handoff. extra includes
preferred_lab_runner, selected_lab_runner.runner_homeboy, and
managed_followups with copyable commands for recent run discovery, evidence
inspection, runner doctor --scope lab-offload, runner environment inspection,
daemon refresh, binary upgrade, managed runner exec, and workspace sync. Use
this output before assuming a runner has picked up a merged Homeboy CLI fix.
Runner-side extension updates use the generic runner/extension primitives instead of a Lab-specific sync command:
homeboy runner exec <runner-id> -- homeboy extension install <source> --id <extension-id> --ref <ref> --replace
homeboy runner exec <runner-id> -- homeboy extension refresh <source> --id <extension-id> --ref <ref>Status output is also the operator handoff for active runner jobs. When the
connected daemon can report active work, each active job
includes lifecycle timing fields such as updated_at_ms, heartbeat_age_ms,
claim_id, claimed_by_runner_id, claimed_at_ms, claim_expires_at_ms, and
claim_expires_in_ms. The top-level operator_commands array gives copyable
next actions for the current session, including job log following, cancellation,
and artifact lookup/fetch commands when the status payload has enough context.
peer-sessions
homeboy runner peer-sessions <runner-id>
homeboy runner peer-sessions <runner-id> --cursor <cursor> --applyInspects persisted peer-session snapshots for one runner. Use --apply only to
remove sessions proven dead by the inspection; --cursor continues a paginated
result set.
reconcile
homeboy runner reconcile <runner-id>
homeboy runner reconcile <runner-id> --attest-absentReconciles one direct runner’s current daemon session: it refreshes the session metadata from the observed daemon and settles jobs that reached a terminal state. A disconnected runner is reported as-is. Successful refresh, reconnect, and runner upgrade observe the same session.
--attest-absent records the operator’s attestation that no workload process
from a dead daemon generation survives. It runs the remote daemon recover --attest-absent <job-id>... only when reconcile is blocked on jobs startup
recovery could not prove gone, naming exactly those jobs, and then reconciles
again. The runner refuses the whole set if any job has a recorded child, belongs
to the live generation, or is out of scope.
The command succeeds only when its postcondition has converged: the runner’s current daemon session is accepting jobs. A blocked result exits non-zero and reports the remaining blocker, one different executable action, and the concrete predicate that must become true before another reconcile can make progress.
disconnect
homeboy runner disconnect <runner-id>Closes a persisted runner tunnel session and removes its local session state.
The JSON payload uses command: "runner.disconnect" and reports which session
state was removed. This is safe to run when no live session exists; it is the
explicit cleanup counterpart to runner connect.
list
homeboy runner list
homeboy runner list --fullRows are always returned under data.entities ([] when no runners are
configured). The default returns bounded inventory summaries (identity, kind,
connection, admission, concurrency, drift, next action); --full returns the
complete redacted runner records plus sessions. The former
runner_summaries key was removed in favour of data.entities.
show
homeboy runner show <id>set
homeboy runner set <id> --json <JSON>
homeboy runner set <id> --base64 <BASE64_JSON>
homeboy runner set <id> --json '{"workspace_root":"/srv/homeboy","concurrency_limit":4}'
homeboy runner set <id> --json '{"runner_exec_wait_timeout_secs":2400,"cancel_on_wait_timeout":true}'Updates a runner by merging a JSON object into the runner config. SSH runner settings live under servers/<id>.json as the server’s runner capability; local runners live under runners/<id>.json.
Arbitrary runner updates must use --json or --base64; positional key=value and trailing arbitrary --key value updates are not accepted.
runner_exec_wait_timeout_secs controls how long the controller waits after a
runner accepts a job (0 returns an in-flight handoff immediately).
cancel_on_wait_timeout controls whether an expired controller wait requests
remote cancellation. Its unset default is true for agent-task workloads and
false for other runner commands. If cancellation returns a terminal job,
Homeboy returns its terminal non-success result rather than an in-flight handoff.
trust
homeboy runner trust <runner-id> --project <project-id> --command test --command bench --allow-raw-exec false
homeboy runner trust <runner-id> --workspace-root <runner-workspace-root> --artifact-policy metadata
homeboy runner trust <runner-id> --peer <controller-server-id> --fingerprint SHA256:...Persists controller-side trust policy for a runner. Policy is stored in the runner config as policy, not in transient CLI state. Repeated values are appended without duplicates.
Policy fields:
--project <id>allows a project to use the runner.--command <family>allows a command family such astest,bench,lint,trace,cargo, orrunner.exec.--allow-raw-exec <true|false>controls arbitraryrunner execshell access. SSH runner raw exec is denied by default until this is explicitly true.--workspace-root <path>limits execution to one or more approved runner workspace roots.--artifact-policy <label>records artifact behavior;noneanddenyblock patch capture.--peer <id>records accepted peer/controller server IDs for runner pairing.--fingerprint <value>records expected peer host keys or equivalent fingerprints.
pair
homeboy runner pair <runner-id> --peer <controller-server-id> --accept-project <project-id> --workspace-root <runner-workspace-root>
homeboy runner pair <runner-id> --fingerprint SHA256:... --allow-raw-exec falsePersists runner-side pairing policy for trusted controllers. pair writes the same durable policy object as trust, using runner-side option names for accepted peer IDs, accepted project IDs, peer fingerprints, workspace roots, and raw exec policy.
remove
homeboy runner remove <id>exec
homeboy runner exec [HOMEBOY_OPTIONS] <runner-id> -- <command...>
homeboy runner exec --project <project-id> --cwd /runner/workspace/project <runner-id> -- <command...>
homeboy runner exec --sync-workspace /local/project@patch --require-path /runner/resources/input <runner-id> -- <command...>
homeboy runner exec --ssh --cwd /runner/workspace/project <runner-id> -- <command...>
homeboy runner exec --run-id ssi-fixture-matrix-summary <runner-id> -- <command...>
homeboy runner exec --cwd /runner/workspace/project --require-path /runner/workspace/project <runner-id> -- <command...>
homeboy runner env <runner-id>exec submits the command to the connected runner daemon when homeboy runner connect <runner-id> has established a live loopback tunnel. If no daemon session is connected, local runners execute directly and SSH runners require explicit diagnostic --ssh. SSH runner raw exec is policy-denied by default until policy.allow_raw_exec is explicitly true.
recipe-run
homeboy runner recipe-run <runner-id> --provider <provider-id> --sync-workspace <local-workspace> --recipe <relative-recipe> --artifacts <relative-artifact-directory> --run-id <durable-run-id>recipe-run snapshots its declared workspace once, resolves the recipe and artifact
directory beneath that materialized root, and dispatches an extension-owned provider.
The provider descriptor declares its executable and version; Homeboy preflights the
executable through the normal runner execution path and records the provider,
resolved argv, source snapshot, terminal JSON result, and promoted artifacts on the
durable run. Recipe and artifact paths must be non-empty relative paths without
parent traversal. runner exec remains available for low-level invocation.
The Homeboy contract is the installed extension manifest’s
recipe_run_providers array. Each descriptor has id, version, executable,
and a command argv array. {recipe} and {artifacts} tokens expand to the
validated workspace-relative paths without shell parsing. Homeboy discovers these
descriptors from installed extensions at execution time; product extensions can
therefore supply providers without being linked into the Homeboy binary.
Inspect the installed provider inventory before dispatching:
homeboy runner recipe-providersThe JSON output lists each declaration’s provider ID and version, owning extension,
installed source reference, executable, structural resolvable state, and validation
state. resolvable does not probe executable readiness on a selected runner; execution
uses the normal runner preflight path. Invalid and duplicate declarations remain visible
but unresolvable; recipe-run fails closed for either.
Unknown provider errors include this exact inventory command and a bounded list of
available provider IDs.
recipe-providers
homeboy runner recipe-providersLists recipe-run provider declarations discovered from installed extension manifests.
Use it to select a provider ID before runner recipe-run and to inspect each provider’s
owning extension, version, executable, and validation state. The command reports
declarations without starting a runner or probing runner-side executable readiness.
--script-file <path> reads the controller-side source, then materializes it as a private, content-addressed script-<sha256>.sh under ${XDG_RUNTIME_DIR:-/tmp}/homeboy-runner-exec/<job-id>/ on the runner. Bash executes that file directly, so $0 is its runner path. The job exports HOMEBOY_RUNNER_EXEC_SCRIPT with the same path and HOMEBOY_RUNNER_EXEC_SCRIPT_SHA256 as sha256:<digest>; the wrapper records the digest in its durable command evidence, makes the file mode 0500, and removes it when the runner job exits.
--script-file - reads stdin verbatim on the controller with the same bounded capture and materialization behavior; it does not stream stdin through bash -s. Zero-byte stdin is a validation error before Homeboy builds or submits a runner execution plan. Whitespace-only stdin is valid and is materialized verbatim, including newlines.
Place Homeboy options before the runner ID and use -- before the remote command. Without the separator, Homeboy diagnoses known exec options that appear after a remote command; the separator preserves remote flags with names such as --cwd, --raw, and --run-id.
Path rules:
- SSH runners require
workspace_rootso local paths are not silently reused remotely. - SSH
--cwdmust be an absolute path under the configuredworkspace_root. - Omitting
--cwdon an SSH runner uses the runnerworkspace_root. --sync-workspace <local-worktree>snapshots a controller-side worktree first, including dirty edits allowed by the normal snapshot safety excludes, then executes from the returned runnerremote_path. It is mutually exclusive with--cwdbecause Homeboy owns the execution cwd for that one command.--require-path <path>preflights one or more runner-side paths before execution. Use it when a command references a lab worktree path so missing controller-only paths fail with a structuredrequire_patherror instead of an empty command failure.--project <id>feeds the runner trust policy project allowlist check.--run-id <id>sets the persisted controller-side runner-exec run id for ad hoc evidence commands. When omitted, Homeboy derives a run id prefix from the command domain, such astrace-matrix-summary.--sshis the explicit diagnostic fallback whenconnectis unavailable; daemon execution is preferred because it records job metadata and supports artifact-oriented workflows.- Diagnostic SSH output serializes as
mode: "diagnostic_ssh"and does not include job/event evidence. - Raw SSH execution remains intentionally explicit and should not be used as production Lab/offload evidence; use connected daemon execution for job/event/artifact-compatible output.
- Mutation-capable executions expose returned source changes through
mutation_artifacts.patch_refwhen a patch artifact is available; callers should not branch on transport mode to find returned mutations.
Dirty local worktree against runner-side resources:
homeboy runner exec --sync-workspace /local/project@patch
--require-path /runner/resources/input
--capture-patch
<runner-id>
-- ./run-workload --input /runner/resources/inputThis is the generic one-command form for “run my local patch on the runner”: Homeboy snapshots the local source, runs the command from the materialized runner copy, preflights any runner-owned resource paths the caller declares, and attaches source snapshot metadata to patch capture output. The resource path meaning remains caller-owned; Homeboy only checks that the path exists on the runner.
Runner job environment:
homeboy runner env <runner-id>shows configured public runner env plussecret_envkeys/references for runner jobs. It does not resolve or print secret values.- Public
envvalues are redacted because legacy configs may still contain tokens;secret_envremains references-only. homeboy ssh <server> -- printenv NAMEinspects the server login shell environment. It does not include runner job env unless the variable is also configured on the server shell.- Use
homeboy runner exec <runner-id> -- printenv NAMEfor final execution-time proof when debugging resolved runner job environment. - Runner jobs carry the OS user-session runtime env:
XDG_RUNTIME_DIRandDBUS_SESSION_BUS_ADDRESSwhen the runner daemon itself has them, plus a Linux derivation from/run/user/<uid>when the daemon does not. The derivation accepts only a directory the job uid owns with mode0700, and the session bus address is added only when<XDG_RUNTIME_DIR>/busexists. Usehomeboy runner exec <runner-id> -- printenv XDG_RUNTIME_DIRfor proof on the runner.
Long-running detached workloads
runner exec is foreground-only: every runner job is process-tree-isolated under the runner daemon, and when the job’s root process exits the runner deterministically terminates and reaps all remaining descendants before reporting terminal evidence. Backgrounding inside the command does not survive this cleanup:
# WRONG: returns immediately, then the runner job cleanup kills the workload.
homeboy runner exec <runner-id> -- bash -lc 'nohup some-workload >/workload.log 2>&1 &'To keep a long-running workload alive past the exec call, give it to the operating system’s user-session service manager instead and let a runner-owned unit own the process:
homeboy runner exec <runner-id> --
bash -lc 'systemd-run --user --unit=my-workload --working-directory=<workspace> <long command>'
# then poll from a separate exec call
homeboy runner exec <runner-id> -- bash -lc 'systemctl --user status my-workload --no-pager'
homeboy runner exec <runner-id> -- bash -lc 'journalctl --user --unit my-workload -n 50'
homeboy runner exec <runner-id> -- bash -lc 'systemctl --user stop my-workload'Runner jobs get a usable XDG_RUNTIME_DIR (see the runner job environment notes above) so systemd-run --user and systemctl --user work without exporting session variables manually.
Runner metrics:
- Local runner execution and connected daemon jobs include a
metricsobject withduration_ms,sample_count, and lightweight resource fields when available. - On Linux runners, metrics are sampled from
/procfor the command process tree and includepeak_rss_bytes,child_process_count_peak,cpu_user_ms, andcpu_system_ms. - CPU accounting is sampled and can miss very short-lived child processes between samples; duration is always recorded, and non-Linux runners report
source: "duration_only".
workspace list
homeboy runner workspace list <runner-id>
homeboy runner workspace list <runner-id> --limit 5workspace list shows recent runner-side Lab workspace directories under the runner’s configured workspace_root and includes a reusable runner exec --cwd <remote-path> command for each entry. Use it after a manual or matrix session when the previous remote_path scrolled out of view and you need to run another command against the same runner-side checkout.
workspace sync
homeboy runner workspace sync <runner-id> --path <local-worktree>
homeboy runner workspace sync <runner-id> --path <local-worktree> --mode snapshot
homeboy runner workspace sync <runner-id> --path <local-worktree> --mode gitworkspace sync materializes a controller-side worktree under the runner’s configured workspace_root so runner execution can run against an explicit remote path while Git operations and canonical edits stay local.
Modes:
snapshotinstalls the current local tree, including dirty edits, from a content manifest and missing SHA-256 blobs streamed directly from the controller. Use this for private or proxy-dependent sources because the runner does not need repository access.snapshot-gituses the same content transfer and installs Git metadata without a throwaway checkout. Sources without Git receive a synthetic baseline.gitrequires a clean local tree, then clones or refreshesremote.origin.urlon the runner and checks out localHEADdetached. Use this only when the runner is allowed to fetch the remote directly.
Snapshot stages use two runner calls: plan checks identity/capacity and reports missing blobs; commit verifies blobs and assembled trees before atomic installation. snapshot_identity is the portable content digest. Transfer stats report stage-wide, cross-root deduplicated missing_blobs, missing_bytes, total_blobs, and total_bytes on each root’s output. No controller source staging tree is created; changed bytes or file metadata during streaming refuse the snapshot.
The runner keeps verified blobs in workspace_root/.homeboy-blobs, independent of mutable execution views. Corrupt blobs are requested again by the next plan. Workspace prune applies blob retention too: seven days since last use and a 10 GiB cap, evicting oldest mtimes first. Interrupted commits can be replayed with the same lease.
Private/proxied sources:
- Private or proxy-dependent source access stays on the controller machine.
- Materialize those sources with
homeboy runner workspace sync <runner-id> --path <local-worktree> --mode snapshot. - Use the returned
remote_pathfor downstreamrunner exec --cwdor job inputs. - Runner-side Git fetches for configured private/proxied hosts are refused with an actionable diagnostic. The default host list includes
github.example.com; override withHOMEBOY_PRIVATE_PROXIED_SOURCE_HOSTSonly when a runner is explicitly allowed to fetch those sources.
Safety rules:
- The remote path is deterministic and lives under
<workspace_root>/_lab_workspaces/. - Snapshot sync excludes dependency directories, build outputs, caches,
.git, and common secret file patterns such as.env*,*.pem, and*.key. - Runner policy can add project-specific generated-state patterns with
snapshot_excludes; configured patterns are merged with the default snapshot safety excludes and affect snapshot hashing, stats, and materialization. - Git sync refuses to overwrite an existing dirty runner-side checkout by default. Use
--allow-dirty-lab-workspaceonly for noisy investigation where discarding runner-side changes is intentional; Lab metadata records that override. - Output includes
local_path,remote_path,sync_mode,snapshot_identity, and snapshotfiles/byteswhen available. - The runner workspace is execution-only; this command does not push branches, commit, or make the runner authoritative for source changes.
workspace prune
homeboy runner workspace prune <runner-id> --min-age-hours 24 --limit 5
homeboy runner workspace prune <runner-id> --apply --min-age-hours 24 --limit 5 --passes 10
homeboy runner workspace prune <runner-id> --cursor <opaque-cursor>workspace prune only removes metadata-backed, inactive Lab workspaces that are old enough, have no pending patch or diff artifact, and are orphaned or have an expired lifecycle TTL. It never removes outside _lab_workspaces.
--min-age-hours defaults to the shared runner age floor (24 hours) that
homeboy cleanup --include remote-lab-workspaces and homeboy runner cache-prune also apply; it is one named constant rather than a literal repeated
per command. --limit defaults to the shared page size (25) and bounds the
workspace directories inspected, sized, retained in memory, and removed in each
pass; it is a page size, not a delete budget, which is why the aggregate
--limit record budget is deliberately not wired to it. --passes bounds apply
passes. Output now includes scanned_workspace_count and scan_complete: when scan_complete is false, has_more is true, continuation_cursor is an opaque token that resumes strictly after the last inspected workspace, and total_candidate_* plus remaining_candidate_* describe only the inspected window. Use the emitted next_command or drain_command unchanged; both carry the cursor when needed.
cache-prune
homeboy runner cache-prune <runner-id>
homeboy runner cache-prune <runner-id> --apply --min-age-hours 24Inventories stale managed Homeboy binary slots below
<workspace_root>/_homeboy_binaries on a runner. --apply removes only
unselected slots that meet the shared minimum-age floor; omit it to inspect the
eligible slots without changing the runner.
Recognized slot layouts:
homeboy-<64-hex sha256>/homeboy— content-addressed slot published byrefresh-homeboy(with itsprovenancefile).homeboy-<ref>/target/release/homeboy— a source checkout left by older refreshes.dev/<16-hex>/homeboy— a dev-sync native build slot.
A slot is selected (never removed) when any of these resolve into it:
the configured homeboy_path (configured_homeboy_path:<path>), any
runner-service controller link ~/.local/share/homeboy/runner-service/*/controllers/*/homeboy
(runner_service_link:<path>, covering every controller’s daemon), or
~/.local/bin/homeboy (local_bin_link:<path>). A reference that exists but
cannot be resolved protects every slot (ambiguous, unresolved_<kind>:<path>).
Process-owned slots are active. dev-source (dev-sync’s source upload
staging) is retained as dev_source_upload_staging; symlinked slots, slots
without their regular binary, and unrecognized names (unrecognized_slot) are
retained for an operator to inspect. --apply re-checks layout, every
protected reference, identity, and process ownership immediately before each
removal.
workspace apply
homeboy runner workspace apply <runner-apply.json>
homeboy runner workspace apply <runner-apply.json> --forceworkspace apply brings a runner-generated fix artifact back to the local source worktree recorded in the artifact’s source_snapshot.local_path. It is local-only: it does not commit, push, or make the runner canonical. Reviewability stays in normal local Git via git status and git diff.
Safety rules:
- The artifact must identify the local source worktree through
source_snapshot.local_path. - Homeboy recalculates the current local
source_snapshot.snapshot_hashbefore applying. - If the local source worktree drifted since the Lab snapshot, apply is refused unless
--forceis explicit. - Unified diffs are checked with
git apply --checkbefore mutation, so conflicts do not partially apply. - Delta paths must be relative and stay inside the source worktree.
- Output includes
apply_status,modified_files,expected_snapshot_hash, andcurrent_snapshot_hash.
Temporary Wave 4 adapter contract, until the runner fix-capture contract settles:
{
"source_snapshot": {
"runner_id": "lab-a",
"local_path": "/path/to/project@branch",
"remote_path": "/srv/homeboy/_lab_workspaces/project-abc123",
"git_sha": "...",
"dirty": false,
"sync_mode": "snapshot",
"snapshot_hash": "sha256:...",
"synced_at": "2026-05-16T00:00:00Z",
"sync_excludes": [".git/", "node_modules/"]
},
"patch": {
"format": "unified_diff",
"content": "diff --git a/file.txt b/file.txtn..."
}
}Delta form is also accepted for explicit file replacement/deletion:
{
"source_snapshot": { "...": "..." },
"delta": {
"files": [
{ "path": "src/file.txt", "content_base64": "Li4u" },
{ "path": "obsolete.txt", "delete": true }
]
}
}Runner Shape
SSH runner records are stored on their server as runner capability config under ~/.config/homeboy/servers/<id>.json.
{
"id": "runner-a",
"host": "runner.example.internal",
"user": "runner",
"port": 22,
"runner": {
"workspace_root": "/srv/homeboy/workspaces",
"homeboy_path": "/usr/local/bin/homeboy",
"daemon": false,
"concurrency_limit": 4,
"artifact_policy": "copy",
"env": {},
"resources": {}
}
}Standalone local runner records are still stored under ~/.config/homeboy/runners/.
{
"id": "lab-local",
"kind": "local",
"server_id": null,
"workspace_root": "/srv/homeboy/workspaces",
"homeboy_path": "/usr/local/bin/homeboy",
"daemon": false,
"concurrency_limit": 2,
"artifact_policy": "copy",
"env": {},
"resources": {}
}Rules:
kindislocalorssh.sshrunner IDs are server IDs; a single SSH machine does not need a separate runner ID.concurrency_limit, when set, must be greater than zero.envandresourcesare metadata maps for futureconnect,doctor,exec, and Desktop workflows.
JSON Output
All command output is wrapped in the global JSON envelope described in the JSON output contract. The data payload uses the generic entity CRUD shape:
command: action identifier such asrunner.add,runner.enable,runner.list,runner.show,runner.set,runner.remove,runner.doctor,runner.connect,runner.status,runner.disconnect,runner.exec,runner.workspace.sync, orrunner.workspace.applyid: present for single-runner actionsentity: runner configuration for single-runner read/write actionsentities: list forlistupdated_fields: list of updated field names for writesdeleted: list of removed runner IDs
Bounded Read-Only Probes
runner status, runs list --include-active-runner-jobs, and agent-task status are read-only inspection commands. Every remote probe they issue is
bounded by a wall-clock deadline (default 15s per probe, overridable with
HOMEBOY_READONLY_PROBE_TIMEOUT_SECONDS). When a probe hits its bound the SSH
child process group is terminated and the command still returns, with the reason
attached:
"probe_degradations": [
{
"probe": "runner_homeboy_identity",
"runner_id": "homeboy-lab",
"reason_code": "readonly_probe.timeout",
"timeout_seconds": 15,
"detail": "read-only probe `runner_homeboy_identity` exceeded its 15s bound ..."
}
]runner status --full also surfaces each degradation as a PARTIAL STATUS (...)
entry in operator_hints. A non-empty probe_degradations list means the answer
is partial — the runner did not respond in time — which is how an operator
distinguishes "the Lab is wedged" from "there is nothing to report".
The same ledger carries readonly_probe.unavailable, which is not a remote
timeout: a probe that reads the controller’s own ambient environment could not
run there at all. Its timeout_seconds is 0 and its hint is labelled
DEGRADED PROBE (...) so it cannot be mistaken for a wedged runner.
runs list --include-active-runner-jobs deliberately reads the latency-bounded
indexed active-job snapshot rather than the full status report: it does not
reconcile the runner session, so a wedged Lab cannot block a listing.