@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.
- package/README.md +3 -1
- package/ansible/macos/policy.json +1 -0
- package/ansible/platforms.json +111 -0
- package/ansible/ubuntu/authorize-sudo.sh +7 -16
- package/ansible/ubuntu/baseline.yml +6 -28
- package/ansible/ubuntu/disk-gc.py +186 -4
- package/ansible/ubuntu/policy.json +18 -3
- package/ansible/ubuntu/t3-desktop.yml +2 -2
- package/ansible/ubuntu/tailscale.yml +20 -9
- package/ansible/ubuntu/tasks/dpkg.yml +46 -0
- package/ansible/ubuntu/tasks/github.yml +50 -0
- package/ansible/ubuntu/tasks/install.yml +38 -0
- package/ansible/ubuntu/tasks/packages.yml +4 -55
- package/ansible/ubuntu/tasks/release.yml +25 -0
- package/ansible/ubuntu/tasks/resources.yml +6 -5
- package/ansible/ubuntu/tasks/snapshot.yml +50 -0
- package/ansible/ubuntu/tasks/unattended-upgrades.yml +24 -0
- package/bootstrap/fleet-remote-bootstrap.sh +18 -41
- package/bootstrap/worker-auth.ts +6 -7
- package/bootstrap/worker-github.ts +55 -44
- package/docs/authentication.md +4 -1
- package/docs/consumer-setup.md +37 -30
- package/docs/declared.md +4 -5
- package/docs/github.md +107 -0
- package/docs/operations.md +1 -1
- package/docs/runbooks.md +17 -6
- package/docs/support-matrix.md +36 -0
- package/docs/ubuntu-sync.md +14 -12
- package/docs/worker-contract.md +28 -1
- package/package.json +2 -2
- package/schema/check-ids.json +55 -7
- package/schema/fleet.schema.json +86 -4
- package/src/auth-checks.ts +14 -6
- package/src/cli/fleet-sync-manager.ts +9 -0
- package/src/cli/fleet-sync-remote.ts +1 -1
- package/src/cli/github-verify.ts +57 -0
- package/src/cli/register-fleet.ts +2 -0
- package/src/cli/registry.ts +7 -0
- package/src/definition.ts +29 -8
- package/src/github/checks.ts +412 -0
- package/src/github/converge.ts +62 -0
- package/src/github/copilot.ts +54 -0
- package/src/github/credential.ts +21 -0
- package/src/github/policy.ts +141 -0
- package/src/github/sync.ts +123 -0
- package/src/github/workflow.ts +223 -0
- package/src/machine/platform.ts +48 -28
- package/src/machine/support-matrix.ts +76 -0
- package/src/manager-sync.ts +15 -7
- package/src/network-definition.ts +1 -1
- package/src/node-converge-linux.ts +3 -2
- package/src/node-converge-shared.ts +6 -2
- package/src/node-converge.ts +1 -1
- package/src/platform/debian-version.ts +62 -0
- package/src/platform/ubuntu-audit.ts +11 -4
- package/src/platform/ubuntu-release.ts +41 -0
- package/src/remote-bootstrap.ts +2 -0
- package/src/repair-catalog.ts +2 -2
- package/src/secrets.ts +58 -13
- package/src/service-auth.ts +2 -2
- package/src/worker-access-checks.ts +66 -16
- package/src/worker-host-checks.ts +36 -10
- 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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
package/docs/operations.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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.
|
package/docs/ubuntu-sync.md
CHANGED
|
@@ -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.
|
|
65
|
-
the
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
215
|
-
provider configuration: agent profiles
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
package/docs/worker-contract.md
CHANGED
|
@@ -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
|
|
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.
|
|
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": "
|
|
75
|
+
"gitHead": "9f9718f2853ed7840c846c8dfc8750903c782b87"
|
|
76
76
|
}
|
package/schema/check-ids.json
CHANGED
|
@@ -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
|
|
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": "
|
|
175
|
-
"meaning": "
|
|
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": "
|
|
179
|
-
"meaning": "The
|
|
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-
|
|
487
|
-
"meaning": "The worker
|
|
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",
|
package/schema/fleet.schema.json
CHANGED
|
@@ -253,13 +253,17 @@
|
|
|
253
253
|
"required": ["release", "architectures"],
|
|
254
254
|
"properties": {
|
|
255
255
|
"release": {
|
|
256
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
}
|
package/src/auth-checks.ts
CHANGED
|
@@ -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: '
|
|
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
|
);
|