@runuai/host 0.9.148 → 0.9.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/db/migrations/0024_task_env_mode.sql +9 -0
  2. package/db/migrations/0025_project_env_authority.sql +147 -0
  3. package/db/migrations/0026_project_env_replica.sql +150 -0
  4. package/db/migrations/meta/_journal.json +21 -0
  5. package/db/schema.ts +312 -2
  6. package/images/standard/Dockerfile +44 -6
  7. package/images/standard/container/uai-init +85 -2
  8. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  9. package/lib/agent-cli.ts +18 -1
  10. package/lib/agent.ts +99 -34
  11. package/lib/agents/codex.ts +9 -1
  12. package/lib/agents/cursor.ts +15 -1
  13. package/lib/agents/grok.ts +8 -0
  14. package/lib/agents/kimi.ts +1 -0
  15. package/lib/agents/machine-durable.ts +2 -0
  16. package/lib/agents/opencode.ts +1 -0
  17. package/lib/agents/proc.ts +6 -1
  18. package/lib/agents/transport.ts +9 -5
  19. package/lib/browser-testing.ts +6 -0
  20. package/lib/codex-auth.ts +2 -0
  21. package/lib/credential-store.ts +141 -6
  22. package/lib/engine-accounts.ts +1 -0
  23. package/lib/env-file.ts +7 -0
  24. package/lib/env-schema.ts +841 -51
  25. package/lib/git-diff.ts +1 -0
  26. package/lib/git-identity.ts +2 -0
  27. package/lib/github-tokens.ts +1 -0
  28. package/lib/host-env.ts +98 -18
  29. package/lib/lease.ts +11 -3
  30. package/lib/machine-gateway-tunnel.ts +195 -17
  31. package/lib/machine-project-env-loader.ts +152 -0
  32. package/lib/managed-runtime.ts +270 -51
  33. package/lib/mcp-gateway.ts +777 -26
  34. package/lib/orchestrator.ts +408 -81
  35. package/lib/project-env-authority.ts +1319 -0
  36. package/lib/project-env-feature.ts +74 -0
  37. package/lib/project-env-loader.ts +237 -0
  38. package/lib/project-env-replica.ts +2094 -0
  39. package/lib/project-env-schema-capture.ts +342 -0
  40. package/lib/project-env-staging.ts +148 -0
  41. package/lib/project-env-task-policy.ts +176 -0
  42. package/lib/runtime-archive.ts +79 -16
  43. package/lib/runtime-authority.ts +44 -0
  44. package/lib/runtime-state.ts +175 -10
  45. package/lib/secrets-proxy/address.ts +72 -0
  46. package/lib/secrets-proxy/audit.ts +30 -0
  47. package/lib/secrets-proxy/ca-store.ts +200 -0
  48. package/lib/secrets-proxy/ca.ts +470 -0
  49. package/lib/secrets-proxy/container-steps.ts +138 -0
  50. package/lib/secrets-proxy/der.ts +273 -0
  51. package/lib/secrets-proxy/engine.ts +1633 -0
  52. package/lib/secrets-proxy/host.ts +93 -0
  53. package/lib/secrets-proxy/hostname.ts +60 -0
  54. package/lib/secrets-proxy/placeholders.ts +196 -0
  55. package/lib/secrets-proxy/policy.ts +141 -0
  56. package/lib/secrets-proxy/readiness.ts +148 -0
  57. package/lib/secrets-proxy/runtime.ts +582 -0
  58. package/lib/secrets-proxy/scrub.ts +559 -0
  59. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  60. package/lib/secrets-proxy/task-scripts.ts +262 -0
  61. package/lib/secrets-proxy/task-trust.ts +574 -0
  62. package/lib/secrets-proxy/trust-watch.ts +80 -0
  63. package/lib/shared-files-s3.ts +3 -0
  64. package/lib/sidecar-guard.ts +58 -0
  65. package/lib/sidecar-resume.ts +51 -3
  66. package/lib/standard-image.ts +50 -12
  67. package/lib/task-env-forwarder.ts +228 -0
  68. package/lib/task-env-source.ts +193 -0
  69. package/lib/task-environment/apple-container.ts +45 -11
  70. package/lib/task-environment/docker.ts +51 -12
  71. package/lib/task-environment/index.ts +6 -2
  72. package/lib/task-environment/machine-task-up.ts +258 -25
  73. package/lib/task-environment/machine.ts +80 -22
  74. package/lib/task-environment/registry.ts +3 -2
  75. package/lib/task-environment/self-identity.ts +20 -0
  76. package/lib/task-environment/self.ts +92 -16
  77. package/lib/task-environment/types.ts +63 -0
  78. package/lib/task-gateway-port.ts +22 -0
  79. package/lib/task-gateway-protocol.ts +190 -0
  80. package/lib/task-gateway-recovery.ts +44 -0
  81. package/lib/task-gateway-state.ts +497 -0
  82. package/lib/transcript.ts +1 -0
  83. package/package.json +13 -1
  84. package/scripts/agent/env-hook.sh +626 -0
  85. package/scripts/agent/task-up.sh +476 -27
  86. package/scripts/agent/uai-env-exec.sh +26 -0
  87. package/src/cli.ts +26 -3
  88. package/src/index.ts +273 -19
  89. package/src/main.ts +939 -48
  90. package/src/project-env-latest.ts +130 -0
  91. package/src/project-env-policy.ts +281 -0
  92. package/src/project-env-wording.ts +47 -0
  93. package/src/protocol.ts +2219 -15
@@ -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
- # Project-defined environment values arrive as one JSON object on stdin. They
77
- # are deliberately NOT part of this host process environment: names such as
78
- # BASH_ENV, PATH, GIT_CONFIG_*, and UAI_* must not influence task setup or the
79
- # credentialed host-side fetch. Values stay in shell memory until Compose reads
80
- # an override document from stdin; they are never written to disk.
81
- project_env_json=$(cat)
82
- [ -n "$project_env_json" ] || project_env_json='{}'
83
- if ! printf '%s' "$project_env_json" | jq -e 'type == "object"' >/dev/null 2>&1; then
84
- emit_err "BAD_ARGS" "task environment payload must be a JSON object" "read task environment"
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
- project_env_json=$(printf '%s' "$project_env_json" | jq -c '.')
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 literal env (ADR-025): a JSON object {VAR: value} on the
1741
- # task row. Non-secret, so written LITERALLY (unlike the ${KEY:-}
1742
- # pass-throughs above). Emitted last so it wins over any same-named declared
1743
- # key. Two producers, both cloud-side and both opaque here:
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
- printf ' %s: "%s"\n' "$pe_key" "$pe_val"
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=$(printf '%s' "$project_env_json" | jq -c \
2042
- --argjson declared "$union_env_keys_json" \
2043
- --argjson preview "$preview_env_raw" '
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
- printf '%s\n' "$filtered_project_env" \
2204
- | jq -r 'to_entries[] | "\(.key)=\(.value)"'
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
- printf '%s=%s\n' "$pe_key" "$pe_val"
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. The
2273
- # capability is confined by the VM boundary — the guest kernel is the
2274
- # task's own, so this grants nothing on the host.
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}')"