@runuai/host 0.9.148 → 0.9.149
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/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/codex.ts +9 -1
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +2 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/transport.ts +9 -5
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +391 -69
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +939 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2219 -15
package/scripts/agent/task-up.sh
CHANGED
|
@@ -37,6 +37,10 @@ setup_err_trap
|
|
|
37
37
|
# Pinned constants (all clusters must agree — see ADR-022).
|
|
38
38
|
STANDARD_IMAGE="uai-standard:dev"
|
|
39
39
|
ASDF_VOLUME="uai-asdf-data"
|
|
40
|
+
# Keep in sync with lib/standard-image.ts. Apple tasks execute the env
|
|
41
|
+
# forwarder from the shared volume's read-only lower mount, never from the
|
|
42
|
+
# node-writable overlay at /opt/asdf-data.
|
|
43
|
+
STANDARD_DEFAULT_NODE_VERSION="22.11.0"
|
|
40
44
|
TASK_NPM_PREFIX="/home/node/.local/share/uai-npm"
|
|
41
45
|
TASK_NPM_CACHE="/home/node/.cache/uai-npm"
|
|
42
46
|
# Host-proved shims and system controls always win. A task-global package with
|
|
@@ -73,17 +77,42 @@ validate_task_runtime_selection
|
|
|
73
77
|
APPLE_TASK_MEMORY="${UAI_APPLE_TASK_MEMORY:-8192M}"
|
|
74
78
|
APPLE_TASK_CPUS="${UAI_APPLE_TASK_CPUS:-4}"
|
|
75
79
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
# ADR-153 makes the task's project-env delivery mode explicit. A loader task's
|
|
81
|
+
# stdin is exactly one base64-encoded task-generation forwarder secret; it is
|
|
82
|
+
# never put in argv, the environment, or a file. A legacy baked task retains
|
|
83
|
+
# the old JSON-object stdin contract until its container is recreated.
|
|
84
|
+
UAI_PROJECT_ENV_MODE=${UAI_PROJECT_ENV_MODE:-baked}
|
|
85
|
+
case "$UAI_PROJECT_ENV_MODE" in
|
|
86
|
+
baked | loader) ;;
|
|
87
|
+
*)
|
|
88
|
+
emit_err "BAD_ARGS" \
|
|
89
|
+
"task project env mode must be baked or loader" \
|
|
90
|
+
"read task environment mode"
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
|
|
94
|
+
task_stdin_payload=$(cat)
|
|
95
|
+
task_gateway_secret_b64=""
|
|
96
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
97
|
+
task_gateway_secret_b64=$task_stdin_payload
|
|
98
|
+
project_env_json='{}'
|
|
99
|
+
# A 32-byte value has one canonical standard-base64 shape. Validate that
|
|
100
|
+
# shape here without decoding it into a shell variable (which cannot carry
|
|
101
|
+
# NUL); the only decode happens later, directly into the forwarder's stdin.
|
|
102
|
+
if [[ ! "$task_gateway_secret_b64" =~ ^[A-Za-z0-9+/]{43}=$ ]]; then
|
|
103
|
+
emit_err "BAD_ARGS" \
|
|
104
|
+
"loader task gateway secret must decode to exactly 32 bytes" \
|
|
105
|
+
"read task gateway secret"
|
|
106
|
+
fi
|
|
107
|
+
else
|
|
108
|
+
project_env_json=$task_stdin_payload
|
|
109
|
+
[ -n "$project_env_json" ] || project_env_json='{}'
|
|
110
|
+
if ! printf '%s' "$project_env_json" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
111
|
+
emit_err "BAD_ARGS" "task environment payload must be a JSON object" "read task environment"
|
|
112
|
+
fi
|
|
113
|
+
project_env_json=$(printf '%s' "$project_env_json" | jq -c '.')
|
|
85
114
|
fi
|
|
86
|
-
|
|
115
|
+
unset task_stdin_payload
|
|
87
116
|
|
|
88
117
|
# -----------------------------------------------------------------------------
|
|
89
118
|
# Paths (mirror host-agent/lib/env.ts).
|
|
@@ -724,6 +753,23 @@ git_host_control() (
|
|
|
724
753
|
"$@"
|
|
725
754
|
)
|
|
726
755
|
|
|
756
|
+
# ADR-153 §4: record, once, the base a project's task worktree starts from, in
|
|
757
|
+
# the host control tree the container never sees, before anything in the task
|
|
758
|
+
# runs. The runner reads that commit's .env.schema from the host cache; a
|
|
759
|
+
# resume keeps the first record and never consults the worktree, which the
|
|
760
|
+
# task can rewrite. "oid <commit>" or "empty" (a branchless repository).
|
|
761
|
+
record_env_schema_base() {
|
|
762
|
+
local project_id="$1" base="$2" dir="$task_uai_dir/env-schemas" tmp
|
|
763
|
+
[ -e "$dir/$project_id.base" ] && return 0
|
|
764
|
+
mkdir -p "$dir" || return 1
|
|
765
|
+
chmod 700 "$dir" || return 1
|
|
766
|
+
tmp=$(mktemp "$dir/.base.XXXXXX") || return 1
|
|
767
|
+
if ! printf '%s\n' "$base" > "$tmp" || ! mv -f "$tmp" "$dir/$project_id.base"; then
|
|
768
|
+
rm -f "$tmp"
|
|
769
|
+
return 1
|
|
770
|
+
fi
|
|
771
|
+
}
|
|
772
|
+
|
|
727
773
|
assert_bare_repo_tree_safe() {
|
|
728
774
|
local repo="$1" code="$2" label="$3" bad_entry=""
|
|
729
775
|
if [ -L "$repo" ] || [ ! -d "$repo" ]; then
|
|
@@ -1268,6 +1314,14 @@ while IFS= read -r project_obj; do
|
|
|
1268
1314
|
"refs/remotes/origin/$project_default" >/dev/null 2>&1; then
|
|
1269
1315
|
# Normal case: branch the task worktree off origin/<defaultBranch>.
|
|
1270
1316
|
step "WORKTREE_FAILED" "git worktree add ($project_id)"
|
|
1317
|
+
# The same commit the task repository just copied, read where the task
|
|
1318
|
+
# never writes: the host cache, under this project's lock.
|
|
1319
|
+
env_schema_oid=$(git_host_control -C "$mirror_dir" rev-parse --verify --quiet \
|
|
1320
|
+
"refs/remotes/origin/$project_default^{commit}" 2>/dev/null || true)
|
|
1321
|
+
if [ -n "$env_schema_oid" ]; then
|
|
1322
|
+
record_env_schema_base "$project_id" "oid $env_schema_oid" \
|
|
1323
|
+
|| log "warning: could not record the .env.schema base for $project_id; its keys without a binding are withheld"
|
|
1324
|
+
fi
|
|
1271
1325
|
if ! git_host_control -C "$worktree_repo_dir" worktree add "$worktree_target" \
|
|
1272
1326
|
-b "$task_branch" "origin/$project_default"; then
|
|
1273
1327
|
emit_err "WORKTREE_FAILED" \
|
|
@@ -1282,6 +1336,15 @@ while IFS= read -r project_obj; do
|
|
|
1282
1336
|
# repo. Leave a note (git-excluded so it isn't accidentally committed).
|
|
1283
1337
|
step "WORKTREE_FAILED" "init empty workspace ($project_id)"
|
|
1284
1338
|
log "warning: $repo_url has no '$project_default' branch (empty repo?); starting an empty workspace for $project_slug"
|
|
1339
|
+
# Only a cache with no ref at all proves the repository empty. A missing
|
|
1340
|
+
# default branch alone (say, a default that could not be resolved) is
|
|
1341
|
+
# recorded as nothing, so its keys without a binding are withheld rather
|
|
1342
|
+
# than read as declaring nothing.
|
|
1343
|
+
if env_schema_refs=$(git_host_control -C "$mirror_dir" for-each-ref --count=1 2>/dev/null) \
|
|
1344
|
+
&& [ -z "$env_schema_refs" ]; then
|
|
1345
|
+
record_env_schema_base "$project_id" "empty" \
|
|
1346
|
+
|| log "warning: could not record the .env.schema base for $project_id; its keys without a binding are withheld"
|
|
1347
|
+
fi
|
|
1285
1348
|
rm -rf "$worktree_target"
|
|
1286
1349
|
mkdir -p "$worktree_target"
|
|
1287
1350
|
git_host_control -C "$worktree_target" init -q -b "$project_default" 2>/dev/null \
|
|
@@ -1390,12 +1453,29 @@ fi
|
|
|
1390
1453
|
UAI_INIT_SOURCE="$STANDARD_CONTAINER_DIR/uai-init"
|
|
1391
1454
|
RUNTIME_MATERIALIZER_SOURCE="$STANDARD_CONTAINER_DIR/uai-materialize-runtimes"
|
|
1392
1455
|
COREPACK_VERSION_SOURCE="$STANDARD_CONTAINER_DIR/corepack-version"
|
|
1456
|
+
ENV_HOOK_SOURCE="$SCRIPT_DIR/env-hook.sh"
|
|
1457
|
+
UAI_ENV_EXEC_SOURCE="$SCRIPT_DIR/uai-env-exec.sh"
|
|
1458
|
+
TASK_FORWARDER_SOURCE="$STANDARD_CONTAINER_DIR/uai-task-forwarder.mjs"
|
|
1393
1459
|
if [ -L "$UAI_INIT_SOURCE" ] || [ ! -f "$UAI_INIT_SOURCE" ] \
|
|
1394
1460
|
|| [ ! -x "$UAI_INIT_SOURCE" ]; then
|
|
1395
1461
|
emit_err "CONTAINER_INIT_FAILED" \
|
|
1396
1462
|
"Uai's packaged task initializer is missing or unsafe. Repair or update the host installation, then retry." \
|
|
1397
1463
|
"resolve packaged container assets"
|
|
1398
1464
|
fi
|
|
1465
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
1466
|
+
for loader_asset in \
|
|
1467
|
+
"$ENV_HOOK_SOURCE" \
|
|
1468
|
+
"$UAI_ENV_EXEC_SOURCE" \
|
|
1469
|
+
"$TASK_FORWARDER_SOURCE"
|
|
1470
|
+
do
|
|
1471
|
+
if [ -L "$loader_asset" ] || [ ! -f "$loader_asset" ] \
|
|
1472
|
+
|| [ ! -r "$loader_asset" ]; then
|
|
1473
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1474
|
+
"Uai's packaged task env loader is missing or unsafe. Repair or update the host installation, then retry." \
|
|
1475
|
+
"resolve packaged env loader assets"
|
|
1476
|
+
fi
|
|
1477
|
+
done
|
|
1478
|
+
fi
|
|
1399
1479
|
if [ "$project_count" -gt 0 ]; then
|
|
1400
1480
|
if [ -L "$RUNTIME_MATERIALIZER_SOURCE" ] \
|
|
1401
1481
|
|| [ ! -f "$RUNTIME_MATERIALIZER_SOURCE" ] \
|
|
@@ -1438,6 +1518,18 @@ if ! copy_task_asset "$UAI_INIT_SOURCE" uai-init 0755; then
|
|
|
1438
1518
|
"stage task container assets"
|
|
1439
1519
|
fi
|
|
1440
1520
|
UAI_INIT_SOURCE="$task_assets_dir/uai-init"
|
|
1521
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
1522
|
+
if ! copy_task_asset "$ENV_HOOK_SOURCE" env-hook.sh 0555 \
|
|
1523
|
+
|| ! copy_task_asset "$UAI_ENV_EXEC_SOURCE" uai-env-exec 0555 \
|
|
1524
|
+
|| ! copy_task_asset "$TASK_FORWARDER_SOURCE" uai-task-forwarder.mjs 0444; then
|
|
1525
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1526
|
+
"Uai could not stage the task env loader into this task's control tree. Check disk space and permissions under the Uai data directory, then retry." \
|
|
1527
|
+
"stage task env loader assets"
|
|
1528
|
+
fi
|
|
1529
|
+
ENV_HOOK_SOURCE="$task_assets_dir/env-hook.sh"
|
|
1530
|
+
UAI_ENV_EXEC_SOURCE="$task_assets_dir/uai-env-exec"
|
|
1531
|
+
TASK_FORWARDER_SOURCE="$task_assets_dir/uai-task-forwarder.mjs"
|
|
1532
|
+
fi
|
|
1441
1533
|
if [ -f "$RUNTIME_MATERIALIZER_SOURCE" ]; then
|
|
1442
1534
|
if ! copy_task_asset "$RUNTIME_MATERIALIZER_SOURCE" uai-materialize-runtimes 0755; then
|
|
1443
1535
|
emit_err "CONTAINER_INIT_FAILED" \
|
|
@@ -1531,8 +1623,10 @@ is_reserved_task_env_name() {
|
|
|
1531
1623
|
GIT_INDEX_FILE | GIT_EXEC_PATH | GIT_ASKPASS | SSH_ASKPASS | \
|
|
1532
1624
|
GIT_SSH | GIT_SSH_COMMAND | HOME | XDG_CONFIG_HOME | XDG_DATA_HOME | \
|
|
1533
1625
|
XDG_CACHE_HOME | \
|
|
1534
|
-
PATH | LD_PRELOAD | LD_AUDIT | LD_LIBRARY_PATH | NODE_OPTIONS | \
|
|
1626
|
+
PATH | SHELL | LD_PRELOAD | LD_AUDIT | LD_LIBRARY_PATH | NODE_OPTIONS | \
|
|
1535
1627
|
NODE_PATH | SHELLOPTS | BASHOPTS | PS4 | BASH_ENV | ENV | \
|
|
1628
|
+
__UAI_ENV_* | __CURSOR_SANDBOX_ENV_RESTORE | GROK_LOGIN_ENV | \
|
|
1629
|
+
NODE_EXTRA_CA_CERTS | SSL_CERT_FILE | REQUESTS_CA_BUNDLE | \
|
|
1536
1630
|
ASDF_* | COREPACK_* | \
|
|
1537
1631
|
[Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Pp][Rr][Ee][Ff][Ii][Xx] | \
|
|
1538
1632
|
[Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Cc][Aa][Cc][Hh][Ee] | \
|
|
@@ -1547,6 +1641,39 @@ is_reserved_task_env_name() {
|
|
|
1547
1641
|
return 1
|
|
1548
1642
|
}
|
|
1549
1643
|
|
|
1644
|
+
# Names that the loader owns are non-secret metadata. Mount the exact launch
|
|
1645
|
+
# set read-only so uai-init can remove even the empty clears inherited from a
|
|
1646
|
+
# derived image before it starts code-server. The editor daemon itself never
|
|
1647
|
+
# loads project env; each terminal shell fetches independently.
|
|
1648
|
+
managed_env_keys_file="$task_uai_dir/managed-env-keys"
|
|
1649
|
+
managed_env_keys_tmp=$(mktemp "$task_uai_dir/.managed-env-keys.XXXXXX")
|
|
1650
|
+
# macOS still ships Bash 3.2, which has indexed arrays but no associative
|
|
1651
|
+
# arrays. Names are validated before this delimiter set is consulted, so `|`
|
|
1652
|
+
# cannot collide with an env name.
|
|
1653
|
+
managed_env_seen='|'
|
|
1654
|
+
{
|
|
1655
|
+
while IFS= read -r managed_name; do
|
|
1656
|
+
[[ "$managed_name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
1657
|
+
is_reserved_task_env_name "$managed_name" && continue
|
|
1658
|
+
case "$managed_env_seen" in *"|$managed_name|"*) continue ;; esac
|
|
1659
|
+
managed_env_seen="${managed_env_seen}${managed_name}|"
|
|
1660
|
+
printf '%s\n' "$managed_name"
|
|
1661
|
+
done < <(jq -r '.[]' <<<"$union_env_keys_json")
|
|
1662
|
+
preview_env_raw=$(jq -r '.[0].preview_env // "{}"' <<<"$task_json")
|
|
1663
|
+
while IFS= read -r managed_name; do
|
|
1664
|
+
[[ "$managed_name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
1665
|
+
if [ "$managed_name" != "UAI_EDITOR_THEME" ] \
|
|
1666
|
+
&& is_reserved_task_env_name "$managed_name"; then
|
|
1667
|
+
continue
|
|
1668
|
+
fi
|
|
1669
|
+
case "$managed_env_seen" in *"|$managed_name|"*) continue ;; esac
|
|
1670
|
+
managed_env_seen="${managed_env_seen}${managed_name}|"
|
|
1671
|
+
printf '%s\n' "$managed_name"
|
|
1672
|
+
done < <(jq -r 'keys[]?' <<<"$preview_env_raw")
|
|
1673
|
+
} > "$managed_env_keys_tmp"
|
|
1674
|
+
chmod 0444 "$managed_env_keys_tmp"
|
|
1675
|
+
mv -f "$managed_env_keys_tmp" "$managed_env_keys_file"
|
|
1676
|
+
|
|
1550
1677
|
# Write the compose file. No top-level name: (the -p flag sets the project),
|
|
1551
1678
|
# no container_name:. Env values live host-side (ADR-015): we emit only
|
|
1552
1679
|
# pass-through references `${KEY:-}`.
|
|
@@ -1605,8 +1732,18 @@ is_reserved_task_env_name() {
|
|
|
1605
1732
|
fi
|
|
1606
1733
|
done < <(jq -c '.[]' <<<"$projects_json")
|
|
1607
1734
|
printf ' - "%s:/usr/local/bin/uai-init:ro"\n' "$UAI_INIT_SOURCE"
|
|
1735
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
1736
|
+
printf ' - "%s:/usr/local/share/uai/env-hook.sh:ro"\n' \
|
|
1737
|
+
"$ENV_HOOK_SOURCE"
|
|
1738
|
+
printf ' - "%s:/usr/local/bin/uai-env-exec:ro"\n' \
|
|
1739
|
+
"$UAI_ENV_EXEC_SOURCE"
|
|
1740
|
+
printf ' - "%s:/usr/local/share/uai/uai-task-forwarder.mjs:ro"\n' \
|
|
1741
|
+
"$TASK_FORWARDER_SOURCE"
|
|
1742
|
+
fi
|
|
1608
1743
|
printf ' - "%s:/run/uai/runtime-projects:ro"\n' \
|
|
1609
1744
|
"$runtime_projects_file"
|
|
1745
|
+
printf ' - "%s:/run/uai/managed-env-keys:ro"\n' \
|
|
1746
|
+
"$managed_env_keys_file"
|
|
1610
1747
|
# Task processes consume only the exact runtimes/package managers proved by
|
|
1611
1748
|
# pre-start maintenance. `:ro` is explicit because Docker's omitted default
|
|
1612
1749
|
# is read-write and a legacy Compose file must never look equivalent.
|
|
@@ -1683,6 +1820,9 @@ is_reserved_task_env_name() {
|
|
|
1683
1820
|
printf ' npm_config_umask: "0002"\n'
|
|
1684
1821
|
printf ' UAI_WORKSPACE: "/workspace"\n'
|
|
1685
1822
|
printf ' UAI_RUNTIME_PROJECTS: "/run/uai/runtime-projects"\n'
|
|
1823
|
+
printf ' UAI_ENV_MODE: "%s"\n' "$UAI_PROJECT_ENV_MODE"
|
|
1824
|
+
printf ' UAI_ENV_HOOK: "0"\n'
|
|
1825
|
+
printf ' SHELL: "/usr/bin/zsh"\n'
|
|
1686
1826
|
printf ' LD_PRELOAD: ""\n'
|
|
1687
1827
|
printf ' LD_AUDIT: ""\n'
|
|
1688
1828
|
printf ' LD_LIBRARY_PATH: ""\n'
|
|
@@ -1737,10 +1877,10 @@ is_reserved_task_env_name() {
|
|
|
1737
1877
|
is_reserved_task_env_name "$ekey" && continue
|
|
1738
1878
|
printf ' %s: ""\n' "$ekey"
|
|
1739
1879
|
done < <(jq -r '.[]' <<<"$union_env_keys_json")
|
|
1740
|
-
# Cloud-computed
|
|
1741
|
-
# task
|
|
1742
|
-
#
|
|
1743
|
-
#
|
|
1880
|
+
# Cloud-computed env (ADR-025): a JSON object {VAR: value} on the task row.
|
|
1881
|
+
# A legacy baked task writes the literals as before. A loader task emits only
|
|
1882
|
+
# empty clears (to defeat a derived image's ENV); the loopback endpoint is
|
|
1883
|
+
# the sole source of the current values. Two producers, both cloud-side:
|
|
1744
1884
|
# - PUBLIC preview URLs under operator-chosen names (EXPO_PACKAGER_PROXY_URL
|
|
1745
1885
|
# and friends), one per preview port that set `urlEnv`;
|
|
1746
1886
|
# - UAI_EDITOR_THEME (ADR-091), the Editor pane's colour theme as a VS Code
|
|
@@ -1759,7 +1899,11 @@ is_reserved_task_env_name() {
|
|
|
1759
1899
|
&& is_reserved_task_env_name "$pe_key"; then
|
|
1760
1900
|
continue
|
|
1761
1901
|
fi
|
|
1762
|
-
|
|
1902
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
1903
|
+
printf ' %s: ""\n' "$pe_key"
|
|
1904
|
+
else
|
|
1905
|
+
printf ' %s: "%s"\n' "$pe_key" "$pe_val"
|
|
1906
|
+
fi
|
|
1763
1907
|
done < <(jq -c 'to_entries[]?' <<<"$preview_env_raw")
|
|
1764
1908
|
printf 'volumes:\n'
|
|
1765
1909
|
printf ' %s:\n' "$ASDF_VOLUME"
|
|
@@ -2038,9 +2182,11 @@ step "COMPOSE_UP_FAILED" "start task container"
|
|
|
2038
2182
|
# without a temporary env/YAML file and without exporting any value to host
|
|
2039
2183
|
# commands.
|
|
2040
2184
|
preview_env_raw=$(jq -r '.[0].preview_env // "{}"' <<<"$task_json")
|
|
2041
|
-
filtered_project_env
|
|
2042
|
-
|
|
2043
|
-
|
|
2185
|
+
filtered_project_env='{}'
|
|
2186
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "baked" ]; then
|
|
2187
|
+
filtered_project_env=$(printf '%s' "$project_env_json" | jq -c \
|
|
2188
|
+
--argjson declared "$union_env_keys_json" \
|
|
2189
|
+
--argjson preview "$preview_env_raw" '
|
|
2044
2190
|
.
|
|
2045
2191
|
| with_entries(.key as $key | select($declared | index($key)))
|
|
2046
2192
|
| with_entries(select(.key | test("^GIT_CONFIG($|_)") | not))
|
|
@@ -2049,6 +2195,7 @@ filtered_project_env=$(printf '%s' "$project_env_json" | jq -c \
|
|
|
2049
2195
|
"i"
|
|
2050
2196
|
) | not))
|
|
2051
2197
|
| with_entries(select(.key | test("^UAI_") | not))
|
|
2198
|
+
| with_entries(select(.key | test("^__UAI_ENV_") | not))
|
|
2052
2199
|
| del(
|
|
2053
2200
|
.GH_TOKEN,
|
|
2054
2201
|
.GITHUB_TOKEN,
|
|
@@ -2070,6 +2217,7 @@ filtered_project_env=$(printf '%s' "$project_env_json" | jq -c \
|
|
|
2070
2217
|
.XDG_DATA_HOME,
|
|
2071
2218
|
.XDG_CACHE_HOME,
|
|
2072
2219
|
.PATH,
|
|
2220
|
+
.SHELL,
|
|
2073
2221
|
.LD_PRELOAD,
|
|
2074
2222
|
.LD_AUDIT,
|
|
2075
2223
|
.LD_LIBRARY_PATH,
|
|
@@ -2080,6 +2228,11 @@ filtered_project_env=$(printf '%s' "$project_env_json" | jq -c \
|
|
|
2080
2228
|
.PS4,
|
|
2081
2229
|
.BASH_ENV,
|
|
2082
2230
|
.ENV,
|
|
2231
|
+
.NODE_EXTRA_CA_CERTS,
|
|
2232
|
+
.SSL_CERT_FILE,
|
|
2233
|
+
.REQUESTS_CA_BUNDLE,
|
|
2234
|
+
.__CURSOR_SANDBOX_ENV_RESTORE,
|
|
2235
|
+
.GROK_LOGIN_ENV,
|
|
2083
2236
|
.UAI_GIT_TRANSPORT,
|
|
2084
2237
|
.CLAUDE_CODE_OAUTH_TOKEN,
|
|
2085
2238
|
.ANTHROPIC_API_KEY,
|
|
@@ -2089,7 +2242,8 @@ filtered_project_env=$(printf '%s' "$project_env_json" | jq -c \
|
|
|
2089
2242
|
.UAI_EDITOR_THEME
|
|
2090
2243
|
)
|
|
2091
2244
|
| delpaths($preview | keys | map([.]))
|
|
2092
|
-
|
|
2245
|
+
')
|
|
2246
|
+
fi
|
|
2093
2247
|
compose_env_override=""
|
|
2094
2248
|
if [ "$TASK_RUNTIME_MODE" = "docker" ]; then
|
|
2095
2249
|
compose_env_override=$(printf '%s\n' "$filtered_project_env" | jq -c \
|
|
@@ -2187,6 +2341,9 @@ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
|
2187
2341
|
printf 'npm_config_store_dir=%s\n' "$TASK_STORE_DIR"
|
|
2188
2342
|
printf 'UAI_WORKSPACE=/workspace\n'
|
|
2189
2343
|
printf 'UAI_RUNTIME_PROJECTS=/run/uai/runtime-projects\n'
|
|
2344
|
+
printf 'UAI_ENV_MODE=%s\n' "$UAI_PROJECT_ENV_MODE"
|
|
2345
|
+
printf 'UAI_ENV_HOOK=0\n'
|
|
2346
|
+
printf 'SHELL=/usr/bin/zsh\n'
|
|
2190
2347
|
printf 'LD_PRELOAD=\n'
|
|
2191
2348
|
printf 'LD_AUDIT=\n'
|
|
2192
2349
|
printf 'LD_LIBRARY_PATH=\n'
|
|
@@ -2200,8 +2357,16 @@ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
|
2200
2357
|
printf 'ANTHROPIC_AUTH_TOKEN=%s\n' "${ANTHROPIC_AUTH_TOKEN:-}"
|
|
2201
2358
|
printf 'XAI_API_KEY=%s\n' "${XAI_API_KEY:-}"
|
|
2202
2359
|
printf 'PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers\n'
|
|
2203
|
-
|
|
2204
|
-
|
|
2360
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
2361
|
+
while IFS= read -r ekey; do
|
|
2362
|
+
[ -n "$ekey" ] || continue
|
|
2363
|
+
is_reserved_task_env_name "$ekey" && continue
|
|
2364
|
+
printf '%s=\n' "$ekey"
|
|
2365
|
+
done < <(jq -r '.[]' <<<"$union_env_keys_json")
|
|
2366
|
+
else
|
|
2367
|
+
printf '%s\n' "$filtered_project_env" \
|
|
2368
|
+
| jq -r 'to_entries[] | "\(.key)=\(.value)"'
|
|
2369
|
+
fi
|
|
2205
2370
|
while IFS= read -r pe_entry; do
|
|
2206
2371
|
[ -n "$pe_entry" ] || continue
|
|
2207
2372
|
pe_key=$(jq -r '.key' <<<"$pe_entry")
|
|
@@ -2211,7 +2376,11 @@ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
|
2211
2376
|
&& is_reserved_task_env_name "$pe_key"; then
|
|
2212
2377
|
continue
|
|
2213
2378
|
fi
|
|
2214
|
-
|
|
2379
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
2380
|
+
printf '%s=\n' "$pe_key"
|
|
2381
|
+
else
|
|
2382
|
+
printf '%s=%s\n' "$pe_key" "$pe_val"
|
|
2383
|
+
fi
|
|
2215
2384
|
done < <(jq -c 'to_entries[]?' <<<"$preview_env_raw")
|
|
2216
2385
|
} > "$task_env_file"
|
|
2217
2386
|
# The mount set matches the rendered Compose file's app service one-for-one
|
|
@@ -2239,6 +2408,7 @@ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
|
2239
2408
|
apple_run_args+=(
|
|
2240
2409
|
-v "$UAI_INIT_SOURCE:/usr/local/bin/uai-init:ro"
|
|
2241
2410
|
-v "$runtime_projects_file:/run/uai/runtime-projects:ro"
|
|
2411
|
+
-v "$managed_env_keys_file:/run/uai/managed-env-keys:ro"
|
|
2242
2412
|
# ADR-108: the shared volume attaches READ-ONLY at a staging path and an
|
|
2243
2413
|
# in-guest overlay presents it writable at /opt/asdf-data (upper = the
|
|
2244
2414
|
# task's own layer). The shared volume is never written on the task path,
|
|
@@ -2254,6 +2424,13 @@ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
|
2254
2424
|
# ownership-proven above; single-attach by runtime construction.
|
|
2255
2425
|
-v "$TASK_STORE_VOLUME:$TASK_STORE_MOUNT"
|
|
2256
2426
|
)
|
|
2427
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
2428
|
+
apple_run_args+=(
|
|
2429
|
+
-v "$ENV_HOOK_SOURCE:/usr/local/share/uai/env-hook.sh:ro"
|
|
2430
|
+
-v "$UAI_ENV_EXEC_SOURCE:/usr/local/bin/uai-env-exec:ro"
|
|
2431
|
+
-v "$TASK_FORWARDER_SOURCE:/usr/local/share/uai/uai-task-forwarder.mjs:ro"
|
|
2432
|
+
)
|
|
2433
|
+
fi
|
|
2257
2434
|
if [ "$shared_files_mode" != "off" ]; then
|
|
2258
2435
|
shared_suffix=""
|
|
2259
2436
|
[ "$shared_files_mode" = "ro" ] && shared_suffix=":ro"
|
|
@@ -2269,13 +2446,15 @@ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
|
2269
2446
|
fi
|
|
2270
2447
|
fi
|
|
2271
2448
|
apple_up_rc=0
|
|
2272
|
-
# CAP_SYS_ADMIN: required for the ADR-108 in-guest overlay mount.
|
|
2273
|
-
#
|
|
2274
|
-
#
|
|
2449
|
+
# CAP_SYS_ADMIN: required for the ADR-108 in-guest overlay mount.
|
|
2450
|
+
# CAP_NET_ADMIN: the ADR-153 secrets proxy's NAT chain, set by the runner's
|
|
2451
|
+
# root execs (node has no sudo). Both are confined by the VM boundary: the
|
|
2452
|
+
# guest kernel is the task's own, so they grant nothing on the host.
|
|
2275
2453
|
"$CONTAINER_CLI" run -d --init \
|
|
2276
2454
|
--name "$app_container" \
|
|
2277
2455
|
-l "com.uai.task=$task_id" \
|
|
2278
2456
|
--cap-add CAP_SYS_ADMIN \
|
|
2457
|
+
--cap-add CAP_NET_ADMIN \
|
|
2279
2458
|
--workdir /workspace \
|
|
2280
2459
|
--memory "$APPLE_TASK_MEMORY" \
|
|
2281
2460
|
--cpus "$APPLE_TASK_CPUS" \
|
|
@@ -2642,6 +2821,7 @@ managed_exec_env=(
|
|
|
2642
2821
|
-e "LD_LIBRARY_PATH="
|
|
2643
2822
|
-e "DYLD_INSERT_LIBRARIES="
|
|
2644
2823
|
-e "DYLD_LIBRARY_PATH="
|
|
2824
|
+
-e "UAI_ENV_HOOK=0"
|
|
2645
2825
|
-e "BASH_ENV="
|
|
2646
2826
|
-e "ENV="
|
|
2647
2827
|
-e "GIT_CONFIG="
|
|
@@ -2709,9 +2889,16 @@ task_runtime_exec_env=(
|
|
|
2709
2889
|
-e "NODE_OPTIONS="
|
|
2710
2890
|
-e "NODE_PATH="
|
|
2711
2891
|
-e "PS4="
|
|
2892
|
+
-e "UAI_ENV_MODE=$UAI_PROJECT_ENV_MODE"
|
|
2893
|
+
-e "UAI_ENV_HOOK=0"
|
|
2894
|
+
-e "SHELL=/usr/bin/zsh"
|
|
2712
2895
|
-e "BASH_ENV="
|
|
2713
2896
|
-e "ENV="
|
|
2714
2897
|
)
|
|
2898
|
+
editor_theme=$(jq -r '.UAI_EDITOR_THEME // empty' <<<"$preview_env_raw")
|
|
2899
|
+
if [ -n "$editor_theme" ]; then
|
|
2900
|
+
task_runtime_exec_env+=( -e "UAI_EDITOR_THEME=$editor_theme" )
|
|
2901
|
+
fi
|
|
2715
2902
|
# ADR-118 (apple only): every runtime-bearing exec — uai-init's installs
|
|
2716
2903
|
# above all — must aim pnpm at the task's block volume, exactly like the
|
|
2717
2904
|
# container env-file. Docker execs keep the historical environment.
|
|
@@ -2722,6 +2909,266 @@ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
|
2722
2909
|
)
|
|
2723
2910
|
fi
|
|
2724
2911
|
|
|
2912
|
+
# ADR-153 secrets proxy, first step: the CA bundle goes into the task's trust
|
|
2913
|
+
# store before anything else runs in it (the forwarder, uai-init, agents). The
|
|
2914
|
+
# runner wrote the bundle and its fixed install script to
|
|
2915
|
+
# UAI_SECRETS_PROXY_DIR (public material only); unset, the proxy is off and
|
|
2916
|
+
# nothing is installed. A failure leaves the task up without the proxy's
|
|
2917
|
+
# trust, and the runner withholds whatever would have needed it.
|
|
2918
|
+
secrets_proxy_trust=skipped
|
|
2919
|
+
secrets_proxy_pins=skipped
|
|
2920
|
+
secrets_proxy_dir=${UAI_SECRETS_PROXY_DIR:-}
|
|
2921
|
+
# Each outcome is also written back to the runner's directory the moment it
|
|
2922
|
+
# is known: uai-init's shells read the env before task-up returns, and the
|
|
2923
|
+
# runner gives the trust env only once the CA is really in place.
|
|
2924
|
+
secrets_proxy_report() {
|
|
2925
|
+
[ -z "$secrets_proxy_dir" ] || printf '%s\n' "$2" > "$secrets_proxy_dir/$1" || true
|
|
2926
|
+
}
|
|
2927
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ] && [ -n "$secrets_proxy_dir" ]; then
|
|
2928
|
+
if [ -f "$secrets_proxy_dir/ca-install.sh" ] && [ -f "$secrets_proxy_dir/ca-bundle.pem" ] \
|
|
2929
|
+
&& crt exec -i -u root "${managed_exec_env[@]}" "$app_container" \
|
|
2930
|
+
/bin/sh -c "$(cat "$secrets_proxy_dir/ca-install.sh")" \
|
|
2931
|
+
< "$secrets_proxy_dir/ca-bundle.pem" >/dev/null; then
|
|
2932
|
+
secrets_proxy_trust=installed
|
|
2933
|
+
else
|
|
2934
|
+
secrets_proxy_trust=failed
|
|
2935
|
+
log "warning: could not install the secrets proxy CA in ${app_container}; its proxied keys stay withheld"
|
|
2936
|
+
fi
|
|
2937
|
+
secrets_proxy_report trust "$secrets_proxy_trust"
|
|
2938
|
+
fi
|
|
2939
|
+
|
|
2940
|
+
# Loader tasks have two pieces of root-owned bootstrap inside the task:
|
|
2941
|
+
# immutable shell entry-point stubs, and a generation-authenticated loopback
|
|
2942
|
+
# forwarder. Maintenance commands above and below keep UAI_ENV_HOOK=0, so the
|
|
2943
|
+
# stubs cannot make runner-owned execs consume project configuration.
|
|
2944
|
+
if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
|
|
2945
|
+
task_gateway_generation=${UAI_TASK_GATEWAY_GENERATION:-}
|
|
2946
|
+
task_gateway_host=${UAI_TASK_GATEWAY_HOST:-}
|
|
2947
|
+
task_gateway_port=${UAI_MCP_GATEWAY_PORT:-5877}
|
|
2948
|
+
if [[ ! "$task_gateway_generation" =~ ^[A-Za-z0-9_-]{16,64}$ ]] \
|
|
2949
|
+
|| [[ ! "$task_gateway_host" =~ ^[A-Za-z0-9][A-Za-z0-9._:-]{0,254}$ ]] \
|
|
2950
|
+
|| [[ ! "$task_gateway_port" =~ ^[0-9]{1,5}$ ]] \
|
|
2951
|
+
|| [ "$task_gateway_port" -lt 1 ] \
|
|
2952
|
+
|| [ "$task_gateway_port" -gt 65535 ]; then
|
|
2953
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
2954
|
+
"Uai could not establish this task's authenticated env channel. Retry the task after updating the host." \
|
|
2955
|
+
"validate task env gateway identity"
|
|
2956
|
+
fi
|
|
2957
|
+
|
|
2958
|
+
# A heredoc nested inside `$(cat ...)` containing `case` arms is parsed as
|
|
2959
|
+
# shell syntax by Bash 3.2 (the system Bash on macOS) and makes every task-up
|
|
2960
|
+
# fail before it runs. `read -d ''` assigns the same literal without a
|
|
2961
|
+
# command-substitution parser context.
|
|
2962
|
+
env_stub_installer=''
|
|
2963
|
+
IFS= read -r -d '' env_stub_installer <<'UAI_ENV_STUB_INSTALLER' || true
|
|
2964
|
+
set -eu
|
|
2965
|
+
ensure_block() {
|
|
2966
|
+
file=$1
|
|
2967
|
+
line=$2
|
|
2968
|
+
case "$file" in
|
|
2969
|
+
/etc/zsh/zshenv | /etc/zsh/zshrc | /etc/bash.bashrc | /etc/profile.d/uai-env.sh) ;;
|
|
2970
|
+
*) exit 1 ;;
|
|
2971
|
+
esac
|
|
2972
|
+
[ ! -L "$file" ] || exit 1
|
|
2973
|
+
[ ! -e "$file" ] || [ -f "$file" ] || exit 1
|
|
2974
|
+
directory=${file%/*}
|
|
2975
|
+
[ -d "$directory" ] && [ ! -L "$directory" ] || exit 1
|
|
2976
|
+
temporary=$(/usr/bin/mktemp "$directory/.uai-env-stub.XXXXXX")
|
|
2977
|
+
trap '/bin/rm -f -- "$temporary"' EXIT
|
|
2978
|
+
if [ -f "$file" ]; then
|
|
2979
|
+
/usr/bin/awk '
|
|
2980
|
+
$0 == "# >>> uai env loader >>>" { skipping = 1; next }
|
|
2981
|
+
$0 == "# <<< uai env loader <<<" { skipping = 0; next }
|
|
2982
|
+
!skipping { print }
|
|
2983
|
+
' "$file" > "$temporary"
|
|
2984
|
+
fi
|
|
2985
|
+
{
|
|
2986
|
+
printf '%s\n' '# >>> uai env loader >>>'
|
|
2987
|
+
printf '%s\n' "$line"
|
|
2988
|
+
printf '%s\n' '# <<< uai env loader <<<'
|
|
2989
|
+
} >> "$temporary"
|
|
2990
|
+
/bin/chown root:root "$temporary"
|
|
2991
|
+
/bin/chmod 0644 "$temporary"
|
|
2992
|
+
/bin/mv -f -- "$temporary" "$file"
|
|
2993
|
+
trap - EXIT
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
ensure_block /etc/zsh/zshenv '[ "${UAI_ENV_HOOK-0}" != 1 ] || { . /usr/local/share/uai/env-hook.sh; __uai_env_sync; }'
|
|
2997
|
+
ensure_block /etc/zsh/zshrc '[ "${UAI_ENV_HOOK-0}" != 1 ] || { type __uai_env_install_zsh_preexec >/dev/null 2>&1 || . /usr/local/share/uai/env-hook.sh; __uai_env_install_zsh_preexec; }'
|
|
2998
|
+
ensure_block /etc/bash.bashrc '[ "${UAI_ENV_HOOK-0}" != 1 ] || { . /usr/local/share/uai/env-hook.sh; __uai_env_sync; __uai_env_install_bash_preexec; }'
|
|
2999
|
+
ensure_block /etc/profile.d/uai-env.sh '[ "${UAI_ENV_HOOK-0}" != 1 ] || { . /usr/local/share/uai/env-hook.sh; if [ -z "${BASH_EXECUTION_STRING-}" ] || [ -z "${BASH_ENV-}" ]; then __uai_env_sync; fi; }'
|
|
3000
|
+
UAI_ENV_STUB_INSTALLER
|
|
3001
|
+
if ! crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
3002
|
+
/bin/bash -c "$env_stub_installer" >/dev/null; then
|
|
3003
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
3004
|
+
"Uai could not install the root-owned task env shell hooks. Retry the task; project env remains withheld until this succeeds." \
|
|
3005
|
+
"install task env shell hooks"
|
|
3006
|
+
fi
|
|
3007
|
+
|
|
3008
|
+
task_forwarder_node=/usr/local/bin/node
|
|
3009
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
3010
|
+
task_forwarder_node="/run/uai/asdf-lower/installs/nodejs/$STANDARD_DEFAULT_NODE_VERSION/bin/node"
|
|
3011
|
+
fi
|
|
3012
|
+
|
|
3013
|
+
task_forwarder_launcher=''
|
|
3014
|
+
IFS= read -r -d '' task_forwarder_launcher <<'UAI_TASK_FORWARDER_LAUNCHER' || true
|
|
3015
|
+
set -eu
|
|
3016
|
+
task_id=$1
|
|
3017
|
+
generation=$2
|
|
3018
|
+
gateway_host=$3
|
|
3019
|
+
gateway_port=$4
|
|
3020
|
+
node_path=$5
|
|
3021
|
+
case "$gateway_port" in ''|*[!0-9]*) exit 1 ;; esac
|
|
3022
|
+
[ "$gateway_port" -ge 1 ] && [ "$gateway_port" -le 65535 ] || exit 1
|
|
3023
|
+
case "$node_path" in
|
|
3024
|
+
/usr/local/bin/node) ;;
|
|
3025
|
+
/run/uai/asdf-lower/installs/nodejs/22.11.0/bin/node)
|
|
3026
|
+
[ ! -L "$node_path" ] && [ -f "$node_path" ] && [ -x "$node_path" ] \
|
|
3027
|
+
|| exit 1
|
|
3028
|
+
;;
|
|
3029
|
+
*) exit 1 ;;
|
|
3030
|
+
esac
|
|
3031
|
+
state_dir=/run/uai/task-forwarder
|
|
3032
|
+
pid_file=$state_dir/pid
|
|
3033
|
+
ready_file=$state_dir/ready
|
|
3034
|
+
error_file=$state_dir/error.log
|
|
3035
|
+
umask 077
|
|
3036
|
+
[ ! -L "$state_dir" ] || exit 1
|
|
3037
|
+
[ ! -e "$state_dir" ] || [ -d "$state_dir" ] || exit 1
|
|
3038
|
+
/usr/bin/install -d -o root -g root -m 0700 "$state_dir"
|
|
3039
|
+
|
|
3040
|
+
if [ -f "$pid_file" ] && [ ! -L "$pid_file" ]; then
|
|
3041
|
+
old_pid=$(/bin/cat "$pid_file" 2>/dev/null || true)
|
|
3042
|
+
case "$old_pid" in ''|*[!0-9]*) old_pid= ;; esac
|
|
3043
|
+
if [ -n "$old_pid" ] && /bin/kill -0 "$old_pid" 2>/dev/null; then
|
|
3044
|
+
old_uid=$(/usr/bin/awk '/^Uid:/ { print $2; exit }' "/proc/$old_pid/status" 2>/dev/null || true)
|
|
3045
|
+
old_args=$(/usr/bin/tr '\000' '\n' < "/proc/$old_pid/cmdline" 2>/dev/null || true)
|
|
3046
|
+
# /run survives a Docker stop/start. Its recorded pid may have been
|
|
3047
|
+
# reused by uai-init or another innocent process; treat that record as
|
|
3048
|
+
# stale and never signal a process unless both uid and argv prove it is
|
|
3049
|
+
# this task's prior forwarder.
|
|
3050
|
+
if [ "$old_uid" = 65532 ] \
|
|
3051
|
+
&& printf '%s\n' "$old_args" | /bin/grep -Fxq '/usr/local/share/uai/uai-task-forwarder.mjs' \
|
|
3052
|
+
&& printf '%s\n' "$old_args" | /bin/grep -Fxq "$task_id"; then
|
|
3053
|
+
/bin/kill -TERM "$old_pid"
|
|
3054
|
+
attempts=0
|
|
3055
|
+
while /bin/kill -0 "$old_pid" 2>/dev/null && [ "$attempts" -lt 50 ]; do
|
|
3056
|
+
/bin/sleep 0.1
|
|
3057
|
+
attempts=$((attempts + 1))
|
|
3058
|
+
done
|
|
3059
|
+
if /bin/kill -0 "$old_pid" 2>/dev/null; then
|
|
3060
|
+
/bin/kill -KILL "$old_pid"
|
|
3061
|
+
fi
|
|
3062
|
+
fi
|
|
3063
|
+
fi
|
|
3064
|
+
fi
|
|
3065
|
+
/bin/rm -f -- "$pid_file" "$ready_file" "$error_file"
|
|
3066
|
+
|
|
3067
|
+
# Non-interactive Bash otherwise gives an asynchronous command /dev/null as
|
|
3068
|
+
# stdin. Preserve the docker/container exec pipe explicitly, then close the
|
|
3069
|
+
# extra descriptor in both parent and child after fd 0 has been duplicated.
|
|
3070
|
+
exec 3<&0
|
|
3071
|
+
/usr/bin/env -i PATH=/usr/bin:/bin \
|
|
3072
|
+
/usr/bin/setsid \
|
|
3073
|
+
/usr/bin/setpriv \
|
|
3074
|
+
--reuid=65532 \
|
|
3075
|
+
--regid=65532 \
|
|
3076
|
+
--clear-groups \
|
|
3077
|
+
--bounding-set=-all \
|
|
3078
|
+
--inh-caps=-all \
|
|
3079
|
+
--ambient-caps=-all \
|
|
3080
|
+
--no-new-privs \
|
|
3081
|
+
"$node_path" \
|
|
3082
|
+
/usr/local/share/uai/uai-task-forwarder.mjs \
|
|
3083
|
+
--task-id "$task_id" \
|
|
3084
|
+
--generation "$generation" \
|
|
3085
|
+
--gateway-host "$gateway_host" \
|
|
3086
|
+
--gateway-port "$gateway_port" \
|
|
3087
|
+
--listen-port 5877 \
|
|
3088
|
+
<&3 3<&- >"$ready_file" 2>"$error_file" &
|
|
3089
|
+
forwarder_pid=$!
|
|
3090
|
+
exec 3<&-
|
|
3091
|
+
printf '%s\n' "$forwarder_pid" > "$pid_file"
|
|
3092
|
+
|
|
3093
|
+
attempts=0
|
|
3094
|
+
while [ "$attempts" -lt 50 ]; do
|
|
3095
|
+
/bin/kill -0 "$forwarder_pid" 2>/dev/null || exit 1
|
|
3096
|
+
if [ -f "$ready_file" ] \
|
|
3097
|
+
&& [ "$(/bin/cat "$ready_file" 2>/dev/null || true)" = READY ]; then
|
|
3098
|
+
exit 0
|
|
3099
|
+
fi
|
|
3100
|
+
/bin/sleep 0.1
|
|
3101
|
+
attempts=$((attempts + 1))
|
|
3102
|
+
done
|
|
3103
|
+
exit 1
|
|
3104
|
+
UAI_TASK_FORWARDER_LAUNCHER
|
|
3105
|
+
|
|
3106
|
+
base64_decode_args=()
|
|
3107
|
+
if printf '' | base64 --decode >/dev/null 2>&1; then
|
|
3108
|
+
base64_decode_args=(--decode)
|
|
3109
|
+
elif printf '' | base64 -D >/dev/null 2>&1; then
|
|
3110
|
+
base64_decode_args=(-D)
|
|
3111
|
+
else
|
|
3112
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
3113
|
+
"Uai could not decode the task env gateway credential on this host." \
|
|
3114
|
+
"start task env forwarder"
|
|
3115
|
+
fi
|
|
3116
|
+
if ! printf '%s' "$task_gateway_secret_b64" \
|
|
3117
|
+
| base64 "${base64_decode_args[@]}" \
|
|
3118
|
+
| crt exec -i -u root "${managed_exec_env[@]}" "$app_container" \
|
|
3119
|
+
/bin/bash -c "$task_forwarder_launcher" uai-task-forwarder \
|
|
3120
|
+
"$task_id" "$task_gateway_generation" "$task_gateway_host" \
|
|
3121
|
+
"$task_gateway_port" "$task_forwarder_node" \
|
|
3122
|
+
>/dev/null; then
|
|
3123
|
+
unset task_gateway_secret_b64
|
|
3124
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
3125
|
+
"Uai could not start the authenticated task env forwarder. Retry the task; project env remains withheld until this succeeds." \
|
|
3126
|
+
"start task env forwarder"
|
|
3127
|
+
fi
|
|
3128
|
+
unset task_gateway_secret_b64
|
|
3129
|
+
fi
|
|
3130
|
+
|
|
3131
|
+
# ADR-153 secrets proxy, second step, now that the forwarder listens: the NAT
|
|
3132
|
+
# chain that sends the pin address's :443 to the forwarder, and the names
|
|
3133
|
+
# pinned to it. In the runner's order (routing-order): the chain before the
|
|
3134
|
+
# names when pinning, the names first when there is nothing to pin, so a
|
|
3135
|
+
# failure part-way never leaves a name pointing at a pin with nothing behind
|
|
3136
|
+
# it. The NAT step needs NET_ADMIN. A Docker container does not have it: the
|
|
3137
|
+
# step is the one privileged exec Uai makes, and only that process gets it.
|
|
3138
|
+
# An Apple task's VM was started with it, so there a root exec has it (and
|
|
3139
|
+
# the CLI has no --privileged).
|
|
3140
|
+
secrets_proxy_chain() {
|
|
3141
|
+
if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
|
|
3142
|
+
set -- crt exec -u root
|
|
3143
|
+
else
|
|
3144
|
+
set -- docker exec --privileged -u root
|
|
3145
|
+
fi
|
|
3146
|
+
"$@" "${managed_exec_env[@]}" "$app_container" \
|
|
3147
|
+
/bin/sh -c "$(cat "$secrets_proxy_dir/chain.sh")" >/dev/null \
|
|
3148
|
+
|| { log "warning: could not redirect the secrets proxy's names in ${app_container}"; return 1; }
|
|
3149
|
+
}
|
|
3150
|
+
secrets_proxy_names() {
|
|
3151
|
+
crt exec -u root "${managed_exec_env[@]}" "$app_container" \
|
|
3152
|
+
/bin/sh -c "$(cat "$secrets_proxy_dir/pins.sh")" >/dev/null \
|
|
3153
|
+
|| { log "warning: could not pin the secrets proxy's names in ${app_container}"; return 1; }
|
|
3154
|
+
}
|
|
3155
|
+
if [ "$secrets_proxy_trust" = "installed" ]; then
|
|
3156
|
+
secrets_proxy_order=$(cat "$secrets_proxy_dir/routing-order" 2>/dev/null || true)
|
|
3157
|
+
# A missing script would run as an empty, successful program.
|
|
3158
|
+
if [ ! -f "$secrets_proxy_dir/pins.sh" ] || [ ! -f "$secrets_proxy_dir/chain.sh" ]; then
|
|
3159
|
+
secrets_proxy_pins=failed
|
|
3160
|
+
log "warning: the secrets proxy's pin scripts are missing for ${app_container}"
|
|
3161
|
+
elif [ "$secrets_proxy_order" = "pin" ]; then
|
|
3162
|
+
if secrets_proxy_chain && secrets_proxy_names; then secrets_proxy_pins=installed; else secrets_proxy_pins=failed; fi
|
|
3163
|
+
elif [ "$secrets_proxy_order" = "unpin" ]; then
|
|
3164
|
+
if secrets_proxy_names && secrets_proxy_chain; then secrets_proxy_pins=installed; else secrets_proxy_pins=failed; fi
|
|
3165
|
+
else
|
|
3166
|
+
secrets_proxy_pins=failed
|
|
3167
|
+
log "warning: the secrets proxy's routing order is missing for ${app_container}"
|
|
3168
|
+
fi
|
|
3169
|
+
secrets_proxy_report pins "$secrets_proxy_pins"
|
|
3170
|
+
fi
|
|
3171
|
+
|
|
2725
3172
|
# OpenCode and code-server share ~/.local/share. OpenCode credentials are
|
|
2726
3173
|
# copied as root below, so creating only its leaf with `mkdir -p` can leave the
|
|
2727
3174
|
# shared parent root-owned (live 2026-08-04). Then node cannot create
|
|
@@ -3088,8 +3535,10 @@ emit_ok "$(jq -nc \
|
|
|
3088
3535
|
--arg wp "$task_dir" \
|
|
3089
3536
|
--arg csport "${code_server_port:-}" \
|
|
3090
3537
|
--arg iw "${init_warning}" \
|
|
3538
|
+
--arg spt "$secrets_proxy_trust" \
|
|
3539
|
+
--arg spp "$secrets_proxy_pins" \
|
|
3091
3540
|
--argjson previewPorts "$preview_ports_runtime_json" \
|
|
3092
3541
|
'{composeProject:$cp,worktreePath:$wp}
|
|
3093
3542
|
+ (if $csport == "" then {} else {codeServerPort:($csport|tonumber)} end)
|
|
3094
3543
|
+ (if $iw == "" then {} else {initWarning:$iw} end)
|
|
3095
|
-
+ {previewPorts:$previewPorts}')"
|
|
3544
|
+
+ {previewPorts:$previewPorts, secretsProxyTrust:$spt, secretsProxyPins:$spp}')"
|