@moondeck/station 0.0.1 → 1.0.1

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 (330) 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/0031_first_turn_receipts.sql +9 -0
  36. package/db/migrations/meta/_journal.json +230 -0
  37. package/db/schema.ts +871 -0
  38. package/images/standard/Dockerfile +347 -0
  39. package/images/standard/README.md +167 -0
  40. package/images/standard/container/code-server-settings.json +63 -0
  41. package/images/standard/container/corepack-version +1 -0
  42. package/images/standard/container/uai-init +704 -0
  43. package/images/standard/container/uai-materialize-runtimes +1527 -0
  44. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  45. package/images/standard/tool-versions +2 -0
  46. package/lib/__fixtures__/skill-index/SKILL.md +10 -0
  47. package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
  48. package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
  49. package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
  50. package/lib/agent-cli.ts +1507 -0
  51. package/lib/agent-errors.ts +22 -0
  52. package/lib/agent.ts +672 -0
  53. package/lib/agents/attach-compatibility.ts +51 -0
  54. package/lib/agents/claude.ts +918 -0
  55. package/lib/agents/codex-token-usage.fixture.json +130 -0
  56. package/lib/agents/codex.ts +1392 -0
  57. package/lib/agents/cursor-period-usage.ts +253 -0
  58. package/lib/agents/cursor.ts +562 -0
  59. package/lib/agents/dispatch.ts +78 -0
  60. package/lib/agents/durable-proc.ts +334 -0
  61. package/lib/agents/factory.ts +57 -0
  62. package/lib/agents/grok.ts +381 -0
  63. package/lib/agents/kimi.ts +329 -0
  64. package/lib/agents/machine-durable.ts +604 -0
  65. package/lib/agents/mock.ts +133 -0
  66. package/lib/agents/mode.ts +64 -0
  67. package/lib/agents/opencode.ts +508 -0
  68. package/lib/agents/output-source.ts +3 -0
  69. package/lib/agents/proc.ts +221 -0
  70. package/lib/agents/rate-limit.ts +337 -0
  71. package/lib/agents/registry.ts +182 -0
  72. package/lib/agents/session-state.ts +197 -0
  73. package/lib/agents/skill-index-preamble.fixture.ts +26 -0
  74. package/lib/agents/transport.ts +741 -0
  75. package/lib/agents/types.ts +254 -0
  76. package/lib/agents/uai.ts +106 -0
  77. package/lib/agents/usage.ts +44 -0
  78. package/lib/apple-one-off.ts +277 -0
  79. package/lib/apple-runtime-recycle.ts +237 -0
  80. package/lib/apple-uninstall-teardown.ts +224 -0
  81. package/lib/assignment-evidence.ts +94 -0
  82. package/lib/attachments.ts +48 -0
  83. package/lib/browser-testing.ts +1537 -0
  84. package/lib/capture-flow.ts +332 -0
  85. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  86. package/lib/cloud-credential-host.ts +512 -0
  87. package/lib/cloud-credential-leases.ts +2587 -0
  88. package/lib/cloud-credential-runtime.ts +1357 -0
  89. package/lib/cloud-state.ts +56 -0
  90. package/lib/codex-auth.ts +456 -0
  91. package/lib/command-db.ts +310 -0
  92. package/lib/compose-network-recovery.ts +283 -0
  93. package/lib/connection-secret-redaction.ts +25 -0
  94. package/lib/container-runtime.ts +1470 -0
  95. package/lib/credential-cleanup-inventory.ts +490 -0
  96. package/lib/credential-consumers/engine-anchor.ts +1880 -0
  97. package/lib/credential-consumers/engines.ts +797 -0
  98. package/lib/credential-consumers/files.ts +934 -0
  99. package/lib/credential-consumers/github.ts +702 -0
  100. package/lib/credential-consumers/mcp.ts +389 -0
  101. package/lib/credential-consumers/task-files.ts +127 -0
  102. package/lib/credential-consumers/task-secrets.ts +10 -0
  103. package/lib/credential-store-pickup.ts +149 -0
  104. package/lib/credential-store-scope.ts +112 -0
  105. package/lib/credential-store.ts +932 -0
  106. package/lib/credentials-activation-authority.ts +709 -0
  107. package/lib/credentials-purge.ts +1006 -0
  108. package/lib/custom-connection-capture.ts +395 -0
  109. package/lib/custom-connection-launch.ts +384 -0
  110. package/lib/custom-connection-runtime.ts +141 -0
  111. package/lib/db.ts +80 -0
  112. package/lib/disk-pressure.ts +42 -0
  113. package/lib/docker-exec.ts +156 -0
  114. package/lib/engine-accounts.ts +1970 -0
  115. package/lib/engine-connect.ts +321 -0
  116. package/lib/engine-credential-env.ts +52 -0
  117. package/lib/engine-login.ts +2933 -0
  118. package/lib/engines.ts +1342 -0
  119. package/lib/enrollment-state.ts +251 -0
  120. package/lib/env-file.ts +246 -0
  121. package/lib/env-schema.ts +937 -0
  122. package/lib/env.ts +195 -0
  123. package/lib/first-turn-receipts.ts +62 -0
  124. package/lib/fs-aliases.ts +49 -0
  125. package/lib/git-clone-credentials.ts +334 -0
  126. package/lib/git-diff.ts +530 -0
  127. package/lib/git-identity.ts +715 -0
  128. package/lib/github-git-auth.ts +207 -0
  129. package/lib/github-origin.ts +28 -0
  130. package/lib/github-tokens.ts +2439 -0
  131. package/lib/host-cloud-url.ts +62 -0
  132. package/lib/host-config.ts +279 -0
  133. package/lib/host-env.ts +424 -0
  134. package/lib/host-logs.ts +965 -0
  135. package/lib/internal-connections.ts +5 -0
  136. package/lib/keyed-promise-tail.ts +23 -0
  137. package/lib/launch-mode.ts +25 -0
  138. package/lib/lease.ts +239 -0
  139. package/lib/legacy-host.ts +769 -0
  140. package/lib/legacy-runtime-retirement.ts +424 -0
  141. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  142. package/lib/machine-exec.ts +238 -0
  143. package/lib/machine-gateway-tunnel.ts +344 -0
  144. package/lib/machine-keys.ts +65 -0
  145. package/lib/machine-project-env-loader.ts +152 -0
  146. package/lib/machine-provider-aws.ts +923 -0
  147. package/lib/machine-provider-local.ts +338 -0
  148. package/lib/machine-provider.ts +90 -0
  149. package/lib/managed-activation-watcher.ts +72 -0
  150. package/lib/managed-install-owner-watcher.ts +55 -0
  151. package/lib/managed-operation-drain.ts +57 -0
  152. package/lib/managed-runtime.ts +4296 -0
  153. package/lib/managed-update-scheduler.ts +125 -0
  154. package/lib/mcp-capture.ts +865 -0
  155. package/lib/mcp-config-lock.ts +2 -0
  156. package/lib/mcp-connections.ts +959 -0
  157. package/lib/mcp-gateway.ts +2594 -0
  158. package/lib/obs.ts +512 -0
  159. package/lib/openrouter-budget.ts +247 -0
  160. package/lib/orchestrator.ts +11409 -0
  161. package/lib/person-capture.ts +327 -0
  162. package/lib/preview-ports.ts +85 -0
  163. package/lib/preview-sidecar.ts +331 -0
  164. package/lib/project-env-authority.ts +1319 -0
  165. package/lib/project-env-feature.ts +74 -0
  166. package/lib/project-env-legacy-tags.ts +130 -0
  167. package/lib/project-env-loader.ts +242 -0
  168. package/lib/project-env-replica.ts +2094 -0
  169. package/lib/project-env-schema-capture.ts +342 -0
  170. package/lib/project-env-staging.ts +148 -0
  171. package/lib/project-env-task-policy.ts +176 -0
  172. package/lib/project-env-task-replica.ts +1752 -0
  173. package/lib/release-feed.ts +61 -0
  174. package/lib/release-manifest.ts +710 -0
  175. package/lib/release-trust.ts +46 -0
  176. package/lib/repo-clone.ts +38 -0
  177. package/lib/routing/shadow.ts +261 -0
  178. package/lib/runner-boot.ts +10 -0
  179. package/lib/runtime-activation-tail.ts +279 -0
  180. package/lib/runtime-archive.ts +1216 -0
  181. package/lib/runtime-authority.ts +166 -0
  182. package/lib/runtime-guard.ts +36 -0
  183. package/lib/runtime-provider-state.ts +169 -0
  184. package/lib/runtime-reconnect.ts +86 -0
  185. package/lib/runtime-state.ts +1033 -0
  186. package/lib/safe-http.ts +364 -0
  187. package/lib/secrets-proxy/address.ts +95 -0
  188. package/lib/secrets-proxy/audit.ts +30 -0
  189. package/lib/secrets-proxy/ca-store.ts +200 -0
  190. package/lib/secrets-proxy/ca.ts +470 -0
  191. package/lib/secrets-proxy/container-steps.ts +138 -0
  192. package/lib/secrets-proxy/der.ts +273 -0
  193. package/lib/secrets-proxy/engine.ts +1633 -0
  194. package/lib/secrets-proxy/host.ts +93 -0
  195. package/lib/secrets-proxy/hostname.ts +57 -0
  196. package/lib/secrets-proxy/placeholders.ts +196 -0
  197. package/lib/secrets-proxy/policy.ts +150 -0
  198. package/lib/secrets-proxy/readiness.ts +148 -0
  199. package/lib/secrets-proxy/runtime.ts +582 -0
  200. package/lib/secrets-proxy/scrub.ts +559 -0
  201. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  202. package/lib/secrets-proxy/task-scripts.ts +262 -0
  203. package/lib/secrets-proxy/task-trust.ts +605 -0
  204. package/lib/secrets-proxy/trust-watch.ts +80 -0
  205. package/lib/secrets.ts +71 -0
  206. package/lib/shared-files-s3.ts +976 -0
  207. package/lib/shared-files.ts +156 -0
  208. package/lib/sidecar-assign.ts +49 -0
  209. package/lib/sidecar-guard.ts +58 -0
  210. package/lib/sidecar-resume.ts +178 -0
  211. package/lib/sidecar.ts +69 -0
  212. package/lib/signin-box-credentials.ts +465 -0
  213. package/lib/signin-box.ts +114 -0
  214. package/lib/skill-index-example.fixture.ts +56 -0
  215. package/lib/skill-layout.ts +417 -0
  216. package/lib/skills.ts +489 -0
  217. package/lib/ssh.ts +401 -0
  218. package/lib/standard-image-cleanup.ts +117 -0
  219. package/lib/standard-image-pin.ts +207 -0
  220. package/lib/standard-image.ts +1697 -0
  221. package/lib/stopped-task-status-queue.ts +120 -0
  222. package/lib/task-container-cli.ts +269 -0
  223. package/lib/task-credential-cleanup.ts +752 -0
  224. package/lib/task-diff-sandbox.ts +464 -0
  225. package/lib/task-diff.ts +91 -0
  226. package/lib/task-env-forwarder.ts +228 -0
  227. package/lib/task-env-source.ts +249 -0
  228. package/lib/task-environment/anchor.ts +31 -0
  229. package/lib/task-environment/apple-container.ts +860 -0
  230. package/lib/task-environment/docker.ts +1344 -0
  231. package/lib/task-environment/index.ts +466 -0
  232. package/lib/task-environment/legacy-adoption.ts +467 -0
  233. package/lib/task-environment/machine-environment.ts +61 -0
  234. package/lib/task-environment/machine-task-up.ts +1227 -0
  235. package/lib/task-environment/machine.ts +974 -0
  236. package/lib/task-environment/registry.ts +59 -0
  237. package/lib/task-environment/self-identity.ts +20 -0
  238. package/lib/task-environment/self.ts +566 -0
  239. package/lib/task-environment/types.ts +642 -0
  240. package/lib/task-environment/workspace-files.ts +685 -0
  241. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  242. package/lib/task-files-machine-retirement-outbox.ts +592 -0
  243. package/lib/task-files-machine-retirement.ts +116 -0
  244. package/lib/task-files-readiness.ts +127 -0
  245. package/lib/task-gateway-port.ts +22 -0
  246. package/lib/task-gateway-protocol.ts +190 -0
  247. package/lib/task-gateway-recovery.ts +47 -0
  248. package/lib/task-gateway-state.ts +512 -0
  249. package/lib/task-identity.ts +19 -0
  250. package/lib/task-inventory.ts +682 -0
  251. package/lib/task-machine-credentials-activation.ts +95 -0
  252. package/lib/task-machine-swap-safety.ts +729 -0
  253. package/lib/task-person-credentials.ts +76 -0
  254. package/lib/task-project-ops-host.ts +147 -0
  255. package/lib/task-project-ops.ts +1062 -0
  256. package/lib/task-secret-staging.ts +16 -0
  257. package/lib/task-status.ts +46 -0
  258. package/lib/task-token.ts +46 -0
  259. package/lib/token-broker-adopt.ts +344 -0
  260. package/lib/token-broker-lambda.ts +105 -0
  261. package/lib/token-broker-local.ts +392 -0
  262. package/lib/token-broker-revision.ts +310 -0
  263. package/lib/token-broker-ssm.ts +525 -0
  264. package/lib/token-broker.ts +218 -0
  265. package/lib/token-lanes/openai-oauth.ts +123 -0
  266. package/lib/token-lanes/registry.ts +45 -0
  267. package/lib/token-lanes/types.ts +163 -0
  268. package/lib/token-lanes/xai-oauth.ts +165 -0
  269. package/lib/transcript.ts +128 -0
  270. package/lib/tunnel-registry.ts +220 -0
  271. package/lib/tunnel-runtime.ts +235 -0
  272. package/lib/ulid.ts +16 -0
  273. package/lib/user-bus-env.ts +116 -0
  274. package/lib/user-credentials-store.ts +435 -0
  275. package/lib/vnode-pressure.ts +142 -0
  276. package/package.json +115 -8
  277. package/runner/runner.mjs +442 -0
  278. package/scripts/agent/_common.sh +633 -0
  279. package/scripts/agent/env-hook.sh +626 -0
  280. package/scripts/agent/moondeck-cli.sh +5 -0
  281. package/scripts/agent/task-down.sh +291 -0
  282. package/scripts/agent/task-status.sh +70 -0
  283. package/scripts/agent/task-up.sh +3761 -0
  284. package/scripts/agent/uai-env-exec.sh +26 -0
  285. package/scripts/install/darwin.ts +1048 -0
  286. package/scripts/install/linux.ts +958 -0
  287. package/scripts/install/types.ts +78 -0
  288. package/scripts/install/util.ts +254 -0
  289. package/scripts/install/win.ts +143 -0
  290. package/src/activation-respawn.ts +155 -0
  291. package/src/apple-tunnel-route.ts +104 -0
  292. package/src/bridge-diagnostics.ts +39 -0
  293. package/src/capture-wire.ts +102 -0
  294. package/src/cli.ts +2585 -0
  295. package/src/cloud-credentials.ts +2144 -0
  296. package/src/cloud-origins.ts +23 -0
  297. package/src/credential-location.ts +248 -0
  298. package/src/credential-wire-validation.ts +25 -0
  299. package/src/custom-connections.ts +237 -0
  300. package/src/engine-account-paths.ts +11 -0
  301. package/src/engine-models.ts +39 -0
  302. package/src/engine-usage.ts +104 -0
  303. package/src/enrollment-next-steps.ts +24 -0
  304. package/src/event-outbox.ts +185 -0
  305. package/src/index.ts +2907 -0
  306. package/src/load-env.ts +97 -0
  307. package/src/machine-tunnel-route.ts +156 -0
  308. package/src/main.ts +6363 -0
  309. package/src/never-proxied-domains.ts +31 -0
  310. package/src/outbound-frame.ts +31 -0
  311. package/src/paths.ts +81 -0
  312. package/src/project-env-latest.ts +130 -0
  313. package/src/project-env-policy.ts +396 -0
  314. package/src/project-env-wording.ts +53 -0
  315. package/src/protocol.ts +5550 -0
  316. package/src/runtime-bootstrap.ts +165 -0
  317. package/src/station-identity.ts +96 -0
  318. package/src/task-env-names.ts +28 -0
  319. package/src/task-secrets.ts +75 -0
  320. package/src/tunnel-dial.ts +102 -0
  321. package/src/ui/server.ts +852 -0
  322. package/src/ui/types.ts +173 -0
  323. package/src/workspace-path-policy.ts +41 -0
  324. package/ui/app.js +1102 -0
  325. package/ui/index.html +90 -0
  326. package/ui/style.css +685 -0
  327. package/ui/uai-favicon.svg +21 -0
  328. package/ui/uai-logo-black.svg +9 -0
  329. package/ui/uai-wheel.svg +9 -0
  330. package/bin/moondeck-station.mjs +0 -3
@@ -0,0 +1,642 @@
1
+ /**
2
+ * Environment-level task boundary (ADR-101 Phase C).
3
+ *
4
+ * Providers own machine/container details. Generic orchestration receives an
5
+ * opaque, durable locator and a handle with bounded argv-based operations; it
6
+ * never receives a Docker socket, Compose project, or generic remote shell.
7
+ */
8
+
9
+ export const TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION = 1 as const;
10
+
11
+ /** Durable rollout generation for project environment delivery (ADR-153). */
12
+ export type ProjectEnvMode = "baked" | "loader";
13
+
14
+ /** Every process launch is explicitly classified at the type boundary. */
15
+ export type ProjectEnvProfile = "maintenance" | "task";
16
+
17
+ export interface TaskEnvironmentLocator {
18
+ schemaVersion: typeof TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION;
19
+ /** Stable provider registry key, for example `docker-compose`. */
20
+ provider: string;
21
+ /** Provider-owned, JSON-serializable reconstruction payload. */
22
+ value: unknown;
23
+ }
24
+
25
+ export interface TaskEnvironmentDescriptor {
26
+ taskId: string;
27
+ locator: TaskEnvironmentLocator;
28
+ /** Persisted independently from the provider locator for rollout safety. */
29
+ envMode?: ProjectEnvMode;
30
+ /** Absolute path as observed inside the environment. */
31
+ workspacePath: string;
32
+ codeServerPort?: number;
33
+ previewPorts?: Array<{ name: string; hostPort: number }>;
34
+ initWarning?: string;
35
+ }
36
+
37
+ export interface TaskEnvironmentExecRequest {
38
+ /** Executable followed by literal argv. Shell command strings are forbidden. */
39
+ argv: readonly [string, ...string[]];
40
+ cwd?: string;
41
+ user?: string;
42
+ /** Host environment names inherited by name, without copying values into argv. */
43
+ inheritEnv?: readonly string[];
44
+ env?: Readonly<Record<string, string>>;
45
+ projectEnvProfile: ProjectEnvProfile;
46
+ stdin?: Uint8Array;
47
+ timeoutMs: number;
48
+ maxOutputBytes: number;
49
+ /**
50
+ * Run with every capability, as root only: the secrets proxy's NAT step
51
+ * needs NET_ADMIN, which a task container does not have (ADR-153). Docker
52
+ * only (`docker exec --privileged`, one process, the container keeps its
53
+ * default capabilities); every other provider refuses it.
54
+ */
55
+ privileged?: true;
56
+ /**
57
+ * ADR-154 §6: start the process under the engine supervisor, admitted by
58
+ * the runner and owned until a cleanup kills it. The provider runs, as
59
+ * root, the anchor's prefix, the task user the process would have run as
60
+ * (or "-" when the provider drops to it itself), "--", then the process
61
+ * as it would have run it; the supervisor drops to that user. A request
62
+ * that must run as root is never anchored.
63
+ */
64
+ anchor?: TaskEnvironmentAnchor;
65
+ /**
66
+ * Called by the provider at the last moment before the process starts,
67
+ * after anything it awaits first (a machine looks up its address);
68
+ * throwing refuses the exec unstarted. The secrets proxy's root-step
69
+ * fence rides on it, so a step's exec cannot start once its operation is
70
+ * no longer live, however long that lookup takes (ADR-153).
71
+ */
72
+ admit?: () => void;
73
+ }
74
+
75
+ export interface TaskEnvironmentExecResult {
76
+ exitCode: number | null;
77
+ signal: string | null;
78
+ stdout: Uint8Array;
79
+ stderr: Uint8Array;
80
+ stdoutTruncated: boolean;
81
+ stderrTruncated: boolean;
82
+ }
83
+
84
+ export interface TaskEnvironmentSpawnRequest {
85
+ argv: readonly [string, ...string[]];
86
+ cwd?: string;
87
+ user?: string;
88
+ /** Host environment names inherited by name, without copying values into argv. */
89
+ inheritEnv?: readonly string[];
90
+ env?: Readonly<Record<string, string>>;
91
+ projectEnvProfile: ProjectEnvProfile;
92
+ timeoutMs: number;
93
+ maxOutputBytes: number;
94
+ /** ADR-154 §6: see `TaskEnvironmentExecRequest.anchor`. */
95
+ anchor?: TaskEnvironmentAnchor;
96
+ /** See `TaskEnvironmentExecRequest.admit`. */
97
+ admit?: () => void;
98
+ }
99
+
100
+ /**
101
+ * ADR-154 §6: the engine supervisor a process starts under
102
+ * (credential-consumers/engine-anchor.ts): its argv up to the user it drops
103
+ * to.
104
+ */
105
+ export interface TaskEnvironmentAnchor {
106
+ readonly prefix: readonly [string, ...string[]];
107
+ /**
108
+ * When the launch carries engine credentials in its admission: what a
109
+ * provider that drops to the task user itself (anchor user "-") runs
110
+ * right after that drop, before the process; it reads them from the
111
+ * supervisor's hand-off. A provider whose supervisor drops (a named user)
112
+ * ignores it: the supervisor hands them to the engine itself.
113
+ */
114
+ readonly injector?: readonly [string, ...string[]];
115
+ }
116
+
117
+ /**
118
+ * Long-lived process request used only by agent-session adapters. Its
119
+ * lifetime is owned by the session and therefore has no wall-clock timeout;
120
+ * unread stdout/stderr remains bounded by `maxOutputBytes`.
121
+ */
122
+ export interface TaskEnvironmentSessionRequest {
123
+ argv: readonly [string, ...string[]];
124
+ cwd?: string;
125
+ user?: string;
126
+ inheritEnv?: readonly string[];
127
+ env?: Readonly<Record<string, string>>;
128
+ projectEnvProfile: ProjectEnvProfile;
129
+ maxOutputBytes: number;
130
+ /** ADR-154 §6: see `TaskEnvironmentExecRequest.anchor`. */
131
+ anchor?: TaskEnvironmentAnchor;
132
+ /** See `TaskEnvironmentExecRequest.admit`. */
133
+ admit?: () => void;
134
+ }
135
+
136
+ /** A bounded control-plane launch of a detached durable-session runner. */
137
+ export interface TaskEnvironmentDetachedSessionRequest
138
+ extends TaskEnvironmentSessionRequest {
139
+ launchTimeoutMs: number;
140
+ }
141
+
142
+ export interface TaskEnvironmentProcessExit {
143
+ exitCode: number | null;
144
+ signal: string | null;
145
+ }
146
+
147
+ /**
148
+ * Streaming process primitive used by durable agent sessions. Providers must
149
+ * keep output buffering within `maxOutputBytes`; AsyncIterable does not imply
150
+ * permission to retain an unbounded unread stream.
151
+ */
152
+ export interface TaskEnvironmentProcess {
153
+ readonly stdout: AsyncIterable<Uint8Array>;
154
+ readonly stderr: AsyncIterable<Uint8Array>;
155
+ readonly completion: Promise<TaskEnvironmentProcessExit>;
156
+ write(chunk: Uint8Array): Promise<void>;
157
+ closeInput(): Promise<void>;
158
+ terminate(signal?: "SIGTERM" | "SIGKILL"): Promise<void>;
159
+ }
160
+
161
+ /**
162
+ * Provider-neutral surface handed to agent adapters. `durableIdentity` is an
163
+ * opaque diagnostic/persistence value; callers must never parse it as a
164
+ * container or machine identifier.
165
+ */
166
+ export interface TaskEnvironmentAgentSessionSurface {
167
+ readonly descriptor: TaskEnvironmentDescriptor;
168
+ readonly durableIdentity: string;
169
+ /** ADR-154 §6: whether the provider starts a request's `anchor`. One that
170
+ * does not never runs a leased engine. */
171
+ readonly anchors?: boolean;
172
+ /** ADR-154 §6: "admission" when a request's environment would reach a
173
+ * command line (a machine's ssh command, a local shell line), so a leased
174
+ * launch's engine credentials travel in its admission instead; absent
175
+ * when the provider passes them by name (`--env NAME`). */
176
+ readonly credentialDelivery?: "admission";
177
+ spawnSession(
178
+ request: TaskEnvironmentSessionRequest,
179
+ ): Promise<TaskEnvironmentProcess>;
180
+ launchDetachedSession(
181
+ request: TaskEnvironmentDetachedSessionRequest,
182
+ ): Promise<TaskEnvironmentExecResult>;
183
+ }
184
+
185
+ export type TaskEnvironmentCopyRequest = (
186
+ | {
187
+ direction: "into";
188
+ /** Absolute host path. */
189
+ source: string;
190
+ /** Absolute environment path. */
191
+ destination: string;
192
+ }
193
+ | {
194
+ direction: "out";
195
+ /** Absolute environment path. */
196
+ source: string;
197
+ /** Absolute host path. */
198
+ destination: string;
199
+ }
200
+ ) &
201
+ TaskEnvironmentCallOptions;
202
+
203
+ /** What every call into an environment may carry. */
204
+ export interface TaskEnvironmentCallOptions {
205
+ /** See `TaskEnvironmentExecRequest.admit`: the provider's last word before
206
+ * its process or transport starts, after anything it awaits first. */
207
+ admit?: () => void;
208
+ }
209
+
210
+ /** How much of a workspace file a read takes. The task controls its
211
+ * workspace, so every read of it is bounded. */
212
+ export interface TaskEnvironmentReadOptions extends TaskEnvironmentCallOptions {
213
+ /** At most this many bytes (default `WORKSPACE_READ_MAX_BYTES`). */
214
+ maxBytes?: number;
215
+ /** Which end of a longer file is read: its start (the default) or its
216
+ * end. */
217
+ from?: "start" | "end";
218
+ }
219
+
220
+ /** The most a workspace read takes, unless it asks for less. */
221
+ export const WORKSPACE_READ_MAX_BYTES = 64 * 1024 * 1024;
222
+ /** How long a workspace read may take before it is given up. */
223
+ export const WORKSPACE_READ_TIMEOUT_MS = 60_000;
224
+
225
+ export interface TaskEnvironmentPort {
226
+ name?: string;
227
+ containerPort: number;
228
+ host: "127.0.0.1";
229
+ hostPort: number;
230
+ }
231
+
232
+ export type TaskEnvironmentStatus =
233
+ | {
234
+ state: "running" | "stopped" | "absent";
235
+ /** Provider-level runtime identities, mapped to the legacy wire's
236
+ * `containers` field while that protocol remains in service. */
237
+ instances: string[];
238
+ workspacePresent: boolean;
239
+ /** Not running, yet its processes still exist (a paused or
240
+ * restarting container): they run again without a new start. */
241
+ frozen?: true;
242
+ }
243
+ | { state: "unknown"; detail: string };
244
+
245
+ /**
246
+ * Recovery is allowed to perform provider-private repair, but its caller owns
247
+ * the surrounding task/channel lifecycle. These callbacks keep that ordering
248
+ * explicit without exposing a Docker container, Compose project, or VM
249
+ * identity to generic orchestration.
250
+ */
251
+ export interface TaskEnvironmentRecoveryContext {
252
+ /** One-way host maintenance latch (standard image, shared CLI state, etc.). */
253
+ maintenanceReady: Promise<void>;
254
+ /** Provider work must stop before every externally-visible side effect when
255
+ * this generation is no longer current. */
256
+ isCurrent(): boolean;
257
+ /** Drain/tombstone consumers before stopping or repairing a live writer. */
258
+ quarantineConsumers?: () => Promise<void>;
259
+ /** Reassert task-local security/runtime prerequisites before admission. */
260
+ prepareConsumers?: () => Promise<void>;
261
+ /** Re-open admission only after the environment is proven operational. */
262
+ allowConsumers?: () => void;
263
+ /** Recreate durable consumers after admission has reopened. */
264
+ ensureConsumersStarted?: () => Promise<void>;
265
+ }
266
+
267
+ export type TaskEnvironmentRecoveryResult =
268
+ | {
269
+ outcome: "recovered";
270
+ /** Optional fresh provider status. Recovery itself is the authoritative
271
+ * proof; callers must not issue a second inventory probe merely to fill
272
+ * this field. */
273
+ status?: TaskEnvironmentStatus;
274
+ descriptor?: TaskEnvironmentDescriptor;
275
+ }
276
+ | {
277
+ outcome: "deferred";
278
+ /** Bounded, operator-safe reason. Deferred recovery is retryable and
279
+ * must never be interpreted as absence. */
280
+ detail: string;
281
+ };
282
+
283
+ export interface TaskEnvironmentHandle<TTeardownResult = void>
284
+ extends TaskEnvironmentAgentSessionSurface {
285
+ readonly descriptor: TaskEnvironmentDescriptor;
286
+ /** ADR-121 strangler ops: task-scoped file IO through the provider, so
287
+ * call sites stop knowing where the workspace physically lives. Paths are
288
+ * ENVIRONMENT-absolute and must sit inside the descriptor's workspace
289
+ * (assertWorkspaceScopedPath). Providers whose workspace is host-side
290
+ * (docker, apple) implement these as direct filesystem access — zero
291
+ * behavior change; machine providers reach over their transport. Read
292
+ * resolves null for a missing file and rejects for any other failure —
293
+ * absence and error are different facts. A read takes only a regular
294
+ * file, never waits on one it opens (a FIFO or device is refused), takes
295
+ * at most `maxBytes`, and is given up past its bound: what the task puts
296
+ * in its workspace never stalls the runner. */
297
+ readWorkspaceFile(path: string, options?: TaskEnvironmentReadOptions): Promise<Uint8Array | null>;
298
+ writeWorkspaceFile(path: string, bytes: Uint8Array, options?: TaskEnvironmentCallOptions): Promise<void>;
299
+ exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult>;
300
+ spawn(request: TaskEnvironmentSpawnRequest): Promise<TaskEnvironmentProcess>;
301
+ copy(request: TaskEnvironmentCopyRequest): Promise<void>;
302
+ ports(): Promise<TaskEnvironmentPort[]>;
303
+ status(): Promise<TaskEnvironmentStatus>;
304
+ recover(
305
+ context: TaskEnvironmentRecoveryContext,
306
+ ): Promise<TaskEnvironmentRecoveryResult>;
307
+ /** Resumable pause: halt the environment's processes while keeping every
308
+ * durable resource (worktree, volumes, container state) so a later
309
+ * provision/recovery can resume it. Idempotent for an already-stopped
310
+ * environment. */
311
+ stop(): Promise<void>;
312
+ /** Idempotent: success means provider-owned runtime resources are absent. */
313
+ teardown(): Promise<TTeardownResult>;
314
+ /**
315
+ * ADR-154 §6, the boot recovery sweep only (task-credential-cleanup.ts):
316
+ * run `work` in this exact runtime while it is stopped, by starting it
317
+ * for `work` alone and stopping it again after, whatever `work` did.
318
+ * Rejects, starting nothing, unless the runtime is provably this task's,
319
+ * stopped (not paused or restarting), idle by its own command, and starts
320
+ * nothing else of the project's either: no enabled healthcheck (its own or
321
+ * its image's), no restart policy, the runner's own init, no auto-remove.
322
+ * `oweStop` is called with the exact container's id before it starts; it
323
+ * must record durably that a stop is owed, and if it throws nothing
324
+ * starts. Rejects after `work` unless the container is proven stopped
325
+ * again. Never creates, recreates or removes anything. Providers that
326
+ * cannot do this omit it, and such a cleanup stays failed.
327
+ */
328
+ runStoppedForCleanup?<T>(
329
+ work: () => Promise<T>,
330
+ oweStop: (containerId: string) => void | Promise<void>,
331
+ ): Promise<T>;
332
+ /**
333
+ * An owed re-stop settled (`runStoppedForCleanup`): true only once the
334
+ * container with exactly this id is proven stopped (stopping it if it
335
+ * runs) or proven gone by a daemon that answered. A failed call is false,
336
+ * never gone.
337
+ */
338
+ settleCleanupStart?(containerId: string): Promise<boolean>;
339
+ /** Direct-route inspection for backends whose container IPs are
340
+ * host-routable (ADR-106 Apple vmnet). Providers that publish host ports
341
+ * instead simply omit it. The proof must bind the answer to this exact
342
+ * task's container (label check), not merely a name lookup. */
343
+ inspectRoute?(): Promise<
344
+ | { kind: "running"; ipv4Address: string }
345
+ | { kind: "unavailable" }
346
+ | { kind: "unknown"; detail: string }
347
+ >;
348
+ /** ADR-121, machine provider only: full ssh argv for the persistent
349
+ * REVERSE gateway tunnel that makes the host's MCP gateway answer on the
350
+ * environment's own loopback. Container providers omit it — their tasks
351
+ * route to the gateway via container topology instead. */
352
+ gatewayTunnelSshArgs?(
353
+ machinePort: number,
354
+ hostPort: number,
355
+ options?: TaskEnvironmentCallOptions,
356
+ ): Promise<string[]>;
357
+ }
358
+
359
+ export interface TaskEnvironmentProvisionRequest<TInput, TCredentials> {
360
+ taskId: string;
361
+ /** Omitted only by direct legacy/provider callers; new orchestration passes loader. */
362
+ envMode?: ProjectEnvMode;
363
+ input: TInput;
364
+ credentials: TCredentials;
365
+ /**
366
+ * Awaited after the provider has derived and validated its durable identity,
367
+ * but before it creates any external resources. A crash after this callback
368
+ * can therefore be recovered from the opaque locator alone.
369
+ */
370
+ onPrepared?: (locator: TaskEnvironmentLocator) => Promise<void>;
371
+ }
372
+
373
+ export interface TaskEnvironmentProvisioned<TResult, TTeardownResult = void> {
374
+ handle: TaskEnvironmentHandle<TTeardownResult>;
375
+ /** Provider launch result retained for rolling wire compatibility. */
376
+ result: TResult;
377
+ /** The provider reused an environment that already existed — a Resume's
378
+ * stopped machine, whose disk holds the task's work — rather than creating
379
+ * it. A failed start stops such an environment; it never destroys it. */
380
+ reused?: boolean;
381
+ }
382
+
383
+ export interface TaskEnvironmentProvider<
384
+ TInput,
385
+ TCredentials,
386
+ TResult,
387
+ TTeardownResult = void,
388
+ > {
389
+ readonly kind: string;
390
+ provision(
391
+ request: TaskEnvironmentProvisionRequest<TInput, TCredentials>,
392
+ ): Promise<TaskEnvironmentProvisioned<TResult, TTeardownResult>>;
393
+ reconstruct(
394
+ locator: TaskEnvironmentLocator,
395
+ envMode?: ProjectEnvMode,
396
+ ): Promise<TaskEnvironmentHandle<TTeardownResult>>;
397
+ }
398
+
399
+ /** Workspace-scope guard for the strangler file ops: the path must be an
400
+ * absolute, NUL-free, dot-segment-free environment path inside the
401
+ * descriptor's workspace. Escaping the workspace through the file ops would
402
+ * hand callers the environment's whole filesystem. */
403
+ export function assertWorkspaceScopedPath(
404
+ descriptor: TaskEnvironmentDescriptor,
405
+ path: string,
406
+ ): void {
407
+ if (!path.startsWith("/") || path.includes("\0")) {
408
+ throw new Error("workspace file path must be an absolute NUL-free path");
409
+ }
410
+ if (path.split("/").some((part) => part === "." || part === "..")) {
411
+ throw new Error("workspace file path must not contain dot segments");
412
+ }
413
+ const root = descriptor.workspacePath.endsWith("/")
414
+ ? descriptor.workspacePath
415
+ : `${descriptor.workspacePath}/`;
416
+ if (path !== descriptor.workspacePath && !path.startsWith(root)) {
417
+ throw new Error("workspace file path escapes the task workspace");
418
+ }
419
+ }
420
+
421
+ /** A malformed process request, distinct from provider/runtime failures. */
422
+ export class TaskEnvironmentRequestValidationError extends Error {
423
+ override name = "TaskEnvironmentRequestValidationError";
424
+ }
425
+
426
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
427
+
428
+ /** Shared admission guard for provider implementations and fake providers. */
429
+ /** A one-shot exec: the only request that may be privileged, and then only
430
+ * as root. Providers pass the privilege on explicitly, never by reading the
431
+ * request, so no other surface can carry it. */
432
+ export function assertTaskEnvironmentExecRequest(request: TaskEnvironmentExecRequest): void {
433
+ const { privileged, ...unprivileged } = request;
434
+ if (privileged !== undefined && (privileged !== true || request.user !== "root")) {
435
+ throw new TaskEnvironmentRequestValidationError("a privileged task environment exec must run as root");
436
+ }
437
+ assertTaskEnvironmentProcessRequest(unprivileged);
438
+ }
439
+
440
+ export function assertTaskEnvironmentProcessRequest(
441
+ request: TaskEnvironmentExecRequest | TaskEnvironmentSpawnRequest,
442
+ ): void {
443
+ assertTaskEnvironmentSessionRequest(request);
444
+ if (!Number.isSafeInteger(request.timeoutMs) || request.timeoutMs <= 0) {
445
+ throw new TaskEnvironmentRequestValidationError("task environment timeout must be a positive integer");
446
+ }
447
+ }
448
+
449
+ /** For providers with no privileged exec: refuse it rather than run the
450
+ * step without the capability it asked for. */
451
+ export function refusePrivilegedExec(request: TaskEnvironmentExecRequest): void {
452
+ if (request.privileged === true) {
453
+ throw new TaskEnvironmentRequestValidationError("this task environment provider has no privileged exec");
454
+ }
455
+ }
456
+
457
+ export function assertTaskEnvironmentSessionRequest(
458
+ request:
459
+ | TaskEnvironmentSessionRequest
460
+ | TaskEnvironmentDetachedSessionRequest
461
+ | TaskEnvironmentExecRequest
462
+ | TaskEnvironmentSpawnRequest,
463
+ ): void {
464
+ // Only a one-shot exec may be privileged (assertTaskEnvironmentExecRequest
465
+ // strips it after checking); anywhere else it is refused outright.
466
+ if ("privileged" in request) {
467
+ throw new TaskEnvironmentRequestValidationError("only a one-shot task environment exec may be privileged");
468
+ }
469
+ if (
470
+ request.projectEnvProfile !== "maintenance" &&
471
+ request.projectEnvProfile !== "task"
472
+ ) {
473
+ throw new TaskEnvironmentRequestValidationError(
474
+ "task environment project env profile must be maintenance or task",
475
+ );
476
+ }
477
+ if (
478
+ !Array.isArray(request.argv) ||
479
+ request.argv.length === 0 ||
480
+ request.argv.some(
481
+ (part) => typeof part !== "string" || part.length === 0 || part.includes("\0"),
482
+ )
483
+ ) {
484
+ throw new TaskEnvironmentRequestValidationError("task environment argv must contain nonempty NUL-free strings");
485
+ }
486
+ if (
487
+ request.cwd !== undefined &&
488
+ (typeof request.cwd !== "string" || !request.cwd.startsWith("/") || request.cwd.includes("\0"))
489
+ ) {
490
+ throw new TaskEnvironmentRequestValidationError("task environment cwd must be an absolute NUL-free path");
491
+ }
492
+ if (
493
+ request.user !== undefined &&
494
+ (typeof request.user !== "string" || request.user.length === 0 || request.user.includes("\0"))
495
+ ) {
496
+ throw new TaskEnvironmentRequestValidationError("task environment user must be a nonempty NUL-free value");
497
+ }
498
+ if (
499
+ request.anchor !== undefined &&
500
+ (request.user === "root" ||
501
+ !Array.isArray(request.anchor.prefix) ||
502
+ request.anchor.prefix.length === 0 ||
503
+ request.anchor.prefix.some(
504
+ (part) => typeof part !== "string" || part.length === 0 || part.includes("\0"),
505
+ ))
506
+ ) {
507
+ throw new TaskEnvironmentRequestValidationError("a task environment anchor must be a NUL-free argv for a task user's process");
508
+ }
509
+ if (
510
+ request.inheritEnv !== undefined &&
511
+ (!Array.isArray(request.inheritEnv) ||
512
+ request.inheritEnv.length > 128 ||
513
+ new Set(request.inheritEnv).size !== request.inheritEnv.length ||
514
+ request.inheritEnv.some(
515
+ (name) => typeof name !== "string" || !ENV_NAME.test(name),
516
+ ))
517
+ ) {
518
+ throw new TaskEnvironmentRequestValidationError("task environment inherited env names are invalid");
519
+ }
520
+ for (const [name, value] of Object.entries(request.env ?? {})) {
521
+ if (
522
+ !ENV_NAME.test(name) ||
523
+ typeof value !== "string" ||
524
+ value.includes("\0")
525
+ ) {
526
+ throw new TaskEnvironmentRequestValidationError("task environment env must contain valid NUL-free entries");
527
+ }
528
+ }
529
+ if (
530
+ !Number.isSafeInteger(request.maxOutputBytes) ||
531
+ request.maxOutputBytes <= 0
532
+ ) {
533
+ throw new TaskEnvironmentRequestValidationError("task environment output limit must be a positive integer");
534
+ }
535
+ if (
536
+ "launchTimeoutMs" in request &&
537
+ (!Number.isSafeInteger(request.launchTimeoutMs) ||
538
+ request.launchTimeoutMs <= 0)
539
+ ) {
540
+ throw new TaskEnvironmentRequestValidationError(
541
+ "task environment detached launch timeout must be a positive integer",
542
+ );
543
+ }
544
+ }
545
+
546
+ export function serializeTaskEnvironmentLocator(
547
+ locator: TaskEnvironmentLocator,
548
+ ): string {
549
+ validateTaskEnvironmentLocator(locator);
550
+ return JSON.stringify(locator);
551
+ }
552
+
553
+ export function parseTaskEnvironmentLocator(raw: string): TaskEnvironmentLocator {
554
+ let parsed: unknown;
555
+ try {
556
+ parsed = JSON.parse(raw);
557
+ } catch {
558
+ throw new Error("task environment locator is not valid JSON");
559
+ }
560
+ return validateTaskEnvironmentLocator(parsed);
561
+ }
562
+
563
+ export function validateTaskEnvironmentLocator(
564
+ value: unknown,
565
+ ): TaskEnvironmentLocator {
566
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
567
+ throw new Error("task environment locator must be an object");
568
+ }
569
+ const record = value as Record<string, unknown>;
570
+ if (
571
+ Object.keys(record).sort().join("\0") !==
572
+ ["provider", "schemaVersion", "value"].sort().join("\0")
573
+ ) {
574
+ throw new Error("task environment locator has unexpected fields");
575
+ }
576
+ if (record.schemaVersion !== TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION) {
577
+ throw new Error("unsupported task environment locator schema");
578
+ }
579
+ if (
580
+ typeof record.provider !== "string" ||
581
+ !/^[a-z][a-z0-9-]{0,63}$/.test(record.provider)
582
+ ) {
583
+ throw new Error("task environment locator provider is invalid");
584
+ }
585
+ assertJsonValue(record.value, 0);
586
+ return {
587
+ schemaVersion: TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
588
+ provider: record.provider,
589
+ value: record.value,
590
+ };
591
+ }
592
+
593
+ function assertJsonValue(value: unknown, depth: number): void {
594
+ if (depth > 16) throw new Error("task environment locator is too deeply nested");
595
+ if (
596
+ value === null ||
597
+ typeof value === "string" ||
598
+ typeof value === "boolean"
599
+ ) {
600
+ return;
601
+ }
602
+ if (typeof value === "number") {
603
+ if (!Number.isFinite(value)) {
604
+ throw new Error("task environment locator contains a non-finite number");
605
+ }
606
+ return;
607
+ }
608
+ if (Array.isArray(value)) {
609
+ if (value.length > 256) {
610
+ throw new Error("task environment locator array is too large");
611
+ }
612
+ for (const item of value) assertJsonValue(item, depth + 1);
613
+ return;
614
+ }
615
+ if (value && typeof value === "object") {
616
+ const entries = Object.entries(value as Record<string, unknown>);
617
+ if (entries.length > 256) {
618
+ throw new Error("task environment locator object is too large");
619
+ }
620
+ for (const [key, item] of entries) {
621
+ if (key.includes("\0")) {
622
+ throw new Error("task environment locator key contains NUL");
623
+ }
624
+ assertJsonValue(item, depth + 1);
625
+ }
626
+ return;
627
+ }
628
+ throw new Error("task environment locator is not JSON-serializable");
629
+ }
630
+
631
+ /**
632
+ * Providers whose workspace is reached only through the environment
633
+ * transport — a machine over ssh (ADR-121) or this very machine in sidecar
634
+ * mode (ADR-133) — as opposed to a container the host can `docker exec` into
635
+ * or bind-mount. Every "is this a machine task?" branch in the host means
636
+ * exactly this, so it asks here rather than comparing one provider name.
637
+ */
638
+ export function isEnvironmentTransportProvider(
639
+ provider: string | null | undefined,
640
+ ): boolean {
641
+ return provider === "machine" || provider === "self";
642
+ }