Self-hosted worker (Cursor My Machines)
Cloud agents can run their tool calls on a machine we host instead of a Cursor-hosted VM. Cursor still does the reasoning; the machine holds the checkout, runs the commands and makes the edits.
Routing
Set on the Mainark side:
| Variable | Values | Meaning |
|---|---|---|
MAINARK_CLOUD_ENV | unset / cloud | Cursor-hosted VM — the default, unchanged behaviour |
machine | a My Machines worker | |
pool | a team Self-Hosted Pool (Enterprise plan) | |
MAINARK_CLOUD_ENV_NAME | e.g. fulreach-workers | which machine or pool |
Both the implement and the fix executor honour it, so a review→fix round lands
where the implement ran. An unknown value fails the dispatch before anything is
claimed — the same fail-closed rule as a missing CURSOR_API_KEY — rather than
quietly spending on Cursor's VMs.
The run's cloudEnv is recorded in the agent_runs metadata, so the ledger
says where each run executed.
Matching
A machine request runs only when the machine belongs to the API key's owner,
its worker name matches MAINARK_CLOUD_ENV_NAME (the worker defaults to the
hostname), and its registered repo matches the target repo. If nothing matches,
the request fails; per Cursor's docs it does not fall back to a hosted VM.
The worker on fulreach-workers
cursor-worker.service runs cursor-agent worker as the mainark user,
authenticated by the CURSOR_API_KEY in /etc/mainark/mainark.env. My Machines
does not need an Enterprise plan; --pool does.
The worker's --worker-dir is a dedicated checkout,
/opt/mainark/agent-mainark. It must never be /opt/mainark/mainark, the
clone the coordinator runs from — an agent routed there would edit the running
orchestrator.
Health, readable without root:
curl -s localhost:8099/readyz # {"connected":true,"claimed":false,...}
cursor-agent status reports "Not logged in" even when the API key works; it
only reflects a browser login. Check auth with cursor-agent models instead.
Known issue: Cursor does not deliver API-routed runs (2026-09-23)
The first live test (MAI-45, run #48) and three follow-up probes all behaved
the same way. Cursor accepted a correctly routed machine request and stored
env: {type: machine, name: fulreach-workers}. The worker was connected,
visible, labelled repo=adriguerra/mainark and owned by the same user as the
key. Yet the worker was never offered the job: its verbose log shows only
keepalive frames. Cursor held each run about 15 minutes, then set ERROR with
no message. Nothing was charged, and the probes used zero tokens.
Probing the API also showed that env.name is mandatory for machine, and
that repo-less machine requests are rejected. So the request Mainark sends is
the only valid form. The next step is to launch a run from the Cursor dashboard
onto the machine: that separates an API-routing fault from a
registration/account fault.
Failing fast when the machine never picks up a run
Because Cursor reports an undelivered run as simply "running", the executor checks for itself whether the job ever reached the worker.
| Variable | Default | Meaning |
|---|---|---|
MAINARK_MACHINE_CHECKOUT | unset | the worker's --worker-dir; enables the checkout signal (preferred) |
MAINARK_MACHINE_HEALTH_URL | unset | e.g. http://127.0.0.1:8099; fallback signal when no checkout is set |
MAINARK_MACHINE_CLAIM_TIMEOUT_S | 300 | how long a run may show no sign of starting |
Checkout signal (preferred). Right after resetting the checkout for a job,
dispatch records where it stands (machineCheckoutBaseline: branch, commit,
dirty). An agent that starts always changes that: it creates its branch, edits
files, or commits. If the checkout is still exactly at the baseline past the
timeout, the run is cancelled with machine_not_delivered: …. Any change, even
once, marks the run delivered (machineDelivery: "seen") and it is never
cancelled. A checkout git cannot read counts as unknown and never cancels.
Health alone could not do this. On the box the worker stays claimed after
every job (seen for days), so every new run looked delivered and the
health check never fired. The checkout signal ignores the worker's state.
Health signal (fallback). Runs dispatched without a checkout baseline use
/readyz and /metrics. If the worker is idle (not claimed, no active session)
on every check past the timeout, the run is cancelled. One busy reading or an
unreachable endpoint disables the check for that run.
Either way, machine_not_delivered is classed as agent_error. It needs a
human and is never auto-retried, because a retry would wait just as long.
One clean checkout per job
The worker runs every job in the same directory, and nothing used to reset it:
after #84 it was still on that PR's branch, with a modified pnpm-lock.yaml and
an untracked pnpm-workspace.yaml from an earlier job's pnpm install.
With MAINARK_MACHINE_CHECKOUT set to the worker's --worker-dir
(/opt/mainark/agent-mainark), Mainark resets it before every machine-routed
job:
| Job | Checkout becomes |
|---|---|
| implement | origin/main on mainark-agent-idle, never main itself |
| fix | origin/<PR branch> on that branch |
Tracked files are restored and untracked ones removed. Ignored files
(node_modules), .git/hooks (the pre-push guard) and the repo's git config
(commit identity) are kept. A failed reset refuses the job before anything is
claimed or paid for.
Do not point a dashboard-launched agent at the same directory: a reset for a Mainark job would discard its work. Give manual use its own worker directory.
Open question before running agents in parallel
My Machines allows several agents on one worker at once. It is not yet
confirmed whether each gets its own worktree or they share the worker's
checkout. Until that is known, keep MAINARK_MAX_INFLIGHT=1.