@crvouga/dev-fleet 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -170,16 +170,10 @@ says where the bucket is and the command is passed its credentials:
170
170
  npx devfleet fleet-state # where the state is, and how it stands with the bucket
171
171
  ```
172
172
 
173
- The copy on the manager is disposable. **To replace the manager**, clone your
174
- project on the new machine and run the same two commands:
175
-
176
- ```sh
177
- npm ci
178
- npx devfleet fleet-sync # with the two bucket credentials in the environment
179
- ```
180
-
181
- The roster and the approved revision come back from the bucket, and the
182
- machine is rebuilt from Git.
173
+ The copy on the manager is disposable: the roster and the approved revision
174
+ come back from the bucket, and the machine is rebuilt from Git.
175
+ [Replacing the manager](docs/manager-recovery.md) is the runbook for a new or
176
+ lost manager.
183
177
 
184
178
  Some runs are started by a machine rather than by you: a worker that hands
185
179
  `fleet-sync` to the manager, for example. They get no environment, so save the
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  # Manager power policy. Workers depend on the manager's singleton services
3
- # (CLI proxy, Beszel hub), so the manager never sleeps on AC power. Sleep on
4
- # battery is left to the human. The settings are the `pmset` object in
3
+ # (CLI proxy, Beszel hub), so the manager never sleeps on AC power and starts
4
+ # again by itself when power returns (autorestart). Sleep on battery is left
5
+ # to the human. The settings are the `pmset` object in
5
6
  # policy.json; fleet-sync measures the same object as manager:power. Changing
6
7
  # them needs an administrator: ansible-playbook -K manager.yml
7
8
  - name: Keep the fleet manager awake on AC power
@@ -29,5 +30,41 @@
29
30
  when: observed != item.value
30
31
  changed_when: true
31
32
 
33
+ # Docker Desktop hosts the CLI proxy and the monitoring hub, so it starts at
34
+ # login: after a power cut the manager comes back without a person. The
35
+ # setting is the `dockerDesktop` object in policy.json; fleet-sync measures it
36
+ # as part of manager:boot-recovery. Only that key of the user's settings file
37
+ # is changed.
38
+ - name: Start Docker Desktop at login
39
+ hosts: localhost
40
+ connection: local
41
+ become: false
42
+ gather_facts: false
43
+ vars_files:
44
+ - policy.json
45
+ vars:
46
+ docker_settings: "{{ lookup('ansible.builtin.env', 'HOME') }}/{{ dockerDesktop.settings }}"
47
+ tasks:
48
+ - name: Find the Docker Desktop settings
49
+ ansible.builtin.stat:
50
+ path: '{{ docker_settings }}'
51
+ register: docker_settings_file
52
+ - name: Read the Docker Desktop settings
53
+ ansible.builtin.slurp:
54
+ src: '{{ docker_settings }}'
55
+ register: docker_settings_text
56
+ when: docker_settings_file.stat.exists
57
+ - name: Turn on start at login, keeping every other setting
58
+ ansible.builtin.copy:
59
+ dest: '{{ docker_settings }}'
60
+ content: "{{ current | combine({dockerDesktop.key: true}) | to_nice_json(indent=2) }}\n"
61
+ mode: '0644'
62
+ vars:
63
+ current: '{{ docker_settings_text.content | b64decode | from_json }}'
64
+ # Docker Desktop writes the file the first time it runs.
65
+ when:
66
+ - docker_settings_file.stat.exists
67
+ - (current[dockerDesktop.key] | default(false)) != true
68
+
32
69
  - name: Stage the fleet's T3 policy
33
70
  ansible.builtin.import_playbook: t3-policy.yml
@@ -4,7 +4,8 @@
4
4
  "architectures": ["arm64", "x86_64"],
5
5
  "pmset": {
6
6
  "sleep": "0",
7
- "womp": "1"
7
+ "womp": "1",
8
+ "autorestart": "1"
8
9
  },
9
10
  "beszelDaemon": "dev.devfleet.beszel-agent",
10
11
  "disk": {
@@ -16,5 +17,9 @@
16
17
  "unhealthyChecks": 3,
17
18
  "maxRestartsPerHour": 3,
18
19
  "intervalSeconds": 60
20
+ },
21
+ "dockerDesktop": {
22
+ "settings": "Library/Group Containers/group.com.docker/settings-store.json",
23
+ "key": "AutoStart"
19
24
  }
20
25
  }
@@ -4,6 +4,8 @@
4
4
  connection: local
5
5
  become: true
6
6
  gather_facts: true
7
+ # fleet-repair re-applies this play alone for a failing beszel agent check.
8
+ tags: [monitoring]
7
9
  vars:
8
10
  beszel_lock: "{{ lookup('ansible.builtin.file', 'lock.json') | from_json }}"
9
11
  beszel_arch: "{{ {'x86_64': 'amd64', 'aarch64': 'arm64'}.get(ansible_facts.architecture, 'unsupported') }}"
@@ -8,24 +8,30 @@
8
8
  - policy.json
9
9
  pre_tasks:
10
10
  - ansible.builtin.import_tasks: tasks/release.yml
11
+ tags: [always]
11
12
  - name: Require an explicit non-root user
12
13
  ansible.builtin.assert:
13
14
  that:
14
15
  - development_user is match('^[a-z_][a-z0-9_-]{0,31}$')
15
16
  - development_user != 'root'
16
17
  fail_msg: Unsupported user; name the non-root developer account.
18
+ tags: [always]
17
19
  - name: Read the designated existing account
18
20
  ansible.builtin.getent:
19
21
  database: passwd
20
22
  key: '{{ development_user }}'
23
+ tags: [always]
21
24
  - name: Refuse a different user identity or home
22
25
  ansible.builtin.assert:
23
26
  that:
24
27
  - ansible_facts.getent_passwd[development_user][1] | int == development_uid | int
25
28
  - ansible_facts.getent_passwd[development_user][4] == development_home
29
+ tags: [always]
26
30
  - ansible.builtin.import_tasks: tasks/dpkg.yml
27
31
  # Memory and disk safety must land even when the disk is nearly full.
32
+ # fleet-repair re-applies them alone (--tags disk) for worker:capacity:*.
28
33
  - ansible.builtin.import_tasks: tasks/resources.yml
34
+ tags: [disk]
29
35
  - name: Inspect capacity before package and service changes
30
36
  ansible.builtin.command:
31
37
  argv:
@@ -4,6 +4,8 @@
4
4
  connection: local
5
5
  become: true
6
6
  gather_facts: false
7
+ # fleet-repair re-applies this play alone for worker:service-recovery.
8
+ tags: [recovery]
7
9
  tasks:
8
10
  - name: Verify the desktop unit runs before adding any recovery trigger
9
11
  ansible.builtin.command:
@@ -4,6 +4,8 @@
4
4
  connection: local
5
5
  become: true
6
6
  gather_facts: true
7
+ # fleet-repair re-applies this play alone for a failing tailscale:* check.
8
+ tags: [network]
7
9
  # The approved version and the signing key's digest: `tailscale` in policy.json.
8
10
  vars_files:
9
11
  - policy.json
@@ -382,10 +382,8 @@ and codes; leave out everything private.
382
382
  - **Upgrade the fleet.** `npm install --save-dev --save-exact @crvouga/dev-fleet@latest`,
383
383
  commit, `npx devfleet fleet-sync`. The canary proves the new revision first.
384
384
  Install an older version and sync to roll back.
385
- - **Replace the manager.** On the new Mac: clone the project, `npm ci`, put
386
- the bucket credentials in the environment, `npx devfleet fleet-sync`. The
387
- roster and the approved revision come back from the bucket. See
388
- [state](state.md) for what the bucket does not hold yet.
385
+ - **Replace the manager.** Follow
386
+ [replacing the manager](manager-recovery.md).
389
387
 
390
388
  ## Moving from a pinned commit and a wrapper script
391
389
 
@@ -0,0 +1,78 @@
1
+ # Replacing the manager
2
+
3
+ This is the one runbook for a manager that is replaced or lost: stolen, a dead
4
+ disk, or a new machine. Other documents link here and do not describe the
5
+ procedure themselves.
6
+
7
+ ## What must exist off the machine
8
+
9
+ Nothing on the old manager is needed. Before you need this runbook, make sure
10
+ these exist elsewhere:
11
+
12
+ | What | Where |
13
+ | ----------------------------------- | ------------------------------------------------------------------------ |
14
+ | Your project: `fleet.json`, the pin | Its Git remote |
15
+ | The fleet state | The bucket `fleet.json` names under `state` |
16
+ | The bucket's two credentials | Your password manager (never the manager alone) |
17
+ | Secrets | Your `secrets` provider; `file` and `env` secrets must be kept elsewhere |
18
+ | The CLI proxy's encrypted snapshots | Its restic repository (`vault` secrets provider only) |
19
+ | Your Tailscale and Docker accounts | Yours: signing in is the only step only you can do |
20
+
21
+ ## Restore
22
+
23
+ On the new machine, with Git, Node and Docker installed:
24
+
25
+ 1. Give it the manager's local host name: `manager.id` in `fleet.json`
26
+ (`sudo scutil --set LocalHostName ID` on macOS). `fleet-sync` refuses to
27
+ run on any other machine.
28
+ 2. Sign in to Tailscale and approve the device. Sign in to Docker Desktop if
29
+ your plan needs it.
30
+ 3. Fetch the project and check every prerequisite. Nothing is changed:
31
+
32
+ ```sh
33
+ git clone YOUR-PROJECT && cd YOUR-PROJECT && npm ci
34
+ export DEVFLEET_BUCKET_ACCESS_KEY_ID=… DEVFLEET_BUCKET_SECRET_ACCESS_KEY=…
35
+ npx devfleet fleet-restore --dry-run
36
+ ```
37
+
38
+ It fails on anything missing (`restore:*` checks, each with its remedy)
39
+ and lists what will be restored, in order.
40
+
41
+ 4. Restore, then converge:
42
+
43
+ ```sh
44
+ npx devfleet fleet-restore # the fleet state, from the bucket
45
+ npx devfleet fleet-sync --take-over # the manager, then every worker
46
+ npx devfleet fleet-state --init # credentials for runs the manager starts itself
47
+ npx devfleet cliproxy install # the proxy, from its snapshots
48
+ ```
49
+
50
+ The proxy's own steps are in [its migration runbook](../cliproxy/docs/migration.md).
51
+ `--take-over` is needed while the old manager's lease runs (seven days
52
+ after its last change). Use it only once the old machine is gone: an old
53
+ manager that wakes up afterwards is refused with `FLEET_MANAGER_LEASE_HELD`.
54
+
55
+ 5. Adopt the T3 instances again: `npx devfleet t3-apply --import ID`.
56
+
57
+ Workers need nothing: they reach the manager by its Tailscale name, so they
58
+ follow it once the new machine holds that name. If the Tailscale name
59
+ changed, change `manager.network.tailscaleName` in `fleet.json` and run
60
+ `fleet-sync`, which re-points every worker.
61
+
62
+ ## What is lost
63
+
64
+ The monitoring hub's metric history, alerts and settings made by hand. Its
65
+ accounts and systems are recreated. With `env` or `file` secrets, whatever
66
+ was only on the old manager.
67
+
68
+ ## Coming back by itself
69
+
70
+ A manager restarts after a power cut only when it is set up to
71
+ (`manager:boot-recovery` in `fleet-status`): `autorestart` and Docker Desktop
72
+ at login are applied by `ansible/macos/manager.yml`. The per-user services
73
+ need a user session. **With FileVault on, a Mac waits at the login window
74
+ after every restart until a person signs in**, so nothing comes back
75
+ unattended. That is the trade-off: FileVault protects the proxy's logins and
76
+ the bucket credentials on a stolen Mac; automatic login without FileVault
77
+ lets the fleet recover from a power cut alone. `fleet-status` reports
78
+ FileVault as a warning with that consequence; the choice is yours.
@@ -304,6 +304,14 @@ The public CLI exposes `devfleet fleet-repair --list` (static catalog) and
304
304
  in this package only executes allowlisted existing owner commands. Native
305
305
  account logins and unknown/manual checks always remain operator steps.
306
306
 
307
+ Every check ID has an owner in `src/check-owners.ts`: a recipe, or the reason
308
+ only a person can repair it, which the plan prints beside the check
309
+ (`manual`). Machine repairs are one tag of a playbook the worker already
310
+ applies: `recovery` (`worker:service-recovery`), `disk`
311
+ (`worker:capacity:*`), `monitoring` (the Beszel agent) and `network` (the
312
+ Tailscale daemon). `state:*` checks take the fleet state from the bucket
313
+ again. A feature fails when a check is reported that has no owner.
314
+
307
315
  Use `--apply --yes` only after review; `--plan-hash` binds application to the
308
316
  previewed steps, targets and checkout/shared-input fingerprints. The remote
309
317
  protocol independently requires explicit apply, a supported repair action and
package/docs/state.md CHANGED
@@ -64,9 +64,9 @@ arguments and never written to a report or a log.
64
64
 
65
65
  **A new fleet.** Nothing more: the first `fleet-sync` seeds the bucket.
66
66
 
67
- **A replacement manager, or a second machine.** Clone your project, `npm ci`,
68
- and run `fleet-sync` with the credentials. The bucket already holds the fleet,
69
- so its state is downloaded before anything is changed.
67
+ **A replacement manager.** The bucket already holds the fleet, so its state is
68
+ downloaded before anything is changed. Follow
69
+ [replacing the manager](manager-recovery.md).
70
70
 
71
71
  **Runs the manager starts by itself.** A worker that hands `fleet-sync` to the
72
72
  manager starts a command there that gets no environment and no project. Save
@@ -172,6 +172,7 @@ it records what it applied. Its report carries the result as the
172
172
  | `local-ahead` | This machine has changes the bucket has not received | The next change, or `--push`, uploads them |
173
173
  | `bucket-ahead` | Another manager changed the bucket; this copy is stale | The next change or `fleet-sync` takes the bucket's version |
174
174
  | `unreachable` | The bucket did not answer, or did not take an upload | Changes still apply locally and are uploaded later |
175
+ | `corrupt` | The bucket answered with a version that cannot be read | Changes apply locally; `--restore --timestamp` repairs it |
175
176
  | `diverged` | Both sides changed | Refused. `--restore --yes` takes the bucket's version |
176
177
 
177
178
  A stale copy never overwrites the bucket. A machine that was offline while
@@ -210,6 +211,7 @@ once `fleet-sync` has fetched it back.
210
211
  | `DEVFLEET_CONFIG` | A configuration file, instead of the project's `fleet.json` |
211
212
  | `DEVFLEET_STATE` | Another state directory. Earlier files are not read for it |
212
213
  | `DEVFLEET_BUCKET` | Another `bucket.json` |
214
+ | `DEVFLEET_MANAGER_HOLDER` | How the manager lease names this machine |
213
215
 
214
216
  How a command reaches the bucket:
215
217
 
@@ -254,7 +256,37 @@ mise exec litestream -- litestream restore -no-expand-env \
254
256
  ~/.local/share/dev-fleet/state/fleet.db
255
257
  ```
256
258
 
259
+ `devfleet fleet-state --restore --timestamp 2026-11-01T00:00:00Z --yes` does
260
+ the same and then uploads that version as the bucket's current one. It is the
261
+ remedy for a `corrupt` replica: the bucket answers, but its latest version
262
+ cannot be read.
263
+
257
264
  Litestream is pinned in `mise.toml` and installed the first time it is used.
258
- The manager is the only machine that runs it. There is one manager, so there is
259
- one writer; two machines changing the state at the same moment are not
260
- supported, only detected afterwards.
265
+ The manager is the only machine that runs it.
266
+
267
+ ## One manager at a time
268
+
269
+ The fleet state carries a **manager lease**: which machine is the manager,
270
+ and until when. Every change takes or renews it, and it reaches the bucket
271
+ with the change. A second machine (a replacement while the old manager still
272
+ runs, or an old manager that wakes up) reads it from the bucket before it
273
+ writes anything and stops with `FLEET_MANAGER_LEASE_HELD`. `fleet-state
274
+ --init` still restores the state on such a machine; it only writes nothing.
275
+
276
+ The lease runs for seven days after the last change. After that, any machine
277
+ may take it. Before that, `--take-over` (on `fleet-sync`, `fleet-state --init`
278
+ or `fleet-state --import`) makes this machine the manager; use it only once
279
+ the other one is gone ([replacing the manager](manager-recovery.md)).
280
+
281
+ A machine is named in the lease by a digest of its machine ID
282
+ (`/etc/machine-id` on Linux, the hardware UUID on macOS), never by its host
283
+ name, which a replacement shares. `DEVFLEET_MANAGER_HOLDER` names it instead.
284
+
285
+ ## Schema version
286
+
287
+ The database records its schema version in `PRAGMA user_version`. The schema
288
+ is a series of numbered migrations (`src/state/migrations/0001-….sql`, …),
289
+ and a command brings an older database forward before it writes. A database a
290
+ newer dev-fleet wrote is refused with `FLEET_STATE_NEWER_THAN_CLI`, read or
291
+ written, and nothing in it changes: install the dev-fleet version the fleet
292
+ runs.
package/docs/topology.md CHANGED
@@ -41,7 +41,8 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
41
41
  manager is asleep, closed or offline, workers have no CLI proxy and no
42
42
  dashboard; nothing fails over, because a singleton never runs twice.
43
43
  - Its state (proxy OAuth/runtime state, the hub database) is backed up off the
44
- machine as each service documents, so a replacement manager restores it.
44
+ machine as each service documents, so a replacement manager restores it
45
+ ([replacing the manager](manager-recovery.md)).
45
46
 
46
47
  ## Workers
47
48
 
@@ -63,7 +64,5 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
63
64
 
64
65
  - **Add or replace a worker:** add it to `workers`, enroll it with
65
66
  `fleet-sync`, done. Remove it by deleting the entry.
66
- - **Replace the manager:** edit `manager`, then move each singleton service with
67
- its documented fence-and-restore runbook (never two copies at once):
68
- [CLI proxy](../cliproxy/docs/migration.md),
69
- [Beszel hub](../ansible/monitoring/README.md).
67
+ - **Replace the manager:** follow
68
+ [replacing the manager](manager-recovery.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/dev-fleet",
3
- "version": "0.6.0",
3
+ "version": "0.7.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": "9f9718f2853ed7840c846c8dfc8750903c782b87"
75
+ "gitHead": "77efce7ad99a3d521169a5e773ad6b97ca21bc75"
76
76
  }
@@ -218,6 +218,10 @@
218
218
  "id": "login:*",
219
219
  "meaning": "A native per-machine login is in place."
220
220
  },
221
+ {
222
+ "id": "manager:boot-recovery",
223
+ "meaning": "The manager comes back by itself after a power cut or reboot."
224
+ },
221
225
  {
222
226
  "id": "manager:docker",
223
227
  "meaning": "The manager runs the pinned Docker Desktop."
@@ -0,0 +1,163 @@
1
+ /** Who repairs a check: a repair recipe (`repair`), or the reason only a
2
+ * person can (`manual`). Every check ID dev-fleet reports matches one entry,
3
+ * by its exact ID or by a prefix ending in `*` (`beszel:*`). The most
4
+ * specific match wins. `login:*` is the sign-in recipe named after the rest
5
+ * of an `auth:` check's ID.
6
+ */
7
+ export interface CheckOwner {
8
+ pattern: string;
9
+ repair?: string;
10
+ manual?: string;
11
+ }
12
+
13
+ const IDENTITY =
14
+ 'It needs a person: a sign-in, an approval or a choice only the operator can make.';
15
+ const EVIDENCE =
16
+ 'It reports how a command or a run went, not the state of a machine: repeat the command once its remedy is followed.';
17
+ const DECLARATION =
18
+ 'The fleet configuration (fleet.json) or the roster is wrong: only the operator edits them.';
19
+ const HARDWARE =
20
+ 'It is a property of the hardware, the firmware or the operating system release, which no command changes.';
21
+
22
+ export const CHECK_OWNERS: CheckOwner[] = [
23
+ // Toolchain and workstation: automatic.
24
+ { pattern: 'tool:*', repair: 'workspace' },
25
+ { pattern: 'tool:docker-*', repair: 'docker' },
26
+ { pattern: 'tool:github-auth*', repair: 'login:github' },
27
+ { pattern: 'dependencies', repair: 'workspace' },
28
+ { pattern: 'auth:toolchain', repair: 'workspace' },
29
+ { pattern: 'workstation:*', repair: 'workstation' },
30
+ { pattern: 'auth:*', repair: 'login:*' },
31
+ { pattern: 'auth:vscode', manual: IDENTITY },
32
+ { pattern: 'auth:workstation', manual: IDENTITY },
33
+ { pattern: 't3:settings', repair: 't3' },
34
+ { pattern: 't3:api', repair: 't3' },
35
+ // The checks that fail when a machine breaks.
36
+ { pattern: 'worker:service-recovery', repair: 'recovery' },
37
+ { pattern: 'worker:capacity', repair: 'disk' },
38
+ { pattern: 'worker:capacity:*', repair: 'disk' },
39
+ { pattern: 'beszel:agent', repair: 'monitoring' },
40
+ { pattern: 'beszel:service', repair: 'monitoring' },
41
+ { pattern: 'beszel:installation', repair: 'monitoring' },
42
+ { pattern: 'beszel:protocol', repair: 'monitoring' },
43
+ { pattern: 'tailscale:configuration', repair: 'network' },
44
+ { pattern: 'tailscale:online', repair: 'network' },
45
+ { pattern: 'tailscale:reachability', repair: 'network' },
46
+ { pattern: 'state:replica', repair: 'state' },
47
+ { pattern: 'state:*', repair: 'state' },
48
+ // What only a person can do.
49
+ {
50
+ pattern: 'beszel:*',
51
+ manual:
52
+ 'The monitoring hub and its accounts are converged by fleet-sync on the manager; follow the remedy, then run fleet-sync.',
53
+ },
54
+ {
55
+ pattern: 'tailscale:*',
56
+ manual:
57
+ 'Signing a machine in to the tailnet and approving it is a person’s; the daemon itself is repaired by the network recipe.',
58
+ },
59
+ {
60
+ pattern: 'state:bucket',
61
+ manual:
62
+ 'The bucket and its credentials are the operator’s: name them in fleet.json and pass the credentials.',
63
+ },
64
+ { pattern: 'state:definition', manual: DECLARATION },
65
+ {
66
+ pattern: 'worker:*',
67
+ manual:
68
+ 'Re-converge the worker with fleet-sync: it applies the whole worker policy at the approved revision.',
69
+ },
70
+ { pattern: 'worker:power-loss-recovery', manual: HARDWARE },
71
+ { pattern: 'worker:github-principal', manual: IDENTITY },
72
+ { pattern: 'worker:vault-renewal', manual: IDENTITY },
73
+ {
74
+ pattern: 'manager:*',
75
+ manual:
76
+ 'The manager converges itself in fleet-sync; a setting that needs an administrator is applied with ansible-playbook -K ansible/macos/manager.yml.',
77
+ },
78
+ { pattern: 'manager:identity', manual: DECLARATION },
79
+ {
80
+ pattern: 'manager:docker',
81
+ manual:
82
+ 'Docker Desktop is installed and updated by a person; no playbook installs it.',
83
+ },
84
+ { pattern: 'macos:*', manual: HARDWARE },
85
+ { pattern: 'ubuntu:release', manual: HARDWARE },
86
+ {
87
+ pattern: 'ubuntu:*',
88
+ manual:
89
+ 'Re-converge the worker with fleet-sync: it applies the Ubuntu baseline.',
90
+ },
91
+ { pattern: 'platform:*', manual: HARDWARE },
92
+ {
93
+ pattern: 'ssh:*',
94
+ manual:
95
+ 'SSH trust (host keys, keys in the agent, the alias) is set up by a person once per machine; dev-fleet never accepts a host key for you.',
96
+ },
97
+ { pattern: 'login:*', manual: IDENTITY },
98
+ { pattern: 'provider:*', manual: IDENTITY },
99
+ {
100
+ pattern: 'agent:*',
101
+ manual:
102
+ 'Re-converge the machine with fleet-sync; an agent that needs a sign-in is a person’s.',
103
+ },
104
+ {
105
+ pattern: 't3:*',
106
+ manual:
107
+ 'T3 pairing and its private route are approved by a person; fleet-sync applies the rest.',
108
+ },
109
+ { pattern: 'vscode:*', manual: IDENTITY },
110
+ {
111
+ pattern: 'git:*',
112
+ manual:
113
+ 'Uncommitted or unpushed work belongs to a person; dev-fleet never resets a checkout.',
114
+ },
115
+ {
116
+ pattern: 'git:configuration',
117
+ manual:
118
+ 'fleet-sync writes the Git identity and credential helper with ansible/github/git.yml; run it from the manager.',
119
+ },
120
+ {
121
+ pattern: 'github:*',
122
+ manual:
123
+ 'fleet-sync signs every node in to GitHub from the stored github-token (a worker retries every ten minutes); a token that is missing, expired or short of a scope or organisation needs a person to issue a new one.',
124
+ },
125
+ {
126
+ pattern: 'github:git-credential',
127
+ manual:
128
+ 'fleet-sync writes the GitHub credential helper with ansible/github/git.yml when the git block is declared; run it from the manager.',
129
+ },
130
+ { pattern: 'github:rate-limit', manual: EVIDENCE },
131
+ { pattern: 'github:workflow', manual: EVIDENCE },
132
+ { pattern: 'fleet:*', manual: DECLARATION },
133
+ { pattern: 'enrollment:*', manual: DECLARATION },
134
+ { pattern: 'notify:*', manual: DECLARATION },
135
+ {
136
+ pattern: 'proxy:*',
137
+ manual:
138
+ 'The CLI proxy is installed with devfleet cliproxy install and its accounts are signed in by a person.',
139
+ },
140
+ {
141
+ pattern: 'accounts:roster-current',
142
+ manual:
143
+ 'The roster reaches every node with devfleet fleet-sync --only accounts, run from the manager.',
144
+ },
145
+ { pattern: 'accounts:native-login', manual: IDENTITY },
146
+ { pattern: 'configuration:*', manual: EVIDENCE },
147
+ { pattern: 'source:*', manual: EVIDENCE },
148
+ { pattern: 'bootstrap:*', manual: EVIDENCE },
149
+ { pattern: 'rollout:*', manual: EVIDENCE },
150
+ { pattern: 'repair:*', manual: EVIDENCE },
151
+ ];
152
+
153
+ const matches = (pattern: string, id: string): boolean =>
154
+ pattern.endsWith('*') ? id.startsWith(pattern.slice(0, -1)) : id === pattern;
155
+
156
+ /** The entry that owns a check, or null for an ID nobody registered. */
157
+ export function checkOwner(id: string): CheckOwner | null {
158
+ return (
159
+ CHECK_OWNERS.filter((owner) => matches(owner.pattern, id)).sort(
160
+ (a, b) => b.pattern.length - a.pattern.length
161
+ )[0] ?? null
162
+ );
163
+ }