@runuai/host 0.9.149 → 0.9.151

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 (107) hide show
  1. package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
  2. package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
  3. package/db/migrations/meta/_journal.json +14 -0
  4. package/db/schema.ts +188 -0
  5. package/lib/agent-cli.ts +11 -13
  6. package/lib/agent.ts +177 -7
  7. package/lib/agents/durable-proc.ts +56 -44
  8. package/lib/agents/registry.ts +19 -3
  9. package/lib/agents/session-state.ts +23 -10
  10. package/lib/agents/transport.ts +101 -69
  11. package/lib/apple-one-off.ts +277 -0
  12. package/lib/apple-runtime-recycle.ts +4 -3
  13. package/lib/attachments.ts +19 -17
  14. package/lib/capture-flow.ts +319 -0
  15. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  16. package/lib/cloud-credential-host.ts +512 -0
  17. package/lib/cloud-credential-leases.ts +2544 -0
  18. package/lib/cloud-credential-runtime.ts +1035 -0
  19. package/lib/codex-auth.ts +21 -1
  20. package/lib/container-runtime.ts +76 -0
  21. package/lib/credential-cleanup-inventory.ts +405 -0
  22. package/lib/credential-consumers/engine-anchor.ts +1876 -0
  23. package/lib/credential-consumers/engines.ts +773 -0
  24. package/lib/credential-consumers/github.ts +594 -0
  25. package/lib/credential-consumers/mcp.ts +384 -0
  26. package/lib/credential-consumers/task-files.ts +127 -0
  27. package/lib/credential-store-pickup.ts +15 -2
  28. package/lib/credential-store.ts +361 -33
  29. package/lib/credentials-activation-authority.ts +546 -0
  30. package/lib/credentials-purge.ts +1006 -0
  31. package/lib/db.ts +11 -0
  32. package/lib/docker-exec.ts +3 -1
  33. package/lib/engine-accounts.ts +42 -32
  34. package/lib/engine-connect.ts +47 -2
  35. package/lib/engine-credential-env.ts +50 -0
  36. package/lib/engine-login.ts +83 -1
  37. package/lib/env-file.ts +62 -8
  38. package/lib/env.ts +5 -0
  39. package/lib/fs-aliases.ts +49 -0
  40. package/lib/git-clone-credentials.ts +334 -0
  41. package/lib/git-diff.ts +219 -177
  42. package/lib/git-identity.ts +59 -14
  43. package/lib/github-origin.ts +28 -0
  44. package/lib/github-tokens.ts +56 -21
  45. package/lib/host-env.ts +17 -4
  46. package/lib/launch-mode.ts +25 -0
  47. package/lib/legacy-runtime-retirement.ts +388 -0
  48. package/lib/machine-exec.ts +14 -3
  49. package/lib/machine-provider-aws.ts +50 -8
  50. package/lib/machine-provider-local.ts +4 -0
  51. package/lib/machine-provider.ts +4 -0
  52. package/lib/managed-operation-drain.ts +8 -0
  53. package/lib/managed-runtime.ts +371 -7
  54. package/lib/mcp-capture.ts +860 -0
  55. package/lib/mcp-connections.ts +8 -1
  56. package/lib/mcp-gateway.ts +168 -10
  57. package/lib/obs.ts +2 -6
  58. package/lib/orchestrator.ts +1847 -236
  59. package/lib/person-capture.ts +327 -0
  60. package/lib/project-env-legacy-tags.ts +130 -0
  61. package/lib/project-env-task-replica.ts +1752 -0
  62. package/lib/runner-boot.ts +10 -0
  63. package/lib/runtime-activation-tail.ts +50 -3
  64. package/lib/runtime-archive.ts +68 -1
  65. package/lib/runtime-reconnect.ts +86 -0
  66. package/lib/runtime-state.ts +429 -13
  67. package/lib/safe-http.ts +364 -0
  68. package/lib/secrets-proxy/task-trust.ts +34 -3
  69. package/lib/sidecar-resume.ts +18 -5
  70. package/lib/skills.ts +47 -8
  71. package/lib/standard-image.ts +157 -103
  72. package/lib/stopped-task-status-queue.ts +68 -10
  73. package/lib/task-credential-cleanup.ts +525 -0
  74. package/lib/task-diff-sandbox.ts +464 -0
  75. package/lib/task-diff.ts +66 -163
  76. package/lib/task-env-source.ts +68 -14
  77. package/lib/task-environment/anchor.ts +31 -0
  78. package/lib/task-environment/apple-container.ts +69 -28
  79. package/lib/task-environment/docker.ts +101 -20
  80. package/lib/task-environment/machine-environment.ts +61 -0
  81. package/lib/task-environment/machine-task-up.ts +459 -96
  82. package/lib/task-environment/machine.ts +101 -11
  83. package/lib/task-environment/self.ts +108 -50
  84. package/lib/task-environment/types.ts +89 -5
  85. package/lib/task-environment/workspace-files.ts +623 -25
  86. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  87. package/lib/task-gateway-recovery.ts +4 -1
  88. package/lib/task-gateway-state.ts +15 -0
  89. package/lib/task-machine-swap-safety.ts +729 -0
  90. package/lib/token-broker-adopt.ts +344 -0
  91. package/lib/token-broker-lambda.ts +56 -2
  92. package/lib/token-broker-local.ts +28 -4
  93. package/lib/token-broker-revision.ts +310 -0
  94. package/lib/token-broker-ssm.ts +368 -28
  95. package/lib/token-lanes/openai-oauth.ts +7 -3
  96. package/lib/token-lanes/xai-oauth.ts +7 -3
  97. package/lib/transcript.ts +9 -5
  98. package/package.json +6 -2
  99. package/scripts/agent/task-up.sh +242 -100
  100. package/src/cloud-credentials.ts +2399 -0
  101. package/src/event-outbox.ts +8 -6
  102. package/src/index.ts +1386 -875
  103. package/src/main.ts +1062 -140
  104. package/src/outbound-frame.ts +31 -0
  105. package/src/project-env-policy.ts +125 -10
  106. package/src/protocol.ts +699 -23
  107. package/src/ui/server.ts +48 -10
@@ -181,17 +181,33 @@ reject_workspace_runtime_shadow() {
181
181
  # post-materialization recheck below keeps refusing — by then the root must
182
182
  # be empty by construction, so an entry there is a bug, not a user file.
183
183
  quarantine_workspace_runtime_shadow() {
184
- local path="$task_workspace/.tool-versions" aside_dir aside
184
+ local path="$task_workspace/.tool-versions" aside_dir aside attempt note
185
185
  if [ -L "$path" ] || [ -e "$path" ]; then
186
186
  aside_dir="$task_workspace/.uai/refused"
187
187
  reject_unsafe_host_directory "$task_workspace/.uai" "the task control directory"
188
188
  reject_unsafe_host_directory "$aside_dir" "the refused-files directory"
189
189
  mkdir -p "$aside_dir"
190
+ # Never onto an entry the task left there: mv moves into a directory, or
191
+ # one a link leads to, outside the workspace (#cb3j0z). The task's
192
+ # container is gone, so nothing races these checks.
190
193
  aside="$aside_dir/tool-versions.$(date +%s)"
194
+ attempt=0
195
+ while [ -L "$aside" ] || [ -e "$aside" ]; do
196
+ attempt=$((attempt + 1))
197
+ aside="$aside_dir/tool-versions.$(date +%s).$attempt"
198
+ done
191
199
  if ! mv -f "$path" "$aside" 2>/dev/null || [ -L "$path" ] || [ -e "$path" ]; then
192
200
  reject_workspace_runtime_shadow
193
201
  fi
194
- cat > "$aside_dir/README.txt" <<'NOTE'
202
+ # Nor written through a link or special file in the note's place: that
203
+ # is removed first (rm never follows a link), and the note is written
204
+ # only to nothing or a regular file.
205
+ note="$aside_dir/README.txt"
206
+ if [ -L "$note" ] || { [ -e "$note" ] && [ ! -f "$note" ]; }; then
207
+ rm -f "$note" 2>/dev/null || true
208
+ fi
209
+ if [ ! -L "$note" ] && { [ ! -e "$note" ] || [ -f "$note" ]; }; then
210
+ cat > "$note" <<'NOTE'
195
211
  Uai moved a /workspace/.tool-versions aside before starting this task.
196
212
 
197
213
  A .tool-versions at the workspace root is outside the runtime proof and would
@@ -200,6 +216,7 @@ to start a container while one exists. The file is kept here, untouched, under
200
216
  its original content. Pin runtimes in a project's own .tool-versions instead
201
217
  (/workspace/<project>/.tool-versions), which the materializer proves.
202
218
  NOTE
219
+ fi
203
220
  log "moved a workspace-root .tool-versions aside to ${aside#"$task_workspace"/} (it would override the runtimes proved for this task; pin per project instead)"
204
221
  fi
205
222
  }
@@ -640,6 +657,25 @@ task_uai_dir_physical="$task_dir_physical/.uai"
640
657
  # 3. Bare mirrors + worktrees, one per selected project.
641
658
  # -----------------------------------------------------------------------------
642
659
 
660
+ # ADR-154 §12: a task on cloud credentials tells the runner where it is
661
+ # with an event file in this private directory (event-clone-done, then
662
+ # event-runtime-ready), and finds the runner's answer to runtime-ready there
663
+ # too. Never on stderr, which a subprocess shares; the path is unset here so
664
+ # no process task-up starts inherits it; nothing blocks.
665
+ uai_handshake_dir="${UAI_TASK_HANDSHAKE_DIR:-}"
666
+ unset UAI_TASK_HANDSHAKE_DIR
667
+ # A task on cloud credentials: every host Git operation runs with nothing of
668
+ # the runner's own (no home, so no .netrc, ~/.gitconfig or ~/.ssh/config),
669
+ # by exactly the route its lease names, and never falls back to another.
670
+ uai_leased_git=0
671
+ if [ -n "$uai_handshake_dir" ]; then
672
+ uai_leased_git=1
673
+ fi
674
+ # ADR-154 §6: no default slot. A task on cloud credentials gets none of the
675
+ # runner owner's own engine sign-ins: not their tokens in its env (the
676
+ # runner strips them from ours), not their config directories in its home.
677
+ uai_on_cloud_credentials=$uai_leased_git
678
+
643
679
  # Prepare the optional SSH identity. Connected users fetch over HTTPS with
644
680
  # their per-host GitHub credential; SSH is used for Git transport only when the
645
681
  # task owner has not connected GitHub. `IdentitiesOnly=yes` +
@@ -650,30 +686,59 @@ task_uai_dir_physical="$task_dir_physical/.uai"
650
686
  # $UAI_TASK_IDENTITY_DIR by the orchestrator) so clone + push run as them. A
651
687
  # task must never borrow the operator's shared identity: that would authorize
652
688
  # one user as a different principal.
689
+ #
690
+ # ADR-154 §12: a task on cloud credentials gets no key file. The runner
691
+ # holds the owner's key in its own memory behind a private agent socket for
692
+ # exactly as long as this clone ($UAI_TASK_SSH_AUTH_SOCK, with the public
693
+ # key naming the identity); ssh asks that agent and nothing else.
653
694
  if [ -n "${UAI_TASK_IDENTITY_DIR:-}" ] && [ -f "${UAI_TASK_IDENTITY_DIR}/id_ed25519" ]; then
654
695
  uai_identity_key="$UAI_TASK_IDENTITY_DIR/id_ed25519"
655
696
  log "git over SSH using the task creator's per-user key"
656
697
  else
657
698
  uai_identity_key=""
658
699
  fi
700
+ uai_task_ssh_auth_sock="${UAI_TASK_SSH_AUTH_SOCK:-}"
701
+ uai_task_ssh_public_key="${UAI_TASK_SSH_PUBLIC_KEY:-}"
702
+ unset UAI_TASK_SSH_AUTH_SOCK UAI_TASK_SSH_PUBLIC_KEY
659
703
  uai_has_ssh_identity=0
704
+ uai_leased_ssh=0
705
+ uai_known_hosts="$UAI_DATA_DIR/identity/known_hosts"
660
706
  if [ -n "$uai_identity_key" ] && [ -f "$uai_identity_key" ]; then
661
707
  uai_has_ssh_identity=1
662
- uai_known_hosts="$UAI_DATA_DIR/identity/known_hosts"
663
708
  GIT_SSH_COMMAND="ssh -i $uai_identity_key -o IdentitiesOnly=yes -o IdentityAgent=none -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
664
709
  [ -f "$uai_known_hosts" ] && GIT_SSH_COMMAND="$GIT_SSH_COMMAND -o UserKnownHostsFile=$uai_known_hosts"
665
710
  export GIT_SSH_COMMAND
666
711
  log "git over SSH using $uai_identity_key (1Password agent bypassed)"
712
+ elif [ -n "$uai_task_ssh_auth_sock" ] && [ -S "$uai_task_ssh_auth_sock" ] && [ -f "$uai_task_ssh_public_key" ]; then
713
+ uai_has_ssh_identity=1
714
+ uai_leased_ssh=1
715
+ GIT_SSH_COMMAND="ssh -F /dev/null -i $uai_task_ssh_public_key -o IdentitiesOnly=yes -o IdentityAgent=$uai_task_ssh_auth_sock -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
716
+ [ -f "$uai_known_hosts" ] && GIT_SSH_COMMAND="$GIT_SSH_COMMAND -o UserKnownHostsFile=$uai_known_hosts"
717
+ export GIT_SSH_COMMAND
718
+ log "git over SSH using the task owner's key, held in the runner's memory"
667
719
  else
668
720
  log "no SSH identity for the task owner — SSH transport fallback unavailable"
669
721
  fi
670
722
 
671
- # Only the non-secret path reaches this script. The task owner's token lives in
672
- # a private per-task `git credential-cache` daemon and is never placed in the
673
- # environment, argv, remote URL, mirror config, Compose, or logs.
723
+ # Only a non-secret path reaches this script. The task owner's token lives in
724
+ # a private per-task `git credential-cache` daemon, or, for a task on cloud
725
+ # credentials (ADR-154 §12), in the runner's own memory behind the helper
726
+ # $UAI_GITHUB_CREDENTIAL_HELPER asks for exactly as long as this clone. It is
727
+ # never placed in the environment, argv, remote URL, mirror config, Compose,
728
+ # or logs. Only the daemon's token is installed in the container below; a
729
+ # task on cloud credentials has its GitHub configuration written by the
730
+ # runner, which records it for cleanup.
674
731
  github_credential_socket="${UAI_GITHUB_CREDENTIAL_SOCKET:-}"
675
- unset UAI_GITHUB_CREDENTIAL_SOCKET
676
- if [ -n "$github_credential_socket" ]; then
732
+ github_credential_helper="${UAI_GITHUB_CREDENTIAL_HELPER:-}"
733
+ unset UAI_GITHUB_CREDENTIAL_SOCKET UAI_GITHUB_CREDENTIAL_HELPER
734
+ if [ -n "$github_credential_helper" ]; then
735
+ github_git_helper="$github_credential_helper"
736
+ elif [ -n "$github_credential_socket" ]; then
737
+ github_git_helper="cache --socket=$github_credential_socket"
738
+ else
739
+ github_git_helper=""
740
+ fi
741
+ if [ -n "$github_git_helper" ]; then
677
742
  uai_git_transport="https"
678
743
  else
679
744
  # Refined after each GitHub fetch below: select SSH only when it was the
@@ -683,6 +748,67 @@ else
683
748
  fi
684
749
  uai_used_ssh_transport=0
685
750
 
751
+ # ADR-154 §12: a task on cloud credentials runs every host Git operation in an
752
+ # environment rebuilt from an allowlist (env -i), never the runner process's
753
+ # own: an ambient GIT_CONFIG_COUNT and its numbered pairs, GIT_EXEC_PATH, an
754
+ # askpass or an agent socket could otherwise redirect or answer its route.
755
+ # What passes is what finds git and ssh and reaches the network (the PATH, the
756
+ # temporary directory, proxies, the CA bundle), never a credential, a
757
+ # configuration or a route.
758
+ uai_git_passthrough=()
759
+ if [ "$uai_leased_git" = "1" ]; then
760
+ for uai_git_name in PATH TMPDIR http_proxy https_proxy all_proxy no_proxy \
761
+ HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY SSL_CERT_FILE SSL_CERT_DIR; do
762
+ if [ -n "${!uai_git_name:-}" ]; then
763
+ uai_git_passthrough+=("$uai_git_name=${!uai_git_name}")
764
+ fi
765
+ done
766
+ fi
767
+
768
+ # Runs git with the NAME=VALUE assignments before `--`, then `git "$@"`: for a
769
+ # task on cloud credentials in those, the allowlist and no home alone;
770
+ # otherwise on top of this process's environment, which the wrappers below
771
+ # narrow themselves.
772
+ uai_git() {
773
+ local uai_assign=()
774
+ while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do
775
+ uai_assign+=("$1")
776
+ shift
777
+ done
778
+ shift
779
+ if [ "$uai_leased_git" = "1" ]; then
780
+ env -i ${uai_git_passthrough[@]+"${uai_git_passthrough[@]}"} \
781
+ HOME=/nonexistent XDG_CONFIG_HOME=/nonexistent LC_ALL=C \
782
+ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 \
783
+ ${uai_assign[@]+"${uai_assign[@]}"} git "$@"
784
+ else
785
+ env ${uai_assign[@]+"${uai_assign[@]}"} git "$@"
786
+ fi
787
+ }
788
+
789
+ # ADR-154 §12: a task on cloud credentials that GitHub refused says which of
790
+ # its owner's credentials was refused (the lease's GitHub sign-in or SSH key)
791
+ # and where to fix that: Uai's Settings, never this host.
792
+ emit_leased_github_denial() {
793
+ case "$1" in
794
+ token)
795
+ emit_err "GITHUB_TOKEN_ACCESS_DENIED" \
796
+ "GitHub denied the task owner's GitHub sign-in access to $2. Confirm that the Uai GitHub App is installed on the owning account or organization, this repository is included in its grant, and the owner can read it. Then start the task again." \
797
+ "$3"
798
+ ;;
799
+ ssh)
800
+ emit_err "GITHUB_SSH_ACCESS_DENIED" \
801
+ "GitHub denied the task owner's SSH key access to $2. Register that key with an account that can read the repository (and authorize it for SAML SSO when required), or connect GitHub in Settings. Then start the task again." \
802
+ "$3"
803
+ ;;
804
+ *)
805
+ emit_err "GITHUB_CONNECTION_REQUIRED" \
806
+ "The task owner has neither a GitHub sign-in nor an SSH key in Uai that can read $2. Connect GitHub in Settings, then start the task again." \
807
+ "$3"
808
+ ;;
809
+ esac
810
+ }
811
+
686
812
  git_with_github_credential() (
687
813
  unset GIT_ASKPASS SSH_ASKPASS SSH_AUTH_SOCK GIT_CONFIG \
688
814
  GIT_CONFIG_PARAMETERS GIT_TRACE GIT_TRACE2 GIT_TRACE_CURL \
@@ -690,13 +816,13 @@ git_with_github_credential() (
690
816
  export LC_ALL=C GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
691
817
  # If a container ever poisoned a shared mirror's URL config, fail closed
692
818
  # instead of silently switching this operation to an SSH principal.
693
- GIT_SSH_COMMAND=/bin/false GIT_TERMINAL_PROMPT=0 git \
819
+ uai_git GIT_SSH_COMMAND=/bin/false GIT_TERMINAL_PROMPT=0 -- \
694
820
  -c core.hooksPath=/dev/null \
695
821
  -c core.fsmonitor=false \
696
822
  -c http.extraHeader= \
697
823
  -c http.sslVerify=true \
698
824
  -c credential.helper= \
699
- -c "credential.helper=cache --socket=$github_credential_socket" \
825
+ -c "credential.helper=$github_git_helper" \
700
826
  "$@"
701
827
  )
702
828
 
@@ -707,7 +833,8 @@ git_with_github_ssh() (
707
833
  GIT_CONFIG_PARAMETERS GIT_TRACE GIT_TRACE2 GIT_TRACE_CURL \
708
834
  GIT_TRACE_PACKET GIT_TRACE_REDACT
709
835
  export LC_ALL=C GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
710
- GIT_TERMINAL_PROMPT=0 git \
836
+ # The SSH command chosen above: the task owner's identity, nothing else.
837
+ uai_git "GIT_SSH_COMMAND=${GIT_SSH_COMMAND:-/bin/false}" GIT_TERMINAL_PROMPT=0 -- \
711
838
  -c core.hooksPath=/dev/null \
712
839
  -c core.fsmonitor=false \
713
840
  -c 'url.git@github.com:.insteadOf=https://github.com/' \
@@ -721,7 +848,7 @@ git_anonymous_https() (
721
848
  GIT_CONFIG_PARAMETERS GIT_TRACE GIT_TRACE2 GIT_TRACE_CURL \
722
849
  GIT_TRACE_PACKET GIT_TRACE_REDACT
723
850
  export LC_ALL=C GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
724
- GIT_SSH_COMMAND=/bin/false GIT_TERMINAL_PROMPT=0 git \
851
+ uai_git GIT_SSH_COMMAND=/bin/false GIT_TERMINAL_PROMPT=0 -- \
725
852
  -c core.hooksPath=/dev/null \
726
853
  -c core.fsmonitor=false \
727
854
  -c http.extraHeader= \
@@ -744,11 +871,11 @@ git_host_control() (
744
871
  # This helper is for local repository bookkeeping and public/local remotes.
745
872
  # It must never inherit the operator's SSH agent or credential helper. Every
746
873
  # authenticated GitHub fetch goes through one of the explicit helpers above.
747
- GIT_CONFIG_COUNT=3 \
748
- GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null \
749
- GIT_CONFIG_KEY_1=core.fsmonitor GIT_CONFIG_VALUE_1=false \
750
- GIT_CONFIG_KEY_2=advice.defaultBranchName GIT_CONFIG_VALUE_2=false \
751
- GIT_SSH_COMMAND=/bin/false GIT_TERMINAL_PROMPT=0 git \
874
+ uai_git GIT_CONFIG_COUNT=3 \
875
+ GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null \
876
+ GIT_CONFIG_KEY_1=core.fsmonitor GIT_CONFIG_VALUE_1=false \
877
+ GIT_CONFIG_KEY_2=advice.defaultBranchName GIT_CONFIG_VALUE_2=false \
878
+ GIT_SSH_COMMAND=/bin/false GIT_TERMINAL_PROMPT=0 -- \
752
879
  -c credential.helper= \
753
880
  "$@"
754
881
  )
@@ -937,7 +1064,15 @@ if [ "$shared_files_mode" != "off" ]; then
937
1064
  [ -n "$shared_owner_user" ] && mkdir -p "$shared_root/users/$shared_owner_user"
938
1065
  # Marker for the preamble: only containers that actually carry the mounts
939
1066
  # get the "## Shared files" briefing (pre-feature containers don't).
940
- : > "$task_workspace/.uai/files-mounted"
1067
+ # Never written through a link or special file the task left in its place
1068
+ # (#cb3j0z): that is removed first (rm never follows a link), and nothing
1069
+ # is written where anything remains. The task's container is gone, so
1070
+ # nothing races this.
1071
+ files_mounted="$task_workspace/.uai/files-mounted"
1072
+ if [ -L "$files_mounted" ] || { [ -e "$files_mounted" ] && [ ! -f "$files_mounted" ]; }; then
1073
+ rm -f "$files_mounted" 2>/dev/null || true
1074
+ fi
1075
+ [ -L "$files_mounted" ] || [ -e "$files_mounted" ] || : > "$files_mounted"
941
1076
  fi
942
1077
 
943
1078
  # Iterate using jq -c so each project is a single JSON object per line.
@@ -1062,7 +1197,7 @@ while IFS= read -r project_obj; do
1062
1197
  # the next attempt starts fresh rather than re-using a half-init.
1063
1198
  fetch_err=""
1064
1199
  fetch_failed=0
1065
- if [ -n "$github_https_url" ] && [ -n "$github_credential_socket" ]; then
1200
+ if [ -n "$github_https_url" ] && [ -n "$github_git_helper" ]; then
1066
1201
  github_fetch_mode="token"
1067
1202
  if ! fetch_err=$(git_with_github_credential \
1068
1203
  -C "$mirror_dir" fetch origin 2>&1); then
@@ -1075,8 +1210,9 @@ while IFS= read -r project_obj; do
1075
1210
  -C "$mirror_dir" fetch origin 2>&1); then
1076
1211
  fetch_failed=1
1077
1212
  # github.com requires an authenticated SSH handshake even for public
1078
- # repositories. Preserve the no-credential public-repo path.
1079
- if git_anonymous_https -C "$mirror_dir" fetch origin \
1213
+ # repositories. Preserve the no-credential public-repo path, except
1214
+ # for a key a task's lease names: its route fails closed.
1215
+ if [ "$uai_leased_ssh" != "1" ] && git_anonymous_https -C "$mirror_dir" fetch origin \
1080
1216
  >/dev/null 2>&1; then
1081
1217
  github_fetch_mode="anonymous"
1082
1218
  fetch_failed=0
@@ -1108,6 +1244,9 @@ while IFS= read -r project_obj; do
1108
1244
  *"Authentication failed"*|*"Invalid username or password"*|*"HTTP 401"*|\
1109
1245
  *"HTTP 403"*|*"HTTP 404"*|*"requested URL returned error: 401"*|\
1110
1246
  *"requested URL returned error: 403"*|*"requested URL returned error: 404"*)
1247
+ if [ "$uai_leased_git" = "1" ]; then
1248
+ emit_leased_github_denial "$github_fetch_mode" "$repo_url" "bare-clone ($project_id)"
1249
+ fi
1111
1250
  if [ "$github_fetch_mode" = "token" ]; then
1112
1251
  emit_err "GITHUB_TOKEN_ACCESS_DENIED" \
1113
1252
  "GitHub denied the connected user's access to $repo_url. Confirm that the Uai GitHub App is installed on the owning account or organization, this repository is included in its grant, and the connected user can write to it. Then retry the task." \
@@ -1148,7 +1287,7 @@ while IFS= read -r project_obj; do
1148
1287
  step "FETCH_FAILED" "git fetch origin ($project_id)"
1149
1288
  fetch_ok=0
1150
1289
  fetch_err=""
1151
- if [ -n "$github_https_url" ] && [ -n "$github_credential_socket" ]; then
1290
+ if [ -n "$github_https_url" ] && [ -n "$github_git_helper" ]; then
1152
1291
  github_fetch_mode="token"
1153
1292
  if fetch_err=$(git_with_github_credential \
1154
1293
  -C "$mirror_dir" fetch origin 2>&1); then
@@ -1164,7 +1303,8 @@ while IFS= read -r project_obj; do
1164
1303
  else
1165
1304
  github_fetch_mode="none"
1166
1305
  fi
1167
- if [ "$fetch_ok" != "1" ]; then
1306
+ # A key a task's lease names never falls back to another principal.
1307
+ if [ "$fetch_ok" != "1" ] && [ "$uai_leased_ssh" != "1" ]; then
1168
1308
  anonymous_err=""
1169
1309
  if anonymous_err=$(git_anonymous_https \
1170
1310
  -C "$mirror_dir" fetch origin 2>&1); then
@@ -1185,6 +1325,9 @@ while IFS= read -r project_obj; do
1185
1325
  *"Authentication failed"*|*"Invalid username or password"*|*"HTTP 401"*|\
1186
1326
  *"HTTP 403"*|*"HTTP 404"*|*"requested URL returned error: 401"*|\
1187
1327
  *"requested URL returned error: 403"*|*"requested URL returned error: 404"*)
1328
+ if [ "$uai_leased_git" = "1" ]; then
1329
+ emit_leased_github_denial "$github_fetch_mode" "$repo_url" "git fetch origin ($project_id)"
1330
+ fi
1188
1331
  case "$github_fetch_mode" in
1189
1332
  token)
1190
1333
  emit_err "GITHUB_TOKEN_ACCESS_DENIED" \
@@ -1385,6 +1528,12 @@ while IFS= read -r project_obj; do
1385
1528
  release_project_lock
1386
1529
  done < <(jq -c '.[]' <<<"$projects_json")
1387
1530
 
1531
+ # Every host Git operation is over: a task on cloud credentials stops being
1532
+ # served its owner's GitHub token and SSH key now.
1533
+ if [ -n "$uai_handshake_dir" ]; then
1534
+ : > "$uai_handshake_dir/event-clone-done"
1535
+ fi
1536
+
1388
1537
  # Git and the checked-out files were created by the host. Reconcile their
1389
1538
  # group/modes only after every host Git operation and before Compose exposes
1390
1539
  # them to uid 1000. The host-only marker avoids recursively walking a large
@@ -1430,7 +1579,7 @@ fi
1430
1579
  share_task_tree_with_container "$task_uai_dir/repos" \
1431
1580
  "task-private Git repositories"
1432
1581
 
1433
- if [ -z "$github_credential_socket" ] && [ "$uai_used_ssh_transport" = "1" ]; then
1582
+ if [ -z "$github_git_helper" ] && [ "$uai_used_ssh_transport" = "1" ]; then
1434
1583
  uai_git_transport="ssh"
1435
1584
  fi
1436
1585
 
@@ -1627,13 +1776,13 @@ is_reserved_task_env_name() {
1627
1776
  NODE_PATH | SHELLOPTS | BASHOPTS | PS4 | BASH_ENV | ENV | \
1628
1777
  __UAI_ENV_* | __CURSOR_SANDBOX_ENV_RESTORE | GROK_LOGIN_ENV | \
1629
1778
  NODE_EXTRA_CA_CERTS | SSL_CERT_FILE | REQUESTS_CA_BUNDLE | \
1630
- ASDF_* | COREPACK_* | \
1779
+ [Aa][Ss][Dd][Ff]_* | [Cc][Oo][Rr][Ee][Pp][Aa][Cc][Kk]_* | \
1631
1780
  [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Pp][Rr][Ee][Ff][Ii][Xx] | \
1632
1781
  [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Cc][Aa][Cc][Hh][Ee] | \
1633
1782
  [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Ll][Oo][Gg][Ss][_-][Dd][Ii][Rr] | \
1634
1783
  [Nn][Pp][Mm]_[Cc][Oo][Nn][Ff][Ii][Gg]_[Uu][Mm][Aa][Ss][Kk] | UAI_* | \
1635
1784
  CLAUDE_CODE_OAUTH_TOKEN | \
1636
- ANTHROPIC_API_KEY | ANTHROPIC_AUTH_TOKEN | XAI_API_KEY | \
1785
+ ANTHROPIC_API_KEY | ANTHROPIC_AUTH_TOKEN | XAI_API_KEY | CURSOR_API_KEY | \
1637
1786
  PLAYWRIGHT_BROWSERS_PATH)
1638
1787
  return 0
1639
1788
  ;;
@@ -1641,6 +1790,30 @@ is_reserved_task_env_name() {
1641
1790
  return 1
1642
1791
  }
1643
1792
 
1793
+ # A baked task's project env, as both runtimes receive it (Docker's Compose
1794
+ # override, Apple's env file): values JSON on stdin; $1 the declared keys
1795
+ # (JSON array), $2 the preview URL env (JSON object). Declared env names
1796
+ # only, minus every name is_reserved_task_env_name reserves (the one
1797
+ # authority, never a second list) and the preview URL names.
1798
+ filter_baked_project_env() {
1799
+ local declared="$1" preview="$2" values reserved='[]' key
1800
+ values=$(cat)
1801
+ while IFS= read -r key; do
1802
+ if is_reserved_task_env_name "$key"; then
1803
+ reserved=$(jq -c --arg key "$key" '. + [$key]' <<<"$reserved")
1804
+ fi
1805
+ done < <(printf '%s' "$values" | jq -r 'keys[] | select(test("^[A-Za-z_][A-Za-z0-9_]*$"))')
1806
+ printf '%s' "$values" | jq -c \
1807
+ --argjson declared "$declared" \
1808
+ --argjson preview "$preview" \
1809
+ --argjson reserved "$reserved" '
1810
+ with_entries(.key as $key | select($declared | index($key)))
1811
+ | with_entries(select(.key | test("^[A-Za-z_][A-Za-z0-9_]*$")))
1812
+ | delpaths($reserved | map([.]))
1813
+ | delpaths($preview | keys | map([.]))
1814
+ '
1815
+ }
1816
+
1644
1817
  # Names that the loader owns are non-secret metadata. Mount the exact launch
1645
1818
  # set read-only so uai-init can remove even the empty clears inherited from a
1646
1819
  # derived image before it starts code-server. The editor daemon itself never
@@ -1844,13 +2017,15 @@ mv -f "$managed_env_keys_tmp" "$managed_env_keys_file"
1844
2017
  # in the container env (rather than the host's ~/.claude mount, which broke
1845
2018
  # with EROFS on session-env writes) means both the orchestrator's headless
1846
2019
  # claude and the code-server terminal's interactive claude authenticate.
1847
- printf ' %s: "${%s:-}"\n' \
1848
- "CLAUDE_CODE_OAUTH_TOKEN" "CLAUDE_CODE_OAUTH_TOKEN"
1849
- # Pasted engine API keys (ADR-070) ride the same at-up interpolation, so the
1850
- # terminal CLIs authenticate like the adapters' exec-time env forwarding.
1851
- printf ' %s: "${%s:-}"\n' "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEY"
1852
- printf ' %s: "${%s:-}"\n' "ANTHROPIC_AUTH_TOKEN" "ANTHROPIC_AUTH_TOKEN"
1853
- printf ' %s: "${%s:-}"\n' "XAI_API_KEY" "XAI_API_KEY"
2020
+ if [ "$uai_on_cloud_credentials" != "1" ]; then
2021
+ printf ' %s: "${%s:-}"\n' \
2022
+ "CLAUDE_CODE_OAUTH_TOKEN" "CLAUDE_CODE_OAUTH_TOKEN"
2023
+ # Pasted engine API keys (ADR-070) ride the same at-up interpolation, so the
2024
+ # terminal CLIs authenticate like the adapters' exec-time env forwarding.
2025
+ printf ' %s: "${%s:-}"\n' "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEY"
2026
+ printf ' %s: "${%s:-}"\n' "ANTHROPIC_AUTH_TOKEN" "ANTHROPIC_AUTH_TOKEN"
2027
+ printf ' %s: "${%s:-}"\n' "XAI_API_KEY" "XAI_API_KEY"
2028
+ fi
1854
2029
  # ADR-053: point Playwright at the shared browser cache volume.
1855
2030
  printf ' PLAYWRIGHT_BROWSERS_PATH: "/opt/pw-browsers"\n'
1856
2031
  # No GH_TOKEN/GITHUB_TOKEN in the container env (ADR-027). `gh` prefers such
@@ -2184,65 +2359,8 @@ step "COMPOSE_UP_FAILED" "start task container"
2184
2359
  preview_env_raw=$(jq -r '.[0].preview_env // "{}"' <<<"$task_json")
2185
2360
  filtered_project_env='{}'
2186
2361
  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" '
2190
- .
2191
- | with_entries(.key as $key | select($declared | index($key)))
2192
- | with_entries(select(.key | test("^GIT_CONFIG($|_)") | not))
2193
- | with_entries(select(.key | test(
2194
- "^(ASDF_|COREPACK_)|^npm_config_(prefix|cache|logs[_-]dir|umask)$";
2195
- "i"
2196
- ) | not))
2197
- | with_entries(select(.key | test("^UAI_") | not))
2198
- | with_entries(select(.key | test("^__UAI_ENV_") | not))
2199
- | del(
2200
- .GH_TOKEN,
2201
- .GITHUB_TOKEN,
2202
- .GH_CONFIG_DIR,
2203
- .GH_HOST,
2204
- .GIT_DIR,
2205
- .GIT_WORK_TREE,
2206
- .GIT_COMMON_DIR,
2207
- .GIT_OBJECT_DIRECTORY,
2208
- .GIT_ALTERNATE_OBJECT_DIRECTORIES,
2209
- .GIT_INDEX_FILE,
2210
- .GIT_EXEC_PATH,
2211
- .GIT_ASKPASS,
2212
- .SSH_ASKPASS,
2213
- .GIT_SSH,
2214
- .GIT_SSH_COMMAND,
2215
- .HOME,
2216
- .XDG_CONFIG_HOME,
2217
- .XDG_DATA_HOME,
2218
- .XDG_CACHE_HOME,
2219
- .PATH,
2220
- .SHELL,
2221
- .LD_PRELOAD,
2222
- .LD_AUDIT,
2223
- .LD_LIBRARY_PATH,
2224
- .NODE_OPTIONS,
2225
- .NODE_PATH,
2226
- .SHELLOPTS,
2227
- .BASHOPTS,
2228
- .PS4,
2229
- .BASH_ENV,
2230
- .ENV,
2231
- .NODE_EXTRA_CA_CERTS,
2232
- .SSL_CERT_FILE,
2233
- .REQUESTS_CA_BUNDLE,
2234
- .__CURSOR_SANDBOX_ENV_RESTORE,
2235
- .GROK_LOGIN_ENV,
2236
- .UAI_GIT_TRANSPORT,
2237
- .CLAUDE_CODE_OAUTH_TOKEN,
2238
- .ANTHROPIC_API_KEY,
2239
- .ANTHROPIC_AUTH_TOKEN,
2240
- .XAI_API_KEY,
2241
- .PLAYWRIGHT_BROWSERS_PATH,
2242
- .UAI_EDITOR_THEME
2243
- )
2244
- | delpaths($preview | keys | map([.]))
2245
- ')
2362
+ filtered_project_env=$(printf '%s' "$project_env_json" \
2363
+ | filter_baked_project_env "$union_env_keys_json" "$preview_env_raw")
2246
2364
  fi
2247
2365
  compose_env_override=""
2248
2366
  if [ "$TASK_RUNTIME_MODE" = "docker" ]; then
@@ -2352,10 +2470,12 @@ if [ "$TASK_RUNTIME_MODE" = "apple-container" ]; then
2352
2470
  printf 'PS4=\n'
2353
2471
  printf 'BASH_ENV=\n'
2354
2472
  printf 'ENV=\n'
2355
- printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "${CLAUDE_CODE_OAUTH_TOKEN:-}"
2356
- printf 'ANTHROPIC_API_KEY=%s\n' "${ANTHROPIC_API_KEY:-}"
2357
- printf 'ANTHROPIC_AUTH_TOKEN=%s\n' "${ANTHROPIC_AUTH_TOKEN:-}"
2358
- printf 'XAI_API_KEY=%s\n' "${XAI_API_KEY:-}"
2473
+ if [ "$uai_on_cloud_credentials" != "1" ]; then
2474
+ printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "${CLAUDE_CODE_OAUTH_TOKEN:-}"
2475
+ printf 'ANTHROPIC_API_KEY=%s\n' "${ANTHROPIC_API_KEY:-}"
2476
+ printf 'ANTHROPIC_AUTH_TOKEN=%s\n' "${ANTHROPIC_AUTH_TOKEN:-}"
2477
+ printf 'XAI_API_KEY=%s\n' "${XAI_API_KEY:-}"
2478
+ fi
2359
2479
  printf 'PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers\n'
2360
2480
  if [ "$UAI_PROJECT_ENV_MODE" = "loader" ]; then
2361
2481
  while IFS= read -r ekey; do
@@ -3295,7 +3415,7 @@ fi
3295
3415
  # CLI atomically rewrites the file on every start, which can invalidate
3296
3416
  # the bind mount and leave a stale dentry, after which claude refuses to
3297
3417
  # start. docker cp gives the container its own stable copy. Best-effort.
3298
- if [ -f "$UAI_OWNER_HOME/.claude.json" ]; then
3418
+ if [ "$uai_on_cloud_credentials" != "1" ] && [ -f "$UAI_OWNER_HOME/.claude.json" ]; then
3299
3419
  crt cp "$UAI_OWNER_HOME/.claude.json" \
3300
3420
  "$app_container":/home/node/.claude.json >/dev/null 2>&1 \
3301
3421
  || log "warning: crt cp of .claude.json failed; claude may need re-login"
@@ -3313,6 +3433,7 @@ fi
3313
3433
  if crt exec -u root "${managed_exec_env[@]}" "$app_container" \
3314
3434
  /usr/bin/mkdir -p /home/node/.codex >/dev/null; then
3315
3435
  for codex_item in auth.json config.toml AGENTS.md version.json installation_id rules; do
3436
+ [ "$uai_on_cloud_credentials" = "1" ] && continue
3316
3437
  if [ -e "$UAI_OWNER_HOME/.codex/$codex_item" ]; then
3317
3438
  # ADR-135: the task's copy must not carry the rotating refresh token —
3318
3439
  # N copies racing to refresh it revoke the whole family. Replace it with
@@ -3348,16 +3469,16 @@ fi
3348
3469
  # session state) so the container's kimi runs on the operator's subscription.
3349
3470
  crt exec -u root "${managed_exec_env[@]}" "$app_container" \
3350
3471
  /usr/bin/mkdir -p /home/node/.kimi-code/credentials >/dev/null 2>&1 || true
3351
- if [ -e "$UAI_OWNER_HOME/.kimi-code/config.toml" ]; then
3472
+ if [ "$uai_on_cloud_credentials" != "1" ] && [ -e "$UAI_OWNER_HOME/.kimi-code/config.toml" ]; then
3352
3473
  crt cp "$UAI_OWNER_HOME/.kimi-code/config.toml" \
3353
3474
  "$app_container":/home/node/.kimi-code/config.toml >/dev/null 2>&1 \
3354
3475
  || log "warning: crt cp of .kimi-code/config.toml failed; kimi may need re-login"
3355
3476
  fi
3356
- if [ -e "$UAI_OWNER_HOME/.kimi-code/device_id" ]; then
3477
+ if [ "$uai_on_cloud_credentials" != "1" ] && [ -e "$UAI_OWNER_HOME/.kimi-code/device_id" ]; then
3357
3478
  crt cp "$UAI_OWNER_HOME/.kimi-code/device_id" \
3358
3479
  "$app_container":/home/node/.kimi-code/device_id >/dev/null 2>&1 || true
3359
3480
  fi
3360
- if [ -e "$UAI_OWNER_HOME/.kimi-code/credentials/kimi-code.json" ]; then
3481
+ if [ "$uai_on_cloud_credentials" != "1" ] && [ -e "$UAI_OWNER_HOME/.kimi-code/credentials/kimi-code.json" ]; then
3361
3482
  crt cp "$UAI_OWNER_HOME/.kimi-code/credentials/kimi-code.json" \
3362
3483
  "$app_container":/home/node/.kimi-code/credentials/kimi-code.json >/dev/null 2>&1 \
3363
3484
  || log "warning: crt cp of .kimi-code credential failed; kimi may need re-login"
@@ -3373,6 +3494,7 @@ crt exec -u root "${managed_exec_env[@]}" "$app_container" \
3373
3494
  crt exec -u root "${managed_exec_env[@]}" "$app_container" \
3374
3495
  /usr/bin/mkdir -p /home/node/.grok >/dev/null 2>&1 || true
3375
3496
  for grok_item in auth.json config.toml models_cache.json agent_id; do
3497
+ [ "$uai_on_cloud_credentials" = "1" ] && continue
3376
3498
  if [ -e "$UAI_OWNER_HOME/.grok/$grok_item" ]; then
3377
3499
  if [ "$grok_item" = auth.json ] && command -v jq >/dev/null 2>&1; then
3378
3500
  brokered_auth="$(mktemp "${TMPDIR:-/tmp}/uai-grok-auth.XXXXXX")"
@@ -3401,6 +3523,7 @@ crt exec -u root "${managed_exec_env[@]}" "$app_container" \
3401
3523
  # (provisionEngineAccounts) since they live in the sealed DB, not on disk here.
3402
3524
  # The shared parent and this leaf were created with node ownership above.
3403
3525
  for oc_item in auth.json config.json; do
3526
+ [ "$uai_on_cloud_credentials" = "1" ] && continue
3404
3527
  if [ -e "$UAI_OWNER_HOME/.local/share/opencode/$oc_item" ]; then
3405
3528
  crt cp "$UAI_OWNER_HOME/.local/share/opencode/$oc_item" \
3406
3529
  "$app_container":/home/node/.local/share/opencode/ >/dev/null 2>&1 \
@@ -3443,6 +3566,25 @@ quarantine_runtime_authority_violation() {
3443
3566
  quarantine_untrusted_app_container
3444
3567
  }
3445
3568
 
3569
+ # ADR-154 §12: the runner writes a task on cloud credentials its owner's
3570
+ # GitHub configuration and SSH key, recorded for cleanup, now: after the
3571
+ # managed-auth scrub, before uai-init installs anything that may need them.
3572
+ # task-up waits for its answer, bounded, and stops on anything but continue.
3573
+ if [ -n "$uai_handshake_dir" ]; then
3574
+ : > "$uai_handshake_dir/event-runtime-ready"
3575
+ uai_waited=0
3576
+ while [ ! -f "$uai_handshake_dir/runtime-ready" ] && [ "$uai_waited" -lt 3000 ]; do
3577
+ sleep 0.2
3578
+ uai_waited=$((uai_waited + 1))
3579
+ done
3580
+ uai_runtime_ready=$(cat "$uai_handshake_dir/runtime-ready" 2>/dev/null || true)
3581
+ if [ "$uai_runtime_ready" != "continue" ]; then
3582
+ emit_err "CREDENTIALS_UNAVAILABLE" \
3583
+ "Uai could not write the task's GitHub sign-in and SSH key into it. Start the task again." \
3584
+ "write the task's credentials"
3585
+ fi
3586
+ fi
3587
+
3446
3588
  init_warning=""
3447
3589
  init_rc=0
3448
3590
  crt exec "${task_runtime_exec_env[@]}" "$app_container" \