@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.
@@ -65,3 +65,5 @@
65
65
  - ansible.builtin.import_tasks: tasks/ssh.yml
66
66
  - ansible.builtin.import_tasks: tasks/credentials.yml
67
67
  when: development_source is defined
68
+ - ansible.builtin.import_tasks: tasks/github.yml
69
+ when: development_source is defined
@@ -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
@@ -1,5 +1,7 @@
1
- import { syncWorkerGithub } from './worker-github';
2
- /** Private scheduled job. The worker retains its own token; raw auth output is never logged. */
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 vault = renewWorkerVault();
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: vault,
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
- /** Acquire declared service credentials from this worker's Vault session; never copy a login directory. */
2
- import { spawnSync } from 'node:child_process';
3
- import { resolveVaultConfig, usesVault } from '../src/vault-config';
4
- import { fleet as definition } from '../src/fleet-config';
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
- export type AuthRunner = (
7
- args: string[],
8
- options?: { input?: string; env?: NodeJS.ProcessEnv }
9
- ) => { status: number | null; stdout: string | null };
10
- const execute: AuthRunner = (args, options = {}) =>
11
- spawnSync(args[0]!, args.slice(1), {
12
- encoding: 'utf8',
13
- timeout: 30_000,
14
- ...options,
15
- });
16
- export function syncWorkerGithub(run: AuthRunner = execute): boolean {
17
- const expected = definition.worker!.githubLogin;
18
- const current = run(['gh', 'api', 'user', '--jq', '.login']);
19
- if (current.status === 0 && current.stdout?.trim() === expected) return true;
20
- // The service token is kept in the shared Vault document only.
21
- if (!usesVault()) return false;
22
- const { addr, mount, project, config } = resolveVaultConfig();
23
- const secret = run([
24
- 'vault',
25
- 'kv',
26
- 'get',
27
- `-address=${addr}`,
28
- '-field=GITHUB_TOKEN',
29
- `${mount}/${project}/${config}`,
30
- ]);
31
- const token = secret.stdout?.trim() ?? '';
32
- if (secret.status !== 0 || !/^[\x21-\x7e]{16,4096}$/.test(token))
33
- return false;
34
- const validated = run(['gh', 'api', 'user', '--jq', '.login'], {
35
- env: { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: undefined },
36
- });
37
- if (validated.status !== 0 || validated.stdout?.trim() !== expected)
38
- return false;
39
- const login = run(
40
- ['gh', 'auth', 'login', '--hostname', 'github.com', '--with-token'],
41
- {
42
- input: token + '\n',
43
- env: { ...process.env, GH_TOKEN: undefined, GITHUB_TOKEN: undefined },
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
- return login.status === 0;
57
+ process.exitCode = EXIT_CODES[result.code];
47
58
  }
@@ -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-principal` | `src/worker-access-checks.ts` |
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
 
@@ -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.
@@ -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,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 238 codes; 29 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
 
@@ -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.
@@ -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 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 |
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
- restores its GitHub login from it (`bootstrap/worker-auth.ts`). It renders no
214
- provider configuration: agent profiles are applied by `fleet-sync`. See
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.5.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": "620b8de05d662b4dc9072457580ca4b4eab54242"
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,7 +176,43 @@
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."
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-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."
488
532
  },
489
533
  {
490
534
  "id": "worker:package:*",
@@ -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
  }