@crvouga/dev-fleet 0.4.0 → 0.6.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 (63) 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 +6 -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/github.yml +50 -0
  12. package/ansible/ubuntu/tasks/install.yml +38 -0
  13. package/ansible/ubuntu/tasks/packages.yml +4 -55
  14. package/ansible/ubuntu/tasks/release.yml +25 -0
  15. package/ansible/ubuntu/tasks/resources.yml +6 -5
  16. package/ansible/ubuntu/tasks/snapshot.yml +50 -0
  17. package/ansible/ubuntu/tasks/unattended-upgrades.yml +24 -0
  18. package/bootstrap/fleet-remote-bootstrap.sh +18 -41
  19. package/bootstrap/worker-auth.ts +6 -7
  20. package/bootstrap/worker-github.ts +55 -44
  21. package/docs/authentication.md +4 -1
  22. package/docs/consumer-setup.md +37 -30
  23. package/docs/declared.md +4 -5
  24. package/docs/github.md +107 -0
  25. package/docs/operations.md +1 -1
  26. package/docs/runbooks.md +17 -6
  27. package/docs/support-matrix.md +36 -0
  28. package/docs/ubuntu-sync.md +14 -12
  29. package/docs/worker-contract.md +28 -1
  30. package/package.json +2 -2
  31. package/schema/check-ids.json +55 -7
  32. package/schema/fleet.schema.json +86 -4
  33. package/src/auth-checks.ts +14 -6
  34. package/src/cli/fleet-sync-manager.ts +9 -0
  35. package/src/cli/fleet-sync-remote.ts +1 -1
  36. package/src/cli/github-verify.ts +57 -0
  37. package/src/cli/register-fleet.ts +2 -0
  38. package/src/cli/registry.ts +7 -0
  39. package/src/definition.ts +29 -8
  40. package/src/github/checks.ts +412 -0
  41. package/src/github/converge.ts +62 -0
  42. package/src/github/copilot.ts +54 -0
  43. package/src/github/credential.ts +21 -0
  44. package/src/github/policy.ts +141 -0
  45. package/src/github/sync.ts +123 -0
  46. package/src/github/workflow.ts +223 -0
  47. package/src/machine/platform.ts +48 -28
  48. package/src/machine/support-matrix.ts +76 -0
  49. package/src/manager-sync.ts +15 -7
  50. package/src/network-definition.ts +1 -1
  51. package/src/node-converge-linux.ts +3 -2
  52. package/src/node-converge-shared.ts +6 -2
  53. package/src/node-converge.ts +1 -1
  54. package/src/platform/debian-version.ts +62 -0
  55. package/src/platform/ubuntu-audit.ts +11 -4
  56. package/src/platform/ubuntu-release.ts +41 -0
  57. package/src/remote-bootstrap.ts +2 -0
  58. package/src/repair-catalog.ts +2 -2
  59. package/src/secrets.ts +58 -13
  60. package/src/service-auth.ts +2 -2
  61. package/src/worker-access-checks.ts +66 -16
  62. package/src/worker-host-checks.ts +36 -10
  63. package/src/worker-readiness.ts +1 -1
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/github.md ADDED
@@ -0,0 +1,107 @@
1
+ # GitHub on every node
2
+
3
+ Every node, the manager and each worker, signs in to GitHub from a stored
4
+ credential. Nobody runs `gh auth login` on a node, and no login directory is
5
+ copied between machines.
6
+
7
+ ## Where the token is
8
+
9
+ The token is the declared secret `github-token`, read through the secrets
10
+ provider your `fleet.json` names, like every other secret:
11
+
12
+ | Provider | Shared token | Per-node token (`"credential": "per-node"`) |
13
+ | -------- | ------------------------------------------------------------------------- | ------------------------------------------- |
14
+ | `env` | `DEVFLEET_SECRET_GITHUB_TOKEN` | `DEVFLEET_SECRET_GITHUB_TOKEN_<NODE>` |
15
+ | `file` | `github-token` | `github-token-<node>` |
16
+ | `sops` | key `github-token` | key `github-token-<node>` |
17
+ | `vault` | `locations["github-token"]`, or field `GITHUB_TOKEN` beside the proxy key | the same location, field `<FIELD>_<NODE>` |
18
+
19
+ The login it must belong to is `worker.githubLogin`.
20
+
21
+ ## The `github` block
22
+
23
+ All of it is optional:
24
+
25
+ ```json
26
+ "github": {
27
+ "manager": "fleet",
28
+ "credential": "shared",
29
+ "scopes": ["repo", "workflow"],
30
+ "repositories": ["example-org/app"],
31
+ "organizations": ["example-org"],
32
+ "sandbox": "example-org/sandbox"
33
+ }
34
+ ```
35
+
36
+ - `manager`: `"fleet"` (the default) signs the manager in as the fleet login,
37
+ like a worker. `{ "login": "…" }` names a person's own login: it is checked
38
+ and never replaced.
39
+ - `credential`: `"shared"` (one token) or `"per-node"` (one per node, so one
40
+ can be revoked alone and nodes do not share a rate limit).
41
+ - `scopes`, `repositories`, `organizations`: what `github:permissions` and
42
+ `github:org-access` hold the token to.
43
+ - `sandbox`: the repository `fleet-github-verify` may use.
44
+
45
+ ## How a node is signed in
46
+
47
+ `bootstrap/worker-github.ts` runs at every `fleet-sync` and, on a worker, every
48
+ ten minutes from the user timer `personal-worker-github`. It is apart from the
49
+ Vault renewal (`personal-worker-auth`): one failing never stops or hides the
50
+ other. It signs gh in with the stored token when the node is not signed in, or
51
+ when the stored token differs from the one gh holds and is valid for the
52
+ declared login. git asks gh for the token at every use
53
+ (`credential.https://github.com…helper = !gh auth git-credential`, set by
54
+ `ansible/github/git.yml` with the identity of the `git` block on every node),
55
+ so a replaced token reaches git with no change and no second copy on disk.
56
+
57
+ A `fleet-sync` fails with `github:sync` when the node cannot be signed in.
58
+
59
+ ## Rotating the token
60
+
61
+ 1. Issue a new token for the fleet login and put it in the store as
62
+ `github-token`.
63
+ 2. Run `fleet-sync` (the canary worker first, as always), or wait up to ten
64
+ minutes for every node's timer. Each node adopts the new token while the old
65
+ one still works.
66
+ 3. When `github:token-current` passes on every node, revoke the old token.
67
+
68
+ ## Checks
69
+
70
+ | Check | What it says |
71
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72
+ | `auth:github` | gh answers `auth status` in this context. |
73
+ | `github:principal` | The node is signed in as its declared login (the fleet login, or the manager's personal one). |
74
+ | `github:token-current` | The node uses the token the store holds now (`GITHUB_TOKEN_STALE` otherwise). Not reported for a personal login. |
75
+ | `github:permissions` | The token has every declared scope and can push to every declared repository (`GITHUB_PERMISSION_MISSING: …`). Warns for a fine-grained token, which reports no scopes. |
76
+ | `github:expiry` | Warns 14 days before the token expires, fails 3 days before. |
77
+ | `github:rate-limit` | Warns below 20 % of the rate limit left, with the reset time. |
78
+ | `github:org-access` | Single sign-on has authorised the token for each declared organisation. |
79
+ | `github:git-credential` | git asks gh for its GitHub credential. |
80
+ | `worker:github-sync` | Why a worker is not signed in: `GITHUB_TOKEN_MISSING`, `GITHUB_TOKEN_REJECTED`, `GITHUB_LOGIN_MISMATCH`, `GITHUB_UNREACHABLE`. |
81
+ | `auth:copilot` | Copilot CLI uses the node's GitHub sign-in; fails `not-signed-in` without one and `COPILOT_TOKEN_UNSUPPORTED` for a classic token. |
82
+ | `github:workflow` | From `fleet-github-verify`: the first step of clone, branch, commit, push, pull request, checks and comment that failed. |
83
+
84
+ ## Copilot CLI
85
+
86
+ Copilot CLI takes the token gh holds when it has none of its own, so the
87
+ node's GitHub sign-in is its sign-in. It accepts OAuth, GitHub App user and
88
+ fine-grained tokens with the Copilot Requests permission, not classic
89
+ personal access tokens. Whether the account is entitled to Copilot is not
90
+ measured; no prompt is sent.
91
+
92
+ ## Verifying the whole workflow
93
+
94
+ `devfleet fleet-github-verify` runs, from the machine it is started on, what an
95
+ agent does on the sandbox repository: clone, a uniquely named branch, an empty
96
+ commit with the configured identity, push, a draft pull request, a read of its
97
+ checks, a comment. It then closes the pull request and deletes the branch,
98
+ whatever failed, and reports anything it could not remove.
99
+
100
+ ## Not yet
101
+
102
+ - Short-lived GitHub App installation tokens minted by the manager.
103
+ - A container registry, package registry and tool installer adapter: today
104
+ only git takes its credential from gh.
105
+ - A launchd timer for the manager: it is signed in at every `fleet-sync`.
106
+ - `fleet-github-verify` runs on the machine it is started on; `--node` and a
107
+ scheduled run are not there yet.
@@ -107,7 +107,7 @@ remote nodes' required lists. Run `fleet-nodes --schema` for editor/agent valida
107
107
  | `ssh:agent` | Manager agent exposes an identity; missing agent warns because IdentityFile keys may still work. |
108
108
  | `ssh:session` | Fresh batch SSH session actually runs the Node/Bun probe. No reused multiplexed connection or agent forwarding. |
109
109
  | `ssh:host-key`, `ssh:dns`, `ssh:auth`, `ssh:network`, `ssh:runtime`, `ssh:timeout` | Classified failure and targeted remedy, without raw logs. |
110
- | `auth:github` | gh auth status --active succeeds on this machine; not proof of Git SSH or every repository permission. |
110
+ | `auth:github` | gh auth status --active succeeds; the `github:*` checks ([GitHub](github.md)) say which login, token, permissions and expiry. |
111
111
  | `auth:vault` | Server-side vault token lookup succeeds in this context; not proof of access to every secret path. |
112
112
  | `auth:vscode` | code CLI finds the local Remote-SSH extension. Other profiles/Cursor/GUI PATH/agents can differ. |
113
113
  | `t3:settings` | Connect explicitly enabled and not revoked in t3 status --json; settings are not connectivity. |
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 244 codes; 31 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
 
@@ -150,6 +147,20 @@ There are 241 codes; 31 need a person.
150
147
  - `GIT_REMOTE_REFUSED`: git remote refused. **Needs a person.**
151
148
  - `GIT_REMOTE_UNREACHABLE`: git remote unreachable.
152
149
 
150
+ ## GitHub sign-in
151
+
152
+ - **Meaning:** A node is not signed in to GitHub from the stored `github-token`, or the token belongs to another login. See [GitHub on every node](github.md).
153
+ - **Confirm:** The `worker:github-sync` and `github:*` checks of `devfleet fleet-status --workers`; on the node, `gh auth status`.
154
+ - **Fix:** Put a valid token for `worker.githubLogin` in the store as `github-token`, then run `devfleet fleet-sync` (a worker also retries every ten minutes).
155
+ - **Escalate:** A missing login or a token issued for another account needs a person to declare the login or issue a new token.
156
+
157
+ - `GITHUB_LOGIN_MISMATCH`: github login mismatch. **Needs a person.**
158
+ - `GITHUB_LOGIN_UNDECLARED`: github login undeclared. **Needs a person.**
159
+ - `GITHUB_SIGN_IN_FAILED`: github sign in failed.
160
+ - `GITHUB_TOKEN_MISSING`: github token missing.
161
+ - `GITHUB_TOKEN_REJECTED`: github token rejected.
162
+ - `GITHUB_UNREACHABLE`: github unreachable.
163
+
153
164
  ## Tailscale
154
165
 
155
166
  - **Meaning:** A machine is not on the tailnet as the fleet declares it.
@@ -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
 
@@ -188,7 +187,7 @@ verification and the CLIProxy singleton.
188
187
  | Disk safety | `personal-fleet-disk-guard` kills only the top user-cgroup writer below 3 GiB free, never deletes files; hourly `personal-fleet-disk-gc.timer` deletes git-ignored build output (`node_modules`, `.cache`, `dist`, ...) in checkouts idle 7d (12h under 15 GiB free) with no process inside, plus unused Docker images and build cache, never source, worktrees or volumes; journal 1G; `/tmp` 25% of RAM, aged 2d hourly, a measured write probe (`TMP_QUOTA_EXHAUSTED`), and T3 agents use disk-backed `/var/tmp` (`TMPDIR`, aged 14d); Docker log rotation | not managed |
189
188
  | Tailscale | pinned APT package, `tailscaled` enabled, native login required | Tailscale app, human login |
190
189
  | Dashboard agent | pinned native binary, hardened `beszel-agent` systemd unit as user `beszel` | not installed |
191
- | Credential renewal | user timer `personal-worker-auth` renews the node's own Vault token and GitHub login every 10min | the human's own Vault and GitHub sessions |
190
+ | Credential renewal | user timer `personal-worker-auth` renews the node's own Vault token every 10min; `personal-worker-github` signs it in to GitHub from the stored token ([GitHub](github.md)) | the human's own Vault and GitHub sessions |
192
191
  | Noninteractive sudo | validated `/etc/sudoers.d/personal-fleet-USER` | never; the human runs `manager.yml` once with `-K` |
193
192
  | T3 worker app | pinned desktop `.deb` is the only T3 process (UI plus embedded server); GDM auto-login, user unit `t3code-desktop.service` bound to the graphical session with `Restart=always`; `personal-worker-recovery` runs a copy of `t3-health.sh` installed in `/usr/local/lib/personal-fleet` every minute: it stops and disables a still-running headless `t3code.service`, reports `T3_NO_GRAPHICAL_SESSION` and restarts the display manager (three times at most) when there is no session, starts a stopped app after `reset-failed`, restarts one whose local API missed three checks, and restarts one on another release than the installed one or with a deferred policy restart once it runs no agent turn; administrator policy | not a worker |
194
193
  | T3 providers | the same managed instances (`fleet/agents/t3.json`) through locked T3 administrator policy | reconciled into the human's T3 settings by `fleet-sync` (`t3:providers`) with the ownership-safe `t3-apply` engine, so every T3 offers the identical set; unrelated and unmanaged instances are preserved. `fleet-sync` also applies the same proxy agent profiles (`configuration:apply`) and verifies the shared Vault gateway credential end to end with `provider:*:inference`, exactly as on workers |
@@ -210,16 +209,19 @@ vault kv get -field=FLEET_WORKER_TOKEN_WORKER_ONE secret/personal/prd \
210
209
  | ssh worker-one 'vault login -address=https://vault.example.com -no-print -'
211
210
  ```
212
211
 
213
- From then on the node's `personal-worker-auth` timer renews that token and
214
- restores its GitHub login from it (`bootstrap/worker-auth.ts`). It renders no
215
- provider configuration: agent profiles are applied by `fleet-sync`. See
216
- [Authentication and accounts](authentication.md).
212
+ From then on the node's `personal-worker-auth` timer renews that token and its
213
+ `personal-worker-github` timer keeps its GitHub sign-in current
214
+ ([GitHub](github.md)). It renders no provider configuration: agent profiles
215
+ are applied by `fleet-sync`. See [Authentication and accounts](authentication.md).
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.6.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": "9f9718f2853ed7840c846c8dfc8750903c782b87"
76
76
  }
@@ -50,6 +50,14 @@
50
50
  "id": "auth:*",
51
51
  "meaning": "A required login or capability works in this process context."
52
52
  },
53
+ {
54
+ "id": "auth:copilot",
55
+ "meaning": "Copilot CLI uses the node's GitHub sign-in with a token it accepts."
56
+ },
57
+ {
58
+ "id": "auth:github",
59
+ "meaning": "gh answers auth status in this context."
60
+ },
53
61
  {
54
62
  "id": "auth:toolchain",
55
63
  "meaning": "The pinned toolchain, dependencies and Docker could be checked."
@@ -168,15 +176,47 @@
168
176
  },
169
177
  {
170
178
  "id": "git:configuration",
171
- "meaning": "The fleet's Git identity and credential helper were written on this node."
179
+ "meaning": "The declared Git identity and the GitHub credential helper are written in the node's global Git configuration."
172
180
  },
173
181
  {
174
- "id": "login:*",
175
- "meaning": "A native per-machine login is in place."
182
+ "id": "github:expiry",
183
+ "meaning": "The GitHub token does not expire within the warning window."
184
+ },
185
+ {
186
+ "id": "github:git-credential",
187
+ "meaning": "git asks gh for its GitHub credential."
188
+ },
189
+ {
190
+ "id": "github:org-access",
191
+ "meaning": "Single sign-on has authorised the GitHub token for each declared organisation."
192
+ },
193
+ {
194
+ "id": "github:permissions",
195
+ "meaning": "The GitHub token has every declared scope and can push to every declared repository."
196
+ },
197
+ {
198
+ "id": "github:principal",
199
+ "meaning": "The node is signed in to GitHub as its declared login."
176
200
  },
177
201
  {
178
- "id": "macos:release",
179
- "meaning": "The manager runs a supported macOS release."
202
+ "id": "github:rate-limit",
203
+ "meaning": "The GitHub rate limit left is above the warning floor."
204
+ },
205
+ {
206
+ "id": "github:sync",
207
+ "meaning": "fleet-sync signed this node in to GitHub from the stored credential."
208
+ },
209
+ {
210
+ "id": "github:token-current",
211
+ "meaning": "The node uses the GitHub token the store holds now."
212
+ },
213
+ {
214
+ "id": "github:workflow",
215
+ "meaning": "fleet-github-verify completed clone, branch, commit, push, pull request, checks and comment on the sandbox repository."
216
+ },
217
+ {
218
+ "id": "login:*",
219
+ "meaning": "A native per-machine login is in place."
180
220
  },
181
221
  {
182
222
  "id": "manager:docker",
@@ -202,6 +242,10 @@
202
242
  "id": "notify:configured",
203
243
  "meaning": "Every notification target fleet.json names has its address available as a secret."
204
244
  },
245
+ {
246
+ "id": "platform:environment",
247
+ "meaning": "The worker runs on a physical or virtual machine of its own, not in a container or WSL2."
248
+ },
205
249
  {
206
250
  "id": "platform:unsupported",
207
251
  "meaning": "The machine runs a supported operating system."
@@ -483,8 +527,12 @@
483
527
  "meaning": "The worker runs the declared revision."
484
528
  },
485
529
  {
486
- "id": "worker:github-principal",
487
- "meaning": "The worker's GitHub login is the declared account."
530
+ "id": "worker:github-sync",
531
+ "meaning": "The worker is signed in to GitHub from the stored credential as the declared login."
532
+ },
533
+ {
534
+ "id": "worker:package:*",
535
+ "meaning": "The pinned Ubuntu package is installed at or past its pin."
488
536
  },
489
537
  {
490
538
  "id": "worker:power-loss-recovery",
@@ -253,13 +253,17 @@
253
253
  "required": ["release", "architectures"],
254
254
  "properties": {
255
255
  "release": {
256
- "const": "26.04"
256
+ "type": "string",
257
+ "pattern": "^[0-9]{2}\\.[0-9]{2}$",
258
+ "description": "An Ubuntu release ansible/ubuntu/policy.json accepts (docs/support-matrix.md)."
257
259
  },
258
260
  "architectures": {
259
- "const": ["amd64", "arm64"]
261
+ "type": "array",
262
+ "items": { "enum": ["amd64", "arm64"] },
263
+ "description": "Exactly the architectures the policy names for that release."
260
264
  }
261
265
  },
262
- "description": "The supported worker release and architectures (fixed)."
266
+ "description": "The worker release and its architectures, as the Ubuntu policy of the pinned dev-fleet names them."
263
267
  },
264
268
  "worker": {
265
269
  "type": "object",
@@ -607,8 +611,24 @@
607
611
  }
608
612
  },
609
613
  "description": "Only with the ntfy notify target: the ntfy topic URL fleet-watch publishes to."
614
+ },
615
+ "github-token": {
616
+ "type": "object",
617
+ "additionalProperties": false,
618
+ "required": ["path", "field"],
619
+ "properties": {
620
+ "path": {
621
+ "type": "string",
622
+ "pattern": "^[A-Za-z0-9._/-]+$"
623
+ },
624
+ "field": {
625
+ "type": "string",
626
+ "pattern": "^[A-Za-z0-9_]+$"
627
+ }
628
+ }
610
629
  }
611
- }
630
+ },
631
+ "description": "Required for every secret except github-token, which defaults to the field GITHUB_TOKEN beside proxy-client-key."
612
632
  }
613
633
  }
614
634
  }
@@ -658,6 +678,68 @@
658
678
  "maximum": 720
659
679
  }
660
680
  }
681
+ },
682
+ "github": {
683
+ "description": "How every node signs in to GitHub. The fleet login is worker.githubLogin.",
684
+ "type": "object",
685
+ "additionalProperties": false,
686
+ "properties": {
687
+ "manager": {
688
+ "description": "fleet: the manager signs in as the fleet login. {login}: a person's own login, checked and never replaced.",
689
+ "oneOf": [
690
+ {
691
+ "const": "fleet"
692
+ },
693
+ {
694
+ "type": "object",
695
+ "additionalProperties": false,
696
+ "required": ["login"],
697
+ "properties": {
698
+ "login": {
699
+ "type": "string",
700
+ "pattern": "^[a-zA-Z0-9-]{1,39}$"
701
+ }
702
+ }
703
+ }
704
+ ]
705
+ },
706
+ "credential": {
707
+ "description": "shared: one github-token for every node. per-node: github-token-<node> for each node.",
708
+ "enum": ["shared", "per-node"]
709
+ },
710
+ "scopes": {
711
+ "description": "Token scopes every node must have, such as repo and workflow.",
712
+ "type": "array",
713
+ "uniqueItems": true,
714
+ "items": {
715
+ "type": "string",
716
+ "pattern": "^[a-z:_]{1,40}$"
717
+ }
718
+ },
719
+ "repositories": {
720
+ "description": "owner/name of repositories every node must be able to push to.",
721
+ "type": "array",
722
+ "uniqueItems": true,
723
+ "items": {
724
+ "type": "string",
725
+ "pattern": "^[a-zA-Z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}$"
726
+ }
727
+ },
728
+ "organizations": {
729
+ "description": "Organisations whose single sign-on must have authorised the token.",
730
+ "type": "array",
731
+ "uniqueItems": true,
732
+ "items": {
733
+ "type": "string",
734
+ "pattern": "^[a-zA-Z0-9-]{1,39}$"
735
+ }
736
+ },
737
+ "sandbox": {
738
+ "type": "string",
739
+ "pattern": "^[a-zA-Z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}$",
740
+ "description": "owner/name of a repository fleet-github-verify branches, pushes to and opens pull requests on, then cleans up."
741
+ }
742
+ }
661
743
  }
662
744
  }
663
745
  }
@@ -6,6 +6,8 @@ import { codingAuth, serviceAuth } from './service-auth';
6
6
  import { workstationChecks } from './workstation-checks';
7
7
  import { toolchainChecks } from './toolchain-checks';
8
8
  import { workerSelection } from '../bootstrap/worker-tools';
9
+ import { githubChecks } from './github/checks';
10
+ import { secretsProvider } from './secrets';
9
11
  import type { Capability, Check, Runner, CommandResult } from './types';
10
12
 
11
13
  export async function authCheck(
@@ -20,6 +22,17 @@ export async function authCheck(
20
22
  return codingAuth(capability, run);
21
23
  if (capability === 'services') return serviceAuth(run);
22
24
  if (capability === 'workstation') return workstationChecks(root, run);
25
+ if (capability === 'github')
26
+ return githubChecks(run, {
27
+ definition,
28
+ ...(nodeId === undefined ? {} : { node: nodeId }),
29
+ secret: secretsProvider(definition.secrets, {
30
+ run: async (argv) => {
31
+ const result = await run(argv, { timeoutMs: 20_000 });
32
+ return result.ok ? result.stdout : null;
33
+ },
34
+ }),
35
+ });
23
36
  if (capability === 'toolchain')
24
37
  return toolchainChecks(
25
38
  root,
@@ -32,16 +45,11 @@ export async function authCheck(
32
45
  }
33
46
 
34
47
  async function basicAuth(
35
- capability: 'github' | 'vault' | 'vscode',
48
+ capability: 'vault' | 'vscode',
36
49
  run: Runner,
37
50
  root: string
38
51
  ): Promise<Check[]> {
39
52
  const spec = {
40
- github: {
41
- command: ['gh', 'auth', 'status', '--active'],
42
- action:
43
- 'Authenticate on this node with gh auth login, then gh auth setup-git.',
44
- },
45
53
  vault: {
46
54
  command: [
47
55
  'vault',
@@ -16,6 +16,7 @@ import { idesOf } from '../ide/definition';
16
16
  import { convergeGitIdentity, gitChecks } from '../git-identity';
17
17
  import macos from '../../ansible/macos/policy.json';
18
18
  import { ansibleCommand } from '../platform/ansible';
19
+ import { convergeGithub } from '../github/converge';
19
20
  import { PACKAGE_ROOT } from '../package-info';
20
21
  import { managerHealth } from '../manager-health';
21
22
  import { readFileSync } from 'node:fs';
@@ -70,6 +71,14 @@ export async function convergeManager(
70
71
  );
71
72
  if (definition.git)
72
73
  checks.push(...(await gitChecks(definition.git, run)));
74
+ // The same GitHub sign-in as every worker, or only a check of a
75
+ // person's own login when the configuration names one. git asks gh
76
+ // for the token through the helper git.yml declared above.
77
+ checks.push(
78
+ ...(await attempt('github:sync', async () => [
79
+ await convergeGithub(input.workspace, run),
80
+ ]))
81
+ );
73
82
  const providers = await attempt('provider:verification', () =>
74
83
  verifyFleetProviders(id)
75
84
  );