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.
Files changed (31) hide show
  1. modal_cursor-0.1.0/.github/workflows/ci.yml +42 -0
  2. modal_cursor-0.1.0/.github/workflows/workflow.yml +20 -0
  3. modal_cursor-0.1.0/.gitignore +19 -0
  4. modal_cursor-0.1.0/.python-version +1 -0
  5. modal_cursor-0.1.0/PKG-INFO +212 -0
  6. modal_cursor-0.1.0/README.md +197 -0
  7. modal_cursor-0.1.0/example.md +250 -0
  8. modal_cursor-0.1.0/modal_cursor/__init__.py +30 -0
  9. modal_cursor-0.1.0/modal_cursor/__main__.py +4 -0
  10. modal_cursor-0.1.0/modal_cursor/cli.py +621 -0
  11. modal_cursor-0.1.0/modal_cursor/control_plane.py +104 -0
  12. modal_cursor-0.1.0/modal_cursor/controller.py +226 -0
  13. modal_cursor-0.1.0/modal_cursor/otel_proxy.py +113 -0
  14. modal_cursor-0.1.0/modal_cursor/pool.py +170 -0
  15. modal_cursor-0.1.0/modal_cursor/pools.py +244 -0
  16. modal_cursor-0.1.0/modal_cursor/py.typed +1 -0
  17. modal_cursor-0.1.0/modal_cursor/registry.py +444 -0
  18. modal_cursor-0.1.0/modal_cursor/spawn.py +182 -0
  19. modal_cursor-0.1.0/modal_cursor/telemetry.py +226 -0
  20. modal_cursor-0.1.0/modal_cursor/templates/pool.py.tmpl +17 -0
  21. modal_cursor-0.1.0/pyproject.toml +67 -0
  22. modal_cursor-0.1.0/pyrightconfig.json +7 -0
  23. modal_cursor-0.1.0/tests/test_cli.py +343 -0
  24. modal_cursor-0.1.0/tests/test_controller.py +91 -0
  25. modal_cursor-0.1.0/tests/test_otel_proxy.py +53 -0
  26. modal_cursor-0.1.0/tests/test_pool.py +74 -0
  27. modal_cursor-0.1.0/tests/test_pools.py +161 -0
  28. modal_cursor-0.1.0/tests/test_registry.py +255 -0
  29. modal_cursor-0.1.0/tests/test_spawn.py +94 -0
  30. modal_cursor-0.1.0/tests/test_telemetry.py +101 -0
  31. 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.