Use Tensorlake Sandboxes as the self-hosted machines for Cursor Cloud Agents.
Cursor keeps the agent loop and the model in its cloud. Every command, file edit, build, and repository checkout runs in a Tensorlake sandbox that you own. A small orchestrator, which also runs in a Tensorlake sandbox, claims Cursor requests and gives each one its own worker sandbox. Idle workers suspend. Follow-up messages resume them with the checkout and caches intact.
Cursor cloud ── pending requests ──▶ orchestrator sandbox
├─ agent worker controller --spawn cursor-tl-spawn
├─ janitor: idle worker ➜ suspend, old ➜ terminate
└─ wake: claimed-offline request ➜ resume sandbox
│
worker sandbox (one per agent) ── agent worker ── outbound HTTPS ──▶ Cursor
You need a Cursor team, a Tensorlake account, and a local machine with Python.
Cursor:
- A Cursor Enterprise team. Cursor offers pools only on Enterprise.
- A service-account API key from Dashboard → Settings → API Keys → Service Accounts. Pool workers reject personal and team keys.
- A team administrator who can enable self-hosted machines and GitHub token minting.
- The Cursor GitHub App connected at the team level, with access to each repository agents will work on.
Without an Enterprise team, a personal key on any plan runs one worker in one sandbox. See My Machines.
Tensorlake:
- An account and API key from Tensorlake Cloud.
- The
tlcommand-line interface (CLI), optional:curl -fsSL https://tensorlake.ai/install | sh. This guide uses it to list sandboxes and read worker logs.
Local machine:
- Python 3.10 or newer and uv.
- Nothing stays running here after setup.
A Cursor team administrator does this once:
- Open Dashboard → Cloud Agents → Self-Hosted.
- Enable Self-hosted Machines.
- Enable GitHub token minting for self-hosted pool workers.
- Give the Cursor GitHub App access to each repository.
One command sets up everything from a fresh clone:
git clone https://github.com/tensorlakeai/cursor-cloud-agents-tensorlake
cd cursor-cloud-agents-tensorlake
uv sync --all-extras
uv run cursor-tl-upcursor-tl-up asks for the Cursor and Tensorlake keys once and saves them to
.env. Then it checks the Cursor key, registers the pool, builds or reuses
both images, launches the orchestrator sandbox, and waits until the controller
reports watching. You need no other flag.
The pool is a Cursor Team Pool: the queue that self-hosted workers serve. Its
name comes from CURSOR_POOL, default tensorlake. You pick it at
cursor.com/agents under Remote Machines. It is
not a Tensorlake sandbox name.
Sandboxes take their names from the pool. The orchestrator sandbox name
includes a hash of the pool name, and each worker sandbox is
cursor-<worker-id>.
The command is idempotent. Run it again after you edit .env, or to repair a
suspended orchestrator sandbox or a dead process. It skips steps that are
already done, and it restarts the orchestrator when the pool settings in
.env changed.
Options:
uv run cursor-tl-up --repo-url URL # serve one repository only; see Repositories
uv run cursor-tl-up --any-repo # let Cursor clone after the claim; see Repositories
uv run cursor-tl-up --computer-use --rebuild # desktop worker image; workers start with --computer-use
uv run cursor-tl-up --restart # restart the orchestrator process even if .env did not change
uv run cursor-tl-up --rebuild # after a code change: rebuild the orchestrator image and recreate its sandbox
uv run cursor-tl-up --non-interactive # fail instead of asking for missing keys, for CI and cron
uv run cursor-tl-up --ready-timeout 600 # seconds Cursor waits for a hibernated worker to reconnect, default 900
uv run cursor-tl-up --no-wait # return without waiting for the orchestrator heartbeatWhen the command reports the pool as registered and the controller as
watching, go to Submit a request.
By default the pool serves every repository the Cursor GitHub App can reach.
Each worker starts with the request's repository as its git origin, and a
checkout hook fetches the requested ref with a short-lived token that Cursor
mints for the run. The worker stores no long-lived git credential.
Two optional flags change how the pool maps to repositories. cursor-tl-up
saves both in .env.
| Default | --repo-url URL |
--any-repo |
|
|---|---|---|---|
| Use when | You want the simplest setup | The service-account key is scoped to one repository, or you want one pool per repository | You want Cursor to clone, and the GitHub App is connected |
| Repositories served | All | One | All |
| Worker | origin set, checkout hook fetches |
Same | No origin; Cursor clones after the claim with --clone-git-repos |
| Web picker | Pick the repository, then the pool | Same | Pick Any repo, then the pool |
| Saved as | CURSOR_POOL_MODE=repo |
CURSOR_POOL_REPO_URL |
CURSOR_POOL_MODE=any-repo |
Background: Cursor keys a pool row by name plus repository. A worker with
origin set joins the row for that repository. A worker with no origin joins
the Any repo row.
The default registers the Any repo row too, but no default worker joins
it. A request sent to that row fails with
No self-hosted worker matches the requested labels. So in default mode,
always pick the repository in the web picker, not Any repo. --repo-url
removes that row.
The same setup, one command at a time.
-
Install and configure:
git clone https://github.com/tensorlakeai/cursor-cloud-agents-tensorlake cd cursor-cloud-agents-tensorlake uv sync --all-extras cp .env.example .envIn
.env, setCURSOR_API_KEYandTENSORLAKE_API_KEY..env.exampledocuments every other variable, and each has a working default. To change the repository mode, see Repositories. -
Register the pool:
uv run cursor-tl-pool register uv run cursor-tl-pool list
With
CURSOR_POOL_REPO_URLthe list must show one row for the pool, withrepoUrlset. Remove a leftover any-repo row withuv run cursor-tl-pool deregister. -
Build the images:
uv run cursor-tl-build-image # prints cursor-tl-worker-<sha8> uv run cursor-tl-build-orchestrator-imageCopy the printed worker image name into
IMAGE_NAMEin.env. -
Launch the orchestrator:
uv run cursor-tl-orchestrator-sandbox uv run cursor-tl-orchestrator-sandbox --status
The command is idempotent. Schedule it, so it resumes a suspended orchestrator sandbox and re-checks the process:
*/15 * * * * cd /path/to/cursor-cloud-agents-tensorlake && uv run cursor-tl-orchestrator-sandbox >/dev/null 2>&1
The web app is the shortest path:
- Open cursor.com/agents.
- Pick the repository. In
any-repomode pick Any repo instead. - Open the machine picker, choose Remote Machines, and pick tensorlake.
- Send the task. In
any-repomode name the repository in the prompt.
Other triggers: @Cursor pool=tensorlake ... in Slack, @cursoragent pool=tensorlake ... on GitHub, pool=tensorlake in a Linear issue, or the API:
uv run cursor-tl-pool agent "Add a unit test for the parser" --repo https://github.com/acme/widgets
uv run cursor-tl-pool agent "Clone https://github.com/acme/widgets and add a unit test for the parser" # any-repo mode: no --repoWatch the request arrive:
tl sbx ls # a cursor-<worker-id> sandbox appears
uv run cursor-tl-orchestrator-sandbox --logs # claim, spawn, worker start
tl sbx exec cursor-<worker-id> tail -n 50 /var/log/cursor-tl/worker.logAfter the session ends, the worker waits WORKER_IDLE_RELEASE_SECS, default
300s, and exits. The janitor then suspends the sandbox. A follow-up message
resumes it. The janitor terminates a sandbox suspended for longer than
SESSION_RETENTION_SECS, default 86400s (one day).
Cursor agents can drive a desktop and a browser on Linux workers. With
computer use on, the worker image builds from tensorlake/ubuntu-vnc: an
Ubuntu desktop with Xfce, Google Chrome, Firefox, and a Virtual Network
Computing (VNC) server, plus the packages Cursor needs. Each worker starts with
--computer-use, and no inbound port opens.
Desktop workers need at least 4096 MB of memory, the SANDBOX_MEMORY_MB
default.
uv run cursor-tl-up --computer-use --rebuildThis sets WORKER_COMPUTER_USE=true in .env, builds the desktop worker
image and pins it as IMAGE_NAME, rebuilds the orchestrator image, and
recreates the orchestrator sandbox. --rebuild matters the first time: the
orchestrator image includes this package, and the orchestrator is what passes
--computer-use to a worker. Confirm the new settings:
uv run cursor-tl-orchestrator-sandbox --status # reports computer_use, display, shell_display-
Set
WORKER_COMPUTER_USE=truein.env. -
Rebuild the worker image and copy the printed name into
IMAGE_NAME:uv run cursor-tl-build-image
-
Start the machine. A machine that already exists keeps its old image, so terminate it first:
uv run cursor-tl-my-machine --name tl-desktop --terminate uv run cursor-tl-my-machine --name tl-desktop
Submit a request as usual, and ask for something that needs a screen:
Open a browser, visit example.com, and take a screenshot. Tell me the page title.
The agent opens Chrome on the sandbox desktop, takes screenshots, and acts on what it sees. It works with or without a repository attached. To check the desktop before you send a task, run Cursor's own preflight in a worker sandbox:
tl sbx exec cursor-<worker-id> agent worker --pool tensorlake --computer-use --display :1 debugThe desktop image boots a TigerVNC and Xfce session on display :1 before any
worker starts, and exports DISPLAY=:1 to the agent's shells. WORKER_DISPLAY
defaults to :1, so the display Cursor screenshots and the display a browser
opens on are the same one. Leave it alone unless you know why you need another.
WORKER_DISPLAY=managed lets the worker create its own desktop instead, and
points the shells at :0, the display Cursor takes when :1 is busy.
managed:N names another number.
WORKER_SHARE_DESKTOP=view or view_and_control asks Cursor to serve the
agent desktop to authorized viewers in Cursor. Cursor shares only a desktop the
worker created itself, so this turns the WORKER_DISPLAY default off and
behaves like managed. Do not combine it with an explicit WORKER_DISPLAY
value. No end-to-end run of this option exists from this repository yet, and
the web app had no desktop viewer at the time of writing.
The desktop image serves display :1 over VNC on port 5901 inside the
sandbox. The sandbox publishes no port, so reach it with a tunnel and watch
what the agent does while it works. Record the viewer window if you want a video.
tl sbx ls | grep cursor # find the worker sandbox
tl sbx tunnel cursor-<worker-id> 5901 # holds the terminal openThe session asks for a VNC password. Set one you know, in another terminal:
tl sbx exec cursor-<worker-id> sh -c \
'printf "%s\n%s\n\n" "$P" "$P" | vncpasswd -f > /tmp/tigervnc.*/passwd &&
chown 1000:1000 /tmp/tigervnc.*/passwd' P=watchme1VNC passwords are at most 8 characters. The server reads the file on each connection, so the running desktop needs no restart.
Then open a viewer. macOS has one built in:
open vnc://localhost:5901The pointer moves, Chrome opens, and the pages the agent reads appear as it reads them. Watching does not disturb the agent. A viewer that also controls the desktop moves the same pointer the agent moves, so watch only.
This works because WORKER_DISPLAY defaults to :1, the display the image
already serves. With WORKER_DISPLAY=managed or WORKER_SHARE_DESKTOP set,
the agent uses a desktop that VNC on 5901 does not serve, so the two are
alternatives: share the desktop through Cursor, or watch it yourself over the
tunnel.
demo/README.md is a full walkthrough: an agent opens a web
app in Chrome inside a sandbox, finds a bug that only a click reveals, fixes
it, and checks the fix by looking again. cursor-tl-demo records the desktop
and copies the video and screenshots out.
uv run cursor-tl-demo watch # wait for a worker sandbox, start recording
uv run cursor-tl-demo collect # video and stills into ./demo-artifactsSet WORKER_COMPUTER_USE=false in .env and run cursor-tl-up --rebuild
again. The worker image goes back to the minimal base and new workers start
without --computer-use. Running workers keep their flags until they exit.
My Machines works on any Cursor plan and needs no orchestrator. One long-lived worker runs in one Tensorlake sandbox, and you pick it from the environment dropdown at cursor.com/agents. Hibernation is by hand.
-
Install and configure:
git clone https://github.com/tensorlakeai/cursor-cloud-agents-tensorlake cd cursor-cloud-agents-tensorlake uv sync --all-extras cp .env.example .envIn
.env, setTENSORLAKE_API_KEY,CURSOR_USER_API_KEY(a personal key from Dashboard → API Keys), andREPOS(one or more HTTPS repository URLs, comma separated). LeaveCURSOR_API_KEYempty. Private repositories also needGIT_USERNAMEandGIT_TOKEN. -
Build the worker image and copy the printed name into
IMAGE_NAME:uv run cursor-tl-build-image
-
Start the machine:
uv run cursor-tl-my-machine --name tl-demo
The machine
tl-demoappears in the environment dropdown at cursor.com/agents within a few seconds. For a desktop and a browser, see Turn it on for My Machines. -
Send a task with
tl-demoselected. Watch it:uv run cursor-tl-my-machine --name tl-demo --status uv run cursor-tl-my-machine --name tl-demo --logs
-
Hibernate, resume, or remove:
uv run cursor-tl-my-machine --name tl-demo --suspend # memory, filesystem, checkout kept uv run cursor-tl-my-machine --name tl-demo # resume and restart the worker uv run cursor-tl-my-machine --name tl-demo --terminate # remove the sandbox
These commands cover the work after setup:
| Command | Effect |
|---|---|
cursor-tl-orchestrator-sandbox |
Create or resume the orchestrator sandbox and ensure the process runs |
cursor-tl-orchestrator-sandbox --status |
State, pool, session counts, controller restarts |
cursor-tl-orchestrator-sandbox --logs [--lines N] |
Tail the orchestrator log |
cursor-tl-orchestrator-sandbox --restart |
Restart the orchestrator process so it reads the current .env |
cursor-tl-orchestrator-sandbox --terminate |
Terminate the orchestrator sandbox. Worker sandboxes stay |
tl sbx tunnel cursor-<worker-id> 5901 |
Reach the worker desktop over VNC. See Watch the desktop live |
cursor-tl-demo watch / collect / stop / status |
Record a worker's desktop and copy the video out. See demo/README.md |
cursor-tl-pool pending |
Pending and claimed-offline requests for the pool |
cursor-tl-pool workers / summary |
Connected and idle workers |
cursor-tl-pool release <request-id> |
Release a stuck claim so Cursor re-queues it |
cursor-tl-pool deregister [--repo-url URL] |
Remove the any-repo pool row, or the repo-bound row |
tl sbx ls |
All sandboxes, including cursor-* workers and their state |
tl sbx terminate cursor-<worker-id> |
Remove one worker sandbox by hand |
.env.example documents every variable. Values in .env win over variables
exported in your shell. To change a value, edit .env and run cursor-tl-up
again. The orchestrator reads its settings at process start, and cursor-tl-up
restarts it when the pool settings changed.
| Variable | Required | Notes |
|---|---|---|
TENSORLAKE_API_KEY |
Yes | Never enters a worker sandbox. |
IMAGE_NAME |
Yes | Worker image. cursor-tl-up writes it. |
CURSOR_API_KEY |
Pools | Enterprise service-account key. |
CURSOR_USER_API_KEY |
My Machines | Personal key. Works on any plan. |
REPOS |
My Machines | HTTPS repositories to clone, comma separated. |
CURSOR_POOL_MODE |
No | repo (default) or any-repo. See Repositories. |
CURSOR_POOL_REPO_URL |
No | Serve one repository only. repo mode only. |
WORKER_COMPUTER_USE |
No | Desktop image, and workers start with --computer-use. |
WORKER_DISPLAY |
No | X display for computer use. Blank reuses :1; managed lets the worker start its own. |
WORKER_SHARE_DESKTOP |
No | view or view_and_control: share the agent desktop with Cursor viewers. Untested here. |
Common changes: CURSOR_POOL (pool name, default tensorlake), MAX_WORKERS
(cap on live worker sandboxes), SANDBOX_CPUS / SANDBOX_MEMORY_MB /
SANDBOX_DISK_MB (worker sizing), and SANDBOX_ALLOW_OUT (egress allowlist,
which must include api2.cursor.sh, api2direct.cursor.sh, and your git host).
Do not set CURSOR_AGENT_WORKER_ID or CURSOR_REQUEST_ID in .env. The
controller supplies them to the spawn hook.
The worker process runs with the Cursor service-account key in its
environment, and repository code runs as the same OS user. Use one
least-privilege service account per customer and set SANDBOX_ALLOW_OUT.
Each entry names the symptom, then the fix:
No self-hosted worker matches the requested labels: The request was sent to the Any repo row, and the pool runs in default mode. Pick the repository in the web picker instead, or see Repositories for--repo-urland--any-repo.- Any-repo worker has an empty workspace: Cursor clones only when the
request names a repository the GitHub App can reach. Name the repository in
the prompt and check
/var/log/cursor-tl/worker.login the worker sandbox. POST /v1/agentsreturns 403integration_not_connected: The request named a repository, and the Cursor GitHub integration is not connected to the team. A team admin connects it in the Cursor integrations dashboard.- Request stays queued: Check that the pool name matches
CURSOR_POOL, that Self-hosted Machines is enabled, and that--logsshows the controller connected.cursor-tl-pool pendinglists what Cursor is waiting on. A log line such aspending-request watch failedwith HTTP 503 means Cursor's API was unavailable. The controller reconnects on its own. - Changed
.env, but the orchestrator still uses the old settings: Runcursor-tl-upagain. It compares the running settings with.envand restarts the process when they differ.--restartforces it. - The agent says it cannot open a browser, or
--computer-useseems ignored: Checkcursor-tl-orchestrator-sandbox --status. Ifcomputer_useis absent orfalsewhile.envsaystrue, the orchestrator is running an older build of this package:cursor-tl-up --computer-use --rebuild. - Screenshots come back empty, or the agent says the browser did not open:
The display Cursor screenshots and the display the browser opened on differ.
cursor-tl-orchestrator-sandbox --statusshowsdisplayandshell_display; with the defaults both are:1. SetWORKER_DISPLAYback to blank, or tomanagedwhenWORKER_SHARE_DESKTOPis on, and runcursor-tl-upagain. tl sbx lsshows nocursor-*sandboxes: ThetlCLI uses the key in your shell, and.envmay hold a key for another Tensorlake project. Export theTENSORLAKE_API_KEYfrom.envin that shell, or usecursor-tl-orchestrator-sandbox --statusand--logs, which read.env.- Controller rejects the key: Only an Enterprise service-account key starts
pool workers. A personal key works only with
cursor-tl-my-machine. - Workspace is empty after a run: Read
/var/log/cursor-tl/checkout.login the worker sandbox. Confirm GitHub token minting is enabled and the GitHub App has access to the repository. HTTPS remotes only. - A new chat started a sandbox in another Tensorlake project: Two
orchestrators serve the same pool, each launched with a different
TENSORLAKE_API_KEY. Cursor splits new requests between them. Runcursor-tl-orchestrator-sandbox --statuswith each key, and--terminatethe one you do not want. A re-launch always uses the key in.env. - A follow-up started a new sandbox: The reconnect window lapsed. Raise
it with
cursor-tl-pool register --ready-timeout 1800, which updates the existing pool.cursor-tl-up --ready-timeoutapplies only when it creates the pool. Or check--logsfor a failed resume. - Worker sandbox stays running after the agent finished: The worker waits
WORKER_IDLE_RELEASE_SECSbefore it exits. Then the next janitor pass suspends it. - A failed spawn left a claimed request: Run
cursor-tl-pool release <request-id>.
The upstream references this guide builds on: