modal-cursor
Run Cursor BYOM worker pools on Modal. One durable Modal control-plane controller owns registration and dispatch for every configured Cursor pool, then creates an isolated sandbox for each claimed request.
This project targets Python 3.11 and newer.
Quickstart
uvx modal-cursor init
uvx modal-cursor doctor
The interactive wizard configures Modal if needed, prompts for the Cursor
service-account key, creates the cursor-service-account Modal Secret, writes
an editable pool configuration, and offers to deploy the all-pools control
plane. Pass a pool name for a scriptable flow; set CURSOR_API_KEY in the
environment when running without a terminal:
export CURSOR_API_KEY="your-service-account-key"
uvx modal-cursor init gpu-training --no-deploy
uvx modal-cursor deploy
init writes pools/gpu-training.py. Customize its worker image, resources,
secrets, and Modal sandbox options before deploying the all-pools control
plane.
For a repository-scoped pool:
uvx modal-cursor init payments \
--repo-url https://github.com/acme/payments
Add --private-repo to configure a worker-side Modal secret containing
GITHUB_TOKEN. The token is used by a temporary Git credential helper during
clone and is removed before the Cursor worker starts; it is not written into
the repository remote URL. Only HTTPS github.com/<owner>/<repo> URLs are
accepted; unsupported repository hosts fail during configuration instead of
during a worker launch.
Destroying a pool stops the shared Modal control plane and uses the live Cursor
registry record—including repo_owner and repo_name for repository-scoped
pools—to deregister it:
uvx modal-cursor destroy pools/payments.py --yes
Runtime design
The runtime has four small boundaries:
Poolowns the canonical pool name, repository scope, Cursor registration, and the pinned worker/control-plane images.Machineis an immutable worker specification. It rejects environment names and Modal options that would override values needed by the worker.Claimis a Pydantic Settings model for the non-secret values passed from the controller to sandbox provisioning.registry.pyowns typed request and response models for the Cursor pool and claim APIs. Unexpected success payloads fail loudly.
The control plane uses Cursor's unfiltered pending-request stream, routes each request by its pool label, atomically claims it, and provisions the matching Modal sandbox. The provisioner monitors the sandbox until Cursor exposes the claimed worker ID, failing on an early sandbox exit or readiness timeout; a failed claim is released for retry.
This deployment uses ephemeral Modal sandboxes, so it registers
workerReadyTimeoutSeconds=0: follow-ups reacquire on a fresh sandbox after a
worker exits. Snapshot/restore hibernation is not supported; nonzero reconnect
windows are rejected during configuration. The controller image installs a
versioned, SHA-256-verified Cursor CLI lab-channel archive instead of
executing an unpinned remote install script.
Credentials
CURSOR_API_KEY is a long-lived Cursor service-account key—not a claim-scoped
credential. Store it in a Modal Secret (the generated default is
cursor-service-account) and treat every controller and worker sandbox as part
of that credential's trust boundary.
The controller receives this key from its Modal Secret and injects it directly
into the worker environment because the Cursor worker CLI requires it. Private
repository credentials are separate: the clone shell receives GITHUB_TOKEN
only when its generated configuration includes the requested GitHub Modal
Secret, and unsets it before launching the Cursor agent.
Runtime tuning is available through the optional MODAL_CURSOR_SANDBOX_TIMEOUT_S,
MODAL_CURSOR_IDLE_RELEASE_TIMEOUT_S, MODAL_CURSOR_SPAWNER_READY_TIMEOUT_S,
MODAL_CURSOR_WORKER_POLL_INTERVAL_S, MODAL_CURSOR_CONTROLLER_TIMEOUT_S, and
MODAL_CURSOR_CONTROLLER_MAX_RETRIES environment variables. Set the standard
OTEL_EXPORTER_OTLP_ENDPOINT environment variable to choose the base URL for
OTLP/HTTP telemetry export; it is validated with the other Pydantic runtime
settings and propagated to the deployed control plane. OTEL_SERVICE_NAME
controls the emitted service name.
Observability
Lifecycle spans and Cursor API request spans are emitted as OpenTelemetry
spans. Set OTEL_EXPORTER_OTLP_ENDPOINT to the base URL of an OTLP-compatible
backend. Without an export configuration, instrumentation is quiet and has no
effect on pool operation.
Spans include pool, request, worker, sandbox, and outcome metadata, but never
Cursor API keys, Modal Secrets, or complete claim/machine payloads.
The controller does not keep one process-lifetime span open: exporters only make completed spans queryable, and a durable controller would otherwise hide its root indefinitely. Registration and pending-request polling are bounded operational spans. Each asynchronous request dispatch is its own visible root trace, with a span link back to the controller context at discovery time, so concurrent requests do not merge into one waterfall:
Control-plane operational spans:
├─ modal_cursor.controller.startup
│ ├─ modal_cursor.pool.register
│ └─ modal_cursor.pool.register
└─ modal_cursor.registry.list_pending_requests
Per-request trace (linked to controller discovery context):
modal_cursor.controller.dispatch
├─ modal_cursor.registry.claim_pending_request
└─ modal_cursor.worker.provision
├─ modal_cursor.worker.create_sandbox
└─ modal_cursor.worker.wait_for_cursor_registration
├─ modal_cursor.worker.registration.poll # attempt=1, not_ready
│ └─ GET 404 # not visible to Cursor yet
└─ modal_cursor.worker.registration.poll # attempt=2, ready
└─ GET 200 # worker connected
Cursor's Enterprise OpenTelemetry export is logs and metrics, not a parent
trace emitted by the Cursor worker controller. The controller therefore owns
the request lifecycle and uses the Cursor request/conversation ID as a
correlation attribute. Cursor's records can be joined in Logfire by
cursor.conversation.id, but they cannot be made children of our Modal spans
without a W3C trace context from Cursor. Because discovery and dispatch cross
an asynchronous queue/thread boundary, the controller uses a span link rather
than pretending the dispatch is a synchronous child of the polling loop.
Registration-wait spans record whether the sandbox process remained alive,
whether registration was pending, the poll count, the registration outcome,
and the registration elapsed time. Each registration poll remains a child
span, making the interval before the worker becomes visible to Cursor explicit
without turning routine state transitions into extra records.
Operations
modal-cursor doctor checks more than object existence. It verifies Modal
credentials, declared secrets, the shared control-plane container, the Cursor
registry response schema, registration drift, and connected/in-use worker
counts. Zero connected workers is valid for a scale-to-zero pool; zero running
control-plane containers is not.
Pool files remain ordinary Python configuration modules. The CLI reads only their literal secret declarations for diagnostics; the deployment module loads the selected pool files to construct one shared Modal application.
Development
uv run ruff format --check modal_cursor tests
uv run ruff check modal_cursor tests
uv run mypy
uv run basedpyright
uv run coverage run -m pytest
uv run coverage report
uv build
The test suite is self-contained; it has no sibling path dependency. The
separate cursor-mock repository mirrors the current repository-aware
deregistration contract for larger integration tests.
Unit tests mock Modal and Cursor network boundaries. They do not prove that a new Cursor CLI release can enroll and serve a real agent. Before a production release, run a disposable live soak test: deploy a pool, create and claim an agent, observe the worker connect and finish a run, then destroy the pool.
Cursor's API is public beta and may change. Compare releases against the Cursor Cloud Agents API and the Modal documentation before upgrading pinned runtime components.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file modal_cursor-0.1.1.tar.gz.
File metadata
- Download URL: modal_cursor-0.1.1.tar.gz
- Upload date:
- Size: 183.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c5110ce1e1e65b43bcd59d7e6b5b6eb1559ba9d3a1bddd877c97fd34e026d29
|
|
| MD5 |
c067d6f088e180ec75f9403cbe6ee4aa
|
|
| BLAKE2b-256 |
b30c550f4d06714be53d9bcda2329704e3e5850bbc7bbfc18515b8eb6a751b6f
|
Provenance
The following attestation bundles were made for modal_cursor-0.1.1.tar.gz:
Publisher:
workflow.yml on modal-labs/modal-cursor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
modal_cursor-0.1.1.tar.gz -
Subject digest:
8c5110ce1e1e65b43bcd59d7e6b5b6eb1559ba9d3a1bddd877c97fd34e026d29 - Sigstore transparency entry: 2657704571
- Sigstore integration time:
-
Permalink:
modal-labs/modal-cursor@c8e94094a0787c87d173f150a74c4074565be1a2 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/modal-labs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@c8e94094a0787c87d173f150a74c4074565be1a2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file modal_cursor-0.1.1-py3-none-any.whl.
File metadata
- Download URL: modal_cursor-0.1.1-py3-none-any.whl
- Upload date:
- Size: 33.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43881669bfd7a000c914d29b6deffd1adad74ff19f2e9a7dd783cce6d6ae0458
|
|
| MD5 |
62ae9bae7d1c79709be53d5e9860b85b
|
|
| BLAKE2b-256 |
417efdd7aa549dcd04dcd2d098bc0d9fb4d042a8a2e692bfdcc09fb0c09b39f6
|
Provenance
The following attestation bundles were made for modal_cursor-0.1.1-py3-none-any.whl:
Publisher:
workflow.yml on modal-labs/modal-cursor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
modal_cursor-0.1.1-py3-none-any.whl -
Subject digest:
43881669bfd7a000c914d29b6deffd1adad74ff19f2e9a7dd783cce6d6ae0458 - Sigstore transparency entry: 2657704664
- Sigstore integration time:
-
Permalink:
modal-labs/modal-cursor@c8e94094a0787c87d173f150a74c4074565be1a2 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/modal-labs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@c8e94094a0787c87d173f150a74c4074565be1a2 -
Trigger Event:
release
-
Statement type: