@runuai/host 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/agent/task-up.sh +168 -0
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"
|
|
@@ -1104,6 +1242,36 @@ managed_clean_env=(
|
|
|
1104
1242
|
"PATH=/usr/bin:/bin"
|
|
1105
1243
|
)
|
|
1106
1244
|
|
|
1245
|
+
# Git rejects a repository owned by another uid even when its shared group is
|
|
1246
|
+
# writable. Trust only the selected worktrees in this task container—never `*`
|
|
1247
|
+
# and never the host cache. The task-private ~/.gitconfig makes these entries
|
|
1248
|
+
# local to this container, and the read-before-add keeps resumes idempotent.
|
|
1249
|
+
step "CONTAINER_INIT_FAILED" "trust task Git worktrees"
|
|
1250
|
+
container_safe_dirs=$(docker exec -u node "${managed_exec_env[@]}" \
|
|
1251
|
+
"$app_container" "${managed_clean_env[@]}" /usr/bin/git config --global \
|
|
1252
|
+
--get-all safe.directory 2>/dev/null || true)
|
|
1253
|
+
while IFS= read -r safe_project; do
|
|
1254
|
+
[ -n "$safe_project" ] || continue
|
|
1255
|
+
safe_project_id=$(jq -r '.id' <<<"$safe_project")
|
|
1256
|
+
safe_project_slug=$(jq -r '.slug' <<<"$safe_project")
|
|
1257
|
+
for safe_worktree in \
|
|
1258
|
+
"/workspace/$safe_project_slug" \
|
|
1259
|
+
"$task_workspace/$safe_project_slug" \
|
|
1260
|
+
"$task_uai_dir/repos/$safe_project_id.git"; do
|
|
1261
|
+
if ! printf '%s\n' "$container_safe_dirs" \
|
|
1262
|
+
| grep -Fqx -- "$safe_worktree"; then
|
|
1263
|
+
if ! docker exec -u node "${managed_exec_env[@]}" "$app_container" \
|
|
1264
|
+
"${managed_clean_env[@]}" /usr/bin/git config --global --add \
|
|
1265
|
+
safe.directory "$safe_worktree" >/dev/null; then
|
|
1266
|
+
emit_err "CONTAINER_INIT_FAILED" \
|
|
1267
|
+
"Uai could not mark $safe_worktree as this task's trusted Git path" \
|
|
1268
|
+
"trust task Git worktrees"
|
|
1269
|
+
fi
|
|
1270
|
+
container_safe_dirs="${container_safe_dirs}"$'\n'"${safe_worktree}"
|
|
1271
|
+
fi
|
|
1272
|
+
done
|
|
1273
|
+
done < <(jq -c '.[]' <<<"$projects_json")
|
|
1274
|
+
|
|
1107
1275
|
# A failed/retried start can preserve a container that still holds a previous
|
|
1108
1276
|
# account. Erase that Uai-managed state first for both connected and
|
|
1109
1277
|
# disconnected starts; otherwise a no-credential resume could briefly reuse a
|