@moondeck/station 0.0.0-stage → 1.0.0

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 (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -2
  3. package/bin/uai-host.mjs +130 -0
  4. package/db/migrations/0000_host_tasks.sql +12 -0
  5. package/db/migrations/0001_host_ui.sql +11 -0
  6. package/db/migrations/0002_host_github_tokens.sql +8 -0
  7. package/db/migrations/0003_host_ssh_keys.sql +8 -0
  8. package/db/migrations/0004_host_owner_name.sql +1 -0
  9. package/db/migrations/0005_host_env_vars.sql +5 -0
  10. package/db/migrations/0006_host_project_env.sql +9 -0
  11. package/db/migrations/0007_host_github_token_kind.sql +7 -0
  12. package/db/migrations/0008_host_mcp_connections.sql +18 -0
  13. package/db/migrations/0009_host_agent_sessions.sql +15 -0
  14. package/db/migrations/0010_host_engine_accounts.sql +13 -0
  15. package/db/migrations/0011_codex_thread_id.sql +5 -0
  16. package/db/migrations/0012_agent_session_attach_key.sql +4 -0
  17. package/db/migrations/0013_github_credential_generations.sql +11 -0
  18. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  19. package/db/migrations/0015_host_settings.sql +9 -0
  20. package/db/migrations/0016_task_environment.sql +2 -0
  21. package/db/migrations/0017_engine_account_scope.sql +7 -0
  22. package/db/migrations/0018_engine_account_position.sql +3 -0
  23. package/db/migrations/0019_engine_account_store_ref.sql +3 -0
  24. package/db/migrations/0020_user_credential_store_ref.sql +6 -0
  25. package/db/migrations/0021_project_env_store_ref.sql +3 -0
  26. package/db/migrations/0022_task_project_ids.sql +5 -0
  27. package/db/migrations/0023_project_env_org.sql +4 -0
  28. package/db/migrations/0024_task_env_mode.sql +9 -0
  29. package/db/migrations/0025_project_env_authority.sql +147 -0
  30. package/db/migrations/0026_project_env_replica.sql +150 -0
  31. package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
  32. package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
  33. package/db/migrations/0029_task_shared_files_mode.sql +5 -0
  34. package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
  35. package/db/migrations/meta/_journal.json +223 -0
  36. package/db/schema.ts +850 -0
  37. package/images/standard/Dockerfile +347 -0
  38. package/images/standard/README.md +167 -0
  39. package/images/standard/container/code-server-settings.json +63 -0
  40. package/images/standard/container/corepack-version +1 -0
  41. package/images/standard/container/uai-init +704 -0
  42. package/images/standard/container/uai-materialize-runtimes +1527 -0
  43. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  44. package/images/standard/tool-versions +2 -0
  45. package/lib/__fixtures__/skill-index/SKILL.md +10 -0
  46. package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
  47. package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
  48. package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
  49. package/lib/agent-cli.ts +1507 -0
  50. package/lib/agent-errors.ts +22 -0
  51. package/lib/agent.ts +672 -0
  52. package/lib/agents/attach-compatibility.ts +51 -0
  53. package/lib/agents/claude.ts +918 -0
  54. package/lib/agents/codex-token-usage.fixture.json +130 -0
  55. package/lib/agents/codex.ts +1392 -0
  56. package/lib/agents/cursor-period-usage.ts +253 -0
  57. package/lib/agents/cursor.ts +562 -0
  58. package/lib/agents/dispatch.ts +78 -0
  59. package/lib/agents/durable-proc.ts +334 -0
  60. package/lib/agents/factory.ts +57 -0
  61. package/lib/agents/grok.ts +381 -0
  62. package/lib/agents/kimi.ts +329 -0
  63. package/lib/agents/machine-durable.ts +604 -0
  64. package/lib/agents/mock.ts +133 -0
  65. package/lib/agents/mode.ts +64 -0
  66. package/lib/agents/opencode.ts +508 -0
  67. package/lib/agents/output-source.ts +3 -0
  68. package/lib/agents/proc.ts +221 -0
  69. package/lib/agents/rate-limit.ts +337 -0
  70. package/lib/agents/registry.ts +182 -0
  71. package/lib/agents/session-state.ts +197 -0
  72. package/lib/agents/skill-index-preamble.fixture.ts +26 -0
  73. package/lib/agents/transport.ts +741 -0
  74. package/lib/agents/types.ts +254 -0
  75. package/lib/agents/uai.ts +106 -0
  76. package/lib/agents/usage.ts +44 -0
  77. package/lib/apple-one-off.ts +277 -0
  78. package/lib/apple-runtime-recycle.ts +237 -0
  79. package/lib/apple-uninstall-teardown.ts +224 -0
  80. package/lib/assignment-evidence.ts +94 -0
  81. package/lib/attachments.ts +48 -0
  82. package/lib/browser-testing.ts +1537 -0
  83. package/lib/capture-flow.ts +332 -0
  84. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  85. package/lib/cloud-credential-host.ts +512 -0
  86. package/lib/cloud-credential-leases.ts +2587 -0
  87. package/lib/cloud-credential-runtime.ts +1357 -0
  88. package/lib/cloud-state.ts +56 -0
  89. package/lib/codex-auth.ts +456 -0
  90. package/lib/command-db.ts +310 -0
  91. package/lib/compose-network-recovery.ts +283 -0
  92. package/lib/connection-secret-redaction.ts +25 -0
  93. package/lib/container-runtime.ts +1453 -0
  94. package/lib/credential-cleanup-inventory.ts +490 -0
  95. package/lib/credential-consumers/engine-anchor.ts +1880 -0
  96. package/lib/credential-consumers/engines.ts +797 -0
  97. package/lib/credential-consumers/files.ts +934 -0
  98. package/lib/credential-consumers/github.ts +702 -0
  99. package/lib/credential-consumers/mcp.ts +389 -0
  100. package/lib/credential-consumers/task-files.ts +127 -0
  101. package/lib/credential-consumers/task-secrets.ts +10 -0
  102. package/lib/credential-store-pickup.ts +149 -0
  103. package/lib/credential-store-scope.ts +112 -0
  104. package/lib/credential-store.ts +932 -0
  105. package/lib/credentials-activation-authority.ts +709 -0
  106. package/lib/credentials-purge.ts +1006 -0
  107. package/lib/custom-connection-capture.ts +395 -0
  108. package/lib/custom-connection-launch.ts +384 -0
  109. package/lib/custom-connection-runtime.ts +141 -0
  110. package/lib/db.ts +80 -0
  111. package/lib/disk-pressure.ts +42 -0
  112. package/lib/docker-exec.ts +156 -0
  113. package/lib/engine-accounts.ts +1970 -0
  114. package/lib/engine-connect.ts +321 -0
  115. package/lib/engine-credential-env.ts +52 -0
  116. package/lib/engine-login.ts +2933 -0
  117. package/lib/engines.ts +1342 -0
  118. package/lib/enrollment-state.ts +251 -0
  119. package/lib/env-file.ts +246 -0
  120. package/lib/env-schema.ts +891 -0
  121. package/lib/env.ts +195 -0
  122. package/lib/fs-aliases.ts +49 -0
  123. package/lib/git-clone-credentials.ts +334 -0
  124. package/lib/git-diff.ts +530 -0
  125. package/lib/git-identity.ts +715 -0
  126. package/lib/github-git-auth.ts +207 -0
  127. package/lib/github-origin.ts +28 -0
  128. package/lib/github-tokens.ts +2439 -0
  129. package/lib/host-cloud-url.ts +62 -0
  130. package/lib/host-config.ts +279 -0
  131. package/lib/host-env.ts +424 -0
  132. package/lib/host-logs.ts +965 -0
  133. package/lib/internal-connections.ts +5 -0
  134. package/lib/keyed-promise-tail.ts +23 -0
  135. package/lib/launch-mode.ts +25 -0
  136. package/lib/lease.ts +239 -0
  137. package/lib/legacy-host.ts +769 -0
  138. package/lib/legacy-runtime-retirement.ts +424 -0
  139. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  140. package/lib/machine-exec.ts +238 -0
  141. package/lib/machine-gateway-tunnel.ts +344 -0
  142. package/lib/machine-keys.ts +65 -0
  143. package/lib/machine-project-env-loader.ts +152 -0
  144. package/lib/machine-provider-aws.ts +923 -0
  145. package/lib/machine-provider-local.ts +338 -0
  146. package/lib/machine-provider.ts +90 -0
  147. package/lib/managed-activation-watcher.ts +72 -0
  148. package/lib/managed-install-owner-watcher.ts +55 -0
  149. package/lib/managed-operation-drain.ts +57 -0
  150. package/lib/managed-runtime.ts +4296 -0
  151. package/lib/managed-update-scheduler.ts +125 -0
  152. package/lib/mcp-capture.ts +865 -0
  153. package/lib/mcp-config-lock.ts +2 -0
  154. package/lib/mcp-connections.ts +959 -0
  155. package/lib/mcp-gateway.ts +2594 -0
  156. package/lib/obs.ts +512 -0
  157. package/lib/openrouter-budget.ts +247 -0
  158. package/lib/orchestrator.ts +11409 -0
  159. package/lib/person-capture.ts +327 -0
  160. package/lib/preview-ports.ts +85 -0
  161. package/lib/preview-sidecar.ts +331 -0
  162. package/lib/project-env-authority.ts +1319 -0
  163. package/lib/project-env-feature.ts +74 -0
  164. package/lib/project-env-legacy-tags.ts +130 -0
  165. package/lib/project-env-loader.ts +242 -0
  166. package/lib/project-env-replica.ts +2094 -0
  167. package/lib/project-env-schema-capture.ts +342 -0
  168. package/lib/project-env-staging.ts +148 -0
  169. package/lib/project-env-task-policy.ts +176 -0
  170. package/lib/project-env-task-replica.ts +1752 -0
  171. package/lib/release-feed.ts +61 -0
  172. package/lib/release-manifest.ts +710 -0
  173. package/lib/release-trust.ts +46 -0
  174. package/lib/repo-clone.ts +38 -0
  175. package/lib/routing/shadow.ts +261 -0
  176. package/lib/runner-boot.ts +10 -0
  177. package/lib/runtime-activation-tail.ts +279 -0
  178. package/lib/runtime-archive.ts +1216 -0
  179. package/lib/runtime-authority.ts +166 -0
  180. package/lib/runtime-guard.ts +36 -0
  181. package/lib/runtime-provider-state.ts +169 -0
  182. package/lib/runtime-reconnect.ts +86 -0
  183. package/lib/runtime-state.ts +1033 -0
  184. package/lib/safe-http.ts +364 -0
  185. package/lib/secrets-proxy/address.ts +95 -0
  186. package/lib/secrets-proxy/audit.ts +30 -0
  187. package/lib/secrets-proxy/ca-store.ts +200 -0
  188. package/lib/secrets-proxy/ca.ts +470 -0
  189. package/lib/secrets-proxy/container-steps.ts +138 -0
  190. package/lib/secrets-proxy/der.ts +273 -0
  191. package/lib/secrets-proxy/engine.ts +1633 -0
  192. package/lib/secrets-proxy/host.ts +93 -0
  193. package/lib/secrets-proxy/hostname.ts +57 -0
  194. package/lib/secrets-proxy/placeholders.ts +196 -0
  195. package/lib/secrets-proxy/policy.ts +150 -0
  196. package/lib/secrets-proxy/readiness.ts +148 -0
  197. package/lib/secrets-proxy/runtime.ts +582 -0
  198. package/lib/secrets-proxy/scrub.ts +559 -0
  199. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  200. package/lib/secrets-proxy/task-scripts.ts +262 -0
  201. package/lib/secrets-proxy/task-trust.ts +605 -0
  202. package/lib/secrets-proxy/trust-watch.ts +80 -0
  203. package/lib/secrets.ts +71 -0
  204. package/lib/shared-files-s3.ts +976 -0
  205. package/lib/shared-files.ts +156 -0
  206. package/lib/sidecar-assign.ts +49 -0
  207. package/lib/sidecar-guard.ts +58 -0
  208. package/lib/sidecar-resume.ts +178 -0
  209. package/lib/sidecar.ts +69 -0
  210. package/lib/signin-box-credentials.ts +465 -0
  211. package/lib/signin-box.ts +114 -0
  212. package/lib/skill-index-example.fixture.ts +56 -0
  213. package/lib/skill-layout.ts +417 -0
  214. package/lib/skills.ts +489 -0
  215. package/lib/ssh.ts +401 -0
  216. package/lib/standard-image-cleanup.ts +117 -0
  217. package/lib/standard-image-pin.ts +207 -0
  218. package/lib/standard-image.ts +1697 -0
  219. package/lib/stopped-task-status-queue.ts +120 -0
  220. package/lib/task-container-cli.ts +269 -0
  221. package/lib/task-credential-cleanup.ts +752 -0
  222. package/lib/task-diff-sandbox.ts +464 -0
  223. package/lib/task-diff.ts +91 -0
  224. package/lib/task-env-forwarder.ts +228 -0
  225. package/lib/task-env-source.ts +249 -0
  226. package/lib/task-environment/anchor.ts +31 -0
  227. package/lib/task-environment/apple-container.ts +860 -0
  228. package/lib/task-environment/docker.ts +1344 -0
  229. package/lib/task-environment/index.ts +466 -0
  230. package/lib/task-environment/legacy-adoption.ts +467 -0
  231. package/lib/task-environment/machine-environment.ts +61 -0
  232. package/lib/task-environment/machine-task-up.ts +1227 -0
  233. package/lib/task-environment/machine.ts +974 -0
  234. package/lib/task-environment/registry.ts +59 -0
  235. package/lib/task-environment/self-identity.ts +20 -0
  236. package/lib/task-environment/self.ts +566 -0
  237. package/lib/task-environment/types.ts +642 -0
  238. package/lib/task-environment/workspace-files.ts +685 -0
  239. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  240. package/lib/task-files-machine-retirement-outbox.ts +592 -0
  241. package/lib/task-files-machine-retirement.ts +116 -0
  242. package/lib/task-files-readiness.ts +127 -0
  243. package/lib/task-gateway-port.ts +22 -0
  244. package/lib/task-gateway-protocol.ts +190 -0
  245. package/lib/task-gateway-recovery.ts +47 -0
  246. package/lib/task-gateway-state.ts +512 -0
  247. package/lib/task-identity.ts +19 -0
  248. package/lib/task-inventory.ts +682 -0
  249. package/lib/task-machine-credentials-activation.ts +95 -0
  250. package/lib/task-machine-swap-safety.ts +729 -0
  251. package/lib/task-person-credentials.ts +76 -0
  252. package/lib/task-project-ops-host.ts +147 -0
  253. package/lib/task-project-ops.ts +1062 -0
  254. package/lib/task-secret-staging.ts +16 -0
  255. package/lib/task-status.ts +46 -0
  256. package/lib/task-token.ts +46 -0
  257. package/lib/token-broker-adopt.ts +344 -0
  258. package/lib/token-broker-lambda.ts +105 -0
  259. package/lib/token-broker-local.ts +392 -0
  260. package/lib/token-broker-revision.ts +310 -0
  261. package/lib/token-broker-ssm.ts +525 -0
  262. package/lib/token-broker.ts +218 -0
  263. package/lib/token-lanes/openai-oauth.ts +123 -0
  264. package/lib/token-lanes/registry.ts +45 -0
  265. package/lib/token-lanes/types.ts +163 -0
  266. package/lib/token-lanes/xai-oauth.ts +165 -0
  267. package/lib/transcript.ts +128 -0
  268. package/lib/tunnel-registry.ts +220 -0
  269. package/lib/tunnel-runtime.ts +235 -0
  270. package/lib/ulid.ts +16 -0
  271. package/lib/user-bus-env.ts +116 -0
  272. package/lib/user-credentials-store.ts +435 -0
  273. package/lib/vnode-pressure.ts +142 -0
  274. package/package.json +131 -4
  275. package/runner/runner.mjs +442 -0
  276. package/scripts/agent/_common.sh +633 -0
  277. package/scripts/agent/env-hook.sh +626 -0
  278. package/scripts/agent/moondeck-cli.sh +5 -0
  279. package/scripts/agent/task-down.sh +291 -0
  280. package/scripts/agent/task-status.sh +70 -0
  281. package/scripts/agent/task-up.sh +3761 -0
  282. package/scripts/agent/uai-env-exec.sh +26 -0
  283. package/scripts/install/darwin.ts +1048 -0
  284. package/scripts/install/linux.ts +958 -0
  285. package/scripts/install/types.ts +78 -0
  286. package/scripts/install/util.ts +254 -0
  287. package/scripts/install/win.ts +143 -0
  288. package/src/activation-respawn.ts +155 -0
  289. package/src/apple-tunnel-route.ts +104 -0
  290. package/src/capture-wire.ts +102 -0
  291. package/src/cli.ts +2583 -0
  292. package/src/cloud-credentials.ts +2144 -0
  293. package/src/cloud-origins.ts +23 -0
  294. package/src/credential-location.ts +248 -0
  295. package/src/credential-wire-validation.ts +25 -0
  296. package/src/custom-connections.ts +237 -0
  297. package/src/engine-account-paths.ts +11 -0
  298. package/src/engine-models.ts +39 -0
  299. package/src/engine-usage.ts +104 -0
  300. package/src/event-outbox.ts +185 -0
  301. package/src/index.ts +2899 -0
  302. package/src/load-env.ts +97 -0
  303. package/src/machine-tunnel-route.ts +156 -0
  304. package/src/main.ts +6349 -0
  305. package/src/never-proxied-domains.ts +31 -0
  306. package/src/outbound-frame.ts +31 -0
  307. package/src/paths.ts +81 -0
  308. package/src/project-env-latest.ts +130 -0
  309. package/src/project-env-policy.ts +396 -0
  310. package/src/project-env-wording.ts +53 -0
  311. package/src/protocol.ts +5545 -0
  312. package/src/runtime-bootstrap.ts +165 -0
  313. package/src/station-identity.ts +96 -0
  314. package/src/task-env-names.ts +28 -0
  315. package/src/task-secrets.ts +75 -0
  316. package/src/tunnel-dial.ts +102 -0
  317. package/src/ui/server.ts +852 -0
  318. package/src/ui/types.ts +173 -0
  319. package/src/workspace-path-policy.ts +41 -0
  320. package/ui/app.js +1102 -0
  321. package/ui/index.html +90 -0
  322. package/ui/style.css +685 -0
  323. package/ui/uai-favicon.svg +21 -0
  324. package/ui/uai-logo-black.svg +9 -0
  325. package/ui/uai-wheel.svg +9 -0
@@ -0,0 +1,562 @@
1
+ /**
2
+ * CursorSession — a real AgentSession backed by **Cursor Agent** (`cursor-agent`),
3
+ * run inside the task environment:
4
+ *
5
+ * CURSOR_API_KEY=… /home/node/.local/bin/cursor-agent -p "<prompt>" \
6
+ * --output-format stream-json --stream-partial-output --force --trust \
7
+ * --model <model> [--resume <sessionId>]
8
+ *
9
+ * One-shot per turn (like Kimi/Grok); sends serialized; continuity via
10
+ * `--resume <sessionId>` captured from the stream. Cursor's stream-json is
11
+ * essentially Claude Code's — the RICHEST of the extra engines (streaming text
12
+ * AND tool cards):
13
+ *
14
+ * {"type":"system","subtype":"init","session_id":…} → session id
15
+ * {"type":"thinking","subtype":"delta"|"completed",…} → skipped
16
+ * {"type":"assistant","message":{content:[{text}]},"timestamp_ms":…}
17
+ * → message_delta
18
+ * {"type":"tool_call","subtype":"started","call_id","tool_call":{…}}
19
+ * → tool_call card
20
+ * {"type":"result","subtype":"success"|"error","result",session_id}
21
+ * → message_complete + end
22
+ *
23
+ * AUTH is a Cursor API key (`CURSOR_API_KEY`) — env-injected at exec, like the
24
+ * Claude token (no config-dir copy). Cloud never sees it (ADR-015). `-p` runs
25
+ * headless with `--force` (auto-run tools; the container is the sandbox) and
26
+ * `--trust` (skip the workspace-trust prompt). Cursor has no system-prompt
27
+ * flag, so the briefing folds into the first turn's prompt.
28
+ */
29
+ import { stripVTControlCharacters } from "node:util";
30
+
31
+ import { ENGINE_ACCOUNT_ENV } from "../credential-consumers/engine-anchor";
32
+ import { requireContainerRuntimeOperational } from "../runtime-guard";
33
+ import type {
34
+ TaskEnvironmentAgentSessionSurface,
35
+ TaskEnvironmentSessionRequest,
36
+ } from "../task-environment/types";
37
+ import { newId } from "../ulid";
38
+ import { register } from "./registry";
39
+ import {
40
+ AGENT_SESSION_OUTPUT_BUFFER_BYTES,
41
+ spawnEnvironmentProcess,
42
+ type AgentEnvironmentProcess,
43
+ } from "./proc";
44
+ import {
45
+ CURSOR_PERIOD_PROBE_TIMEOUT_MS,
46
+ CURSOR_PERIOD_USAGE_LOG,
47
+ cursorAccessTokenFromAuthJson,
48
+ cursorPeriodProbeDue,
49
+ markCursorPeriodProbed,
50
+ readCursorAuthFromEnvironment,
51
+ readCursorPeriodUsage,
52
+ } from "./cursor-period-usage";
53
+ import { rateLimitErrorUsage } from "./rate-limit";
54
+ import { extractResultUsage } from "./usage";
55
+ import type {
56
+ AgentAccountUsage,
57
+ AgentEvent,
58
+ AgentEventHandler,
59
+ AgentKind,
60
+ AgentSession,
61
+ AgentUsage,
62
+ RosterAgent,
63
+ } from "./types";
64
+
65
+ const CURSOR_BIN = "/home/node/.local/bin/cursor-agent";
66
+
67
+ // Cursor replays its captured sandbox environment immediately before it
68
+ // evaluates each shell command. In loader tasks this restore hook is the last
69
+ // step in that replay, so the command sees the current project environment
70
+ // rather than Cursor's stale launch snapshot.
71
+ export const CURSOR_SANDBOX_ENV_RESTORE =
72
+ ". /usr/local/share/uai/env-hook.sh; __uai_env_sync";
73
+
74
+ // A curated fallback slice of Cursor's signed-in 2026.10.22 catalog. The
75
+ // newest version of each tier comes first; older known-good ids follow.
76
+ export const CURSOR_MODELS = [
77
+ "grok-4.7-high",
78
+ "grok-4.7-high-fast",
79
+ "composer-2.5",
80
+ "composer-2.5-fast",
81
+ "claude-opus-5-5-high",
82
+ "claude-fable-5-1-thinking-high",
83
+ "claude-sonnet-5-5-high",
84
+ "gpt-5.6-sol-high",
85
+ "gemini-4-argon-high",
86
+ "kimi-k3-high",
87
+ "auto",
88
+ "claude-opus-4-8-thinking-high",
89
+ "cursor-grok-4.5-high",
90
+ "claude-fable-5-thinking-high",
91
+ ];
92
+ const CURSOR_DEFAULT_MODEL = "grok-4.7-high";
93
+ const CURSOR_EFFORTS: string[] = []; // effort is baked into the model id
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Pure protocol mapping.
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function assistantText(msg: Record<string, unknown>): string {
100
+ const message = msg.message as { content?: unknown } | undefined;
101
+ const content = Array.isArray(message?.content) ? message!.content : [];
102
+ let out = "";
103
+ for (const raw of content) {
104
+ const block = raw as { type?: unknown; text?: unknown };
105
+ if (block.type === "text" && typeof block.text === "string") out += block.text;
106
+ }
107
+ return out;
108
+ }
109
+
110
+ /** "shellToolCall" → "shell", "readFileToolCall" → "readFile". */
111
+ function prettyTool(name: string): string {
112
+ return name.replace(/ToolCall$/, "") || "tool";
113
+ }
114
+
115
+ export interface MappedCursorLine {
116
+ textDelta?: string;
117
+ toolCall?: { id: string; title: string; detail: string };
118
+ end?: boolean;
119
+ finalText?: string;
120
+ sessionId?: string;
121
+ errorText?: string;
122
+ /** This model needs paid Cursor credits; the account itself is still usable. */
123
+ creditRefusal?: boolean;
124
+ /** Set when `errorText` is a usage cap. Secret-free; source `error-text`. */
125
+ accountUsage?: AgentAccountUsage;
126
+ /** Tokens from the result envelope. Claude-shaped lines carry cost;
127
+ * cursor-agent 2026.10.x carries camelCase counts and no cost. */
128
+ usage?: AgentUsage;
129
+ }
130
+
131
+ /** Cursor Premium can refuse one paid-credit model while the same account can
132
+ * still run Cursor's Grok and Composer models. This is not account exhaustion:
133
+ * keep it non-retryable so the orchestrator neither rotates nor cools the
134
+ * account, and give the person an actionable model-specific error. */
135
+ export function cursorCreditsRequiredMessage(message: string): string | null {
136
+ const match = stripVTControlCharacters(message).match(
137
+ /(?:ActionRequiredError:\s*)?([^\r\n]+?)\s+is not included in your weekly usage\b/i,
138
+ );
139
+ const model = match?.[1]?.trim();
140
+ return model
141
+ ? `${model} needs Cursor credits on this plan; use a Grok or Composer model`
142
+ : null;
143
+ }
144
+
145
+ export function mapCursorLine(line: string, now: number = Date.now()): MappedCursorLine {
146
+ const t = line.trim();
147
+ if (!t.startsWith("{")) return {};
148
+ let m: Record<string, unknown>;
149
+ try {
150
+ m = JSON.parse(t) as Record<string, unknown>;
151
+ } catch {
152
+ return {};
153
+ }
154
+
155
+ if (m.type === "system") {
156
+ return typeof m.session_id === "string" ? { sessionId: m.session_id } : {};
157
+ }
158
+ if (m.type === "assistant") {
159
+ // Streaming partials carry timestamp_ms; the final full message (finalized
160
+ // by `result`) does not — only stream the partials to avoid doubling.
161
+ if (typeof m.timestamp_ms === "number") {
162
+ const text = assistantText(m);
163
+ if (text) return { textDelta: text };
164
+ }
165
+ return {};
166
+ }
167
+ if (m.type === "tool_call" && m.subtype === "started") {
168
+ const tc = (m.tool_call ?? {}) as Record<string, unknown>;
169
+ const [name, spec] = Object.entries(tc)[0] ?? ["tool", {}];
170
+ const args = ((spec as Record<string, unknown>)?.args ?? {}) as Record<string, unknown>;
171
+ const pick = args.command ?? args.path ?? args.filePath ?? args.file_path ?? args.query;
172
+ return {
173
+ toolCall: {
174
+ id: typeof m.call_id === "string" ? m.call_id : newId(),
175
+ title: prettyTool(name),
176
+ detail: typeof pick === "string" ? pick : "",
177
+ },
178
+ };
179
+ }
180
+ if (m.type === "result") {
181
+ const errored = m.is_error === true;
182
+ const rawErrorText = errored
183
+ ? typeof m.result === "string"
184
+ ? m.result
185
+ : "cursor turn failed"
186
+ : undefined;
187
+ const creditError = rawErrorText
188
+ ? cursorCreditsRequiredMessage(rawErrorText)
189
+ : null;
190
+ const errorText = creditError ?? rawErrorText;
191
+ const accountUsage = errorText ? rateLimitErrorUsage(errorText, now) : null;
192
+ return {
193
+ end: true,
194
+ sessionId: typeof m.session_id === "string" ? m.session_id : undefined,
195
+ finalText: typeof m.result === "string" ? m.result : undefined,
196
+ errorText,
197
+ ...(creditError ? { creditRefusal: true } : {}),
198
+ ...(accountUsage ? { accountUsage } : {}),
199
+ usage: extractResultUsage(m),
200
+ };
201
+ }
202
+ // thinking / user / other → nothing.
203
+ return {};
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Session
208
+ // ---------------------------------------------------------------------------
209
+
210
+ export type Spawner = (
211
+ request: TaskEnvironmentSessionRequest,
212
+ ) => AgentEnvironmentProcess;
213
+
214
+ export class CursorSession implements AgentSession {
215
+ readonly agentId: string;
216
+ readonly kind: AgentKind = "cursor";
217
+
218
+ private readonly environment: TaskEnvironmentAgentSessionSurface;
219
+ private readonly model?: string;
220
+ private readonly systemPreamble: string;
221
+ private readonly agentEnv: Record<string, string>;
222
+ private readonly spawner: Spawner;
223
+ private readonly apiKey: string;
224
+ private readonly readAuthFile: (signal: AbortSignal) => Promise<string | null>;
225
+ private readonly fetchImpl: typeof fetch;
226
+ private readonly probeTimeoutMs: number;
227
+
228
+ private readonly handlers = new Set<AgentEventHandler>();
229
+ private sessionId: string | null = null;
230
+ private current: AgentEnvironmentProcess | null = null;
231
+ private queue: Promise<void> = Promise.resolve();
232
+ private sentPreamble = false;
233
+ private closed = false;
234
+
235
+ constructor(args: {
236
+ taskId: string;
237
+ agent: RosterAgent;
238
+ environment: TaskEnvironmentAgentSessionSurface;
239
+ systemPreamble: string;
240
+ agentEnv?: Record<string, string>;
241
+ spawner?: Spawner;
242
+ apiKey?: string;
243
+ /** Reads the CLI's auth.json from the task environment. Tests inject this.
244
+ * The signal aborts when the probe deadline passes; the default reader
245
+ * kills the auth process. */
246
+ readAuthFile?: (signal: AbortSignal) => Promise<string | null>;
247
+ /** Dashboard fetch. Tests inject this. Production uses the host's fetch. */
248
+ fetchImpl?: typeof fetch;
249
+ /** Whole-probe bound, including the auth.json read. Tests shrink it. */
250
+ probeTimeoutMs?: number;
251
+ }) {
252
+ this.agentId = args.agent.id;
253
+ this.environment = args.environment;
254
+ this.model = args.agent.model;
255
+ this.systemPreamble = args.systemPreamble;
256
+ this.agentEnv = args.agentEnv ?? {};
257
+ this.spawner =
258
+ args.spawner ?? ((request) => spawnEnvironmentProcess(this.environment, request));
259
+ // An account-bound key (ADR-076/ADR-131 D3, injected through agentEnv)
260
+ // beats the host's own .env.local key — a workspace's Cursor account must
261
+ // never fall through to the operator's, and on a shared host the ambient
262
+ // key is usually absent anyway.
263
+ this.apiKey =
264
+ args.apiKey ?? this.agentEnv.CURSOR_API_KEY ?? process.env.CURSOR_API_KEY ?? "";
265
+ this.readAuthFile =
266
+ args.readAuthFile ?? ((signal) => readCursorAuthFromEnvironment(this.environment, signal));
267
+ this.fetchImpl = args.fetchImpl ?? fetch;
268
+ this.probeTimeoutMs = args.probeTimeoutMs ?? CURSOR_PERIOD_PROBE_TIMEOUT_MS;
269
+ }
270
+
271
+ /**
272
+ * One dashboard probe for a leased account. The read runs after the CLI
273
+ * child has exited: the CLI rewrites ~/.config/cursor/auth.json at
274
+ * startup, and a probe overlapped with the turn reads the expired access
275
+ * token. A missing lease, a probe still inside the five-minute gate, or
276
+ * any failure sends nothing. The token never leaves this method except
277
+ * as the bearer of the two dashboard calls.
278
+ *
279
+ * The credential is marked before the first await. Two sessions that start
280
+ * together must not both pass the gate. The returned function is the read;
281
+ * call it once the child has exited, still before turn_complete.
282
+ */
283
+ private probePeriodUsage(): () => Promise<void> {
284
+ const skip = (): Promise<void> => Promise.resolve();
285
+ const credentialId = this.agentEnv[ENGINE_ACCOUNT_ENV];
286
+ if (!credentialId || this.closed) return skip;
287
+ const now = Date.now();
288
+ if (!cursorPeriodProbeDue(credentialId, now)) return skip;
289
+ markCursorPeriodProbed(credentialId, now);
290
+ return () => this.readPeriodUsage();
291
+ }
292
+
293
+ /** Auth read and both dashboard calls share one deadline. On expiry the
294
+ * auth reader is aborted (the default reader kills its process) and this
295
+ * promise settles so turn_complete is not held. */
296
+ private readPeriodUsage(): Promise<void> {
297
+ const controller = new AbortController();
298
+ const timer = setTimeout(() => controller.abort(), this.probeTimeoutMs);
299
+ let finished = false;
300
+ const usagePromise = this.collectPeriodUsage(controller.signal);
301
+ return new Promise((resolve) => {
302
+ const finish = (usage: AgentAccountUsage | null): void => {
303
+ if (finished) return;
304
+ finished = true;
305
+ clearTimeout(timer);
306
+ if (!this.closed) {
307
+ if (!usage) console.warn(CURSOR_PERIOD_USAGE_LOG);
308
+ else this.emit({ type: "account_usage", usage });
309
+ }
310
+ resolve();
311
+ };
312
+ controller.signal.addEventListener("abort", () => finish(null));
313
+ void usagePromise.then(
314
+ (usage) => finish(controller.signal.aborted ? null : usage),
315
+ () => finish(null),
316
+ );
317
+ });
318
+ }
319
+
320
+ private async collectPeriodUsage(signal: AbortSignal): Promise<AgentAccountUsage | null> {
321
+ if (signal.aborted) return null;
322
+ const raw = await this.readAuthFile(signal);
323
+ if (signal.aborted) return null;
324
+ const token = raw ? cursorAccessTokenFromAuthJson(raw) : null;
325
+ if (!token) return null;
326
+ return readCursorPeriodUsage({
327
+ token,
328
+ fetchImpl: this.fetchImpl,
329
+ signal,
330
+ });
331
+ }
332
+
333
+ onEvent(handler: AgentEventHandler): () => void {
334
+ this.handlers.add(handler);
335
+ return () => this.handlers.delete(handler);
336
+ }
337
+
338
+ private emit(event: AgentEvent): void {
339
+ if (this.closed && event.type !== "exit") return;
340
+ for (const h of this.handlers) h(event);
341
+ }
342
+
343
+ async send(text: string): Promise<void> {
344
+ if (this.closed) return;
345
+ const turn = this.queue.catch(() => {}).then(() => this.runTurn(text));
346
+ this.queue = turn;
347
+ return turn;
348
+ }
349
+
350
+ private buildRequest(prompt: string): TaskEnvironmentSessionRequest {
351
+ const argv = [
352
+ CURSOR_BIN,
353
+ "--disable-auto-update",
354
+ "-p",
355
+ prompt,
356
+ "--output-format",
357
+ "stream-json",
358
+ "--stream-partial-output",
359
+ "--force", // container is the sandbox — auto-run tools
360
+ "--trust", // skip the workspace-trust prompt in headless
361
+ "--approve-mcps", // load the gateway MCP servers from ~/.cursor/mcp.json (ADR-057)
362
+ ];
363
+ // Long form only: the pinned CLI (2026.08.25) and its successors have no
364
+ // `-m` short flag and exit on it before the turn starts.
365
+ if (this.model && this.model !== "auto") argv.push("--model", this.model);
366
+ if (this.sessionId) argv.push("--resume", this.sessionId);
367
+ return {
368
+ argv: argv as [string, ...string[]],
369
+ projectEnvProfile: "task",
370
+ user: "node",
371
+ env: {
372
+ ...this.agentEnv,
373
+ CURSOR_API_KEY: this.apiKey,
374
+ ...(this.environment.descriptor.envMode === "loader"
375
+ ? { __CURSOR_SANDBOX_ENV_RESTORE: CURSOR_SANDBOX_ENV_RESTORE }
376
+ : {}),
377
+ },
378
+ maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
379
+ };
380
+ }
381
+
382
+ private async runTurn(text: string): Promise<void> {
383
+ if (this.closed) return;
384
+ requireContainerRuntimeOperational();
385
+ // No system-prompt flag — fold the briefing into the first turn; later
386
+ // turns carry it via the resumed session.
387
+ const prompt =
388
+ !this.sentPreamble && this.systemPreamble.trim().length > 0
389
+ ? `${this.systemPreamble}\n\n---\n\n${text}`
390
+ : text;
391
+ this.sentPreamble = true;
392
+
393
+ const child = this.spawner(this.buildRequest(prompt));
394
+ this.current = child;
395
+ // Mark the gate now, before the child-exit await. The read itself waits
396
+ // until the CLI has rewritten auth.json.
397
+ const periodUsage = this.probePeriodUsage();
398
+
399
+ let acc = "";
400
+ let sawText = false;
401
+ let buf = "";
402
+ // Usage rides the result line but turn_complete fires on process exit;
403
+ // capture it here and attach it below (ADR-071 metering).
404
+ let turnUsage: AgentUsage | undefined;
405
+ // A result line that already explained the failure owns the error. The
406
+ // exit path must not add a second "cursor exited N" for the same turn.
407
+ // A usage cap is tracked on its own: a generic JSON error must not hide
408
+ // a cap that only shows up on stderr.
409
+ let sawError = false;
410
+ let reportedRateLimit = false;
411
+ let reportedCreditRefusal = false;
412
+ const consume = (chunk: string): void => {
413
+ buf += chunk;
414
+ let nl: number;
415
+ while ((nl = buf.indexOf("\n")) >= 0) {
416
+ const line = buf.slice(0, nl);
417
+ buf = buf.slice(nl + 1);
418
+ const m = mapCursorLine(line);
419
+ if (m.sessionId) this.sessionId = m.sessionId;
420
+ if (typeof m.textDelta === "string") {
421
+ // Cursor (--stream-partial-output) streams incremental token deltas
422
+ // and then a REDUNDANT full-message snapshot. The timestamp_ms
423
+ // heuristic in mapCursorLine drops that final snapshot for models
424
+ // whose final line is untagged — but Cursor's multi-model support
425
+ // (ADR-068) means SOME models tag the final full snapshot with
426
+ // timestamp_ms too, so it slips through and gets appended on top of
427
+ // the tokens: every message doubled (live 2026-07-21). Reconcile
428
+ // against what we've already streamed instead of trusting the tag —
429
+ // robust to incremental tokens, cumulative snapshots, and the
430
+ // redundant final repeat alike.
431
+ const t = m.textDelta;
432
+ let delta: string | null;
433
+ if (t === acc) {
434
+ delta = null; // full-message snapshot we've already emitted
435
+ } else if (acc.length > 0 && t.startsWith(acc)) {
436
+ delta = t.slice(acc.length); // cumulative snapshot → growth only
437
+ acc = t;
438
+ } else {
439
+ delta = t; // incremental token / genuinely new text
440
+ acc += t;
441
+ }
442
+ if (delta) {
443
+ sawText = true;
444
+ this.emit({ type: "message_delta", text: delta });
445
+ }
446
+ }
447
+ if (m.toolCall) {
448
+ this.emit({
449
+ type: "tool_call",
450
+ id: m.toolCall.id,
451
+ title: m.toolCall.title,
452
+ detail: m.toolCall.detail,
453
+ });
454
+ }
455
+ if (m.end) {
456
+ if (m.usage) turnUsage = m.usage;
457
+ if (m.errorText) {
458
+ sawError = true;
459
+ if (m.creditRefusal) reportedCreditRefusal = true;
460
+ if (m.accountUsage) {
461
+ reportedRateLimit = true;
462
+ this.emit({ type: "account_usage", usage: m.accountUsage });
463
+ this.emit({
464
+ type: "error",
465
+ message: m.errorText,
466
+ retryable: "rate_limit",
467
+ ...(m.accountUsage.resetsAt === undefined
468
+ ? {}
469
+ : { resetsAt: m.accountUsage.resetsAt }),
470
+ });
471
+ } else {
472
+ this.emit({ type: "error", message: m.errorText });
473
+ }
474
+ }
475
+ const finalText = m.finalText ?? acc;
476
+ if (sawText || finalText) {
477
+ this.emit({ type: "message_complete", text: finalText });
478
+ }
479
+ }
480
+ }
481
+ };
482
+ child.stdout?.on("data", (b: Buffer) => consume(b.toString("utf8")));
483
+ let stderr = "";
484
+ child.stderr?.on("data", (b: Buffer) => {
485
+ stderr += b.toString("utf8");
486
+ });
487
+
488
+ await new Promise<void>((resolve) => {
489
+ const finish = (code: number | null, spawnErr?: string): void => {
490
+ this.current = null;
491
+ if (buf.trim().length > 0) consume("\n");
492
+ if (this.closed) return resolve();
493
+ if (spawnErr) {
494
+ this.emit({ type: "error", message: `cursor spawn failed: ${spawnErr}` });
495
+ } else if (code !== 0) {
496
+ // Some cursor-agent errors (e.g. an invalid key) are coloured; strip
497
+ // the escape codes so the cap check and the error a human sees are
498
+ // plain text. A generic result error must not skip this check.
499
+ const plain = stripVTControlCharacters(stderr);
500
+ const tail = plain.trim().slice(-500);
501
+ const creditRefusal = cursorCreditsRequiredMessage(plain);
502
+ const message =
503
+ creditRefusal ??
504
+ `cursor exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`;
505
+ const accountUsage = reportedRateLimit ? undefined : rateLimitErrorUsage(plain);
506
+ if (accountUsage) {
507
+ this.emit({ type: "account_usage", usage: accountUsage });
508
+ }
509
+ if (!sawError || (creditRefusal && !reportedCreditRefusal)) {
510
+ if (accountUsage) {
511
+ this.emit({
512
+ type: "error",
513
+ message,
514
+ retryable: "rate_limit",
515
+ ...(accountUsage.resetsAt === undefined
516
+ ? {}
517
+ : { resetsAt: accountUsage.resetsAt }),
518
+ });
519
+ } else {
520
+ this.emit({ type: "error", message });
521
+ }
522
+ }
523
+ }
524
+ resolve();
525
+ };
526
+ child.on("exit", (code) => finish(code));
527
+ child.on("error", (err) => finish(null, err.message));
528
+ });
529
+ await periodUsage();
530
+ if (!this.closed) this.emit({ type: "turn_complete", usage: turnUsage });
531
+ }
532
+
533
+ async interrupt(): Promise<void> {
534
+ this.current?.kill("SIGKILL");
535
+ }
536
+
537
+ async resolvePermission(): Promise<void> {
538
+ /* --force auto-approves; no permission requests are emitted */
539
+ }
540
+
541
+ async close(): Promise<void> {
542
+ if (this.closed) return;
543
+ this.closed = true;
544
+ this.current?.kill("SIGKILL");
545
+ this.current = null;
546
+ this.emit({ type: "exit", code: 0 });
547
+ this.handlers.clear();
548
+ }
549
+ }
550
+
551
+ register({
552
+ kind: "cursor",
553
+ label: "Cursor",
554
+ supportedModels: () => [...CURSOR_MODELS],
555
+ defaultModel: CURSOR_DEFAULT_MODEL,
556
+ supportedEfforts: () => [...CURSOR_EFFORTS],
557
+ // Gated on a Cursor API key in the host env (loaded from .env.local); the
558
+ // adapter injects it into the container. No key → engine not advertised.
559
+ available: () => Boolean(process.env.CURSOR_API_KEY),
560
+ create: async ({ taskId, agent, environment, systemPreamble, agentEnv }) =>
561
+ new CursorSession({ taskId, agent, environment, systemPreamble, agentEnv }),
562
+ });
@@ -0,0 +1,78 @@
1
+ /** Shared typed-dispatch contract for adapters that expose ADR-083's action. */
2
+
3
+ import {
4
+ MAX_AGENT_ID_CHARS,
5
+ MAX_SECRETARY_DISPATCH_INSTRUCTION_CHARS,
6
+ MAX_SECRETARY_DISPATCH_RECIPIENTS,
7
+ } from "../../src/protocol";
8
+
9
+ export const DISPATCH_TOOL_NAME = "dispatch";
10
+ export const MAX_DISPATCH_RECIPIENTS = MAX_SECRETARY_DISPATCH_RECIPIENTS;
11
+ export const MAX_DISPATCH_RECIPIENT_ID_CHARS =
12
+ MAX_AGENT_ID_CHARS;
13
+ export const MAX_DISPATCH_INSTRUCTION_CHARS =
14
+ MAX_SECRETARY_DISPATCH_INSTRUCTION_CHARS;
15
+ export const DISPATCH_RECIPIENT_ID_PATTERN = "^[A-Za-z0-9_-]+$";
16
+
17
+ export const DISPATCH_INPUT_SCHEMA = {
18
+ type: "object",
19
+ properties: {
20
+ recipients: {
21
+ type: "array",
22
+ minItems: 1,
23
+ maxItems: MAX_DISPATCH_RECIPIENTS,
24
+ uniqueItems: true,
25
+ items: {
26
+ type: "string",
27
+ minLength: 1,
28
+ maxLength: MAX_DISPATCH_RECIPIENT_ID_CHARS,
29
+ pattern: DISPATCH_RECIPIENT_ID_PATTERN,
30
+ },
31
+ description:
32
+ "Exact @ids of the crew agents that should receive the instruction, without the @ prefix.",
33
+ },
34
+ instruction: {
35
+ type: "string",
36
+ minLength: 1,
37
+ maxLength: MAX_DISPATCH_INSTRUCTION_CHARS,
38
+ description:
39
+ "Self-contained backstage instruction. Include all context the recipients need; do not include prose meant only for the human.",
40
+ },
41
+ },
42
+ required: ["recipients", "instruction"],
43
+ additionalProperties: false,
44
+ } as const;
45
+
46
+ export interface DispatchInput {
47
+ recipients: string[];
48
+ instruction: string;
49
+ }
50
+
51
+ const recipientId = new RegExp(DISPATCH_RECIPIENT_ID_PATTERN);
52
+
53
+ export function parseDispatchInput(value: unknown): DispatchInput | null {
54
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
55
+ return null;
56
+ }
57
+ const record = value as Record<string, unknown>;
58
+ const recipients = record.recipients;
59
+ const instruction = record.instruction;
60
+ if (
61
+ !Array.isArray(recipients) ||
62
+ recipients.length === 0 ||
63
+ recipients.length > MAX_DISPATCH_RECIPIENTS ||
64
+ !recipients.every(
65
+ (recipient): recipient is string =>
66
+ typeof recipient === "string" &&
67
+ recipient.length <= MAX_DISPATCH_RECIPIENT_ID_CHARS &&
68
+ recipientId.test(recipient),
69
+ ) ||
70
+ new Set(recipients).size !== recipients.length ||
71
+ typeof instruction !== "string" ||
72
+ instruction.trim().length === 0 ||
73
+ instruction.length > MAX_DISPATCH_INSTRUCTION_CHARS
74
+ ) {
75
+ return null;
76
+ }
77
+ return { recipients, instruction };
78
+ }