@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.10 → 0.1.12

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.
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # `sudo adl reconcile --host NAME [--mode plan|apply]` -- one full pass of what
5
+ # the 12-hourly job does (the job's program is `adl reconcile`, which runs this),
6
+ # also run on demand:
7
+ #
8
+ # 1. tools: this package's bin/sync-and-reconcile with
9
+ # `--source npm --device NAME --owner OWNER` (npm update, then tools
10
+ # reconcile). The owner is the device's `administrator` in this package's
11
+ # manifest (bin/lib/fleet-host.sh, the lookup bin/setup shares). --mode is
12
+ # forwarded only when given, so the default is apply.
13
+ # 2. repositories: `adl maintain --location <repo_location> --expect-user
14
+ # OWNER`, the package's own bin/adl, run AS THE ADMINISTRATOR -- never as
15
+ # root. sudo resets PATH to its secure_path, which has neither Homebrew nor
16
+ # /usr/local/bin, so PATH is always handed over explicitly with `env PATH=`
17
+ # (every sudo -u call that ends up running adl, git or bun must). --mode
18
+ # plan becomes --dry-run.
19
+ #
20
+ # Both steps always run (a failed tools step does not hide a repository problem,
21
+ # and vice versa); the exit code is the first failure's, and each failure is
22
+ # reported as an ERROR line. A host that declares no repo_location skips step 2
23
+ # with a log line.
24
+ #
25
+ # ADL_SETUP_TEST_ROOT is the same single test switch bin/setup has: unset in
26
+ # every real run; when set it only keeps the caller's PATH so `id`, `uname` and
27
+ # `sudo` can be stubbed (packages/fleet/tests/reconcile-host.test.ts).
28
+
29
+ if [[ -n "${ADL_SETUP_TEST_ROOT:-}" ]]; then
30
+ export PATH="${PATH:-/usr/bin:/bin}"
31
+ else
32
+ export PATH="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
33
+ fi
34
+
35
+ host=""
36
+ mode=""
37
+ while [[ "$#" -gt 0 ]]; do
38
+ case "$1" in
39
+ --host) host="${2:-}"; shift 2 ;;
40
+ --mode) mode="${2:-}"; shift 2 ;;
41
+ *) echo "ERROR unknown argument: $1" >&2; exit 2 ;;
42
+ esac
43
+ done
44
+ [[ -n "$host" ]] || { echo "ERROR usage: sudo adl reconcile --host NAME [--mode plan|apply]" >&2; exit 2; }
45
+ if [[ -n "$mode" && "$mode" != plan && "$mode" != apply ]]; then
46
+ echo "ERROR unsupported mode: $mode (expected plan or apply)" >&2
47
+ exit 2
48
+ fi
49
+ [[ "$(id -u)" -eq 0 ]] || { echo "ERROR adl reconcile must run as root: sudo adl reconcile --host $host" >&2; exit 1; }
50
+
51
+ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
52
+ package_dir="$(cd -- "$script_dir/.." && pwd)"
53
+ # shellcheck source=lib/fleet-host.sh
54
+ source "$script_dir/lib/fleet-host.sh"
55
+ fleet_host_resolve "adl reconcile" "$package_dir/packages/fleet/manifest.json" "$host" || exit $?
56
+ owner="$fleet_host_owner"
57
+ repo_location="$fleet_host_repo_location"
58
+
59
+ [[ -x "$script_dir/sync-and-reconcile" ]] || { echo "ERROR adl reconcile: package scheduler script is missing: $script_dir/sync-and-reconcile" >&2; exit 1; }
60
+
61
+ # One lock around both steps: an on-demand run overlapping the timer must not
62
+ # run a second repository pass after its tools step lost sync's own lock.
63
+ if [[ -n "${ADL_SETUP_TEST_ROOT:-}" ]]; then
64
+ lock_dir="${ADL_SETUP_TEST_ROOT%/}/adl-reconcile.lock"
65
+ else
66
+ lock_dir=/var/run/adl-reconcile.lock
67
+ fi
68
+ if ! mkdir "$lock_dir" 2>/dev/null; then
69
+ echo "ERROR adl reconcile: another reconcile is running (lock: $lock_dir)" >&2
70
+ exit 1
71
+ fi
72
+ trap 'rmdir -- "$lock_dir"' EXIT
73
+
74
+ # 1. tools
75
+ sync_args=(--source npm --device "$host" --owner "$owner")
76
+ if [[ -n "$mode" ]]; then sync_args+=(--mode "$mode"); fi
77
+ tools_rc=0
78
+ "$script_dir/sync-and-reconcile" "${sync_args[@]}" || tools_rc=$?
79
+ if [[ "$tools_rc" -ne 0 ]]; then
80
+ echo "ERROR global-tools: tools reconcile failed (exit $tools_rc)" >&2
81
+ fi
82
+
83
+ # 2. repositories
84
+ repos_rc=0
85
+ if [[ -z "$repo_location" ]]; then
86
+ echo "global-tools: repo maintenance skipped: $host declares no repo_location"
87
+ else
88
+ adl_bin="$package_dir/bin/adl"
89
+ if [[ ! -x "$adl_bin" ]]; then
90
+ echo "ERROR global-tools: package adl is missing or not executable: $adl_bin" >&2
91
+ repos_rc=1
92
+ else
93
+ if [[ "$fleet_host_platform" == mac ]]; then
94
+ brew_bin_dir=/opt/homebrew/bin
95
+ else
96
+ brew_bin_dir=/home/linuxbrew/.linuxbrew/bin
97
+ fi
98
+ maintain_args=(maintain --location "$repo_location" --expect-user "$owner")
99
+ if [[ "$mode" == plan ]]; then maintain_args+=(--dry-run); fi
100
+ # /usr/local/bin first: git must resolve to the managed controller, which
101
+ # routes Azure DevOps traffic through the agent identity; Homebrew's git
102
+ # would bypass it.
103
+ sudo -u "$owner" -H env PATH="/usr/local/bin:$brew_bin_dir:/usr/bin:/bin" "$adl_bin" "${maintain_args[@]}" || repos_rc=$?
104
+ if [[ "$repos_rc" -ne 0 ]]; then
105
+ echo "ERROR global-tools: repository maintenance failed (exit $repos_rc)" >&2
106
+ fi
107
+ fi
108
+ fi
109
+
110
+ if [[ "$tools_rc" -ne 0 ]]; then exit "$tools_rc"; fi
111
+ exit "$repos_rc"
package/bin/setup ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # `sudo adl setup --host NAME` -- the whole onboarding of a host after
5
+ # `npm install -g @amsterdamdatalabs/adl-agent-control`:
6
+ #
7
+ # 1. check this is THE package the job manages (the one in the owner's global
8
+ # npm root), and that its bin/adl is there
9
+ # 2. run the first reconcile: `<this package>/bin/adl reconcile --host NAME`
10
+ # 3. only if that succeeded, arm the job that re-runs the same command every
11
+ # 12 hours
12
+ #
13
+ # A host that cannot reconcile is never left with a timer that fails unseen.
14
+ # The job's program is the package's own bin/adl, not a shell script: on macOS
15
+ # the one-time Full Disk Access grant (needed to read the repositories under the
16
+ # host's repo_location) goes to that binary, and the launchd/systemd job and
17
+ # `sudo adl reconcile --host NAME` are then literally the same command.
18
+ #
19
+ # Everything comes from this package: it locates itself from its own path and
20
+ # reads packages/fleet/manifest.json beside it (no git checkout, no source
21
+ # flag -- npm is the only source). The account the job runs npm as is the
22
+ # device's `administrator` in that manifest; there is no owner flag.
23
+ #
24
+ # ADL_SETUP_TEST_ROOT is the one test switch (packages/fleet/tests/setup.test.ts):
25
+ # unset in every real run. When set it relocates the system paths under that
26
+ # directory and keeps the caller's PATH so the system commands can be stubbed.
27
+
28
+ if [[ -n "${ADL_SETUP_TEST_ROOT:-}" ]]; then
29
+ export PATH="${PATH:-/usr/bin:/bin}"
30
+ # The sourced sync-and-reconcile has its own test switch; one switch here
31
+ # drives both, so a test sets only ADL_SETUP_TEST_ROOT.
32
+ export SYNC_AND_RECONCILE_TEST_ROOT="$ADL_SETUP_TEST_ROOT"
33
+ else
34
+ export PATH="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
35
+ fi
36
+
37
+ host=""
38
+ while [[ "$#" -gt 0 ]]; do
39
+ case "$1" in
40
+ --host) host="${2:-}"; shift 2 ;;
41
+ *) echo "ERROR unknown argument: $1" >&2; exit 2 ;;
42
+ esac
43
+ done
44
+ [[ -n "$host" ]] || { echo "ERROR usage: sudo adl setup --host NAME" >&2; exit 2; }
45
+ [[ "$(id -u)" -eq 0 ]] || { echo "ERROR adl setup must run as root: sudo adl setup --host $host" >&2; exit 1; }
46
+
47
+ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
48
+ package_dir="$(cd -- "$script_dir/.." && pwd)"
49
+ package_dir_real="$(cd -P -- "$script_dir/.." && pwd -P)"
50
+ # shellcheck source=lib/fleet-host.sh
51
+ source "$script_dir/lib/fleet-host.sh"
52
+ fleet_host_resolve "adl setup" "$package_dir/packages/fleet/manifest.json" "$host" || exit $?
53
+ owner="$fleet_host_owner"
54
+ minute="$fleet_host_minute"
55
+
56
+ # The job keeps THE package installed in the owner's global npm root up to date
57
+ # and reconciles from it. This must be that package, or the host would be armed
58
+ # to manage a different copy than the one the operator just ran. Ask npm the same
59
+ # way the job does (the functions are the job's own, from the sibling
60
+ # sync-and-reconcile), and compare real paths.
61
+ [[ -f "$script_dir/sync-and-reconcile" ]] || { echo "ERROR adl setup: package scheduler script is missing: $script_dir/sync-and-reconcile" >&2; exit 1; }
62
+ # shellcheck source=sync-and-reconcile
63
+ SYNC_AND_RECONCILE_SOURCE_ONLY=1 source "$script_dir/sync-and-reconcile"
64
+ os_name="$(uname -s)"
65
+ npm_root="$(npm_global_root "$owner" "$os_name")" || {
66
+ echo "ERROR adl setup: cannot resolve the global npm root as $owner" >&2
67
+ exit 1
68
+ }
69
+ managed_dir="$(npm_platform_package_dir "$npm_root" "$os_name" "$(uname -m)")" || exit 1
70
+ managed_real="$(cd -P -- "$managed_dir" 2>/dev/null && pwd -P)" || managed_real=""
71
+ if [[ -z "$managed_real" || "$package_dir_real" != "$managed_real" ]]; then
72
+ {
73
+ echo "ERROR adl setup: this package ($package_dir_real) is not the package the scheduled job manages ($managed_dir)."
74
+ echo "ERROR adl setup: install it where the job looks, as $owner, then run setup again:"
75
+ echo "ERROR adl setup: Linux: npm install -g --prefix /usr/local @amsterdamdatalabs/adl-agent-control"
76
+ echo "ERROR adl setup: macOS: npm install -g @amsterdamdatalabs/adl-agent-control"
77
+ } >&2
78
+ exit 1
79
+ fi
80
+
81
+ adl_bin="$package_dir_real/bin/adl"
82
+ [[ -x "$adl_bin" ]] || { echo "ERROR adl setup: the package adl is missing or not executable: $package_dir/bin/adl" >&2; exit 1; }
83
+
84
+ if [[ -n "${ADL_SETUP_TEST_ROOT:-}" ]]; then
85
+ system_root="${ADL_SETUP_TEST_ROOT%/}"
86
+ else
87
+ system_root=""
88
+ fi
89
+ legacy_adl_sync="$system_root/usr/local/libexec/adl-sync"
90
+ launch_daemons_dir="$system_root/Library/LaunchDaemons"
91
+ systemd_dir="$system_root/etc/systemd/system"
92
+ brew_prefix="$system_root/home/linuxbrew/.linuxbrew"
93
+
94
+ # Preflight that must hold before the first reconcile: the Linux job runs
95
+ # Homebrew as a non-root owner, and that owner needs git's safe.directory for
96
+ # the brew repositories or every `brew update` inside the reconcile fails.
97
+ if [[ "$fleet_host_platform" == linux ]]; then
98
+ if [[ -d "$brew_prefix" ]]; then
99
+ brew_owner="$(stat -c '%U' "$brew_prefix")"
100
+ [[ "$brew_owner" != root ]] || {
101
+ echo "ERROR Homebrew prefix must be owned by a non-root account: /home/linuxbrew/.linuxbrew" >&2
102
+ exit 1
103
+ }
104
+ else
105
+ echo "ERROR Homebrew prefix missing: /home/linuxbrew/.linuxbrew" >&2
106
+ exit 1
107
+ fi
108
+ for brew_repo in \
109
+ "$brew_prefix/Homebrew" \
110
+ "$brew_prefix/Homebrew/Library/Taps/adl-tan-theta/homebrew-tap"; do
111
+ [[ -d "$brew_repo/.git" ]] || continue
112
+ if ! (cd /tmp && sudo -u "$brew_owner" -H git config --global --get-all safe.directory 2>/dev/null) | grep -Fxq "$brew_repo"; then
113
+ (cd /tmp && sudo -u "$brew_owner" -H git config --global --add safe.directory "$brew_repo")
114
+ fi
115
+ done
116
+ fi
117
+
118
+ mkdir -p "$system_root/var/lib/adl" "$system_root/var/log/adl"
119
+
120
+ echo "adl setup: first reconcile for $host (owner $owner)"
121
+ reconcile_rc=0
122
+ "$adl_bin" reconcile --host "$host" || reconcile_rc=$?
123
+ if [[ "$reconcile_rc" -ne 0 ]]; then
124
+ echo "ERROR adl setup: first reconcile failed (exit $reconcile_rc); scheduler NOT armed. Fix the failure and re-run: sudo adl setup --host $host" >&2
125
+ exit "$reconcile_rc"
126
+ fi
127
+
128
+ case "$fleet_host_platform" in
129
+ mac)
130
+ mkdir -p "$launch_daemons_dir"
131
+ plist="$launch_daemons_dir/com.adl.global-tools.plist"
132
+ {
133
+ printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>'
134
+ printf '%s\n' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
135
+ printf '%s\n' '<plist version="1.0"><dict>'
136
+ printf '%s\n' '<key>Label</key><string>com.adl.global-tools</string>'
137
+ printf '%s\n' '<key>ProgramArguments</key><array>'
138
+ printf '<string>%s</string>\n' "$adl_bin"
139
+ printf '%s\n' '<string>reconcile</string>'
140
+ printf '<string>--host</string><string>%s</string>\n' "$host"
141
+ printf '%s\n' '</array>'
142
+ printf '%s\n' '<key>StartCalendarInterval</key><array>'
143
+ for hour in 0 12; do printf '<dict><key>Hour</key><integer>%s</integer><key>Minute</key><integer>%s</integer></dict>\n' "$hour" "$minute"; done
144
+ printf '%s\n' '</array><key>ProcessType</key><string>Background</string>'
145
+ printf '%s\n' '<key>StandardOutPath</key><string>/var/log/adl/stdout.log</string>'
146
+ printf '%s\n' '<key>StandardErrorPath</key><string>/var/log/adl/stderr.log</string>'
147
+ printf '%s\n' '</dict></plist>'
148
+ } >"$plist"
149
+ chown root:wheel "$plist"
150
+ chmod 0644 "$plist"
151
+ plutil -lint "$plist"
152
+ # bootout fails when the job is not loaded, so ask first instead of
153
+ # swallowing the error: a bootout that fails on a loaded job must stop us.
154
+ if launchctl print system/com.adl.global-tools >/dev/null 2>&1; then
155
+ launchctl bootout system/com.adl.global-tools
156
+ # bootout returns before launchd has finished unloading, and a
157
+ # bootstrap in that window fails with an I/O error. Wait until the
158
+ # label is really gone, bounded, and fail loud if it is not.
159
+ unloaded=0
160
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
161
+ if ! launchctl print system/com.adl.global-tools >/dev/null 2>&1; then unloaded=1; break; fi
162
+ sleep 0.5
163
+ done
164
+ [[ "$unloaded" -eq 1 ]] || {
165
+ echo "ERROR adl setup: com.adl.global-tools is still loaded after bootout (waited 5s); not bootstrapping. Run: sudo launchctl bootout system/com.adl.global-tools" >&2
166
+ exit 1
167
+ }
168
+ fi
169
+ launchctl bootstrap system "$plist"
170
+ ;;
171
+ linux)
172
+ mkdir -p "$systemd_dir"
173
+ cat > "$systemd_dir/adl-global-tools.service" <<EOF
174
+ [Unit]
175
+ Description=Reconcile Adl global tools and repositories (adl reconcile)
176
+ After=network-online.target
177
+ Wants=network-online.target
178
+
179
+ [Service]
180
+ Type=oneshot
181
+ ExecStart=$adl_bin reconcile --host $host
182
+ StandardOutput=append:/var/log/adl/stdout.log
183
+ StandardError=append:/var/log/adl/stderr.log
184
+ Nice=10
185
+ IOSchedulingClass=best-effort
186
+ IOSchedulingPriority=7
187
+ TimeoutStartSec=3h
188
+ EOF
189
+ cat > "$systemd_dir/adl-global-tools.timer" <<EOF
190
+ [Unit]
191
+ Description=Run Adl global-tool reconciliation every 12 hours
192
+
193
+ [Timer]
194
+ OnCalendar=*-*-* 00,12:$minute:00
195
+ Persistent=true
196
+ Unit=adl-global-tools.service
197
+
198
+ [Install]
199
+ WantedBy=timers.target
200
+ EOF
201
+ systemctl daemon-reload
202
+ systemctl enable --now adl-global-tools.timer
203
+ # enable --now leaves an already-active timer on its old schedule.
204
+ systemctl restart adl-global-tools.timer
205
+ ;;
206
+ esac
207
+
208
+ # The job used to run a copy of bin/sync-and-reconcile installed here. Its
209
+ # definition now points at the package's adl, so the copy is dead weight that
210
+ # would keep running stale update logic if anyone invoked it.
211
+ if [[ -e "$legacy_adl_sync" ]]; then
212
+ rm -f -- "$legacy_adl_sync"
213
+ echo "adl setup: removed the legacy scheduler copy $legacy_adl_sync"
214
+ fi
215
+
216
+ echo "adl setup: $host reconciled and armed (every 12 hours at minute $minute); run on demand with: sudo adl reconcile --host $host"
@@ -13,14 +13,14 @@ sync_and_reconcile_platform() {
13
13
  case "$(uname -s)" in
14
14
  Darwin) printf '%s\n' mac ;;
15
15
  Linux) printf '%s\n' linux ;;
16
- *) echo "unsupported operating system for scheduled sync: $(uname -s)" >&2; return 1 ;;
16
+ *) echo "ERROR unsupported operating system for scheduled sync: $(uname -s)" >&2; return 1 ;;
17
17
  esac
18
18
  }
19
19
 
20
20
  require_linux_automation_root() {
21
21
  local platform="$1" owner="$2"
22
22
  if [[ "$platform" == linux && "$owner" != root ]]; then
23
- echo "global-tools: Linux scheduled sync must run as root; got --owner $owner" >&2
23
+ echo "ERROR global-tools: Linux scheduled sync must run as root; got --owner $owner" >&2
24
24
  return 1
25
25
  fi
26
26
  }
@@ -35,7 +35,7 @@ parse_sync_source() {
35
35
  done
36
36
  case "$sync_source" in
37
37
  npm|checkout) ;;
38
- *) echo "unsupported source: $sync_source" >&2; return 2 ;;
38
+ *) echo "ERROR unsupported source: $sync_source" >&2; return 2 ;;
39
39
  esac
40
40
  }
41
41
 
@@ -45,12 +45,12 @@ npm_package_spec() {
45
45
  latest-stable) printf '%s@latest\n' '@amsterdamdatalabs/adl-agent-control' ;;
46
46
  pinned)
47
47
  [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$ ]] || {
48
- echo "pinned update_policy requires a valid installed package version" >&2
48
+ echo "ERROR pinned update_policy requires a valid installed package version" >&2
49
49
  return 1
50
50
  }
51
51
  printf '%s@%s\n' '@amsterdamdatalabs/adl-agent-control' "$version"
52
52
  ;;
53
- *) echo "unsupported update_policy: $policy" >&2; return 1 ;;
53
+ *) echo "ERROR unsupported update_policy: $policy" >&2; return 1 ;;
54
54
  esac
55
55
  }
56
56
 
@@ -59,9 +59,33 @@ npm_platform_package_dir() {
59
59
  case "$os:$arch" in
60
60
  Darwin:arm64) platform=darwin-arm64 ;;
61
61
  Linux:x86_64) platform=linux-x64 ;;
62
- *) echo "unsupported platform for npm package: $os $arch" >&2; return 1 ;;
62
+ *) echo "ERROR unsupported platform for npm package: $os $arch" >&2; return 1 ;;
63
63
  esac
64
- printf '%s/@amsterdamdatalabs/adl-agent-control-%s\n' "${npm_root%/}" "$platform"
64
+ # A global install nests optionalDependencies under the wrapper; npm never
65
+ # hoists them to the global root.
66
+ printf '%s/@amsterdamdatalabs/adl-agent-control/node_modules/@amsterdamdatalabs/adl-agent-control-%s\n' "${npm_root%/}" "$platform"
67
+ }
68
+
69
+ # Sets the `npm_env` array the owner's npm runs under: the same PATH and, on
70
+ # Linux, the same /usr/local prefix for every npm call this job makes. bin/setup
71
+ # sources this file and uses npm_global_root below, so it asks npm the very
72
+ # same question the scheduled job does.
73
+ resolve_npm_env() {
74
+ local os="$1" npm_path
75
+ case "$os" in
76
+ Darwin) npm_path=/opt/homebrew/bin:/usr/bin:/bin ;;
77
+ Linux) npm_path=/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin ;;
78
+ *) echo "ERROR unsupported operating system for npm package: $os" >&2; return 1 ;;
79
+ esac
80
+ npm_env=(PATH="$npm_path")
81
+ if [[ "$os" == Linux ]]; then npm_env=(npm_config_prefix=/usr/local PATH="$npm_path"); fi
82
+ }
83
+
84
+ # Prints OWNER's global npm root (`npm root -g` as that user, under npm_env).
85
+ npm_global_root() {
86
+ local owner="$1" os="$2"
87
+ resolve_npm_env "$os" || return 1
88
+ sudo -u "$owner" -H env "${npm_env[@]}" npm root -g
65
89
  }
66
90
 
67
91
  prepare_adl_npm_migration() {
@@ -72,7 +96,7 @@ prepare_adl_npm_migration() {
72
96
  previous_executable="$(jq -r --arg platform "$platform" --arg device "$device" '(.[$platform].common.tools // {}) * (.[$platform].devices[$device].tools // {}) | .adl.executable // empty' "$applied_manifest")" || return 1
73
97
  [[ "$previous_manager" == bun-compile && "$previous_executable" == /usr/local/bin/adl ]] || return 0
74
98
  [[ -f "$legacy_adl" && ! -L "$legacy_adl" ]] || {
75
- echo "prior manifest proves adl was a Bun binary, but the expected regular file is missing or is a symlink: $legacy_adl" >&2
99
+ echo "ERROR prior manifest proves adl was a Bun binary, but the expected regular file is missing or is a symlink: $legacy_adl" >&2
76
100
  return 1
77
101
  }
78
102
  rm -f -- "$legacy_adl"
@@ -96,39 +120,37 @@ while [[ "$#" -gt 0 ]]; do
96
120
  --repo) repo="${2:-}"; shift 2 ;;
97
121
  --owner) owner="${2:-}"; shift 2 ;;
98
122
  --mode) mode="${2:-}"; shift 2 ;;
99
- *) echo "unknown argument: $1" >&2; exit 2 ;;
123
+ *) echo "ERROR unknown argument: $1" >&2; exit 2 ;;
100
124
  esac
101
125
  done
102
126
  case "$sync_source" in
103
127
  npm|checkout) ;;
104
- *) echo "unsupported source: $sync_source" >&2; exit 2 ;;
128
+ *) echo "ERROR unsupported source: $sync_source" >&2; exit 2 ;;
105
129
  esac
106
130
  [[ -n "$device" && -n "$owner" ]] || {
107
- echo "usage: sync-and-reconcile [--source npm|checkout] --device NAME --owner USER [--repo PATH] [--mode audit|plan|apply]" >&2
131
+ echo "ERROR usage: sync-and-reconcile [--source npm|checkout] --device NAME --owner USER [--repo PATH] [--mode audit|plan|apply]" >&2
108
132
  exit 2
109
133
  }
110
134
  [[ "$mode" == audit || "$mode" == plan || "$mode" == apply ]] || {
111
- echo "unsupported reconcile mode: $mode" >&2
135
+ echo "ERROR unsupported reconcile mode: $mode" >&2
112
136
  exit 2
113
137
  }
114
138
  if [[ "$sync_source" == checkout && -z "$repo" ]]; then
115
- echo "checkout source requires --repo PATH" >&2
139
+ echo "ERROR checkout source requires --repo PATH" >&2
116
140
  exit 2
117
141
  fi
118
142
  if [[ "$sync_source" == npm && -n "$repo" ]]; then
119
- echo "npm source does not accept --repo; use --source checkout for a checkout path" >&2
143
+ echo "ERROR npm source does not accept --repo; use --source checkout for a checkout path" >&2
120
144
  exit 2
121
145
  fi
122
146
 
123
147
  if [[ -n "${SYNC_AND_RECONCILE_TEST_ROOT:-}" ]]; then
124
148
  lock_dir="${SYNC_AND_RECONCILE_TEST_ROOT%/}/adl-sync.lock"
125
- scheduler_path="${SYNC_AND_RECONCILE_TEST_ROOT%/}/scheduler"
126
149
  else
127
150
  lock_dir=/var/run/adl-sync.lock
128
- scheduler_path=/usr/local/libexec/adl-sync
129
151
  fi
130
152
  if ! mkdir "$lock_dir" 2>/dev/null; then
131
- echo "global-tools: another reconciliation is running (lock: $lock_dir)" >&2
153
+ echo "ERROR global-tools: another reconciliation is running (lock: $lock_dir)" >&2
132
154
  exit 1
133
155
  fi
134
156
  trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT
@@ -137,28 +159,22 @@ host_platform="$(sync_and_reconcile_platform)"
137
159
  require_linux_automation_root "$host_platform" "$owner" || exit 1
138
160
 
139
161
  if [[ "$sync_source" == checkout ]]; then
140
- [[ -e "$repo/.git" ]] || { echo "developer checkout missing: $repo" >&2; exit 1; }
162
+ [[ -e "$repo/.git" ]] || { echo "ERROR developer checkout missing: $repo" >&2; exit 1; }
141
163
  manifest="$repo/packages/fleet/manifest.json"
142
- [[ -f "$manifest" ]] || { echo "developer checkout manifest missing: $manifest" >&2; exit 1; }
143
- [[ -x "$repo/bin/reconcile" ]] || { echo "developer checkout reconcile is missing: $repo/bin/reconcile" >&2; exit 1; }
164
+ [[ -f "$manifest" ]] || { echo "ERROR developer checkout manifest missing: $manifest" >&2; exit 1; }
165
+ [[ -x "$repo/bin/reconcile" ]] || { echo "ERROR developer checkout reconcile is missing: $repo/bin/reconcile" >&2; exit 1; }
144
166
  echo "global-tools: source=checkout (developer mode)"
145
167
  reconcile_dir="$repo/bin"
146
168
  else
147
169
  os="$(uname -s)"
148
170
  arch="$(uname -m)"
149
- case "$os" in
150
- Darwin) npm_path=/opt/homebrew/bin:/usr/bin:/bin ;;
151
- Linux) npm_path=/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin ;;
152
- *) echo "unsupported operating system for npm package: $os" >&2; exit 1 ;;
153
- esac
154
- npm_env=(PATH="$npm_path")
155
- if [[ "$os" == Linux ]]; then npm_env=(npm_config_prefix=/usr/local PATH="$npm_path"); fi
156
- npm_root="$(sudo -u "$owner" -H env "${npm_env[@]}" npm root -g)" || {
157
- echo "cannot resolve the global npm root as $owner" >&2
171
+ resolve_npm_env "$os" || exit 1
172
+ npm_root="$(npm_global_root "$owner" "$os")" || {
173
+ echo "ERROR cannot resolve the global npm root as $owner" >&2
158
174
  exit 1
159
175
  }
160
176
  [[ "$npm_root" == /*/lib/node_modules ]] || {
161
- echo "global npm root has an unsupported path: $npm_root" >&2
177
+ echo "ERROR global npm root has an unsupported path: $npm_root" >&2
162
178
  exit 1
163
179
  }
164
180
  npm_prefix="${npm_root%/lib/node_modules}"
@@ -167,14 +183,14 @@ else
167
183
  package_spec=""
168
184
  if [[ -f "$installed_manifest" ]]; then
169
185
  update_policy="$(jq -er '.config.update_policy | select(type == "string")' "$installed_manifest")" || {
170
- echo "installed adl package manifest has no config.update_policy: $installed_manifest" >&2
186
+ echo "ERROR installed adl package manifest has no config.update_policy: $installed_manifest" >&2
171
187
  exit 1
172
188
  }
173
189
  case "$update_policy" in
174
190
  latest-stable) package_spec="$(npm_package_spec latest-stable)" ;;
175
191
  pinned)
176
192
  pinned_version="$(jq -er --arg platform "$host_platform" '.[$platform].common.tools.adl.version | select(type == "string")' "$installed_manifest")" || {
177
- echo "installed adl package manifest requires a version for pinned update_policy: $installed_manifest" >&2
193
+ echo "ERROR installed adl package manifest requires a version for pinned update_policy: $installed_manifest" >&2
178
194
  exit 1
179
195
  }
180
196
  package_spec="$(npm_package_spec pinned "$pinned_version")" || exit 1
@@ -199,15 +215,12 @@ else
199
215
  prepare_adl_npm_migration "$applied_manifest" "$device" "$host_platform" "$legacy_adl" "$legacy_hash"
200
216
 
201
217
  sudo -u "$owner" -H env "${npm_env[@]}" npm install --global --prefix "$npm_prefix" --no-audit --no-fund "$package_spec"
202
- [[ -d "$platform_dir" ]] || { echo "npm install did not create the platform package: $platform_dir" >&2; exit 1; }
218
+ [[ -d "$platform_dir" ]] || { echo "ERROR npm install did not create the platform package: $platform_dir" >&2; exit 1; }
203
219
  installed_manifest="$platform_dir/packages/fleet/manifest.json"
204
- [[ -f "$installed_manifest" ]] || { echo "installed adl package manifest missing: $installed_manifest" >&2; exit 1; }
205
- [[ -x "$platform_dir/bin/reconcile" ]] || { echo "installed adl package reconcile is missing: $platform_dir/bin/reconcile" >&2; exit 1; }
206
- [[ -x "$platform_dir/bin/sync-and-reconcile" ]] || { echo "installed adl package scheduler is missing: $platform_dir/bin/sync-and-reconcile" >&2; exit 1; }
207
- [[ -x "$platform_dir/bin/validate-manifest" ]] || { echo "installed adl package manifest validator is missing: $platform_dir/bin/validate-manifest" >&2; exit 1; }
220
+ [[ -f "$installed_manifest" ]] || { echo "ERROR installed adl package manifest missing: $installed_manifest" >&2; exit 1; }
221
+ [[ -x "$platform_dir/bin/reconcile" ]] || { echo "ERROR installed adl package reconcile is missing: $platform_dir/bin/reconcile" >&2; exit 1; }
222
+ [[ -x "$platform_dir/bin/validate-manifest" ]] || { echo "ERROR installed adl package manifest validator is missing: $platform_dir/bin/validate-manifest" >&2; exit 1; }
208
223
  "$platform_dir/bin/validate-manifest" "$installed_manifest"
209
- install -m 0755 "$platform_dir/bin/sync-and-reconcile" "$scheduler_path.new"
210
- mv "$scheduler_path.new" "$scheduler_path"
211
224
  reconcile_dir="$platform_dir/bin"
212
225
  fi
213
226
 
@@ -15,7 +15,7 @@ if ! jq -e '
15
15
  .config.schema == 1 and
16
16
  (.config.repository | type == "string" and test("^https://dev\\.azure\\.com/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+/_git/[A-Za-z0-9._-]+$")) and
17
17
  .config.branch == "main" and
18
- .config.interval_seconds == 21600 and
18
+ .config.interval_seconds == 43200 and
19
19
  .config.update_policy == "latest-stable" and
20
20
  .config.allow_major_updates == true and
21
21
  .config.remove_deleted_entries == true and
@@ -43,28 +43,21 @@ if ! jq -e '
43
43
  all(.minimum_free_gib >= 1) and
44
44
  all((.home | type) == "string" and (.home | startswith("/"))) and
45
45
  all((.default_agent | type) == "string" and (.default_agent | test("^agent-[a-z0-9-]+$"))) and
46
- all((keys - ["administrator", "default_agent", "home", "launch_agents", "minimum_free_gib", "schedule_minute", "scratch_repos", "brew_upgrade_all", "surface_clis", "git_maintain_repos", "tools"] | length) == 0) and
46
+ all((keys - ["administrator", "default_agent", "home", "minimum_free_gib", "schedule_minute", "scratch_repos", "brew_upgrade_all", "surface_clis", "repo_location", "tools"] | length) == 0) and
47
47
  all((.scratch_repos // []) | type == "array" and (. | unique | length) == length and
48
48
  all(type == "string" and startswith("/") and . != "/" and
49
49
  (endswith("/") | not) and (contains("..") | not) and (test("\\s") | not))) and
50
- # git_maintain_repos (2026-09-30): an explicit, closed list of
51
- # repositories bin/reconcile passes to git-maintain via --repo argv --
52
- # never a recursive scan root, deliberately, since these devices own
53
- # checkouts that sit at a fixed path outside any device home (the Linux
54
- # VPSes use /opt/adl-azdo) and a recursive scan of a shared parent like
55
- # /opt would also walk into unrelated, explicitly off-limits
56
- # directories. The azagent/vsts.agent substring bans are exactly that
57
- # guard, enforced at the schema layer (case-insensitively -- a manifest
58
- # value could be typed AzAgent2 as easily as azagent2) so a mistyped or
59
- # copy-pasted entry can never point there. git-maintain itself refuses
60
- # the same names again, case-insensitively and by realpath too, at the
61
- # point of actual use (jobs/git-maintain/src/main.ts,
62
- # isForbiddenAzurePipelinesPath) -- this schema check alone only ever
63
- # sees the manifest literal string, never what a symlink resolves to.
64
- all((.git_maintain_repos // []) | type == "array" and (. | unique | length) == length and
65
- all(type == "string" and startswith("/") and . != "/" and
66
- (endswith("/") | not) and (contains("..") | not) and (test("\\s") | not) and
67
- (ascii_downcase | contains("azagent") | not) and (ascii_downcase | contains("vsts.agent") | not))) and
50
+ # repo_location (2026-09-30): the ONE directory `adl reconcile` maintains
51
+ # repositories under (immediate children carrying adl-config.toml only). It
52
+ # is the whole scope of that step, so it must be an exact absolute directory
53
+ # path: never "/", no trailing slash, no "..", no whitespace. The
54
+ # azagent/vsts.agent substring bans keep it off Azure Pipelines agent
55
+ # installs (case-insensitively -- a value could be typed AzAgent2 as easily
56
+ # as azagent2); the maintenance step refuses the same names again, by
57
+ # realpath too, at the point of use.
58
+ all((.repo_location // "/ok") | type == "string" and startswith("/") and . != "/" and
59
+ (endswith("/") | not) and (contains("..") | not) and (test("\\s") | not) and
60
+ (ascii_downcase | contains("azagent") | not) and (ascii_downcase | contains("vsts.agent") | not)) and
68
61
  all((.brew_upgrade_all // false) | type == "boolean") and
69
62
  all(
70
63
  (.surface_clis // []) as $surfaces |
@@ -159,30 +152,8 @@ if ! jq -e '
159
152
  ((.allow_scripts // []) | type == "array" and
160
153
  all(type == "string" and test("^(@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$")))
161
154
  else true end)) and
162
- (.linux.common | has("launch_agents") | not) and
163
- ([.linux.devices[] | has("launch_agents")] | any | not) and
164
- (.mac.common.launch_agents["git-maintain"] as $job |
165
- ($job | type == "object") and
166
- ($job.repo_scope | type == "object" and keys == ["glob", "recursive", "root"] and
167
- (.root | relpath) and .glob == "*" and .recursive == true) and
168
- ($job.environment | has("GIT_MAINTAIN_ROOT") | not) and
169
- ($job.environment | has("GIT_MAINTAIN_GLOB") | not)) and
170
- ([.mac.common.launch_agents, .mac.devices[].launch_agents] | map(. // {}) |
171
- all(type == "object") and
172
- ([.[] | to_entries[] | .value] |
173
- all(
174
- type == "object" and
175
- (keys - ["label", "source", "entry", "start_interval_seconds", "run_at_load", "environment", "log", "replaces", "repo_scope"] | length == 0) and
176
- ((.label | type) == "string" and (.label | test("^[A-Za-z0-9][A-Za-z0-9._-]*$"))) and
177
- (.source | relpath) and
178
- (.entry | relpath) and
179
- ((.start_interval_seconds | type) == "number" and .start_interval_seconds >= 60 and (.start_interval_seconds | floor) == .start_interval_seconds) and
180
- ((.run_at_load | type) == "boolean") and
181
- ((.environment | type) == "object" and (.environment | to_entries | all((.key | test("^[A-Z_][A-Z0-9_]*$")) and (.value | type) == "string"))) and
182
- (.log | relpath) and
183
- ((.replaces // []) | type == "array" and all(relpath))
184
- ) and
185
- (map(.label) | length == (unique | length))))
155
+ # launch_agents are gone (2026-09-30): a stale declaration must fail loud.
156
+ ([.mac.common, .linux.common] | all(keys - ["os_upgrade", "tools"] | length == 0))
186
157
  ' "$manifest" >/dev/null; then
187
158
  echo "global-tools manifest: INVALID ($manifest does not satisfy bin/validate-manifest's schema)" >&2
188
159
  exit 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amsterdamdatalabs/adl-agent-control-linux-x64",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "adl CLI binary for linux/x64 (agent identity + Azure DevOps front door — see docs/architecture/cli-design.md)",
5
5
  "license": "UNLICENSED",
6
6
  "os": [
@@ -14,8 +14,6 @@
14
14
  "packages/fleet/manifest.json",
15
15
  "packages/fleet/manifest.schema.json",
16
16
  "packages/fleet/src",
17
- "packages/fleet/jobs",
18
- "packages/fleet/systemd",
19
17
  "packages/agent-settings/surface-policy.json",
20
18
  "packages/agent-settings/protected.json",
21
19
  "packages/agent-settings/src",
@@ -8,8 +8,7 @@
8
8
  "start": "bun src/main.ts",
9
9
  "build": "bun ../../workflows/scripts/build/agent-hooks/build.ts",
10
10
  "typecheck": "tsc --noEmit",
11
- "test": "bun test --timeout 30000",
12
- "test:coverage": "bun test --coverage --timeout 30000",
11
+ "test": "bun test --coverage --timeout 30000",
13
12
  "check": "bun run typecheck && bun test --coverage"
14
13
  },
15
14
  "dependencies": {