@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,166 @@
1
+ import type {
2
+ ProjectEnvMode,
3
+ ProjectEnvProfile,
4
+ } from "./task-environment/types";
5
+
6
+ /**
7
+ * Host-owned runtime/cache authority for every command launched in a task
8
+ * container. Admission and recovery separately prove the container's fixed
9
+ * Config.Env; these values are reasserted for each managed exec so a caller
10
+ * cannot accidentally omit the policy or inherit project-controlled values.
11
+ */
12
+ export const RUNTIME_AUTHORITY_ENV: Readonly<Record<string, string>> =
13
+ Object.freeze({
14
+ // Shared, host-maintained shims stay ahead of task-installed commands so
15
+ // `npm install -g claude/codex/node` cannot replace Uai's proved tools.
16
+ // Packages with new command names remain available from the private
17
+ // container-layer prefix at the final component, after every trusted
18
+ // control directory.
19
+ PATH: "/opt/asdf-data/shims:/opt/asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/share/uai-npm/bin",
20
+ HOME: "/home/node",
21
+ XDG_CONFIG_HOME: "/home/node/.config",
22
+ XDG_DATA_HOME: "/home/node/.local/share",
23
+ XDG_CACHE_HOME: "/home/node/.cache",
24
+ ASDF_DIR: "/opt/asdf",
25
+ ASDF_DATA_DIR: "/opt/asdf-data",
26
+ ASDF_CONFIG_FILE: "/dev/null",
27
+ ASDF_DEFAULT_TOOL_VERSIONS_FILENAME: ".tool-versions",
28
+ ASDF_NODEJS_VERSION: "",
29
+ ASDF_PYTHON_VERSION: "",
30
+ ASDF_GOLANG_VERSION: "",
31
+ ASDF_RUBY_VERSION: "",
32
+ ASDF_RUST_VERSION: "",
33
+ // asdf-nodejs wraps global npm installs with an automatic reshim. Task
34
+ // mounts are intentionally read-only and private bins are already on PATH;
35
+ // only host maintenance may mutate shared shims.
36
+ ASDF_SKIP_RESHIM: "1",
37
+ // Corepack selections are completely prepared by host maintenance. Task
38
+ // contexts see this path only through the read-only shared volume.
39
+ COREPACK_HOME: "/opt/asdf-data/corepack",
40
+ COREPACK_DEFAULT_TO_LATEST: "0",
41
+ COREPACK_ENV_FILE: "0",
42
+ COREPACK_NPM_REGISTRY: "https://registry.npmjs.org",
43
+ COREPACK_ENABLE_NETWORK: "0",
44
+ COREPACK_ENABLE_AUTO_PIN: "0",
45
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
46
+ COREPACK_ENABLE_PROJECT_SPEC: "1",
47
+ COREPACK_ENABLE_STRICT: "1",
48
+ COREPACK_ENABLE_UNSAFE_CUSTOM_URLS: "0",
49
+ COREPACK_INTEGRITY_CHECK: "1",
50
+ // npm treats npm_config_* names case-insensitively and lifecycle scripts
51
+ // often receive the lowercase spelling. Pin both to task-private storage
52
+ // so image/project environment cannot turn an ordinary global install
53
+ // into a shared-cache writer.
54
+ NPM_CONFIG_PREFIX: "/home/node/.local/share/uai-npm",
55
+ npm_config_prefix: "/home/node/.local/share/uai-npm",
56
+ NPM_CONFIG_CACHE: "/home/node/.cache/uai-npm",
57
+ npm_config_cache: "/home/node/.cache/uai-npm",
58
+ NPM_CONFIG_LOGS_DIR: "/home/node/.cache/uai-npm/_logs",
59
+ npm_config_logs_dir: "/home/node/.cache/uai-npm/_logs",
60
+ NPM_CONFIG_UMASK: "0002",
61
+ npm_config_umask: "0002",
62
+ LD_PRELOAD: "",
63
+ LD_AUDIT: "",
64
+ LD_LIBRARY_PATH: "",
65
+ NODE_OPTIONS: "",
66
+ NODE_PATH: "",
67
+ // SHELLOPTS/BASHOPTS deliberately absent: present-in-env (even empty)
68
+ // makes bash export its live options into children — the Editor-terminal
69
+ // nounset poisoning of 2026-08-16.
70
+ PS4: "",
71
+ BASH_ENV: "",
72
+ ENV: "",
73
+ });
74
+
75
+ /**
76
+ * ADR-121: the MACHINE flavor of the authority env. A machine is a
77
+ * single-task trust domain — the container constants above defend SHARED
78
+ * host resources (the read-only asdf/corepack volume, uai-npm prefixes)
79
+ * that do not exist on a machine, and carrying them anyway pinned corepack
80
+ * to an unwritable /opt path with the network off, so `pnpm install`
81
+ * failed out of the box (live 2026-08-27, first EC2 task — the agent had
82
+ * to hand-repair its own environment). Keep the hygiene (loader/env
83
+ * clearing, prompt-free corepack), drop the container-topology pins.
84
+ */
85
+ export const MACHINE_RUNTIME_AUTHORITY_ENV: Readonly<Record<string, string>> =
86
+ Object.freeze({
87
+ PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/bin",
88
+ HOME: "/home/node",
89
+ XDG_CONFIG_HOME: "/home/node/.config",
90
+ XDG_DATA_HOME: "/home/node/.local/share",
91
+ XDG_CACHE_HOME: "/home/node/.cache",
92
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
93
+ LD_PRELOAD: "",
94
+ LD_AUDIT: "",
95
+ LD_LIBRARY_PATH: "",
96
+ NODE_OPTIONS: "",
97
+ NODE_PATH: "",
98
+ PS4: "",
99
+ BASH_ENV: "",
100
+ ENV: "",
101
+ });
102
+
103
+ /** Root-owned loader paths installed in every PR2+ task environment. */
104
+ export const PROJECT_ENV_HOOK_PATH = "/usr/local/share/uai/env-hook.sh";
105
+ export const PROJECT_ENV_EXEC_PATH = "/usr/local/bin/uai-env-exec";
106
+
107
+ /** Names that must reach a provider-added outer shell as well as the payload. */
108
+ export const PROJECT_ENV_PROFILE_ENV_NAMES = Object.freeze([
109
+ "BASH_ENV",
110
+ "SHELL",
111
+ "UAI_ENV_HOOK",
112
+ ] as const);
113
+
114
+ /**
115
+ * Apply the caller's classified project-env policy at the final provider
116
+ * boundary. The overlay is deliberately last, so inherited/caller env cannot
117
+ * opt a maintenance command into the hook or replace the task login shell.
118
+ * Legacy baked environments are a rollout no-op: their image has no loader.
119
+ */
120
+ export function applyProjectEnvProfile<
121
+ T extends {
122
+ projectEnvProfile: ProjectEnvProfile;
123
+ env?: Readonly<Record<string, string>>;
124
+ },
125
+ >(
126
+ request: T,
127
+ envMode: ProjectEnvMode,
128
+ taskShell: "/usr/bin/zsh" | "/bin/bash",
129
+ ): T {
130
+ if (envMode === "baked") return request;
131
+ const profileEnv =
132
+ request.projectEnvProfile === "maintenance"
133
+ ? { BASH_ENV: "", UAI_ENV_HOOK: "0" }
134
+ : {
135
+ BASH_ENV: PROJECT_ENV_HOOK_PATH,
136
+ SHELL: taskShell,
137
+ UAI_ENV_HOOK: "1",
138
+ };
139
+ return { ...request, env: { ...request.env, ...profileEnv } };
140
+ }
141
+
142
+ /** Rewrite a session env assembled for a CONTAINER into its machine form:
143
+ * every container-authority key is dropped, then the machine authority is
144
+ * overlaid. Non-authority keys (engine credentials, task tokens, preview
145
+ * env) pass through untouched. */
146
+ export function machineSessionAuthorityEnv(
147
+ env: Readonly<Record<string, string>> | undefined,
148
+ ): Record<string, string> {
149
+ const out: Record<string, string> = {};
150
+ for (const [key, value] of Object.entries(env ?? {})) {
151
+ if (key in RUNTIME_AUTHORITY_ENV) continue;
152
+ out[key] = value;
153
+ }
154
+ return { ...out, ...MACHINE_RUNTIME_AUTHORITY_ENV };
155
+ }
156
+
157
+ /** Docker `exec` options for the fixed authority environment. Callers append
158
+ * this after every less-trusted `-e` option and immediately before the
159
+ * container name. Container admission additionally rejects noncanonical
160
+ * case/underscore/hyphen aliases for npm's host-owned path settings. */
161
+ export function runtimeAuthorityDockerExecArgs(): string[] {
162
+ return Object.entries(RUNTIME_AUTHORITY_ENV).flatMap(([key, value]) => [
163
+ "-e",
164
+ `${key}=${value}`,
165
+ ]);
166
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Process-wide, dependency-light admission seam for background work that can
3
+ * reach Docker without entering through HostCommands. It defaults open so
4
+ * isolated library tests and non-daemon embeddings preserve their historical
5
+ * behavior; main.ts installs the authoritative ADR-101 readiness predicate.
6
+ */
7
+ let containersOperational = (): boolean => true;
8
+
9
+ /**
10
+ * A typed failure for work that reached its last Docker boundary after the
11
+ * process-wide runtime verdict was invalidated. Callers that otherwise use a
12
+ * boolean for an ordinary domain result (notably GitHub's disconnected state)
13
+ * must be able to keep infrastructure unavailability distinct.
14
+ */
15
+ export class ContainerRuntimeUnavailableError extends Error {
16
+ constructor(message = "container runtime is not operational") {
17
+ super(message);
18
+ this.name = "ContainerRuntimeUnavailableError";
19
+ }
20
+ }
21
+
22
+ export function setContainerRuntimeOperationalCheck(
23
+ check: () => boolean,
24
+ ): void {
25
+ containersOperational = check;
26
+ }
27
+
28
+ export function backgroundContainerWorkAllowed(): boolean {
29
+ return containersOperational();
30
+ }
31
+
32
+ export function requireContainerRuntimeOperational(): void {
33
+ if (!backgroundContainerWorkAllowed()) {
34
+ throw new ContainerRuntimeUnavailableError();
35
+ }
36
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Crash-safe identity for a container backend that may hold task state.
3
+ *
4
+ * The marker is deliberately monotonic. Once a bundled-runtime task can create
5
+ * a container, image, or volume, `auto` must not silently move the host back
6
+ * to an ambient Docker Desktop/OrbStack daemon on the next service restart. An
7
+ * explicit `UAI_RUNTIME=docker` remains the operator's switch.
8
+ */
9
+
10
+ import { randomBytes } from "node:crypto";
11
+ import {
12
+ chmodSync,
13
+ closeSync,
14
+ existsSync,
15
+ fsyncSync,
16
+ linkSync,
17
+ lstatSync,
18
+ mkdirSync,
19
+ openSync,
20
+ readFileSync,
21
+ unlinkSync,
22
+ writeFileSync,
23
+ } from "node:fs";
24
+ import { basename, dirname, join } from "node:path";
25
+
26
+ export type StatefulRuntimeProvider = "apple-container";
27
+
28
+ export interface RuntimeProviderState {
29
+ readonly schemaVersion: 1;
30
+ readonly statefulProvider: StatefulRuntimeProvider;
31
+ readonly claimedAt: number;
32
+ }
33
+
34
+ export interface RuntimeProviderStateStore {
35
+ read(): RuntimeProviderState | null | Promise<RuntimeProviderState | null>;
36
+ claim(
37
+ provider: StatefulRuntimeProvider,
38
+ ): RuntimeProviderState | Promise<RuntimeProviderState>;
39
+ }
40
+
41
+ export function runtimeProviderStatePath(dataDir: string): string {
42
+ return join(dataDir, "container-runtime-provider.json");
43
+ }
44
+
45
+ export function fileRuntimeProviderStateStore(
46
+ file: string,
47
+ ): RuntimeProviderStateStore {
48
+ return {
49
+ read: () => readRuntimeProviderState(file),
50
+ claim: (provider) => claimRuntimeProviderState(file, provider),
51
+ };
52
+ }
53
+
54
+ export function readRuntimeProviderState(
55
+ file: string,
56
+ ): RuntimeProviderState | null {
57
+ if (!existsSync(file)) return null;
58
+ const stat = lstatSync(file);
59
+ if (!stat.isFile() || stat.isSymbolicLink()) {
60
+ throw new Error("container runtime provider state must be a regular file");
61
+ }
62
+ let value: unknown;
63
+ try {
64
+ value = JSON.parse(readFileSync(file, "utf8"));
65
+ } catch {
66
+ throw new Error("container runtime provider state is malformed");
67
+ }
68
+ return validateRuntimeProviderState(value);
69
+ }
70
+
71
+ /**
72
+ * Durably claim the first stateful fallback provider without an overwrite
73
+ * race. The marker is written before task provisioning can mutate that
74
+ * provider. A false-positive after an interrupted task-up is safe (and only
75
+ * makes `auto` conservative); a missing marker after VM state was created is
76
+ * not.
77
+ */
78
+ export function claimRuntimeProviderState(
79
+ file: string,
80
+ provider: StatefulRuntimeProvider,
81
+ ): RuntimeProviderState {
82
+ const existing = readRuntimeProviderState(file);
83
+ if (existing) {
84
+ if (existing.statefulProvider !== provider) {
85
+ throw new Error("container runtime provider is already claimed");
86
+ }
87
+ return existing;
88
+ }
89
+
90
+ const state: RuntimeProviderState = {
91
+ schemaVersion: 1,
92
+ statefulProvider: provider,
93
+ claimedAt: Date.now(),
94
+ };
95
+ const parent = dirname(file);
96
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
97
+ const temporary = join(
98
+ parent,
99
+ `.${basename(file)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`,
100
+ );
101
+ try {
102
+ writeDurableFile(temporary, `${JSON.stringify(state, null, 2)}\n`);
103
+ try {
104
+ linkSync(temporary, file);
105
+ fsyncDirectory(parent);
106
+ return state;
107
+ } catch (error) {
108
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
109
+ const winner = readRuntimeProviderState(file);
110
+ if (!winner) throw new Error("container runtime provider claim disappeared");
111
+ if (winner.statefulProvider !== provider) {
112
+ throw new Error("container runtime provider was claimed concurrently");
113
+ }
114
+ return winner;
115
+ }
116
+ } finally {
117
+ try {
118
+ unlinkSync(temporary);
119
+ } catch (error) {
120
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
121
+ }
122
+ }
123
+ }
124
+
125
+ function validateRuntimeProviderState(value: unknown): RuntimeProviderState {
126
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
127
+ throw new Error("container runtime provider state is malformed");
128
+ }
129
+ const record = value as Record<string, unknown>;
130
+ if (
131
+ Object.keys(record).length !== 3 ||
132
+ record.schemaVersion !== 1 ||
133
+ record.statefulProvider !== "apple-container" ||
134
+ typeof record.claimedAt !== "number" ||
135
+ !Number.isSafeInteger(record.claimedAt) ||
136
+ record.claimedAt <= 0
137
+ ) {
138
+ throw new Error("container runtime provider state is malformed");
139
+ }
140
+ return {
141
+ schemaVersion: 1,
142
+ statefulProvider: "apple-container",
143
+ claimedAt: record.claimedAt,
144
+ };
145
+ }
146
+
147
+ function writeDurableFile(file: string, body: string): void {
148
+ const descriptor = openSync(file, "wx", 0o600);
149
+ try {
150
+ writeFileSync(descriptor, body, "utf8");
151
+ fsyncSync(descriptor);
152
+ } finally {
153
+ closeSync(descriptor);
154
+ }
155
+ chmodSync(file, 0o600);
156
+ }
157
+
158
+ function fsyncDirectory(directory: string): void {
159
+ let descriptor: number | null = null;
160
+ try {
161
+ descriptor = openSync(directory, "r");
162
+ fsyncSync(descriptor);
163
+ } catch {
164
+ // Some filesystems do not expose directory fsync. The marker itself was
165
+ // still fsync'd before the atomic hard-link publication.
166
+ } finally {
167
+ if (descriptor !== null) closeSync(descriptor);
168
+ }
169
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * A known-ready container runtime revalidated when the bridge reconnects: a
3
+ * healthy daemon can still have restarted while the bridge was offline. One
4
+ * label scan keeps ordinary cloud/network flaps cheap; only a missing,
5
+ * exited or unknowable active app takes the full per-task recovery.
6
+ *
7
+ * ADR-154: it runs on the credentials it began on, as an activation does
8
+ * (credentialsActivationFence). Off the runner's own before this process's
9
+ * credential sweep has succeeded, or once leaving them begins, it neither
10
+ * recovers nor publishes: it hands the runtime to the credentials-aware
11
+ * activation, which sweeps first. Off them after the sweep, its recovery
12
+ * goes through the credentials wrapper (the sweep already done).
13
+ */
14
+
15
+ import {
16
+ cloudCredentialsSwept,
17
+ credentialsActivationFence,
18
+ onOwnCredentials,
19
+ recoverRuntimeOnCredentials,
20
+ } from "./cloud-credential-host";
21
+
22
+ export interface RuntimeReconnectDeps {
23
+ /** Suspends a ready runtime for revalidation; its epoch, or null. */
24
+ suspend(): number | null;
25
+ /** Probes the daemon again: ready or not. */
26
+ probe(): Promise<boolean>;
27
+ /** The readiness epoch is still this one, and the daemon ready. */
28
+ epochIsCurrent(epoch: number): boolean;
29
+ /** The epoch is current and the runner is not stopping. */
30
+ activationIsCurrent(epoch: number): boolean;
31
+ persistedEnvironmentsMatch(): Promise<boolean>;
32
+ recoveryVerdict(): Promise<"clean" | "needed" | "unknown">;
33
+ /** The orchestrator's per-task recovery. */
34
+ recover(isCurrent: () => boolean): Promise<boolean>;
35
+ /** A bounded activation retry for the epoch. */
36
+ scheduleRetry(epoch: number): void;
37
+ /** A full, credentials-aware activation pass for the epoch. */
38
+ requestActivation(epoch: number): void;
39
+ /** Marks the runtime operational for the epoch, resumes deferred agent
40
+ * work and re-advertises; false when it could not be marked. */
41
+ publish(epoch: number): boolean;
42
+ }
43
+
44
+ export async function revalidateRuntimeOnConnect(deps: RuntimeReconnectDeps): Promise<void> {
45
+ const epoch = deps.suspend();
46
+ if (epoch === null) return;
47
+ const fence = credentialsActivationFence(() => deps.activationIsCurrent(epoch));
48
+ const handOver = (): void => {
49
+ if (deps.activationIsCurrent(epoch)) deps.requestActivation(epoch);
50
+ };
51
+ /** Not current any more: leaving the runner's own credentials hands the
52
+ * runtime to the activation; a stale epoch ends here. */
53
+ const stopped = (): boolean => {
54
+ if (fence.isCurrent()) return false;
55
+ if (fence.leftLegacy()) handOver();
56
+ return true;
57
+ };
58
+ try {
59
+ if (!(await deps.probe()) || !deps.epochIsCurrent(epoch)) return;
60
+ if (!onOwnCredentials() && !cloudCredentialsSwept()) {
61
+ handOver();
62
+ return;
63
+ }
64
+ if (!(await deps.persistedEnvironmentsMatch())) return;
65
+ if (stopped()) return;
66
+ let recovered = (await deps.recoveryVerdict()) === "clean";
67
+ if (!recovered) {
68
+ recovered = await recoverRuntimeOnCredentials(deps.recover, () => fence.isCurrent());
69
+ }
70
+ if (stopped()) return;
71
+ // Prove the daemon once more after recovery so a mid-sweep loss cannot
72
+ // reopen commands and tunnels on an unresolved backend.
73
+ if (!(await deps.probe()) || !deps.epochIsCurrent(epoch)) return;
74
+ if (stopped()) return;
75
+ if (!recovered) {
76
+ deps.scheduleRetry(epoch);
77
+ return;
78
+ }
79
+ deps.publish(epoch);
80
+ } catch (error) {
81
+ console.warn(
82
+ `[host-agent] container runtime reconnect validation failed: ${error instanceof Error ? error.message : String(error)}`,
83
+ );
84
+ if (deps.activationIsCurrent(epoch)) deps.scheduleRetry(epoch);
85
+ }
86
+ }