@moondeck/station 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -2
  3. package/bin/uai-host.mjs +130 -0
  4. package/db/migrations/0000_host_tasks.sql +12 -0
  5. package/db/migrations/0001_host_ui.sql +11 -0
  6. package/db/migrations/0002_host_github_tokens.sql +8 -0
  7. package/db/migrations/0003_host_ssh_keys.sql +8 -0
  8. package/db/migrations/0004_host_owner_name.sql +1 -0
  9. package/db/migrations/0005_host_env_vars.sql +5 -0
  10. package/db/migrations/0006_host_project_env.sql +9 -0
  11. package/db/migrations/0007_host_github_token_kind.sql +7 -0
  12. package/db/migrations/0008_host_mcp_connections.sql +18 -0
  13. package/db/migrations/0009_host_agent_sessions.sql +15 -0
  14. package/db/migrations/0010_host_engine_accounts.sql +13 -0
  15. package/db/migrations/0011_codex_thread_id.sql +5 -0
  16. package/db/migrations/0012_agent_session_attach_key.sql +4 -0
  17. package/db/migrations/0013_github_credential_generations.sql +11 -0
  18. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  19. package/db/migrations/0015_host_settings.sql +9 -0
  20. package/db/migrations/0016_task_environment.sql +2 -0
  21. package/db/migrations/0017_engine_account_scope.sql +7 -0
  22. package/db/migrations/0018_engine_account_position.sql +3 -0
  23. package/db/migrations/0019_engine_account_store_ref.sql +3 -0
  24. package/db/migrations/0020_user_credential_store_ref.sql +6 -0
  25. package/db/migrations/0021_project_env_store_ref.sql +3 -0
  26. package/db/migrations/0022_task_project_ids.sql +5 -0
  27. package/db/migrations/0023_project_env_org.sql +4 -0
  28. package/db/migrations/0024_task_env_mode.sql +9 -0
  29. package/db/migrations/0025_project_env_authority.sql +147 -0
  30. package/db/migrations/0026_project_env_replica.sql +150 -0
  31. package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
  32. package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
  33. package/db/migrations/0029_task_shared_files_mode.sql +5 -0
  34. package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
  35. package/db/migrations/meta/_journal.json +223 -0
  36. package/db/schema.ts +850 -0
  37. package/images/standard/Dockerfile +347 -0
  38. package/images/standard/README.md +167 -0
  39. package/images/standard/container/code-server-settings.json +63 -0
  40. package/images/standard/container/corepack-version +1 -0
  41. package/images/standard/container/uai-init +704 -0
  42. package/images/standard/container/uai-materialize-runtimes +1527 -0
  43. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  44. package/images/standard/tool-versions +2 -0
  45. package/lib/__fixtures__/skill-index/SKILL.md +10 -0
  46. package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
  47. package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
  48. package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
  49. package/lib/agent-cli.ts +1507 -0
  50. package/lib/agent-errors.ts +22 -0
  51. package/lib/agent.ts +672 -0
  52. package/lib/agents/attach-compatibility.ts +51 -0
  53. package/lib/agents/claude.ts +918 -0
  54. package/lib/agents/codex-token-usage.fixture.json +130 -0
  55. package/lib/agents/codex.ts +1392 -0
  56. package/lib/agents/cursor-period-usage.ts +253 -0
  57. package/lib/agents/cursor.ts +562 -0
  58. package/lib/agents/dispatch.ts +78 -0
  59. package/lib/agents/durable-proc.ts +334 -0
  60. package/lib/agents/factory.ts +57 -0
  61. package/lib/agents/grok.ts +381 -0
  62. package/lib/agents/kimi.ts +329 -0
  63. package/lib/agents/machine-durable.ts +604 -0
  64. package/lib/agents/mock.ts +133 -0
  65. package/lib/agents/mode.ts +64 -0
  66. package/lib/agents/opencode.ts +508 -0
  67. package/lib/agents/output-source.ts +3 -0
  68. package/lib/agents/proc.ts +221 -0
  69. package/lib/agents/rate-limit.ts +337 -0
  70. package/lib/agents/registry.ts +182 -0
  71. package/lib/agents/session-state.ts +197 -0
  72. package/lib/agents/skill-index-preamble.fixture.ts +26 -0
  73. package/lib/agents/transport.ts +741 -0
  74. package/lib/agents/types.ts +254 -0
  75. package/lib/agents/uai.ts +106 -0
  76. package/lib/agents/usage.ts +44 -0
  77. package/lib/apple-one-off.ts +277 -0
  78. package/lib/apple-runtime-recycle.ts +237 -0
  79. package/lib/apple-uninstall-teardown.ts +224 -0
  80. package/lib/assignment-evidence.ts +94 -0
  81. package/lib/attachments.ts +48 -0
  82. package/lib/browser-testing.ts +1537 -0
  83. package/lib/capture-flow.ts +332 -0
  84. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  85. package/lib/cloud-credential-host.ts +512 -0
  86. package/lib/cloud-credential-leases.ts +2587 -0
  87. package/lib/cloud-credential-runtime.ts +1357 -0
  88. package/lib/cloud-state.ts +56 -0
  89. package/lib/codex-auth.ts +456 -0
  90. package/lib/command-db.ts +310 -0
  91. package/lib/compose-network-recovery.ts +283 -0
  92. package/lib/connection-secret-redaction.ts +25 -0
  93. package/lib/container-runtime.ts +1453 -0
  94. package/lib/credential-cleanup-inventory.ts +490 -0
  95. package/lib/credential-consumers/engine-anchor.ts +1880 -0
  96. package/lib/credential-consumers/engines.ts +797 -0
  97. package/lib/credential-consumers/files.ts +934 -0
  98. package/lib/credential-consumers/github.ts +702 -0
  99. package/lib/credential-consumers/mcp.ts +389 -0
  100. package/lib/credential-consumers/task-files.ts +127 -0
  101. package/lib/credential-consumers/task-secrets.ts +10 -0
  102. package/lib/credential-store-pickup.ts +149 -0
  103. package/lib/credential-store-scope.ts +112 -0
  104. package/lib/credential-store.ts +932 -0
  105. package/lib/credentials-activation-authority.ts +709 -0
  106. package/lib/credentials-purge.ts +1006 -0
  107. package/lib/custom-connection-capture.ts +395 -0
  108. package/lib/custom-connection-launch.ts +384 -0
  109. package/lib/custom-connection-runtime.ts +141 -0
  110. package/lib/db.ts +80 -0
  111. package/lib/disk-pressure.ts +42 -0
  112. package/lib/docker-exec.ts +156 -0
  113. package/lib/engine-accounts.ts +1970 -0
  114. package/lib/engine-connect.ts +321 -0
  115. package/lib/engine-credential-env.ts +52 -0
  116. package/lib/engine-login.ts +2933 -0
  117. package/lib/engines.ts +1342 -0
  118. package/lib/enrollment-state.ts +251 -0
  119. package/lib/env-file.ts +246 -0
  120. package/lib/env-schema.ts +891 -0
  121. package/lib/env.ts +195 -0
  122. package/lib/fs-aliases.ts +49 -0
  123. package/lib/git-clone-credentials.ts +334 -0
  124. package/lib/git-diff.ts +530 -0
  125. package/lib/git-identity.ts +715 -0
  126. package/lib/github-git-auth.ts +207 -0
  127. package/lib/github-origin.ts +28 -0
  128. package/lib/github-tokens.ts +2439 -0
  129. package/lib/host-cloud-url.ts +62 -0
  130. package/lib/host-config.ts +279 -0
  131. package/lib/host-env.ts +424 -0
  132. package/lib/host-logs.ts +965 -0
  133. package/lib/internal-connections.ts +5 -0
  134. package/lib/keyed-promise-tail.ts +23 -0
  135. package/lib/launch-mode.ts +25 -0
  136. package/lib/lease.ts +239 -0
  137. package/lib/legacy-host.ts +769 -0
  138. package/lib/legacy-runtime-retirement.ts +424 -0
  139. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  140. package/lib/machine-exec.ts +238 -0
  141. package/lib/machine-gateway-tunnel.ts +344 -0
  142. package/lib/machine-keys.ts +65 -0
  143. package/lib/machine-project-env-loader.ts +152 -0
  144. package/lib/machine-provider-aws.ts +923 -0
  145. package/lib/machine-provider-local.ts +338 -0
  146. package/lib/machine-provider.ts +90 -0
  147. package/lib/managed-activation-watcher.ts +72 -0
  148. package/lib/managed-install-owner-watcher.ts +55 -0
  149. package/lib/managed-operation-drain.ts +57 -0
  150. package/lib/managed-runtime.ts +4296 -0
  151. package/lib/managed-update-scheduler.ts +125 -0
  152. package/lib/mcp-capture.ts +865 -0
  153. package/lib/mcp-config-lock.ts +2 -0
  154. package/lib/mcp-connections.ts +959 -0
  155. package/lib/mcp-gateway.ts +2594 -0
  156. package/lib/obs.ts +512 -0
  157. package/lib/openrouter-budget.ts +247 -0
  158. package/lib/orchestrator.ts +11409 -0
  159. package/lib/person-capture.ts +327 -0
  160. package/lib/preview-ports.ts +85 -0
  161. package/lib/preview-sidecar.ts +331 -0
  162. package/lib/project-env-authority.ts +1319 -0
  163. package/lib/project-env-feature.ts +74 -0
  164. package/lib/project-env-legacy-tags.ts +130 -0
  165. package/lib/project-env-loader.ts +242 -0
  166. package/lib/project-env-replica.ts +2094 -0
  167. package/lib/project-env-schema-capture.ts +342 -0
  168. package/lib/project-env-staging.ts +148 -0
  169. package/lib/project-env-task-policy.ts +176 -0
  170. package/lib/project-env-task-replica.ts +1752 -0
  171. package/lib/release-feed.ts +61 -0
  172. package/lib/release-manifest.ts +710 -0
  173. package/lib/release-trust.ts +46 -0
  174. package/lib/repo-clone.ts +38 -0
  175. package/lib/routing/shadow.ts +261 -0
  176. package/lib/runner-boot.ts +10 -0
  177. package/lib/runtime-activation-tail.ts +279 -0
  178. package/lib/runtime-archive.ts +1216 -0
  179. package/lib/runtime-authority.ts +166 -0
  180. package/lib/runtime-guard.ts +36 -0
  181. package/lib/runtime-provider-state.ts +169 -0
  182. package/lib/runtime-reconnect.ts +86 -0
  183. package/lib/runtime-state.ts +1033 -0
  184. package/lib/safe-http.ts +364 -0
  185. package/lib/secrets-proxy/address.ts +95 -0
  186. package/lib/secrets-proxy/audit.ts +30 -0
  187. package/lib/secrets-proxy/ca-store.ts +200 -0
  188. package/lib/secrets-proxy/ca.ts +470 -0
  189. package/lib/secrets-proxy/container-steps.ts +138 -0
  190. package/lib/secrets-proxy/der.ts +273 -0
  191. package/lib/secrets-proxy/engine.ts +1633 -0
  192. package/lib/secrets-proxy/host.ts +93 -0
  193. package/lib/secrets-proxy/hostname.ts +57 -0
  194. package/lib/secrets-proxy/placeholders.ts +196 -0
  195. package/lib/secrets-proxy/policy.ts +150 -0
  196. package/lib/secrets-proxy/readiness.ts +148 -0
  197. package/lib/secrets-proxy/runtime.ts +582 -0
  198. package/lib/secrets-proxy/scrub.ts +559 -0
  199. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  200. package/lib/secrets-proxy/task-scripts.ts +262 -0
  201. package/lib/secrets-proxy/task-trust.ts +605 -0
  202. package/lib/secrets-proxy/trust-watch.ts +80 -0
  203. package/lib/secrets.ts +71 -0
  204. package/lib/shared-files-s3.ts +976 -0
  205. package/lib/shared-files.ts +156 -0
  206. package/lib/sidecar-assign.ts +49 -0
  207. package/lib/sidecar-guard.ts +58 -0
  208. package/lib/sidecar-resume.ts +178 -0
  209. package/lib/sidecar.ts +69 -0
  210. package/lib/signin-box-credentials.ts +465 -0
  211. package/lib/signin-box.ts +114 -0
  212. package/lib/skill-index-example.fixture.ts +56 -0
  213. package/lib/skill-layout.ts +417 -0
  214. package/lib/skills.ts +489 -0
  215. package/lib/ssh.ts +401 -0
  216. package/lib/standard-image-cleanup.ts +117 -0
  217. package/lib/standard-image-pin.ts +207 -0
  218. package/lib/standard-image.ts +1697 -0
  219. package/lib/stopped-task-status-queue.ts +120 -0
  220. package/lib/task-container-cli.ts +269 -0
  221. package/lib/task-credential-cleanup.ts +752 -0
  222. package/lib/task-diff-sandbox.ts +464 -0
  223. package/lib/task-diff.ts +91 -0
  224. package/lib/task-env-forwarder.ts +228 -0
  225. package/lib/task-env-source.ts +249 -0
  226. package/lib/task-environment/anchor.ts +31 -0
  227. package/lib/task-environment/apple-container.ts +860 -0
  228. package/lib/task-environment/docker.ts +1344 -0
  229. package/lib/task-environment/index.ts +466 -0
  230. package/lib/task-environment/legacy-adoption.ts +467 -0
  231. package/lib/task-environment/machine-environment.ts +61 -0
  232. package/lib/task-environment/machine-task-up.ts +1227 -0
  233. package/lib/task-environment/machine.ts +974 -0
  234. package/lib/task-environment/registry.ts +59 -0
  235. package/lib/task-environment/self-identity.ts +20 -0
  236. package/lib/task-environment/self.ts +566 -0
  237. package/lib/task-environment/types.ts +642 -0
  238. package/lib/task-environment/workspace-files.ts +685 -0
  239. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  240. package/lib/task-files-machine-retirement-outbox.ts +592 -0
  241. package/lib/task-files-machine-retirement.ts +116 -0
  242. package/lib/task-files-readiness.ts +127 -0
  243. package/lib/task-gateway-port.ts +22 -0
  244. package/lib/task-gateway-protocol.ts +190 -0
  245. package/lib/task-gateway-recovery.ts +47 -0
  246. package/lib/task-gateway-state.ts +512 -0
  247. package/lib/task-identity.ts +19 -0
  248. package/lib/task-inventory.ts +682 -0
  249. package/lib/task-machine-credentials-activation.ts +95 -0
  250. package/lib/task-machine-swap-safety.ts +729 -0
  251. package/lib/task-person-credentials.ts +76 -0
  252. package/lib/task-project-ops-host.ts +147 -0
  253. package/lib/task-project-ops.ts +1062 -0
  254. package/lib/task-secret-staging.ts +16 -0
  255. package/lib/task-status.ts +46 -0
  256. package/lib/task-token.ts +46 -0
  257. package/lib/token-broker-adopt.ts +344 -0
  258. package/lib/token-broker-lambda.ts +105 -0
  259. package/lib/token-broker-local.ts +392 -0
  260. package/lib/token-broker-revision.ts +310 -0
  261. package/lib/token-broker-ssm.ts +525 -0
  262. package/lib/token-broker.ts +218 -0
  263. package/lib/token-lanes/openai-oauth.ts +123 -0
  264. package/lib/token-lanes/registry.ts +45 -0
  265. package/lib/token-lanes/types.ts +163 -0
  266. package/lib/token-lanes/xai-oauth.ts +165 -0
  267. package/lib/transcript.ts +128 -0
  268. package/lib/tunnel-registry.ts +220 -0
  269. package/lib/tunnel-runtime.ts +235 -0
  270. package/lib/ulid.ts +16 -0
  271. package/lib/user-bus-env.ts +116 -0
  272. package/lib/user-credentials-store.ts +435 -0
  273. package/lib/vnode-pressure.ts +142 -0
  274. package/package.json +131 -4
  275. package/runner/runner.mjs +442 -0
  276. package/scripts/agent/_common.sh +633 -0
  277. package/scripts/agent/env-hook.sh +626 -0
  278. package/scripts/agent/moondeck-cli.sh +5 -0
  279. package/scripts/agent/task-down.sh +291 -0
  280. package/scripts/agent/task-status.sh +70 -0
  281. package/scripts/agent/task-up.sh +3761 -0
  282. package/scripts/agent/uai-env-exec.sh +26 -0
  283. package/scripts/install/darwin.ts +1048 -0
  284. package/scripts/install/linux.ts +958 -0
  285. package/scripts/install/types.ts +78 -0
  286. package/scripts/install/util.ts +254 -0
  287. package/scripts/install/win.ts +143 -0
  288. package/src/activation-respawn.ts +155 -0
  289. package/src/apple-tunnel-route.ts +104 -0
  290. package/src/capture-wire.ts +102 -0
  291. package/src/cli.ts +2583 -0
  292. package/src/cloud-credentials.ts +2144 -0
  293. package/src/cloud-origins.ts +23 -0
  294. package/src/credential-location.ts +248 -0
  295. package/src/credential-wire-validation.ts +25 -0
  296. package/src/custom-connections.ts +237 -0
  297. package/src/engine-account-paths.ts +11 -0
  298. package/src/engine-models.ts +39 -0
  299. package/src/engine-usage.ts +104 -0
  300. package/src/event-outbox.ts +185 -0
  301. package/src/index.ts +2899 -0
  302. package/src/load-env.ts +97 -0
  303. package/src/machine-tunnel-route.ts +156 -0
  304. package/src/main.ts +6349 -0
  305. package/src/never-proxied-domains.ts +31 -0
  306. package/src/outbound-frame.ts +31 -0
  307. package/src/paths.ts +81 -0
  308. package/src/project-env-latest.ts +130 -0
  309. package/src/project-env-policy.ts +396 -0
  310. package/src/project-env-wording.ts +53 -0
  311. package/src/protocol.ts +5545 -0
  312. package/src/runtime-bootstrap.ts +165 -0
  313. package/src/station-identity.ts +96 -0
  314. package/src/task-env-names.ts +28 -0
  315. package/src/task-secrets.ts +75 -0
  316. package/src/tunnel-dial.ts +102 -0
  317. package/src/ui/server.ts +852 -0
  318. package/src/ui/types.ts +173 -0
  319. package/src/workspace-path-policy.ts +41 -0
  320. package/ui/app.js +1102 -0
  321. package/ui/index.html +90 -0
  322. package/ui/style.css +685 -0
  323. package/ui/uai-favicon.svg +21 -0
  324. package/ui/uai-logo-black.svg +9 -0
  325. package/ui/uai-wheel.svg +9 -0
@@ -0,0 +1,704 @@
1
+ #!/bin/bash
2
+ # uai-init — prepare a standard-image task container (ADR-022).
3
+ #
4
+ # Runs INSIDE the container (Linux, bash 5 — may use anything). Invoked by
5
+ # the host via `docker exec <app> /usr/local/bin/uai-init` after compose up.
6
+ # Spec: ../../../docs/runtime.md §uai-init.
7
+ #
8
+ # Workspace layout (ADR-022): /workspace/<project-slug>/ — one git worktree
9
+ # per selected project. A 0-project (scratchpad) task has an empty /workspace.
10
+ #
11
+ # Responsibilities:
12
+ # 1. For each /workspace/<slug>/ project, run a dependency-install heuristic
13
+ # for the folder's stack. task-up has already materialized and verified
14
+ # its declared runtimes or the image's root defaults before this task
15
+ # container starts; this script never mutates the shared asdf volume.
16
+ # 2. Launch code-server on 0.0.0.0:8080 (auth disabled — reached only via
17
+ # the authenticated tunnel), opening /workspace.
18
+ #
19
+ # Best-effort + idempotent: an ordinary failing folder logs and is skipped.
20
+ # Re-running when things are already up is a no-op. A host runtime-authority
21
+ # violation exits 78 so task-up/recovery can quarantine the task instead of
22
+ # starting agents against a runtime context that was never proved.
23
+
24
+ set -uo pipefail
25
+
26
+ WORKSPACE="${UAI_WORKSPACE:-/workspace}"
27
+ RUNTIME_PROJECTS="${UAI_RUNTIME_PROJECTS:-/run/uai/runtime-projects}"
28
+
29
+ # Compose owns runtime/cache selection. Reassert it here for rolling recovery
30
+ # and remove every inherited asdf/Corepack/npm authority variable before any
31
+ # dependency command resolves a runtime. No command in this script installs or
32
+ # reshims. Direct source-tree tests may prepend stubs; the packaged path never
33
+ # honors those test-only seams. Bash does not import environment names that
34
+ # contain `-`, but npm treats LOGS-DIR and LOGS_DIR as the same setting. Strip
35
+ # every owned spelling with GNU/BSD env before continuing, then export only
36
+ # Uai's exact canonical names. Unrelated registry/auth npm config survives.
37
+ npm_sanitize_args=()
38
+ while IFS= read -r -d '' environment_entry; do
39
+ environment_name=${environment_entry%%=*}
40
+ case "$environment_name" in
41
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Pp][Rr][Ee][Ff][Ii][Xx] | \
42
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Cc][Aa][Cc][Hh][Ee] | \
43
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Ll][Oo][Gg][Ss][_-][Dd][Ii][Rr] | \
44
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Uu][Mm][Aa][Ss][Kk])
45
+ npm_sanitize_args+=( -u "$environment_name" )
46
+ ;;
47
+ esac
48
+ done < <(/usr/bin/env -0)
49
+ if [ "${#npm_sanitize_args[@]}" -gt 0 ]; then
50
+ exec /usr/bin/env "${npm_sanitize_args[@]}" \
51
+ /bin/bash "${BASH_SOURCE[0]}" "$@"
52
+ fi
53
+
54
+ # Parse the role only after the one-time sanitized re-exec above. Parsing it
55
+ # first would shift away --dependency-worker, causing the clean process to
56
+ # restart as the main/editor role whenever inherited npm authority was
57
+ # present (the normal task environment).
58
+ UAI_INIT_ROLE=main
59
+ case ${1-} in
60
+ --dependency-worker)
61
+ UAI_INIT_ROLE=dependency-worker
62
+ shift
63
+ ;;
64
+ "") ;;
65
+ *)
66
+ printf '%s\n' 'uai-init: invalid invocation' >&2
67
+ exit 64
68
+ ;;
69
+ esac
70
+ [ "$#" -eq 0 ] || {
71
+ printf '%s\n' 'uai-init: invalid invocation' >&2
72
+ exit 64
73
+ }
74
+
75
+ for runtime_var in $(compgen -A variable 2>/dev/null); do
76
+ case "$runtime_var" in
77
+ ASDF_* | COREPACK_* | \
78
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Pp][Rr][Ee][Ff][Ii][Xx] | \
79
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Cc][Aa][Cc][Hh][Ee] | \
80
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Ll][Oo][Gg][Ss]_[Dd][Ii][Rr] | \
81
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Uu][Mm][Aa][Ss][Kk])
82
+ unset "$runtime_var"
83
+ ;;
84
+ esac
85
+ done
86
+ unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH NODE_OPTIONS NODE_PATH PS4 BASH_ENV ENV
87
+ case "${BASH_SOURCE[0]}" in
88
+ /usr/local/bin/uai-init | /uai-init-current) export HOME=/home/node ;;
89
+ esac
90
+ export XDG_CONFIG_HOME="$HOME/.config"
91
+ export XDG_DATA_HOME="$HOME/.local/share"
92
+ export XDG_CACHE_HOME="$HOME/.cache"
93
+ export ASDF_DIR=/opt/asdf
94
+ export ASDF_DATA_DIR=/opt/asdf-data
95
+ export ASDF_CONFIG_FILE=/dev/null
96
+ export ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions
97
+ export ASDF_NODEJS_VERSION=
98
+ export ASDF_PYTHON_VERSION=
99
+ export ASDF_GOLANG_VERSION=
100
+ export ASDF_RUBY_VERSION=
101
+ export ASDF_RUST_VERSION=
102
+ export ASDF_SKIP_RESHIM=1
103
+ export COREPACK_HOME=/opt/asdf-data/corepack
104
+ export COREPACK_DEFAULT_TO_LATEST=0
105
+ export COREPACK_ENV_FILE=0
106
+ export COREPACK_NPM_REGISTRY=https://registry.npmjs.org
107
+ export COREPACK_ENABLE_NETWORK=0
108
+ export COREPACK_ENABLE_AUTO_PIN=0
109
+ export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
110
+ export COREPACK_ENABLE_PROJECT_SPEC=1
111
+ export COREPACK_ENABLE_STRICT=1
112
+ export COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=0
113
+ export COREPACK_INTEGRITY_CHECK=1
114
+ export NPM_CONFIG_PREFIX="$HOME/.local/share/uai-npm"
115
+ export npm_config_prefix="$NPM_CONFIG_PREFIX"
116
+ export NPM_CONFIG_CACHE="$HOME/.cache/uai-npm"
117
+ export npm_config_cache="$NPM_CONFIG_CACHE"
118
+ export NPM_CONFIG_LOGS_DIR="$NPM_CONFIG_CACHE/_logs"
119
+ export npm_config_logs_dir="$NPM_CONFIG_LOGS_DIR"
120
+ export NPM_CONFIG_UMASK=0002
121
+ export npm_config_umask=0002
122
+ export NODE_OPTIONS=
123
+ export NODE_PATH=
124
+ export PATH="/opt/asdf-data/shims:/opt/asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$NPM_CONFIG_PREFIX/bin"
125
+ # Fixed here, not from the environment: the packaged path must not let a
126
+ # forged mount table or lower path defeat the shared-cache backstop below.
127
+ UAI_PROC_MOUNTS=/proc/mounts
128
+ UAI_ASDF_LOWER_DIR=/run/uai/asdf-lower
129
+ case "${BASH_SOURCE[0]}" in
130
+ /usr/local/bin/uai-init | /uai-init-current) ;;
131
+ *)
132
+ if [ -n "${UAI_INIT_TEST_ASDF_DATA_DIR:-}" ]; then
133
+ export ASDF_DATA_DIR="$UAI_INIT_TEST_ASDF_DATA_DIR"
134
+ export COREPACK_HOME="$ASDF_DATA_DIR/corepack"
135
+ fi
136
+ if [ -n "${UAI_INIT_TEST_PATH:-}" ]; then
137
+ export PATH="$UAI_INIT_TEST_PATH:$PATH"
138
+ fi
139
+ if [ -n "${UAI_INIT_TEST_PROC_MOUNTS:-}" ]; then
140
+ UAI_PROC_MOUNTS="$UAI_INIT_TEST_PROC_MOUNTS"
141
+ fi
142
+ if [ -n "${UAI_INIT_TEST_ASDF_LOWER_DIR:-}" ]; then
143
+ UAI_ASDF_LOWER_DIR="$UAI_INIT_TEST_ASDF_LOWER_DIR"
144
+ fi
145
+ ;;
146
+ esac
147
+ UAI_ENV_EXEC=/usr/local/bin/uai-env-exec
148
+ UAI_ENV_HOOK_FILE=/usr/local/share/uai/env-hook.sh
149
+ MANAGED_ENV_KEYS=/run/uai/managed-env-keys
150
+ UAI_INIT_SELF=/usr/local/bin/uai-init
151
+ case "${BASH_SOURCE[0]}" in
152
+ /usr/local/bin/uai-init | /uai-init-current) ;;
153
+ *)
154
+ UAI_INIT_SELF=${BASH_SOURCE[0]}
155
+ [ -z "${UAI_INIT_TEST_ENV_EXEC:-}" ] \
156
+ || UAI_ENV_EXEC=$UAI_INIT_TEST_ENV_EXEC
157
+ [ -z "${UAI_INIT_TEST_ENV_HOOK:-}" ] \
158
+ || UAI_ENV_HOOK_FILE=$UAI_INIT_TEST_ENV_HOOK
159
+ [ -z "${UAI_INIT_TEST_MANAGED_ENV_KEYS:-}" ] \
160
+ || MANAGED_ENV_KEYS=$UAI_INIT_TEST_MANAGED_ENV_KEYS
161
+ ;;
162
+ esac
163
+ ASDF_SH="$ASDF_DIR/asdf.sh"
164
+
165
+ log() { echo "uai-init: $*"; }
166
+
167
+ reject_writable_shared_runtime_cache() {
168
+ # The host proves the Docker mount metadata before invoking us. Keep an
169
+ # in-container backstop as well so a current initializer staged into a
170
+ # legacy/recovered container cannot become a shared-cache writer. This is a
171
+ # metadata/access check only: never create even a temporary shared file.
172
+ if [ ! -d "$ASDF_DATA_DIR" ]; then
173
+ log "error: shared runtime cache is missing; refusing dependency or editor startup"
174
+ return 1
175
+ fi
176
+ # ADR-108 (apple): /opt/asdf-data is an in-guest overlay — writable BY
177
+ # DESIGN, with every write landing in the task-private upper layer. That is
178
+ # safe exactly when the overlay is real (this mount table, not trust) and
179
+ # its lower is the read-only shared attachment; the shared volume then
180
+ # still cannot be written from this container. Any other writable shape
181
+ # remains the violation it always was.
182
+ # /usr/bin paths: present on both merged-usr Debian (in-container) and
183
+ # macOS (the direct source-tree test harness). Token-exact matching against
184
+ # comma-delimited mount options — substring matching accepted
185
+ # lowerdir=<lower>-evil — plus exactly ONE effective mount at the merged
186
+ # path (a later mount would shadow the proven one).
187
+ local overlay_lines="" any_lines="" overlay_opts=""
188
+ overlay_lines=$(/usr/bin/grep -c "^overlay ${ASDF_DATA_DIR} overlay " \
189
+ "$UAI_PROC_MOUNTS" 2>/dev/null) || overlay_lines=0
190
+ if [ "$overlay_lines" != "0" ]; then
191
+ if [ "$overlay_lines" != "1" ]; then
192
+ log "error: expected exactly one shared runtime cache overlay, found $overlay_lines; refusing dependency or editor startup"
193
+ return 1
194
+ fi
195
+ any_lines=$(/usr/bin/grep -c " ${ASDF_DATA_DIR} " "$UAI_PROC_MOUNTS" \
196
+ 2>/dev/null) || any_lines=0
197
+ if [ "$any_lines" != "1" ]; then
198
+ log "error: unexpected extra mounts at the shared runtime cache; refusing dependency or editor startup"
199
+ return 1
200
+ fi
201
+ overlay_opts=$(/usr/bin/grep "^overlay ${ASDF_DATA_DIR} overlay " \
202
+ "$UAI_PROC_MOUNTS" | /usr/bin/cut -d" " -f4)
203
+ case ",$overlay_opts," in
204
+ *",lowerdir=${UAI_ASDF_LOWER_DIR},"*) ;;
205
+ *)
206
+ log "error: shared runtime cache overlay has a foreign lower; refusing dependency or editor startup"
207
+ return 1
208
+ ;;
209
+ esac
210
+ case ",$overlay_opts," in
211
+ *",upperdir=/var/lib/uai/asdf-upper,"*) ;;
212
+ *)
213
+ log "error: shared runtime cache overlay has a foreign upper; refusing dependency or editor startup"
214
+ return 1
215
+ ;;
216
+ esac
217
+ case ",$overlay_opts," in
218
+ *",workdir=/var/lib/uai/asdf-work,"*) ;;
219
+ *)
220
+ log "error: shared runtime cache overlay has a foreign workdir; refusing dependency or editor startup"
221
+ return 1
222
+ ;;
223
+ esac
224
+ if [ ! -d "$UAI_ASDF_LOWER_DIR" ]; then
225
+ log "error: shared runtime cache lower attachment is missing; refusing dependency or editor startup"
226
+ return 1
227
+ fi
228
+ if [ -w "$UAI_ASDF_LOWER_DIR" ]; then
229
+ log "error: shared runtime cache lower attachment is writable; refusing dependency or editor startup"
230
+ return 1
231
+ fi
232
+ return 0
233
+ fi
234
+ if [ -w "$ASDF_DATA_DIR" ]; then
235
+ log "error: shared runtime cache is writable in the task container; refusing dependency or editor startup"
236
+ return 1
237
+ fi
238
+ }
239
+
240
+ prepare_private_npm_authority() {
241
+ local path prefix_probe="" cache_probe="" logs_probe=""
242
+
243
+ # These directories live in the task container layer, never on the shared
244
+ # asdf volume. Reject redirects before mkdir, then prove both exact write
245
+ # targets. The task has not started any agents yet, so there is no concurrent
246
+ # task process that can race these checks.
247
+ for path in \
248
+ "$HOME/.local" \
249
+ "$HOME/.local/share" \
250
+ "$NPM_CONFIG_PREFIX" \
251
+ "$HOME/.cache" \
252
+ "$NPM_CONFIG_CACHE" \
253
+ "$NPM_CONFIG_LOGS_DIR"
254
+ do
255
+ if [ -L "$path" ] || { [ -e "$path" ] && [ ! -d "$path" ]; }; then
256
+ log "error: private npm authority path is unsafe: $path"
257
+ return 1
258
+ fi
259
+ done
260
+
261
+ if ! /bin/mkdir -p -- \
262
+ "$NPM_CONFIG_PREFIX/bin" "$NPM_CONFIG_CACHE" "$NPM_CONFIG_LOGS_DIR"; then
263
+ log "error: could not prepare the private npm prefix/cache"
264
+ return 1
265
+ fi
266
+ for path in \
267
+ "$NPM_CONFIG_PREFIX" \
268
+ "$NPM_CONFIG_PREFIX/bin" \
269
+ "$NPM_CONFIG_CACHE" \
270
+ "$NPM_CONFIG_LOGS_DIR"
271
+ do
272
+ if [ -L "$path" ] || [ ! -d "$path" ]; then
273
+ log "error: private npm authority path changed during preparation: $path"
274
+ return 1
275
+ fi
276
+ done
277
+
278
+ prefix_probe=$(/usr/bin/mktemp \
279
+ "$NPM_CONFIG_PREFIX/.uai-write-probe.XXXXXX") || {
280
+ log "error: private npm prefix is not writable"
281
+ return 1
282
+ }
283
+ cache_probe=$(/usr/bin/mktemp \
284
+ "$NPM_CONFIG_CACHE/.uai-write-probe.XXXXXX") || {
285
+ /bin/rm -f -- "$prefix_probe"
286
+ log "error: private npm cache is not writable"
287
+ return 1
288
+ }
289
+ logs_probe=$(/usr/bin/mktemp \
290
+ "$NPM_CONFIG_LOGS_DIR/.uai-write-probe.XXXXXX") || {
291
+ /bin/rm -f -- "$prefix_probe" "$cache_probe"
292
+ log "error: private npm logs directory is not writable"
293
+ return 1
294
+ }
295
+ /bin/rm -f -- "$prefix_probe" "$cache_probe" "$logs_probe"
296
+ }
297
+
298
+ reject_workspace_runtime_shadow() {
299
+ local shadow="$WORKSPACE/.tool-versions"
300
+ # The root is never a selected project. Any entry here changes asdf lookup
301
+ # for no-manifest projects without being part of the pre-start proof. Check
302
+ # metadata only so a FIFO/socket/device cannot block initialization.
303
+ if [ -L "$shadow" ] || [ -e "$shadow" ]; then
304
+ log "error: workspace-root .tool-versions appeared after runtime proof; refusing dependency or editor startup"
305
+ return 1
306
+ fi
307
+ }
308
+
309
+ # Fail before sourcing asdf or executing any project input if this is an old
310
+ # writable task mount, or if task-private npm state redirects somewhere else.
311
+ reject_writable_shared_runtime_cache || exit 78
312
+ prepare_private_npm_authority || exit 78
313
+
314
+ # Make asdf + its shims available to this non-interactive shell. A bare
315
+ # `docker exec` sources no profile, so source asdf explicitly here.
316
+ if [ -f "$ASDF_SH" ]; then
317
+ # shellcheck disable=SC1090
318
+ . "$ASDF_SH"
319
+ else
320
+ log "warning: asdf not found at $ASDF_SH; dependency runtime resolution may fail"
321
+ fi
322
+
323
+ # ---------------------------------------------------------------------------
324
+ # 0. Git transport selection (ADR-027). A connected user's App token is the
325
+ # primary clone/fetch/push credential over HTTPS; the host writes it into
326
+ # gh's private config and runs `gh auth setup-git` before agents start. SSH
327
+ # transport is only the no-GitHub-connection fallback. The SSH key remains
328
+ # useful in either mode for commit/tag signing.
329
+ # ---------------------------------------------------------------------------
330
+
331
+ if [ "$UAI_INIT_ROLE" = main ]; then
332
+ if [ "${UAI_SKIP_GIT_TRANSPORT:-0}" = "1" ]; then
333
+ # Recovery/connect/disconnect reconciles the current credential immediately
334
+ # before invoking us. Do not overwrite that live decision with the static
335
+ # transport marker captured when Compose was first created.
336
+ log "keeping the reconciled GitHub transport"
337
+ elif [ "${UAI_GIT_TRANSPORT:-anonymous}" = "ssh" ]; then
338
+ # Remove a prior token-mode reverse rewrite if this container is being
339
+ # repaired after disconnect, then route canonical HTTPS origins over SSH.
340
+ /usr/bin/git config --global --unset-all url."https://github.com/".insteadOf \
341
+ >/dev/null 2>&1 || true
342
+ /usr/bin/git config --global url."git@github.com:".insteadOf "https://github.com/"
343
+ /usr/bin/git config --global core.sshCommand "/usr/bin/ssh -o StrictHostKeyChecking=accept-new"
344
+ log "using SSH as the GitHub transport fallback"
345
+ else
346
+ # Heal containers created by the old SSH-only policy. Canonical origins are
347
+ # HTTPS; reverse legacy SSH URLs (including submodules) through gh as well.
348
+ /usr/bin/git config --global --unset-all url."git@github.com:".insteadOf \
349
+ >/dev/null 2>&1 || true
350
+ /usr/bin/git config --global --unset-all core.sshCommand >/dev/null 2>&1 || true
351
+ /usr/bin/git config --global --replace-all url."https://github.com/".insteadOf \
352
+ "git@github.com:"
353
+ /usr/bin/git config --global --add url."https://github.com/".insteadOf \
354
+ "ssh://git@github.com/"
355
+ if [ "${UAI_GIT_TRANSPORT:-}" = "https" ]; then
356
+ log "using the connected GitHub credential for HTTPS Git"
357
+ else
358
+ log "using anonymous HTTPS Git for public repositories"
359
+ fi
360
+ fi
361
+
362
+ # Signed commits (policy): when the uai SSH identity is present, configure git
363
+ # to SSH-sign every commit + tag with it. The pubkey is registered on GitHub
364
+ # so commits show as Verified. Signing is independent of Git transport.
365
+ if [ -f "$HOME/.ssh/id_ed25519.pub" ]; then
366
+ log "enabling SSH commit signing with the uai identity"
367
+ /usr/bin/git config --global gpg.format ssh
368
+ /usr/bin/git config --global user.signingkey "$HOME/.ssh/id_ed25519.pub"
369
+ /usr/bin/git config --global commit.gpgsign true
370
+ /usr/bin/git config --global tag.gpgsign true
371
+ fi
372
+
373
+ # Attribution policy: never co-author with the agents. Claude Code honors
374
+ # `includeCoAuthoredBy: false` (drops the "Co-Authored-By: Claude" commit
375
+ # trailer and the "Generated with Claude Code" PR footer). Seed it into the
376
+ # per-task Claude user settings if absent. Codex is covered by the
377
+ # orchestrator's system preamble.
378
+ cc_settings="$HOME/.claude/settings.json"
379
+ if [ ! -f "$cc_settings" ]; then
380
+ mkdir -p "$HOME/.claude"
381
+ printf '{\n "includeCoAuthoredBy": false\n}\n' > "$cc_settings"
382
+ log "seeded ~/.claude/settings.json (includeCoAuthoredBy: false)"
383
+ fi
384
+ fi
385
+
386
+ # ---------------------------------------------------------------------------
387
+ # 1. Per-folder dependency install (runtimes are already materialized).
388
+ # ---------------------------------------------------------------------------
389
+
390
+ install_node_deps() {
391
+ # Pick the package manager from the packageManager field, else a lockfile,
392
+ # else npm. corepack provisions pnpm/yarn from the packageManager field.
393
+ local pm=""
394
+ if [ -f package.json ]; then
395
+ pm=$(jq -r '.packageManager // ""' package.json 2>/dev/null | sed 's/@.*//')
396
+ fi
397
+ if [ -z "$pm" ]; then
398
+ if [ -f pnpm-lock.yaml ]; then pm="pnpm"
399
+ elif [ -f yarn.lock ]; then pm="yarn"
400
+ elif [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then pm="npm"
401
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then pm="bun"
402
+ else pm="npm"
403
+ fi
404
+ fi
405
+
406
+ case "$pm" in
407
+ pnpm) log "pnpm install"; pnpm install ;;
408
+ yarn) log "yarn install"; yarn install ;;
409
+ bun) log "bun install"; bun install ;;
410
+ *) log "npm install"; npm install --no-audit --no-fund ;;
411
+ esac
412
+ }
413
+
414
+ install_python_deps() {
415
+ # uv handles both requirements.txt and pyproject.toml; create a project .venv
416
+ # so the install is isolated to the folder. Pin uv to ASDF's Python (the
417
+ # version asdf just resolved from the in-scope .tool-versions) so the venv
418
+ # matches the project's declared runtime. uv otherwise selects via
419
+ # .python-version / requires-python, which can disagree with .tool-versions
420
+ # (or be junk, e.g. a stray pyenv-virtualenv name) — asdf is the authority.
421
+ local py
422
+ py="$(asdf which python 2>/dev/null || true)"
423
+ if [ -f pyproject.toml ]; then
424
+ log "uv sync (pyproject.toml)${py:+ — asdf python $py}"
425
+ if [ -n "$py" ]; then uv sync --python "$py"; else uv sync; fi
426
+ elif [ -f requirements.txt ]; then
427
+ log "uv venv + pip install -r requirements.txt${py:+ — asdf python $py}"
428
+ if [ -n "$py" ]; then uv venv --python "$py"; else uv venv; fi \
429
+ && uv pip install -r requirements.txt
430
+ fi
431
+ }
432
+
433
+ install_folder_deps() {
434
+ local dir="$1" name
435
+ name=$(basename "$dir")
436
+
437
+ # Enter the folder; asdf auto-resolves either its already-materialized local
438
+ # declarations or the already-proven root image defaults.
439
+ cd "$dir" || { log "cannot cd into $name — skipping"; return; }
440
+
441
+ # Dependency-install heuristic. Run every matcher that applies (a polyglot
442
+ # folder may have more than one); each is independently best-effort.
443
+ if [ -f package.json ]; then
444
+ install_node_deps || log "node deps failed in $name — continuing"
445
+ fi
446
+ if [ -f pyproject.toml ] || [ -f requirements.txt ]; then
447
+ install_python_deps || log "python deps failed in $name — continuing"
448
+ fi
449
+ if [ -f Cargo.toml ]; then
450
+ log "cargo build ($name)"
451
+ cargo build || log "cargo build failed in $name — continuing"
452
+ fi
453
+ if [ -f go.mod ]; then
454
+ log "go mod download ($name)"
455
+ go mod download || log "go mod download failed in $name — continuing"
456
+ fi
457
+ }
458
+
459
+ reject_workspace_runtime_shadow || exit 78
460
+
461
+ # ADR-122: dependency installs leave the task-start critical path. The loop
462
+ # below runs in a DETACHED background process so uai-init (and with it
463
+ # task-up) returns as soon as the cheap, provable work is done — the editor
464
+ # and the agents start immediately, warm or cold. Progress is observable in
465
+ # the workspace itself:
466
+ #
467
+ # $WORKSPACE/.moondeck/init/deps-status running | done | failed | authority-violation
468
+ # $WORKSPACE/.moondeck/init/deps.log the full install transcript
469
+ #
470
+ # The runtime-authority posture narrows deliberately: the synchronous
471
+ # workspace-shadow proofs above and below still exit 78 (task-up
472
+ # quarantines), but a shadow that APPEARS MID-INSTALL can no longer stop the
473
+ # task — agents are already running by then, and an agent could author the
474
+ # same shadow a second later anyway. The runner still refuses to run any
475
+ # further dependency command under a shadowed root and records
476
+ # `authority-violation`; the shared asdf volume stays read-only-proven
477
+ # regardless.
478
+ run_workspace_installs() {
479
+ local seen_runtime_projects="|" project_slug folder git_marker
480
+ while IFS= read -r project_slug || [ -n "$project_slug" ]; do
481
+ if [[ ! "$project_slug" =~ ^[a-z0-9-]{1,64}$ ]]; then
482
+ log "warning: unsafe runtime-project allowlist entry — skipping"
483
+ continue
484
+ fi
485
+ case "$seen_runtime_projects" in
486
+ *"|$project_slug|"*)
487
+ log "warning: duplicate runtime-project entry $project_slug — skipping"
488
+ continue
489
+ ;;
490
+ esac
491
+ seen_runtime_projects="${seen_runtime_projects}${project_slug}|"
492
+ folder="$WORKSPACE/$project_slug"
493
+ git_marker="$folder/.git"
494
+ if [ -L "$folder" ] || [ ! -d "$folder" ] || [ -L "$git_marker" ] \
495
+ || { [ ! -f "$git_marker" ] && [ ! -d "$git_marker" ]; }; then
496
+ log "warning: allowlisted project $project_slug is not a safe Git worktree — skipping"
497
+ continue
498
+ fi
499
+ if ! reject_workspace_runtime_shadow; then
500
+ printf 'authority-violation\n' > "$deps_status_file"
501
+ return 78
502
+ fi
503
+ # Run in a subshell so a `cd` (or a failing command under the relaxed
504
+ # error mode) in one folder never leaks into the next.
505
+ ( install_folder_deps "$folder" )
506
+ done < "$RUNTIME_PROJECTS"
507
+ return 0
508
+ }
509
+
510
+ # The detached worker is the only uai-init role that receives project env.
511
+ # Its caller has already used uai-env-exec to perform one authenticated
512
+ # refresh; this role does no editor or Git-control-plane work and exits after
513
+ # the installs. Paths are repeated here because a fresh process cannot inherit
514
+ # the parent's shell locals.
515
+ if [ "$UAI_INIT_ROLE" = dependency-worker ]; then
516
+ deps_state_dir="$WORKSPACE/.moondeck/init"
517
+ deps_status_file="$deps_state_dir/deps-status"
518
+ run_workspace_installs
519
+ exit $?
520
+ fi
521
+
522
+ if [ -d "$WORKSPACE" ] && [ -f "$RUNTIME_PROJECTS" ] \
523
+ && [ ! -L "$RUNTIME_PROJECTS" ]; then
524
+ deps_state_dir="$WORKSPACE/.moondeck/init"
525
+ deps_status_file="$deps_state_dir/deps-status"
526
+ deps_log_file="$deps_state_dir/deps.log"
527
+ deps_lock_dir="$deps_state_dir/deps.lock.d"
528
+ mkdir -p "$deps_state_dir"
529
+
530
+ # One runner at a time. uai-init is re-runnable (task-up retry, recovery),
531
+ # and each re-run SHOULD refresh dependencies — but never concurrently.
532
+ # mkdir is the portable atomic lock; a pid that no longer answers marks a
533
+ # crashed runner whose lock is stale and may be stolen.
534
+ deps_spawn=1
535
+ if mkdir "$deps_lock_dir" 2>/dev/null; then
536
+ # Hold the lock as ourselves until the runner's pid replaces it, so a
537
+ # concurrent uai-init never reads an empty pid file as a stale lock.
538
+ printf '%s\n' "$$" > "$deps_lock_dir/pid"
539
+ else
540
+ deps_holder=$(cat "$deps_lock_dir/pid" 2>/dev/null || true)
541
+ if [ -n "$deps_holder" ] && kill -0 "$deps_holder" 2>/dev/null; then
542
+ log "dependency install already running (pid $deps_holder) — leaving it be"
543
+ deps_spawn=0
544
+ else
545
+ rm -rf "$deps_lock_dir"
546
+ if mkdir "$deps_lock_dir" 2>/dev/null; then
547
+ printf '%s\n' "$$" > "$deps_lock_dir/pid"
548
+ else
549
+ log "warning: could not acquire the dependency install lock — skipping installs"
550
+ deps_spawn=0
551
+ fi
552
+ fi
553
+ fi
554
+ if [ "$deps_spawn" -eq 1 ]; then
555
+ printf 'running\n' > "$deps_status_file"
556
+ log "installing workspace dependencies in the background (status: $deps_status_file, log: $deps_log_file)"
557
+ (
558
+ trap 'rm -rf "$deps_lock_dir"' EXIT
559
+ deps_rc=0
560
+ if [ "${UAI_ENV_MODE:-baked}" = loader ]; then
561
+ UAI_ENV_HOOK=1 \
562
+ BASH_ENV="$UAI_ENV_HOOK_FILE" \
563
+ SHELL=/usr/bin/zsh \
564
+ "$UAI_ENV_EXEC" "$UAI_INIT_SELF" --dependency-worker \
565
+ || deps_rc=$?
566
+ else
567
+ run_workspace_installs || deps_rc=$?
568
+ fi
569
+ if [ "$deps_rc" -eq 0 ]; then
570
+ printf 'done\n' > "$deps_status_file"
571
+ log "workspace dependency install complete"
572
+ elif [ "$deps_rc" -ne 78 ]; then
573
+ printf 'failed\n' > "$deps_status_file"
574
+ log "workspace dependency install failed (exit $deps_rc)"
575
+ fi
576
+ ) >>"$deps_log_file" 2>&1 </dev/null &
577
+ printf '%s\n' "$!" > "$deps_lock_dir/pid"
578
+ disown
579
+ fi
580
+ elif [ ! -d "$WORKSPACE" ]; then
581
+ log "workspace $WORKSPACE missing — scratchpad/no-project task, skipping installs"
582
+ else
583
+ log "warning: trusted runtime-project allowlist missing or unsafe — skipping installs"
584
+ fi
585
+
586
+ reject_workspace_runtime_shadow || exit 78
587
+
588
+ # ---------------------------------------------------------------------------
589
+ # 2. code-server — the Editor pane. Bound to 0.0.0.0:8080, auth disabled
590
+ # (reached only through the authenticated tunnel / editor proxy). Opens
591
+ # /workspace so all worktrees are visible at the top level. Idempotent:
592
+ # skips launch if already running.
593
+ # ---------------------------------------------------------------------------
594
+
595
+ editor_root="$WORKSPACE"
596
+ [ -d "$editor_root" ] || editor_root="/home/node"
597
+
598
+ # Keep code-server on Uai's managed data path even when a project declares
599
+ # XDG_DATA_HOME. Otherwise code-server starts with a fresh profile elsewhere,
600
+ # which restores the light theme and Workspace Trust prompt.
601
+ cs_data_dir="$HOME/.local/share/code-server"
602
+ cs_user_dir="$cs_data_dir/User"
603
+ cs_seed="/usr/local/share/uai/code-server-settings.json"
604
+ # The source-tree sibling keeps direct smoke tests and development runs on the
605
+ # same seed as the packaged image; the image always uses the root-owned path.
606
+ cs_source_seed="$(dirname "${BASH_SOURCE[0]}")/code-server-settings.json"
607
+ if [ ! -f "$cs_seed" ] && [ -f "$cs_source_seed" ]; then
608
+ cs_seed="$cs_source_seed"
609
+ fi
610
+
611
+ # ADR-091: seed the Editor pane in the palette the task was created from.
612
+ # UAI_EDITOR_THEME carries a VS Code theme LABEL ("Quiet Light"), already
613
+ # resolved cloud-side from the theme registry — the container never maps a Uai
614
+ # palette id and never sees a raw user string. Unset is the normal state for a
615
+ # task created before this shipped or outside a browser; the seed's own baked
616
+ # default then stands.
617
+ #
618
+ # Applied ONLY while seeding. An existing settings.json is still left entirely
619
+ # alone, so a user's own editor tweaks survive a restart — which is also why
620
+ # changing the app's palette later does not re-theme an already-created task
621
+ # (docs/theming.md). If jq is missing or the filter fails, fall through to the
622
+ # plain copy: a themed editor is worth less than an editor that starts.
623
+ seed_code_server_settings() {
624
+ local seed="$1" dest="$2" theme="${UAI_EDITOR_THEME:-}"
625
+ if [ -n "$theme" ] && command -v jq >/dev/null 2>&1; then
626
+ if jq --arg theme "$theme" '.["workbench.colorTheme"] = $theme' \
627
+ "$seed" >"$dest.tmp" 2>/dev/null && [ -s "$dest.tmp" ]; then
628
+ mv "$dest.tmp" "$dest"
629
+ log "seeded code-server settings (theme: $theme)"
630
+ return 0
631
+ fi
632
+ rm -f "$dest.tmp"
633
+ log "warning: could not apply UAI_EDITOR_THEME=$theme; using image default"
634
+ fi
635
+ cp "$seed" "$dest" || return 1
636
+ log "seeded code-server settings"
637
+ }
638
+
639
+ if [ ! -f "$cs_user_dir/settings.json" ]; then
640
+ if [ ! -f "$cs_seed" ]; then
641
+ log "warning: code-server settings seed missing at $cs_seed"
642
+ elif mkdir -p "$cs_user_dir" &&
643
+ seed_code_server_settings "$cs_seed" "$cs_user_dir/settings.json"; then
644
+ :
645
+ else
646
+ log "warning: could not seed code-server settings at $cs_user_dir/settings.json"
647
+ fi
648
+ fi
649
+
650
+ if pgrep -f 'code-server.*--bind-addr' >/dev/null 2>&1; then
651
+ log "code-server already running"
652
+ else
653
+ log "launching code-server on 0.0.0.0:8080"
654
+ # Compose deliberately replaces the image PATH with Uai's runtime authority
655
+ # path, which excludes task-writable home bins. code-server is an image-owned
656
+ # standalone install, so invoke its exact baked path instead of relying on
657
+ # PATH. A source-tree test invocation may resolve its isolated stub, but the
658
+ # two production destinations never consult PATH for this executable.
659
+ code_server_bin=/home/node/.local/bin/code-server
660
+ case "${BASH_SOURCE[0]}" in
661
+ /usr/local/bin/uai-init | /uai-init-current) ;;
662
+ *)
663
+ test_code_server_bin=$(command -v code-server 2>/dev/null || true)
664
+ [ -z "$test_code_server_bin" ] || code_server_bin="$test_code_server_bin"
665
+ ;;
666
+ esac
667
+ code_server_env=()
668
+ if [ "${UAI_ENV_MODE:-baked}" = loader ]; then
669
+ if [ -L "$MANAGED_ENV_KEYS" ] || [ ! -f "$MANAGED_ENV_KEYS" ]; then
670
+ log "error: managed env key list is unavailable; refusing editor startup"
671
+ exit 78
672
+ fi
673
+ code_server_env=(/usr/bin/env)
674
+ while IFS= read -r managed_name || [ -n "$managed_name" ]; do
675
+ case $managed_name in
676
+ ''|[!A-Za-z_]*|*[!A-Za-z0-9_]*)
677
+ log "error: managed env key list is invalid; refusing editor startup"
678
+ exit 78
679
+ ;;
680
+ esac
681
+ code_server_env+=( -u "$managed_name" )
682
+ done < "$MANAGED_ENV_KEYS"
683
+ code_server_env+=(
684
+ -u __UAI_ENV_KEYS
685
+ -u __UAI_ENV_ETAG
686
+ UAI_ENV_HOOK=1
687
+ "BASH_ENV=$UAI_ENV_HOOK_FILE"
688
+ SHELL=/usr/bin/zsh
689
+ )
690
+ fi
691
+ nohup "${code_server_env[@]}" "$code_server_bin" \
692
+ --user-data-dir "$cs_data_dir" \
693
+ --disable-workspace-trust \
694
+ --auth none \
695
+ --disable-telemetry \
696
+ --disable-update-check \
697
+ --bind-addr 0.0.0.0:8080 \
698
+ "$editor_root" \
699
+ >/tmp/code-server.log 2>&1 &
700
+ disown
701
+ fi
702
+
703
+ # Always succeed: the container is up and code-server has been (re)launched.
704
+ exit 0