@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,291 @@
1
+ #!/usr/bin/env bash
2
+ # task-down <taskId> — tear the task stack down. Idempotent (ADR-022).
3
+ #
4
+ # 1. docker compose down -v --rmi local --remove-orphans.
5
+ # 2. rm -rf the whole task dir — workspace + task-private Git repositories.
6
+ # Host caches under projects/<id>/host-cache.git persist.
7
+ # 3. Persist final state: status=killed (unless already error/shipped).
8
+ #
9
+ # Destruction needs explicit terminal intent: the runner sets
10
+ # UAI_TASK_DOWN_TERMINAL=1 only on the cloud taskDown path (a task close or
11
+ # orphan GC). Without it the script refuses before touching Docker or disk —
12
+ # a stray caller (recovery, a credential-cleanup fallback, an operator
13
+ # running the script by hand) must never delete a live task's worktree
14
+ # (2026-10-20: a cleanup fallback did, while Docker was briefly gone).
15
+
16
+ set -euo pipefail
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ # shellcheck source=_common.sh
19
+ . "$SCRIPT_DIR/_common.sh"
20
+ # The station's standard image (task-up.sh STANDARD_IMAGE): local whenever a
21
+ # Docker task has run here, and only ever run with --pull never.
22
+ TASK_CLEANUP_IMAGE="moondeck-standard:dev"
23
+
24
+ [ "$#" -eq 1 ] || emit_err "BAD_ARGS" "usage: task-down <taskId>" "args"
25
+
26
+ task_id="$1"
27
+ # Make the ASCII security grammar independent of the operator's collation.
28
+ export LC_ALL=C
29
+ if [ "${#task_id}" -gt 128 ] ||
30
+ [[ ! "$task_id" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then
31
+ emit_err "BAD_ARGS" "invalid task id" "task identity validation"
32
+ fi
33
+ # Before UAI_TASK_ID is set: a refusal must not mark the live task errored.
34
+ if [ "${UAI_TASK_DOWN_TERMINAL:-}" != "1" ]; then
35
+ emit_err "BAD_ARGS" \
36
+ "task-down requires terminal intent (UAI_TASK_DOWN_TERMINAL=1, set only by the cloud taskDown); refusing to destroy task $task_id" \
37
+ "terminal intent"
38
+ fi
39
+ UAI_TASK_ID="$task_id"
40
+ setup_err_trap
41
+ validate_task_runtime_selection
42
+
43
+ step "TASK_NOT_FOUND" "read task row"
44
+ # Teardown must never depend on bookkeeping: a broken/missing command
45
+ # snapshot (seen live 2026-07-21 cleaning up after a failed task-up —
46
+ # db_get_task errored, the trap reported TASK_NOT_FOUND, and the container
47
+ # survived) still tears the stack down by convention-derived names.
48
+ task_json=$(db_get_task "$task_id" 2>/dev/null) || task_json="[]"
49
+ row_count=$(jq 'length' <<<"$task_json" 2>/dev/null) || row_count=0
50
+ if [ "$row_count" -lt 1 ]; then
51
+ log "task row unavailable — tearing down by derived names"
52
+ else
53
+ UAI_RETRY_STATUS=$(jq -r '.[0].status // ""' <<<"$task_json")
54
+ fi
55
+ # Persisted runtime paths are recovery hints, never deletion authority. A
56
+ # corrupt row must not aim compose/rm outside this exact task identity.
57
+ compose_project=$(compose_project_for_task "$task_id")
58
+ task_dir="$UAI_WORKSPACE_ROOT/tasks/$task_id"
59
+
60
+ # -----------------------------------------------------------------------------
61
+ # 1. Compose stack.
62
+ # -----------------------------------------------------------------------------
63
+
64
+ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
65
+ step "COMPOSE_DOWN_FAILED" "remove task container"
66
+ app_container=$(apple_app_container_for_task "$task_id")
67
+ derived_image="moondeck-task-${task_id}"
68
+ # Ownership proof before destruction (review 2026-08-18 rounds 5+6): every
69
+ # container Uai creates carries io.moondeck.task=<id>. Destruction requires the
70
+ # inspect response to prove EXACT identity — exactly one record, whose
71
+ # top-level id is the requested name, carrying the exact label — the same
72
+ # response-confusion discipline recovery applies. A container merely
73
+ # OCCUPYING the reserved name, or a confused/multi-record answer, is not
74
+ # authority to delete anything.
75
+ if existing_json=$(crt inspect "$app_container" 2>/dev/null); then
76
+ existing_ok=$(printf '%s' "$existing_json" \
77
+ | jq -r --arg name "$app_container" --arg tid "$task_id" '
78
+ if (type == "array" and length == 1
79
+ and (.[0] | type == "object")
80
+ and .[0].id == $name
81
+ and ((.[0].configuration.labels["io.moondeck.task"] // "") == $tid))
82
+ then "ok" else "mismatch" end' 2>/dev/null) \
83
+ || existing_ok="mismatch"
84
+ if [ "$existing_ok" != "ok" ]; then
85
+ emit_err "COMPOSE_DOWN_FAILED" \
86
+ "A container named $app_container exists but could not be proven as this task's own (exact identity + ownership label); Moondeck will not delete a container it cannot prove it owns. Remove or rename it, then retry." \
87
+ "remove task container"
88
+ fi
89
+ crt stop --time 10 "$app_container" >/dev/null 2>&1 || true
90
+ crt delete --force "$app_container" >/dev/null 2>&1 || true
91
+ fi
92
+ # A derived image is task-private; removing it mirrors `down --rmi local`.
93
+ crt image delete "$derived_image" >/dev/null 2>&1 || true
94
+ # Destruction is not best-effort: prove the container is gone before the
95
+ # writable Git data below is deleted or success is reported. A CLI failure
96
+ # that does not name this container as not-found proves nothing — the
97
+ # workspace and its Git data must survive until absence is confirmed. The
98
+ # Apple runtime has no per-task networks or sidecars; its one per-task
99
+ # volume (the ADR-118 store) is removed under the same ownership proof
100
+ # below.
101
+ if crt inspect "$app_container" >/dev/null 2>&1; then
102
+ emit_err "COMPOSE_DOWN_FAILED" \
103
+ "task runtime resources remain after teardown" \
104
+ "container verification after cleanup"
105
+ fi
106
+ if ! apple_container_proven_absent "$app_container"; then
107
+ emit_err "COMPOSE_DOWN_FAILED" \
108
+ "could not verify task container removal; refusing to delete the task workspace" \
109
+ "container verification after cleanup"
110
+ fi
111
+ # ADR-118: the task's store volume. Deleting only after the container is
112
+ # proven absent releases the attachment; the same exact-identity +
113
+ # ownership-label discipline gates destruction, and absence must be proven
114
+ # before success is reported — a leaked volume defeats the orphan GC that
115
+ # keys on this exact name. A pre-ADR-118 task simply has no such volume
116
+ # and passes straight through the absence proof.
117
+ step "COMPOSE_DOWN_FAILED" "remove task store volume"
118
+ store_volume=$(apple_store_volume_for_task "$task_id")
119
+ if store_volume_json=$(crt volume inspect "$store_volume" 2>/dev/null); then
120
+ store_volume_ok=$(printf '%s' "$store_volume_json" \
121
+ | jq -r --arg name "$store_volume" --arg tid "$task_id" '
122
+ if (type == "array" and length == 1
123
+ and (.[0] | type == "object")
124
+ and .[0].id == $name
125
+ and ((.[0].configuration.labels["io.moondeck.task"] // "") == $tid))
126
+ then "ok" else "mismatch" end' 2>/dev/null) \
127
+ || store_volume_ok="mismatch"
128
+ if [ "$store_volume_ok" != "ok" ]; then
129
+ emit_err "COMPOSE_DOWN_FAILED" \
130
+ "A volume named $store_volume exists but could not be proven as this task's own (exact identity + ownership label); Moondeck will not delete a volume it cannot prove it owns. Remove or rename it, then retry." \
131
+ "remove task store volume"
132
+ fi
133
+ crt volume delete "$store_volume" >/dev/null 2>&1 || true
134
+ fi
135
+ if ! apple_volume_proven_absent "$store_volume"; then
136
+ emit_err "COMPOSE_DOWN_FAILED" \
137
+ "could not verify task store volume removal; refusing to delete the task workspace" \
138
+ "store volume verification after cleanup"
139
+ fi
140
+ else
141
+ uai_require_compose_plugin
142
+ step "COMPOSE_DOWN_FAILED" "docker compose down -v --rmi local"
143
+ if [ ! -L "$task_dir" ] && [ -f "$task_dir/.uai/docker-compose.yml" ]; then
144
+ docker compose -p "$compose_project" -f "$task_dir/.uai/docker-compose.yml" \
145
+ down -v --rmi local --remove-orphans >/dev/null 2>&1 || true
146
+ else
147
+ # Fallback: tear down by project name only.
148
+ docker compose -p "$compose_project" down -v --rmi local --remove-orphans >/dev/null 2>&1 || true
149
+ fi
150
+ # `compose down` is intentionally best-effort because an already-absent
151
+ # stack can make a config-less fallback fail. Destruction itself is not
152
+ # best-effort: before deleting the worktree or reporting success, prove that
153
+ # no task-owned container/network/volume remains under exact labels.
154
+ preview_containers=""
155
+ if ! preview_containers=$(docker ps -aq \
156
+ --filter "label=io.moondeck.preview-sidecar.task=$task_id"); then
157
+ emit_err "COMPOSE_DOWN_FAILED" \
158
+ "could not verify preview teardown for task $task_id" \
159
+ "docker ps after preview teardown"
160
+ fi
161
+ if [ -n "$preview_containers" ]; then
162
+ # Docker ids contain no whitespace; intentional splitting passes each exact
163
+ # id as its own argv entry.
164
+ # shellcheck disable=SC2086
165
+ docker rm -f $preview_containers >/dev/null 2>&1 || \
166
+ emit_err "COMPOSE_DOWN_FAILED" "could not remove task preview sidecars" "docker rm previews"
167
+ fi
168
+
169
+ remaining_containers=""
170
+ if ! remaining_containers=$(docker ps -aq \
171
+ --filter "label=com.docker.compose.project=$compose_project"); then
172
+ emit_err "COMPOSE_DOWN_FAILED" \
173
+ "could not verify teardown for compose project $compose_project" \
174
+ "docker ps after compose down"
175
+ fi
176
+ if [ -n "$remaining_containers" ]; then
177
+ # shellcheck disable=SC2086
178
+ docker rm -f $remaining_containers >/dev/null 2>&1 || \
179
+ emit_err "COMPOSE_DOWN_FAILED" "could not remove compose containers" "docker rm after compose down"
180
+ fi
181
+
182
+ remaining_networks=""
183
+ if ! remaining_networks=$(docker network ls -q \
184
+ --filter "label=com.docker.compose.project=$compose_project"); then
185
+ emit_err "COMPOSE_DOWN_FAILED" \
186
+ "could not verify compose networks for $compose_project" \
187
+ "docker network ls after compose down"
188
+ fi
189
+ if [ -n "$remaining_networks" ]; then
190
+ # shellcheck disable=SC2086
191
+ docker network rm $remaining_networks >/dev/null 2>&1 || \
192
+ emit_err "COMPOSE_DOWN_FAILED" "could not remove compose networks" "docker network rm"
193
+ fi
194
+
195
+ remaining_volumes=""
196
+ if ! remaining_volumes=$(docker volume ls -q \
197
+ --filter "label=com.docker.compose.project=$compose_project"); then
198
+ emit_err "COMPOSE_DOWN_FAILED" \
199
+ "could not verify compose volumes for $compose_project" \
200
+ "docker volume ls after compose down"
201
+ fi
202
+ if [ -n "$remaining_volumes" ]; then
203
+ # shellcheck disable=SC2086
204
+ docker volume rm $remaining_volumes >/dev/null 2>&1 || \
205
+ emit_err "COMPOSE_DOWN_FAILED" "could not remove compose volumes" "docker volume rm"
206
+ fi
207
+
208
+ # Re-query every exact label. A successful remove command is not itself proof,
209
+ # and a failed query is unknown rather than empty.
210
+ verified_previews=""
211
+ verified_containers=""
212
+ verified_networks=""
213
+ verified_volumes=""
214
+ if ! verified_previews=$(docker ps -aq --filter "label=io.moondeck.preview-sidecar.task=$task_id") ||
215
+ ! verified_containers=$(docker ps -aq --filter "label=com.docker.compose.project=$compose_project") ||
216
+ ! verified_networks=$(docker network ls -q --filter "label=com.docker.compose.project=$compose_project") ||
217
+ ! verified_volumes=$(docker volume ls -q --filter "label=com.docker.compose.project=$compose_project"); then
218
+ emit_err "COMPOSE_DOWN_FAILED" \
219
+ "could not verify task runtime cleanup" \
220
+ "docker label verification after cleanup"
221
+ fi
222
+ if [ -n "$verified_previews" ] || [ -n "$verified_containers" ] ||
223
+ [ -n "$verified_networks" ] || [ -n "$verified_volumes" ]; then
224
+ emit_err "COMPOSE_DOWN_FAILED" \
225
+ "task runtime resources remain after teardown" \
226
+ "docker label verification after cleanup"
227
+ fi
228
+ fi
229
+
230
+ # -----------------------------------------------------------------------------
231
+ # 2. Remove the entire task directory (workspace + task-private Git data).
232
+ # No host Git command reads a repository that the just-removed container could
233
+ # have modified. Legacy shared-mirror worktree metadata may remain stale, but
234
+ # that retired `repo.git` path is never trusted by task-up again.
235
+ # -----------------------------------------------------------------------------
236
+
237
+ if [ -e "$task_dir" ] || [ -L "$task_dir" ]; then
238
+ step "WORKTREE_REMOVE_FAILED" "rm -rf task dir"
239
+ if ! rm -rf "$task_dir" 2>/dev/null \
240
+ && [ "$TASK_RUNTIME_MODE" != "apple-container" ] \
241
+ && [ ! -L "$task_dir" ] && [ -d "$task_dir" ]; then
242
+ # On Linux the task's container writes its files as node (1000) into the
243
+ # bind-mounted workspace, in directories without group write. A host user
244
+ # that is not uid 1000 (an OrbStack VM's 501, say) then can't remove them:
245
+ # "Permission denied", and the task stayed `stopped` (2026-11-01
246
+ # rehearsal). Remove what is left as root in a throwaway container that
247
+ # sees only this task directory: no network, every capability dropped but
248
+ # the two that deleting another user's files needs, and `find -delete`,
249
+ # which never follows a link out of the mount. The image runs by the ID
250
+ # its tag names right now, so the tag can't be swapped between the look
251
+ # and the run; without a well-formed ID nothing runs, and the absence
252
+ # check below reports the leftover.
253
+ step "WORKTREE_REMOVE_FAILED" "remove the task container's files"
254
+ cleanup_image_id="$(docker image inspect --format '{{.Id}}' "$TASK_CLEANUP_IMAGE" 2>/dev/null || true)"
255
+ if printf '%s\n' "$cleanup_image_id" | grep -Eqx 'sha256:[0-9a-f]{64}'; then
256
+ docker run --rm --pull never --network none --user 0:0 \
257
+ --cap-drop ALL --cap-add DAC_OVERRIDE --cap-add FOWNER \
258
+ --security-opt no-new-privileges --entrypoint /usr/bin/find \
259
+ -v "$task_dir:/task" "$cleanup_image_id" /task -mindepth 1 -delete \
260
+ >/dev/null 2>&1 || true
261
+ fi
262
+ rm -rf "$task_dir" 2>/dev/null || true
263
+ fi
264
+ fi
265
+ [ ! -e "$task_dir" ] && [ ! -L "$task_dir" ] || \
266
+ emit_err "WORKTREE_REMOVE_FAILED" "task directory remains after teardown" "task dir verification"
267
+
268
+ # -----------------------------------------------------------------------------
269
+ # 3. Persist final state.
270
+ # -----------------------------------------------------------------------------
271
+
272
+ current_status=$(jq -r '.[0].status // ""' <<<"$task_json")
273
+ if [ "$current_status" != "shipped" ] && [ "$current_status" != "error" ]; then
274
+ new_status="killed"
275
+ else
276
+ new_status="$current_status"
277
+ fi
278
+
279
+ step "DB_UPDATE_FAILED" "persist final state"
280
+ db_update_task "$task_id" \
281
+ "status='$new_status'" \
282
+ "code_server_port=NULL" \
283
+ "preview_ports='[]'" \
284
+ "compose_project=NULL" \
285
+ "worktree_path=NULL" \
286
+ "locked_at=NULL" \
287
+ "ended_at=(unixepoch()*1000)"
288
+
289
+ emit_event "$task_id" "status_changed" "$(jq -nc --arg f "$current_status" --arg t "$new_status" '{from:$f,to:$t}')"
290
+
291
+ emit_ok "$(jq -nc --arg s "$new_status" '{status:$s}')"
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # task-status <taskId> — introspect the actual runtime state.
3
+ #
4
+ # Returns the truth from docker / git / process listings rather than
5
+ # trusting the Task row. Used by POST /api/tasks/:id/reconcile to detect
6
+ # drift after a manual `docker compose down` or similar.
7
+
8
+ set -euo pipefail
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ # shellcheck source=_common.sh
11
+ . "$SCRIPT_DIR/_common.sh"
12
+
13
+ [ "$#" -eq 1 ] || emit_err "BAD_ARGS" "usage: task-status <taskId>" "args"
14
+
15
+ task_id="$1"
16
+ UAI_TASK_ID="$task_id"
17
+ setup_err_trap
18
+
19
+ step "TASK_NOT_FOUND" "read task row"
20
+ validate_task_runtime_selection
21
+
22
+ task_json=$(db_get_task "$task_id")
23
+ [ "$(jq 'length' <<<"$task_json")" -ge 1 ] \
24
+ || emit_err "TASK_NOT_FOUND" "no such task: $task_id" "read task row"
25
+
26
+ compose_project=$(jq -r '.[0].compose_project // ""' <<<"$task_json")
27
+ worktree_path=$(jq -r '.[0].worktree_path // ""' <<<"$task_json")
28
+
29
+ # Containers running under this task, JSON array of names.
30
+ step "DOCKER_PS_FAILED" "docker ps"
31
+ containers_json="[]"
32
+ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
33
+ # Absence (inspect exit 1) is a definitive "not running" answer, never an
34
+ # unknown-state error; only a live "running" state lists the container.
35
+ apple_app=$(apple_app_container_for_task "$task_id")
36
+ apple_state=$(crt inspect "$apple_app" 2>/dev/null \
37
+ | jq -r '.[0].status.state // empty' 2>/dev/null) || apple_state=""
38
+ if [ "$apple_state" = "running" ]; then
39
+ containers_json=$(jq -nc --arg n "$apple_app" '[$n]')
40
+ fi
41
+ elif [ -n "$compose_project" ]; then
42
+ if ! raw=$(docker ps --filter "label=com.docker.compose.project=$compose_project" --format '{{.Names}}'); then
43
+ jq -nc \
44
+ --arg m "docker ps did not answer; runtime state is unknown" \
45
+ '{ok:false,error:{code:"DOCKER_PS_FAILED",message:$m,step:"docker ps"}}' >&2
46
+ exit 1
47
+ fi
48
+ if [ -n "$raw" ]; then
49
+ containers_json=$(printf '%s\n' "$raw" | jq -R . | jq -sc .)
50
+ fi
51
+ fi
52
+
53
+ # Compose running iff at least one container is up.
54
+ if [ "$containers_json" = "[]" ]; then
55
+ compose_running="false"
56
+ else
57
+ compose_running="true"
58
+ fi
59
+
60
+ # Worktree present iff the directory exists.
61
+ worktree_present="false"
62
+ if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
63
+ worktree_present="true"
64
+ fi
65
+
66
+ emit_ok "$(jq -nc \
67
+ --argjson cr "$compose_running" \
68
+ --argjson cs "$containers_json" \
69
+ --argjson wp "$worktree_present" \
70
+ '{composeRunning:$cr,containers:$cs,worktreePresent:$wp}')"