The current runner target is Fly. Apis can execute an eligible CLI job there while keeping policy, provider selection and orchestration on its dispatcher host. Moving a CLI job does not move the daemon fleet or make the dispatcher independent of its current host.
The earlier VM + Tailscale recommendation and GitHub Actions routing for
Bucket C are superseded. The historical Bucket-A container utilities remain
in deploy/cloud/; they are not the Fly job image.
In particular, a runner must never use that directory's full-snapshot secret
materialization entrypoint.
| Surface | Implemented boundary |
|---|---|
| Fly executor and job entrypoint | Merged in #1460. Supports explicitly forced Fly dispatch and vetted classes listed in APIS_FLY_RUNNER_CLASSES; an unset list opts in no classes. |
| Repository delivery inside a job | Merged in #1473. A job with a delivery specification can clone, commit, push and open or update a PR. This alone does not route normal builder work to Fly. |
| Dispatcher routing for builder delivery | Merged in #1475 (2026-10-20, 8656c9a2). It adds the explicit open_or_merge_pr opt-in; merging it does not enable it, and this guide does not enable it. A delivery counts as verified only after the pull request is read back from GitHub and matches the runner's reported repository, base, branch and commit. |
The design starts at #1456. Later accepted rulings use a single login machine per provider, stopped between jobs, including Claude. Earlier per-job Claude and API-key recommendations must not be treated as the current activation instructions.
A merge, an image build, a successful CLI process or an earlier pilot receipt
does not establish that an instance is deployed or that repository delivery
works. Retrieve its current deployment_configuration before any deployment.
It must bind the target, region, admitted source and image, command, credentials
by reference, budget, action gates and verification. Missing bindings remain
unknown; this generic guide cannot supply them.
- Bucket A: daemon loop bodies. Network-facing services can be hosted away from a device, but moving their SSE loops, state and webhook endpoints is a separate deployment. The retained VM scaffold is historical, not a current Fly daemon-fleet runbook. Preserve one consumer per daemon during a cutover.
- Bucket B: device-bound work. Screen, microphone, desktop control and local Google-session capabilities remain on the device that owns them. Moving a runner does not move those permissions or sessions.
- Bucket C: CLI jobs. Use the existing Fly executor and job image below. Each job receives its declared credentials and returns an attributed result. GitHub Actions still runs repository CI and its configured review workflow; it is not the replacement executor for these Fly jobs.
| File | Responsibility |
|---|---|
deploy/fly-runner/Dockerfile |
Pinned CLI/runtime image with a narrow build context; no SOPS, age key, private secrets repository or daemon snapshot entrypoint. |
deploy/fly-runner/fly.toml |
Generic image-build configuration with placeholder app and region, no public services or app-level secrets. |
deploy/fly-runner/run_job.py |
Validates one job, prepares its constrained child, records the outcome and clears job material. |
execution/daemons/apis/fly_runner.py |
Eligibility, Fly API lifecycle, login queues, budget ledger and result collection. |
execution/daemons/apis/dispatch_role.py |
Existing role-dispatch CLI, including --runner fly, --provider and --repository. |
An image build must use the runner Dockerfile and its adjacent
Dockerfile.dockerignore. After the instance binding and build authority are
verified, the existing build-only invocation can be parameterized from that
binding, from the repository root:
fly deploy \
--app "${APIS_FLY_APP:?retrieve the instance binding}" \
--primary-region "${APIS_FLY_REGION:?retrieve the instance binding}" \
--config deploy/fly-runner/fly.toml \
--dockerfile deploy/fly-runner/Dockerfile \
--build-only --push \
--image-label "${RUNNER_IMAGE_LABEL:?use the admitted source revision}" .This builds and pushes an image; it does not deploy the dispatcher, update the
login machines or authorize running a job. Do not replace --build-only with
an ordinary fly deploy: the job lifecycle belongs to the existing Machines
API executor. The exact instance command and build arguments come from its
binding. Keep the dispatcher and runner implementation compatible; inspect the
actual machine image digest rather than inferring it from a tag or local HEAD.
Files under deploy/fly-runner/, such as run_job.py and repo_delivery.py,
run inside the job image, not in the dispatcher. Merging a change to these
files does not change what jobs run. After the merge, use the build-only command
above to build and push a new image labelled with the merged commit's short SHA.
Update the dispatcher's configured image reference (APIS_FLY_IMAGE in the
instance binding), restart the dispatcher, and confirm that the running process
loaded the new reference. On the first job after the switch, verify that the
machine's image digest matches the pushed image and that the job's signed result
reflects the change.
Changes only to the dispatcher under execution/daemons/apis/ need a dispatcher
restart, not an image rebuild.
For an already admitted read-only pilot task, the existing dispatcher command is:
uv run --python 3.13 --extra dev python execution/daemons/apis/dispatch_role.py \
--role "${ROLE:?use the task's assigned role}" \
--task-file "${TASK_FILE:?use the admitted task brief}" \
--provider "${PROVIDER:?use an enabled provider}" \
--runner fly --action-class carry_forward_check \
--repository "${REPOSITORY:?use the admitted owner/repository}" --jsonThis is a real dispatch, not a dry-run or authentication check. It still needs the role identity, provider login, result grants, budget and action gates. Forcing the executor does not grant an otherwise ineligible role access. Provider activation remains a separate setting and ruling: configuring a Claude login does not reactivate Claude for automatic swarm work.
Each provider uses its own single login machine and persistent login volume;
jobs queue for that provider and the machine stops between jobs. Do not copy
one login across concurrent machines. Codex retains its own ChatGPT login,
Cursor its own plan login, and Claude its supplied OAuth token. Claude's token
belongs only on its Fly login volume as a mode-0600 file at
/data/claude/oauth_token; it must not be copied into the dispatcher's env file.
No metered API-key fallback is implied by a missing or failed login.
The login volume is the declared persistent exception. Job scratch and the child's temporary home are separate from that login state. The lifecycle must confirm the machine stopped and job material was cleared before another job can use it; a provider-auth failure is not permission to skip that boundary.
The dispatcher retains Fly API authority and App private keys. Job credentials
arrive as machine-level files under /run/job/, not Fly app secrets that would
be injected into every machine. The runner consumes and removes those files
before starting the CLI, constructs its child environment from the shared
allowlist, and keeps signing/result-publishing authority out of the child.
A role needs its own Neotoma identity; the shared daemon bearer is not a Fly
fallback. Probe reports contain names and presence checks, never values.
A key-only Fly job signs its result record with AAuth, but its child has no
agent-side Neotoma MCP tools because it has no role bearer for the MCP config.
Repository delivery additionally needs a distinct PR-opener App configuration.
The existing App helper resolves that identity;
the delivery path requests a short-lived installation token for exactly one
repository with contents:write and pull_requests:write (plus GitHub's implicit
metadata read). Do not substitute the approving App or a shared PAT. The
delivery record's schema, the role's store grant and the dispatcher's retrieve
grant must be verified before enabling dispatcher delivery routing.
The existing executor enforces numeric
configuration bounds and a local dollar ledger. Admission counts completed
spend, login-volume and stopped-rootfs storage, outstanding reservations and the
incoming job's maximum-duration cost against APIS_FLY_MONTHLY_BUDGET_USD
(default 20). Daily launch and concurrent-run limits also apply. Invalid or
unreadable ledger state refuses admission. The ledger is an estimate for this
dispatcher, not a provider invoice or proof that a billing alert exists.
A provider billing alert is a separate backstop, not the admission mechanism.
The accepted Fly usage ruling exempts remote jobs from the dispatcher's local subscription pace and weekly-usage ceilings because the Fly logins are separate. Local runs retain their local usage rules. The Fly dollar cap still applies; this does not enable a deactivated provider or authorize a new budget.
Treat launch failure, provider authentication failure, missing result and
deadline expiry as distinct evidence. The pilot's local fallback is controlled
by APIS_FLY_LOCAL_FALLBACK; do not describe a fallback as cloud success.
A successful run requires its exact fly_runner_result in Neotoma, with the
run, role, provider, machine, exit status and credential-absence report checked.
Machine exit zero alone is insufficient. After repository delivery, also
resolve the recorded remote branch, commit and PR; a missing or undeclared
delivery field is not a successful publication.
Read back the actual machine state, image and volume attachment, and verify that only the declared provider login persists. Preserve any live review, credential, schema, grant or activation gate. These checks establish an instance's behavior; this document and its build template do not assert that any instance has been deployed.