@crvouga/dev-fleet 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +3 -1
  2. package/ansible/macos/policy.json +1 -0
  3. package/ansible/platforms.json +111 -0
  4. package/ansible/ubuntu/authorize-sudo.sh +7 -16
  5. package/ansible/ubuntu/baseline.yml +4 -28
  6. package/ansible/ubuntu/disk-gc.py +186 -4
  7. package/ansible/ubuntu/policy.json +18 -3
  8. package/ansible/ubuntu/t3-desktop.yml +2 -2
  9. package/ansible/ubuntu/tailscale.yml +20 -9
  10. package/ansible/ubuntu/tasks/dpkg.yml +46 -0
  11. package/ansible/ubuntu/tasks/install.yml +38 -0
  12. package/ansible/ubuntu/tasks/packages.yml +4 -55
  13. package/ansible/ubuntu/tasks/release.yml +25 -0
  14. package/ansible/ubuntu/tasks/resources.yml +6 -5
  15. package/ansible/ubuntu/tasks/snapshot.yml +50 -0
  16. package/ansible/ubuntu/tasks/unattended-upgrades.yml +24 -0
  17. package/bootstrap/fleet-remote-bootstrap.sh +18 -41
  18. package/docs/consumer-setup.md +32 -30
  19. package/docs/declared.md +4 -5
  20. package/docs/runbooks.md +3 -6
  21. package/docs/support-matrix.md +36 -0
  22. package/docs/ubuntu-sync.md +9 -7
  23. package/docs/worker-contract.md +28 -1
  24. package/package.json +2 -2
  25. package/schema/check-ids.json +8 -4
  26. package/schema/fleet.schema.json +7 -3
  27. package/src/cli/fleet-sync-remote.ts +1 -1
  28. package/src/definition.ts +18 -7
  29. package/src/machine/platform.ts +48 -28
  30. package/src/machine/support-matrix.ts +76 -0
  31. package/src/manager-sync.ts +15 -7
  32. package/src/network-definition.ts +1 -1
  33. package/src/node-converge.ts +1 -1
  34. package/src/platform/debian-version.ts +62 -0
  35. package/src/platform/ubuntu-audit.ts +11 -4
  36. package/src/platform/ubuntu-release.ts +41 -0
  37. package/src/remote-bootstrap.ts +2 -0
  38. package/src/worker-host-checks.ts +36 -10
@@ -1,56 +1,5 @@
1
1
  ---
2
- - name: Create owned APT configuration directory
3
- ansible.builtin.file:
4
- path: /etc/personal-fleet
5
- state: directory
6
- owner: root
7
- group: root
8
- mode: '0755'
9
- - name: Declare snapshot sources used only for managed package transactions
10
- ansible.builtin.copy:
11
- dest: /etc/personal-fleet/ubuntu.sources
12
- owner: root
13
- group: root
14
- mode: '0644'
15
- backup: true
16
- content: |
17
- Types: deb
18
- URIs: https://snapshot.ubuntu.com/ubuntu/{{ snapshot }}/
19
- Suites: resolute resolute-updates resolute-security
20
- Components: main universe restricted multiverse
21
- Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
22
- register: snapshot_sources
23
- - name: Isolate snapshot selection from unrelated APT operations
24
- ansible.builtin.copy:
25
- dest: /etc/personal-fleet/apt.conf
26
- owner: root
27
- group: root
28
- mode: '0644'
29
- content: |
30
- Dir::Etc::sourcelist "/etc/personal-fleet/ubuntu.sources";
31
- Dir::Etc::sourceparts "-";
32
- DPkg::Lock::Timeout "{{ aptLockTimeoutSeconds }}";
33
- - name: Inspect immutable snapshot package index
34
- ansible.builtin.stat:
35
- path: '/var/lib/apt/lists/snapshot.ubuntu.com_ubuntu_{{ snapshot }}_dists_resolute_InRelease'
36
- register: snapshot_index
37
- - name: Fetch signed snapshot indexes after source change or interrupted setup
38
- ansible.builtin.apt:
39
- update_cache: true
40
- cache_valid_time: 0
41
- lock_timeout: '{{ aptLockTimeoutSeconds }}'
42
- environment:
43
- APT_CONFIG: /etc/personal-fleet/apt.conf
44
- when: snapshot_sources.changed or not snapshot_index.stat.exists
45
- - name: Install exact managed packages without downgrading or restarting services
46
- ansible.builtin.apt:
47
- name: '{{ item.key }}={{ item.value }}'
48
- state: present
49
- lock_timeout: '{{ aptLockTimeoutSeconds }}'
50
- allow_downgrade: false
51
- allow_unauthenticated: false
52
- fail_on_autoremove: true
53
- policy_rc_d: 101
54
- environment:
55
- APT_CONFIG: /etc/personal-fleet/apt.conf
56
- loop: '{{ packages | dict2items }}'
2
+ - ansible.builtin.include_tasks: install.yml
3
+ vars:
4
+ install_pins: '{{ packages }}'
5
+ - ansible.builtin.import_tasks: unattended-upgrades.yml
@@ -0,0 +1,25 @@
1
+ ---
2
+ # The accepted releases are named once, in policy.json. This selects the
3
+ # entry of the running release and refuses everything else.
4
+ - name: Select the accepted release this machine runs
5
+ ansible.builtin.set_fact:
6
+ ubuntu_release: "{{ releases | selectattr('release', 'equalto', ansible_facts.distribution_version) | first | default({}) }}"
7
+ - name: Require an accepted release, its codename and one of its architectures
8
+ ansible.builtin.assert:
9
+ that:
10
+ - ansible_facts.distribution == 'Ubuntu'
11
+ - ubuntu_release.release is defined
12
+ - ansible_facts.distribution_release == ubuntu_release.codename
13
+ - "(ansible_facts.architecture | replace('x86_64', 'amd64') | replace('aarch64', 'arm64')) in ubuntu_release.architectures"
14
+ fail_msg: UBUNTU_BASELINE_MISMATCH; workers run a release docs/support-matrix.md lists. No distribution upgrade is permitted.
15
+ - name: Pin the packages of this release, its own pins over the common ones
16
+ ansible.builtin.set_fact:
17
+ packages: '{{ packages | combine(ubuntu_release.packages | default({})) }}'
18
+ - name: Read the kernel release to tell WSL2 apart from a machine of its own
19
+ ansible.builtin.slurp:
20
+ src: /proc/sys/kernel/osrelease
21
+ register: kernel_release
22
+ - name: Refuse WSL2 by name
23
+ ansible.builtin.assert:
24
+ that: not (kernel_release.content | b64decode | lower is search('microsoft|wsl'))
25
+ fail_msg: WORKER_ENVIRONMENT_UNSUPPORTED; WSL2 is not a worker. Run the worker on a physical or virtual machine of its own (docs/support-matrix.md).
@@ -49,10 +49,11 @@
49
49
  line: /swap.fleet none swap sw 0 0
50
50
  when: active_swap.stdout | trim == ''
51
51
 
52
- - name: Install the systemd userspace OOM killer
53
- ansible.builtin.apt:
54
- name: systemd-oomd
55
- state: present
52
+ - name: Install the systemd userspace OOM killer at its pin
53
+ ansible.builtin.include_tasks: install.yml
54
+ vars:
55
+ install_pins:
56
+ systemd-oomd: "{{ packages['systemd-oomd'] }}"
56
57
  - name: Create memory and disk policy directories
57
58
  ansible.builtin.file:
58
59
  path: '{{ item }}'
@@ -276,7 +277,7 @@
276
277
  After=docker.service
277
278
  [Service]
278
279
  Type=oneshot
279
- ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-inode-percent {{ resourceSafety.inodeWarnFreePercent }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}
280
+ ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-inode-percent {{ resourceSafety.inodeWarnFreePercent }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}{% for root in resourceSafety.diskGcWorktreeRoots %} --worktree-root {{ root }}{% endfor %} --worktree-idle-days {{ resourceSafety.diskGcWorktreeIdleDays }}{% for cache, mib in resourceSafety.diskGcCacheCapsMiB.items() %} --cache-cap {{ cache }}={{ mib }}{% endfor %} --keep-newest /var/cache/personal-fleet={{ resourceSafety.diskGcPackagesKept }} --mise {{ development_home }}/.local/bin/mise --user {{ development_user }}
280
281
 
281
282
  TimeoutStartSec=2h
282
283
  Nice=19
@@ -0,0 +1,50 @@
1
+ ---
2
+ # The snapshot every managed package comes from, separate from the system's
3
+ # own sources. Included before any pinned package is installed; a second
4
+ # inclusion in one run changes nothing.
5
+ - name: Create owned APT configuration directory
6
+ ansible.builtin.file:
7
+ path: /etc/personal-fleet
8
+ state: directory
9
+ owner: root
10
+ group: root
11
+ mode: '0755'
12
+ - name: Declare snapshot sources used only for managed package transactions
13
+ ansible.builtin.copy:
14
+ dest: /etc/personal-fleet/ubuntu.sources
15
+ owner: root
16
+ group: root
17
+ mode: '0644'
18
+ backup: true
19
+ content: |
20
+ Types: deb
21
+ URIs: https://snapshot.ubuntu.com/ubuntu/{{ snapshot }}/
22
+ Suites: {{ ubuntu_release.codename }} {{ ubuntu_release.codename }}-updates {{ ubuntu_release.codename }}-security
23
+ Components: main universe restricted multiverse
24
+ Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
25
+ register: snapshot_sources
26
+ # Fetching the snapshot indexes must leave the indexes of the system's own
27
+ # sources in place: the system's APT and unattended upgrades still use them.
28
+ - name: Isolate snapshot selection from unrelated APT operations
29
+ ansible.builtin.copy:
30
+ dest: /etc/personal-fleet/apt.conf
31
+ owner: root
32
+ group: root
33
+ mode: '0644'
34
+ content: |
35
+ Dir::Etc::sourcelist "/etc/personal-fleet/ubuntu.sources";
36
+ Dir::Etc::sourceparts "-";
37
+ APT::Get::List-Cleanup "false";
38
+ DPkg::Lock::Timeout "{{ aptLockTimeoutSeconds }}";
39
+ - name: Inspect immutable snapshot package index
40
+ ansible.builtin.stat:
41
+ path: '/var/lib/apt/lists/snapshot.ubuntu.com_ubuntu_{{ snapshot }}_dists_{{ ubuntu_release.codename }}_InRelease'
42
+ register: snapshot_index
43
+ - name: Fetch signed snapshot indexes after source change or interrupted setup
44
+ ansible.builtin.apt:
45
+ update_cache: true
46
+ cache_valid_time: 0
47
+ lock_timeout: '{{ aptLockTimeoutSeconds }}'
48
+ environment:
49
+ APT_CONFIG: /etc/personal-fleet/apt.conf
50
+ when: snapshot_sources.changed or not snapshot_index.stat.exists
@@ -0,0 +1,24 @@
1
+ ---
2
+ # Ubuntu installs security updates by itself, and they keep flowing: a pin is
3
+ # a minimum, never a hold. What runs unattended is declared here so it is
4
+ # known: security updates only, never a reboot, never a removal. The fleet's
5
+ # own file sorts after the package's 50unattended-upgrades and replaces its
6
+ # origins.
7
+ - name: Declare which updates Ubuntu installs unattended
8
+ ansible.builtin.copy:
9
+ dest: /etc/apt/apt.conf.d/52personal-fleet-unattended-upgrades
10
+ owner: root
11
+ group: root
12
+ mode: '0644'
13
+ content: |
14
+ // Managed by personal Fleet: unattended upgrades
15
+ APT::Periodic::Update-Package-Lists "1";
16
+ APT::Periodic::Unattended-Upgrade "1";
17
+ #clear Unattended-Upgrade::Allowed-Origins;
18
+ #clear Unattended-Upgrade::Origins-Pattern;
19
+ Unattended-Upgrade::Allowed-Origins {
20
+ "${distro_id}:${distro_codename}-security";
21
+ };
22
+ Unattended-Upgrade::Automatic-Reboot "false";
23
+ Unattended-Upgrade::Remove-Unused-Dependencies "false";
24
+ Unattended-Upgrade::Remove-New-Unused-Dependencies "false";
@@ -2,14 +2,17 @@
2
2
  # Executed over strict SSH from the selected verified Git revision. stdin is
3
3
  # one line holding the base64 fleet configuration (empty when there is none),
4
4
  # then the base64 Git bundle (nothing in cached mode). All executable content
5
- # remains in this tracked source.
5
+ # remains in this tracked source. The Ubuntu releases workers may run are
6
+ # named once, in ansible/ubuntu/policy.json; the manager passes them in.
6
7
  set -eu
7
8
  umask 077
8
9
  revision=$1
9
10
  bundle_sha=$2
10
11
  node=$3
11
12
  source_mode=${4:-transfer}
13
+ releases=${5:-}
12
14
  case "$source_mode" in cached|transfer) ;; *) exit 2;; esac
15
+ case "$releases" in *[!0-9.\ ]*|'') exit 2;; esac
13
16
  case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
14
17
  [ "${#revision}" = 40 ] || exit 2
15
18
  case "$bundle_sha" in *[!a-f0-9]*|'') exit 2;; esac
@@ -26,7 +29,8 @@ remedy() {
26
29
  case "$1" in
27
30
  FLEET_REMOTE_LOCKED) echo 'Another fleet operation holds this machine. Wait for it to finish; rerun fleet-sync afterwards. Never remove the lock while its holder runs.';;
28
31
  ROOT_DEVELOPMENT_USER_REJECTED) echo 'Connect as the non-root developer account the fleet configuration names.';;
29
- UBUNTU_BASELINE_MISMATCH|UBUNTU_ARCHITECTURE_UNSUPPORTED|MACOS_BASELINE_MISMATCH|MACOS_ARCHITECTURE_UNSUPPORTED|PLATFORM_UNSUPPORTED) echo 'Workers run Ubuntu 26.04 on x86_64 or aarch64. Reinstall the machine; fleet-sync never upgrades an operating system.';;
32
+ UBUNTU_BASELINE_MISMATCH|UBUNTU_ARCHITECTURE_UNSUPPORTED|PLATFORM_UNSUPPORTED) echo 'Workers run an Ubuntu release docs/support-matrix.md lists, on x86_64 or aarch64. Reinstall the machine; fleet-sync never upgrades an operating system.';;
33
+ WORKER_ENVIRONMENT_UNSUPPORTED) echo 'WSL2 is not a worker. Run the worker on a physical or virtual machine of its own (docs/support-matrix.md).';;
30
34
  FLEET_STATE_PATH_UNSAFE) echo 'A fleet state directory under ~/.local/share/personal is a symbolic link. Replace it with a real directory.';;
31
35
  BOOTSTRAP_CAPACITY_UNVERIFIED|BOOTSTRAP_DISK_CAPACITY_REQUIRED) echo 'Free at least 2 GiB and 50000 inodes on the home file system, then rerun fleet-sync.';;
32
36
  SOURCE_DIGEST_MISMATCH) echo 'The bundle changed in transit. Rerun fleet-sync; if it repeats, check the SSH route.';;
@@ -37,8 +41,6 @@ remedy() {
37
41
  APT_DISK_FULL) echo 'Package installation ran out of disk space. Free space under /var and /usr, then rerun fleet-sync.';;
38
42
  DPKG_INTERRUPTED) echo 'An earlier package installation that fleet-sync did not start was interrupted. Run sudo dpkg --configure -a once no package process is running, then rerun fleet-sync.';;
39
43
  APT_UPDATE_FAILED|APT_INSTALL_FAILED) echo 'Run sudo apt-get update and install the bootstrap packages by hand to see the error, then rerun fleet-sync.';;
40
- XCODE_COMMAND_LINE_TOOLS_REQUIRED) echo 'Install the Xcode Command Line Tools (xcode-select --install), then rerun fleet-sync.';;
41
- BOOTSTRAP_LOCK_TOOL_REQUIRED) echo 'Install python3 so the operation lock can be taken, then rerun fleet-sync.';;
42
44
  SOURCE_CACHE_CHANGED|SOURCE_STATUS_FAILED|SOURCE_CHECKOUT_CONFLICT) echo 'The fleet-owned checkout of this revision holds changes fleet-sync did not make. Inspect and remove that revisions directory, then rerun fleet-sync.';;
43
45
  CONFIGURATION_INSTALL_FAILED) echo 'The fleet configuration could not be written under ~/.config/dev-fleet. Check that directory, then rerun fleet-sync.';;
44
46
  TOOLCHAIN_*) echo 'The pinned toolchain did not converge. Rerun fleet-sync; it resumes where it stopped. If it repeats, run devfleet fleet-status --node for this worker.';;
@@ -64,22 +66,14 @@ trap '' HUP
64
66
  trap 'exit 1' INT TERM
65
67
  # No shell startup assumptions, and no implicit root development user.
66
68
  [ "$(id -u)" != 0 ] || die ROOT_DEVELOPMENT_USER_REJECTED
67
- # Ubuntu 26.04 is the accepted worker platform; macOS 14+ follows the same
68
- # phases best effort with launchd instead of systemd.
69
- platform=$(uname -s)
70
- case "$platform" in
71
- Linux)
72
- # shellcheck source=/dev/null
73
- . /etc/os-release
74
- [ "$ID" = ubuntu ] && [ "$VERSION_ID" = 26.04 ] || die UBUNTU_BASELINE_MISMATCH
75
- case "$(uname -m)" in x86_64|aarch64) ;; *) die UBUNTU_ARCHITECTURE_UNSUPPORTED;; esac ;;
76
- Darwin)
77
- major=$(sw_vers -productVersion | cut -d. -f1)
78
- case "$major" in ''|*[!0-9]*) die MACOS_BASELINE_MISMATCH;; esac
79
- [ "$major" -ge 14 ] || die MACOS_BASELINE_MISMATCH
80
- case "$(uname -m)" in arm64|x86_64) ;; *) die MACOS_ARCHITECTURE_UNSUPPORTED;; esac ;;
81
- *) die PLATFORM_UNSUPPORTED;;
82
- esac
69
+ # Workers are the Ubuntu releases the policy accepts (docs/support-matrix.md).
70
+ [ "$(uname -s)" = Linux ] || die PLATFORM_UNSUPPORTED
71
+ # shellcheck source=/dev/null
72
+ . /etc/os-release
73
+ [ "$ID" = ubuntu ] || die UBUNTU_BASELINE_MISMATCH
74
+ case " $releases " in *" $VERSION_ID "*) ;; *) die UBUNTU_BASELINE_MISMATCH;; esac
75
+ case "$(uname -m)" in x86_64|aarch64) ;; *) die UBUNTU_ARCHITECTURE_UNSUPPORTED;; esac
76
+ ! grep -qiE 'microsoft|wsl' /proc/sys/kernel/osrelease 2>/dev/null || die WORKER_ENVIRONMENT_UNSUPPORTED
83
77
  root="$HOME/.local/share/personal/fleet"
84
78
  # Refuse symlinked state roots rather than allowing writes through them.
85
79
  for path in "$HOME/.local" "$HOME/.local/share" "$HOME/.local/share/personal" "$root"; do
@@ -89,14 +83,8 @@ mkdir -p "$root"
89
83
  phase=lock
90
84
  [ ! -L "$root/remote-operation.lock" ] || exit 1
91
85
  exec 9>"$root/remote-operation.lock"
92
- # macOS has no flock(1); lock the same descriptor through fcntl. The lock
93
- # follows this shell's open descriptor and is released when it exits.
94
- if command -v flock >/dev/null 2>&1; then
95
- flock -n 9 || die FLEET_REMOTE_LOCKED
96
- else
97
- command -v python3 >/dev/null 2>&1 || die BOOTSTRAP_LOCK_TOOL_REQUIRED
98
- python3 -c 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' 9>&9 || die FLEET_REMOTE_LOCKED
99
- fi
86
+ # The lock follows this shell's open descriptor and is released when it exits.
87
+ flock -n 9 || die FLEET_REMOTE_LOCKED
100
88
  # The operation record: a manager that lost its session reads it to follow
101
89
  # this operation to its end instead of starting another (see
102
90
  # src/remote-operation.ts). The result of the last operation is beside it.
@@ -132,14 +120,9 @@ if [ "$source_mode" = transfer ]; then
132
120
  bundle=$(mktemp "$root/source.XXXXXX")
133
121
  # Leave no executable payload copies; this temporary file is only Git data.
134
122
  base64 -d >"$bundle"
135
- if command -v sha256sum >/dev/null 2>&1; then
136
- printf '%s %s\n' "$bundle_sha" "$bundle" | sha256sum -c - >/dev/null 2>&1 || die SOURCE_DIGEST_MISMATCH
137
- else
138
- [ "$(shasum -a 256 "$bundle" | cut -d' ' -f1)" = "$bundle_sha" ] || die SOURCE_DIGEST_MISMATCH
139
- fi
123
+ printf '%s %s\n' "$bundle_sha" "$bundle" | sha256sum -c - >/dev/null 2>&1 || die SOURCE_DIGEST_MISMATCH
140
124
  fi
141
125
  phase=prerequisites
142
- if [ "$platform" = Linux ]; then
143
126
  missing=''
144
127
  for package in git curl ca-certificates unzip xz-utils build-essential python3 ansible-core; do
145
128
  dpkg-query -W -f='${db:Status-Status}' "$package" 2>/dev/null | grep -qx installed || missing="$missing $package"
@@ -171,12 +154,6 @@ if [ -n "$missing" ]; then
171
154
  rm -f -- "$pending" "$apt_log"
172
155
  apt_log=''
173
156
  fi
174
- else
175
- # Apple Command Line Tools provide git and python3; Ansible runs through the
176
- # pinned uv tool runner installed by the shared mise toolchain below.
177
- xcode-select -p >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
178
- command -v git >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
179
- fi
180
157
  phase=source-verification
181
158
  deployment="$root/revisions/$revision"
182
159
  [ ! -L "$root/revisions" ] && [ ! -L "$deployment" ] || exit 1
@@ -235,7 +212,7 @@ else
235
212
  toolchain_warm=''
236
213
  rm -f -- "$status_file" "$ready_file"
237
214
  fi
238
- if command -v timeout >/dev/null 2>&1; then bounded='timeout 1800'; else bounded=''; fi
215
+ bounded='timeout 1800'
239
216
  # shellcheck disable=SC2086
240
217
  if [ -z "$toolchain_warm" ] && ! $bounded bash bootstrap/bootstrap.sh --worker --node "$node" --non-interactive --status-file "$status_file" >/dev/null 2>&1; then
241
218
  tool_phase=$(sed -n 's/.*"phase":"\([a-z0-9-]*\)".*/\1/p' "$status_file" 2>/dev/null) || tool_phase=''
@@ -153,35 +153,35 @@ npx devfleet fleet-state --example > fleet.json
153
153
  Then replace every placeholder. Each field, from
154
154
  [`schema/fleet.schema.json`](../schema/fleet.schema.json):
155
155
 
156
- | Field | Value |
157
- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
158
- | `version` | `3` |
159
- | `source.repository` | `https://github.com/crvouga/dev-fleet.git`, or your fork if you publish one. Machines only accept code from it. |
160
- | `state` | `{ "bucket", "endpoint"?, "region"?, "prefix"? }` or `{ "directory": "/absolute/path" }`. Never credentials. |
161
- | `secrets` | See [secrets](#secrets). |
162
- | `manager.id` | A short lowercase id, `^[a-z][a-z0-9-]{0,62}$`, e.g. `manager-mac`. |
163
- | `manager.os` | `"macOS"` |
164
- | `manager.network.tailscaleName` | The manager's MagicDNS name: `tailscale status --json` → `.Self.DNSName` without the trailing dot. |
165
- | `manager.network.user` | The manager's login name (`id -un`). |
166
- | `manager.neverSleepOnPower` | `true` (workers depend on the manager's services). |
167
- | `workers[].id` | A short lowercase id. The **first** worker is the canary: a new revision must pass there before the others get it. |
168
- | `workers[].ssh` | The worker's alias in the manager's `~/.ssh/config`. |
169
- | `workers[].os` | `"linux"` |
170
- | `workers[].network.tailscaleName` | The worker's MagicDNS name (from `tailscale status --json` → `.Peer[].DNSName` on the manager). |
171
- | `workers[].network.sshPort` | `22` unless the worker's sshd listens elsewhere. |
172
- | `workers[].network.user` | The worker account (`ssh WORKER_ALIAS id -un`). |
173
- | `workers[].network.t3Port` | `3773` |
174
- | `services.proxy.endpoint` | The public HTTPS origin of the CLI proxy, e.g. `https://cli-proxy.example.com`. |
175
- | `services.monitoring.hubPort` | `8443` |
176
- | `services.monitoring.publicUrl` | The public HTTPS origin of the monitoring dashboard. |
177
- | `services.monitoring.managerAccount` | The email of the monitoring account the manager enrolls machines with. |
178
- | `services.monitoring.viewerAccount` | The email of the read-only account visitors are signed in as. |
179
- | `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }` (fixed). |
180
- | `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`. |
181
- | `worker.githubLogin` | The GitHub login every worker must be signed in as. |
182
- | `worker.vaultUser` | The Vault user each worker's token is issued for. |
183
- | `worker.toolchain`, `agentProfile` | `"mise.lock"`, `"proxy"` (fixed). |
184
- | `worker.unattendedSudo`, `persistentT3`, `neverSleep` | `true` (fixed policy). |
156
+ | Field | Value |
157
+ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
158
+ | `version` | `3` |
159
+ | `source.repository` | `https://github.com/crvouga/dev-fleet.git`, or your fork if you publish one. Machines only accept code from it. |
160
+ | `state` | `{ "bucket", "endpoint"?, "region"?, "prefix"? }` or `{ "directory": "/absolute/path" }`. Never credentials. |
161
+ | `secrets` | See [secrets](#secrets). |
162
+ | `manager.id` | A short lowercase id, `^[a-z][a-z0-9-]{0,62}$`, e.g. `manager-mac`. |
163
+ | `manager.os` | `"macOS"` |
164
+ | `manager.network.tailscaleName` | The manager's MagicDNS name: `tailscale status --json` → `.Self.DNSName` without the trailing dot. |
165
+ | `manager.network.user` | The manager's login name (`id -un`). |
166
+ | `manager.neverSleepOnPower` | `true` (workers depend on the manager's services). |
167
+ | `workers[].id` | A short lowercase id. The **first** worker is the canary: a new revision must pass there before the others get it. |
168
+ | `workers[].ssh` | The worker's alias in the manager's `~/.ssh/config`. |
169
+ | `workers[].os` | `"linux"` |
170
+ | `workers[].network.tailscaleName` | The worker's MagicDNS name (from `tailscale status --json` → `.Peer[].DNSName` on the manager). |
171
+ | `workers[].network.sshPort` | `22` unless the worker's sshd listens elsewhere. |
172
+ | `workers[].network.user` | The worker account (`ssh WORKER_ALIAS id -un`). |
173
+ | `workers[].network.t3Port` | `3773` |
174
+ | `services.proxy.endpoint` | The public HTTPS origin of the CLI proxy, e.g. `https://cli-proxy.example.com`. |
175
+ | `services.monitoring.hubPort` | `8443` |
176
+ | `services.monitoring.publicUrl` | The public HTTPS origin of the monitoring dashboard. |
177
+ | `services.monitoring.managerAccount` | The email of the monitoring account the manager enrolls machines with. |
178
+ | `services.monitoring.viewerAccount` | The email of the read-only account visitors are signed in as. |
179
+ | `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }`: a release of the [support matrix](support-matrix.md). |
180
+ | `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`. |
181
+ | `worker.githubLogin` | The GitHub login every worker must be signed in as. |
182
+ | `worker.vaultUser` | The Vault user each worker's token is issued for. |
183
+ | `worker.toolchain`, `agentProfile` | `"mise.lock"`, `"proxy"` (fixed). |
184
+ | `worker.unattendedSudo`, `persistentT3`, `neverSleep` | `true` (fixed policy). |
185
185
 
186
186
  Use the same values in every place they repeat. Validate before going on:
187
187
 
@@ -352,7 +352,9 @@ Read `summary` and `remedy.action` of the failing check. The common ones:
352
352
  | `TAILSCALE_NODE_OFFLINE` | The worker is off or off the tailnet. It is listed and skipped; others continue. |
353
353
  | `TAILSCALE_DEVICE_REPLACED` | The tailnet device behind a worker changed. Nothing is written to it until the human confirms the replacement. |
354
354
  | `SUDO_AUTHORIZATION_REQUIRED` | The worker account cannot use sudo without a password. Run the first sync interactively. |
355
- | `UBUNTU_BASELINE_MISMATCH` | The worker is not Ubuntu 26.04. |
355
+ | `UBUNTU_BASELINE_MISMATCH` | The worker is not an Ubuntu release the [support matrix](support-matrix.md) lists. |
356
+ | `WORKER_ENVIRONMENT_UNSUPPORTED` | The worker is WSL2 or a container. Use a physical or virtual machine of its own. |
357
+ | `PACKAGE_NEWER_THAN_PIN` (warning) | Ubuntu's security updates moved a pinned package past its pin. It is kept; the pin is a minimum. |
356
358
  | `ROOT_DEVELOPMENT_USER_REJECTED` | The worker's SSH user is root. Use a normal account with sudo. |
357
359
  | `BOOTSTRAP_DISK_CAPACITY_REQUIRED` | Less than 2 GiB free (or too few inodes) where the fleet keeps its files on the worker. |
358
360
  | `FLEET_REMOTE_LOCKED` | Another operation is running on that worker. Wait and run again. |
package/docs/declared.md CHANGED
@@ -47,11 +47,10 @@ stays in the playbook: the playbook is its declaration.
47
47
  launchd job from TypeScript and has never worked from the published package.
48
48
  It should become a playbook.
49
49
  - **The rest of a worker's packages.** Only the packages in `policy.json` are
50
- exact. `systemd-oomd` and the T3 desktop libraries are installed at whatever
51
- version the machine's own archive holds, and the machine's own updates move
52
- everything else. Pinning them needs every worker on the snapshot with its
53
- own updates off, so security fixes arrive only when the snapshot moves. That
54
- is a policy choice, not a missing line.
50
+ pinned. The T3 desktop libraries are installed at whatever version the
51
+ machine's own archive holds, and Ubuntu's security updates move everything
52
+ else, pinned packages included: a pin is a minimum
53
+ ([worker contract](worker-contract.md#package-pins-and-security-updates)).
55
54
  - **What a worker needs before Ansible exists.** `fleet-remote-bootstrap.sh`
56
55
  installs Git, curl, CA certificates, Python and `ansible-core` from the
57
56
  machine's own archive (the snapshot is HTTPS, and a new machine has no CA
package/docs/runbooks.md CHANGED
@@ -8,29 +8,26 @@ person (a login, an approval, a choice) is marked **needs a person**.
8
8
  `features/rules/runbooks.feature` fails when a code in the source has no
9
9
  entry here, or a `devfleet` command quoted in the documents does not exist.
10
10
 
11
- There are 241 codes; 31 need a person.
11
+ There are 238 codes; 29 need a person.
12
12
 
13
13
  ## Bootstrap of a new or returning worker
14
14
 
15
15
  - **Meaning:** The remote bootstrap (`bootstrap/fleet-remote-bootstrap.sh`) stopped before node convergence. It reports the phase and the code, never its output.
16
16
  - **Confirm:** Run `devfleet fleet-sync --dry-run` and read the `bootstrap:*` check of the worker; `ssh WORKER` and look under `~/.local/share/personal/fleet`.
17
17
  - **Fix:** Follow the remedy the check prints, then run `devfleet fleet-sync`. The bootstrap resumes where it stopped.
18
- - **Escalate:** A platform or architecture code means the machine must be reinstalled: Ubuntu 26.04 on x86_64 or aarch64.
18
+ - **Escalate:** A platform or architecture code means the machine must be reinstalled with a release [the support matrix](support-matrix.md) lists, on a machine of its own.
19
19
 
20
20
  - `BOOTSTRAP_CAPACITY_UNVERIFIED`: bootstrap capacity unverified.
21
21
  - `BOOTSTRAP_DISK_CAPACITY_REQUIRED`: bootstrap disk capacity required.
22
- - `BOOTSTRAP_LOCK_TOOL_REQUIRED`: bootstrap lock tool required.
23
22
  - `CONFIGURATION_INSTALL_FAILED`: configuration install failed.
24
23
  - `FLEET_APPROVED_BOOTSTRAP_UNAVAILABLE`: fleet approved bootstrap unavailable.
25
24
  - `FLEET_STATE_PATH_UNSAFE`: fleet state path unsafe.
26
- - `MACOS_ARCHITECTURE_UNSUPPORTED`: macos architecture unsupported. **Needs a person.**
27
- - `MACOS_BASELINE_MISMATCH`: macos baseline mismatch. **Needs a person.**
28
25
  - `PLATFORM_UNSUPPORTED`: platform unsupported. **Needs a person.**
29
26
  - `REMOTE_BOOTSTRAP_EVIDENCE_INVALID`: remote bootstrap evidence invalid.
30
27
  - `REMOTE_BOOTSTRAP_EXIT_FAILED`: remote bootstrap exit failed.
31
28
  - `ROOT_DEVELOPMENT_USER_REJECTED`: root development user rejected.
32
29
  - `UBUNTU_ARCHITECTURE_UNSUPPORTED`: ubuntu architecture unsupported. **Needs a person.**
33
- - `XCODE_COMMAND_LINE_TOOLS_REQUIRED`: xcode command line tools required. **Needs a person.**
30
+ - `WORKER_ENVIRONMENT_UNSUPPORTED`: worker environment unsupported: a container or WSL2. **Needs a person.**
34
31
 
35
32
  ## Ubuntu packages
36
33
 
@@ -0,0 +1,36 @@
1
+ # Support matrix
2
+
3
+ Which platforms dev-fleet runs on, in which role, and what happens to the
4
+ rest. The default is one macOS manager and Ubuntu workers treated as cattle.
5
+
6
+ - **supported**: converged, measured and tested on every pull request.
7
+ - **best effort**: works and is not tested as a fleet role.
8
+ - **refused**: stopped with the named reason before anything is installed.
9
+
10
+ <!-- Generated by `bun src/machine/support-matrix.ts` from ansible/macos/policy.json, ansible/ubuntu/policy.json and ansible/platforms.json; features/rules/support-matrix.feature fails when it differs. -->
11
+
12
+ | Role | Platform | Versions | Architectures | Status | Note |
13
+ | -------------- | --------- | ------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------------- |
14
+ | manager | macOS | 14 or newer | arm64, x86_64 | supported | ansible/macos/policy.json |
15
+ | worker | Ubuntu | 26.04 (resolute) | amd64, arm64 | supported | ansible/ubuntu/policy.json; a machine of its own, physical or virtual |
16
+ | manager | any other | any | any | refused | The fleet configuration accepts only a macOS manager. |
17
+ | worker | Ubuntu | any other release | any | refused | UBUNTU_BASELINE_MISMATCH |
18
+ | worker | Ubuntu | an accepted release | any other | refused | UBUNTU_BASELINE_MISMATCH |
19
+ | worker | Debian | any | any | refused | UBUNTU_BASELINE_MISMATCH |
20
+ | worker | macOS | any | any | refused | The fleet configuration accepts only Linux workers. |
21
+ | worker | Windows | any | any | refused | The fleet configuration accepts only Linux workers. |
22
+ | worker | WSL2 | any | any | refused | WORKER_ENVIRONMENT_UNSUPPORTED |
23
+ | worker | container | any | any | refused | WORKER_ENVIRONMENT_UNSUPPORTED |
24
+ | toolchain only | macOS | any | arm64, x86_64 | best effort | No fleet role: devfleet bootstrap installs the pinned tools (docs/machine-setup.md). |
25
+ | toolchain only | Linux | any | x86_64, aarch64 | best effort | No fleet role: devfleet bootstrap installs the pinned tools (docs/machine-setup.md). |
26
+ | toolchain only | WSL2 | any | x86_64 | best effort | Treated as Linux: devfleet bootstrap installs the pinned tools (docs/machine-setup.md). |
27
+ | toolchain only | Windows | any | x64 | best effort | No fleet role: bootstrap/bootstrap.ps1 installs the pinned tools (docs/machine-setup.md). |
28
+ | toolchain only | Alpine | any | any | refused | musl: the pinned mise build needs glibc. |
29
+
30
+ A worker is refused by the first check that can tell: the fleet configuration
31
+ refuses a worker that is not Linux, the platform probe refuses another
32
+ release, CPU or environment (`ubuntu:release`, `platform:environment`), and
33
+ the bootstrap and the Ubuntu baseline refuse them again on the machine.
34
+
35
+ Supporting another platform means adding it to the policy that names it, with
36
+ its package pins, a fresh-box job and its row here.
@@ -61,10 +61,9 @@ resolve enrolled fleet
61
61
 
62
62
  Bare invocation selects every canonical Linux and macOS node. It does not mean
63
63
  “this computer,” and the manager is not implicitly a deployment target. Its own
64
- node is targeted only if explicitly declared in Fleet membership. Ubuntu 26.04 is
65
- the accepted worker platform; a node declared `"os": "macOS"` runs the same phases
66
- best effort through `ansible/macos` (see the platform matrix below).
67
- Windows machines stay declared for diagnostics and are never converged.
64
+ node is targeted only if explicitly declared in Fleet membership. Workers are the
65
+ Ubuntu releases the [support matrix](support-matrix.md) lists; it also says what
66
+ is refused (macOS and Windows workers, WSL2, containers) and why.
68
67
  `--node ID` filters troubleshooting/rollout. `--all` is an explicit spelling of the
69
68
  default. Neither flag authorizes managing arbitrary Tailscale peers.
70
69
 
@@ -217,9 +216,12 @@ provider configuration: agent profiles are applied by `fleet-sync`. See
217
216
 
218
217
  ## Ubuntu and remote-work safety
219
218
 
220
- The declared common baseline is Ubuntu **26.04 LTS**, amd64/arm64. Other releases
221
- receive `UBUNTU_BASELINE_MISMATCH`; there is no automatic release upgrade or
222
- package downgrade. Fleet audits release, packages, service state and dpkg
219
+ The accepted releases are named once, in `ansible/ubuntu/policy.json`, and
220
+ listed in the [support matrix](support-matrix.md). Other releases receive
221
+ `UBUNTU_BASELINE_MISMATCH`, and WSL2 and containers
222
+ `WORKER_ENVIRONMENT_UNSUPPORTED`; there is no automatic release upgrade or
223
+ package downgrade. Package pins are minimums: see the
224
+ [worker contract](worker-contract.md#package-pins-and-security-updates). Fleet audits release, packages, service state and dpkg
223
225
  consistency, declares snapshot sources for managed transactions, and pins its
224
226
  selected APT packages. Unrelated applications and package sources are preserved.
225
227
 
@@ -12,7 +12,8 @@ devfleet fleet-status --workers
12
12
  ```
13
13
 
14
14
  The manager needs Node/npm, Git, SSH, Tailscale, and its own authorized T3 link.
15
- Initially each Ubuntu 26.04 worker (or best-effort macOS 14+ node) needs a
15
+ Initially each worker (an Ubuntu release the [support matrix](support-matrix.md)
16
+ lists) needs a
16
17
  designated non-root account, trusted SSH access and administrator authorization. Standard Ubuntu cloud-image users
17
18
  already commonly have noninteractive sudo; otherwise establish the repository's
18
19
  `authorize-sudo.sh` policy once during administrator enrollment. Routine commands
@@ -39,6 +40,32 @@ HEAD. Private rosters contain connection paths and identities, not weaker policy
39
40
  | No routine password prompts | Verified BatchMode SSH, `sudo -n`, source-owned NOPASSWD policy; missing initial authorization is a failure |
40
41
  | Keep service authorization alive | Native service credentials stay per-node; Vault self-renewal timer and GitHub recovery from authorized Vault credentials; expiring T3 links are reported before expiry |
41
42
 
43
+ ## Package pins and security updates
44
+
45
+ A pin in `ansible/ubuntu/policy.json` is a **minimum**, never a hold. Ubuntu
46
+ keeps installing security updates by itself, and a worker is not turned red by
47
+ one:
48
+
49
+ - The Ubuntu baseline installs a pinned package from the snapshot only when it
50
+ is absent or older than its pin, and never downgrades
51
+ (`ansible/ubuntu/tasks/install.yml`; every pinned package, `systemd-oomd`
52
+ included, goes through it).
53
+ - What Ubuntu installs unattended is declared in
54
+ `/etc/apt/apt.conf.d/52personal-fleet-unattended-upgrades`: security updates
55
+ only, no automatic reboot, no removals.
56
+ - `worker:package:NAME` passes at the pin, **warns** with
57
+ `PACKAGE_NEWER_THAN_PIN` above it, and fails with `PACKAGE_BELOW_PIN` below
58
+ it or when the package is missing. A warning does not block convergence or
59
+ readiness; it says the pin can be raised when the snapshot moves.
60
+ - Tailscale follows the same rule: a newer Tailscale is kept; an older one
61
+ stops with `TAILSCALE_UPGRADE_PENDING`, because upgrading an active VPN can
62
+ drop SSH.
63
+
64
+ A package run cut off by a power loss is finished by the next sync: the
65
+ baseline waits for the APT lock, runs `dpkg --configure -a` once (bounded to
66
+ ten minutes) and audits again; only a database still inconsistent stops with
67
+ `APT_STATE_BROKEN`.
68
+
42
69
  Health does not mutate configuration, renew tokens, log in, install software, or
43
70
  make billable inference requests. It audits the immutable desired worker deployment,
44
71
  not an unrelated developer branch. Every declared worker must be enrolled and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/dev-fleet",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Keeps a set of development machines in sync: one manager and many workers",
5
5
  "license": "MIT",
6
6
  "repository": "github:crvouga/dev-fleet",
@@ -72,5 +72,5 @@
72
72
  "engines": {
73
73
  "node": ">=22"
74
74
  },
75
- "gitHead": "f2813f818b062ed53335afbeee8f67de3b3ac4d4"
75
+ "gitHead": "620b8de05d662b4dc9072457580ca4b4eab54242"
76
76
  }
@@ -174,10 +174,6 @@
174
174
  "id": "login:*",
175
175
  "meaning": "A native per-machine login is in place."
176
176
  },
177
- {
178
- "id": "macos:release",
179
- "meaning": "The manager runs a supported macOS release."
180
- },
181
177
  {
182
178
  "id": "manager:docker",
183
179
  "meaning": "The manager runs the pinned Docker Desktop."
@@ -202,6 +198,10 @@
202
198
  "id": "notify:configured",
203
199
  "meaning": "Every notification target fleet.json names has its address available as a secret."
204
200
  },
201
+ {
202
+ "id": "platform:environment",
203
+ "meaning": "The worker runs on a physical or virtual machine of its own, not in a container or WSL2."
204
+ },
205
205
  {
206
206
  "id": "platform:unsupported",
207
207
  "meaning": "The machine runs a supported operating system."
@@ -486,6 +486,10 @@
486
486
  "id": "worker:github-principal",
487
487
  "meaning": "The worker's GitHub login is the declared account."
488
488
  },
489
+ {
490
+ "id": "worker:package:*",
491
+ "meaning": "The pinned Ubuntu package is installed at or past its pin."
492
+ },
489
493
  {
490
494
  "id": "worker:power-loss-recovery",
491
495
  "meaning": "The worker powers on after AC loss."