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:

VariableValuesMeaning
MAINARK_CLOUD_ENVunset / cloudCursor-hosted VM — the default, unchanged behaviour
machinea My Machines worker
poola team Self-Hosted Pool (Enterprise plan)
MAINARK_CLOUD_ENV_NAMEe.g. fulreach-workerswhich 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.

VariableDefaultMeaning
MAINARK_MACHINE_CHECKOUTunsetthe worker's --worker-dir; enables the checkout signal (preferred)
MAINARK_MACHINE_HEALTH_URLunsete.g. http://127.0.0.1:8099; fallback signal when no checkout is set
MAINARK_MACHINE_CLAIM_TIMEOUT_S300how 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:

JobCheckout becomes
implementorigin/main on mainark-agent-idle, never main itself
fixorigin/<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.