Skip to main content

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:

  • Pool owns the canonical pool name, repository scope, Cursor registration, and the pinned worker/control-plane images.
  • Machine is an immutable worker specification. It rejects environment names and Modal options that would override values needed by the worker.
  • Claim is a Pydantic Settings model for the non-secret values passed from the controller to sandbox provisioning.
  • registry.py owns 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.

Metadata

Release files for modal-cursor 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for modal-cursor 0.1.1
File Size Uploaded Provenance
modal_cursor-0.1.1.tar.gz 183.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for modal-cursor 0.1.1
File Interpreter ABI Platform
modal_cursor-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 217.2 kB

Security for modal-cursor 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for modal_cursor-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 31, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page