@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.
- package/README.md +22 -5
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +80 -30
- package/images/standard/Dockerfile +36 -10
- package/images/standard/README.md +63 -18
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +308 -38
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/lib/agent-cli.ts +69 -4
- package/lib/agent.ts +46 -7
- package/lib/agents/claude.ts +13 -8
- package/lib/agents/codex.ts +11 -6
- package/lib/agents/cursor.ts +39 -29
- package/lib/agents/durable-proc.ts +20 -27
- package/lib/agents/factory.ts +9 -25
- package/lib/agents/grok.ts +43 -30
- package/lib/agents/kimi.ts +44 -29
- package/lib/agents/opencode.ts +43 -31
- package/lib/agents/proc.ts +149 -114
- package/lib/agents/transport.ts +62 -50
- package/lib/agents/types.ts +6 -4
- package/lib/apple-runtime-recycle.ts +236 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/browser-testing.ts +233 -93
- package/lib/codex-auth.ts +40 -6
- package/lib/command-db.ts +20 -0
- package/lib/container-runtime.ts +1338 -0
- package/lib/db.ts +1 -0
- package/lib/docker-exec.ts +87 -5
- package/lib/engine-accounts.ts +68 -5
- package/lib/engine-login.ts +1952 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +155 -0
- package/lib/env.ts +4 -0
- package/lib/git-diff.ts +98 -32
- package/lib/git-identity.ts +199 -87
- package/lib/github-tokens.ts +202 -91
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-logs.ts +962 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +49 -0
- package/lib/managed-runtime.ts +3644 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-gateway.ts +450 -23
- package/lib/orchestrator.ts +3070 -223
- package/lib/preview-sidecar.ts +57 -13
- package/lib/release-manifest.ts +708 -0
- package/lib/release-trust.ts +28 -0
- package/lib/runtime-activation-tail.ts +232 -0
- package/lib/runtime-archive.ts +1086 -0
- package/lib/runtime-authority.ts +79 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-state.ts +232 -12
- package/lib/skills.ts +24 -3
- package/lib/ssh.ts +18 -0
- package/lib/standard-image.ts +1104 -141
- package/lib/stopped-task-status-queue.ts +44 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-diff.ts +66 -46
- package/lib/task-environment/apple-container.ts +757 -0
- package/lib/task-environment/docker.ts +945 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +443 -0
- package/lib/task-environment/registry.ts +58 -0
- package/lib/task-environment/types.ts +408 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +585 -0
- package/lib/tunnel-registry.ts +135 -19
- package/lib/tunnel-runtime.ts +235 -0
- package/package.json +1 -1
- package/scripts/agent/_common.sh +123 -3
- package/scripts/agent/task-down.sh +146 -38
- package/scripts/agent/task-status.sh +19 -3
- package/scripts/agent/task-up.sh +1463 -109
- package/scripts/install/darwin.ts +848 -50
- package/scripts/install/linux.ts +838 -35
- package/scripts/install/types.ts +43 -0
- package/scripts/install/util.ts +215 -8
- package/scripts/install/win.ts +12 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/cli.ts +1464 -72
- package/src/event-outbox.ts +83 -4
- package/src/index.ts +871 -50
- package/src/main.ts +1398 -255
- package/src/paths.ts +17 -1
- package/src/protocol.ts +695 -1
- package/src/runtime-bootstrap.ts +165 -0
- package/src/ui/server.ts +46 -10
- package/src/ui/types.ts +37 -0
package/scripts/agent/task-up.sh
CHANGED
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
# branch, seed .tool-versions when missing.
|
|
14
14
|
# 4. Generate .uai/docker-compose.yml in bash (and a derived .uai/Dockerfile
|
|
15
15
|
# when any project declares `extra`).
|
|
16
|
-
# 5.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# 5. Build the derived image when needed, then materialize every runtime and
|
|
17
|
+
# package-manager default needed by the selected projects through the
|
|
18
|
+
# exact image the task will run.
|
|
19
|
+
# 6. docker compose -p task-<id> up -d (without rebuilding).
|
|
20
|
+
# 7. docker cp AI auth into the container, then docker exec uai-init.
|
|
21
|
+
# 8. Discover code-server + preview ports; mark status=running; unlock.
|
|
19
22
|
#
|
|
20
23
|
# The AIs are NOT started here — the chat orchestrator drives them and
|
|
21
24
|
# delivers the task's initial prompt once the task is running.
|
|
@@ -34,10 +37,33 @@ setup_err_trap
|
|
|
34
37
|
# Pinned constants (all clusters must agree — see ADR-022).
|
|
35
38
|
STANDARD_IMAGE="uai-standard:dev"
|
|
36
39
|
ASDF_VOLUME="uai-asdf-data"
|
|
40
|
+
TASK_NPM_PREFIX="/home/node/.local/share/uai-npm"
|
|
41
|
+
TASK_NPM_CACHE="/home/node/.cache/uai-npm"
|
|
42
|
+
# Host-proved shims and system controls always win. A task-global package with
|
|
43
|
+
# a new command name remains discoverable at the final path component, while a
|
|
44
|
+
# package named node/claude/git/sh cannot intercept managed or agent startup.
|
|
45
|
+
TASK_RUNTIME_PATH="/opt/asdf-data/shims:/opt/asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$TASK_NPM_PREFIX/bin"
|
|
37
46
|
# ADR-053: host-wide Playwright browser cache (Chromium downloads once).
|
|
38
47
|
PW_VOLUME="uai-playwright"
|
|
48
|
+
# ADR-106: on the Apple runtime the cache is a host DIRECTORY instead. Apple
|
|
49
|
+
# block volumes allow exactly ONE writable attachment, and session start
|
|
50
|
+
# writes browsers from inside every task — a shared writable volume let the
|
|
51
|
+
# first task exclude every later `container run` (live 2026-08-17). Virtiofs
|
|
52
|
+
# directory shares carry no exclusivity, like /workspace itself.
|
|
53
|
+
PW_BROWSERS_DIR="$UAI_WORKSPACE_ROOT/pw-browsers"
|
|
39
54
|
DERIVED_IMAGE="uai-task-${task_id}"
|
|
40
55
|
|
|
56
|
+
# ADR-106: runtime selection is shared with task-down/task-status via
|
|
57
|
+
# _common.sh; `docker` keeps the historical Compose path byte-for-byte, while
|
|
58
|
+
# `apple-container` drives the bundled Apple CLI directly (no Compose, no
|
|
59
|
+
# published ports — tasks are reached at their vmnet IP).
|
|
60
|
+
validate_task_runtime_selection
|
|
61
|
+
# Per-container VM sizing (Apple runtime only; Docker tasks share the daemon
|
|
62
|
+
# VM). Overridable for constrained hosts; ADR-106 picked these defaults for a
|
|
63
|
+
# dev task with language servers + builds.
|
|
64
|
+
APPLE_TASK_MEMORY="${UAI_APPLE_TASK_MEMORY:-8192M}"
|
|
65
|
+
APPLE_TASK_CPUS="${UAI_APPLE_TASK_CPUS:-4}"
|
|
66
|
+
|
|
41
67
|
# Project-defined environment values arrive as one JSON object on stdin. They
|
|
42
68
|
# are deliberately NOT part of this host process environment: names such as
|
|
43
69
|
# BASH_ENV, PATH, GIT_CONFIG_*, and UAI_* must not influence task setup or the
|
|
@@ -60,6 +86,8 @@ task_uai_dir="$task_dir/.uai"
|
|
|
60
86
|
projects_root="$UAI_WORKSPACE_ROOT/projects"
|
|
61
87
|
compose_project=$(compose_project_for_task "$task_id")
|
|
62
88
|
app_container=$(app_container_for_task "$task_id")
|
|
89
|
+
[ "$TASK_RUNTIME_MODE" != "apple-container" ] \
|
|
90
|
+
|| app_container=$(apple_app_container_for_task "$task_id")
|
|
63
91
|
|
|
64
92
|
reject_unsafe_host_directory() {
|
|
65
93
|
local path="$1" label="$2"
|
|
@@ -70,6 +98,36 @@ reject_unsafe_host_directory() {
|
|
|
70
98
|
fi
|
|
71
99
|
}
|
|
72
100
|
|
|
101
|
+
reject_unsafe_runtime_manifest() {
|
|
102
|
+
local path="$1" project_slug="$2" project_id="$3"
|
|
103
|
+
if [ -L "$path" ]; then
|
|
104
|
+
emit_err "WORKTREE_FAILED" \
|
|
105
|
+
"Uai refused a symbolic-link .tool-versions for $project_slug" \
|
|
106
|
+
"validate task worktree ($project_id)"
|
|
107
|
+
fi
|
|
108
|
+
# Never open an untrusted special file: `-f` is a metadata-only check, so a
|
|
109
|
+
# FIFO/socket/device cannot block the host while it validates a resumed
|
|
110
|
+
# container-writable workspace.
|
|
111
|
+
if [ -e "$path" ] && [ ! -f "$path" ]; then
|
|
112
|
+
emit_err "WORKTREE_FAILED" \
|
|
113
|
+
"Uai refused a non-regular .tool-versions for $project_slug" \
|
|
114
|
+
"validate task worktree ($project_id)"
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
reject_workspace_runtime_shadow() {
|
|
119
|
+
local path="$task_workspace/.tool-versions"
|
|
120
|
+
# The workspace root is not a project. Any entry here would shadow the
|
|
121
|
+
# image defaults for a no-manifest project (and even a scratchpad command),
|
|
122
|
+
# while the trusted materializer proves only the selected project paths.
|
|
123
|
+
# Metadata-only predicates keep FIFO/device/socket cases nonblocking.
|
|
124
|
+
if [ -L "$path" ] || [ -e "$path" ]; then
|
|
125
|
+
emit_err "WORKTREE_FAILED" \
|
|
126
|
+
"Uai refused a workspace-root .tool-versions because it could override the runtimes proved for this task" \
|
|
127
|
+
"validate task workspace runtime authority"
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
|
|
73
131
|
mapped_host_port() {
|
|
74
132
|
local container="$1" container_port="$2"
|
|
75
133
|
docker port "$container" "$container_port" 2>/dev/null \
|
|
@@ -78,6 +136,292 @@ mapped_host_port() {
|
|
|
78
136
|
|| true
|
|
79
137
|
}
|
|
80
138
|
|
|
139
|
+
# A failed Docker call cannot be followed by an unbounded health probe: the
|
|
140
|
+
# same stuck socket would defeat the operation's own watchdog. This portable
|
|
141
|
+
# Bash 3.2 wrapper gives `docker info` 15 seconds, writes completion before its
|
|
142
|
+
# exact wrapper PID disappears, and reaps every helper on success or timeout.
|
|
143
|
+
docker_daemon_reachable_bounded() {
|
|
144
|
+
local probe_dir wrapper_pid watchdog_pid probe_rc=0 watchdog_sleep_pid
|
|
145
|
+
|
|
146
|
+
probe_dir=$(mktemp -d "$task_uai_dir/.docker-info-watchdog.XXXXXX")
|
|
147
|
+
(
|
|
148
|
+
runtime_probe_pid=""
|
|
149
|
+
finish_runtime_probe() {
|
|
150
|
+
local wrapper_rc=$?
|
|
151
|
+
trap - EXIT
|
|
152
|
+
: > "$probe_dir/completed"
|
|
153
|
+
exit "$wrapper_rc"
|
|
154
|
+
}
|
|
155
|
+
terminate_runtime_probe() {
|
|
156
|
+
local signal_rc="$1"
|
|
157
|
+
trap - TERM INT
|
|
158
|
+
if [ -n "$runtime_probe_pid" ]; then
|
|
159
|
+
kill -TERM "$runtime_probe_pid" >/dev/null 2>&1 || true
|
|
160
|
+
sleep 2
|
|
161
|
+
kill -KILL "$runtime_probe_pid" >/dev/null 2>&1 || true
|
|
162
|
+
wait "$runtime_probe_pid" 2>/dev/null || true
|
|
163
|
+
fi
|
|
164
|
+
exit "$signal_rc"
|
|
165
|
+
}
|
|
166
|
+
trap finish_runtime_probe EXIT
|
|
167
|
+
trap 'terminate_runtime_probe 143' TERM
|
|
168
|
+
trap 'terminate_runtime_probe 130' INT
|
|
169
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
170
|
+
"$CONTAINER_CLI" system status >/dev/null 2>&1 &
|
|
171
|
+
else
|
|
172
|
+
docker info --format '{{.ServerVersion}}' >/dev/null 2>&1 &
|
|
173
|
+
fi
|
|
174
|
+
runtime_probe_pid=$!
|
|
175
|
+
if wait "$runtime_probe_pid"; then exit 0; else exit $?; fi
|
|
176
|
+
) &
|
|
177
|
+
wrapper_pid=$!
|
|
178
|
+
|
|
179
|
+
(
|
|
180
|
+
watchdog_sleep_pid=""
|
|
181
|
+
cancel_probe_watchdog() {
|
|
182
|
+
trap - TERM INT
|
|
183
|
+
if [ -n "$watchdog_sleep_pid" ]; then
|
|
184
|
+
kill -TERM "$watchdog_sleep_pid" >/dev/null 2>&1 || true
|
|
185
|
+
wait "$watchdog_sleep_pid" 2>/dev/null || true
|
|
186
|
+
fi
|
|
187
|
+
exit 0
|
|
188
|
+
}
|
|
189
|
+
trap cancel_probe_watchdog TERM INT
|
|
190
|
+
sleep 15 &
|
|
191
|
+
watchdog_sleep_pid=$!
|
|
192
|
+
if ! wait "$watchdog_sleep_pid"; then exit 0; fi
|
|
193
|
+
watchdog_sleep_pid=""
|
|
194
|
+
if [ ! -f "$probe_dir/completed" ] \
|
|
195
|
+
&& kill -0 "$wrapper_pid" >/dev/null 2>&1; then
|
|
196
|
+
: > "$probe_dir/timed-out"
|
|
197
|
+
kill -TERM "$wrapper_pid" >/dev/null 2>&1 || true
|
|
198
|
+
sleep 5 &
|
|
199
|
+
watchdog_sleep_pid=$!
|
|
200
|
+
wait "$watchdog_sleep_pid" 2>/dev/null || true
|
|
201
|
+
watchdog_sleep_pid=""
|
|
202
|
+
if [ ! -f "$probe_dir/completed" ]; then
|
|
203
|
+
kill -KILL "$wrapper_pid" >/dev/null 2>&1 || true
|
|
204
|
+
fi
|
|
205
|
+
fi
|
|
206
|
+
) &
|
|
207
|
+
watchdog_pid=$!
|
|
208
|
+
|
|
209
|
+
if wait "$wrapper_pid"; then probe_rc=0; else probe_rc=$?; fi
|
|
210
|
+
kill -TERM "$watchdog_pid" >/dev/null 2>&1 || true
|
|
211
|
+
wait "$watchdog_pid" 2>/dev/null || true
|
|
212
|
+
[ ! -f "$probe_dir/timed-out" ] || probe_rc=124
|
|
213
|
+
rm -f "$probe_dir/completed" "$probe_dir/timed-out"
|
|
214
|
+
rmdir "$probe_dir" 2>/dev/null || true
|
|
215
|
+
return "$probe_rc"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# A Docker-dependent step can fail for its own, degradable reason (for example,
|
|
219
|
+
# uai-init exits nonzero while the container remains healthy) or because the
|
|
220
|
+
# daemon disappeared after TypeScript admitted task-up. Only the latter is
|
|
221
|
+
# retryable host infrastructure state. Re-probe before translating a failed
|
|
222
|
+
# Docker step into an operation-specific error or warning so task-up never
|
|
223
|
+
# publishes status=running against an unknown backend.
|
|
224
|
+
require_docker_after_failed_step() {
|
|
225
|
+
local step_label="$1" runtime_name="Docker"
|
|
226
|
+
[ "$TASK_RUNTIME_MODE" != "apple-container" ] \
|
|
227
|
+
|| runtime_name="the Apple container runtime"
|
|
228
|
+
if ! docker_daemon_reachable_bounded; then
|
|
229
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
230
|
+
"The container runtime stopped responding during task startup. Retry after $runtime_name is running again." \
|
|
231
|
+
"$step_label"
|
|
232
|
+
fi
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Stop an app that failed a post-create authority proof and verify it is no
|
|
236
|
+
# longer running before any credential, dependency, editor, or agent work can
|
|
237
|
+
# begin. Preserve the container/worktree for diagnosis; a retry recreates the
|
|
238
|
+
# container from Uai's current Compose contract.
|
|
239
|
+
quarantine_untrusted_app_container() {
|
|
240
|
+
local authority_running=""
|
|
241
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
242
|
+
# `container stop` is idempotent; proven absence quarantines as well as a
|
|
243
|
+
# stopped state does. An inspect that fails WITHOUT naming this container
|
|
244
|
+
# as not-found proves nothing and must not read as stopped.
|
|
245
|
+
if ! crt stop --time 30 "$app_container" >/dev/null 2>&1; then
|
|
246
|
+
require_docker_after_failed_step "quarantine untrusted task container"
|
|
247
|
+
fi
|
|
248
|
+
apple_quarantine_proven=""
|
|
249
|
+
quarantine_state=$(crt inspect "$app_container" 2>/dev/null \
|
|
250
|
+
| jq -r '.[0].status.state // empty' 2>/dev/null) || quarantine_state=""
|
|
251
|
+
if [ -n "$quarantine_state" ] && [ "$quarantine_state" != "running" ]; then
|
|
252
|
+
apple_quarantine_proven=1
|
|
253
|
+
elif [ -z "$quarantine_state" ] \
|
|
254
|
+
&& apple_container_proven_absent "$app_container"; then
|
|
255
|
+
apple_quarantine_proven=1
|
|
256
|
+
fi
|
|
257
|
+
if [ -z "$apple_quarantine_proven" ]; then
|
|
258
|
+
crt stop --signal KILL "$app_container" >/dev/null 2>&1 || true
|
|
259
|
+
quarantine_state=$(crt inspect "$app_container" 2>/dev/null \
|
|
260
|
+
| jq -r '.[0].status.state // empty' 2>/dev/null) || quarantine_state=""
|
|
261
|
+
if [ -n "$quarantine_state" ] && [ "$quarantine_state" != "running" ]; then
|
|
262
|
+
apple_quarantine_proven=1
|
|
263
|
+
elif [ -z "$quarantine_state" ] \
|
|
264
|
+
&& apple_container_proven_absent "$app_container"; then
|
|
265
|
+
apple_quarantine_proven=1
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
[ -n "$apple_quarantine_proven" ]
|
|
269
|
+
return
|
|
270
|
+
fi
|
|
271
|
+
if ! docker stop --time 30 "$app_container" >/dev/null 2>&1; then
|
|
272
|
+
require_docker_after_failed_step "quarantine untrusted task container"
|
|
273
|
+
fi
|
|
274
|
+
authority_running=$(docker ps -q --filter "name=^/${app_container}$") \
|
|
275
|
+
|| require_docker_after_failed_step "verify task-container quarantine"
|
|
276
|
+
if [ -n "$authority_running" ]; then
|
|
277
|
+
docker kill "$app_container" >/dev/null 2>&1 || true
|
|
278
|
+
authority_running=$(docker ps -q --filter "name=^/${app_container}$") \
|
|
279
|
+
|| require_docker_after_failed_step "verify task-container quarantine"
|
|
280
|
+
fi
|
|
281
|
+
[ -z "$authority_running" ]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# Run the maintenance container under two independent deadlines. The inner GNU
|
|
285
|
+
# timeout bounds the actual writer (including the shared-volume lock wait); the
|
|
286
|
+
# host watchdog is slightly looser and covers a Docker CLI that hangs before it
|
|
287
|
+
# creates or starts the container. Bash 3.2 has no `wait -n` or `timeout`, so
|
|
288
|
+
# keep and reap both exact child PIDs explicitly.
|
|
289
|
+
run_runtime_materializer() {
|
|
290
|
+
local runtime_image="$1" wrapper_pid watchdog_pid materialization_rc=0
|
|
291
|
+
local watchdog_dir watchdog_sleep_pid
|
|
292
|
+
|
|
293
|
+
watchdog_dir=$(mktemp -d "$task_uai_dir/.runtime-materializer-watchdog.XXXXXX")
|
|
294
|
+
|
|
295
|
+
# The completion marker is written from the wrapper's EXIT trap before its
|
|
296
|
+
# PID can disappear. The watchdog consults it before every signal, closing
|
|
297
|
+
# the wait/reap/PID-reuse race around a just-finished Docker client.
|
|
298
|
+
(
|
|
299
|
+
runtime_docker_pid=""
|
|
300
|
+
finish_runtime_wrapper() {
|
|
301
|
+
local wrapper_rc=$?
|
|
302
|
+
trap - EXIT
|
|
303
|
+
: > "$watchdog_dir/completed"
|
|
304
|
+
exit "$wrapper_rc"
|
|
305
|
+
}
|
|
306
|
+
terminate_runtime_wrapper() {
|
|
307
|
+
local signal_rc="$1"
|
|
308
|
+
trap - TERM INT
|
|
309
|
+
if [ -n "$runtime_docker_pid" ]; then
|
|
310
|
+
# Do not reap before the grace period: the exact child PID therefore
|
|
311
|
+
# cannot be reused between TERM and KILL.
|
|
312
|
+
kill -TERM "$runtime_docker_pid" >/dev/null 2>&1 || true
|
|
313
|
+
sleep 10
|
|
314
|
+
kill -KILL "$runtime_docker_pid" >/dev/null 2>&1 || true
|
|
315
|
+
wait "$runtime_docker_pid" 2>/dev/null || true
|
|
316
|
+
fi
|
|
317
|
+
exit "$signal_rc"
|
|
318
|
+
}
|
|
319
|
+
trap finish_runtime_wrapper EXIT
|
|
320
|
+
trap 'terminate_runtime_wrapper 143' TERM
|
|
321
|
+
trap 'terminate_runtime_wrapper 130' INT
|
|
322
|
+
|
|
323
|
+
# ADR-108: on the Apple runtime this function is never called —
|
|
324
|
+
# materialization runs inside the app container against its in-guest
|
|
325
|
+
# overlay. This maintenance-container path is Docker-only.
|
|
326
|
+
docker run --rm --user node --group-add "$task_container_gid" \
|
|
327
|
+
--workdir /workspace \
|
|
328
|
+
-v "$task_workspace:/workspace:ro" \
|
|
329
|
+
-v "$ASDF_VOLUME:/opt/asdf-data:rw" \
|
|
330
|
+
-v "$RUNTIME_MATERIALIZER_SOURCE:/usr/local/bin/uai-materialize-runtimes:ro" \
|
|
331
|
+
-v "$COREPACK_VERSION_SOURCE:/usr/local/share/uai/corepack-version:ro" \
|
|
332
|
+
-v "$runtime_projects_file:/run/uai/runtime-projects:ro" \
|
|
333
|
+
--env LD_PRELOAD= \
|
|
334
|
+
--env LD_AUDIT= \
|
|
335
|
+
--env LD_LIBRARY_PATH= \
|
|
336
|
+
--env BASH_ENV= \
|
|
337
|
+
--env ENV= \
|
|
338
|
+
--entrypoint /usr/bin/env \
|
|
339
|
+
"$runtime_image" \
|
|
340
|
+
-i \
|
|
341
|
+
HOME=/home/node \
|
|
342
|
+
ASDF_DIR=/opt/asdf \
|
|
343
|
+
ASDF_DATA_DIR=/opt/asdf-data \
|
|
344
|
+
PATH=/opt/asdf-data/shims:/opt/asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
|
345
|
+
/usr/bin/timeout --signal=TERM --kill-after=10s 600s \
|
|
346
|
+
/usr/local/bin/uai-materialize-runtimes >&2 &
|
|
347
|
+
runtime_docker_pid=$!
|
|
348
|
+
if wait "$runtime_docker_pid"; then
|
|
349
|
+
exit 0
|
|
350
|
+
else
|
|
351
|
+
exit $?
|
|
352
|
+
fi
|
|
353
|
+
) &
|
|
354
|
+
wrapper_pid=$!
|
|
355
|
+
|
|
356
|
+
(
|
|
357
|
+
watchdog_sleep_pid=""
|
|
358
|
+
cancel_runtime_watchdog() {
|
|
359
|
+
trap - TERM INT
|
|
360
|
+
if [ -n "$watchdog_sleep_pid" ]; then
|
|
361
|
+
kill -TERM "$watchdog_sleep_pid" >/dev/null 2>&1 || true
|
|
362
|
+
wait "$watchdog_sleep_pid" 2>/dev/null || true
|
|
363
|
+
fi
|
|
364
|
+
exit 0
|
|
365
|
+
}
|
|
366
|
+
trap cancel_runtime_watchdog TERM INT
|
|
367
|
+
|
|
368
|
+
sleep 620 &
|
|
369
|
+
watchdog_sleep_pid=$!
|
|
370
|
+
if ! wait "$watchdog_sleep_pid"; then
|
|
371
|
+
exit 0
|
|
372
|
+
fi
|
|
373
|
+
watchdog_sleep_pid=""
|
|
374
|
+
|
|
375
|
+
if [ ! -f "$watchdog_dir/completed" ] \
|
|
376
|
+
&& kill -0 "$wrapper_pid" >/dev/null 2>&1; then
|
|
377
|
+
: > "$watchdog_dir/timed-out"
|
|
378
|
+
kill -TERM "$wrapper_pid" >/dev/null 2>&1 || true
|
|
379
|
+
# The wrapper gives its exact Docker child 10 seconds before KILL. This
|
|
380
|
+
# slightly looser guard is only a backstop for a wedged signal handler.
|
|
381
|
+
sleep 15 &
|
|
382
|
+
watchdog_sleep_pid=$!
|
|
383
|
+
wait "$watchdog_sleep_pid" 2>/dev/null || true
|
|
384
|
+
watchdog_sleep_pid=""
|
|
385
|
+
if [ ! -f "$watchdog_dir/completed" ]; then
|
|
386
|
+
kill -KILL "$wrapper_pid" >/dev/null 2>&1 || true
|
|
387
|
+
fi
|
|
388
|
+
fi
|
|
389
|
+
) &
|
|
390
|
+
watchdog_pid=$!
|
|
391
|
+
|
|
392
|
+
cancel_runtime_materializer() {
|
|
393
|
+
local signal_rc="$1"
|
|
394
|
+
trap - TERM INT
|
|
395
|
+
if [ ! -f "$watchdog_dir/completed" ]; then
|
|
396
|
+
kill -TERM "$wrapper_pid" >/dev/null 2>&1 || true
|
|
397
|
+
fi
|
|
398
|
+
kill -TERM "$watchdog_pid" >/dev/null 2>&1 || true
|
|
399
|
+
wait "$watchdog_pid" 2>/dev/null || true
|
|
400
|
+
wait "$wrapper_pid" 2>/dev/null || true
|
|
401
|
+
rm -f "$watchdog_dir/completed" "$watchdog_dir/timed-out"
|
|
402
|
+
rmdir "$watchdog_dir" 2>/dev/null || true
|
|
403
|
+
exit "$signal_rc"
|
|
404
|
+
}
|
|
405
|
+
trap 'cancel_runtime_materializer 143' TERM
|
|
406
|
+
trap 'cancel_runtime_materializer 130' INT
|
|
407
|
+
|
|
408
|
+
if wait "$wrapper_pid"; then
|
|
409
|
+
materialization_rc=0
|
|
410
|
+
else
|
|
411
|
+
materialization_rc=$?
|
|
412
|
+
fi
|
|
413
|
+
kill -TERM "$watchdog_pid" >/dev/null 2>&1 || true
|
|
414
|
+
wait "$watchdog_pid" 2>/dev/null || true
|
|
415
|
+
|
|
416
|
+
trap - TERM INT
|
|
417
|
+
if [ -f "$watchdog_dir/timed-out" ]; then
|
|
418
|
+
materialization_rc=124
|
|
419
|
+
fi
|
|
420
|
+
rm -f "$watchdog_dir/completed" "$watchdog_dir/timed-out"
|
|
421
|
+
rmdir "$watchdog_dir" 2>/dev/null || true
|
|
422
|
+
return "$materialization_rc"
|
|
423
|
+
}
|
|
424
|
+
|
|
81
425
|
# -----------------------------------------------------------------------------
|
|
82
426
|
# 1. Read rows.
|
|
83
427
|
# -----------------------------------------------------------------------------
|
|
@@ -88,6 +432,7 @@ task_json=$(db_get_task "$task_id")
|
|
|
88
432
|
|| emit_err "TASK_NOT_FOUND" "no such task: $task_id" "read task row"
|
|
89
433
|
|
|
90
434
|
task_branch=$(jq -r '.[0].branch' <<<"$task_json")
|
|
435
|
+
UAI_RETRY_STATUS=$(jq -r '.[0].status // ""' <<<"$task_json")
|
|
91
436
|
|
|
92
437
|
# The standard container runs as node:node (1000:1000), while Docker Desktop
|
|
93
438
|
# hosts commonly run as 501:20 and Linux hosts may use another uid/gid. Share
|
|
@@ -117,6 +462,18 @@ esac
|
|
|
117
462
|
step "PROJECT_NOT_FOUND" "read task projects"
|
|
118
463
|
projects_json=$(db_get_projects_for_task "$task_id")
|
|
119
464
|
project_count=$(jq 'length' <<<"$projects_json")
|
|
465
|
+
if ! jq -e '
|
|
466
|
+
map(.slug) as $slugs
|
|
467
|
+
| all($slugs[];
|
|
468
|
+
type == "string"
|
|
469
|
+
and test("^[a-z0-9-]{1,64}$"))
|
|
470
|
+
and (($slugs | unique | length) == ($slugs | length))
|
|
471
|
+
' <<<"$projects_json" >/dev/null; then
|
|
472
|
+
emit_err "WORKTREE_FAILED" \
|
|
473
|
+
"Uai refused an unsafe or duplicate project workspace slug" \
|
|
474
|
+
"validate task projects"
|
|
475
|
+
fi
|
|
476
|
+
runtime_project_slugs=$(jq -r '.[].slug' <<<"$projects_json")
|
|
120
477
|
|
|
121
478
|
# -----------------------------------------------------------------------------
|
|
122
479
|
# 2. Lock + status=starting.
|
|
@@ -145,27 +502,62 @@ reject_unsafe_host_directory "$task_uai_dir" "the task runtime directory"
|
|
|
145
502
|
# This also cuts off legacy containers that still have the old shared
|
|
146
503
|
# `repo.git` bind mount before we inspect their worktree layout below.
|
|
147
504
|
step "COMPOSE_DOWN_FAILED" "remove prior task containers"
|
|
148
|
-
if [
|
|
149
|
-
|
|
150
|
-
|
|
505
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
506
|
+
# Selection fencing guarantees an apple-mode host has no Docker task rows,
|
|
507
|
+
# so the only possible prior container is this task's fixed name. `inspect`
|
|
508
|
+
# exiting nonzero afterwards is the absence proof.
|
|
509
|
+
crt stop --time 10 "$app_container" >/dev/null 2>&1 || true
|
|
510
|
+
crt delete --force "$app_container" >/dev/null 2>&1 || true
|
|
511
|
+
if crt inspect "$app_container" >/dev/null 2>&1; then
|
|
512
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
513
|
+
"the previous task container is still present; Uai will not let the host access its writable Git repository until that container is removed" \
|
|
514
|
+
"remove prior task containers"
|
|
515
|
+
fi
|
|
516
|
+
if ! apple_container_proven_absent "$app_container"; then
|
|
517
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
518
|
+
"could not verify removal of the previous task container" \
|
|
519
|
+
"remove prior task containers"
|
|
520
|
+
fi
|
|
151
521
|
else
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
522
|
+
if [ -f "$task_uai_dir/docker-compose.yml" ]; then
|
|
523
|
+
docker compose -p "$compose_project" -f "$task_uai_dir/docker-compose.yml" \
|
|
524
|
+
down --remove-orphans >/dev/null 2>&1 || true
|
|
525
|
+
else
|
|
526
|
+
docker compose -p "$compose_project" down --remove-orphans \
|
|
527
|
+
>/dev/null 2>&1 || true
|
|
528
|
+
fi
|
|
529
|
+
prior_containers=""
|
|
530
|
+
if ! prior_containers=$(docker ps -aq \
|
|
531
|
+
--filter "label=com.docker.compose.project=$compose_project"); then
|
|
532
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
533
|
+
"could not verify removal of the previous task container" \
|
|
534
|
+
"remove prior task containers"
|
|
535
|
+
fi
|
|
536
|
+
if [ -n "$prior_containers" ]; then
|
|
537
|
+
emit_err "COMPOSE_DOWN_FAILED" \
|
|
538
|
+
"the previous task container is still running or stopped; Uai will not let the host access its writable Git repository until that container is removed" \
|
|
539
|
+
"remove prior task containers"
|
|
540
|
+
fi
|
|
166
541
|
fi
|
|
167
542
|
reject_unsafe_host_directory "$task_workspace" "the task workspace"
|
|
168
543
|
mkdir -p "$task_workspace"
|
|
544
|
+
reject_workspace_runtime_shadow
|
|
545
|
+
|
|
546
|
+
# Git resolves symlinked ancestors when it writes a linked worktree's absolute
|
|
547
|
+
# `.git` pointer. Keep the configured (lexical) task paths as Uai's stable host
|
|
548
|
+
# paths, but also remember their physical aliases for validation and bind-mount
|
|
549
|
+
# destinations. The task root itself was rejected above if it was a symlink;
|
|
550
|
+
# `pwd -P` therefore resolves only trusted ancestors such as a migrated
|
|
551
|
+
# UAI_WORKSPACE_ROOT (`~/.uai-host-app/workspace -> ~/.uai`).
|
|
552
|
+
step "WORKTREE_FAILED" "resolve task workspace paths"
|
|
553
|
+
if ! task_dir_physical=$(cd "$task_dir" && pwd -P) \
|
|
554
|
+
|| [ -z "$task_dir_physical" ]; then
|
|
555
|
+
emit_err "WORKTREE_FAILED" \
|
|
556
|
+
"Uai could not resolve the task workspace's physical host path" \
|
|
557
|
+
"resolve task workspace paths"
|
|
558
|
+
fi
|
|
559
|
+
task_workspace_physical="$task_dir_physical/workspace"
|
|
560
|
+
task_uai_dir_physical="$task_dir_physical/.uai"
|
|
169
561
|
|
|
170
562
|
# -----------------------------------------------------------------------------
|
|
171
563
|
# 3. Bare mirrors + worktrees, one per selected project.
|
|
@@ -486,16 +878,14 @@ while IFS= read -r project_obj; do
|
|
|
486
878
|
# that old bind mount. The new cache path has never entered a container.
|
|
487
879
|
mirror_dir="$projects_root/$project_id/host-cache.git"
|
|
488
880
|
task_repo_dir="$task_uai_dir/repos/$project_id.git"
|
|
881
|
+
task_repo_dir_physical="$task_uai_dir_physical/repos/$project_id.git"
|
|
489
882
|
worktree_target="$task_workspace/$project_slug"
|
|
490
883
|
reject_unsafe_host_directory "$worktree_target" \
|
|
491
884
|
"the project worktree for $project_slug"
|
|
492
885
|
# Check before remote/cache Git: a dangling symlink makes `-e` false and the
|
|
493
886
|
# later seed write would otherwise follow it outside the workspace.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
"Uai refused a symbolic-link .tool-versions for $project_slug" \
|
|
497
|
-
"validate task worktree ($project_id)"
|
|
498
|
-
fi
|
|
887
|
+
reject_unsafe_runtime_manifest "$worktree_target/.tool-versions" \
|
|
888
|
+
"$project_slug" "$project_id"
|
|
499
889
|
worktree_exists=0
|
|
500
890
|
standalone_worktree=0
|
|
501
891
|
if [ -e "$worktree_target/.git" ] || [ -L "$worktree_target/.git" ]; then
|
|
@@ -513,14 +903,18 @@ while IFS= read -r project_obj; do
|
|
|
513
903
|
gitdir_lines=$(awk 'END { print NR }' "$worktree_target/.git")
|
|
514
904
|
gitdir_value=$(sed -n 's/^gitdir: //p' "$worktree_target/.git")
|
|
515
905
|
case "$gitdir_value" in
|
|
516
|
-
"$task_repo_dir"/worktrees/*)
|
|
906
|
+
"$task_repo_dir"/worktrees/*)
|
|
907
|
+
gitdir_leaf=${gitdir_value#"$task_repo_dir"/worktrees/}
|
|
908
|
+
;;
|
|
909
|
+
"$task_repo_dir_physical"/worktrees/*)
|
|
910
|
+
gitdir_leaf=${gitdir_value#"$task_repo_dir_physical"/worktrees/}
|
|
911
|
+
;;
|
|
517
912
|
*)
|
|
518
913
|
emit_err "WORKTREE_FAILED" \
|
|
519
914
|
"This task uses Uai's legacy shared Git layout. Its container has been removed so it can no longer write the shared mirror. Preserve any uncommitted changes from $worktree_target, then recreate the task to migrate safely." \
|
|
520
915
|
"legacy task repository requires recreation ($project_id)"
|
|
521
916
|
;;
|
|
522
917
|
esac
|
|
523
|
-
gitdir_leaf=${gitdir_value#"$task_repo_dir"/worktrees/}
|
|
524
918
|
case "$gitdir_leaf" in
|
|
525
919
|
''|.|..|*/*)
|
|
526
920
|
emit_err "WORKTREE_FAILED" \
|
|
@@ -860,11 +1254,8 @@ while IFS= read -r project_obj; do
|
|
|
860
1254
|
# Tool-version seeding: only when the worktree has no checked-in
|
|
861
1255
|
# .tool-versions AND the project declares one. Never overwrite a file
|
|
862
1256
|
# the repo already ships.
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
"Uai refused a symbolic-link .tool-versions for $project_slug" \
|
|
866
|
-
"validate task worktree ($project_id)"
|
|
867
|
-
fi
|
|
1257
|
+
reject_unsafe_runtime_manifest "$worktree_target/.tool-versions" \
|
|
1258
|
+
"$project_slug" "$project_id"
|
|
868
1259
|
if [ ! -e "$worktree_target/.tool-versions" ] \
|
|
869
1260
|
&& [ -n "$tool_versions" ] && [ "$tool_versions" != "null" ]; then
|
|
870
1261
|
printf '%s\n' "$tool_versions" > "$worktree_target/.tool-versions"
|
|
@@ -936,6 +1327,53 @@ step "RENDER_FAILED" "generate compose"
|
|
|
936
1327
|
|
|
937
1328
|
mkdir -p "$task_uai_dir"
|
|
938
1329
|
|
|
1330
|
+
STANDARD_CONTAINER_DIR="${UAI_STANDARD_CONTAINER_ASSETS_DIR:-$SCRIPT_DIR/../../images/standard/container}"
|
|
1331
|
+
if ! STANDARD_CONTAINER_DIR=$(cd "$STANDARD_CONTAINER_DIR" && pwd -P); then
|
|
1332
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1333
|
+
"Uai's packaged container asset directory is missing. Repair or update the host installation, then retry." \
|
|
1334
|
+
"resolve packaged container assets"
|
|
1335
|
+
fi
|
|
1336
|
+
UAI_INIT_SOURCE="$STANDARD_CONTAINER_DIR/uai-init"
|
|
1337
|
+
RUNTIME_MATERIALIZER_SOURCE="$STANDARD_CONTAINER_DIR/uai-materialize-runtimes"
|
|
1338
|
+
COREPACK_VERSION_SOURCE="$STANDARD_CONTAINER_DIR/corepack-version"
|
|
1339
|
+
if [ -L "$UAI_INIT_SOURCE" ] || [ ! -f "$UAI_INIT_SOURCE" ] \
|
|
1340
|
+
|| [ ! -x "$UAI_INIT_SOURCE" ]; then
|
|
1341
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1342
|
+
"Uai's packaged task initializer is missing or unsafe. Repair or update the host installation, then retry." \
|
|
1343
|
+
"resolve packaged container assets"
|
|
1344
|
+
fi
|
|
1345
|
+
if [ "$project_count" -gt 0 ]; then
|
|
1346
|
+
if [ -L "$RUNTIME_MATERIALIZER_SOURCE" ] \
|
|
1347
|
+
|| [ ! -f "$RUNTIME_MATERIALIZER_SOURCE" ] \
|
|
1348
|
+
|| [ ! -x "$RUNTIME_MATERIALIZER_SOURCE" ]; then
|
|
1349
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1350
|
+
"Uai's packaged runtime materializer is missing or unsafe. Repair or update the host installation, then retry." \
|
|
1351
|
+
"resolve packaged container assets"
|
|
1352
|
+
fi
|
|
1353
|
+
if [ -L "$COREPACK_VERSION_SOURCE" ] \
|
|
1354
|
+
|| [ ! -f "$COREPACK_VERSION_SOURCE" ] \
|
|
1355
|
+
|| [ ! -r "$COREPACK_VERSION_SOURCE" ]; then
|
|
1356
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1357
|
+
"Uai's packaged Corepack version pin is missing or unsafe. Repair or update the host installation, then retry." \
|
|
1358
|
+
"resolve packaged container assets"
|
|
1359
|
+
fi
|
|
1360
|
+
fi
|
|
1361
|
+
|
|
1362
|
+
# The host-authored allowlist is the single project-set authority shared by
|
|
1363
|
+
# pre-start runtime proof and task-local dependency installation. It lives in
|
|
1364
|
+
# the container-unmounted .uai control tree and is exposed only as this one
|
|
1365
|
+
# read-only file. Atomic replacement keeps resumptions from observing a
|
|
1366
|
+
# partially written list.
|
|
1367
|
+
runtime_projects_file="$task_uai_dir/runtime-projects"
|
|
1368
|
+
runtime_projects_tmp=$(mktemp "$task_uai_dir/.runtime-projects.XXXXXX")
|
|
1369
|
+
if [ -n "$runtime_project_slugs" ]; then
|
|
1370
|
+
printf '%s\n' "$runtime_project_slugs" > "$runtime_projects_tmp"
|
|
1371
|
+
else
|
|
1372
|
+
: > "$runtime_projects_tmp"
|
|
1373
|
+
fi
|
|
1374
|
+
chmod 0644 "$runtime_projects_tmp"
|
|
1375
|
+
mv -f "$runtime_projects_tmp" "$runtime_projects_file"
|
|
1376
|
+
|
|
939
1377
|
# Union of preview ports across projects, de-duped by containerPort
|
|
940
1378
|
# (first-in-position wins the name). Validated: name non-empty, 1..65535.
|
|
941
1379
|
union_preview_ports_json=$(jq -c '
|
|
@@ -985,6 +1423,34 @@ if [ -n "$extra_block" ]; then
|
|
|
985
1423
|
has_derived=1
|
|
986
1424
|
fi
|
|
987
1425
|
|
|
1426
|
+
# One fail-closed authority for names projects must never control, whether the
|
|
1427
|
+
# value arrived as an ordinary declared env key or as a preview urlEnv literal.
|
|
1428
|
+
# UAI_EDITOR_THEME is the sole preview exception below: it is computed by the
|
|
1429
|
+
# cloud from the task palette, not sourced from a project.
|
|
1430
|
+
is_reserved_task_env_name() {
|
|
1431
|
+
case "$1" in
|
|
1432
|
+
GH_TOKEN | GITHUB_TOKEN | GH_CONFIG_DIR | GH_HOST | \
|
|
1433
|
+
GIT_CONFIG* | GIT_DIR | GIT_WORK_TREE | GIT_COMMON_DIR | \
|
|
1434
|
+
GIT_OBJECT_DIRECTORY | GIT_ALTERNATE_OBJECT_DIRECTORIES | \
|
|
1435
|
+
GIT_INDEX_FILE | GIT_EXEC_PATH | GIT_ASKPASS | SSH_ASKPASS | \
|
|
1436
|
+
GIT_SSH | GIT_SSH_COMMAND | HOME | XDG_CONFIG_HOME | XDG_DATA_HOME | \
|
|
1437
|
+
XDG_CACHE_HOME | \
|
|
1438
|
+
PATH | LD_PRELOAD | LD_AUDIT | LD_LIBRARY_PATH | NODE_OPTIONS | \
|
|
1439
|
+
NODE_PATH | SHELLOPTS | BASHOPTS | PS4 | BASH_ENV | ENV | \
|
|
1440
|
+
ASDF_* | COREPACK_* | \
|
|
1441
|
+
[Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Pp][Rr][Ee][Ff][Ii][Xx] | \
|
|
1442
|
+
[Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Cc][Aa][Cc][Hh][Ee] | \
|
|
1443
|
+
[Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Ll][Oo][Gg][Ss][_-][Dd][Ii][Rr] | \
|
|
1444
|
+
[Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Uu][Mm][Aa][Ss][Kk] | UAI_* | \
|
|
1445
|
+
CLAUDE_CODE_OAUTH_TOKEN | \
|
|
1446
|
+
ANTHROPIC_API_KEY | ANTHROPIC_AUTH_TOKEN | XAI_API_KEY | \
|
|
1447
|
+
PLAYWRIGHT_BROWSERS_PATH)
|
|
1448
|
+
return 0
|
|
1449
|
+
;;
|
|
1450
|
+
esac
|
|
1451
|
+
return 1
|
|
1452
|
+
}
|
|
1453
|
+
|
|
988
1454
|
# Write the compose file. No top-level name: (the -p flag sets the project),
|
|
989
1455
|
# no container_name:. Env values live host-side (ADR-015): we emit only
|
|
990
1456
|
# pass-through references `${KEY:-}`.
|
|
@@ -1005,6 +1471,11 @@ fi
|
|
|
1005
1471
|
printf ' pull_policy: never\n'
|
|
1006
1472
|
fi
|
|
1007
1473
|
printf ' init: true\n'
|
|
1474
|
+
# Keep the host MCP gateway reachable through Docker's ordinary
|
|
1475
|
+
# host-gateway address. (The Apple runtime has no extra_hosts equivalent
|
|
1476
|
+
# and none is rendered for it; its gateway route is the vmnet gateway IP.)
|
|
1477
|
+
printf ' extra_hosts:\n'
|
|
1478
|
+
printf ' - "host.docker.internal:host-gateway"\n'
|
|
1008
1479
|
# Give node the one host group that owns this task's bind-mounted files.
|
|
1009
1480
|
# Numeric group_add works even when the group has a different name (or no
|
|
1010
1481
|
# name) in the image. Root hosts use node's existing gid 1000 above.
|
|
@@ -1018,13 +1489,32 @@ fi
|
|
|
1018
1489
|
# repository is mounted; the host cache never enters a container. Legacy
|
|
1019
1490
|
# worktrees fail closed before Compose rendering and must be recreated.
|
|
1020
1491
|
printf ' - "%s:%s"\n' "$task_workspace" "$task_workspace"
|
|
1492
|
+
if [ "$task_workspace_physical" != "$task_workspace" ]; then
|
|
1493
|
+
# Git canonicalizes a symlinked workspace root in both directions: the
|
|
1494
|
+
# worktree's `.git` points at the physical repository, and the repository's
|
|
1495
|
+
# worktree metadata points back at the physical workspace. Mount the same
|
|
1496
|
+
# already-approved workspace at that alias without exposing its `.uai`
|
|
1497
|
+
# sibling or any broader host directory.
|
|
1498
|
+
printf ' - "%s:%s"\n' \
|
|
1499
|
+
"$task_workspace" "$task_workspace_physical"
|
|
1500
|
+
fi
|
|
1021
1501
|
while IFS= read -r vol_obj; do
|
|
1022
1502
|
[ -n "$vol_obj" ] || continue
|
|
1023
1503
|
vol_pid=$(jq -r '.id' <<<"$vol_obj")
|
|
1024
1504
|
printf ' - "%s/repos/%s.git:%s/repos/%s.git"\n' \
|
|
1025
1505
|
"$task_uai_dir" "$vol_pid" "$task_uai_dir" "$vol_pid"
|
|
1506
|
+
if [ "$task_uai_dir_physical" != "$task_uai_dir" ]; then
|
|
1507
|
+
printf ' - "%s/repos/%s.git:%s/repos/%s.git"\n' \
|
|
1508
|
+
"$task_uai_dir" "$vol_pid" "$task_uai_dir_physical" "$vol_pid"
|
|
1509
|
+
fi
|
|
1026
1510
|
done < <(jq -c '.[]' <<<"$projects_json")
|
|
1027
|
-
printf ' - "%s:/
|
|
1511
|
+
printf ' - "%s:/usr/local/bin/uai-init:ro"\n' "$UAI_INIT_SOURCE"
|
|
1512
|
+
printf ' - "%s:/run/uai/runtime-projects:ro"\n' \
|
|
1513
|
+
"$runtime_projects_file"
|
|
1514
|
+
# Task processes consume only the exact runtimes/package managers proved by
|
|
1515
|
+
# pre-start maintenance. `:ro` is explicit because Docker's omitted default
|
|
1516
|
+
# is read-write and a legacy Compose file must never look equivalent.
|
|
1517
|
+
printf ' - "%s:/opt/asdf-data:ro"\n' "$ASDF_VOLUME"
|
|
1028
1518
|
# ADR-053: host-wide Playwright browser cache. Always mounted (harmless
|
|
1029
1519
|
# when browser testing is off); Chromium downloads once per HOST.
|
|
1030
1520
|
printf ' - "%s:/opt/pw-browsers"\n' "$PW_VOLUME"
|
|
@@ -1056,6 +1546,61 @@ fi
|
|
|
1056
1546
|
# the bind-mounted workspace and independent of project-defined XDG/HOME
|
|
1057
1547
|
# values so its credential helper always resolves the connected user.
|
|
1058
1548
|
printf ' GH_CONFIG_DIR: "/home/node/.config/gh"\n'
|
|
1549
|
+
# Runtime selection/cache authority is host-owned. These values override
|
|
1550
|
+
# both project-declared keys and a derived image's ENV so uai-init cannot be
|
|
1551
|
+
# redirected into an unproved runtime or a different shared-cache writer.
|
|
1552
|
+
printf ' PATH: "%s"\n' "$TASK_RUNTIME_PATH"
|
|
1553
|
+
printf ' HOME: "/home/node"\n'
|
|
1554
|
+
printf ' XDG_CONFIG_HOME: "/home/node/.config"\n'
|
|
1555
|
+
printf ' XDG_DATA_HOME: "/home/node/.local/share"\n'
|
|
1556
|
+
printf ' XDG_CACHE_HOME: "/home/node/.cache"\n'
|
|
1557
|
+
printf ' ASDF_DIR: "/opt/asdf"\n'
|
|
1558
|
+
printf ' ASDF_DATA_DIR: "/opt/asdf-data"\n'
|
|
1559
|
+
printf ' ASDF_CONFIG_FILE: "/dev/null"\n'
|
|
1560
|
+
printf ' ASDF_DEFAULT_TOOL_VERSIONS_FILENAME: ".tool-versions"\n'
|
|
1561
|
+
printf ' ASDF_NODEJS_VERSION: ""\n'
|
|
1562
|
+
printf ' ASDF_PYTHON_VERSION: ""\n'
|
|
1563
|
+
printf ' ASDF_GOLANG_VERSION: ""\n'
|
|
1564
|
+
printf ' ASDF_RUBY_VERSION: ""\n'
|
|
1565
|
+
printf ' ASDF_RUST_VERSION: ""\n'
|
|
1566
|
+
printf ' ASDF_SKIP_RESHIM: "1"\n'
|
|
1567
|
+
printf ' COREPACK_HOME: "/opt/asdf-data/corepack"\n'
|
|
1568
|
+
printf ' COREPACK_DEFAULT_TO_LATEST: "0"\n'
|
|
1569
|
+
printf ' COREPACK_ENV_FILE: "0"\n'
|
|
1570
|
+
printf ' COREPACK_NPM_REGISTRY: "https://registry.npmjs.org"\n'
|
|
1571
|
+
printf ' COREPACK_ENABLE_NETWORK: "0"\n'
|
|
1572
|
+
printf ' COREPACK_ENABLE_AUTO_PIN: "0"\n'
|
|
1573
|
+
printf ' COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"\n'
|
|
1574
|
+
printf ' COREPACK_ENABLE_PROJECT_SPEC: "1"\n'
|
|
1575
|
+
printf ' COREPACK_ENABLE_STRICT: "1"\n'
|
|
1576
|
+
printf ' COREPACK_ENABLE_UNSAFE_CUSTOM_URLS: "0"\n'
|
|
1577
|
+
printf ' COREPACK_INTEGRITY_CHECK: "1"\n'
|
|
1578
|
+
# npm injects lowercase config names into lifecycle scripts, so pin both
|
|
1579
|
+
# cases. These task-private paths are never part of the shared asdf volume.
|
|
1580
|
+
printf ' NPM_CONFIG_PREFIX: "%s"\n' "$TASK_NPM_PREFIX"
|
|
1581
|
+
printf ' npm_config_prefix: "%s"\n' "$TASK_NPM_PREFIX"
|
|
1582
|
+
printf ' NPM_CONFIG_CACHE: "%s"\n' "$TASK_NPM_CACHE"
|
|
1583
|
+
printf ' npm_config_cache: "%s"\n' "$TASK_NPM_CACHE"
|
|
1584
|
+
printf ' NPM_CONFIG_LOGS_DIR: "%s/_logs"\n' "$TASK_NPM_CACHE"
|
|
1585
|
+
printf ' npm_config_logs_dir: "%s/_logs"\n' "$TASK_NPM_CACHE"
|
|
1586
|
+
printf ' NPM_CONFIG_UMASK: "0002"\n'
|
|
1587
|
+
printf ' npm_config_umask: "0002"\n'
|
|
1588
|
+
printf ' UAI_WORKSPACE: "/workspace"\n'
|
|
1589
|
+
printf ' UAI_RUNTIME_PROJECTS: "/run/uai/runtime-projects"\n'
|
|
1590
|
+
printf ' LD_PRELOAD: ""\n'
|
|
1591
|
+
printf ' LD_AUDIT: ""\n'
|
|
1592
|
+
printf ' LD_LIBRARY_PATH: ""\n'
|
|
1593
|
+
printf ' NODE_OPTIONS: ""\n'
|
|
1594
|
+
printf ' NODE_PATH: ""\n'
|
|
1595
|
+
# SHELLOPTS/BASHOPTS are deliberately ABSENT, never empty: bash exports its
|
|
1596
|
+
# live options into any child when these names were present in its startup
|
|
1597
|
+
# environment, so an "empty clear" let set -euo scripts stamp nounset into
|
|
1598
|
+
# code-server and break every asdf shim in the Editor terminal (found live
|
|
1599
|
+
# 2026-08-16, on BOTH engines; the reserved-name filter still strips any
|
|
1600
|
+
# project-declared value).
|
|
1601
|
+
printf ' PS4: ""\n'
|
|
1602
|
+
printf ' BASH_ENV: ""\n'
|
|
1603
|
+
printf ' ENV: ""\n'
|
|
1059
1604
|
# Host-resident Claude auth (ADR-021). Headless `claude --print` no longer
|
|
1060
1605
|
# uses the interactive subscription/keychain path, so it needs a token from
|
|
1061
1606
|
# `claude setup-token`. Interpolated from the host-agent env at up-time, so
|
|
@@ -1093,17 +1638,7 @@ fi
|
|
|
1093
1638
|
# decrypted values at `compose up` time.
|
|
1094
1639
|
while IFS= read -r ekey; do
|
|
1095
1640
|
[ -n "$ekey" ] || continue
|
|
1096
|
-
|
|
1097
|
-
GH_TOKEN | GITHUB_TOKEN | GH_CONFIG_DIR | GH_HOST | \
|
|
1098
|
-
GIT_CONFIG* | GIT_DIR | GIT_WORK_TREE | GIT_COMMON_DIR | \
|
|
1099
|
-
GIT_OBJECT_DIRECTORY | GIT_ALTERNATE_OBJECT_DIRECTORIES | \
|
|
1100
|
-
GIT_INDEX_FILE | GIT_EXEC_PATH | GIT_ASKPASS | SSH_ASKPASS | \
|
|
1101
|
-
GIT_SSH | GIT_SSH_COMMAND | HOME | XDG_CONFIG_HOME | XDG_DATA_HOME | \
|
|
1102
|
-
BASH_ENV | ENV | UAI_GIT_TRANSPORT | \
|
|
1103
|
-
CLAUDE_CODE_OAUTH_TOKEN | ANTHROPIC_API_KEY | \
|
|
1104
|
-
ANTHROPIC_AUTH_TOKEN | XAI_API_KEY | PLAYWRIGHT_BROWSERS_PATH | \
|
|
1105
|
-
UAI_EDITOR_THEME) continue ;;
|
|
1106
|
-
esac
|
|
1641
|
+
is_reserved_task_env_name "$ekey" && continue
|
|
1107
1642
|
printf ' %s: ""\n' "$ekey"
|
|
1108
1643
|
done < <(jq -r '.[]' <<<"$union_env_keys_json")
|
|
1109
1644
|
# Cloud-computed literal env (ADR-025): a JSON object {VAR: value} on the
|
|
@@ -1124,6 +1659,10 @@ fi
|
|
|
1124
1659
|
pe_key=$(jq -r '.key' <<<"$pe_entry")
|
|
1125
1660
|
pe_val=$(jq -r '.value' <<<"$pe_entry")
|
|
1126
1661
|
[ -n "$pe_key" ] || continue
|
|
1662
|
+
if [ "$pe_key" != "UAI_EDITOR_THEME" ] \
|
|
1663
|
+
&& is_reserved_task_env_name "$pe_key"; then
|
|
1664
|
+
continue
|
|
1665
|
+
fi
|
|
1127
1666
|
printf ' %s: "%s"\n' "$pe_key" "$pe_val"
|
|
1128
1667
|
done < <(jq -c 'to_entries[]?' <<<"$preview_env_raw")
|
|
1129
1668
|
printf 'volumes:\n'
|
|
@@ -1145,13 +1684,30 @@ if [ "$has_derived" = "1" ]; then
|
|
|
1145
1684
|
fi
|
|
1146
1685
|
|
|
1147
1686
|
# -----------------------------------------------------------------------------
|
|
1148
|
-
# 5.
|
|
1687
|
+
# 5. Build (when derived) + pre-materialize project runtimes.
|
|
1149
1688
|
# -----------------------------------------------------------------------------
|
|
1150
1689
|
|
|
1151
1690
|
# The shared asdf cache volume is host-wide and declared external — make
|
|
1152
|
-
# sure it exists before compose references it. Idempotent
|
|
1153
|
-
|
|
1154
|
-
|
|
1691
|
+
# sure it exists before compose references it. Idempotent under Docker; the
|
|
1692
|
+
# Apple CLI errors on an existing name, so tolerate creation and then demand
|
|
1693
|
+
# the volume actually exists.
|
|
1694
|
+
crt volume create "$ASDF_VOLUME" >/dev/null 2>&1 || true
|
|
1695
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
1696
|
+
# Browsers ride a host directory here, never a block volume — see
|
|
1697
|
+
# PW_BROWSERS_DIR above for why.
|
|
1698
|
+
if ! /bin/mkdir -p "$PW_BROWSERS_DIR"; then
|
|
1699
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1700
|
+
"Uai could not create the shared Playwright browsers directory $PW_BROWSERS_DIR." \
|
|
1701
|
+
"create shared volumes"
|
|
1702
|
+
fi
|
|
1703
|
+
if ! crt volume inspect "$ASDF_VOLUME" >/dev/null 2>&1; then
|
|
1704
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1705
|
+
"Uai could not create the shared $ASDF_VOLUME volume on the Apple container runtime. Retry after the runtime is healthy." \
|
|
1706
|
+
"create shared volumes"
|
|
1707
|
+
fi
|
|
1708
|
+
else
|
|
1709
|
+
crt volume create "$PW_VOLUME" >/dev/null 2>&1 || true
|
|
1710
|
+
fi
|
|
1155
1711
|
|
|
1156
1712
|
# The standard base image is built by the host (ensureStandardImage), never
|
|
1157
1713
|
# pulled from a registry — and derived tasks FROM it too. If it's absent (the
|
|
@@ -1159,24 +1715,215 @@ docker volume create "$PW_VOLUME" >/dev/null 2>&1 || true
|
|
|
1159
1715
|
# a doomed registry pull; fail with an actionable message instead (the host
|
|
1160
1716
|
# also (re)builds it on demand before task-up — this is the belt-and-suspenders
|
|
1161
1717
|
# check for a still-in-progress or failed build). Live 2026-07-22.
|
|
1162
|
-
if !
|
|
1718
|
+
if ! crt image inspect "$STANDARD_IMAGE" >/dev/null 2>&1; then
|
|
1163
1719
|
emit_err "STANDARD_IMAGE_MISSING" \
|
|
1164
1720
|
"the host base image $STANDARD_IMAGE is not built yet — the host builds it at startup (a few minutes on first run, and after Docker Desktop finishes starting). Wait for the host log line 'built standard image', or restart the host, then retry. If it keeps failing the host log shows the build error (often Docker Desktop low on memory/disk)." \
|
|
1165
1721
|
"check standard image"
|
|
1166
1722
|
fi
|
|
1167
1723
|
|
|
1168
|
-
|
|
1724
|
+
# A derived task must materialize through the exact filesystem it will run:
|
|
1725
|
+
# project `extra` may add runtime build dependencies. Split build from up so a
|
|
1726
|
+
# failed materialization can never start the task container, and so a later up
|
|
1727
|
+
# cannot silently rebuild a different image after the proof succeeds.
|
|
1728
|
+
runtime_image="$STANDARD_IMAGE"
|
|
1729
|
+
if [ "$has_derived" = "1" ]; then
|
|
1730
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
1731
|
+
step "COMPOSE_UP_FAILED" "build derived task image"
|
|
1732
|
+
if ! "$CONTAINER_CLI" build -t "$DERIVED_IMAGE" \
|
|
1733
|
+
-f "$task_uai_dir/Dockerfile" "$task_uai_dir" >&2; then
|
|
1734
|
+
require_docker_after_failed_step "build derived task image"
|
|
1735
|
+
emit_err "COMPOSE_UP_FAILED" \
|
|
1736
|
+
"Uai could not build this task's derived image. Check the project's extra setup commands, then retry." \
|
|
1737
|
+
"build derived task image"
|
|
1738
|
+
fi
|
|
1739
|
+
else
|
|
1740
|
+
step "COMPOSE_UP_FAILED" "docker compose build app"
|
|
1741
|
+
if ! docker compose -p "$compose_project" \
|
|
1742
|
+
-f "$task_uai_dir/docker-compose.yml" build app >&2; then
|
|
1743
|
+
require_docker_after_failed_step "docker compose build app"
|
|
1744
|
+
emit_err "COMPOSE_UP_FAILED" \
|
|
1745
|
+
"Uai could not build this task's derived image. Check the project's extra setup commands, then retry." \
|
|
1746
|
+
"docker compose build app"
|
|
1747
|
+
fi
|
|
1748
|
+
fi
|
|
1749
|
+
runtime_image="$DERIVED_IMAGE"
|
|
1750
|
+
fi
|
|
1751
|
+
|
|
1752
|
+
# Resolve the mutable service tag once and use only its immutable local image
|
|
1753
|
+
# ID for both maintenance and app creation. The on-disk Compose file keeps its
|
|
1754
|
+
# tag so task-down can still identify/remove a derived image, but no concurrent
|
|
1755
|
+
# rebuild can swap code between runtime proof and execution.
|
|
1756
|
+
step "CONTAINER_INIT_FAILED" "resolve task image identity"
|
|
1757
|
+
runtime_image_id=""
|
|
1758
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
1759
|
+
# One snapshot supplies identity, environment, and entrypoint together, so
|
|
1760
|
+
# every image-authority decision below describes the same bytes. The Apple
|
|
1761
|
+
# CLI cannot run an image by its 64-hex id, so the id is re-checked against
|
|
1762
|
+
# the tag after the container is created instead of run directly.
|
|
1763
|
+
apple_image_snapshot=""
|
|
1764
|
+
if ! apple_image_snapshot=$("$CONTAINER_CLI" image inspect \
|
|
1765
|
+
"$runtime_image" 2>/dev/null); then
|
|
1766
|
+
require_docker_after_failed_step "resolve task image identity"
|
|
1767
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1768
|
+
"Uai could not resolve the exact local image for this task. Retry after the host image is available." \
|
|
1769
|
+
"resolve task image identity"
|
|
1770
|
+
fi
|
|
1771
|
+
runtime_image_id=$(printf '%s\n' "$apple_image_snapshot" \
|
|
1772
|
+
| jq -r '.[0].id // empty' 2>/dev/null) || runtime_image_id=""
|
|
1773
|
+
if ! printf '%s\n' "$runtime_image_id" \
|
|
1774
|
+
| grep -Eq '^[0-9a-f]{64}$'; then
|
|
1775
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1776
|
+
"The Apple container runtime returned an invalid image identity for $runtime_image; Uai refused to start an unpinned task container." \
|
|
1777
|
+
"resolve task image identity"
|
|
1778
|
+
fi
|
|
1779
|
+
# Without a --entrypoint override flag, an image-declared ENTRYPOINT would
|
|
1780
|
+
# prefix every argv this script passes (including the materializer's
|
|
1781
|
+
# `env -i`). Refuse such an image before anything executes from it.
|
|
1782
|
+
if ! printf '%s\n' "$apple_image_snapshot" | jq -e '
|
|
1783
|
+
(.[0].variants | type == "array" and length == 1)
|
|
1784
|
+
and ((.[0].variants[0].config.config.Entrypoint // []) | length == 0)
|
|
1785
|
+
' >/dev/null 2>&1; then
|
|
1786
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1787
|
+
"Uai refused this task image because it declares an ENTRYPOINT or multiple platform variants, which the Apple container runtime cannot safely override. Remove the ENTRYPOINT from the project's extra setup, then retry." \
|
|
1788
|
+
"resolve task image identity"
|
|
1789
|
+
fi
|
|
1790
|
+
else
|
|
1791
|
+
if ! runtime_image_id=$(docker image inspect --format '{{.Id}}' \
|
|
1792
|
+
"$runtime_image" 2>/dev/null); then
|
|
1793
|
+
require_docker_after_failed_step "resolve task image identity"
|
|
1794
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1795
|
+
"Uai could not resolve the exact local image for this task. Retry after the host image is available." \
|
|
1796
|
+
"resolve task image identity"
|
|
1797
|
+
fi
|
|
1798
|
+
if ! printf '%s\n' "$runtime_image_id" \
|
|
1799
|
+
| grep -Eq '^sha256:[0-9a-f]{64}$'; then
|
|
1800
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1801
|
+
"Docker returned an invalid image identity for $runtime_image; Uai refused to start an unpinned task container." \
|
|
1802
|
+
"resolve task image identity"
|
|
1803
|
+
fi
|
|
1804
|
+
fi
|
|
1805
|
+
|
|
1806
|
+
# Docker keeps image Config.Env entries whose names differ only by case from
|
|
1807
|
+
# later `docker exec -e` overrides. npm treats those spellings
|
|
1808
|
+
# case-insensitively and, depending on environment order, a mixed-case value
|
|
1809
|
+
# can beat both canonical values. Reject such an image before maintenance or
|
|
1810
|
+
# app creation; ordinary registry/auth npm config remains allowed, while the
|
|
1811
|
+
# four path/permission authorities must use one of the two spellings Uai can
|
|
1812
|
+
# deterministically replace.
|
|
1813
|
+
step "CONTAINER_INIT_FAILED" "verify task image npm authority"
|
|
1814
|
+
runtime_image_env_json=""
|
|
1815
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
1816
|
+
# From the same snapshot the identity pin came from — the single variant
|
|
1817
|
+
# was already proven above.
|
|
1818
|
+
runtime_image_env_json=$(printf '%s\n' "$apple_image_snapshot" \
|
|
1819
|
+
| jq -c '.[0].variants[0].config.config.Env // []' 2>/dev/null) \
|
|
1820
|
+
|| runtime_image_env_json=""
|
|
1821
|
+
if [ -z "$runtime_image_env_json" ]; then
|
|
1822
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1823
|
+
"Uai could not inspect this task image's npm environment authority. Retry after the Apple container runtime is stable." \
|
|
1824
|
+
"verify task image npm authority"
|
|
1825
|
+
fi
|
|
1826
|
+
elif ! runtime_image_env_json=$(docker image inspect \
|
|
1827
|
+
--format '{{json .Config.Env}}' "$runtime_image_id" 2>/dev/null); then
|
|
1828
|
+
require_docker_after_failed_step "verify task image npm authority"
|
|
1829
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1830
|
+
"Uai could not inspect this task image's npm environment authority. Retry after Docker is stable." \
|
|
1831
|
+
"verify task image npm authority"
|
|
1832
|
+
fi
|
|
1833
|
+
if ! printf '%s\n' "$runtime_image_env_json" | jq -e '
|
|
1834
|
+
def is_owned_npm_config($owned):
|
|
1835
|
+
ascii_downcase as $folded
|
|
1836
|
+
| ($folded | startswith("npm_config_"))
|
|
1837
|
+
and (($owned | index($folded[11:] | gsub("_"; "-"))) != null);
|
|
1838
|
+
(. // []) as $env
|
|
1839
|
+
| [
|
|
1840
|
+
"NPM_CONFIG_PREFIX", "npm_config_prefix",
|
|
1841
|
+
"NPM_CONFIG_CACHE", "npm_config_cache",
|
|
1842
|
+
"NPM_CONFIG_LOGS_DIR", "npm_config_logs_dir",
|
|
1843
|
+
"NPM_CONFIG_UMASK", "npm_config_umask"
|
|
1844
|
+
] as $canonical
|
|
1845
|
+
| [
|
|
1846
|
+
"prefix", "cache", "logs-dir", "umask"
|
|
1847
|
+
] as $owned
|
|
1848
|
+
| ($env | type == "array")
|
|
1849
|
+
and all($env[]; type == "string")
|
|
1850
|
+
and all($env[];
|
|
1851
|
+
(split("=")[0]) as $key
|
|
1852
|
+
| (($key | is_owned_npm_config($owned)) | not)
|
|
1853
|
+
or (($canonical | index($key)) != null)
|
|
1854
|
+
)
|
|
1855
|
+
' >/dev/null 2>&1; then
|
|
1856
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1857
|
+
"Uai refused this task image because a mixed-case npm prefix/cache/log setting could redirect global package writes. Remove that image ENV setting (unrelated registry/auth settings remain supported) and retry." \
|
|
1858
|
+
"verify task image npm authority"
|
|
1859
|
+
fi
|
|
1860
|
+
|
|
1861
|
+
# Runtime installation is a host-maintenance phase, not task-container init.
|
|
1862
|
+
# Give the one-shot container only the immutable workspace view and the
|
|
1863
|
+
# explicitly writable host-wide asdf volume. Run it for every selected-project
|
|
1864
|
+
# task: projects without .tool-versions still use the image's proven root
|
|
1865
|
+
# defaults and may request a Corepack-managed package manager. A true
|
|
1866
|
+
# zero-project scratchpad has nothing to prepare and remains the only skip.
|
|
1867
|
+
# GNU timeout lives in the Debian standard image (and therefore every derived
|
|
1868
|
+
# image); it bounds the shared lock wait plus all installs and positive
|
|
1869
|
+
# verification without leaving an orphaned volume writer when the cloud's
|
|
1870
|
+
# 15-minute RPC deadline expires.
|
|
1871
|
+
if [ "$project_count" -gt 0 ]; then
|
|
1872
|
+
step "CONTAINER_INIT_FAILED" "materialize task runtimes"
|
|
1873
|
+
materialization_rc=0
|
|
1874
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
1875
|
+
# ADR-108: the shared volume is never written on the task path. Apple
|
|
1876
|
+
# materialization runs INSIDE the app container after the in-guest
|
|
1877
|
+
# overlay is established (section 6a below) — a missing toolchain
|
|
1878
|
+
# installs into the task's own upper layer with zero shared-volume
|
|
1879
|
+
# coordination, at any task count.
|
|
1880
|
+
log "apple runtime materialization runs in-container after the overlay is established"
|
|
1881
|
+
else
|
|
1882
|
+
run_runtime_materializer "$runtime_image_id" || materialization_rc=$?
|
|
1883
|
+
fi
|
|
1884
|
+
if [ "$materialization_rc" -ne 0 ]; then
|
|
1885
|
+
require_docker_after_failed_step "materialize task runtimes"
|
|
1886
|
+
case "$materialization_rc" in
|
|
1887
|
+
124|137)
|
|
1888
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1889
|
+
"Runtime materialization did not finish inside Uai's 10-minute startup budget. Retry the task: Uai will re-check every required runtime and package manager before it starts any task container, and will not trust a partial cache." \
|
|
1890
|
+
"materialize task runtimes"
|
|
1891
|
+
;;
|
|
1892
|
+
*)
|
|
1893
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1894
|
+
"Uai could not install and verify every runtime and package manager needed by this task. Check the project's .tool-versions and packageManager entries and the host's network and disk availability, then retry." \
|
|
1895
|
+
"materialize task runtimes"
|
|
1896
|
+
;;
|
|
1897
|
+
esac
|
|
1898
|
+
fi
|
|
1899
|
+
else
|
|
1900
|
+
log "zero-project scratchpad — skipping runtime materialization"
|
|
1901
|
+
fi
|
|
1902
|
+
|
|
1903
|
+
# -----------------------------------------------------------------------------
|
|
1904
|
+
# 6. Compose up.
|
|
1905
|
+
# -----------------------------------------------------------------------------
|
|
1906
|
+
|
|
1907
|
+
step "COMPOSE_UP_FAILED" "start task container"
|
|
1169
1908
|
# Limit the supplied values to keys the selected projects actually declare,
|
|
1170
|
-
# then remove Uai-managed names and preview URL names.
|
|
1171
|
-
#
|
|
1172
|
-
#
|
|
1909
|
+
# then remove Uai-managed names and preview URL names. Both runtimes consume
|
|
1910
|
+
# the same filtered object: Docker merges it as a Compose stdin override, and
|
|
1911
|
+
# the Apple path serializes it into a transient env-file below. JSON is valid
|
|
1912
|
+
# YAML, so Compose can merge this secret-bearing override directly from stdin
|
|
1913
|
+
# without a temporary env/YAML file and without exporting any value to host
|
|
1914
|
+
# commands.
|
|
1173
1915
|
preview_env_raw=$(jq -r '.[0].preview_env // "{}"' <<<"$task_json")
|
|
1174
|
-
|
|
1916
|
+
filtered_project_env=$(printf '%s' "$project_env_json" | jq -c \
|
|
1175
1917
|
--argjson declared "$union_env_keys_json" \
|
|
1176
1918
|
--argjson preview "$preview_env_raw" '
|
|
1177
1919
|
.
|
|
1178
1920
|
| with_entries(.key as $key | select($declared | index($key)))
|
|
1179
1921
|
| with_entries(select(.key | test("^GIT_CONFIG($|_)") | not))
|
|
1922
|
+
| with_entries(select(.key | test(
|
|
1923
|
+
"^(ASDF_|COREPACK_)|^npm_config_(prefix|cache|logs[_-]dir|umask)$";
|
|
1924
|
+
"i"
|
|
1925
|
+
) | not))
|
|
1926
|
+
| with_entries(select(.key | test("^UAI_") | not))
|
|
1180
1927
|
| del(
|
|
1181
1928
|
.GH_TOKEN,
|
|
1182
1929
|
.GITHUB_TOKEN,
|
|
@@ -1196,6 +1943,16 @@ compose_env_override=$(printf '%s' "$project_env_json" | jq -c \
|
|
|
1196
1943
|
.HOME,
|
|
1197
1944
|
.XDG_CONFIG_HOME,
|
|
1198
1945
|
.XDG_DATA_HOME,
|
|
1946
|
+
.XDG_CACHE_HOME,
|
|
1947
|
+
.PATH,
|
|
1948
|
+
.LD_PRELOAD,
|
|
1949
|
+
.LD_AUDIT,
|
|
1950
|
+
.LD_LIBRARY_PATH,
|
|
1951
|
+
.NODE_OPTIONS,
|
|
1952
|
+
.NODE_PATH,
|
|
1953
|
+
.SHELLOPTS,
|
|
1954
|
+
.BASHOPTS,
|
|
1955
|
+
.PS4,
|
|
1199
1956
|
.BASH_ENV,
|
|
1200
1957
|
.ENV,
|
|
1201
1958
|
.UAI_GIT_TRANSPORT,
|
|
@@ -1207,18 +1964,466 @@ compose_env_override=$(printf '%s' "$project_env_json" | jq -c \
|
|
|
1207
1964
|
.UAI_EDITOR_THEME
|
|
1208
1965
|
)
|
|
1209
1966
|
| delpaths($preview | keys | map([.]))
|
|
1210
|
-
| {services:{app:{environment:.}}}
|
|
1211
1967
|
')
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1968
|
+
compose_env_override=""
|
|
1969
|
+
if [ "$TASK_RUNTIME_MODE" = "docker" ]; then
|
|
1970
|
+
compose_env_override=$(printf '%s\n' "$filtered_project_env" | jq -c \
|
|
1971
|
+
--arg image "$runtime_image_id" \
|
|
1972
|
+
'{services:{app:{environment:.,image:$image,pull_policy:"never"}}}')
|
|
1973
|
+
fi
|
|
1974
|
+
|
|
1975
|
+
# Detect a tag replacement that completed during materialization and fail with
|
|
1976
|
+
# a clear retry instead of starting a task under a stale generation. The
|
|
1977
|
+
# immutable stdin override below also closes the remaining recheck→up window.
|
|
1978
|
+
step "CONTAINER_INIT_FAILED" "verify task image identity"
|
|
1979
|
+
current_runtime_image_id=""
|
|
1980
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
1981
|
+
if ! current_runtime_image_id=$("$CONTAINER_CLI" image inspect \
|
|
1982
|
+
"$runtime_image" 2>/dev/null | jq -r '.[0].id // empty' 2>/dev/null); then
|
|
1983
|
+
require_docker_after_failed_step "verify task image identity"
|
|
1984
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1985
|
+
"Uai could not re-check this task's image after runtime materialization. Retry after the Apple container runtime is stable." \
|
|
1986
|
+
"verify task image identity"
|
|
1987
|
+
fi
|
|
1988
|
+
elif ! current_runtime_image_id=$(docker image inspect --format '{{.Id}}' \
|
|
1989
|
+
"$runtime_image" 2>/dev/null); then
|
|
1990
|
+
require_docker_after_failed_step "verify task image identity"
|
|
1991
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1992
|
+
"Uai could not re-check this task's image after runtime materialization. Retry after Docker is stable." \
|
|
1993
|
+
"verify task image identity"
|
|
1994
|
+
fi
|
|
1995
|
+
if [ "$current_runtime_image_id" != "$runtime_image_id" ]; then
|
|
1996
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1997
|
+
"This task's image changed while Uai was materializing runtimes. Retry so the new image receives a complete runtime proof before any task container starts." \
|
|
1998
|
+
"verify task image identity"
|
|
1999
|
+
fi
|
|
2000
|
+
reject_workspace_runtime_shadow
|
|
2001
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
2002
|
+
step "COMPOSE_UP_FAILED" "start task container"
|
|
2003
|
+
# The Apple CLI takes environment through an env-file whose values are read
|
|
2004
|
+
# verbatim from the first `=` to the newline. A value containing a newline
|
|
2005
|
+
# cannot survive that framing — refuse it rather than truncate a secret.
|
|
2006
|
+
if ! printf '%s\n' "$filtered_project_env" | jq -e \
|
|
2007
|
+
'to_entries | all(.value | type == "string" and (test("[\r\n]") | not))' \
|
|
2008
|
+
>/dev/null 2>&1; then
|
|
2009
|
+
emit_err "COMPOSE_UP_FAILED" \
|
|
2010
|
+
"A project environment value contains a newline, which the Apple container runtime cannot pass through its env-file. Store the value single-line (for example base64) or run this task on a Docker host." \
|
|
2011
|
+
"start task container"
|
|
2012
|
+
fi
|
|
2013
|
+
task_env_file=$(mktemp "$task_uai_dir/.task-env.XXXXXX")
|
|
2014
|
+
chmod 0600 "$task_env_file"
|
|
2015
|
+
{
|
|
2016
|
+
# Mirrors the Compose service environment exactly (see the rendered
|
|
2017
|
+
# docker-compose.yml for the rationale on each block). Later lines win in
|
|
2018
|
+
# both dialects, so ordering here preserves the Compose precedence:
|
|
2019
|
+
# static authority, engine tokens, declared project values, preview env.
|
|
2020
|
+
printf 'UAI_GIT_TRANSPORT=%s\n' "$uai_git_transport"
|
|
2021
|
+
printf 'GH_CONFIG_DIR=/home/node/.config/gh\n'
|
|
2022
|
+
printf 'PATH=%s\n' "$TASK_RUNTIME_PATH"
|
|
2023
|
+
printf 'HOME=/home/node\n'
|
|
2024
|
+
printf 'XDG_CONFIG_HOME=/home/node/.config\n'
|
|
2025
|
+
printf 'XDG_DATA_HOME=/home/node/.local/share\n'
|
|
2026
|
+
printf 'XDG_CACHE_HOME=/home/node/.cache\n'
|
|
2027
|
+
printf 'ASDF_DIR=/opt/asdf\n'
|
|
2028
|
+
printf 'ASDF_DATA_DIR=/opt/asdf-data\n'
|
|
2029
|
+
printf 'ASDF_CONFIG_FILE=/dev/null\n'
|
|
2030
|
+
printf 'ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions\n'
|
|
2031
|
+
printf 'ASDF_NODEJS_VERSION=\n'
|
|
2032
|
+
printf 'ASDF_PYTHON_VERSION=\n'
|
|
2033
|
+
printf 'ASDF_GOLANG_VERSION=\n'
|
|
2034
|
+
printf 'ASDF_RUBY_VERSION=\n'
|
|
2035
|
+
printf 'ASDF_RUST_VERSION=\n'
|
|
2036
|
+
printf 'ASDF_SKIP_RESHIM=1\n'
|
|
2037
|
+
printf 'COREPACK_HOME=/opt/asdf-data/corepack\n'
|
|
2038
|
+
printf 'COREPACK_DEFAULT_TO_LATEST=0\n'
|
|
2039
|
+
printf 'COREPACK_ENV_FILE=0\n'
|
|
2040
|
+
printf 'COREPACK_NPM_REGISTRY=https://registry.npmjs.org\n'
|
|
2041
|
+
printf 'COREPACK_ENABLE_NETWORK=0\n'
|
|
2042
|
+
printf 'COREPACK_ENABLE_AUTO_PIN=0\n'
|
|
2043
|
+
printf 'COREPACK_ENABLE_DOWNLOAD_PROMPT=0\n'
|
|
2044
|
+
printf 'COREPACK_ENABLE_PROJECT_SPEC=1\n'
|
|
2045
|
+
printf 'COREPACK_ENABLE_STRICT=1\n'
|
|
2046
|
+
printf 'COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=0\n'
|
|
2047
|
+
printf 'COREPACK_INTEGRITY_CHECK=1\n'
|
|
2048
|
+
printf 'NPM_CONFIG_PREFIX=%s\n' "$TASK_NPM_PREFIX"
|
|
2049
|
+
printf 'npm_config_prefix=%s\n' "$TASK_NPM_PREFIX"
|
|
2050
|
+
printf 'NPM_CONFIG_CACHE=%s\n' "$TASK_NPM_CACHE"
|
|
2051
|
+
printf 'npm_config_cache=%s\n' "$TASK_NPM_CACHE"
|
|
2052
|
+
printf 'NPM_CONFIG_LOGS_DIR=%s/_logs\n' "$TASK_NPM_CACHE"
|
|
2053
|
+
printf 'npm_config_logs_dir=%s/_logs\n' "$TASK_NPM_CACHE"
|
|
2054
|
+
printf 'NPM_CONFIG_UMASK=0002\n'
|
|
2055
|
+
printf 'npm_config_umask=0002\n'
|
|
2056
|
+
printf 'UAI_WORKSPACE=/workspace\n'
|
|
2057
|
+
printf 'UAI_RUNTIME_PROJECTS=/run/uai/runtime-projects\n'
|
|
2058
|
+
printf 'LD_PRELOAD=\n'
|
|
2059
|
+
printf 'LD_AUDIT=\n'
|
|
2060
|
+
printf 'LD_LIBRARY_PATH=\n'
|
|
2061
|
+
printf 'NODE_OPTIONS=\n'
|
|
2062
|
+
printf 'NODE_PATH=\n'
|
|
2063
|
+
printf 'PS4=\n'
|
|
2064
|
+
printf 'BASH_ENV=\n'
|
|
2065
|
+
printf 'ENV=\n'
|
|
2066
|
+
printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "${CLAUDE_CODE_OAUTH_TOKEN:-}"
|
|
2067
|
+
printf 'ANTHROPIC_API_KEY=%s\n' "${ANTHROPIC_API_KEY:-}"
|
|
2068
|
+
printf 'ANTHROPIC_AUTH_TOKEN=%s\n' "${ANTHROPIC_AUTH_TOKEN:-}"
|
|
2069
|
+
printf 'XAI_API_KEY=%s\n' "${XAI_API_KEY:-}"
|
|
2070
|
+
printf 'PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers\n'
|
|
2071
|
+
printf '%s\n' "$filtered_project_env" \
|
|
2072
|
+
| jq -r 'to_entries[] | "\(.key)=\(.value)"'
|
|
2073
|
+
while IFS= read -r pe_entry; do
|
|
2074
|
+
[ -n "$pe_entry" ] || continue
|
|
2075
|
+
pe_key=$(jq -r '.key' <<<"$pe_entry")
|
|
2076
|
+
pe_val=$(jq -r '.value' <<<"$pe_entry")
|
|
2077
|
+
[ -n "$pe_key" ] || continue
|
|
2078
|
+
if [ "$pe_key" != "UAI_EDITOR_THEME" ] \
|
|
2079
|
+
&& is_reserved_task_env_name "$pe_key"; then
|
|
2080
|
+
continue
|
|
2081
|
+
fi
|
|
2082
|
+
printf '%s=%s\n' "$pe_key" "$pe_val"
|
|
2083
|
+
done < <(jq -c 'to_entries[]?' <<<"$preview_env_raw")
|
|
2084
|
+
} > "$task_env_file"
|
|
2085
|
+
# The mount set matches the rendered Compose file's app service one-for-one
|
|
2086
|
+
# (minus host-gateway and group_add, which have no Apple equivalent and are
|
|
2087
|
+
# not needed — ownership maps transparently across the VM boundary).
|
|
2088
|
+
apple_run_args=(
|
|
2089
|
+
-v "$task_workspace:/workspace"
|
|
2090
|
+
-v "$task_workspace:$task_workspace"
|
|
2091
|
+
)
|
|
2092
|
+
if [ "$task_workspace_physical" != "$task_workspace" ]; then
|
|
2093
|
+
apple_run_args+=( -v "$task_workspace:$task_workspace_physical" )
|
|
2094
|
+
fi
|
|
2095
|
+
while IFS= read -r vol_obj; do
|
|
2096
|
+
[ -n "$vol_obj" ] || continue
|
|
2097
|
+
vol_pid=$(jq -r '.id' <<<"$vol_obj")
|
|
2098
|
+
apple_run_args+=(
|
|
2099
|
+
-v "$task_uai_dir/repos/$vol_pid.git:$task_uai_dir/repos/$vol_pid.git"
|
|
2100
|
+
)
|
|
2101
|
+
if [ "$task_uai_dir_physical" != "$task_uai_dir" ]; then
|
|
2102
|
+
apple_run_args+=(
|
|
2103
|
+
-v "$task_uai_dir/repos/$vol_pid.git:$task_uai_dir_physical/repos/$vol_pid.git"
|
|
2104
|
+
)
|
|
2105
|
+
fi
|
|
2106
|
+
done < <(jq -c '.[]' <<<"$projects_json")
|
|
2107
|
+
apple_run_args+=(
|
|
2108
|
+
-v "$UAI_INIT_SOURCE:/usr/local/bin/uai-init:ro"
|
|
2109
|
+
-v "$runtime_projects_file:/run/uai/runtime-projects:ro"
|
|
2110
|
+
# ADR-108: the shared volume attaches READ-ONLY at a staging path and an
|
|
2111
|
+
# in-guest overlay presents it writable at /opt/asdf-data (upper = the
|
|
2112
|
+
# task's own layer). The shared volume is never written on the task path,
|
|
2113
|
+
# so any number of tasks coexist and first-time installs need no window.
|
|
2114
|
+
-v "$ASDF_VOLUME:/run/uai/asdf-lower:ro"
|
|
2115
|
+
# In-container materialization (post-overlay) uses the packaged current
|
|
2116
|
+
# materializer + Corepack pin, exactly like the retired maintenance
|
|
2117
|
+
# container did.
|
|
2118
|
+
-v "$RUNTIME_MATERIALIZER_SOURCE:/usr/local/bin/uai-materialize-runtimes:ro"
|
|
2119
|
+
-v "$COREPACK_VERSION_SOURCE:/usr/local/share/uai/corepack-version:ro"
|
|
2120
|
+
-v "$PW_BROWSERS_DIR:/opt/pw-browsers"
|
|
2121
|
+
)
|
|
2122
|
+
if [ "$shared_files_mode" != "off" ]; then
|
|
2123
|
+
shared_suffix=""
|
|
2124
|
+
[ "$shared_files_mode" = "ro" ] && shared_suffix=":ro"
|
|
2125
|
+
if [ -n "$shared_owner_org" ]; then
|
|
2126
|
+
apple_run_args+=(
|
|
2127
|
+
-v "$shared_root/orgs/$shared_owner_org:/workspace/files/org$shared_suffix"
|
|
2128
|
+
)
|
|
2129
|
+
fi
|
|
2130
|
+
if [ -n "$shared_owner_user" ]; then
|
|
2131
|
+
apple_run_args+=(
|
|
2132
|
+
-v "$shared_root/users/$shared_owner_user:/workspace/files/me$shared_suffix"
|
|
2133
|
+
)
|
|
2134
|
+
fi
|
|
2135
|
+
fi
|
|
2136
|
+
apple_up_rc=0
|
|
2137
|
+
# CAP_SYS_ADMIN: required for the ADR-108 in-guest overlay mount. The
|
|
2138
|
+
# capability is confined by the VM boundary — the guest kernel is the
|
|
2139
|
+
# task's own, so this grants nothing on the host.
|
|
2140
|
+
"$CONTAINER_CLI" run -d --init \
|
|
2141
|
+
--name "$app_container" \
|
|
2142
|
+
-l "com.uai.task=$task_id" \
|
|
2143
|
+
--cap-add CAP_SYS_ADMIN \
|
|
2144
|
+
--workdir /workspace \
|
|
2145
|
+
--memory "$APPLE_TASK_MEMORY" \
|
|
2146
|
+
--cpus "$APPLE_TASK_CPUS" \
|
|
2147
|
+
--env-file "$task_env_file" \
|
|
2148
|
+
"${apple_run_args[@]}" \
|
|
2149
|
+
"$runtime_image" >/dev/null || apple_up_rc=$?
|
|
2150
|
+
rm -f "$task_env_file"
|
|
2151
|
+
if [ "$apple_up_rc" -ne 0 ]; then
|
|
2152
|
+
require_docker_after_failed_step "start task container"
|
|
2153
|
+
emit_err "COMPOSE_UP_FAILED" \
|
|
2154
|
+
"Uai could not start this task's container on the Apple container runtime. Retry the task." \
|
|
2155
|
+
"start task container"
|
|
2156
|
+
fi
|
|
2157
|
+
# The runtime cannot start a container by immutable image id, so prove the
|
|
2158
|
+
# tag still resolves to the pinned bytes now that the container exists. A
|
|
2159
|
+
# concurrent retag loses either way: before create, the recheck above; here,
|
|
2160
|
+
# the container is quarantined before credentials are installed.
|
|
2161
|
+
apple_post_up_image_id=$("$CONTAINER_CLI" image inspect "$runtime_image" \
|
|
2162
|
+
2>/dev/null | jq -r '.[0].id // empty' 2>/dev/null) \
|
|
2163
|
+
|| apple_post_up_image_id=""
|
|
2164
|
+
if [ "$apple_post_up_image_id" != "$runtime_image_id" ]; then
|
|
2165
|
+
if ! quarantine_untrusted_app_container; then
|
|
2166
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2167
|
+
"This task's image changed while its container was being created, and Uai could not prove the container stopped. Stop it manually before retrying." \
|
|
2168
|
+
"verify task image identity"
|
|
2169
|
+
fi
|
|
2170
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2171
|
+
"This task's image changed while its container was being created. Retry so the new image receives a complete runtime proof first." \
|
|
2172
|
+
"verify task image identity"
|
|
2173
|
+
fi
|
|
1215
2174
|
else
|
|
2175
|
+
step "COMPOSE_UP_FAILED" "docker compose up -d"
|
|
1216
2176
|
printf '%s\n' "$compose_env_override" | docker compose -p "$compose_project" \
|
|
1217
|
-
-f "$task_uai_dir/docker-compose.yml" -f - up -d >/dev/null
|
|
2177
|
+
-f "$task_uai_dir/docker-compose.yml" -f - up -d --no-build >/dev/null
|
|
2178
|
+
fi
|
|
2179
|
+
|
|
2180
|
+
# Rendered YAML is not runtime evidence: Compose could reuse unexpected state,
|
|
2181
|
+
# or a compatibility bug could silently drop `:ro`. Inspect the exact created
|
|
2182
|
+
# app before it receives credentials. There must be one named volume at this
|
|
2183
|
+
# destination, with Docker itself reporting the mount read-only, and the same
|
|
2184
|
+
# shared volume must not be exposed again at any other destination.
|
|
2185
|
+
step "CONTAINER_INIT_FAILED" "verify task runtime isolation"
|
|
2186
|
+
app_mounts_json=""
|
|
2187
|
+
apple_app_snapshot=""
|
|
2188
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
2189
|
+
# One container snapshot backs both the mount and environment proofs, so
|
|
2190
|
+
# they describe the same observed state.
|
|
2191
|
+
apple_app_snapshot=$(crt inspect "$app_container" 2>/dev/null) \
|
|
2192
|
+
|| apple_app_snapshot=""
|
|
2193
|
+
app_mounts_json=$(printf '%s\n' "$apple_app_snapshot" \
|
|
2194
|
+
| jq -c '.[0].configuration.mounts // empty' 2>/dev/null) \
|
|
2195
|
+
|| app_mounts_json=""
|
|
2196
|
+
fi
|
|
2197
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ] \
|
|
2198
|
+
&& [ -z "$app_mounts_json" ]; then
|
|
2199
|
+
require_docker_after_failed_step "inspect task runtime isolation"
|
|
2200
|
+
if ! quarantine_untrusted_app_container; then
|
|
2201
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2202
|
+
"Uai could not inspect or stop the task container before credentials were installed. Stop it manually before retrying." \
|
|
2203
|
+
"verify task runtime isolation"
|
|
2204
|
+
fi
|
|
2205
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2206
|
+
"Uai could not inspect the task container's shared runtime mount. Retry to recreate it safely." \
|
|
2207
|
+
"verify task runtime isolation"
|
|
2208
|
+
elif [ "$TASK_RUNTIME_MODE" = "docker" ] \
|
|
2209
|
+
&& ! app_mounts_json=$(docker inspect --format '{{json .Mounts}}' \
|
|
2210
|
+
"$app_container" 2>/dev/null); then
|
|
2211
|
+
require_docker_after_failed_step "inspect task runtime isolation"
|
|
2212
|
+
if ! quarantine_untrusted_app_container; then
|
|
2213
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2214
|
+
"Uai could not inspect or stop the task container before credentials were installed. Stop it manually before retrying." \
|
|
2215
|
+
"verify task runtime isolation"
|
|
2216
|
+
fi
|
|
2217
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2218
|
+
"Uai could not inspect the task container's shared runtime mount. Retry to recreate it safely." \
|
|
2219
|
+
"verify task runtime isolation"
|
|
2220
|
+
fi
|
|
2221
|
+
app_config_env_json=""
|
|
2222
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
2223
|
+
app_config_env_json=$(printf '%s\n' "$apple_app_snapshot" \
|
|
2224
|
+
| jq -c '.[0].configuration.initProcess.environment // empty' \
|
|
2225
|
+
2>/dev/null) || app_config_env_json=""
|
|
2226
|
+
fi
|
|
2227
|
+
if { [ "$TASK_RUNTIME_MODE" = "apple-container" ] \
|
|
2228
|
+
&& [ -z "$app_config_env_json" ]; } \
|
|
2229
|
+
|| { [ "$TASK_RUNTIME_MODE" = "docker" ] \
|
|
2230
|
+
&& ! app_config_env_json=$(docker inspect \
|
|
2231
|
+
--format '{{json .Config.Env}}' "$app_container" 2>/dev/null); }; then
|
|
2232
|
+
require_docker_after_failed_step "inspect task runtime environment"
|
|
2233
|
+
if ! quarantine_untrusted_app_container; then
|
|
2234
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2235
|
+
"Uai could not inspect or stop the task container before credentials were installed. Stop it manually before retrying." \
|
|
2236
|
+
"verify task runtime isolation"
|
|
2237
|
+
fi
|
|
2238
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2239
|
+
"Uai could not inspect the task container's runtime environment. Retry to recreate it safely." \
|
|
2240
|
+
"verify task runtime isolation"
|
|
2241
|
+
fi
|
|
2242
|
+
# The two runtimes report mounts in different shapes; each filter asserts the
|
|
2243
|
+
# same contract — exactly one /opt/asdf-data mount, it is the shared
|
|
2244
|
+
# uai-asdf-data volume, the runtime itself reports it read-only, and that
|
|
2245
|
+
# volume appears at no other destination.
|
|
2246
|
+
mounts_proof_filter='
|
|
2247
|
+
[ .[] | select(.Destination == "/opt/asdf-data") ] as $runtime
|
|
2248
|
+
| [ .[] | select(.Name? == "uai-asdf-data") ] as $shared
|
|
2249
|
+
| ($runtime | length) == 1
|
|
2250
|
+
and ($shared | length) == 1
|
|
2251
|
+
and $runtime[0].Type == "volume"
|
|
2252
|
+
and $runtime[0].Name == "uai-asdf-data"
|
|
2253
|
+
and $runtime[0].RW == false
|
|
2254
|
+
'
|
|
2255
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
2256
|
+
# ADR-108: the shared volume attaches READ-ONLY at the overlay staging
|
|
2257
|
+
# path, appears nowhere else, and /opt/asdf-data is NOT a host mount at
|
|
2258
|
+
# all — the writable view there is the in-guest overlay, proven from
|
|
2259
|
+
# inside the guest right after this (host inspect cannot see guest
|
|
2260
|
+
# mounts).
|
|
2261
|
+
mounts_proof_filter='
|
|
2262
|
+
[ .[] | select(.destination == "/run/uai/asdf-lower") ] as $runtime
|
|
2263
|
+
| [ .[] | select((.type.volume.name? // "") == "uai-asdf-data") ] as $shared
|
|
2264
|
+
| ($runtime | length) == 1
|
|
2265
|
+
and ($shared | length) == 1
|
|
2266
|
+
and (($runtime[0].type.volume.name? // "") == "uai-asdf-data")
|
|
2267
|
+
and ((($runtime[0].options // []) | index("ro")) != null)
|
|
2268
|
+
and ((($runtime[0].options // []) | index("rw")) == null)
|
|
2269
|
+
and (([ .[] | select(.destination == "/opt/asdf-data") ] | length) == 0)
|
|
2270
|
+
'
|
|
2271
|
+
fi
|
|
2272
|
+
if ! printf '%s\n' "$app_mounts_json" \
|
|
2273
|
+
| jq -e "$mounts_proof_filter" >/dev/null 2>&1; then
|
|
2274
|
+
if ! quarantine_untrusted_app_container; then
|
|
2275
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2276
|
+
"Uai found an unsafe shared runtime mount and could not prove the task container stopped. Stop it manually before retrying." \
|
|
2277
|
+
"verify task runtime isolation"
|
|
2278
|
+
fi
|
|
2279
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2280
|
+
"Uai stopped this task because its shared runtime cache was not the exact read-only uai-asdf-data volume. Retry to recreate the container safely." \
|
|
2281
|
+
"verify task runtime isolation"
|
|
2282
|
+
fi
|
|
2283
|
+
if ! printf '%s\n' "$app_config_env_json" | jq -e \
|
|
2284
|
+
--arg path "$TASK_RUNTIME_PATH" \
|
|
2285
|
+
--arg prefix "$TASK_NPM_PREFIX" \
|
|
2286
|
+
--arg cache "$TASK_NPM_CACHE" '
|
|
2287
|
+
def is_owned_npm_config($owned):
|
|
2288
|
+
ascii_downcase as $folded
|
|
2289
|
+
| ($folded | startswith("npm_config_"))
|
|
2290
|
+
and (($owned | index($folded[11:] | gsub("_"; "-"))) != null);
|
|
2291
|
+
(. // []) as $env
|
|
2292
|
+
| [
|
|
2293
|
+
"NPM_CONFIG_PREFIX=\($prefix)",
|
|
2294
|
+
"npm_config_prefix=\($prefix)",
|
|
2295
|
+
"NPM_CONFIG_CACHE=\($cache)",
|
|
2296
|
+
"npm_config_cache=\($cache)",
|
|
2297
|
+
"NPM_CONFIG_LOGS_DIR=\($cache)/_logs",
|
|
2298
|
+
"npm_config_logs_dir=\($cache)/_logs",
|
|
2299
|
+
"NPM_CONFIG_UMASK=0002",
|
|
2300
|
+
"npm_config_umask=0002"
|
|
2301
|
+
] as $required
|
|
2302
|
+
| [
|
|
2303
|
+
"prefix", "cache", "logs-dir", "umask"
|
|
2304
|
+
] as $owned
|
|
2305
|
+
| ($env | type == "array")
|
|
2306
|
+
and all($env[]; type == "string")
|
|
2307
|
+
and all($required[];
|
|
2308
|
+
. as $required_value
|
|
2309
|
+
| ([ $env[] | select(. == $required_value) ] | length) == 1
|
|
2310
|
+
)
|
|
2311
|
+
and ([
|
|
2312
|
+
$env[]
|
|
2313
|
+
| split("=")[0]
|
|
2314
|
+
| select(is_owned_npm_config($owned))
|
|
2315
|
+
] | length) == ($required | length)
|
|
2316
|
+
and ([ $env[] | select(. == "PATH=\($path)") ] | length) == 1
|
|
2317
|
+
and ([ $env[] | select(. == "ASDF_DATA_DIR=/opt/asdf-data") ] | length) == 1
|
|
2318
|
+
and ([ $env[] | select(. == "ASDF_SKIP_RESHIM=1") ] | length) == 1
|
|
2319
|
+
and ([ $env[] | select(. == "COREPACK_HOME=/opt/asdf-data/corepack") ] | length) == 1
|
|
2320
|
+
and ([ $env[] | select(. == "COREPACK_ENABLE_NETWORK=0") ] | length) == 1
|
|
2321
|
+
' >/dev/null 2>&1; then
|
|
2322
|
+
if ! quarantine_untrusted_app_container; then
|
|
2323
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2324
|
+
"Uai found an unsafe task runtime environment and could not prove the container stopped. Stop it manually before retrying." \
|
|
2325
|
+
"verify task runtime isolation"
|
|
2326
|
+
fi
|
|
2327
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2328
|
+
"Uai stopped this task because its runtime/npm environment did not match the fixed private-prefix and read-only-cache authority. Retry to recreate the container safely." \
|
|
2329
|
+
"verify task runtime isolation"
|
|
1218
2330
|
fi
|
|
1219
2331
|
|
|
1220
2332
|
# -----------------------------------------------------------------------------
|
|
1221
|
-
#
|
|
2333
|
+
# 6a. ADR-108 (apple only): establish the in-guest runtime overlay, then
|
|
2334
|
+
# materialize runtimes INSIDE the container. The shared volume is never
|
|
2335
|
+
# written on the task path: the read-only lower proved above merges under
|
|
2336
|
+
# a task-private upper layer, so a missing toolchain installs into this
|
|
2337
|
+
# task's own layer with zero shared-volume coordination. Both steps run
|
|
2338
|
+
# before any credential, editor, or agent work; a failure quarantines the
|
|
2339
|
+
# container exactly like the isolation proofs above.
|
|
2340
|
+
# -----------------------------------------------------------------------------
|
|
2341
|
+
|
|
2342
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
2343
|
+
step "CONTAINER_INIT_FAILED" "establish task runtime overlay"
|
|
2344
|
+
overlay_rc=0
|
|
2345
|
+
"$CONTAINER_CLI" exec -u root "$app_container" /bin/sh -c '
|
|
2346
|
+
set -e
|
|
2347
|
+
/bin/mkdir -p /var/lib/uai/asdf-upper /var/lib/uai/asdf-work /opt/asdf-data
|
|
2348
|
+
# The merged root inherits the UPPER directory'"'"'s metadata; without this
|
|
2349
|
+
# the node user cannot write the merged view at all (live 2026-08-18).
|
|
2350
|
+
/bin/chown node:node /var/lib/uai/asdf-upper
|
|
2351
|
+
if ! /bin/grep -q "^overlay /opt/asdf-data overlay " /proc/mounts; then
|
|
2352
|
+
/bin/mount -t overlay overlay \
|
|
2353
|
+
-o lowerdir=/run/uai/asdf-lower,upperdir=/var/lib/uai/asdf-upper,workdir=/var/lib/uai/asdf-work \
|
|
2354
|
+
/opt/asdf-data
|
|
2355
|
+
fi
|
|
2356
|
+
# Prove the exact shape from the guest mount table, not from trust —
|
|
2357
|
+
# token-exact (comma-delimited; substring matching accepted
|
|
2358
|
+
# lowerdir=/run/uai/asdf-lower-evil), exactly ONE effective mount at the
|
|
2359
|
+
# merged path and at the lower, a read-only lower, and a real node-owned
|
|
2360
|
+
# upper directory.
|
|
2361
|
+
overlay_lines=$(/bin/grep -c "^overlay /opt/asdf-data overlay " /proc/mounts || true)
|
|
2362
|
+
[ "$overlay_lines" = "1" ] || { echo "expected exactly one /opt/asdf-data overlay, found $overlay_lines" >&2; exit 1; }
|
|
2363
|
+
any_lines=$(/bin/grep -c " /opt/asdf-data " /proc/mounts || true)
|
|
2364
|
+
[ "$any_lines" = "1" ] || { echo "unexpected extra mounts at /opt/asdf-data" >&2; exit 1; }
|
|
2365
|
+
opts=$(/bin/grep "^overlay /opt/asdf-data overlay " /proc/mounts | /usr/bin/cut -d" " -f4)
|
|
2366
|
+
case ",$opts," in *",lowerdir=/run/uai/asdf-lower,"*) ;; *) echo "overlay lower is foreign" >&2; exit 1 ;; esac
|
|
2367
|
+
case ",$opts," in *",upperdir=/var/lib/uai/asdf-upper,"*) ;; *) echo "overlay upper is foreign" >&2; exit 1 ;; esac
|
|
2368
|
+
case ",$opts," in *",workdir=/var/lib/uai/asdf-work,"*) ;; *) echo "overlay workdir is foreign" >&2; exit 1 ;; esac
|
|
2369
|
+
case ",$opts," in *",ro,"*) echo "overlay is unexpectedly read-only" >&2; exit 1 ;; esac
|
|
2370
|
+
lower_lines=$(/bin/grep -c " /run/uai/asdf-lower " /proc/mounts || true)
|
|
2371
|
+
[ "$lower_lines" = "1" ] || { echo "expected exactly one lower attachment, found $lower_lines" >&2; exit 1; }
|
|
2372
|
+
lopts=$(/bin/grep " /run/uai/asdf-lower " /proc/mounts | /usr/bin/cut -d" " -f4)
|
|
2373
|
+
case ",$lopts," in *",ro,"*) ;; *) echo "lower attachment is not read-only" >&2; exit 1 ;; esac
|
|
2374
|
+
[ ! -L /var/lib/uai/asdf-upper ] || { echo "upper is a symlink" >&2; exit 1; }
|
|
2375
|
+
[ -d /var/lib/uai/asdf-upper ] || { echo "upper is not a directory" >&2; exit 1; }
|
|
2376
|
+
[ "$(/usr/bin/stat -c %U /var/lib/uai/asdf-upper)" = "node" ] || { echo "upper is not node-owned" >&2; exit 1; }
|
|
2377
|
+
/usr/bin/test -d /opt/asdf-data/plugins
|
|
2378
|
+
' >/dev/null || overlay_rc=$?
|
|
2379
|
+
if [ "$overlay_rc" -ne 0 ]; then
|
|
2380
|
+
require_docker_after_failed_step "establish task runtime overlay"
|
|
2381
|
+
if ! quarantine_untrusted_app_container; then
|
|
2382
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2383
|
+
"Uai could not establish this task's runtime overlay, and could not prove the container stopped. Stop it manually before retrying." \
|
|
2384
|
+
"establish task runtime overlay"
|
|
2385
|
+
fi
|
|
2386
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2387
|
+
"Uai could not establish this task's runtime overlay on the Apple container runtime. Retry the task." \
|
|
2388
|
+
"establish task runtime overlay"
|
|
2389
|
+
fi
|
|
2390
|
+
|
|
2391
|
+
if [ "$project_count" -gt 0 ]; then
|
|
2392
|
+
step "CONTAINER_INIT_FAILED" "materialize task runtimes in container"
|
|
2393
|
+
materialization_rc=0
|
|
2394
|
+
"$CONTAINER_CLI" exec -u node -w /workspace "$app_container" \
|
|
2395
|
+
/usr/bin/env -i \
|
|
2396
|
+
HOME=/home/node \
|
|
2397
|
+
ASDF_DIR=/opt/asdf \
|
|
2398
|
+
ASDF_DATA_DIR=/opt/asdf-data \
|
|
2399
|
+
PATH=/opt/asdf-data/shims:/opt/asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
|
2400
|
+
/usr/bin/timeout --signal=TERM --kill-after=10s 600s \
|
|
2401
|
+
/usr/local/bin/uai-materialize-runtimes >&2 || materialization_rc=$?
|
|
2402
|
+
if [ "$materialization_rc" -ne 0 ]; then
|
|
2403
|
+
require_docker_after_failed_step "materialize task runtimes in container"
|
|
2404
|
+
if ! quarantine_untrusted_app_container; then
|
|
2405
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2406
|
+
"Runtime materialization failed and Uai could not prove the task container stopped. Stop it manually before retrying." \
|
|
2407
|
+
"materialize task runtimes in container"
|
|
2408
|
+
fi
|
|
2409
|
+
case "$materialization_rc" in
|
|
2410
|
+
124|137)
|
|
2411
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2412
|
+
"Runtime materialization did not finish inside Uai's 10-minute startup budget. Retry the task: Uai re-checks every required runtime and package manager before agents start." \
|
|
2413
|
+
"materialize task runtimes in container"
|
|
2414
|
+
;;
|
|
2415
|
+
*)
|
|
2416
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2417
|
+
"Uai could not install and verify every runtime and package manager needed by this task. Check the project's .tool-versions and packageManager entries and the host's network availability, then retry." \
|
|
2418
|
+
"materialize task runtimes in container"
|
|
2419
|
+
;;
|
|
2420
|
+
esac
|
|
2421
|
+
fi
|
|
2422
|
+
fi
|
|
2423
|
+
fi
|
|
2424
|
+
|
|
2425
|
+
# -----------------------------------------------------------------------------
|
|
2426
|
+
# 7. Container init — copy AI creds in, install deps, launch code-server.
|
|
1222
2427
|
# -----------------------------------------------------------------------------
|
|
1223
2428
|
|
|
1224
2429
|
step "CONTAINER_INIT_FAILED" "uai-init (deps + code-server)"
|
|
@@ -1234,7 +2439,38 @@ managed_exec_env=(
|
|
|
1234
2439
|
-e "PATH=/usr/bin:/bin"
|
|
1235
2440
|
-e "XDG_CONFIG_HOME="
|
|
1236
2441
|
-e "XDG_DATA_HOME="
|
|
2442
|
+
-e "XDG_CACHE_HOME="
|
|
2443
|
+
-e "ASDF_DIR=/opt/asdf"
|
|
2444
|
+
-e "ASDF_DATA_DIR=/opt/asdf-data"
|
|
2445
|
+
-e "ASDF_CONFIG_FILE=/dev/null"
|
|
2446
|
+
-e "ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions"
|
|
2447
|
+
-e "ASDF_NODEJS_VERSION="
|
|
2448
|
+
-e "ASDF_PYTHON_VERSION="
|
|
2449
|
+
-e "ASDF_GOLANG_VERSION="
|
|
2450
|
+
-e "ASDF_RUBY_VERSION="
|
|
2451
|
+
-e "ASDF_RUST_VERSION="
|
|
2452
|
+
-e "ASDF_SKIP_RESHIM=1"
|
|
2453
|
+
-e "COREPACK_HOME=/opt/asdf-data/corepack"
|
|
2454
|
+
-e "COREPACK_DEFAULT_TO_LATEST=0"
|
|
2455
|
+
-e "COREPACK_ENV_FILE=0"
|
|
2456
|
+
-e "COREPACK_NPM_REGISTRY=https://registry.npmjs.org"
|
|
2457
|
+
-e "COREPACK_ENABLE_NETWORK=0"
|
|
2458
|
+
-e "COREPACK_ENABLE_AUTO_PIN=0"
|
|
2459
|
+
-e "COREPACK_ENABLE_DOWNLOAD_PROMPT=0"
|
|
2460
|
+
-e "COREPACK_ENABLE_PROJECT_SPEC=1"
|
|
2461
|
+
-e "COREPACK_ENABLE_STRICT=1"
|
|
2462
|
+
-e "COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=0"
|
|
2463
|
+
-e "COREPACK_INTEGRITY_CHECK=1"
|
|
2464
|
+
-e "NPM_CONFIG_PREFIX=$TASK_NPM_PREFIX"
|
|
2465
|
+
-e "npm_config_prefix=$TASK_NPM_PREFIX"
|
|
2466
|
+
-e "NPM_CONFIG_CACHE=$TASK_NPM_CACHE"
|
|
2467
|
+
-e "npm_config_cache=$TASK_NPM_CACHE"
|
|
2468
|
+
-e "NPM_CONFIG_LOGS_DIR=$TASK_NPM_CACHE/_logs"
|
|
2469
|
+
-e "npm_config_logs_dir=$TASK_NPM_CACHE/_logs"
|
|
2470
|
+
-e "NPM_CONFIG_UMASK=0002"
|
|
2471
|
+
-e "npm_config_umask=0002"
|
|
1237
2472
|
-e "LD_PRELOAD="
|
|
2473
|
+
-e "LD_AUDIT="
|
|
1238
2474
|
-e "LD_LIBRARY_PATH="
|
|
1239
2475
|
-e "DYLD_INSERT_LIBRARIES="
|
|
1240
2476
|
-e "DYLD_LIBRARY_PATH="
|
|
@@ -1260,13 +2496,62 @@ managed_clean_env=(
|
|
|
1260
2496
|
"PATH=/usr/bin:/bin"
|
|
1261
2497
|
)
|
|
1262
2498
|
|
|
2499
|
+
# Runtime-bearing task initialization gets the same fixed environment as
|
|
2500
|
+
# Compose and agent launches. Unlike the root/control array above, this path
|
|
2501
|
+
# includes task-private global packages last so unique tools are usable while
|
|
2502
|
+
# they remain unable to shadow shared shims or system controls.
|
|
2503
|
+
task_runtime_exec_env=(
|
|
2504
|
+
-e "HOME=/home/node"
|
|
2505
|
+
-e "PATH=$TASK_RUNTIME_PATH"
|
|
2506
|
+
-e "XDG_CONFIG_HOME=/home/node/.config"
|
|
2507
|
+
-e "XDG_DATA_HOME=/home/node/.local/share"
|
|
2508
|
+
-e "XDG_CACHE_HOME=/home/node/.cache"
|
|
2509
|
+
-e "ASDF_DIR=/opt/asdf"
|
|
2510
|
+
-e "ASDF_DATA_DIR=/opt/asdf-data"
|
|
2511
|
+
-e "ASDF_CONFIG_FILE=/dev/null"
|
|
2512
|
+
-e "ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions"
|
|
2513
|
+
-e "ASDF_NODEJS_VERSION="
|
|
2514
|
+
-e "ASDF_PYTHON_VERSION="
|
|
2515
|
+
-e "ASDF_GOLANG_VERSION="
|
|
2516
|
+
-e "ASDF_RUBY_VERSION="
|
|
2517
|
+
-e "ASDF_RUST_VERSION="
|
|
2518
|
+
-e "ASDF_SKIP_RESHIM=1"
|
|
2519
|
+
-e "COREPACK_HOME=/opt/asdf-data/corepack"
|
|
2520
|
+
-e "COREPACK_DEFAULT_TO_LATEST=0"
|
|
2521
|
+
-e "COREPACK_ENV_FILE=0"
|
|
2522
|
+
-e "COREPACK_NPM_REGISTRY=https://registry.npmjs.org"
|
|
2523
|
+
-e "COREPACK_ENABLE_NETWORK=0"
|
|
2524
|
+
-e "COREPACK_ENABLE_AUTO_PIN=0"
|
|
2525
|
+
-e "COREPACK_ENABLE_DOWNLOAD_PROMPT=0"
|
|
2526
|
+
-e "COREPACK_ENABLE_PROJECT_SPEC=1"
|
|
2527
|
+
-e "COREPACK_ENABLE_STRICT=1"
|
|
2528
|
+
-e "COREPACK_ENABLE_UNSAFE_CUSTOM_URLS=0"
|
|
2529
|
+
-e "COREPACK_INTEGRITY_CHECK=1"
|
|
2530
|
+
-e "NPM_CONFIG_PREFIX=$TASK_NPM_PREFIX"
|
|
2531
|
+
-e "npm_config_prefix=$TASK_NPM_PREFIX"
|
|
2532
|
+
-e "NPM_CONFIG_CACHE=$TASK_NPM_CACHE"
|
|
2533
|
+
-e "npm_config_cache=$TASK_NPM_CACHE"
|
|
2534
|
+
-e "NPM_CONFIG_LOGS_DIR=$TASK_NPM_CACHE/_logs"
|
|
2535
|
+
-e "npm_config_logs_dir=$TASK_NPM_CACHE/_logs"
|
|
2536
|
+
-e "NPM_CONFIG_UMASK=0002"
|
|
2537
|
+
-e "npm_config_umask=0002"
|
|
2538
|
+
-e "LD_PRELOAD="
|
|
2539
|
+
-e "LD_AUDIT="
|
|
2540
|
+
-e "LD_LIBRARY_PATH="
|
|
2541
|
+
-e "NODE_OPTIONS="
|
|
2542
|
+
-e "NODE_PATH="
|
|
2543
|
+
-e "PS4="
|
|
2544
|
+
-e "BASH_ENV="
|
|
2545
|
+
-e "ENV="
|
|
2546
|
+
)
|
|
2547
|
+
|
|
1263
2548
|
# OpenCode and code-server share ~/.local/share. OpenCode credentials are
|
|
1264
2549
|
# copied as root below, so creating only its leaf with `mkdir -p` can leave the
|
|
1265
2550
|
# shared parent root-owned (live 2026-08-04). Then node cannot create
|
|
1266
2551
|
# code-server's User profile and the editor silently falls back to its light,
|
|
1267
2552
|
# full-chrome defaults. Repair both explicitly on every start/resume; GNU
|
|
1268
2553
|
# `install -d` also corrects an existing directory with the wrong owner.
|
|
1269
|
-
if !
|
|
2554
|
+
if ! crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
1270
2555
|
/usr/bin/install -d -o node -g node -m 0755 \
|
|
1271
2556
|
/home/node/.local/share \
|
|
1272
2557
|
/home/node/.local/share/opencode >/dev/null; then
|
|
@@ -1280,7 +2565,7 @@ fi
|
|
|
1280
2565
|
# and never the host cache. The task-private ~/.gitconfig makes these entries
|
|
1281
2566
|
# local to this container, and the read-before-add keeps resumes idempotent.
|
|
1282
2567
|
step "CONTAINER_INIT_FAILED" "trust task Git worktrees"
|
|
1283
|
-
container_safe_dirs=$(
|
|
2568
|
+
container_safe_dirs=$(crt exec -u node "${managed_exec_env[@]}" \
|
|
1284
2569
|
"$app_container" "${managed_clean_env[@]}" /usr/bin/git config --global \
|
|
1285
2570
|
--get-all safe.directory 2>/dev/null || true)
|
|
1286
2571
|
while IFS= read -r safe_project; do
|
|
@@ -1290,10 +2575,12 @@ while IFS= read -r safe_project; do
|
|
|
1290
2575
|
for safe_worktree in \
|
|
1291
2576
|
"/workspace/$safe_project_slug" \
|
|
1292
2577
|
"$task_workspace/$safe_project_slug" \
|
|
1293
|
-
"$task_uai_dir/repos/$safe_project_id.git"
|
|
2578
|
+
"$task_uai_dir/repos/$safe_project_id.git" \
|
|
2579
|
+
"$task_workspace_physical/$safe_project_slug" \
|
|
2580
|
+
"$task_uai_dir_physical/repos/$safe_project_id.git"; do
|
|
1294
2581
|
if ! printf '%s\n' "$container_safe_dirs" \
|
|
1295
2582
|
| grep -Fqx -- "$safe_worktree"; then
|
|
1296
|
-
if !
|
|
2583
|
+
if ! crt exec -u node "${managed_exec_env[@]}" "$app_container" \
|
|
1297
2584
|
"${managed_clean_env[@]}" /usr/bin/git config --global --add \
|
|
1298
2585
|
safe.directory "$safe_worktree" >/dev/null; then
|
|
1299
2586
|
emit_err "CONTAINER_INIT_FAILED" \
|
|
@@ -1305,11 +2592,31 @@ while IFS= read -r safe_project; do
|
|
|
1305
2592
|
done
|
|
1306
2593
|
done < <(jq -c '.[]' <<<"$projects_json")
|
|
1307
2594
|
|
|
2595
|
+
# A running container is not sufficient proof that its linked worktrees are
|
|
2596
|
+
# usable. In particular, Git may have canonicalized a symlinked host workspace
|
|
2597
|
+
# root while Compose mounted only its lexical spelling. Verify the actual path
|
|
2598
|
+
# agents use before credentials, dependency installation, or status=running.
|
|
2599
|
+
step "CONTAINER_INIT_FAILED" "verify task Git worktrees"
|
|
2600
|
+
while IFS= read -r verify_project; do
|
|
2601
|
+
[ -n "$verify_project" ] || continue
|
|
2602
|
+
verify_project_slug=$(jq -r '.slug' <<<"$verify_project")
|
|
2603
|
+
verify_is_worktree=""
|
|
2604
|
+
if ! verify_is_worktree=$(crt exec -u node "${managed_exec_env[@]}" \
|
|
2605
|
+
"$app_container" "${managed_clean_env[@]}" /usr/bin/git \
|
|
2606
|
+
-C "/workspace/$verify_project_slug" rev-parse \
|
|
2607
|
+
--is-inside-work-tree 2>/dev/null) \
|
|
2608
|
+
|| [ "$verify_is_worktree" != "true" ]; then
|
|
2609
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2610
|
+
"Uai could not open $verify_project_slug as a Git worktree inside the task container. Retry the task after checking the host workspace path." \
|
|
2611
|
+
"verify task Git worktree ($verify_project_slug)"
|
|
2612
|
+
fi
|
|
2613
|
+
done < <(jq -c '.[]' <<<"$projects_json")
|
|
2614
|
+
|
|
1308
2615
|
# A failed/retried start can preserve a container that still holds a previous
|
|
1309
2616
|
# account. Erase that Uai-managed state first for both connected and
|
|
1310
2617
|
# disconnected starts; otherwise a no-credential resume could briefly reuse a
|
|
1311
2618
|
# stale token before the asynchronous reconciler catches up.
|
|
1312
|
-
if !
|
|
2619
|
+
if ! crt exec -u node "${managed_exec_env[@]}" "$app_container" \
|
|
1313
2620
|
"${managed_clean_env[@]}" /bin/sh -c '
|
|
1314
2621
|
set -eu
|
|
1315
2622
|
/bin/rm -f "$GH_CONFIG_DIR/hosts.yml"
|
|
@@ -1326,6 +2633,7 @@ if ! docker exec -u node "${managed_exec_env[@]}" "$app_container" \
|
|
|
1326
2633
|
done
|
|
1327
2634
|
test ! -e "$GH_CONFIG_DIR/hosts.yml"
|
|
1328
2635
|
' >/dev/null; then
|
|
2636
|
+
require_docker_after_failed_step "clear container GitHub credential"
|
|
1329
2637
|
emit_err "GITHUB_AUTH_UNAVAILABLE" \
|
|
1330
2638
|
"Uai could not clear the previous managed GitHub credential from the task container. Retry the task before doing GitHub work." \
|
|
1331
2639
|
"clear container GitHub credential"
|
|
@@ -1340,16 +2648,18 @@ if [ -n "$github_credential_socket" ]; then
|
|
|
1340
2648
|
if ! printf 'protocol=https\nhost=github.com\n\n' \
|
|
1341
2649
|
| git_with_github_credential credential fill \
|
|
1342
2650
|
| sed -n 's/^password=//p' \
|
|
1343
|
-
|
|
|
2651
|
+
| crt exec -i -u node "${managed_exec_env[@]}" "$app_container" \
|
|
1344
2652
|
"${managed_clean_env[@]}" /usr/bin/gh auth login --with-token \
|
|
1345
2653
|
--hostname github.com --git-protocol https >/dev/null; then
|
|
2654
|
+
require_docker_after_failed_step "configure container GitHub credential"
|
|
1346
2655
|
emit_err "GITHUB_AUTH_UNAVAILABLE" \
|
|
1347
2656
|
"GitHub is connected on this host, but Uai could not install that credential in the task container. Retry the task; if it persists, reconnect GitHub on this host." \
|
|
1348
2657
|
"configure container GitHub credential"
|
|
1349
2658
|
fi
|
|
1350
|
-
if !
|
|
2659
|
+
if ! crt exec -u node "${managed_exec_env[@]}" "$app_container" \
|
|
1351
2660
|
"${managed_clean_env[@]}" /usr/bin/gh auth setup-git \
|
|
1352
2661
|
--hostname github.com >/dev/null; then
|
|
2662
|
+
require_docker_after_failed_step "configure container Git credential helper"
|
|
1353
2663
|
emit_err "GITHUB_AUTH_UNAVAILABLE" \
|
|
1354
2664
|
"GitHub is connected on this host, but Uai could not configure Git HTTPS in the task container. Retry the task; if it persists, reconnect GitHub on this host." \
|
|
1355
2665
|
"configure container Git credential helper"
|
|
@@ -1362,11 +2672,11 @@ fi
|
|
|
1362
2672
|
# the bind mount and leave a stale dentry, after which claude refuses to
|
|
1363
2673
|
# start. docker cp gives the container its own stable copy. Best-effort.
|
|
1364
2674
|
if [ -f "$UAI_OWNER_HOME/.claude.json" ]; then
|
|
1365
|
-
|
|
2675
|
+
crt cp "$UAI_OWNER_HOME/.claude.json" \
|
|
1366
2676
|
"$app_container":/home/node/.claude.json >/dev/null 2>&1 \
|
|
1367
|
-
|| log "warning:
|
|
1368
|
-
|
|
1369
|
-
chown node:node /home/node/.claude.json >/dev/null 2>&1 || true
|
|
2677
|
+
|| log "warning: crt cp of .claude.json failed; claude may need re-login"
|
|
2678
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2679
|
+
/usr/bin/chown node:node /home/node/.claude.json >/dev/null 2>&1 || true
|
|
1370
2680
|
fi
|
|
1371
2681
|
|
|
1372
2682
|
# Copy Codex credentials/config into a task-private /home/node/.codex.
|
|
@@ -1376,18 +2686,18 @@ fi
|
|
|
1376
2686
|
# docker cp preserves the host uid/gid, so a skipped chown leaves 0600
|
|
1377
2687
|
# files owned by e.g. macOS 501:20 that the container's node (1000:1000)
|
|
1378
2688
|
# cannot read — Codex then dies at startup.
|
|
1379
|
-
if
|
|
1380
|
-
mkdir -p /home/node/.codex >/dev/null; then
|
|
2689
|
+
if crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2690
|
+
/usr/bin/mkdir -p /home/node/.codex >/dev/null; then
|
|
1381
2691
|
for codex_item in auth.json config.toml AGENTS.md version.json installation_id rules; do
|
|
1382
2692
|
if [ -e "$UAI_OWNER_HOME/.codex/$codex_item" ]; then
|
|
1383
|
-
|
|
2693
|
+
crt cp "$UAI_OWNER_HOME/.codex/$codex_item" \
|
|
1384
2694
|
"$app_container":/home/node/.codex/ >/dev/null \
|
|
1385
|
-
|| log "ERROR:
|
|
2695
|
+
|| log "ERROR: crt cp of .codex/$codex_item failed; Codex in this task may not authenticate"
|
|
1386
2696
|
fi
|
|
1387
2697
|
done
|
|
1388
|
-
|
|
1389
|
-
chown -R node:node /home/node/.codex >/dev/null \
|
|
1390
|
-
|| log "ERROR: chown of /home/node/.codex failed — 0600 credentials remain unreadable by node. Fix: docker exec -u root $app_container chown -R node:node /home/node/.codex"
|
|
2698
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2699
|
+
/usr/bin/chown -R node:node /home/node/.codex >/dev/null \
|
|
2700
|
+
|| log "ERROR: chown of /home/node/.codex failed — 0600 credentials remain unreadable by node. Fix (docker mode; use the bundled container CLI on Apple hosts): docker exec -u root -e PATH=/usr/bin:/bin $app_container /usr/bin/chown -R node:node /home/node/.codex"
|
|
1391
2701
|
else
|
|
1392
2702
|
log "ERROR: mkdir /home/node/.codex failed in $app_container; Codex credentials were not injected"
|
|
1393
2703
|
fi
|
|
@@ -1396,39 +2706,39 @@ fi
|
|
|
1396
2706
|
# The Linux `kimi` binary is baked into the image; here we copy ONLY the
|
|
1397
2707
|
# arch-independent config + credential (never the host's macOS bin/, nor live
|
|
1398
2708
|
# session state) so the container's kimi runs on the operator's subscription.
|
|
1399
|
-
|
|
1400
|
-
mkdir -p /home/node/.kimi-code/credentials >/dev/null 2>&1 || true
|
|
2709
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2710
|
+
/usr/bin/mkdir -p /home/node/.kimi-code/credentials >/dev/null 2>&1 || true
|
|
1401
2711
|
if [ -e "$UAI_OWNER_HOME/.kimi-code/config.toml" ]; then
|
|
1402
|
-
|
|
2712
|
+
crt cp "$UAI_OWNER_HOME/.kimi-code/config.toml" \
|
|
1403
2713
|
"$app_container":/home/node/.kimi-code/config.toml >/dev/null 2>&1 \
|
|
1404
|
-
|| log "warning:
|
|
2714
|
+
|| log "warning: crt cp of .kimi-code/config.toml failed; kimi may need re-login"
|
|
1405
2715
|
fi
|
|
1406
2716
|
if [ -e "$UAI_OWNER_HOME/.kimi-code/device_id" ]; then
|
|
1407
|
-
|
|
2717
|
+
crt cp "$UAI_OWNER_HOME/.kimi-code/device_id" \
|
|
1408
2718
|
"$app_container":/home/node/.kimi-code/device_id >/dev/null 2>&1 || true
|
|
1409
2719
|
fi
|
|
1410
2720
|
if [ -e "$UAI_OWNER_HOME/.kimi-code/credentials/kimi-code.json" ]; then
|
|
1411
|
-
|
|
2721
|
+
crt cp "$UAI_OWNER_HOME/.kimi-code/credentials/kimi-code.json" \
|
|
1412
2722
|
"$app_container":/home/node/.kimi-code/credentials/kimi-code.json >/dev/null 2>&1 \
|
|
1413
|
-
|| log "warning:
|
|
2723
|
+
|| log "warning: crt cp of .kimi-code credential failed; kimi may need re-login"
|
|
1414
2724
|
fi
|
|
1415
|
-
|
|
1416
|
-
chown -R node:node /home/node/.kimi-code >/dev/null 2>&1 || true
|
|
2725
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2726
|
+
/usr/bin/chown -R node:node /home/node/.kimi-code >/dev/null 2>&1 || true
|
|
1417
2727
|
|
|
1418
2728
|
# Copy Grok subscription/config into a task-private /home/node/.grok. The Linux
|
|
1419
2729
|
# `grok` binary is baked into the image; here we copy the arch-independent
|
|
1420
2730
|
# auth + config (auth.json is an OIDC token the CLI refreshes in-container).
|
|
1421
|
-
|
|
1422
|
-
mkdir -p /home/node/.grok >/dev/null 2>&1 || true
|
|
2731
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2732
|
+
/usr/bin/mkdir -p /home/node/.grok >/dev/null 2>&1 || true
|
|
1423
2733
|
for grok_item in auth.json config.toml models_cache.json agent_id; do
|
|
1424
2734
|
if [ -e "$UAI_OWNER_HOME/.grok/$grok_item" ]; then
|
|
1425
|
-
|
|
2735
|
+
crt cp "$UAI_OWNER_HOME/.grok/$grok_item" \
|
|
1426
2736
|
"$app_container":/home/node/.grok/ >/dev/null 2>&1 \
|
|
1427
|
-
|| log "warning:
|
|
2737
|
+
|| log "warning: crt cp of .grok/$grok_item failed; grok may need re-login"
|
|
1428
2738
|
fi
|
|
1429
2739
|
done
|
|
1430
|
-
|
|
1431
|
-
chown -R node:node /home/node/.grok >/dev/null 2>&1 || true
|
|
2740
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2741
|
+
/usr/bin/chown -R node:node /home/node/.grok >/dev/null 2>&1 || true
|
|
1432
2742
|
|
|
1433
2743
|
# Copy OpenCode provider auth into a task-private /home/node/.local/share/opencode
|
|
1434
2744
|
# (ADR-077), same per-task-writable-copy pattern as Codex. The Linux `opencode`
|
|
@@ -1438,13 +2748,13 @@ docker exec -u root "$app_container" \
|
|
|
1438
2748
|
# The shared parent and this leaf were created with node ownership above.
|
|
1439
2749
|
for oc_item in auth.json config.json; do
|
|
1440
2750
|
if [ -e "$UAI_OWNER_HOME/.local/share/opencode/$oc_item" ]; then
|
|
1441
|
-
|
|
2751
|
+
crt cp "$UAI_OWNER_HOME/.local/share/opencode/$oc_item" \
|
|
1442
2752
|
"$app_container":/home/node/.local/share/opencode/ >/dev/null 2>&1 \
|
|
1443
|
-
|| log "warning:
|
|
2753
|
+
|| log "warning: crt cp of opencode/$oc_item failed; opencode may need re-login"
|
|
1444
2754
|
fi
|
|
1445
2755
|
done
|
|
1446
|
-
|
|
1447
|
-
chown -R node:node /home/node/.local/share/opencode >/dev/null 2>&1 || true
|
|
2756
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2757
|
+
/usr/bin/chown -R node:node /home/node/.local/share/opencode >/dev/null 2>&1 || true
|
|
1448
2758
|
|
|
1449
2759
|
# Copy the resolved SSH identity into the container for commit/tag signing and
|
|
1450
2760
|
# as the no-GitHub-connection transport fallback. Connected users clone, fetch,
|
|
@@ -1454,17 +2764,17 @@ docker exec -u root "$app_container" \
|
|
|
1454
2764
|
# permissions problem hours later; live 2026-07-21). The channel-ensure path
|
|
1455
2765
|
# (ensureTaskSshIdentity) re-asserts all of this on reconnects.
|
|
1456
2766
|
if [ -f "$uai_identity_key" ]; then
|
|
1457
|
-
|
|
1458
|
-
mkdir -p /home/node/.ssh >/dev/null 2>&1 \
|
|
2767
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2768
|
+
/usr/bin/mkdir -p /home/node/.ssh >/dev/null 2>&1 \
|
|
1459
2769
|
|| log "warning: ssh setup: mkdir ~/.ssh failed in $app_container"
|
|
1460
|
-
|
|
2770
|
+
crt cp "$uai_identity_key" \
|
|
1461
2771
|
"$app_container":/home/node/.ssh/id_ed25519 >/dev/null 2>&1 \
|
|
1462
2772
|
|| log "warning: ssh setup: key copy failed — signing and SSH fallback unavailable"
|
|
1463
|
-
|
|
2773
|
+
crt cp "${uai_identity_key}.pub" \
|
|
1464
2774
|
"$app_container":/home/node/.ssh/id_ed25519.pub >/dev/null 2>&1 \
|
|
1465
2775
|
|| log "warning: ssh setup: pubkey copy failed (signing will be off)"
|
|
1466
|
-
|
|
1467
|
-
'chown -R node:node /home/node/.ssh && chmod 700 /home/node/.ssh && chmod 600 /home/node/.ssh/id_ed25519' \
|
|
2776
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" /bin/sh -c \
|
|
2777
|
+
'/usr/bin/chown -R node:node /home/node/.ssh && /usr/bin/chmod 700 /home/node/.ssh && /usr/bin/chmod 600 /home/node/.ssh/id_ed25519' \
|
|
1468
2778
|
>/dev/null 2>&1 \
|
|
1469
2779
|
|| log "warning: ssh setup: chown/chmod failed — key may be unreadable by node"
|
|
1470
2780
|
fi
|
|
@@ -1475,26 +2785,70 @@ fi
|
|
|
1475
2785
|
# anywhere on this path) used to fail the whole task over a step anyone can
|
|
1476
2786
|
# re-run. Retry once, then come up DEGRADED with a warning the host surfaces
|
|
1477
2787
|
# as a system note in the task feed.
|
|
2788
|
+
quarantine_runtime_authority_violation() {
|
|
2789
|
+
quarantine_untrusted_app_container
|
|
2790
|
+
}
|
|
2791
|
+
|
|
1478
2792
|
init_warning=""
|
|
1479
|
-
|
|
2793
|
+
init_rc=0
|
|
2794
|
+
crt exec "${task_runtime_exec_env[@]}" "$app_container" \
|
|
2795
|
+
/usr/local/bin/uai-init >/dev/null || init_rc=$?
|
|
2796
|
+
if [ "$init_rc" -ne 0 ]; then
|
|
2797
|
+
require_docker_after_failed_step "uai-init (deps + code-server)"
|
|
2798
|
+
if [ "$init_rc" -eq 78 ]; then
|
|
2799
|
+
# Exit 78 is reserved for a host-authority violation, not an ordinary
|
|
2800
|
+
# best-effort dependency failure. Keep the already-created app unavailable
|
|
2801
|
+
# and prove it stopped before returning an actionable task-up failure.
|
|
2802
|
+
if ! quarantine_runtime_authority_violation; then
|
|
2803
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2804
|
+
"A workspace runtime-authority violation was detected, and Uai could not prove the task container stopped. Stop it manually before retrying." \
|
|
2805
|
+
"quarantine runtime-authority violation"
|
|
2806
|
+
fi
|
|
2807
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2808
|
+
"Uai stopped this task because its runtime isolation changed after startup (for example, a writable shared cache, an unsafe private npm path, or a workspace-root .tool-versions). Retry after removing the reported redirect; project runtime manifests belong only inside selected project directories." \
|
|
2809
|
+
"uai-init runtime authority"
|
|
2810
|
+
fi
|
|
1480
2811
|
log "warning: uai-init failed — retrying once"
|
|
1481
2812
|
sleep 2
|
|
1482
|
-
|
|
2813
|
+
init_rc=0
|
|
2814
|
+
crt exec "${task_runtime_exec_env[@]}" "$app_container" \
|
|
2815
|
+
/usr/local/bin/uai-init >/dev/null || init_rc=$?
|
|
2816
|
+
if [ "$init_rc" -ne 0 ]; then
|
|
2817
|
+
require_docker_after_failed_step "uai-init (deps + code-server)"
|
|
2818
|
+
if [ "$init_rc" -eq 78 ]; then
|
|
2819
|
+
if ! quarantine_runtime_authority_violation; then
|
|
2820
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2821
|
+
"A workspace runtime-authority violation was detected, and Uai could not prove the task container stopped. Stop it manually before retrying." \
|
|
2822
|
+
"quarantine runtime-authority violation"
|
|
2823
|
+
fi
|
|
2824
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2825
|
+
"Uai stopped this task because its runtime isolation changed after startup (for example, a writable shared cache, an unsafe private npm path, or a workspace-root .tool-versions). Retry after removing the reported redirect." \
|
|
2826
|
+
"uai-init runtime authority"
|
|
2827
|
+
fi
|
|
1483
2828
|
init_warning="Workspace init (deps + editor) failed twice — the task is up, but the editor and installed dependencies may be missing. Retry from a task terminal with: /usr/local/bin/uai-init"
|
|
1484
2829
|
log "warning: $init_warning"
|
|
1485
2830
|
fi
|
|
1486
2831
|
fi
|
|
1487
2832
|
|
|
1488
2833
|
# -----------------------------------------------------------------------------
|
|
1489
|
-
#
|
|
2834
|
+
# 8. Discover code-server + preview ports, mark running, unlock.
|
|
1490
2835
|
# -----------------------------------------------------------------------------
|
|
1491
2836
|
|
|
1492
2837
|
# code-server: container exposes 8080; Docker auto-maps to a random host
|
|
1493
2838
|
# port. Discover via `docker port`. Best-effort — if it fails the Editor
|
|
1494
2839
|
# pane is just unavailable.
|
|
1495
|
-
code_server_port
|
|
1496
|
-
if [
|
|
1497
|
-
|
|
2840
|
+
code_server_port=""
|
|
2841
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
2842
|
+
# No ports are published on the Apple runtime: the container's vmnet IP is
|
|
2843
|
+
# host-routable, so the Editor tunnel dials <container-ip>:8080 directly
|
|
2844
|
+
# (resolved per-request from the provider's route inspection).
|
|
2845
|
+
:
|
|
2846
|
+
else
|
|
2847
|
+
code_server_port=$(mapped_host_port "$app_container" 8080)
|
|
2848
|
+
if [ -z "$code_server_port" ]; then
|
|
2849
|
+
require_docker_after_failed_step "discover code-server port"
|
|
2850
|
+
log "warning: could not discover code-server port for ${app_container} (editor pane unavailable)"
|
|
2851
|
+
fi
|
|
1498
2852
|
fi
|
|
1499
2853
|
|
|
1500
2854
|
# ADR-043 Stage 2: preview ports are no longer published at task-up, so there is
|