modal-cursor 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal_cursor-0.1.0/.github/workflows/ci.yml +42 -0
- modal_cursor-0.1.0/.github/workflows/workflow.yml +20 -0
- modal_cursor-0.1.0/.gitignore +19 -0
- modal_cursor-0.1.0/.python-version +1 -0
- modal_cursor-0.1.0/PKG-INFO +212 -0
- modal_cursor-0.1.0/README.md +197 -0
- modal_cursor-0.1.0/example.md +250 -0
- modal_cursor-0.1.0/modal_cursor/__init__.py +30 -0
- modal_cursor-0.1.0/modal_cursor/__main__.py +4 -0
- modal_cursor-0.1.0/modal_cursor/cli.py +621 -0
- modal_cursor-0.1.0/modal_cursor/control_plane.py +104 -0
- modal_cursor-0.1.0/modal_cursor/controller.py +226 -0
- modal_cursor-0.1.0/modal_cursor/otel_proxy.py +113 -0
- modal_cursor-0.1.0/modal_cursor/pool.py +170 -0
- modal_cursor-0.1.0/modal_cursor/pools.py +244 -0
- modal_cursor-0.1.0/modal_cursor/py.typed +1 -0
- modal_cursor-0.1.0/modal_cursor/registry.py +444 -0
- modal_cursor-0.1.0/modal_cursor/spawn.py +182 -0
- modal_cursor-0.1.0/modal_cursor/telemetry.py +226 -0
- modal_cursor-0.1.0/modal_cursor/templates/pool.py.tmpl +17 -0
- modal_cursor-0.1.0/pyproject.toml +67 -0
- modal_cursor-0.1.0/pyrightconfig.json +7 -0
- modal_cursor-0.1.0/tests/test_cli.py +343 -0
- modal_cursor-0.1.0/tests/test_controller.py +91 -0
- modal_cursor-0.1.0/tests/test_otel_proxy.py +53 -0
- modal_cursor-0.1.0/tests/test_pool.py +74 -0
- modal_cursor-0.1.0/tests/test_pools.py +161 -0
- modal_cursor-0.1.0/tests/test_registry.py +255 -0
- modal_cursor-0.1.0/tests/test_spawn.py +94 -0
- modal_cursor-0.1.0/tests/test_telemetry.py +101 -0
- modal_cursor-0.1.0/uv.lock +2161 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
quality:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
16
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
17
|
+
with:
|
|
18
|
+
enable-cache: true
|
|
19
|
+
python-version: "3.11"
|
|
20
|
+
- run: uv sync --locked --all-groups
|
|
21
|
+
- run: uv run ruff format --check modal_cursor tests
|
|
22
|
+
- run: uv run ruff check modal_cursor tests
|
|
23
|
+
- run: uv run mypy
|
|
24
|
+
- run: uv run coverage run -m pytest
|
|
25
|
+
- run: uv run coverage report
|
|
26
|
+
- run: uv build
|
|
27
|
+
|
|
28
|
+
tests:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
strategy:
|
|
31
|
+
fail-fast: false
|
|
32
|
+
matrix:
|
|
33
|
+
python-version: ["3.11", "3.14"]
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
36
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
37
|
+
with:
|
|
38
|
+
enable-cache: true
|
|
39
|
+
cache-suffix: py${{ matrix.python-version }}
|
|
40
|
+
python-version: ${{ matrix.python-version }}
|
|
41
|
+
- run: uv sync --locked --all-groups
|
|
42
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
15
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.11"
|
|
18
|
+
- run: uv build
|
|
19
|
+
- name: Publish package distributions to PyPI
|
|
20
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Test caches
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
|
|
18
|
+
# Generated pool applications
|
|
19
|
+
pools/*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: modal-cursor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Modal-backed controller for Cursor bring-your-own-machine worker pools.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: cyclopts<5,>=4.20
|
|
7
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
8
|
+
Requires-Dist: httpx<1,>=0.28.1
|
|
9
|
+
Requires-Dist: logfire[httpx]<5,>=4.41
|
|
10
|
+
Requires-Dist: modal<2,>=1.5.4
|
|
11
|
+
Requires-Dist: opentelemetry-api<2,>=1.44
|
|
12
|
+
Requires-Dist: pydantic-settings<3,>=2.15.0
|
|
13
|
+
Requires-Dist: rich<16,>=13.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# modal-cursor
|
|
17
|
+
|
|
18
|
+
Run Cursor bring-your-own-machine worker pools on Modal. One durable Modal
|
|
19
|
+
control-plane controller owns registration and dispatch for every configured
|
|
20
|
+
Cursor pool, then creates an isolated sandbox for each claimed request.
|
|
21
|
+
|
|
22
|
+
This project targets Python 3.11 and newer.
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv sync --all-groups
|
|
28
|
+
uv run modal setup
|
|
29
|
+
export CURSOR_API_KEY="your-service-account-key"
|
|
30
|
+
uv run modal-cursor init gpu-training
|
|
31
|
+
uv run modal-cursor deploy
|
|
32
|
+
uv run modal-cursor doctor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`init` writes an editable `pools/gpu-training.py` configuration. Customize its
|
|
36
|
+
worker image, resources, secrets, and Modal sandbox options before deploying
|
|
37
|
+
the all-pools control plane.
|
|
38
|
+
|
|
39
|
+
For a repository-scoped pool:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run modal-cursor init payments \
|
|
43
|
+
--repo-url https://github.com/acme/payments
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Add `--private-repo` to configure a worker-side Modal secret containing
|
|
47
|
+
`GITHUB_TOKEN`. The token is used by a temporary Git credential helper during
|
|
48
|
+
clone and is removed before the Cursor worker starts; it is not written into
|
|
49
|
+
the repository remote URL. Only HTTPS `github.com/<owner>/<repo>` URLs are
|
|
50
|
+
accepted; unsupported repository hosts fail during configuration instead of
|
|
51
|
+
during a worker launch.
|
|
52
|
+
|
|
53
|
+
Destroying a pool stops the shared Modal control plane and uses the live Cursor
|
|
54
|
+
registry record—including `repo_owner` and `repo_name` for repository-scoped
|
|
55
|
+
pools—to deregister it:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv run modal-cursor destroy pools/payments.py --yes
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Runtime design
|
|
62
|
+
|
|
63
|
+
The runtime has four small boundaries:
|
|
64
|
+
|
|
65
|
+
- `Pool` owns the canonical pool name, repository scope, Cursor registration,
|
|
66
|
+
and the pinned worker/control-plane images.
|
|
67
|
+
- `Machine` is an immutable worker specification. It rejects environment names
|
|
68
|
+
and Modal options that would override values needed by the worker.
|
|
69
|
+
- `Claim` is a Pydantic Settings model for the non-secret values passed from
|
|
70
|
+
the controller to sandbox provisioning.
|
|
71
|
+
- `registry.py` owns typed request and response models for the Cursor pool and
|
|
72
|
+
claim APIs. Unexpected success payloads fail loudly.
|
|
73
|
+
|
|
74
|
+
The control plane uses Cursor's unfiltered pending-request stream, routes each
|
|
75
|
+
request by its pool label, atomically claims it, and provisions the matching
|
|
76
|
+
Modal sandbox. The provisioner monitors the sandbox until Cursor exposes the
|
|
77
|
+
claimed worker ID, failing on an early sandbox exit or readiness timeout; a
|
|
78
|
+
failed claim is released for retry.
|
|
79
|
+
|
|
80
|
+
This deployment uses ephemeral Modal sandboxes, so it registers
|
|
81
|
+
`workerReadyTimeoutSeconds=0`: follow-ups reacquire on a fresh sandbox after a
|
|
82
|
+
worker exits. Snapshot/restore hibernation is not supported; nonzero reconnect
|
|
83
|
+
windows are rejected during configuration. The controller image installs a
|
|
84
|
+
versioned, SHA-256-verified Cursor CLI lab-channel archive instead of
|
|
85
|
+
executing an unpinned remote install script.
|
|
86
|
+
|
|
87
|
+
## Credentials
|
|
88
|
+
|
|
89
|
+
`CURSOR_API_KEY` is a long-lived Cursor service-account key—not a claim-scoped
|
|
90
|
+
credential. Store it in a Modal Secret (the generated default is
|
|
91
|
+
`cursor-service-account`) and treat every controller and worker sandbox as part
|
|
92
|
+
of that credential's trust boundary.
|
|
93
|
+
|
|
94
|
+
The controller receives this key from its Modal Secret and injects it directly
|
|
95
|
+
into the worker environment because the Cursor worker CLI requires it. Private
|
|
96
|
+
repository credentials are separate: the clone shell receives `GITHUB_TOKEN`
|
|
97
|
+
only when its generated configuration includes the requested GitHub Modal
|
|
98
|
+
Secret, and unsets it before launching the Cursor agent.
|
|
99
|
+
|
|
100
|
+
Runtime tuning is available through the optional `MODAL_CURSOR_SANDBOX_TIMEOUT_S`,
|
|
101
|
+
`MODAL_CURSOR_IDLE_RELEASE_TIMEOUT_S`, `MODAL_CURSOR_SPAWNER_READY_TIMEOUT_S`,
|
|
102
|
+
`MODAL_CURSOR_WORKER_POLL_INTERVAL_S`, `MODAL_CURSOR_CONTROLLER_TIMEOUT_S`, and
|
|
103
|
+
`MODAL_CURSOR_CONTROLLER_MAX_RETRIES` environment variables. Set the standard
|
|
104
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` environment variable to choose the base URL for
|
|
105
|
+
OTLP/HTTP telemetry export; it is validated with the other Pydantic runtime
|
|
106
|
+
settings and propagated to the deployed control plane. `OTEL_SERVICE_NAME`
|
|
107
|
+
controls the emitted service name.
|
|
108
|
+
|
|
109
|
+
## Observability
|
|
110
|
+
|
|
111
|
+
Lifecycle spans and Cursor API request spans are emitted as OpenTelemetry
|
|
112
|
+
spans. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to the base URL of an OTLP-compatible
|
|
113
|
+
backend. Without an export configuration, instrumentation is quiet and has no
|
|
114
|
+
effect on pool operation.
|
|
115
|
+
Spans include pool, request, worker, sandbox, and outcome metadata, but never
|
|
116
|
+
Cursor API keys, Modal Secrets, or complete claim/machine payloads.
|
|
117
|
+
|
|
118
|
+
Cursor's Enterprise OpenTelemetry Export can be routed through the optional
|
|
119
|
+
authenticated Modal bridge when a backend's OTLP acknowledgement is too strict
|
|
120
|
+
for Cursor's connection test. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to the
|
|
121
|
+
backend's base URL, then deploy it with:
|
|
122
|
+
|
|
123
|
+
```console
|
|
124
|
+
uv run modal deploy modal_cursor/otel_proxy.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Use the printed `modal.run` URL as Cursor's collector base URL, without `/v1`.
|
|
128
|
+
Add an `X-Logfire-Token` header whose value matches the bridge's configured
|
|
129
|
+
upstream write token, then enable logs and metrics. The bridge forwards
|
|
130
|
+
`/v1/logs` and `/v1/metrics` to the configured backend and returns a protobuf
|
|
131
|
+
acknowledgement. `Authorization` is also accepted, but the dedicated header
|
|
132
|
+
avoids client-specific authorization-header handling. Deploy behind a separate
|
|
133
|
+
ingress credential if the endpoint will be shared beyond this team.
|
|
134
|
+
|
|
135
|
+
The controller does not keep one process-lifetime span open: exporters only
|
|
136
|
+
make completed spans queryable, and a durable controller would otherwise hide
|
|
137
|
+
its root indefinitely. Registration and pending-request polling are bounded
|
|
138
|
+
operational spans. Each asynchronous request dispatch is its own visible root
|
|
139
|
+
trace, with a span link back to the controller context at discovery time, so
|
|
140
|
+
concurrent requests do not merge into one waterfall:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
Control-plane operational spans:
|
|
144
|
+
├─ modal_cursor.controller.startup
|
|
145
|
+
│ ├─ modal_cursor.pool.register
|
|
146
|
+
│ └─ modal_cursor.pool.register
|
|
147
|
+
└─ modal_cursor.registry.list_pending_requests
|
|
148
|
+
|
|
149
|
+
Per-request trace (linked to controller discovery context):
|
|
150
|
+
modal_cursor.controller.dispatch
|
|
151
|
+
├─ modal_cursor.registry.claim_pending_request
|
|
152
|
+
└─ modal_cursor.worker.provision
|
|
153
|
+
├─ modal_cursor.worker.create_sandbox
|
|
154
|
+
└─ modal_cursor.worker.wait_for_cursor_registration
|
|
155
|
+
├─ modal_cursor.worker.registration.poll # attempt=1, not_ready
|
|
156
|
+
│ └─ GET 404 # not visible to Cursor yet
|
|
157
|
+
└─ modal_cursor.worker.registration.poll # attempt=2, ready
|
|
158
|
+
└─ GET 200 # worker connected
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Cursor's Enterprise OpenTelemetry export is logs and metrics, not a parent
|
|
162
|
+
trace emitted by the Cursor worker controller. The controller therefore owns
|
|
163
|
+
the request lifecycle and uses the Cursor request/conversation ID as a
|
|
164
|
+
correlation attribute. Cursor's records can be joined in Logfire by
|
|
165
|
+
`cursor.conversation.id`, but they cannot be made children of our Modal spans
|
|
166
|
+
without a W3C trace context from Cursor. Because discovery and dispatch cross
|
|
167
|
+
an asynchronous queue/thread boundary, the controller uses a span link rather
|
|
168
|
+
than pretending the dispatch is a synchronous child of the polling loop.
|
|
169
|
+
Registration-wait spans record whether the sandbox process remained alive,
|
|
170
|
+
whether registration was pending, the poll count, the registration outcome,
|
|
171
|
+
and the registration elapsed time. Each registration poll remains a child
|
|
172
|
+
span, making the interval before the worker becomes visible to Cursor explicit
|
|
173
|
+
without turning routine state transitions into extra records.
|
|
174
|
+
|
|
175
|
+
## Operations
|
|
176
|
+
|
|
177
|
+
`modal-cursor doctor` checks more than object existence. It verifies Modal
|
|
178
|
+
credentials, declared secrets, the shared control-plane container, the Cursor
|
|
179
|
+
registry response schema, registration drift, and connected/in-use worker
|
|
180
|
+
counts. Zero connected workers is valid for a scale-to-zero pool; zero running
|
|
181
|
+
control-plane containers is not.
|
|
182
|
+
|
|
183
|
+
Pool files remain ordinary Python configuration modules. The CLI reads only
|
|
184
|
+
their literal secret declarations for diagnostics; the deployment module loads
|
|
185
|
+
the selected pool files to construct one shared Modal application.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
uv sync --all-groups
|
|
191
|
+
uv run ruff format --check modal_cursor tests
|
|
192
|
+
uv run ruff check modal_cursor tests
|
|
193
|
+
uv run mypy
|
|
194
|
+
uv run basedpyright
|
|
195
|
+
uv run coverage run -m pytest
|
|
196
|
+
uv run coverage report
|
|
197
|
+
uv build
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The test suite is self-contained; it has no sibling path dependency. The
|
|
201
|
+
separate `cursor-mock` repository mirrors the current repository-aware
|
|
202
|
+
deregistration contract for larger integration tests.
|
|
203
|
+
|
|
204
|
+
Unit tests mock Modal and Cursor network boundaries. They do not prove that a
|
|
205
|
+
new Cursor CLI release can enroll and serve a real agent. Before a production
|
|
206
|
+
release, run a disposable live soak test: deploy a pool, create and claim an
|
|
207
|
+
agent, observe the worker connect and finish a run, then destroy the pool.
|
|
208
|
+
|
|
209
|
+
Cursor's API is public beta and may change. Compare releases against the
|
|
210
|
+
[Cursor Cloud Agents API](https://cursor.com/docs/cloud-agent/api/endpoints) and
|
|
211
|
+
the [Modal documentation](https://modal.com/docs) before upgrading pinned
|
|
212
|
+
runtime components.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# modal-cursor
|
|
2
|
+
|
|
3
|
+
Run Cursor bring-your-own-machine worker pools on Modal. One durable Modal
|
|
4
|
+
control-plane controller owns registration and dispatch for every configured
|
|
5
|
+
Cursor pool, then creates an isolated sandbox for each claimed request.
|
|
6
|
+
|
|
7
|
+
This project targets Python 3.11 and newer.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync --all-groups
|
|
13
|
+
uv run modal setup
|
|
14
|
+
export CURSOR_API_KEY="your-service-account-key"
|
|
15
|
+
uv run modal-cursor init gpu-training
|
|
16
|
+
uv run modal-cursor deploy
|
|
17
|
+
uv run modal-cursor doctor
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`init` writes an editable `pools/gpu-training.py` configuration. Customize its
|
|
21
|
+
worker image, resources, secrets, and Modal sandbox options before deploying
|
|
22
|
+
the all-pools control plane.
|
|
23
|
+
|
|
24
|
+
For a repository-scoped pool:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv run modal-cursor init payments \
|
|
28
|
+
--repo-url https://github.com/acme/payments
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add `--private-repo` to configure a worker-side Modal secret containing
|
|
32
|
+
`GITHUB_TOKEN`. The token is used by a temporary Git credential helper during
|
|
33
|
+
clone and is removed before the Cursor worker starts; it is not written into
|
|
34
|
+
the repository remote URL. Only HTTPS `github.com/<owner>/<repo>` URLs are
|
|
35
|
+
accepted; unsupported repository hosts fail during configuration instead of
|
|
36
|
+
during a worker launch.
|
|
37
|
+
|
|
38
|
+
Destroying a pool stops the shared Modal control plane and uses the live Cursor
|
|
39
|
+
registry record—including `repo_owner` and `repo_name` for repository-scoped
|
|
40
|
+
pools—to deregister it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv run modal-cursor destroy pools/payments.py --yes
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Runtime design
|
|
47
|
+
|
|
48
|
+
The runtime has four small boundaries:
|
|
49
|
+
|
|
50
|
+
- `Pool` owns the canonical pool name, repository scope, Cursor registration,
|
|
51
|
+
and the pinned worker/control-plane images.
|
|
52
|
+
- `Machine` is an immutable worker specification. It rejects environment names
|
|
53
|
+
and Modal options that would override values needed by the worker.
|
|
54
|
+
- `Claim` is a Pydantic Settings model for the non-secret values passed from
|
|
55
|
+
the controller to sandbox provisioning.
|
|
56
|
+
- `registry.py` owns typed request and response models for the Cursor pool and
|
|
57
|
+
claim APIs. Unexpected success payloads fail loudly.
|
|
58
|
+
|
|
59
|
+
The control plane uses Cursor's unfiltered pending-request stream, routes each
|
|
60
|
+
request by its pool label, atomically claims it, and provisions the matching
|
|
61
|
+
Modal sandbox. The provisioner monitors the sandbox until Cursor exposes the
|
|
62
|
+
claimed worker ID, failing on an early sandbox exit or readiness timeout; a
|
|
63
|
+
failed claim is released for retry.
|
|
64
|
+
|
|
65
|
+
This deployment uses ephemeral Modal sandboxes, so it registers
|
|
66
|
+
`workerReadyTimeoutSeconds=0`: follow-ups reacquire on a fresh sandbox after a
|
|
67
|
+
worker exits. Snapshot/restore hibernation is not supported; nonzero reconnect
|
|
68
|
+
windows are rejected during configuration. The controller image installs a
|
|
69
|
+
versioned, SHA-256-verified Cursor CLI lab-channel archive instead of
|
|
70
|
+
executing an unpinned remote install script.
|
|
71
|
+
|
|
72
|
+
## Credentials
|
|
73
|
+
|
|
74
|
+
`CURSOR_API_KEY` is a long-lived Cursor service-account key—not a claim-scoped
|
|
75
|
+
credential. Store it in a Modal Secret (the generated default is
|
|
76
|
+
`cursor-service-account`) and treat every controller and worker sandbox as part
|
|
77
|
+
of that credential's trust boundary.
|
|
78
|
+
|
|
79
|
+
The controller receives this key from its Modal Secret and injects it directly
|
|
80
|
+
into the worker environment because the Cursor worker CLI requires it. Private
|
|
81
|
+
repository credentials are separate: the clone shell receives `GITHUB_TOKEN`
|
|
82
|
+
only when its generated configuration includes the requested GitHub Modal
|
|
83
|
+
Secret, and unsets it before launching the Cursor agent.
|
|
84
|
+
|
|
85
|
+
Runtime tuning is available through the optional `MODAL_CURSOR_SANDBOX_TIMEOUT_S`,
|
|
86
|
+
`MODAL_CURSOR_IDLE_RELEASE_TIMEOUT_S`, `MODAL_CURSOR_SPAWNER_READY_TIMEOUT_S`,
|
|
87
|
+
`MODAL_CURSOR_WORKER_POLL_INTERVAL_S`, `MODAL_CURSOR_CONTROLLER_TIMEOUT_S`, and
|
|
88
|
+
`MODAL_CURSOR_CONTROLLER_MAX_RETRIES` environment variables. Set the standard
|
|
89
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` environment variable to choose the base URL for
|
|
90
|
+
OTLP/HTTP telemetry export; it is validated with the other Pydantic runtime
|
|
91
|
+
settings and propagated to the deployed control plane. `OTEL_SERVICE_NAME`
|
|
92
|
+
controls the emitted service name.
|
|
93
|
+
|
|
94
|
+
## Observability
|
|
95
|
+
|
|
96
|
+
Lifecycle spans and Cursor API request spans are emitted as OpenTelemetry
|
|
97
|
+
spans. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to the base URL of an OTLP-compatible
|
|
98
|
+
backend. Without an export configuration, instrumentation is quiet and has no
|
|
99
|
+
effect on pool operation.
|
|
100
|
+
Spans include pool, request, worker, sandbox, and outcome metadata, but never
|
|
101
|
+
Cursor API keys, Modal Secrets, or complete claim/machine payloads.
|
|
102
|
+
|
|
103
|
+
Cursor's Enterprise OpenTelemetry Export can be routed through the optional
|
|
104
|
+
authenticated Modal bridge when a backend's OTLP acknowledgement is too strict
|
|
105
|
+
for Cursor's connection test. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to the
|
|
106
|
+
backend's base URL, then deploy it with:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
uv run modal deploy modal_cursor/otel_proxy.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use the printed `modal.run` URL as Cursor's collector base URL, without `/v1`.
|
|
113
|
+
Add an `X-Logfire-Token` header whose value matches the bridge's configured
|
|
114
|
+
upstream write token, then enable logs and metrics. The bridge forwards
|
|
115
|
+
`/v1/logs` and `/v1/metrics` to the configured backend and returns a protobuf
|
|
116
|
+
acknowledgement. `Authorization` is also accepted, but the dedicated header
|
|
117
|
+
avoids client-specific authorization-header handling. Deploy behind a separate
|
|
118
|
+
ingress credential if the endpoint will be shared beyond this team.
|
|
119
|
+
|
|
120
|
+
The controller does not keep one process-lifetime span open: exporters only
|
|
121
|
+
make completed spans queryable, and a durable controller would otherwise hide
|
|
122
|
+
its root indefinitely. Registration and pending-request polling are bounded
|
|
123
|
+
operational spans. Each asynchronous request dispatch is its own visible root
|
|
124
|
+
trace, with a span link back to the controller context at discovery time, so
|
|
125
|
+
concurrent requests do not merge into one waterfall:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
Control-plane operational spans:
|
|
129
|
+
├─ modal_cursor.controller.startup
|
|
130
|
+
│ ├─ modal_cursor.pool.register
|
|
131
|
+
│ └─ modal_cursor.pool.register
|
|
132
|
+
└─ modal_cursor.registry.list_pending_requests
|
|
133
|
+
|
|
134
|
+
Per-request trace (linked to controller discovery context):
|
|
135
|
+
modal_cursor.controller.dispatch
|
|
136
|
+
├─ modal_cursor.registry.claim_pending_request
|
|
137
|
+
└─ modal_cursor.worker.provision
|
|
138
|
+
├─ modal_cursor.worker.create_sandbox
|
|
139
|
+
└─ modal_cursor.worker.wait_for_cursor_registration
|
|
140
|
+
├─ modal_cursor.worker.registration.poll # attempt=1, not_ready
|
|
141
|
+
│ └─ GET 404 # not visible to Cursor yet
|
|
142
|
+
└─ modal_cursor.worker.registration.poll # attempt=2, ready
|
|
143
|
+
└─ GET 200 # worker connected
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Cursor's Enterprise OpenTelemetry export is logs and metrics, not a parent
|
|
147
|
+
trace emitted by the Cursor worker controller. The controller therefore owns
|
|
148
|
+
the request lifecycle and uses the Cursor request/conversation ID as a
|
|
149
|
+
correlation attribute. Cursor's records can be joined in Logfire by
|
|
150
|
+
`cursor.conversation.id`, but they cannot be made children of our Modal spans
|
|
151
|
+
without a W3C trace context from Cursor. Because discovery and dispatch cross
|
|
152
|
+
an asynchronous queue/thread boundary, the controller uses a span link rather
|
|
153
|
+
than pretending the dispatch is a synchronous child of the polling loop.
|
|
154
|
+
Registration-wait spans record whether the sandbox process remained alive,
|
|
155
|
+
whether registration was pending, the poll count, the registration outcome,
|
|
156
|
+
and the registration elapsed time. Each registration poll remains a child
|
|
157
|
+
span, making the interval before the worker becomes visible to Cursor explicit
|
|
158
|
+
without turning routine state transitions into extra records.
|
|
159
|
+
|
|
160
|
+
## Operations
|
|
161
|
+
|
|
162
|
+
`modal-cursor doctor` checks more than object existence. It verifies Modal
|
|
163
|
+
credentials, declared secrets, the shared control-plane container, the Cursor
|
|
164
|
+
registry response schema, registration drift, and connected/in-use worker
|
|
165
|
+
counts. Zero connected workers is valid for a scale-to-zero pool; zero running
|
|
166
|
+
control-plane containers is not.
|
|
167
|
+
|
|
168
|
+
Pool files remain ordinary Python configuration modules. The CLI reads only
|
|
169
|
+
their literal secret declarations for diagnostics; the deployment module loads
|
|
170
|
+
the selected pool files to construct one shared Modal application.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
uv sync --all-groups
|
|
176
|
+
uv run ruff format --check modal_cursor tests
|
|
177
|
+
uv run ruff check modal_cursor tests
|
|
178
|
+
uv run mypy
|
|
179
|
+
uv run basedpyright
|
|
180
|
+
uv run coverage run -m pytest
|
|
181
|
+
uv run coverage report
|
|
182
|
+
uv build
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The test suite is self-contained; it has no sibling path dependency. The
|
|
186
|
+
separate `cursor-mock` repository mirrors the current repository-aware
|
|
187
|
+
deregistration contract for larger integration tests.
|
|
188
|
+
|
|
189
|
+
Unit tests mock Modal and Cursor network boundaries. They do not prove that a
|
|
190
|
+
new Cursor CLI release can enroll and serve a real agent. Before a production
|
|
191
|
+
release, run a disposable live soak test: deploy a pool, create and claim an
|
|
192
|
+
agent, observe the worker connect and finish a run, then destroy the pool.
|
|
193
|
+
|
|
194
|
+
Cursor's API is public beta and may change. Compare releases against the
|
|
195
|
+
[Cursor Cloud Agents API](https://cursor.com/docs/cloud-agent/api/endpoints) and
|
|
196
|
+
the [Modal documentation](https://modal.com/docs) before upgrading pinned
|
|
197
|
+
runtime components.
|