BYOC and managed runners under one governance model#
Connected Compute is the workspace primitive for running governed agent jobs on either BYOC sources you operate or LumenFlow-managed sources. A source can be a laptop, CI runner, Docker host, Kubernetes worker, or the first managed pool.
Both paths use the same control-plane contract:
| Layer | What is shared |
|---|---|
| Claim | POST /api/v1/runtime/claim assigns eligible runtime work |
| Lease | Lease records bound execution and retry state |
| Evidence | Receipts, approvals, and replay links stay workspace-scoped |
| Routing | Workspace policy chooses byoc_only, byoc_first, managed_first, or managed_only |
| Budget | Per-job limits refuse before silent fallback |
When you post a job, the capabilities you require decide where it can run. A short list of capabilities — including local tool execution, browser actions, local model generation, energy sampling, and MCP tool access — run only on a runner you connect; if no approved runner is online, the job stays queued instead of running somewhere that cannot actually do the work. Jobs that only need LumenFlow-managed capabilities run on the hosted service without any runner connected. Approving a capability for a runner on the compute page covers it regardless of which equivalent name that runner later reports it under — you approve once, not once per runner version. If a runner asks to claim work with a capability you have not yet approved, it shows up on the compute page for you to review and approve, without needing to reconnect the runner.
What is live#
- connected-runtime enrollment and health posture
- the runtime claim path and assignment lifecycle
- compute source read models for operator dashboards
- workspace routing policy UI and per-job "assigned to X, why" detail
- the first managed-worker registration path
- a runner-side compute-class classification (the default is
cpu-small), recorded on each assignment run
Current runner setup#
Open /dashboard/{workspaceId}/compute and use Runner setup. Enrolling
a runner creates a one-time runtime credential and walks through install,
enrol, doctor, approve, and start steps for your platform.
On Linux, the verified path is the pnpm CLI already in your LumenFlow
checkout: pnpm compute:connect, compute:doctor, compute:run and
compute:service. It needs one prerequisite package — bubblewrap, which
confines local tool execution — and the same runner directory flows through
every step. The start step renders a systemd --user unit with
Restart=always. Once doctor confirms the sandbox is available, the runner
requests the local:tool_exec and agent:code capabilities, which you
approve from the same page before the service starts.
compute:service installs the runner as a systemd service. install runs in
the systemd user scope by default, and needs --operator-approved once the
runner's capabilities and posture are approved in the workspace. status, drain and
uninstall manage the installed service. Host actuation is Linux only. On
macOS and Windows, compute:service install --dry-run prints the plan and the
other verbs refuse. Windows and macOS use the diagnostic bootstrap below.
Local tool execution only ever runs inside a checkout under
~/.lumenflow/workspaces — the start step creates one there for you (a
detached git worktree) and points --workspace-root at it. Any other
directory is refused, so doctor reporting a workspace-root problem means the
fix is a checkout in that directory, not somewhere else on the host.
The diagnostic bootstrap commands use Node to fetch the app-hosted bootstrap
from /api/v1/runner/bootstrap. They do not install @hellmai/lumenflow-cli,
do not require npm registry access, and do not require
LUMENFLOW_CONTROL_PLANE_TOKEN for doctor or run. The runtime claim made
with the one-time credential registers the runner session on the cloud side.
Treat the runtime credential as burned if it appears in chat, logs, shell history you do not control, screenshots, or any shared channel. Re-enrol the runner and use the newly shown credential.
Runner onboarding for agents#
An agent can bring a runner online under a policy the workspace owner sets. The pieces are separate, and each one is checked on every use.
- Policy. An owner or admin session sets the policy with
PUT /api/v1/workspaces/{workspaceId}/runner-onboarding/policy. No key, grant or agent can change it. A change that widens the policy takes effect only after a second, distinct owner or admin confirms it withPOST /api/v1/workspaces/{workspaceId}/runner-onboarding/policy/confirm.GETon the same path shows the policy in force, its expiry, and any widening that is waiting for a second approver. Once the policy expires, no grant can be issued and a held grant is refused. - Grant. An owner or admin session, or a key with
runners:manage, issues a grant withPOST /api/v1/workspaces/{workspaceId}/runner-onboarding/grants. The grant is anlf_rog_credential that carries onlyrunner:onboard.*scopes. Its lifetime is 7 to 30 days, 7 by default. A workspace holds a capped number of live grants. Revoke one withDELETE /api/v1/workspaces/{workspaceId}/runner-onboarding/grants/{grantId}. A revoked grant is refused on its next use. - Bootstrap. The runner's doctor and run steps fetch the runner script from
GET /api/v1/runner/bootstrap, as described above. - Capability approval. The runner's requested capabilities are approved by an owner in the workspace, before the service starts.
The credential kinds and their scopes are listed in Credentials and scopes.
Claim and heartbeat pacing#
Every claim and heartbeat response tells your runner how soon to check in
again. An ordinary response carries a poll-after hint in milliseconds — 30
seconds for claim, 60 seconds for heartbeat by default — so a well-behaved
runner never needs a fixed guess. If your workspace's request budget is
temporarily used up, the response is a 429 with a Retry-After header
naming the minimum wait before trying again; if the platform's own admission
check is briefly unavailable, you get a 503 with the same kind of
Retry-After header rather than a silent failure. Both are temporary and
independent of your billing or credit balance. The published runner client
already honours these headers — you do not need to implement backoff
yourself.
Job results#
Whichever substrate runs the work — a LumenFlow-managed worker or a
connected runner you operate — the result comes back in one shape: the
lumenflow.job_result/v1 envelope. Poll GET /api/v1/sidekick/jobs/{job_id} (or watch the SSE stream) until the
top-level status field reaches succeeded or failed, then read
the envelope from the response's result_envelope field (it stays
null until it is stored).
needs_approval is a paused state, not a finished one — the job is
waiting on a human to approve or reject it, not still running and not
done. It is visible on result_envelope as soon as it is stored, the
same way a finished job's envelope is, but it does not fire a push,
channel, or webhook delivery the way succeeded/failed do; keep
polling (or watching the SSE stream) past it for the job's eventual
succeeded or failed outcome.
result_envelope field | Meaning |
|---|---|
status | succeeded, failed, needs_approval, or cancelled (an owner stop) |
summary | One paragraph a human can read |
outputs | Whatever structured data your job produced |
artifact_refs | Links to any files or artifacts the job created |
evidence_refs | Links into the workspace's evidence trail for this job |
correlation | Echoed back exactly as you sent it, unmodified |
error | null on success; a reason code and message on failure |
The envelope is identical regardless of where the job actually ran, so your integration never needs to know or care which substrate executed it.
When LumenFlow's own hosted worker runs the job (rather than a runner you
operate), a failed job's error.reason_code is one of a small, stable
set, so your integration can react without parsing the summary text:
reason_code | Meaning |
|---|---|
model_unavailable | The workspace has no usable AI model configured for this kind of job |
model_provider_error | The configured model's own API call failed — the message names the model and the HTTP status, never the raw response from the model provider |
managed_artifact_persistence_error | The job produced its output, but saving it failed |
runtime_worker_error | Any other hosted-worker failure |
A runner you operate is free to send its own reason_code values in
result.error — these four only describe what the hosted worker sets.
Rather than polling, a Job created with callback_url (and a
callback_secret to sign it) gets this same envelope pushed to you once it
reaches a terminal state — see "Receiving results by webhook" in the
project-connection guide for the exact headers and a signature-verification
snippet.
If the runner executing your job goes dark mid-run — a crash, a dropped
connection, a powered-off machine — and never reports back, the platform
eventually gives up retrying and delivers a failed result with reason
lease_expired, so your integration never waits forever for a runner
that is not coming back.
Governed execution is billed on BYOC too#
Running work through Connected Compute on a BYOC source you operate is billed for the governance plane that ran it: admission, lease and fencing, sandboxed execution, evidence, and approvals — every completed run is recorded as usage. Your own infrastructure cost stays yours; Free and Team include a monthly allowance of governed-execution seconds for work on your own hardware (not AI usage), with metered overage beyond it on Team. LumenFlow-managed compute is billed separately, per the existing managed-compute credit model.
Private-network boundary#
Connected Compute is not a raw tunnel. Private-network access composes with the federation runtime and its admission gates. A BYOC source can execute approved work, but it does not silently become a private-network proxy.