@moondeck/station 0.0.1 → 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 (326) 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 +115 -8
  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
  326. package/bin/moondeck-station.mjs +0 -3
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Best-effort task.status frames have no acknowledgement, but they must at
3
+ * least survive bridge disconnects within this host process. The local task
4
+ * mirror remains the authority at flush time so a later Resume wins over an
5
+ * older queued stop.
6
+ */
7
+ /**
8
+ * Why the host stopped the task, when the host knows.
9
+ *
10
+ * `"manual"` is ADR-028's local-UI Stop — a person at the machine pressed it.
11
+ * Everything else the host reports through this frame is automatic (writable
12
+ * -container quarantine during a runtime activation or a shared-volume
13
+ * repair), and carries no reason. The cloud needs the difference because
14
+ * ADR-144 lets a collaborator resume a task that stopped on its own but not
15
+ * one a human deliberately paused, and until now both arrived as the same
16
+ * three fields.
17
+ */
18
+ export type StoppedTaskReason = "manual";
19
+
20
+ interface StoppedTaskState {
21
+ status: string;
22
+ runEpoch: number;
23
+ }
24
+
25
+ interface PendingStoppedTaskStatus {
26
+ reason: StoppedTaskReason | undefined;
27
+ runEpoch: number | undefined;
28
+ }
29
+
30
+ export class StoppedTaskStatusQueue {
31
+ private readonly pending = new Map<string, PendingStoppedTaskStatus>();
32
+
33
+ constructor(
34
+ private readonly stateForTask: (
35
+ taskId: string,
36
+ ) => StoppedTaskState | null | undefined,
37
+ ) {}
38
+
39
+ enqueue(
40
+ taskId: string,
41
+ reason?: StoppedTaskReason,
42
+ runEpoch?: number,
43
+ ): void {
44
+ const previous = this.pending.get(taskId);
45
+ const previousEpoch = previous?.runEpoch ?? 0;
46
+ const nextEpoch = runEpoch ?? 0;
47
+ // A command result can arrive after the next assignment has already
48
+ // queued its own status. Run epochs are monotonic, so an older result must
49
+ // not replace the newer pending truth (or donate its reason to it).
50
+ if (previous !== undefined && nextEpoch < previousEpoch) return;
51
+ // A reason, once known, STICKS across a re-enqueue. The same task can be
52
+ // queued twice before a flush — an operator Stop, then a quarantine sweep
53
+ // that finds its container already down — and the automatic pass arriving
54
+ // second does not make the human's decision retroactively automatic.
55
+ // Dropping it would silently hand the task back to every collaborator.
56
+ this.pending.set(taskId, {
57
+ reason:
58
+ previous !== undefined && previousEpoch === nextEpoch
59
+ ? reason ?? previous.reason
60
+ : reason,
61
+ runEpoch,
62
+ });
63
+ }
64
+
65
+ /** A failed taskUp may have stopped a legacy runtime before recreation lost
66
+ * Docker. Queue the physical truth after the command result is published. */
67
+ enqueueStoppedTaskUp(command: string, args: readonly unknown[]): boolean {
68
+ if (command !== "taskUp") return false;
69
+ const input = args[0];
70
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
71
+ return false;
72
+ }
73
+ const task = (input as { task?: unknown }).task;
74
+ if (!task || typeof task !== "object" || Array.isArray(task)) return false;
75
+ const taskId = (task as { id?: unknown }).id;
76
+ const state = typeof taskId === "string" ? this.stateForTask(taskId) : null;
77
+ if (typeof taskId !== "string" || state?.status !== "stopped") {
78
+ return false;
79
+ }
80
+ const hasRunEpoch = Object.hasOwn(task, "runEpoch");
81
+ const inputRunEpoch = (task as { runEpoch?: unknown }).runEpoch;
82
+ if (hasRunEpoch) {
83
+ if (
84
+ !Number.isSafeInteger(inputRunEpoch) ||
85
+ (inputRunEpoch as number) < 1 ||
86
+ inputRunEpoch !== state.runEpoch
87
+ ) {
88
+ return false;
89
+ }
90
+ } else if (state.runEpoch !== 0) {
91
+ // Never relabel a delayed v0 taskUp result as the current v1 run.
92
+ return false;
93
+ }
94
+ const runEpoch = hasRunEpoch ? (inputRunEpoch as number) : undefined;
95
+ this.enqueue(taskId, undefined, runEpoch);
96
+ return true;
97
+ }
98
+
99
+ flush(
100
+ publish: (
101
+ taskId: string,
102
+ reason?: StoppedTaskReason,
103
+ runEpoch?: number,
104
+ ) => void,
105
+ ): void {
106
+ for (const [taskId, pending] of [...this.pending]) {
107
+ const state = this.stateForTask(taskId);
108
+ if (
109
+ state?.status === "stopped" &&
110
+ (pending.runEpoch === undefined
111
+ ? state.runEpoch === 0
112
+ : state.runEpoch === pending.runEpoch)
113
+ ) {
114
+ if (pending.runEpoch === undefined) publish(taskId, pending.reason);
115
+ else publish(taskId, pending.reason, pending.runEpoch);
116
+ }
117
+ this.pending.delete(taskId);
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Backend-aware CLI access to TASK app containers for the auxiliary
3
+ * subsystems that predate the TaskEnvironment provider (GitHub credential
4
+ * injection, git identity, Codex reinjection, skills). Found live 2026-08-16:
5
+ * these spawned the literal `docker` binary with Compose replica names, so on
6
+ * a Docker-free Apple host every agent session start died at the credential
7
+ * step with spawn ENOENT.
8
+ *
9
+ * The exec/cp verbs are flag-compatible across Docker and the bundled Apple
10
+ * CLI, so most callers only need the command + container-name translation
11
+ * here. Verbs that differ (ps/rm/inventory) get explicit helpers.
12
+ */
13
+
14
+ import { spawn as nodeSpawn } from "node:child_process";
15
+
16
+ import {
17
+ appleContainerRuntimeBinding,
18
+ pinnedContainerRuntimeProvider,
19
+ } from "./container-runtime";
20
+ import { dockerCli, type DockerResult } from "./docker-exec";
21
+ import { assertSafeHostTaskId } from "./task-identity";
22
+
23
+ export interface TaskContainerBackend {
24
+ command: string;
25
+ apple: boolean;
26
+ }
27
+
28
+ /** The pin, not the readiness-gated identity: session ensure and recovery
29
+ * both run while the public state can still be `checking`. */
30
+ export function taskContainerBackend(): TaskContainerBackend {
31
+ if (pinnedContainerRuntimeProvider() === "apple-container") {
32
+ const binding = appleContainerRuntimeBinding();
33
+ if (binding !== null) {
34
+ return { command: binding.containerCliPath, apple: true };
35
+ }
36
+ }
37
+ return { command: "docker", apple: false };
38
+ }
39
+
40
+ /** The app container's name under the selected backend: Compose replica
41
+ * naming for Docker, the provider's fixed name for Apple (must match
42
+ * appleTaskContainerName in task-environment/apple-container.ts). */
43
+ export function taskAppContainerName(taskId: string): string {
44
+ assertSafeHostTaskId(taskId);
45
+ const lowered = taskId.toLowerCase();
46
+ return taskContainerBackend().apple
47
+ ? `moondeck-task-${lowered}-app`
48
+ : `task-${lowered}-app-1`;
49
+ }
50
+
51
+ /** dockerCli-compatible runner that targets the selected backend. Every kill
52
+ * (deadline or overflow) forces settlement within a bounded grace. */
53
+ export function taskContainerCli(
54
+ args: string[],
55
+ opts: {
56
+ input?: string;
57
+ timeoutMs?: number;
58
+ maxOutputBytes?: number;
59
+ } = {},
60
+ ): Promise<DockerResult> {
61
+ const backend = taskContainerBackend();
62
+ if (!backend.apple) return dockerCli(args, opts);
63
+ return new Promise<DockerResult>((resolve) => {
64
+ const child = nodeSpawn(backend.command, args, {
65
+ stdio: ["pipe", "pipe", "pipe"],
66
+ });
67
+ let stdout = "";
68
+ let stderr = "";
69
+ let settled = false;
70
+ const finish = (status: number | null): void => {
71
+ if (settled) return;
72
+ settled = true;
73
+ clearTimeout(timer);
74
+ resolve({ status, stdout, stderr });
75
+ };
76
+ const killAndSettle = (): void => {
77
+ child.kill("SIGKILL");
78
+ const settle = setTimeout(() => finish(null), 5_000);
79
+ settle.unref?.();
80
+ };
81
+ const timer = setTimeout(killAndSettle, opts.timeoutMs ?? 30_000);
82
+ timer.unref?.();
83
+ child.stdout.setEncoding("utf8");
84
+ child.stderr.setEncoding("utf8");
85
+ child.stdout.on("data", (chunk: string) => {
86
+ stdout += chunk;
87
+ if (
88
+ opts.maxOutputBytes !== undefined &&
89
+ stdout.length + stderr.length > opts.maxOutputBytes
90
+ ) {
91
+ killAndSettle();
92
+ }
93
+ });
94
+ child.stderr.on("data", (chunk: string) => {
95
+ stderr += chunk;
96
+ if (
97
+ opts.maxOutputBytes !== undefined &&
98
+ stdout.length + stderr.length > opts.maxOutputBytes
99
+ ) {
100
+ killAndSettle();
101
+ }
102
+ });
103
+ child.stdin.on("error", () => {});
104
+ if (opts.input !== undefined) child.stdin.write(opts.input);
105
+ child.stdin.end();
106
+ child.on("error", () => finish(null));
107
+ child.on("close", (code) => finish(code));
108
+ });
109
+ }
110
+
111
+ export type TaskContainerState =
112
+ | { kind: "running" }
113
+ | { kind: "stopped"; state: string }
114
+ | { kind: "absent" }
115
+ | { kind: "unknown"; detail: string };
116
+
117
+ /** Backend-aware state probe for one exact task app container. Absence is
118
+ * proven only by the complete not-found error naming the container (Apple)
119
+ * or an empty exact-name filter answer (Docker); anything else is unknown. */
120
+ export async function taskAppContainerState(
121
+ containerName: string,
122
+ run: typeof taskContainerCli = taskContainerCli,
123
+ ): Promise<TaskContainerState> {
124
+ const backend = taskContainerBackend();
125
+ if (backend.apple) {
126
+ const inspected = await run(["inspect", containerName], {
127
+ timeoutMs: 30_000,
128
+ maxOutputBytes: 1024 * 1024,
129
+ });
130
+ if (inspected.status !== 0) {
131
+ if (
132
+ inspected.stdout.trim() === "" &&
133
+ inspected.stderr
134
+ .split("\n")
135
+ .some(
136
+ (line) =>
137
+ line.trim() === `Error: container not found: ${containerName}`,
138
+ )
139
+ ) {
140
+ return { kind: "absent" };
141
+ }
142
+ return {
143
+ kind: "unknown",
144
+ detail: inspected.stderr.trim().slice(0, 300) || "inspect failed",
145
+ };
146
+ }
147
+ try {
148
+ const parsed = JSON.parse(inspected.stdout) as Array<{
149
+ id?: unknown;
150
+ status?: { state?: unknown };
151
+ }>;
152
+ if (
153
+ !Array.isArray(parsed) ||
154
+ parsed.length !== 1 ||
155
+ parsed[0]?.id !== containerName
156
+ ) {
157
+ return { kind: "unknown", detail: "inspect shape is invalid" };
158
+ }
159
+ const state = parsed[0]?.status?.state;
160
+ if (typeof state !== "string") {
161
+ return { kind: "unknown", detail: "inspect state is missing" };
162
+ }
163
+ return state === "running"
164
+ ? { kind: "running" }
165
+ : { kind: "stopped", state };
166
+ } catch {
167
+ return { kind: "unknown", detail: "inspect output is not JSON" };
168
+ }
169
+ }
170
+ const listed = await run(
171
+ [
172
+ "ps",
173
+ "--all",
174
+ "--filter",
175
+ `name=^/${containerName}$`,
176
+ "--format",
177
+ "{{.State}}",
178
+ ],
179
+ { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 },
180
+ );
181
+ if (listed.status !== 0) {
182
+ return {
183
+ kind: "unknown",
184
+ detail: listed.stderr.trim().slice(0, 300) || "docker ps failed",
185
+ };
186
+ }
187
+ const states = listed.stdout
188
+ .split(/\r?\n/)
189
+ .map((line) => line.trim())
190
+ .filter(Boolean);
191
+ if (states.length === 0) return { kind: "absent" };
192
+ if (states.length !== 1) {
193
+ return { kind: "unknown", detail: `${states.length} containers matched` };
194
+ }
195
+ return states[0] === "running"
196
+ ? { kind: "running" }
197
+ : { kind: "stopped", state: states[0]! };
198
+ }
199
+
200
+ /** Force-remove one exact task app container and prove its absence. */
201
+ export async function removeTaskAppContainer(
202
+ containerName: string,
203
+ run: typeof taskContainerCli = taskContainerCli,
204
+ ): Promise<void> {
205
+ const backend = taskContainerBackend();
206
+ const removed = await run(
207
+ backend.apple
208
+ ? ["delete", "--force", containerName]
209
+ : ["rm", "--force", containerName],
210
+ { timeoutMs: 60_000, maxOutputBytes: 64 * 1024 },
211
+ );
212
+ if (removed.status !== 0 && backend.apple) {
213
+ // Deleting an already-absent container is success; anything else is not.
214
+ const state = await taskAppContainerState(containerName, run);
215
+ if (state.kind === "absent") return;
216
+ throw new Error(
217
+ `remove ${containerName} failed: ${removed.stderr.trim().slice(0, 300)}`,
218
+ );
219
+ }
220
+ const after = await taskAppContainerState(containerName, run);
221
+ if (after.kind !== "absent") {
222
+ throw new Error(`remove ${containerName} was not verified`);
223
+ }
224
+ }
225
+
226
+ /** Names of RUNNING task app containers under the selected backend. */
227
+ export async function listRunningTaskAppContainerNames(
228
+ run: typeof taskContainerCli = taskContainerCli,
229
+ ): Promise<string[] | null> {
230
+ const backend = taskContainerBackend();
231
+ if (backend.apple) {
232
+ const listed = await run(["list", "--format", "json"], {
233
+ timeoutMs: 30_000,
234
+ maxOutputBytes: 8 * 1024 * 1024,
235
+ });
236
+ if (listed.status !== 0) return null;
237
+ try {
238
+ const parsed = JSON.parse(listed.stdout) as Array<{
239
+ configuration?: { id?: unknown };
240
+ status?: { state?: unknown };
241
+ }>;
242
+ if (!Array.isArray(parsed)) return null;
243
+ // `container list` without --all already returns running containers,
244
+ // but the state check keeps this honest against CLI behavior drift.
245
+ return parsed
246
+ .filter(
247
+ (item) =>
248
+ item?.status?.state === undefined ||
249
+ item?.status?.state === "running",
250
+ )
251
+ .map((item) => item?.configuration?.id)
252
+ .filter(
253
+ (id): id is string =>
254
+ typeof id === "string" && /^moondeck-task-[a-z0-9][a-z0-9_-]*-app$/.test(id),
255
+ );
256
+ } catch {
257
+ return null;
258
+ }
259
+ }
260
+ const listed = await run(
261
+ ["ps", "--filter", "status=running", "--format", "{{.Names}}"],
262
+ { timeoutMs: 30_000, maxOutputBytes: 1024 * 1024 },
263
+ );
264
+ if (listed.status !== 0) return null;
265
+ return listed.stdout
266
+ .split(/\r?\n/)
267
+ .map((line) => line.trim())
268
+ .filter(Boolean);
269
+ }