@crvouga/dev-fleet 0.5.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/ansible/ubuntu/baseline.yml +2 -0
- package/ansible/ubuntu/tasks/github.yml +50 -0
- 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 +5 -0
- package/docs/github.md +107 -0
- package/docs/operations.md +1 -1
- package/docs/runbooks.md +15 -1
- package/docs/ubuntu-sync.md +5 -5
- package/package.json +2 -2
- package/schema/check-ids.json +47 -3
- package/schema/fleet.schema.json +79 -1
- package/src/auth-checks.ts +14 -6
- package/src/cli/fleet-sync-manager.ts +9 -0
- 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 +11 -1
- 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/node-converge-linux.ts +3 -2
- package/src/node-converge-shared.ts +6 -2
- 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-readiness.ts +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
# This worker's GitHub sign-in, apart from its Vault renewal: a failure of one
|
|
3
|
+
# never hides or stops the other. The job signs in from the stored credential
|
|
4
|
+
# and adopts a rotated token while the old one still works.
|
|
5
|
+
- name: Declare the GitHub sign-in job
|
|
6
|
+
vars:
|
|
7
|
+
# Every step the job runs (the secret read and four gh calls) may take
|
|
8
|
+
# its full timeout (GITHUB_STEP_TIMEOUT_MS), plus the runtime's start.
|
|
9
|
+
github_step_timeout_seconds: 30
|
|
10
|
+
github_job_steps: 5
|
|
11
|
+
github_job_start_seconds: 30
|
|
12
|
+
ansible.builtin.copy:
|
|
13
|
+
dest: '{{ development_home }}/.config/systemd/user/personal-worker-github.service'
|
|
14
|
+
owner: '{{ development_user }}'
|
|
15
|
+
mode: '0600'
|
|
16
|
+
content: |
|
|
17
|
+
[Unit]
|
|
18
|
+
Description=Sign this worker in to GitHub from its stored credential
|
|
19
|
+
[Service]
|
|
20
|
+
Type=oneshot
|
|
21
|
+
WorkingDirectory={{ development_source }}
|
|
22
|
+
ExecStart={{ development_home }}/.local/bin/mise exec -- bun bootstrap/worker-github.ts
|
|
23
|
+
Environment="PATH={{ development_home }}/.local/share/mise/shims:{{ development_home }}/.local/bin:/usr/local/bin:/usr/bin:/bin"
|
|
24
|
+
UMask=0077
|
|
25
|
+
TimeoutStartSec={{ github_step_timeout_seconds * github_job_steps + github_job_start_seconds }}
|
|
26
|
+
- name: Declare the GitHub sign-in schedule
|
|
27
|
+
ansible.builtin.copy:
|
|
28
|
+
dest: '{{ development_home }}/.config/systemd/user/personal-worker-github.timer'
|
|
29
|
+
owner: '{{ development_user }}'
|
|
30
|
+
mode: '0600'
|
|
31
|
+
content: |
|
|
32
|
+
[Unit]
|
|
33
|
+
Description=Keep this worker signed in to GitHub with the stored credential
|
|
34
|
+
[Timer]
|
|
35
|
+
OnBootSec=2min
|
|
36
|
+
OnUnitActiveSec=10min
|
|
37
|
+
Persistent=true
|
|
38
|
+
[Install]
|
|
39
|
+
WantedBy=timers.target
|
|
40
|
+
- name: Enable this user's GitHub sign-in timer
|
|
41
|
+
become: true
|
|
42
|
+
become_user: '{{ development_user }}'
|
|
43
|
+
environment:
|
|
44
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
45
|
+
ansible.builtin.systemd_service:
|
|
46
|
+
scope: user
|
|
47
|
+
name: personal-worker-github.timer
|
|
48
|
+
daemon_reload: true
|
|
49
|
+
enabled: true
|
|
50
|
+
state: started
|
package/bootstrap/worker-auth.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** Private scheduled job: renews this worker's own Vault token, and nothing
|
|
2
|
+
* else. GitHub sign-in is its own job (worker-github.ts), so a failure of
|
|
3
|
+
* one never stops or hides the other. Raw auth output is never logged.
|
|
4
|
+
*/
|
|
3
5
|
import { spawnSync } from 'node:child_process';
|
|
4
6
|
import { resolveVaultConfig, usesVault } from '../src/vault-config';
|
|
5
7
|
|
|
@@ -49,14 +51,11 @@ if (import.meta.main) {
|
|
|
49
51
|
jobId: 'vault-self-renewal',
|
|
50
52
|
operation: 'vault.token.renew',
|
|
51
53
|
});
|
|
52
|
-
const
|
|
53
|
-
const github = vault && syncWorkerGithub();
|
|
54
|
-
const ok = vault && github;
|
|
54
|
+
const ok = renewWorkerVault();
|
|
55
55
|
log.info('job_finished', {
|
|
56
56
|
jobId: 'vault-self-renewal',
|
|
57
57
|
operation: 'vault.token.renew',
|
|
58
|
-
vaultAuthenticated:
|
|
59
|
-
githubAuthenticated: github,
|
|
58
|
+
vaultAuthenticated: ok,
|
|
60
59
|
outcome: ok ? 'success' : 'authentication_required',
|
|
61
60
|
durationMs: Date.now() - started,
|
|
62
61
|
retryDelayMs: 600000,
|
|
@@ -1,47 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/** GitHub sign-in of this node from the stored credential; never copies a
|
|
2
|
+
* login directory. Runs from the timer (`personal-worker-github`) and from
|
|
3
|
+
* every sync. One JSON line, never a token or raw output.
|
|
4
|
+
*/
|
|
5
|
+
import { hostname } from 'node:os';
|
|
6
|
+
import { fleet } from '../src/fleet-config';
|
|
7
|
+
import { secretsProvider } from '../src/secrets';
|
|
8
|
+
import { runCommand } from '../src/runner';
|
|
9
|
+
import { hostId } from '../src/topology';
|
|
10
|
+
import { githubCredential } from '../src/github/credential';
|
|
11
|
+
import { githubNode } from '../src/github/policy';
|
|
12
|
+
import { syncGithub, type GithubSyncResult } from '../src/github/sync';
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// The
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
14
|
+
/** The exit status of each outcome, so the unit's result names it too. */
|
|
15
|
+
export const EXIT_CODES: Record<GithubSyncResult['code'], number> = {
|
|
16
|
+
GITHUB_OK: 0,
|
|
17
|
+
GITHUB_TOKEN_MISSING: 10,
|
|
18
|
+
GITHUB_TOKEN_REJECTED: 11,
|
|
19
|
+
GITHUB_LOGIN_MISMATCH: 12,
|
|
20
|
+
GITHUB_UNREACHABLE: 13,
|
|
21
|
+
GITHUB_SIGN_IN_FAILED: 14,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const GITHUB_JOB = 'dev-fleet-github';
|
|
25
|
+
|
|
26
|
+
if (import.meta.main) {
|
|
27
|
+
const started = Date.now();
|
|
28
|
+
// The manager is never a declared worker; `--node` names a worker whose
|
|
29
|
+
// host name is not its id.
|
|
30
|
+
const flag = process.argv.indexOf('--node');
|
|
31
|
+
const id = flag > 0 ? process.argv[flag + 1] : hostId(hostname());
|
|
32
|
+
const target = githubNode(fleet, id);
|
|
33
|
+
const result: GithubSyncResult =
|
|
34
|
+
target.login === null
|
|
35
|
+
? { code: 'GITHUB_LOGIN_MISMATCH', adopted: false }
|
|
36
|
+
: await syncGithub({
|
|
37
|
+
run: runCommand,
|
|
38
|
+
token: githubCredential(
|
|
39
|
+
fleet,
|
|
40
|
+
target.node,
|
|
41
|
+
secretsProvider(fleet.secrets)
|
|
42
|
+
),
|
|
43
|
+
login: target.login,
|
|
44
|
+
// A person's own login on the manager is measured, never replaced.
|
|
45
|
+
apply: target.managed,
|
|
46
|
+
});
|
|
47
|
+
process.stdout.write(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
name: GITHUB_JOB,
|
|
50
|
+
event: 'job_finished',
|
|
51
|
+
role: target.role,
|
|
52
|
+
code: result.code,
|
|
53
|
+
adopted: result.adopted,
|
|
54
|
+
durationMs: Date.now() - started,
|
|
55
|
+
}) + '\n'
|
|
45
56
|
);
|
|
46
|
-
|
|
57
|
+
process.exitCode = EXIT_CODES[result.code];
|
|
47
58
|
}
|
package/docs/authentication.md
CHANGED
|
@@ -54,6 +54,7 @@ Every failure code is in [the runbooks](runbooks.md).
|
|
|
54
54
|
| `monitoring-account-password` | yes | `DEVFLEET_SECRET_MONITORING_ACCOUNT_PASSWORD` | `~/.config/dev-fleet/secrets/monitoring-account-password` |
|
|
55
55
|
| `monitoring-tunnel-token` | yes | `DEVFLEET_SECRET_MONITORING_TUNNEL_TOKEN` | `~/.config/dev-fleet/secrets/monitoring-tunnel-token` |
|
|
56
56
|
| `proxy-tunnel-token` | yes | `DEVFLEET_SECRET_PROXY_TUNNEL_TOKEN` | `~/.config/dev-fleet/secrets/proxy-tunnel-token` |
|
|
57
|
+
| `github-token` | when its feature is declared | `DEVFLEET_SECRET_GITHUB_TOKEN` | `~/.config/dev-fleet/secrets/github-token` |
|
|
57
58
|
| `monitoring-backup-access-key` | when its feature is declared | `DEVFLEET_SECRET_MONITORING_BACKUP_ACCESS_KEY` | `~/.config/dev-fleet/secrets/monitoring-backup-access-key` |
|
|
58
59
|
| `monitoring-backup-secret-key` | when its feature is declared | `DEVFLEET_SECRET_MONITORING_BACKUP_SECRET_KEY` | `~/.config/dev-fleet/secrets/monitoring-backup-secret-key` |
|
|
59
60
|
| `notify-webhook-url` | when its feature is declared | `DEVFLEET_SECRET_NOTIFY_WEBHOOK_URL` | `~/.config/dev-fleet/secrets/notify-webhook-url` |
|
|
@@ -67,10 +68,11 @@ Every failure code is in [the runbooks](runbooks.md).
|
|
|
67
68
|
|
|
68
69
|
| Check | Measured in |
|
|
69
70
|
| ----------------------------- | ------------------------------------------ |
|
|
71
|
+
| `auth:github` | `src/github/checks.ts` |
|
|
70
72
|
| `auth:vault` | `src/cli/fleet-sync-vault.ts` |
|
|
71
73
|
| `worker:git-identity` | `src/git-identity.ts` |
|
|
72
74
|
| `worker:git-push-auth` | `src/definition.ts`, `src/git-identity.ts` |
|
|
73
|
-
| `worker:github-
|
|
75
|
+
| `worker:github-sync` | `src/worker-access-checks.ts` |
|
|
74
76
|
| `worker:proxy-authentication` | `src/worker-access-checks.ts` |
|
|
75
77
|
| `worker:vault-renewal` | `src/worker-access-checks.ts` |
|
|
76
78
|
|
|
@@ -84,6 +86,7 @@ Every failure code is in [the runbooks](runbooks.md).
|
|
|
84
86
|
| -------------------------------- | -------------------------------------- | ------------------------------------- |
|
|
85
87
|
| `personal-fleet-disk-gc.timer` | `ansible/ubuntu/tasks/resources.yml` | OnBootSec=10min, OnCalendar=hourly |
|
|
86
88
|
| `personal-worker-auth.timer` | `ansible/ubuntu/tasks/credentials.yml` | OnBootSec=2min, OnUnitActiveSec=10min |
|
|
89
|
+
| `personal-worker-github.timer` | `ansible/ubuntu/tasks/github.yml` | OnBootSec=2min, OnUnitActiveSec=10min |
|
|
87
90
|
| `personal-worker-recovery.timer` | `ansible/ubuntu/tasks/recovery.yml` | OnBootSec=1min, OnUnitActiveSec=1min |
|
|
88
91
|
| `personal-worker-recovery.timer` | `ansible/ubuntu/tasks/recovery.yml` | OnBootSec=5min, OnUnitActiveSec=15min |
|
|
89
92
|
|
package/docs/consumer-setup.md
CHANGED
|
@@ -179,6 +179,7 @@ Then replace every placeholder. Each field, from
|
|
|
179
179
|
| `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }`: a release of the [support matrix](support-matrix.md). |
|
|
180
180
|
| `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`. |
|
|
181
181
|
| `worker.githubLogin` | The GitHub login every worker must be signed in as. |
|
|
182
|
+
| `github` (optional) | Who the manager signs in to GitHub as, per-node tokens, required scopes; see [GitHub](github.md). |
|
|
182
183
|
| `worker.vaultUser` | The Vault user each worker's token is issued for. |
|
|
183
184
|
| `worker.toolchain`, `agentProfile` | `"mise.lock"`, `"proxy"` (fixed). |
|
|
184
185
|
| `worker.unattendedSudo`, `persistentT3`, `neverSleep` | `true` (fixed policy). |
|
|
@@ -206,6 +207,10 @@ dev-fleet asks for four secrets by purpose: `proxy-client-key`,
|
|
|
206
207
|
| `file` | `{ "provider": "file" }` (optional `"directory"`) | One private file per name, default `~/.config/dev-fleet/secrets/NAME`. |
|
|
207
208
|
| `env` | `{ "provider": "env" }` | `DEVFLEET_SECRET_PROXY_CLIENT_KEY` and so on (upper case, `_` for `-`). |
|
|
208
209
|
|
|
210
|
+
A fifth, `github-token`, is the GitHub token of `worker.githubLogin`: every node
|
|
211
|
+
is signed in with it ([GitHub](github.md)). With `vault` it needs no location
|
|
212
|
+
of its own: it defaults to the field `GITHUB_TOKEN` beside `proxy-client-key`.
|
|
213
|
+
|
|
209
214
|
Only `vault` gives each worker its own login and renewal and gives the CLI
|
|
210
215
|
proxy its encrypted backup; with the other three those two features are not
|
|
211
216
|
available yet. You never write a secret value into the project; the human puts
|
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,7 +8,7 @@ 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
|
|
|
@@ -147,6 +147,20 @@ There are 238 codes; 29 need a person.
|
|
|
147
147
|
- `GIT_REMOTE_REFUSED`: git remote refused. **Needs a person.**
|
|
148
148
|
- `GIT_REMOTE_UNREACHABLE`: git remote unreachable.
|
|
149
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
|
+
|
|
150
164
|
## Tailscale
|
|
151
165
|
|
|
152
166
|
- **Meaning:** A machine is not on the tailnet as the fleet declares it.
|
package/docs/ubuntu-sync.md
CHANGED
|
@@ -187,7 +187,7 @@ verification and the CLIProxy singleton.
|
|
|
187
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 |
|
|
188
188
|
| Tailscale | pinned APT package, `tailscaled` enabled, native login required | Tailscale app, human login |
|
|
189
189
|
| Dashboard agent | pinned native binary, hardened `beszel-agent` systemd unit as user `beszel` | not installed |
|
|
190
|
-
| 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 |
|
|
191
191
|
| Noninteractive sudo | validated `/etc/sudoers.d/personal-fleet-USER` | never; the human runs `manager.yml` once with `-K` |
|
|
192
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 |
|
|
193
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 |
|
|
@@ -209,10 +209,10 @@ vault kv get -field=FLEET_WORKER_TOKEN_WORKER_ONE secret/personal/prd \
|
|
|
209
209
|
| ssh worker-one 'vault login -address=https://vault.example.com -no-print -'
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
-
From then on the node's `personal-worker-auth` timer renews that token and
|
|
213
|
-
|
|
214
|
-
provider configuration: agent profiles
|
|
215
|
-
[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).
|
|
216
216
|
|
|
217
217
|
## Ubuntu and remote-work safety
|
|
218
218
|
|
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,7 +176,43 @@
|
|
|
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."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
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."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
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."
|
|
172
216
|
},
|
|
173
217
|
{
|
|
174
218
|
"id": "login:*",
|
|
@@ -483,8 +527,8 @@
|
|
|
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."
|
|
488
532
|
},
|
|
489
533
|
{
|
|
490
534
|
"id": "worker:package:*",
|
package/schema/fleet.schema.json
CHANGED
|
@@ -611,8 +611,24 @@
|
|
|
611
611
|
}
|
|
612
612
|
},
|
|
613
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
|
+
}
|
|
614
629
|
}
|
|
615
|
-
}
|
|
630
|
+
},
|
|
631
|
+
"description": "Required for every secret except github-token, which defaults to the field GITHUB_TOKEN beside proxy-client-key."
|
|
616
632
|
}
|
|
617
633
|
}
|
|
618
634
|
}
|
|
@@ -662,6 +678,68 @@
|
|
|
662
678
|
"maximum": 720
|
|
663
679
|
}
|
|
664
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
|
+
}
|
|
665
743
|
}
|
|
666
744
|
}
|
|
667
745
|
}
|