@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.6 → 0.1.8
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/bin/adl +2 -2
- package/bin/lean-ctx-observe-strip +76 -3
- package/bin/reconcile +460 -240
- package/bin/validate-manifest +36 -16
- package/package.json +1 -1
- package/packages/agent-hooks/src/deploy/deploy.ts +98 -18
- package/packages/agent-hooks/src/deploy/doctor.ts +61 -11
- package/packages/agent-hooks/src/deploy/json-merge.ts +430 -18
- package/packages/agent-hooks/src/deploy/lock.ts +36 -2
- package/packages/agent-hooks/src/lean-ctx/env.ts +17 -0
- package/packages/agent-hooks/src/lean-ctx/mcp.ts +4 -2
- package/packages/agent-settings/assets/lean-ctx/config.toml +3 -0
- package/packages/agent-settings/assets/lean-ctx/gitignore +4 -0
- package/packages/agent-settings/assets/lean-ctx/layout.toml +2 -0
- package/packages/agent-settings/src/deploy/agents-deploy.ts +10 -5
- package/packages/agent-settings/src/deploy/doctor.ts +39 -3
- package/packages/agent-settings/src/deploy/lean-ctx-deploy.ts +104 -12
- package/packages/agent-settings/src/deploy/lean-ctx-merge.ts +580 -0
- package/packages/agent-settings/src/deploy/lock.ts +4 -1
- package/packages/agent-settings/src/deploy/mcp-deploy.ts +9 -6
- package/packages/agent-settings/src/deploy/settings.ts +6 -3
- package/packages/agent-skills/dist/deploy.d.ts +2 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +15 -8
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/dist/manifest.d.ts +1 -0
- package/packages/agent-skills/dist/manifest.d.ts.map +1 -1
- package/packages/agent-skills/dist/manifest.js +6 -2
- package/packages/agent-skills/dist/manifest.js.map +1 -1
- package/packages/agent-skills/src/deploy.ts +15 -8
- package/packages/agent-skills/src/manifest.ts +5 -2
- package/packages/azdo/src/azdo-repo-doctor.ts +401 -91
- package/packages/azdo/src/lib/graph-identity.ts +190 -3
- package/packages/fleet/jobs/git-maintain/src/main.ts +305 -45
- package/packages/fleet/manifest.json +6 -4
- package/packages/fleet/manifest.schema.json +15 -5
- package/packages/repo-controls/README.md +13 -6
- package/packages/repo-controls/assets/adl-config.default.toml +1 -0
- package/packages/repo-controls/src/cli.ts +6 -1
- package/packages/repo-controls/src/gitignore-block.ts +17 -0
- package/packages/repo-controls/src/install.ts +88 -6
package/bin/reconcile
CHANGED
|
@@ -180,6 +180,12 @@ collect_lean_ctx_observe_strip_backups() {
|
|
|
180
180
|
original="${BASH_REMATCH[1]}"
|
|
181
181
|
backup="${BASH_REMATCH[2]}"
|
|
182
182
|
record_backup "$original" "$backup"
|
|
183
|
+
elif [[ "$line" =~ ^lean-ctx-observe-strip:\ backed\ up\ previous\ hook\ script\ (.+)\ to\ (.+)$ ]]; then
|
|
184
|
+
# ~/.claude/hooks/lean-ctx-* scripts: same two-path shape as a skill
|
|
185
|
+
# directory (sibling "hooks.pre-lean-ctx-observe-strip-<ts>/" backup).
|
|
186
|
+
original="${BASH_REMATCH[1]}"
|
|
187
|
+
backup="${BASH_REMATCH[2]}"
|
|
188
|
+
record_backup "$original" "$backup"
|
|
183
189
|
fi
|
|
184
190
|
done <<<"$text"
|
|
185
191
|
}
|
|
@@ -456,28 +462,15 @@ run_npm() {
|
|
|
456
462
|
fi
|
|
457
463
|
}
|
|
458
464
|
|
|
459
|
-
# Whether a read-only, owner-scoped query (brew_query, npm_installed_version)
|
|
460
|
-
# can run without asking for privilege reconcile does not already have. Apply
|
|
461
|
-
# mode always runs as real root (checked elsewhere), which can sudo -u any
|
|
462
|
-
# administrator with no password. A plan/audit preview run has no such
|
|
463
|
-
# guarantee -- $administrator on a Linux VPS device is "root", and previewing
|
|
464
|
-
# that device's plan from an unprivileged account must not demand a sudo
|
|
465
|
-
# password it may not have; it falls back to the pre-2026-09-28 unconditional
|
|
466
|
-
# message instead. Self-administered devices (m3: administrator=tarunrana)
|
|
467
|
-
# need no escalation at all, so their preview can query for real.
|
|
468
|
-
can_query_as_administrator() {
|
|
469
|
-
[[ "$(id -un)" == "$administrator" ]] || [[ "$(id -u)" -eq 0 ]]
|
|
470
|
-
}
|
|
471
|
-
|
|
472
465
|
# Read-only Homebrew query, run exactly like run_brew's real mutations (same
|
|
473
466
|
# owner, cwd, and stdin) but never a mutation itself. A named function so
|
|
474
467
|
# tests can stub just this, instead of having to fake sudo's own argument
|
|
475
468
|
# parsing the way a raw inline `sudo -u ... "$brew_bin" ...` would require.
|
|
476
|
-
# Apply-mode only
|
|
477
|
-
#
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
#
|
|
469
|
+
# Apply-mode only: `brew list`/`brew outdated` have been observed to take
|
|
470
|
+
# minutes under real load on a contended box (Homebrew's own auto-update
|
|
471
|
+
# against a large git checkout), so a plan preview never calls this -- it
|
|
472
|
+
# would turn a "quick look" into a slow, load-dependent one for every brew
|
|
473
|
+
# formula on the device.
|
|
481
474
|
brew_query() {
|
|
482
475
|
(cd /tmp && sudo -u "$brew_owner" -H "$brew_bin" "$@" </dev/null)
|
|
483
476
|
}
|
|
@@ -800,6 +793,27 @@ install_surface_policy_file() {
|
|
|
800
793
|
echo "installed required surface policy: $target"
|
|
801
794
|
}
|
|
802
795
|
|
|
796
|
+
# A resolved release version against the first line of a release-archive-
|
|
797
|
+
# latest tool's own `--version` output. Different tools format that line
|
|
798
|
+
# differently -- lean-ctx: "lean-ctx 3.10.5 (official, ...)" (version padded
|
|
799
|
+
# by spaces on both sides); agent-vault: "agent-vault version 0.39.3" (flush
|
|
800
|
+
# against the end of the line, no trailing space); a plain "vX.Y.Z" line
|
|
801
|
+
# with no separating space before the version at all -- so the LEFT boundary
|
|
802
|
+
# is a string start or a real space, with an OPTIONAL literal "v" directly
|
|
803
|
+
# after that boundary (never "v" floating anywhere: found in review,
|
|
804
|
+
# 2026-09-28, `(^|[ v])` treated a bare "v" as equivalent to a space no
|
|
805
|
+
# matter what preceded it, so "tool dev0.39.3"/"tool rev0.39.3" -- where the
|
|
806
|
+
# "v" belongs to "dev"/"rev", not a version prefix -- silently matched and
|
|
807
|
+
# would have skipped a real upgrade). The RIGHT boundary is a space or a
|
|
808
|
+
# string end and never accepts "v", or "0.39.3" would incorrectly match
|
|
809
|
+
# inside "0.39.30". Dots in the version are escaped so they match literally,
|
|
810
|
+
# not "any character," in the regex below.
|
|
811
|
+
release_archive_version_matches() {
|
|
812
|
+
local version_line="$1" version="$2" escaped
|
|
813
|
+
escaped="$(printf '%s' "$version" | sed 's/[.[\*^$()+?{}|]/\\&/g')"
|
|
814
|
+
grep -Eq "(^| )v?${escaped}( |\$)" <<<"$version_line"
|
|
815
|
+
}
|
|
816
|
+
|
|
803
817
|
# Resolves the release tag GitHub's own "latest" redirect currently points
|
|
804
818
|
# at, without touching the api.github.com REST API (unauthenticated calls
|
|
805
819
|
# there are rate-limited per source IP; this fleet's shared egress IP already
|
|
@@ -813,65 +827,33 @@ resolve_latest_release_tag() {
|
|
|
813
827
|
basename "$location"
|
|
814
828
|
}
|
|
815
829
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
local release_dir archive_file extract_dir actual_sha256
|
|
836
|
-
release_dir="$(mktemp -d)"
|
|
837
|
-
trap 'rm -rf "$release_dir"' EXIT
|
|
838
|
-
archive_file="$release_dir/release.tar.gz"
|
|
839
|
-
extract_dir="$release_dir/extract"
|
|
840
|
-
mkdir -p "$extract_dir"
|
|
841
|
-
curl --proto '=https' --proto-redir '=https' -fsSL --retry 3 -o "$archive_file" "$url"
|
|
842
|
-
case "$(uname -s)" in
|
|
843
|
-
Darwin) actual_sha256="$(shasum -a 256 "$archive_file" | awk '{print $1}')" ;;
|
|
844
|
-
Linux) actual_sha256="$(sha256sum "$archive_file" | awk '{print $1}')" ;;
|
|
845
|
-
*) echo "unsupported operating system for release archive" >&2; exit 1 ;;
|
|
846
|
-
esac
|
|
847
|
-
[[ "$actual_sha256" == "$expected_sha256" ]] || {
|
|
848
|
-
echo "checksum mismatch for $package $version" >&2
|
|
849
|
-
exit 1
|
|
850
|
-
}
|
|
851
|
-
tar -xzf "$archive_file" -C "$extract_dir" "$archive_executable"
|
|
852
|
-
[[ -f "$extract_dir/$archive_executable" ]] || {
|
|
853
|
-
echo "archive executable missing: $archive_executable" >&2
|
|
854
|
-
exit 1
|
|
855
|
-
}
|
|
856
|
-
atomic_install_executable "$extract_dir/$archive_executable" "$target"
|
|
857
|
-
)
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
# Like run_release_archive, but never pins a version or checksum in the
|
|
861
|
-
# manifest: it always installs whatever GitHub's own "latest" alias currently
|
|
862
|
-
# serves, verified against the checksums file published *in that same
|
|
863
|
-
# release* (never a value carried over from a previous run or hardcoded
|
|
864
|
-
# here) -- fails loud, installs nothing, if that checksums asset is missing
|
|
865
|
-
# or doesn't list our asset. `.../releases/latest/download/<name>` is GitHub's
|
|
866
|
-
# own stable alias for "the matching asset of whatever release is latest
|
|
867
|
-
# right now", so the download itself never needs the resolved tag.
|
|
830
|
+
# A GitHub release archive, installed under /usr/local/bin, verified against
|
|
831
|
+
# a checksum published *in that same release* (never a value carried over
|
|
832
|
+
# from a previous run or hardcoded here) -- fails loud, installs nothing, if
|
|
833
|
+
# the checksums asset is missing or doesn't list our asset. It always
|
|
834
|
+
# installs whatever GitHub's own "latest" release was when install_one_tool
|
|
835
|
+
# resolved $tag (resolve_latest_release_tag), fetched with $tag pinned into
|
|
836
|
+
# BOTH download URLs -- `.../releases/download/<tag>/<name>`, never through
|
|
837
|
+
# GitHub's separate "always whatever is latest right now" alias for the
|
|
838
|
+
# downloads themselves. Fixed 2026-09-29 (review): that alias re-resolves on
|
|
839
|
+
# every request, so a release published between the tag resolve and the
|
|
840
|
+
# archive download would otherwise serve the checksums file from one release
|
|
841
|
+
# and the archive from a newer one -- silently mixing versions past the
|
|
842
|
+
# checksum check (which would still "pass" against whichever checksums file
|
|
843
|
+
# actually got fetched). Pinning both fetches to the one $tag closes that
|
|
844
|
+
# window.
|
|
845
|
+
# Tarun, 2026-09-29 (hard rule): versions are never pinned in the manifest --
|
|
846
|
+
# this replaced the old pinned run_release_archive (fixed version + url +
|
|
847
|
+
# sha256), which validate-manifest no longer accepts on any manager; $tag
|
|
848
|
+
# here is resolved fresh every run, never carried over or hardcoded.
|
|
868
849
|
run_release_archive_latest() {
|
|
869
850
|
local package="$1"
|
|
870
851
|
local repo="$2"
|
|
871
|
-
local
|
|
872
|
-
local
|
|
873
|
-
local
|
|
874
|
-
local
|
|
852
|
+
local tag="$3"
|
|
853
|
+
local asset="$4"
|
|
854
|
+
local checksums_asset="$5"
|
|
855
|
+
local archive_executable="$6"
|
|
856
|
+
local target="$7"
|
|
875
857
|
[[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
|
|
876
858
|
[[ "$target" == /usr/local/bin/* ]] || { echo "release archive target must be under /usr/local/bin: $target" >&2; exit 1; }
|
|
877
859
|
[[ "$archive_executable" != /* && "$archive_executable" != *..* ]] || {
|
|
@@ -888,13 +870,13 @@ run_release_archive_latest() {
|
|
|
888
870
|
extract_dir="$release_dir/extract"
|
|
889
871
|
mkdir -p "$extract_dir"
|
|
890
872
|
curl --proto '=https' --proto-redir '=https' -fsSL --retry 3 \
|
|
891
|
-
-o "$checksums_file" "https://github.com/$repo/releases/
|
|
873
|
+
-o "$checksums_file" "https://github.com/$repo/releases/download/$tag/$checksums_asset"
|
|
892
874
|
expected_sha256="$(awk -v want="$asset" '$2 == want { print $1; found = 1 } END { exit !found }' "$checksums_file")" || {
|
|
893
875
|
echo "no published checksum for $asset in $repo's $checksums_asset -- refusing to install unverified bytes" >&2
|
|
894
876
|
exit 1
|
|
895
877
|
}
|
|
896
878
|
curl --proto '=https' --proto-redir '=https' -fsSL --retry 3 \
|
|
897
|
-
-o "$archive_file" "https://github.com/$repo/releases/
|
|
879
|
+
-o "$archive_file" "https://github.com/$repo/releases/download/$tag/$asset"
|
|
898
880
|
case "$(uname -s)" in
|
|
899
881
|
Darwin) actual_sha256="$(shasum -a 256 "$archive_file" | awk '{print $1}')" ;;
|
|
900
882
|
Linux) actual_sha256="$(sha256sum "$archive_file" | awk '{print $1}')" ;;
|
|
@@ -944,7 +926,7 @@ resolve_surface_mode() {
|
|
|
944
926
|
# root") is a real environment fault, not a flaky install, and still
|
|
945
927
|
# aborts immediately -- `exit` is unconditional and is never caught here.
|
|
946
928
|
install_one_tool() {
|
|
947
|
-
local tool_id="$1" manager package executable previous_executable migrating_from_bun=0 rc=0
|
|
929
|
+
local tool_id="$1" manager package executable previous_executable migrating_from_bun=0 rc=0 npm_spec npm_allow_scripts npm_target_version npm_installed_ver brew_outdated_output brew_installed_version
|
|
948
930
|
manager="$(jq -r --arg id "$tool_id" '.[$id].manager' "$resolved_file")"
|
|
949
931
|
package="$(jq -r --arg id "$tool_id" '.[$id].package' "$resolved_file")"
|
|
950
932
|
executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$resolved_file")"
|
|
@@ -1000,15 +982,14 @@ install_one_tool() {
|
|
|
1000
982
|
run_apt install -y "$package" || rc=1
|
|
1001
983
|
;;
|
|
1002
984
|
npm)
|
|
1003
|
-
#
|
|
1004
|
-
#
|
|
1005
|
-
#
|
|
985
|
+
# Tarun, 2026-09-29 (hard rule): versions are never pinned -- every
|
|
986
|
+
# fleet tool installs latest stable. npm entries never carry a
|
|
987
|
+
# .version (validate-manifest rejects one outright); $npm_spec stays a
|
|
988
|
+
# separate variable from $package only because the exact run_npm
|
|
989
|
+
# invocation below is asserted at the source level. Ownership
|
|
1006
990
|
# verification below still uses the bare package name: npm's global
|
|
1007
|
-
# tree and package.json are keyed by package, never by
|
|
1008
|
-
# version spec.
|
|
1009
|
-
npm_version="$(jq -r --arg id "$tool_id" '.[$id].version // empty' "$resolved_file")"
|
|
991
|
+
# tree and package.json are keyed by package, never by a version spec.
|
|
1010
992
|
npm_spec="$package"
|
|
1011
|
-
[[ -z "$npm_version" ]] || npm_spec="$package@$npm_version"
|
|
1012
993
|
# Only @amsterdamdatalabs/enact-docs-search's manifest entry carries
|
|
1013
994
|
# this (docs/operations/global-tools.md Topic 2, decided 2026-09-28):
|
|
1014
995
|
# its own dependencies' install scripts, explicitly reviewed and
|
|
@@ -1019,41 +1000,20 @@ install_one_tool() {
|
|
|
1019
1000
|
previous_executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$previous_resolved_file")"
|
|
1020
1001
|
echo "would remove retired Bun compilation before npm install: $tool_id ($previous_executable and $(bun_source_hash_file "$tool_id"))"
|
|
1021
1002
|
fi
|
|
1022
|
-
#
|
|
1023
|
-
#
|
|
1024
|
-
#
|
|
1025
|
-
#
|
|
1026
|
-
#
|
|
1027
|
-
|
|
1028
|
-
# conservative message instead of guessing. Either way, only query
|
|
1029
|
-
# when it needs no new sudo privilege (can_query_as_administrator) --
|
|
1030
|
-
# a preview run must not demand a password a different device's real
|
|
1031
|
-
# administrator would need.
|
|
1032
|
-
if [[ -n "$npm_version" ]] && can_query_as_administrator; then
|
|
1033
|
-
if npm_plan_installed="$(npm_installed_version "$package")"; then
|
|
1034
|
-
if [[ -n "$npm_plan_installed" && "$npm_plan_installed" == "$npm_version" ]]; then
|
|
1035
|
-
echo "already latest: $tool_id (npm:$package@$npm_plan_installed)"
|
|
1036
|
-
else
|
|
1037
|
-
echo "would install or upgrade npm package: $npm_spec"
|
|
1038
|
-
fi
|
|
1039
|
-
else
|
|
1040
|
-
echo "would install or upgrade npm package: $npm_spec (installed version not checked: npm ls -g failed)"
|
|
1041
|
-
fi
|
|
1042
|
-
else
|
|
1043
|
-
echo "would install or upgrade npm package: $npm_spec"
|
|
1044
|
-
fi
|
|
1003
|
+
# "latest stable" is not knowable without a per-package registry
|
|
1004
|
+
# round trip (npm_latest_version) -- doing that here would make a
|
|
1005
|
+
# plan preview slow and network-bound for every npm tool on the
|
|
1006
|
+
# device, so plan mode always prints this unconditional message
|
|
1007
|
+
# instead of guessing.
|
|
1008
|
+
echo "would install or upgrade npm package: $npm_spec"
|
|
1045
1009
|
else
|
|
1046
1010
|
if is_in_place_bun_to_npm_migration "$tool_id"; then
|
|
1047
1011
|
migrating_from_bun=1
|
|
1048
1012
|
prepare_in_place_bun_to_npm_migration "$tool_id" || rc=1
|
|
1049
1013
|
fi
|
|
1050
|
-
if [[ "$rc" -eq 0 ]]; then
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
elif ! npm_target_version="$(npm_latest_version "$package")"; then
|
|
1054
|
-
echo "$tool_id: npm view could not resolve the latest version of $package" >&2
|
|
1055
|
-
rc=1
|
|
1056
|
-
fi
|
|
1014
|
+
if [[ "$rc" -eq 0 ]] && ! npm_target_version="$(npm_latest_version "$package")"; then
|
|
1015
|
+
echo "$tool_id: npm view could not resolve the latest version of $package" >&2
|
|
1016
|
+
rc=1
|
|
1057
1017
|
fi
|
|
1058
1018
|
if [[ "$rc" -eq 0 ]]; then
|
|
1059
1019
|
if ! npm_installed_ver="$(npm_installed_version "$package")"; then
|
|
@@ -1081,21 +1041,8 @@ install_one_tool() {
|
|
|
1081
1041
|
fi
|
|
1082
1042
|
[[ "$rc" -eq 0 ]] && { ensure_global_pip_executable "$executable" || rc=1; }
|
|
1083
1043
|
;;
|
|
1084
|
-
release-archive)
|
|
1085
|
-
local version url sha256 archive_executable
|
|
1086
|
-
version="$(jq -r --arg id "$tool_id" '.[$id].version' "$resolved_file")"
|
|
1087
|
-
url="$(jq -r --arg id "$tool_id" '.[$id].url' "$resolved_file")"
|
|
1088
|
-
sha256="$(jq -r --arg id "$tool_id" '.[$id].sha256' "$resolved_file")"
|
|
1089
|
-
archive_executable="$(jq -r --arg id "$tool_id" '.[$id].archive_executable' "$resolved_file")"
|
|
1090
|
-
if [[ "$mode" == apply && -x "$executable" ]] &&
|
|
1091
|
-
"$executable" version 2>/dev/null | head -n 1 | grep -Fq "v$version"; then
|
|
1092
|
-
echo "pinned release already installed: $package $version"
|
|
1093
|
-
else
|
|
1094
|
-
run_release_archive "$package" "$version" "$url" "$sha256" "$archive_executable" "$executable" || rc=1
|
|
1095
|
-
fi
|
|
1096
|
-
;;
|
|
1097
1044
|
release-archive-latest)
|
|
1098
|
-
local repo asset checksums_asset archive_executable latest_tag latest_version
|
|
1045
|
+
local repo asset checksums_asset archive_executable latest_tag latest_version resolved_asset resolved_checksums_asset
|
|
1099
1046
|
repo="$(jq -r --arg id "$tool_id" '.[$id].repo' "$resolved_file")"
|
|
1100
1047
|
asset="$(jq -r --arg id "$tool_id" '.[$id].asset' "$resolved_file")"
|
|
1101
1048
|
checksums_asset="$(jq -r --arg id "$tool_id" '.[$id].checksums_asset // "SHA256SUMS"' "$resolved_file")"
|
|
@@ -1106,10 +1053,17 @@ install_one_tool() {
|
|
|
1106
1053
|
latest_tag="$(resolve_latest_release_tag "$repo")" || { rc=1; latest_tag=""; }
|
|
1107
1054
|
if [[ "$rc" -eq 0 ]]; then
|
|
1108
1055
|
latest_version="${latest_tag#v}"
|
|
1109
|
-
|
|
1056
|
+
# A release whose asset filename bakes in the version (agent-vault:
|
|
1057
|
+
# agent-vault_<ver>_darwin_arm64.tar.gz) declares "{version}" in
|
|
1058
|
+
# .asset/.checksums_asset; lean-ctx's own asset names carry no
|
|
1059
|
+
# version at all, so this is a no-op substring replace for it.
|
|
1060
|
+
resolved_asset="${asset//\{version\}/$latest_version}"
|
|
1061
|
+
resolved_checksums_asset="${checksums_asset//\{version\}/$latest_version}"
|
|
1062
|
+
if [[ -x "$executable" ]] &&
|
|
1063
|
+
release_archive_version_matches "$("$executable" --version 2>/dev/null | head -n 1)" "$latest_version"; then
|
|
1110
1064
|
echo "latest release already installed: $package $latest_version"
|
|
1111
1065
|
else
|
|
1112
|
-
run_release_archive_latest "$package" "$repo" "$
|
|
1066
|
+
run_release_archive_latest "$package" "$repo" "$latest_tag" "$resolved_asset" "$resolved_checksums_asset" "$archive_executable" "$executable" || rc=1
|
|
1113
1067
|
fi
|
|
1114
1068
|
fi
|
|
1115
1069
|
fi
|
|
@@ -1654,6 +1608,212 @@ remove_one_repo_dir() {
|
|
|
1654
1608
|
rm -rf -- "$target"
|
|
1655
1609
|
}
|
|
1656
1610
|
|
|
1611
|
+
# Lexical-only path normalization: collapses repeated slashes ("//" ->
|
|
1612
|
+
# "/") and current-dir segments ("/./" -> "/"), and drops one trailing
|
|
1613
|
+
# slash. Never resolves ".." or symlinks, and never touches the filesystem
|
|
1614
|
+
# -- the target frequently doesn't exist yet when this runs. Second review
|
|
1615
|
+
# pass, 2026-09-29: without this, "/usr/local/bin//agent-vault" and
|
|
1616
|
+
# "/usr/local/bin/agent-vault" compare unequal even though they name the
|
|
1617
|
+
# same real file, silently defeating executable_claimed_by_current_manifest
|
|
1618
|
+
# below (the guard would say "not claimed" and let the old manager's `rm -f`
|
|
1619
|
+
# through) -- previous_resolved_file is never validated, so a malformed
|
|
1620
|
+
# path there is exactly the case this guard exists to protect against.
|
|
1621
|
+
normalize_manifest_path() {
|
|
1622
|
+
local path="$1"
|
|
1623
|
+
while [[ "$path" == *//* ]]; do path="${path//\/\//\/}"; done
|
|
1624
|
+
while [[ "$path" == */./* ]]; do path="${path//\/.\//\/}"; done
|
|
1625
|
+
path="${path%/}"
|
|
1626
|
+
printf '%s\n' "$path"
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
# A previous entry's executable path can be EXACTLY the path a CURRENT
|
|
1630
|
+
# entry now owns -- e.g. agent-vault-cli migrating release-archive ->
|
|
1631
|
+
# release-archive-latest, both /usr/local/bin/agent-vault. Bug found in
|
|
1632
|
+
# review, 2026-09-29: the install loop runs BEFORE the removal loop below
|
|
1633
|
+
# and already wrote/verified that binary; "retain shared package after
|
|
1634
|
+
# entry deletion" only skips removal when manager+package are BOTH
|
|
1635
|
+
# unchanged, so a manager-only change (exactly this migration) still
|
|
1636
|
+
# selects the id for removal, and the OLD manager's case arm then does a
|
|
1637
|
+
# plain `rm -f` on the path the install pass upstream just installed.
|
|
1638
|
+
# Checked by executable path, not by tool_id/manager/package, because that
|
|
1639
|
+
# is the one thing the filesystem actually cares about -- both sides
|
|
1640
|
+
# normalized first (see normalize_manifest_path) so a malformed legacy path
|
|
1641
|
+
# in previous_resolved_file still compares equal to a clean one.
|
|
1642
|
+
#
|
|
1643
|
+
# Second review pass, 2026-09-29 (bare-name executables): most managers
|
|
1644
|
+
# store an ABSOLUTE path in .executable (release-archive-latest, shim,
|
|
1645
|
+
# bun-compile, repo-script all require the /usr/local/bin/ prefix), but pip,
|
|
1646
|
+
# npm, and brew store a bare command name instead -- pip's own
|
|
1647
|
+
# ensure_global_pip_executable/remove_global_pip_executable ALWAYS place (or
|
|
1648
|
+
# remove) that name as a fixed /usr/local/bin/<name> symlink regardless of
|
|
1649
|
+
# the pip package name, so a bare pip .executable maps there and IS
|
|
1650
|
+
# compared. npm and brew do not get the same treatment: their real install
|
|
1651
|
+
# location depends on the device's own npm global prefix / Homebrew prefix,
|
|
1652
|
+
# resolved only through a live `command -v` PATH search
|
|
1653
|
+
# (npm_global_executable_path) that varies by platform -- never a single
|
|
1654
|
+
# fixed path this function can compute without shelling out. A migration
|
|
1655
|
+
# INTO an npm or brew entry is not detected here and the old manager's real
|
|
1656
|
+
# removal proceeds exactly as before this fix. That gap is narrower than it
|
|
1657
|
+
# looks: npm and brew each remove through their OWN package manager (`npm
|
|
1658
|
+
# uninstall -g`, `brew uninstall`), never the raw `rm -f` this guard
|
|
1659
|
+
# protects, so nothing here relies on rm to leave an npm/brew-owned file
|
|
1660
|
+
# alone in the first place.
|
|
1661
|
+
executable_claimed_by_current_manifest() {
|
|
1662
|
+
local target="$1" normalized_target normalized_current current_manager current_executable
|
|
1663
|
+
normalized_target="$(normalize_manifest_path "$target")"
|
|
1664
|
+
while IFS=$'\t' read -r current_manager current_executable; do
|
|
1665
|
+
[[ -n "$current_executable" ]] || continue
|
|
1666
|
+
if [[ "$current_executable" == /* ]]; then
|
|
1667
|
+
normalized_current="$(normalize_manifest_path "$current_executable")"
|
|
1668
|
+
elif [[ "$current_manager" == pip ]]; then
|
|
1669
|
+
normalized_current="$(normalize_manifest_path "/usr/local/bin/$current_executable")"
|
|
1670
|
+
else
|
|
1671
|
+
continue
|
|
1672
|
+
fi
|
|
1673
|
+
[[ "$normalized_current" == "$normalized_target" ]] && return 0
|
|
1674
|
+
done < <(jq -r '.[] | [.manager, .executable] | @tsv' "$resolved_file")
|
|
1675
|
+
return 1
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
# Uninstalls a single previously-applied tool the CURRENT manifest no longer
|
|
1679
|
+
# declares (or has migrated to a different manager/package) -- extracted
|
|
1680
|
+
# into its own function, like install_one_tool, so
|
|
1681
|
+
# packages/fleet/tests/test-manifest.sh can exercise it directly through the
|
|
1682
|
+
# RECONCILE_SOURCE_ONLY seam instead of only through a real apply run.
|
|
1683
|
+
remove_one_manifest_entry() {
|
|
1684
|
+
local tool_id="$1" manager package executable removed_repo_dir_target
|
|
1685
|
+
if jq -e --arg id "$tool_id" '.config.protected_tools | index($id)' "$manifest" >/dev/null; then
|
|
1686
|
+
echo "refusing to remove protected bootstrap tool: $tool_id" >&2
|
|
1687
|
+
exit 1
|
|
1688
|
+
fi
|
|
1689
|
+
manager="$(jq -r --arg id "$tool_id" '.[$id].manager' "$previous_resolved_file")"
|
|
1690
|
+
package="$(jq -r --arg id "$tool_id" '.[$id].package' "$previous_resolved_file")"
|
|
1691
|
+
executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$previous_resolved_file")"
|
|
1692
|
+
if is_in_place_bun_to_npm_migration "$tool_id"; then
|
|
1693
|
+
if [[ "$mode" == apply ]]; then
|
|
1694
|
+
echo "retired Bun compilation already removed for npm migration: $tool_id"
|
|
1695
|
+
fi
|
|
1696
|
+
return 0
|
|
1697
|
+
fi
|
|
1698
|
+
if jq -e --arg manager "$manager" --arg package "$package" 'to_entries | any(.value.manager == $manager and .value.package == $package)' "$resolved_file" >/dev/null; then
|
|
1699
|
+
echo "retain shared package after entry deletion: $manager:$package"
|
|
1700
|
+
return 0
|
|
1701
|
+
fi
|
|
1702
|
+
# Truthfulness fix (second review pass, 2026-09-29): for the managers that
|
|
1703
|
+
# remove via a raw filesystem `rm -f` on a fixed path (never their own
|
|
1704
|
+
# package manager's uninstall command), check executable_claimed_by_
|
|
1705
|
+
# current_manifest BEFORE claiming an "uninstall" happened at all. The
|
|
1706
|
+
# log line printed below this case is the only outcome a human or another
|
|
1707
|
+
# tool sees; printing "explicit manifest deletion: uninstall ..." and only
|
|
1708
|
+
# THEN discovering the file is retained (as this used to) makes the log
|
|
1709
|
+
# lie about what just happened, exactly backwards from "log the decision
|
|
1710
|
+
# as the outcome."
|
|
1711
|
+
case "$manager" in
|
|
1712
|
+
release-archive | release-archive-latest | shim | bun-compile)
|
|
1713
|
+
if executable_claimed_by_current_manifest "$executable"; then
|
|
1714
|
+
echo "retain $tool_id ($manager:$package): $executable is still declared by the current manifest"
|
|
1715
|
+
return 0
|
|
1716
|
+
fi
|
|
1717
|
+
;;
|
|
1718
|
+
esac
|
|
1719
|
+
echo "explicit manifest deletion: uninstall $tool_id ($manager:$package)"
|
|
1720
|
+
case "$manager" in
|
|
1721
|
+
brew) run_brew uninstall "$package" ;;
|
|
1722
|
+
apt) run_apt remove -y "$package" ;;
|
|
1723
|
+
npm) run_npm uninstall -g "$package" ;;
|
|
1724
|
+
pip)
|
|
1725
|
+
run_pip uninstall --break-system-packages -y "$package"
|
|
1726
|
+
remove_global_pip_executable "$executable"
|
|
1727
|
+
;;
|
|
1728
|
+
# "release-archive" (pinned version+url+sha256) is retired from
|
|
1729
|
+
# validate-manifest's accepted managers (Tarun, 2026-09-29: versions
|
|
1730
|
+
# are never pinned) -- no CURRENT manifest.json can declare one, so
|
|
1731
|
+
# install_one_tool no longer has a case for it. This removal arm stays
|
|
1732
|
+
# so a device whose applied-manifest.json still remembers an old
|
|
1733
|
+
# release-archive entry (agent-vault-cli, before this change) can
|
|
1734
|
+
# still cleanly uninstall it the next time it reconciles; previous
|
|
1735
|
+
# state is read straight from disk, never re-validated.
|
|
1736
|
+
release-archive)
|
|
1737
|
+
[[ "$executable" == /usr/local/bin/* ]] || {
|
|
1738
|
+
echo "refusing unsafe release archive removal target: $executable" >&2
|
|
1739
|
+
exit 1
|
|
1740
|
+
}
|
|
1741
|
+
if [[ "$mode" == apply ]]; then
|
|
1742
|
+
rm -f -- "$executable"
|
|
1743
|
+
else
|
|
1744
|
+
echo "would remove pinned release executable: $executable"
|
|
1745
|
+
fi
|
|
1746
|
+
;;
|
|
1747
|
+
release-archive-latest)
|
|
1748
|
+
[[ "$executable" == /usr/local/bin/* ]] || {
|
|
1749
|
+
echo "refusing unsafe release archive removal target: $executable" >&2
|
|
1750
|
+
exit 1
|
|
1751
|
+
}
|
|
1752
|
+
if [[ "$mode" == apply ]]; then
|
|
1753
|
+
rm -f -- "$executable"
|
|
1754
|
+
else
|
|
1755
|
+
echo "would remove latest release executable: $executable"
|
|
1756
|
+
fi
|
|
1757
|
+
;;
|
|
1758
|
+
shim)
|
|
1759
|
+
[[ "$executable" == /usr/local/bin/* ]] || {
|
|
1760
|
+
echo "refusing unsafe shim removal target: $executable" >&2
|
|
1761
|
+
exit 1
|
|
1762
|
+
}
|
|
1763
|
+
if [[ "$mode" == apply ]]; then
|
|
1764
|
+
rm -f -- "$executable"
|
|
1765
|
+
else
|
|
1766
|
+
echo "would remove shim: $executable"
|
|
1767
|
+
fi
|
|
1768
|
+
;;
|
|
1769
|
+
repo-script)
|
|
1770
|
+
if [[ "$mode" == apply ]]; then
|
|
1771
|
+
remove_one_repo_script "$executable"
|
|
1772
|
+
else
|
|
1773
|
+
[[ "$executable" =~ ^/usr/local/bin/[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
|
|
1774
|
+
echo "refusing unsafe managed script removal target: $executable" >&2
|
|
1775
|
+
exit 1
|
|
1776
|
+
}
|
|
1777
|
+
echo "would remove managed repo script: $executable"
|
|
1778
|
+
fi
|
|
1779
|
+
;;
|
|
1780
|
+
repo-dir)
|
|
1781
|
+
removed_repo_dir_target="$(jq -r --arg id "$tool_id" '.[$id].target' "$previous_resolved_file")"
|
|
1782
|
+
if [[ "$mode" == apply ]]; then
|
|
1783
|
+
remove_one_repo_dir "$removed_repo_dir_target"
|
|
1784
|
+
else
|
|
1785
|
+
[[ "$removed_repo_dir_target" == "$repo_dir_libexec_root"/* ]] || {
|
|
1786
|
+
echo "refusing unsafe repo-dir removal target: $removed_repo_dir_target" >&2
|
|
1787
|
+
exit 1
|
|
1788
|
+
}
|
|
1789
|
+
echo "would remove repo dir: $removed_repo_dir_target"
|
|
1790
|
+
fi
|
|
1791
|
+
;;
|
|
1792
|
+
bun-compile)
|
|
1793
|
+
[[ "$executable" == /usr/local/bin/* ]] || {
|
|
1794
|
+
echo "refusing unsafe bun binary removal target: $executable" >&2
|
|
1795
|
+
exit 1
|
|
1796
|
+
}
|
|
1797
|
+
if [[ "$mode" == apply ]]; then
|
|
1798
|
+
rm -f -- "$executable"
|
|
1799
|
+
else
|
|
1800
|
+
echo "would remove bun binary: $executable"
|
|
1801
|
+
fi
|
|
1802
|
+
;;
|
|
1803
|
+
embedding-model)
|
|
1804
|
+
if [[ "$mode" == apply ]]; then
|
|
1805
|
+
[[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
|
|
1806
|
+
command -v bun >/dev/null 2>&1 || { echo "embedding-model requires bun; ensure bun installs before this tool" >&2; exit 1; }
|
|
1807
|
+
(cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
|
|
1808
|
+
bun "$repo_root/packages/fleet/src/install-embedding-model.ts" uninstall </dev/null)
|
|
1809
|
+
else
|
|
1810
|
+
echo "would remove embedding model cache: ~/.cache/adl-embedding/model (as $administrator)"
|
|
1811
|
+
fi
|
|
1812
|
+
;;
|
|
1813
|
+
*) echo "unsupported manager in previous manifest: $manager" >&2; exit 1 ;;
|
|
1814
|
+
esac
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1657
1817
|
# config.git_global_config: global git config for the administrator (the
|
|
1658
1818
|
# account whose HOME the per-user deployers below also write to). Apply runs git as
|
|
1659
1819
|
# that user (sudo -u -H) so ~/.gitconfig stays user-owned; plan/audit only
|
|
@@ -1825,6 +1985,37 @@ run_surface_update() {
|
|
|
1825
1985
|
sudo -u "$administrator" -H env PATH="$surface_cli_search_path" "$resolved" "$@" </dev/null
|
|
1826
1986
|
}
|
|
1827
1987
|
|
|
1988
|
+
# Extracts an update command's own reported failure reason from its
|
|
1989
|
+
# captured output, or prints nothing (empty) if none is found. Confirmed
|
|
1990
|
+
# live for kimi on strato and hostinger-vps-1 (2026-09-30): `kimi upgrade`
|
|
1991
|
+
# printed "error: failed to check for updates: CDN /latest returned HTTP
|
|
1992
|
+
# 521" and still exited 0. Broadened by independent review the same day:
|
|
1993
|
+
# optional leading whitespace before the marker, ANSI color codes stripped
|
|
1994
|
+
# first (real CLIs colorize their own output), and any of
|
|
1995
|
+
# "error:"/"Error:"/"ERROR:"/"✖ error:" -- case-insensitive by
|
|
1996
|
+
# construction (POSIX bracket character classes, not a sed `-I`/`I` flag,
|
|
1997
|
+
# which BSD sed does not support). Applied identically to every surface's
|
|
1998
|
+
# update path, native/npm and Homebrew-cask alike, rather than one
|
|
1999
|
+
# per-surface parser.
|
|
2000
|
+
#
|
|
2001
|
+
# Never `cmd1 | cmd2` piped straight into the caller's assignment: see the
|
|
2002
|
+
# inner `if var=$(grep ...); then` below (not `grep | sed`) for why -- under
|
|
2003
|
+
# this file's `set -euo pipefail`, grep's ordinary "no match" exit would
|
|
2004
|
+
# otherwise abort the run on every surface that DIDN'T report an error. An
|
|
2005
|
+
# `if`-tested command is exempt from that abort by design; the function's
|
|
2006
|
+
# OWN exit status is therefore never meaningful (an `if` with no `else`
|
|
2007
|
+
# always returns 0), so callers check the printed string's emptiness, never
|
|
2008
|
+
# this function's exit status.
|
|
2009
|
+
surface_update_error_reason() {
|
|
2010
|
+
local output="$1" plain line
|
|
2011
|
+
# LC_ALL=C: the strip is bytewise. Under en_US.UTF-8 glibc resolves [@-~]
|
|
2012
|
+
# by collation, so the escape's final letter would never match.
|
|
2013
|
+
plain="$(LC_ALL=C sed -E $'s/\x1b\\[[0-9;]*[@-~]//g' <<<"$output")"
|
|
2014
|
+
if line="$(grep -m1 -iE '^[[:space:]]*(✖[[:space:]]*)?error:' <<<"$plain")"; then
|
|
2015
|
+
sed -E 's/^[[:space:]]*(✖[[:space:]]*)?[eE][rR][rR][oO][rR]:[[:space:]]*//' <<<"$line"
|
|
2016
|
+
fi
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1828
2019
|
# Reconciles one declared surface_clis entry. report_only (hostinger's loose
|
|
1829
2020
|
# /root/codex, unknown install method) is version-reported only -- run_surface_update
|
|
1830
2021
|
# is never called for it, by construction (this branch returns before
|
|
@@ -1863,6 +2054,26 @@ reconcile_one_surface_cli() {
|
|
|
1863
2054
|
echo "surface update failed: $name (exit $update_status)" >&2
|
|
1864
2055
|
return 1
|
|
1865
2056
|
fi
|
|
2057
|
+
# Same broadened check as the native/npm path below (independent
|
|
2058
|
+
# review, 2026-09-30): brew itself can exit 0 while its own output
|
|
2059
|
+
# reports a real problem. No install/upgrade claim without checking
|
|
2060
|
+
# for one first -- Homebrew has no version comparison in this branch
|
|
2061
|
+
# (it acts on cask PRESENCE, not a read version), so there is no
|
|
2062
|
+
# "version changed" success case to weigh it against here; any error
|
|
2063
|
+
# line found is a hard failure.
|
|
2064
|
+
local update_error_line
|
|
2065
|
+
update_error_line="$(surface_update_error_reason "$update_output")"
|
|
2066
|
+
if [[ -n "$update_error_line" ]]; then
|
|
2067
|
+
# Install vs upgrade distinguished in the failure wording too (second
|
|
2068
|
+
# coordinator pass, 2026-09-30): the same `cask_action[0]` that already
|
|
2069
|
+
# picks the real brew argv above also names which one failed.
|
|
2070
|
+
if [[ "${cask_action[0]}" == install ]]; then
|
|
2071
|
+
echo "surface $name: cask install failed ($update_error_line)" >&2
|
|
2072
|
+
else
|
|
2073
|
+
echo "surface $name: cask upgrade failed ($update_error_line)" >&2
|
|
2074
|
+
fi
|
|
2075
|
+
return 1
|
|
2076
|
+
fi
|
|
1866
2077
|
if [[ "${cask_action[0]}" == install ]]; then
|
|
1867
2078
|
echo "surface $name: Homebrew cask installed"
|
|
1868
2079
|
else
|
|
@@ -1891,7 +2102,7 @@ reconcile_one_surface_cli() {
|
|
|
1891
2102
|
echo "surface $name: report-only, currently ${ver:-unknown}"
|
|
1892
2103
|
return 0
|
|
1893
2104
|
fi
|
|
1894
|
-
local old_version new_version update_output update_status=0
|
|
2105
|
+
local old_version new_version update_output update_status=0 update_error_line
|
|
1895
2106
|
old_version="$(surface_cli_version "$resolved")"
|
|
1896
2107
|
update_output="$(run_with_timeout "$SURFACE_UPDATE_TIMEOUT_SECONDS" run_surface_update "$resolved" ${update_args[@]+"${update_args[@]}"} 2>&1)" || update_status=$?
|
|
1897
2108
|
printf '%s\n' "$update_output"
|
|
@@ -1902,12 +2113,48 @@ reconcile_one_surface_cli() {
|
|
|
1902
2113
|
echo "surface update failed: $name (exit $update_status)" >&2
|
|
1903
2114
|
return 1
|
|
1904
2115
|
fi
|
|
2116
|
+
# A surface's update command can exit 0 while its own output says the
|
|
2117
|
+
# check itself failed -- confirmed live for kimi (strato + hostinger,
|
|
2118
|
+
# 2026-09-30): `kimi upgrade` printed
|
|
2119
|
+
# "error: failed to check for updates: CDN /latest returned HTTP 521" and
|
|
2120
|
+
# still exited 0 (AGENTS.md: no silent fallback). Read new_version BEFORE
|
|
2121
|
+
# judging anything (independent review, 2026-09-30): a version CHANGE is
|
|
2122
|
+
# proof positive the update happened, which outranks an error line the
|
|
2123
|
+
# command also printed (e.g. a flaky post-install self-check after a
|
|
2124
|
+
# real, successful install) -- reported as a note on the success line,
|
|
2125
|
+
# never a failure. Only an UNCHANGED version with an error line is the
|
|
2126
|
+
# case this whole check exists for: the command claimed nothing changed,
|
|
2127
|
+
# and said why, and that is reported as a failure -- wording "update
|
|
2128
|
+
# check failed", not "could not check for updates", since the reported
|
|
2129
|
+
# reason may not even be about the check itself (kept generic rather than
|
|
2130
|
+
# asserting which).
|
|
2131
|
+
#
|
|
2132
|
+
# An EMPTY new_version (second coordinator pass, 2026-09-30: the binary
|
|
2133
|
+
# broke, or --version now produces no output) must never count as
|
|
2134
|
+
# "updated" just because empty differs from a non-empty old_version --
|
|
2135
|
+
# that used to read as "surface X: 1.2.3 -> unknown", an arrow-style
|
|
2136
|
+
# SUCCESS line for a version we cannot actually confirm. Checked before
|
|
2137
|
+
# the "changed" branch below, so only a REAL, non-empty, different
|
|
2138
|
+
# version ever reaches it.
|
|
1905
2139
|
new_version="$(surface_cli_version "$resolved")"
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2140
|
+
update_error_line="$(surface_update_error_reason "$update_output")"
|
|
2141
|
+
if [[ -z "$new_version" ]]; then
|
|
2142
|
+
echo "surface $name: update ran but version unreadable (${update_error_line:-"--version produced no output"}); was ${old_version:-unknown}" >&2
|
|
2143
|
+
return 1
|
|
1910
2144
|
fi
|
|
2145
|
+
if [[ "$old_version" != "$new_version" ]]; then
|
|
2146
|
+
if [[ -n "$update_error_line" ]]; then
|
|
2147
|
+
echo "surface $name: updated ${old_version:-unknown} -> $new_version (reported: $update_error_line)"
|
|
2148
|
+
else
|
|
2149
|
+
echo "surface $name: ${old_version:-unknown} -> $new_version"
|
|
2150
|
+
fi
|
|
2151
|
+
return 0
|
|
2152
|
+
fi
|
|
2153
|
+
if [[ -n "$update_error_line" ]]; then
|
|
2154
|
+
echo "surface $name: update check failed ($update_error_line); kept ${old_version:-unknown}" >&2
|
|
2155
|
+
return 1
|
|
2156
|
+
fi
|
|
2157
|
+
echo "surface $name: already latest ($new_version)"
|
|
1911
2158
|
return 0
|
|
1912
2159
|
}
|
|
1913
2160
|
|
|
@@ -2196,111 +2443,7 @@ reconcile_surface_clis
|
|
|
2196
2443
|
if [[ "$have_previous" -eq 1 ]]; then
|
|
2197
2444
|
while IFS= read -r tool_id; do
|
|
2198
2445
|
[[ -n "$tool_id" ]] || continue
|
|
2199
|
-
|
|
2200
|
-
echo "refusing to remove protected bootstrap tool: $tool_id" >&2
|
|
2201
|
-
exit 1
|
|
2202
|
-
fi
|
|
2203
|
-
manager="$(jq -r --arg id "$tool_id" '.[$id].manager' "$previous_resolved_file")"
|
|
2204
|
-
package="$(jq -r --arg id "$tool_id" '.[$id].package' "$previous_resolved_file")"
|
|
2205
|
-
executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$previous_resolved_file")"
|
|
2206
|
-
if is_in_place_bun_to_npm_migration "$tool_id"; then
|
|
2207
|
-
if [[ "$mode" == apply ]]; then
|
|
2208
|
-
echo "retired Bun compilation already removed for npm migration: $tool_id"
|
|
2209
|
-
fi
|
|
2210
|
-
continue
|
|
2211
|
-
fi
|
|
2212
|
-
if jq -e --arg manager "$manager" --arg package "$package" 'to_entries | any(.value.manager == $manager and .value.package == $package)' "$resolved_file" >/dev/null; then
|
|
2213
|
-
echo "retain shared package after entry deletion: $manager:$package"
|
|
2214
|
-
continue
|
|
2215
|
-
fi
|
|
2216
|
-
echo "explicit manifest deletion: uninstall $tool_id ($manager:$package)"
|
|
2217
|
-
case "$manager" in
|
|
2218
|
-
brew) run_brew uninstall "$package" ;;
|
|
2219
|
-
apt) run_apt remove -y "$package" ;;
|
|
2220
|
-
npm) run_npm uninstall -g "$package" ;;
|
|
2221
|
-
pip)
|
|
2222
|
-
run_pip uninstall --break-system-packages -y "$package"
|
|
2223
|
-
remove_global_pip_executable "$executable"
|
|
2224
|
-
;;
|
|
2225
|
-
release-archive)
|
|
2226
|
-
[[ "$executable" == /usr/local/bin/* ]] || {
|
|
2227
|
-
echo "refusing unsafe release archive removal target: $executable" >&2
|
|
2228
|
-
exit 1
|
|
2229
|
-
}
|
|
2230
|
-
if [[ "$mode" == apply ]]; then
|
|
2231
|
-
rm -f -- "$executable"
|
|
2232
|
-
else
|
|
2233
|
-
echo "would remove pinned release executable: $executable"
|
|
2234
|
-
fi
|
|
2235
|
-
;;
|
|
2236
|
-
release-archive-latest)
|
|
2237
|
-
[[ "$executable" == /usr/local/bin/* ]] || {
|
|
2238
|
-
echo "refusing unsafe release archive removal target: $executable" >&2
|
|
2239
|
-
exit 1
|
|
2240
|
-
}
|
|
2241
|
-
if [[ "$mode" == apply ]]; then
|
|
2242
|
-
rm -f -- "$executable"
|
|
2243
|
-
else
|
|
2244
|
-
echo "would remove latest release executable: $executable"
|
|
2245
|
-
fi
|
|
2246
|
-
;;
|
|
2247
|
-
shim)
|
|
2248
|
-
[[ "$executable" == /usr/local/bin/* ]] || {
|
|
2249
|
-
echo "refusing unsafe shim removal target: $executable" >&2
|
|
2250
|
-
exit 1
|
|
2251
|
-
}
|
|
2252
|
-
if [[ "$mode" == apply ]]; then
|
|
2253
|
-
rm -f -- "$executable"
|
|
2254
|
-
else
|
|
2255
|
-
echo "would remove shim: $executable"
|
|
2256
|
-
fi
|
|
2257
|
-
;;
|
|
2258
|
-
repo-script)
|
|
2259
|
-
if [[ "$mode" == apply ]]; then
|
|
2260
|
-
remove_one_repo_script "$executable"
|
|
2261
|
-
else
|
|
2262
|
-
[[ "$executable" =~ ^/usr/local/bin/[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
|
|
2263
|
-
echo "refusing unsafe managed script removal target: $executable" >&2
|
|
2264
|
-
exit 1
|
|
2265
|
-
}
|
|
2266
|
-
echo "would remove managed repo script: $executable"
|
|
2267
|
-
fi
|
|
2268
|
-
;;
|
|
2269
|
-
repo-dir)
|
|
2270
|
-
removed_repo_dir_target="$(jq -r --arg id "$tool_id" '.[$id].target' "$previous_resolved_file")"
|
|
2271
|
-
if [[ "$mode" == apply ]]; then
|
|
2272
|
-
remove_one_repo_dir "$removed_repo_dir_target"
|
|
2273
|
-
else
|
|
2274
|
-
[[ "$removed_repo_dir_target" == "$repo_dir_libexec_root"/* ]] || {
|
|
2275
|
-
echo "refusing unsafe repo-dir removal target: $removed_repo_dir_target" >&2
|
|
2276
|
-
exit 1
|
|
2277
|
-
}
|
|
2278
|
-
echo "would remove repo dir: $removed_repo_dir_target"
|
|
2279
|
-
fi
|
|
2280
|
-
;;
|
|
2281
|
-
bun-compile)
|
|
2282
|
-
[[ "$executable" == /usr/local/bin/* ]] || {
|
|
2283
|
-
echo "refusing unsafe bun binary removal target: $executable" >&2
|
|
2284
|
-
exit 1
|
|
2285
|
-
}
|
|
2286
|
-
if [[ "$mode" == apply ]]; then
|
|
2287
|
-
rm -f -- "$executable"
|
|
2288
|
-
else
|
|
2289
|
-
echo "would remove bun binary: $executable"
|
|
2290
|
-
fi
|
|
2291
|
-
;;
|
|
2292
|
-
embedding-model)
|
|
2293
|
-
if [[ "$mode" == apply ]]; then
|
|
2294
|
-
[[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
|
|
2295
|
-
command -v bun >/dev/null 2>&1 || { echo "embedding-model requires bun; ensure bun installs before this tool" >&2; exit 1; }
|
|
2296
|
-
(cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
|
|
2297
|
-
bun "$repo_root/packages/fleet/src/install-embedding-model.ts" uninstall </dev/null)
|
|
2298
|
-
else
|
|
2299
|
-
echo "would remove embedding model cache: ~/.cache/adl-embedding/model (as $administrator)"
|
|
2300
|
-
fi
|
|
2301
|
-
;;
|
|
2302
|
-
*) echo "unsupported manager in previous manifest: $manager" >&2; exit 1 ;;
|
|
2303
|
-
esac
|
|
2446
|
+
remove_one_manifest_entry "$tool_id"
|
|
2304
2447
|
# Selects an id when it was deleted outright (absent from the current
|
|
2305
2448
|
# manifest) OR retained but migrated to a different manager/package (e.g.
|
|
2306
2449
|
# lean-ctx moving from npm:lean-ctx-bin to release-archive-latest:lean-ctx)
|
|
@@ -2642,6 +2785,83 @@ else
|
|
|
2642
2785
|
echo "global-tools: $mode complete; no state changed"
|
|
2643
2786
|
fi
|
|
2644
2787
|
|
|
2788
|
+
# git_maintain_repos (2026-09-30, arm-only decision the same day): an
|
|
2789
|
+
# explicit, closed list of repositories this device's own checkout lives
|
|
2790
|
+
# at OUTSIDE any recursively-scanned home directory -- the Linux VPSes'
|
|
2791
|
+
# root-owned /opt/adl-azdo is the confirmed case for both strato-vps-1 and
|
|
2792
|
+
# hostinger-vps-1 (STATUS.md's 2026-09-29 first-deployment record;
|
|
2793
|
+
# hostinger's own fleet-cutover --apply landed 2026-09-29T23:43Z, confirmed
|
|
2794
|
+
# the same day). git-maintain's own scheduling (packages/fleet/src/
|
|
2795
|
+
# launch-agents.ts: a dedicated compiled binary, ad-hoc signed, run by a
|
|
2796
|
+
# launchd LaunchAgent under TCC) is macOS-only by construction, so it never
|
|
2797
|
+
# reaches these checkouts on its own. This runs the job directly, on
|
|
2798
|
+
# reconcile's own schedule (every platform this script runs on already has
|
|
2799
|
+
# one), passing each declared path as `--repo` argv -- never a recursive
|
|
2800
|
+
# scan of a shared parent like `/opt` (which also holds unrelated,
|
|
2801
|
+
# explicitly off-limits directories), and never a new environment
|
|
2802
|
+
# variable: the list itself comes only from the manifest, and
|
|
2803
|
+
# GIT_MAINTAIN_DRY below is this job's own pre-existing dry/real contract,
|
|
2804
|
+
# driven by reconcile's own --mode exactly like every other step in this
|
|
2805
|
+
# file, not a knob an operator sets by hand. Absent or empty (every mac
|
|
2806
|
+
# device today) is a silent no-op, the same convention
|
|
2807
|
+
# launch_agents/scratch_repos/surface_clis already use.
|
|
2808
|
+
#
|
|
2809
|
+
# ARM-ONLY, always, on this path (independent review, 2026-09-30): the full
|
|
2810
|
+
# job on this SAME checkout `bin/reconcile` runs from turned out to be new,
|
|
2811
|
+
# unapproved, root-run behaviour once actually reasoned through, not merely
|
|
2812
|
+
# a self-checkout read/write ordering hazard -- see the `--arm-only`
|
|
2813
|
+
# decision recorded in jobs/git-maintain/src/main.ts's own header comment
|
|
2814
|
+
# for the full list (a silent fast-forward-driven auto-deploy of
|
|
2815
|
+
# `origin/integration`/main with no approval gate, colliding with
|
|
2816
|
+
# fleet-cutover.sh re-reading fleet-retire-list.json from this same
|
|
2817
|
+
# checkout after reconcile runs, `git branch -D` as root, and a fetch that
|
|
2818
|
+
# still runs even in dry/plan/audit mode and exits 0 on its own failure).
|
|
2819
|
+
# `--arm-only` narrows this call to ONLY `armHooks`: no fetch, no
|
|
2820
|
+
# fast-forward, no branch prune, no bundle-refresh installer, no network,
|
|
2821
|
+
# and no Azure DevOps identity resolution. The only remaining write is
|
|
2822
|
+
# `.git/config`'s `core.hooksPath`, real only in apply mode
|
|
2823
|
+
# (GIT_MAINTAIN_DRY=1 below makes it a `[dry] would arm ...` log line and
|
|
2824
|
+
# nothing else, in both plan AND audit mode). That is never a tracked file
|
|
2825
|
+
# this script (or anything downstream of it) reads, so arm-only has no
|
|
2826
|
+
# self-checkout read/write ordering hazard left to avoid -- this step stays
|
|
2827
|
+
# in this late position anyway, unchanged from the full-job version: it is
|
|
2828
|
+
# still the simplest place for it, and there is no reason to special-case
|
|
2829
|
+
# arm-only's position just because its own hazard is gone.
|
|
2830
|
+
git_maintain_repos=()
|
|
2831
|
+
while IFS= read -r repo; do
|
|
2832
|
+
[[ -n "$repo" ]] && git_maintain_repos+=("$repo")
|
|
2833
|
+
done < <(jq -r --arg platform "$platform" --arg device "$device" \
|
|
2834
|
+
'.[$platform].devices[$device].git_maintain_repos // [] | .[]' "$manifest")
|
|
2835
|
+
if [[ "${#git_maintain_repos[@]}" -gt 0 ]]; then
|
|
2836
|
+
git_maintain_repo_args=(--arm-only)
|
|
2837
|
+
for repo in "${git_maintain_repos[@]}"; do
|
|
2838
|
+
git_maintain_repo_args+=(--repo "$repo")
|
|
2839
|
+
done
|
|
2840
|
+
# Unlike default-agent-file.ts/managed-settings.ts's plan branches earlier
|
|
2841
|
+
# in this file (which never fail, by construction, so they run
|
|
2842
|
+
# unguarded), this job's dry pass still validates each declared path is a
|
|
2843
|
+
# real repo -- a genuine failure mode even in a non-apply mode, e.g. a
|
|
2844
|
+
# declared checkout that has not been cloned yet. So every non-apply mode
|
|
2845
|
+
# is guarded exactly like apply mode here, deliberately deviating from
|
|
2846
|
+
# those sibling steps: report it, collect it, never let it abort the rest
|
|
2847
|
+
# of this script under this file's own `set -e`. `$mode` (plan OR audit)
|
|
2848
|
+
# is reported verbatim -- audit is a real, distinct reconcile mode, never
|
|
2849
|
+
# relabeled "plan".
|
|
2850
|
+
if [[ "$mode" != apply ]]; then
|
|
2851
|
+
echo "global-tools: git-maintain (declared repos, arm-only) mode=$mode device=$device -> ${git_maintain_repos[*]}"
|
|
2852
|
+
if ! GIT_MAINTAIN_DRY=1 bun "$repo_root/packages/fleet/jobs/git-maintain/src/main.ts" "${git_maintain_repo_args[@]}"; then
|
|
2853
|
+
echo "global-tools: TOOL INSTALL FAILED: git-maintain-repos" >&2
|
|
2854
|
+
failed_tools+=("git-maintain-repos")
|
|
2855
|
+
fi
|
|
2856
|
+
else
|
|
2857
|
+
echo "global-tools: git-maintain (declared repos, arm-only) mode=apply device=$device -> ${git_maintain_repos[*]}"
|
|
2858
|
+
if ! bun "$repo_root/packages/fleet/jobs/git-maintain/src/main.ts" "${git_maintain_repo_args[@]}"; then
|
|
2859
|
+
echo "global-tools: TOOL INSTALL FAILED: git-maintain-repos" >&2
|
|
2860
|
+
failed_tools+=("git-maintain-repos")
|
|
2861
|
+
fi
|
|
2862
|
+
fi
|
|
2863
|
+
fi
|
|
2864
|
+
|
|
2645
2865
|
# A recorded, continued-past tool failure (install_one_tool /
|
|
2646
2866
|
# build_one_bun_tool / install_one_embedding_model_tool / write_one_shim)
|
|
2647
2867
|
# still lets every post-pass above run -- sweep, kimi-hooks, global-agents,
|