@runuai/host 0.9.2 → 0.9.4
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.
|
@@ -207,14 +207,26 @@ fi
|
|
|
207
207
|
editor_root="$WORKSPACE"
|
|
208
208
|
[ -d "$editor_root" ] || editor_root="/home/node"
|
|
209
209
|
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
|
|
210
|
+
# Keep code-server on Uai's managed data path even when a project declares
|
|
211
|
+
# XDG_DATA_HOME. Otherwise code-server starts with a fresh profile elsewhere,
|
|
212
|
+
# which restores the light theme and Workspace Trust prompt.
|
|
213
|
+
cs_data_dir="$HOME/.local/share/code-server"
|
|
214
|
+
cs_user_dir="$cs_data_dir/User"
|
|
213
215
|
cs_seed="/usr/local/share/uai/code-server-settings.json"
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
# The source-tree sibling keeps direct smoke tests and development runs on the
|
|
217
|
+
# same seed as the packaged image; the image always uses the root-owned path.
|
|
218
|
+
cs_source_seed="$(dirname "${BASH_SOURCE[0]}")/code-server-settings.json"
|
|
219
|
+
if [ ! -f "$cs_seed" ] && [ -f "$cs_source_seed" ]; then
|
|
220
|
+
cs_seed="$cs_source_seed"
|
|
221
|
+
fi
|
|
222
|
+
if [ ! -f "$cs_user_dir/settings.json" ]; then
|
|
223
|
+
if [ ! -f "$cs_seed" ]; then
|
|
224
|
+
log "warning: code-server settings seed missing at $cs_seed"
|
|
225
|
+
elif mkdir -p "$cs_user_dir" && cp "$cs_seed" "$cs_user_dir/settings.json"; then
|
|
226
|
+
log "seeded code-server settings"
|
|
227
|
+
else
|
|
228
|
+
log "warning: could not seed code-server settings at $cs_user_dir/settings.json"
|
|
229
|
+
fi
|
|
218
230
|
fi
|
|
219
231
|
|
|
220
232
|
if pgrep -f 'code-server.*--bind-addr' >/dev/null 2>&1; then
|
|
@@ -222,6 +234,8 @@ if pgrep -f 'code-server.*--bind-addr' >/dev/null 2>&1; then
|
|
|
222
234
|
else
|
|
223
235
|
log "launching code-server on 0.0.0.0:8080"
|
|
224
236
|
nohup code-server \
|
|
237
|
+
--user-data-dir "$cs_data_dir" \
|
|
238
|
+
--disable-workspace-trust \
|
|
225
239
|
--auth none \
|
|
226
240
|
--disable-telemetry \
|
|
227
241
|
--bind-addr 0.0.0.0:8080 \
|
package/package.json
CHANGED
package/scripts/agent/task-up.sh
CHANGED
|
@@ -89,6 +89,31 @@ task_json=$(db_get_task "$task_id")
|
|
|
89
89
|
|
|
90
90
|
task_branch=$(jq -r '.[0].branch' <<<"$task_json")
|
|
91
91
|
|
|
92
|
+
# The standard container runs as node:node (1000:1000), while Docker Desktop
|
|
93
|
+
# hosts commonly run as 501:20 and Linux hosts may use another uid/gid. Share
|
|
94
|
+
# only this task's writable trees through the host's effective group. A root
|
|
95
|
+
# host is the exception: never add the unprivileged container user to gid 0;
|
|
96
|
+
# root can chgrp the task trees directly to node's gid instead.
|
|
97
|
+
task_container_gid=$(id -g)
|
|
98
|
+
if [ "$(id -u)" = "0" ]; then
|
|
99
|
+
task_container_gid=1000
|
|
100
|
+
elif [ "$task_container_gid" = "0" ]; then
|
|
101
|
+
task_container_gid=""
|
|
102
|
+
for candidate_gid in $(id -G); do
|
|
103
|
+
if [ "$candidate_gid" != "0" ]; then
|
|
104
|
+
task_container_gid="$candidate_gid"
|
|
105
|
+
break
|
|
106
|
+
fi
|
|
107
|
+
done
|
|
108
|
+
fi
|
|
109
|
+
case "$task_container_gid" in
|
|
110
|
+
''|*[!0-9]*)
|
|
111
|
+
emit_err "WORKTREE_FAILED" \
|
|
112
|
+
"Uai could not resolve a safe host group for the task container" \
|
|
113
|
+
"resolve task container filesystem group"
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
|
|
92
117
|
step "PROJECT_NOT_FOUND" "read task projects"
|
|
93
118
|
projects_json=$(db_get_projects_for_task "$task_id")
|
|
94
119
|
project_count=$(jq 'length' <<<"$projects_json")
|
|
@@ -280,6 +305,55 @@ assert_bare_repo_tree_safe() {
|
|
|
280
305
|
fi
|
|
281
306
|
}
|
|
282
307
|
|
|
308
|
+
# Make a host-created task tree writable by the container's uid-1000 node user
|
|
309
|
+
# without making it world-writable or changing ownership away from the host.
|
|
310
|
+
# `find` deliberately does not follow repository symlinks. Setgid directories
|
|
311
|
+
# keep Git-created descendants in the shared group; core.sharedRepository
|
|
312
|
+
# below makes Git's own files group-writable as well.
|
|
313
|
+
set_task_entry_container_access() {
|
|
314
|
+
local entry="$1"
|
|
315
|
+
chgrp "$task_container_gid" "$entry" || return 1
|
|
316
|
+
if [ -d "$entry" ]; then
|
|
317
|
+
chmod ug+rwx,g+s "$entry" || return 1
|
|
318
|
+
else
|
|
319
|
+
chmod ug+rw "$entry" || return 1
|
|
320
|
+
fi
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
share_task_entry_with_container() {
|
|
324
|
+
local entry="$1" label="$2"
|
|
325
|
+
if ! set_task_entry_container_access "$entry"; then
|
|
326
|
+
emit_err "WORKTREE_FAILED" \
|
|
327
|
+
"Uai could not share the $label with the task container" \
|
|
328
|
+
"set task filesystem permissions"
|
|
329
|
+
fi
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
share_task_tree_with_container() {
|
|
333
|
+
local root="$1" label="$2" entry entry_list failed_entry=""
|
|
334
|
+
[ -d "$root" ] || return 0
|
|
335
|
+
|
|
336
|
+
entry_list=$(mktemp "/tmp/uai-task-tree.XXXXXX")
|
|
337
|
+
if ! find "$root" \( -type d -o -type f \) -print0 > "$entry_list"; then
|
|
338
|
+
rm -f "$entry_list"
|
|
339
|
+
emit_err "WORKTREE_FAILED" \
|
|
340
|
+
"Uai could not enumerate the complete $label before sharing it with the task container" \
|
|
341
|
+
"enumerate task filesystem"
|
|
342
|
+
fi
|
|
343
|
+
while IFS= read -r -d '' entry; do
|
|
344
|
+
if ! set_task_entry_container_access "$entry"; then
|
|
345
|
+
failed_entry="$entry"
|
|
346
|
+
break
|
|
347
|
+
fi
|
|
348
|
+
done < "$entry_list"
|
|
349
|
+
rm -f "$entry_list"
|
|
350
|
+
if [ -n "$failed_entry" ]; then
|
|
351
|
+
emit_err "WORKTREE_FAILED" \
|
|
352
|
+
"Uai could not share the complete $label with the task container" \
|
|
353
|
+
"set task filesystem permissions"
|
|
354
|
+
fi
|
|
355
|
+
}
|
|
356
|
+
|
|
283
357
|
sanitize_mirror_control_plane() {
|
|
284
358
|
local mirror="$1" origin="$2" config_tmp
|
|
285
359
|
assert_bare_repo_tree_safe "$mirror" "FETCH_FAILED" "host repository cache"
|
|
@@ -328,6 +402,7 @@ sanitize_task_repo_control_plane() {
|
|
|
328
402
|
git_host_control config --file "$config_tmp" core.repositoryformatversion 0
|
|
329
403
|
git_host_control config --file "$config_tmp" core.filemode true
|
|
330
404
|
git_host_control config --file "$config_tmp" core.bare "$bare"
|
|
405
|
+
git_host_control config --file "$config_tmp" core.sharedRepository group
|
|
331
406
|
git_host_control config --file "$config_tmp" remote.origin.url "$origin"
|
|
332
407
|
git_host_control config --file "$config_tmp" remote.origin.fetch \
|
|
333
408
|
'+refs/heads/*:refs/remotes/origin/*'
|
|
@@ -340,6 +415,13 @@ sanitize_task_repo_control_plane() {
|
|
|
340
415
|
mv -f "$config_tmp" "$repo/config"
|
|
341
416
|
rm -rf "$repo/hooks"
|
|
342
417
|
mkdir -p "$repo/hooks"
|
|
418
|
+
# mktemp is intentionally 0600. Once its trusted contents are atomically in
|
|
419
|
+
# place, grant only the selected task group access; this must run on every
|
|
420
|
+
# resume even after the one-time tree migration marker exists.
|
|
421
|
+
share_task_entry_with_container "$repo/config" \
|
|
422
|
+
"task-private Git config"
|
|
423
|
+
share_task_entry_with_container "$repo/hooks" \
|
|
424
|
+
"task-private Git hooks directory"
|
|
343
425
|
}
|
|
344
426
|
|
|
345
427
|
# ADR-062 shared files: two host-resident roots mounted into the container
|
|
@@ -758,6 +840,8 @@ while IFS= read -r project_obj; do
|
|
|
758
840
|
|| { git_host_control -C "$worktree_target" init -q \
|
|
759
841
|
&& git_host_control -C "$worktree_target" symbolic-ref HEAD "refs/heads/$project_default"; }
|
|
760
842
|
git_host_control -C "$worktree_target" remote add origin "$repo_url" 2>/dev/null || true
|
|
843
|
+
sanitize_task_repo_control_plane "$worktree_target/.git" "$repo_url" \
|
|
844
|
+
"$project_default" false
|
|
761
845
|
printf '%s\n' \
|
|
762
846
|
"# Empty repository" \
|
|
763
847
|
"" \
|
|
@@ -785,10 +869,59 @@ while IFS= read -r project_obj; do
|
|
|
785
869
|
&& [ -n "$tool_versions" ] && [ "$tool_versions" != "null" ]; then
|
|
786
870
|
printf '%s\n' "$tool_versions" > "$worktree_target/.tool-versions"
|
|
787
871
|
fi
|
|
872
|
+
if [ -d "$worktree_target/.git" ] && [ ! -L "$worktree_target/.git" ]; then
|
|
873
|
+
share_task_tree_with_container "$worktree_target/.git" \
|
|
874
|
+
"standalone task Git repository"
|
|
875
|
+
fi
|
|
788
876
|
|
|
789
877
|
release_project_lock
|
|
790
878
|
done < <(jq -c '.[]' <<<"$projects_json")
|
|
791
879
|
|
|
880
|
+
# Git and the checked-out files were created by the host. Reconcile their
|
|
881
|
+
# group/modes only after every host Git operation and before Compose exposes
|
|
882
|
+
# them to uid 1000. The host-only marker avoids recursively walking a large
|
|
883
|
+
# workspace (for example node_modules) on every resume, while its gid payload
|
|
884
|
+
# forces a fresh pass if the task moves to a differently-grouped host. This
|
|
885
|
+
# first pass self-heals 0.9.2 task repos whose mktemp config was mode 0600 on a
|
|
886
|
+
# uid-501 macOS host. The sanitizer above repairs its rewritten control files
|
|
887
|
+
# on every later resume.
|
|
888
|
+
step "WORKTREE_FAILED" "share task files with container"
|
|
889
|
+
mkdir -p "$task_uai_dir"
|
|
890
|
+
# The shared group may be a broad host group (for example macOS `staff`). Keep
|
|
891
|
+
# its writable leaves behind host-only parents; Docker bind-mounts the exact
|
|
892
|
+
# workspace/repository roots, so node does not need host-path traversal here.
|
|
893
|
+
chmod 700 "$task_dir" "$task_uai_dir"
|
|
894
|
+
filesystem_group_marker="$task_uai_dir/container-filesystem-group-v1"
|
|
895
|
+
marked_container_gid=""
|
|
896
|
+
if [ -L "$filesystem_group_marker" ]; then
|
|
897
|
+
emit_err "WORKTREE_FAILED" \
|
|
898
|
+
"Uai refused a symbolic-link task filesystem marker" \
|
|
899
|
+
"share task files with container"
|
|
900
|
+
elif [ -f "$filesystem_group_marker" ]; then
|
|
901
|
+
marked_container_gid=$(cat "$filesystem_group_marker" 2>/dev/null || true)
|
|
902
|
+
elif [ -e "$filesystem_group_marker" ]; then
|
|
903
|
+
emit_err "WORKTREE_FAILED" \
|
|
904
|
+
"Uai refused a non-file task filesystem marker" \
|
|
905
|
+
"share task files with container"
|
|
906
|
+
fi
|
|
907
|
+
if [ "$marked_container_gid" != "$task_container_gid" ]; then
|
|
908
|
+
share_task_tree_with_container "$task_workspace" "task workspace"
|
|
909
|
+
filesystem_group_marker_tmp=$(mktemp \
|
|
910
|
+
"$task_uai_dir/.container-filesystem-group.XXXXXX")
|
|
911
|
+
printf '%s\n' "$task_container_gid" > "$filesystem_group_marker_tmp"
|
|
912
|
+
chmod 600 "$filesystem_group_marker_tmp"
|
|
913
|
+
mv -f "$filesystem_group_marker_tmp" "$filesystem_group_marker"
|
|
914
|
+
else
|
|
915
|
+
# A container may have changed a mount root's mode. These two O(1) repairs
|
|
916
|
+
# keep entry into the already-normalized trees reliable without walking them.
|
|
917
|
+
share_task_entry_with_container "$task_workspace" "task workspace root"
|
|
918
|
+
fi
|
|
919
|
+
# Task repos are repacked, so this tree stays small; normalize it every time.
|
|
920
|
+
# Unlike the workspace pass, this covers Git metadata created during the host
|
|
921
|
+
# refresh and any loose objects/locks written by the prior container.
|
|
922
|
+
share_task_tree_with_container "$task_uai_dir/repos" \
|
|
923
|
+
"task-private Git repositories"
|
|
924
|
+
|
|
792
925
|
if [ -z "$github_credential_socket" ] && [ "$uai_used_ssh_transport" = "1" ]; then
|
|
793
926
|
uai_git_transport="ssh"
|
|
794
927
|
fi
|
|
@@ -872,6 +1005,11 @@ fi
|
|
|
872
1005
|
printf ' pull_policy: never\n'
|
|
873
1006
|
fi
|
|
874
1007
|
printf ' init: true\n'
|
|
1008
|
+
# Give node the one host group that owns this task's bind-mounted files.
|
|
1009
|
+
# Numeric group_add works even when the group has a different name (or no
|
|
1010
|
+
# name) in the image. Root hosts use node's existing gid 1000 above.
|
|
1011
|
+
printf ' group_add:\n'
|
|
1012
|
+
printf ' - "%s"\n' "$task_container_gid"
|
|
875
1013
|
printf ' working_dir: /workspace\n'
|
|
876
1014
|
printf ' volumes:\n'
|
|
877
1015
|
printf ' - "%s:/workspace"\n' "$task_workspace"
|
|
@@ -953,7 +1091,7 @@ fi
|
|
|
953
1091
|
GIT_CONFIG* | GIT_DIR | GIT_WORK_TREE | GIT_COMMON_DIR | \
|
|
954
1092
|
GIT_OBJECT_DIRECTORY | GIT_ALTERNATE_OBJECT_DIRECTORIES | \
|
|
955
1093
|
GIT_INDEX_FILE | GIT_EXEC_PATH | GIT_ASKPASS | SSH_ASKPASS | \
|
|
956
|
-
GIT_SSH | GIT_SSH_COMMAND | HOME | XDG_CONFIG_HOME | \
|
|
1094
|
+
GIT_SSH | GIT_SSH_COMMAND | HOME | XDG_CONFIG_HOME | XDG_DATA_HOME | \
|
|
957
1095
|
BASH_ENV | ENV | UAI_GIT_TRANSPORT | \
|
|
958
1096
|
CLAUDE_CODE_OAUTH_TOKEN | ANTHROPIC_API_KEY | \
|
|
959
1097
|
ANTHROPIC_AUTH_TOKEN | XAI_API_KEY | PLAYWRIGHT_BROWSERS_PATH) continue ;;
|
|
@@ -1042,6 +1180,7 @@ compose_env_override=$(printf '%s' "$project_env_json" | jq -c \
|
|
|
1042
1180
|
.GIT_SSH_COMMAND,
|
|
1043
1181
|
.HOME,
|
|
1044
1182
|
.XDG_CONFIG_HOME,
|
|
1183
|
+
.XDG_DATA_HOME,
|
|
1045
1184
|
.BASH_ENV,
|
|
1046
1185
|
.ENV,
|
|
1047
1186
|
.UAI_GIT_TRANSPORT,
|
|
@@ -1078,6 +1217,7 @@ managed_exec_env=(
|
|
|
1078
1217
|
-e "GH_CONFIG_DIR=$managed_gh_config_dir"
|
|
1079
1218
|
-e "PATH=/usr/bin:/bin"
|
|
1080
1219
|
-e "XDG_CONFIG_HOME="
|
|
1220
|
+
-e "XDG_DATA_HOME="
|
|
1081
1221
|
-e "LD_PRELOAD="
|
|
1082
1222
|
-e "LD_LIBRARY_PATH="
|
|
1083
1223
|
-e "DYLD_INSERT_LIBRARIES="
|
|
@@ -1104,6 +1244,36 @@ managed_clean_env=(
|
|
|
1104
1244
|
"PATH=/usr/bin:/bin"
|
|
1105
1245
|
)
|
|
1106
1246
|
|
|
1247
|
+
# Git rejects a repository owned by another uid even when its shared group is
|
|
1248
|
+
# writable. Trust only the selected worktrees in this task container—never `*`
|
|
1249
|
+
# and never the host cache. The task-private ~/.gitconfig makes these entries
|
|
1250
|
+
# local to this container, and the read-before-add keeps resumes idempotent.
|
|
1251
|
+
step "CONTAINER_INIT_FAILED" "trust task Git worktrees"
|
|
1252
|
+
container_safe_dirs=$(docker exec -u node "${managed_exec_env[@]}" \
|
|
1253
|
+
"$app_container" "${managed_clean_env[@]}" /usr/bin/git config --global \
|
|
1254
|
+
--get-all safe.directory 2>/dev/null || true)
|
|
1255
|
+
while IFS= read -r safe_project; do
|
|
1256
|
+
[ -n "$safe_project" ] || continue
|
|
1257
|
+
safe_project_id=$(jq -r '.id' <<<"$safe_project")
|
|
1258
|
+
safe_project_slug=$(jq -r '.slug' <<<"$safe_project")
|
|
1259
|
+
for safe_worktree in \
|
|
1260
|
+
"/workspace/$safe_project_slug" \
|
|
1261
|
+
"$task_workspace/$safe_project_slug" \
|
|
1262
|
+
"$task_uai_dir/repos/$safe_project_id.git"; do
|
|
1263
|
+
if ! printf '%s\n' "$container_safe_dirs" \
|
|
1264
|
+
| grep -Fqx -- "$safe_worktree"; then
|
|
1265
|
+
if ! docker exec -u node "${managed_exec_env[@]}" "$app_container" \
|
|
1266
|
+
"${managed_clean_env[@]}" /usr/bin/git config --global --add \
|
|
1267
|
+
safe.directory "$safe_worktree" >/dev/null; then
|
|
1268
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1269
|
+
"Uai could not mark $safe_worktree as this task's trusted Git path" \
|
|
1270
|
+
"trust task Git worktrees"
|
|
1271
|
+
fi
|
|
1272
|
+
container_safe_dirs="${container_safe_dirs}"$'\n'"${safe_worktree}"
|
|
1273
|
+
fi
|
|
1274
|
+
done
|
|
1275
|
+
done < <(jq -c '.[]' <<<"$projects_json")
|
|
1276
|
+
|
|
1107
1277
|
# A failed/retried start can preserve a container that still holds a previous
|
|
1108
1278
|
# account. Erase that Uai-managed state first for both connected and
|
|
1109
1279
|
# disconnected starts; otherwise a no-credential resume could briefly reuse a
|