@runuai/host 0.9.13 → 0.9.42

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 (97) hide show
  1. package/README.md +22 -5
  2. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  3. package/db/migrations/0015_host_settings.sql +9 -0
  4. package/db/migrations/0016_task_environment.sql +2 -0
  5. package/db/migrations/meta/_journal.json +21 -0
  6. package/db/schema.ts +80 -30
  7. package/images/standard/Dockerfile +36 -10
  8. package/images/standard/README.md +63 -18
  9. package/images/standard/container/corepack-version +1 -0
  10. package/images/standard/container/uai-init +308 -38
  11. package/images/standard/container/uai-materialize-runtimes +1527 -0
  12. package/lib/agent-cli.ts +69 -4
  13. package/lib/agent.ts +46 -7
  14. package/lib/agents/claude.ts +13 -8
  15. package/lib/agents/codex.ts +11 -6
  16. package/lib/agents/cursor.ts +39 -29
  17. package/lib/agents/durable-proc.ts +20 -27
  18. package/lib/agents/factory.ts +9 -25
  19. package/lib/agents/grok.ts +43 -30
  20. package/lib/agents/kimi.ts +44 -29
  21. package/lib/agents/opencode.ts +43 -31
  22. package/lib/agents/proc.ts +149 -114
  23. package/lib/agents/transport.ts +62 -50
  24. package/lib/agents/types.ts +6 -4
  25. package/lib/apple-runtime-recycle.ts +236 -0
  26. package/lib/apple-uninstall-teardown.ts +224 -0
  27. package/lib/browser-testing.ts +233 -93
  28. package/lib/codex-auth.ts +40 -6
  29. package/lib/command-db.ts +20 -0
  30. package/lib/container-runtime.ts +1338 -0
  31. package/lib/db.ts +1 -0
  32. package/lib/docker-exec.ts +87 -5
  33. package/lib/engine-accounts.ts +68 -5
  34. package/lib/engine-login.ts +1952 -0
  35. package/lib/enrollment-state.ts +251 -0
  36. package/lib/env-file.ts +155 -0
  37. package/lib/env.ts +4 -0
  38. package/lib/git-diff.ts +98 -32
  39. package/lib/git-identity.ts +199 -87
  40. package/lib/github-tokens.ts +202 -91
  41. package/lib/host-cloud-url.ts +62 -0
  42. package/lib/host-config.ts +279 -0
  43. package/lib/host-logs.ts +962 -0
  44. package/lib/keyed-promise-tail.ts +23 -0
  45. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  46. package/lib/managed-activation-watcher.ts +72 -0
  47. package/lib/managed-install-owner-watcher.ts +55 -0
  48. package/lib/managed-operation-drain.ts +49 -0
  49. package/lib/managed-runtime.ts +3644 -0
  50. package/lib/managed-update-scheduler.ts +125 -0
  51. package/lib/mcp-gateway.ts +450 -23
  52. package/lib/orchestrator.ts +3070 -223
  53. package/lib/preview-sidecar.ts +57 -13
  54. package/lib/release-manifest.ts +708 -0
  55. package/lib/release-trust.ts +28 -0
  56. package/lib/runtime-activation-tail.ts +232 -0
  57. package/lib/runtime-archive.ts +1086 -0
  58. package/lib/runtime-authority.ts +79 -0
  59. package/lib/runtime-guard.ts +36 -0
  60. package/lib/runtime-provider-state.ts +169 -0
  61. package/lib/runtime-state.ts +232 -12
  62. package/lib/skills.ts +24 -3
  63. package/lib/ssh.ts +18 -0
  64. package/lib/standard-image.ts +1104 -141
  65. package/lib/stopped-task-status-queue.ts +44 -0
  66. package/lib/task-container-cli.ts +269 -0
  67. package/lib/task-diff.ts +66 -46
  68. package/lib/task-environment/apple-container.ts +757 -0
  69. package/lib/task-environment/docker.ts +945 -0
  70. package/lib/task-environment/index.ts +364 -0
  71. package/lib/task-environment/legacy-adoption.ts +443 -0
  72. package/lib/task-environment/registry.ts +58 -0
  73. package/lib/task-environment/types.ts +408 -0
  74. package/lib/task-identity.ts +19 -0
  75. package/lib/task-inventory.ts +585 -0
  76. package/lib/tunnel-registry.ts +135 -19
  77. package/lib/tunnel-runtime.ts +235 -0
  78. package/package.json +1 -1
  79. package/scripts/agent/_common.sh +123 -3
  80. package/scripts/agent/task-down.sh +146 -38
  81. package/scripts/agent/task-status.sh +19 -3
  82. package/scripts/agent/task-up.sh +1463 -109
  83. package/scripts/install/darwin.ts +848 -50
  84. package/scripts/install/linux.ts +838 -35
  85. package/scripts/install/types.ts +43 -0
  86. package/scripts/install/util.ts +215 -8
  87. package/scripts/install/win.ts +12 -0
  88. package/src/apple-tunnel-route.ts +104 -0
  89. package/src/cli.ts +1464 -72
  90. package/src/event-outbox.ts +83 -4
  91. package/src/index.ts +871 -50
  92. package/src/main.ts +1398 -255
  93. package/src/paths.ts +17 -1
  94. package/src/protocol.ts +695 -1
  95. package/src/runtime-bootstrap.ts +165 -0
  96. package/src/ui/server.ts +46 -10
  97. package/src/ui/types.ts +37 -0
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bash
1
+ #!/bin/bash
2
2
  # uai-init — prepare a standard-image task container (ADR-022).
3
3
  #
4
4
  # Runs INSIDE the container (Linux, bash 5 — may use anything). Invoked by
@@ -9,31 +9,278 @@
9
9
  # per selected project. A 0-project (scratchpad) task has an empty /workspace.
10
10
  #
11
11
  # Responsibilities:
12
- # 1. For each /workspace/<slug>/ that carries a .tool-versions: `asdf
13
- # install` (downloads cached on the host-wide /opt/asdf-data volume),
14
- # then a dependency-install heuristic for the folder's stack.
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.
15
16
  # 2. Launch code-server on 0.0.0.0:8080 (auth disabled — reached only via
16
17
  # the authenticated tunnel), opening /workspace.
17
18
  #
18
- # Best-effort + idempotent: a failing folder logs and is skipped; it never
19
- # aborts the whole init. Re-running when things are already up is a no-op.
20
- # uai-init itself always exits 0 so task-up's `docker exec ... uai-init` step
21
- # succeeds as long as the container is reachable.
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.
22
23
 
23
24
  set -uo pipefail
24
25
 
25
26
  WORKSPACE="${UAI_WORKSPACE:-/workspace}"
26
- ASDF_SH="${ASDF_DIR:-/opt/asdf}/asdf.sh"
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
+ for runtime_var in $(compgen -A variable 2>/dev/null); do
55
+ case "$runtime_var" in
56
+ ASDF_* | COREPACK_* | \
57
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Pp][Rr][Ee][Ff][Ii][Xx] | \
58
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Cc][Aa][Cc][Hh][Ee] | \
59
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Ll][Oo][Gg][Ss]_[Dd][Ii][Rr] | \
60
+ [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Uu][Mm][Aa][Ss][Kk])
61
+ unset "$runtime_var"
62
+ ;;
63
+ esac
64
+ done
65
+ unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH NODE_OPTIONS NODE_PATH PS4 BASH_ENV ENV
66
+ case "${BASH_SOURCE[0]}" in
67
+ /usr/local/bin/uai-init | /uai-init-current) export HOME=/home/node ;;
68
+ esac
69
+ export XDG_CONFIG_HOME="$HOME/.config"
70
+ export XDG_DATA_HOME="$HOME/.local/share"
71
+ export XDG_CACHE_HOME="$HOME/.cache"
72
+ export ASDF_DIR=/opt/asdf
73
+ export ASDF_DATA_DIR=/opt/asdf-data
74
+ export ASDF_CONFIG_FILE=/dev/null
75
+ export ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions
76
+ export ASDF_NODEJS_VERSION=
77
+ export ASDF_PYTHON_VERSION=
78
+ export ASDF_GOLANG_VERSION=
79
+ export ASDF_RUBY_VERSION=
80
+ export ASDF_RUST_VERSION=
81
+ export ASDF_SKIP_RESHIM=1
82
+ export COREPACK_HOME=/opt/asdf-data/corepack
83
+ export COREPACK_DEFAULT_TO_LATEST=0
84
+ export COREPACK_ENV_FILE=0
85
+ export COREPACK_NPM_REGISTRY=https://registry.npmjs.org
86
+ export COREPACK_ENABLE_NETWORK=0
87
+ export COREPACK_ENABLE_AUTO_PIN=0
88
+ export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
89
+ export COREPACK_ENABLE_PROJECT_SPEC=1
90
+ export COREPACK_ENABLE_STRICT=1
91
+ export COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=0
92
+ export COREPACK_INTEGRITY_CHECK=1
93
+ export NPM_CONFIG_PREFIX="$HOME/.local/share/uai-npm"
94
+ export npm_config_prefix="$NPM_CONFIG_PREFIX"
95
+ export NPM_CONFIG_CACHE="$HOME/.cache/uai-npm"
96
+ export npm_config_cache="$NPM_CONFIG_CACHE"
97
+ export NPM_CONFIG_LOGS_DIR="$NPM_CONFIG_CACHE/_logs"
98
+ export npm_config_logs_dir="$NPM_CONFIG_LOGS_DIR"
99
+ export NPM_CONFIG_UMASK=0002
100
+ export npm_config_umask=0002
101
+ export NODE_OPTIONS=
102
+ export NODE_PATH=
103
+ 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"
104
+ # Fixed here, not from the environment: the packaged path must not let a
105
+ # forged mount table or lower path defeat the shared-cache backstop below.
106
+ UAI_PROC_MOUNTS=/proc/mounts
107
+ UAI_ASDF_LOWER_DIR=/run/uai/asdf-lower
108
+ case "${BASH_SOURCE[0]}" in
109
+ /usr/local/bin/uai-init | /uai-init-current) ;;
110
+ *)
111
+ if [ -n "${UAI_INIT_TEST_ASDF_DATA_DIR:-}" ]; then
112
+ export ASDF_DATA_DIR="$UAI_INIT_TEST_ASDF_DATA_DIR"
113
+ export COREPACK_HOME="$ASDF_DATA_DIR/corepack"
114
+ fi
115
+ if [ -n "${UAI_INIT_TEST_PATH:-}" ]; then
116
+ export PATH="$UAI_INIT_TEST_PATH:$PATH"
117
+ fi
118
+ if [ -n "${UAI_INIT_TEST_PROC_MOUNTS:-}" ]; then
119
+ UAI_PROC_MOUNTS="$UAI_INIT_TEST_PROC_MOUNTS"
120
+ fi
121
+ if [ -n "${UAI_INIT_TEST_ASDF_LOWER_DIR:-}" ]; then
122
+ UAI_ASDF_LOWER_DIR="$UAI_INIT_TEST_ASDF_LOWER_DIR"
123
+ fi
124
+ ;;
125
+ esac
126
+ ASDF_SH="$ASDF_DIR/asdf.sh"
27
127
 
28
128
  log() { echo "uai-init: $*"; }
29
129
 
130
+ reject_writable_shared_runtime_cache() {
131
+ # The host proves the Docker mount metadata before invoking us. Keep an
132
+ # in-container backstop as well so a current initializer staged into a
133
+ # legacy/recovered container cannot become a shared-cache writer. This is a
134
+ # metadata/access check only: never create even a temporary shared file.
135
+ if [ ! -d "$ASDF_DATA_DIR" ]; then
136
+ log "error: shared runtime cache is missing; refusing dependency or editor startup"
137
+ return 1
138
+ fi
139
+ # ADR-108 (apple): /opt/asdf-data is an in-guest overlay — writable BY
140
+ # DESIGN, with every write landing in the task-private upper layer. That is
141
+ # safe exactly when the overlay is real (this mount table, not trust) and
142
+ # its lower is the read-only shared attachment; the shared volume then
143
+ # still cannot be written from this container. Any other writable shape
144
+ # remains the violation it always was.
145
+ # /usr/bin paths: present on both merged-usr Debian (in-container) and
146
+ # macOS (the direct source-tree test harness). Token-exact matching against
147
+ # comma-delimited mount options — substring matching accepted
148
+ # lowerdir=<lower>-evil — plus exactly ONE effective mount at the merged
149
+ # path (a later mount would shadow the proven one).
150
+ local overlay_lines="" any_lines="" overlay_opts=""
151
+ overlay_lines=$(/usr/bin/grep -c "^overlay ${ASDF_DATA_DIR} overlay " \
152
+ "$UAI_PROC_MOUNTS" 2>/dev/null) || overlay_lines=0
153
+ if [ "$overlay_lines" != "0" ]; then
154
+ if [ "$overlay_lines" != "1" ]; then
155
+ log "error: expected exactly one shared runtime cache overlay, found $overlay_lines; refusing dependency or editor startup"
156
+ return 1
157
+ fi
158
+ any_lines=$(/usr/bin/grep -c " ${ASDF_DATA_DIR} " "$UAI_PROC_MOUNTS" \
159
+ 2>/dev/null) || any_lines=0
160
+ if [ "$any_lines" != "1" ]; then
161
+ log "error: unexpected extra mounts at the shared runtime cache; refusing dependency or editor startup"
162
+ return 1
163
+ fi
164
+ overlay_opts=$(/usr/bin/grep "^overlay ${ASDF_DATA_DIR} overlay " \
165
+ "$UAI_PROC_MOUNTS" | /usr/bin/cut -d" " -f4)
166
+ case ",$overlay_opts," in
167
+ *",lowerdir=${UAI_ASDF_LOWER_DIR},"*) ;;
168
+ *)
169
+ log "error: shared runtime cache overlay has a foreign lower; refusing dependency or editor startup"
170
+ return 1
171
+ ;;
172
+ esac
173
+ case ",$overlay_opts," in
174
+ *",upperdir=/var/lib/uai/asdf-upper,"*) ;;
175
+ *)
176
+ log "error: shared runtime cache overlay has a foreign upper; refusing dependency or editor startup"
177
+ return 1
178
+ ;;
179
+ esac
180
+ case ",$overlay_opts," in
181
+ *",workdir=/var/lib/uai/asdf-work,"*) ;;
182
+ *)
183
+ log "error: shared runtime cache overlay has a foreign workdir; refusing dependency or editor startup"
184
+ return 1
185
+ ;;
186
+ esac
187
+ if [ ! -d "$UAI_ASDF_LOWER_DIR" ]; then
188
+ log "error: shared runtime cache lower attachment is missing; refusing dependency or editor startup"
189
+ return 1
190
+ fi
191
+ if [ -w "$UAI_ASDF_LOWER_DIR" ]; then
192
+ log "error: shared runtime cache lower attachment is writable; refusing dependency or editor startup"
193
+ return 1
194
+ fi
195
+ return 0
196
+ fi
197
+ if [ -w "$ASDF_DATA_DIR" ]; then
198
+ log "error: shared runtime cache is writable in the task container; refusing dependency or editor startup"
199
+ return 1
200
+ fi
201
+ }
202
+
203
+ prepare_private_npm_authority() {
204
+ local path prefix_probe="" cache_probe="" logs_probe=""
205
+
206
+ # These directories live in the task container layer, never on the shared
207
+ # asdf volume. Reject redirects before mkdir, then prove both exact write
208
+ # targets. The task has not started any agents yet, so there is no concurrent
209
+ # task process that can race these checks.
210
+ for path in \
211
+ "$HOME/.local" \
212
+ "$HOME/.local/share" \
213
+ "$NPM_CONFIG_PREFIX" \
214
+ "$HOME/.cache" \
215
+ "$NPM_CONFIG_CACHE" \
216
+ "$NPM_CONFIG_LOGS_DIR"
217
+ do
218
+ if [ -L "$path" ] || { [ -e "$path" ] && [ ! -d "$path" ]; }; then
219
+ log "error: private npm authority path is unsafe: $path"
220
+ return 1
221
+ fi
222
+ done
223
+
224
+ if ! /bin/mkdir -p -- \
225
+ "$NPM_CONFIG_PREFIX/bin" "$NPM_CONFIG_CACHE" "$NPM_CONFIG_LOGS_DIR"; then
226
+ log "error: could not prepare the private npm prefix/cache"
227
+ return 1
228
+ fi
229
+ for path in \
230
+ "$NPM_CONFIG_PREFIX" \
231
+ "$NPM_CONFIG_PREFIX/bin" \
232
+ "$NPM_CONFIG_CACHE" \
233
+ "$NPM_CONFIG_LOGS_DIR"
234
+ do
235
+ if [ -L "$path" ] || [ ! -d "$path" ]; then
236
+ log "error: private npm authority path changed during preparation: $path"
237
+ return 1
238
+ fi
239
+ done
240
+
241
+ prefix_probe=$(/usr/bin/mktemp \
242
+ "$NPM_CONFIG_PREFIX/.uai-write-probe.XXXXXX") || {
243
+ log "error: private npm prefix is not writable"
244
+ return 1
245
+ }
246
+ cache_probe=$(/usr/bin/mktemp \
247
+ "$NPM_CONFIG_CACHE/.uai-write-probe.XXXXXX") || {
248
+ /bin/rm -f -- "$prefix_probe"
249
+ log "error: private npm cache is not writable"
250
+ return 1
251
+ }
252
+ logs_probe=$(/usr/bin/mktemp \
253
+ "$NPM_CONFIG_LOGS_DIR/.uai-write-probe.XXXXXX") || {
254
+ /bin/rm -f -- "$prefix_probe" "$cache_probe"
255
+ log "error: private npm logs directory is not writable"
256
+ return 1
257
+ }
258
+ /bin/rm -f -- "$prefix_probe" "$cache_probe" "$logs_probe"
259
+ }
260
+
261
+ reject_workspace_runtime_shadow() {
262
+ local shadow="$WORKSPACE/.tool-versions"
263
+ # The root is never a selected project. Any entry here changes asdf lookup
264
+ # for no-manifest projects without being part of the pre-start proof. Check
265
+ # metadata only so a FIFO/socket/device cannot block initialization.
266
+ if [ -L "$shadow" ] || [ -e "$shadow" ]; then
267
+ log "error: workspace-root .tool-versions appeared after runtime proof; refusing dependency or editor startup"
268
+ return 1
269
+ fi
270
+ }
271
+
272
+ # Fail before sourcing asdf or executing any project input if this is an old
273
+ # writable task mount, or if task-private npm state redirects somewhere else.
274
+ reject_writable_shared_runtime_cache || exit 78
275
+ prepare_private_npm_authority || exit 78
276
+
30
277
  # Make asdf + its shims available to this non-interactive shell. A bare
31
278
  # `docker exec` sources no profile, so source asdf explicitly here.
32
279
  if [ -f "$ASDF_SH" ]; then
33
280
  # shellcheck disable=SC1090
34
281
  . "$ASDF_SH"
35
282
  else
36
- log "warning: asdf not found at $ASDF_SH; runtime install will be skipped"
283
+ log "warning: asdf not found at $ASDF_SH; dependency runtime resolution may fail"
37
284
  fi
38
285
 
39
286
  # ---------------------------------------------------------------------------
@@ -98,7 +345,7 @@ if [ ! -f "$cc_settings" ]; then
98
345
  fi
99
346
 
100
347
  # ---------------------------------------------------------------------------
101
- # 1. Per-folder runtime + dependency install.
348
+ # 1. Per-folder dependency install (runtimes are already materialized).
102
349
  # ---------------------------------------------------------------------------
103
350
 
104
351
  install_node_deps() {
@@ -117,10 +364,6 @@ install_node_deps() {
117
364
  fi
118
365
  fi
119
366
 
120
- # corepack ships with Node and resolves pnpm/yarn per the packageManager
121
- # field; harmless no-op when already enabled.
122
- corepack enable >/dev/null 2>&1 || true
123
-
124
367
  case "$pm" in
125
368
  pnpm) log "pnpm install"; pnpm install ;;
126
369
  yarn) log "yarn install"; yarn install ;;
@@ -148,20 +391,14 @@ install_python_deps() {
148
391
  fi
149
392
  }
150
393
 
151
- install_folder() {
394
+ install_folder_deps() {
152
395
  local dir="$1" name
153
396
  name=$(basename "$dir")
154
397
 
155
- # Enter the folder; asdf auto-resolves the nearest .tool-versions here.
398
+ # Enter the folder; asdf auto-resolves either its already-materialized local
399
+ # declarations or the already-proven root image defaults.
156
400
  cd "$dir" || { log "cannot cd into $name — skipping"; return; }
157
401
 
158
- # Runtime install. asdf reads the in-scope .tool-versions and downloads any
159
- # missing versions (cached on the /opt/asdf-data volume).
160
- if command -v asdf >/dev/null 2>&1; then
161
- log "asdf install ($name)"
162
- asdf install || log "asdf install failed in $name — continuing"
163
- fi
164
-
165
402
  # Dependency-install heuristic. Run every matcher that applies (a polyglot
166
403
  # folder may have more than one); each is independently best-effort.
167
404
  if [ -f package.json ]; then
@@ -180,23 +417,43 @@ install_folder() {
180
417
  fi
181
418
  }
182
419
 
183
- if [ -d "$WORKSPACE" ]; then
184
- shopt -s nullglob
185
- for folder in "$WORKSPACE"/*/; do
186
- [ -d "$folder" ] || continue
187
- if [ -f "${folder}.tool-versions" ]; then
188
- # Run in a subshell so a `cd` (or a failing command under the relaxed
189
- # error mode) in one folder never leaks into the next.
190
- ( install_folder "$folder" )
191
- else
192
- log "no .tool-versions in $(basename "$folder") — skipping per-folder install"
420
+ reject_workspace_runtime_shadow || exit 78
421
+
422
+ if [ -d "$WORKSPACE" ] && [ -f "$RUNTIME_PROJECTS" ] \
423
+ && [ ! -L "$RUNTIME_PROJECTS" ]; then
424
+ seen_runtime_projects="|"
425
+ while IFS= read -r project_slug || [ -n "$project_slug" ]; do
426
+ if [[ ! "$project_slug" =~ ^[a-z0-9-]{1,64}$ ]]; then
427
+ log "warning: unsafe runtime-project allowlist entry — skipping"
428
+ continue
193
429
  fi
194
- done
195
- shopt -u nullglob
196
- else
430
+ case "$seen_runtime_projects" in
431
+ *"|$project_slug|"*)
432
+ log "warning: duplicate runtime-project entry $project_slug — skipping"
433
+ continue
434
+ ;;
435
+ esac
436
+ seen_runtime_projects="${seen_runtime_projects}${project_slug}|"
437
+ folder="$WORKSPACE/$project_slug"
438
+ git_marker="$folder/.git"
439
+ if [ -L "$folder" ] || [ ! -d "$folder" ] || [ -L "$git_marker" ] \
440
+ || { [ ! -f "$git_marker" ] && [ ! -d "$git_marker" ]; }; then
441
+ log "warning: allowlisted project $project_slug is not a safe Git worktree — skipping"
442
+ continue
443
+ fi
444
+ reject_workspace_runtime_shadow || exit 78
445
+ # Run in a subshell so a `cd` (or a failing command under the relaxed
446
+ # error mode) in one folder never leaks into the next.
447
+ ( install_folder_deps "$folder" )
448
+ done < "$RUNTIME_PROJECTS"
449
+ elif [ ! -d "$WORKSPACE" ]; then
197
450
  log "workspace $WORKSPACE missing — scratchpad/no-project task, skipping installs"
451
+ else
452
+ log "warning: trusted runtime-project allowlist missing or unsafe — skipping installs"
198
453
  fi
199
454
 
455
+ reject_workspace_runtime_shadow || exit 78
456
+
200
457
  # ---------------------------------------------------------------------------
201
458
  # 2. code-server — the Editor pane. Bound to 0.0.0.0:8080, auth disabled
202
459
  # (reached only through the authenticated tunnel / editor proxy). Opens
@@ -263,7 +520,20 @@ if pgrep -f 'code-server.*--bind-addr' >/dev/null 2>&1; then
263
520
  log "code-server already running"
264
521
  else
265
522
  log "launching code-server on 0.0.0.0:8080"
266
- nohup code-server \
523
+ # Compose deliberately replaces the image PATH with Uai's runtime authority
524
+ # path, which excludes task-writable home bins. code-server is an image-owned
525
+ # standalone install, so invoke its exact baked path instead of relying on
526
+ # PATH. A source-tree test invocation may resolve its isolated stub, but the
527
+ # two production destinations never consult PATH for this executable.
528
+ code_server_bin=/home/node/.local/bin/code-server
529
+ case "${BASH_SOURCE[0]}" in
530
+ /usr/local/bin/uai-init | /uai-init-current) ;;
531
+ *)
532
+ test_code_server_bin=$(command -v code-server 2>/dev/null || true)
533
+ [ -z "$test_code_server_bin" ] || code_server_bin="$test_code_server_bin"
534
+ ;;
535
+ esac
536
+ nohup "$code_server_bin" \
267
537
  --user-data-dir "$cs_data_dir" \
268
538
  --disable-workspace-trust \
269
539
  --auth none \