@crvouga/dev-fleet 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +265 -37
  2. package/agents/setup.md +42 -12
  3. package/ansible/macos/manager.yml +10 -7
  4. package/ansible/macos/policy.json +2 -10
  5. package/ansible/macos/t3-policy.yml +49 -0
  6. package/ansible/monitoring/README.md +86 -25
  7. package/ansible/monitoring/agent.yml +40 -2
  8. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  9. package/ansible/monitoring/lock.json +5 -0
  10. package/ansible/pyproject.toml +12 -0
  11. package/ansible/ubuntu/baseline.yml +1 -1
  12. package/ansible/ubuntu/disk-gc.py +336 -0
  13. package/ansible/ubuntu/policy.json +13 -3
  14. package/ansible/ubuntu/t3-desktop.yml +19 -0
  15. package/ansible/ubuntu/t3-health.sh +93 -22
  16. package/ansible/ubuntu/t3-idle.py +42 -0
  17. package/ansible/ubuntu/t3-policy.yml +4 -11
  18. package/ansible/ubuntu/tailscale.yml +8 -5
  19. package/ansible/ubuntu/tasks/recovery.yml +60 -1
  20. package/ansible/ubuntu/tasks/resources.yml +61 -1
  21. package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
  22. package/ansible/ubuntu/wip-guard.py +121 -0
  23. package/ansible/uv.lock +190 -0
  24. package/bootstrap/bootstrap.sh +25 -18
  25. package/bootstrap/doctor.ts +46 -1
  26. package/bootstrap/fleet-manager-run.sh +42 -15
  27. package/bootstrap/fleet-remote-bootstrap.sh +85 -12
  28. package/bootstrap/global-config.ts +64 -6
  29. package/bootstrap/install-launcher.sh +3 -0
  30. package/bootstrap/native-t3.ts +22 -11
  31. package/bootstrap/npm.ts +10 -2
  32. package/bootstrap/release-version.ts +157 -0
  33. package/cliproxy/README.md +3 -3
  34. package/cliproxy/scripts/accounts.py +216 -0
  35. package/cliproxy/scripts/checkpoint.py +29 -1
  36. package/cliproxy/scripts/cli.py +1 -1
  37. package/cliproxy/scripts/clientkeys.py +99 -0
  38. package/cliproxy/scripts/lease.py +80 -0
  39. package/cliproxy/scripts/ops +137 -25
  40. package/cliproxy/scripts/restore-verify +15 -0
  41. package/docs/accounts.md +154 -0
  42. package/docs/consumer-setup.md +408 -0
  43. package/docs/declared.md +91 -0
  44. package/docs/durability.md +26 -0
  45. package/docs/machine-setup.md +400 -0
  46. package/docs/operations.md +276 -0
  47. package/docs/releasing.md +103 -0
  48. package/docs/state.md +254 -0
  49. package/docs/topology.md +69 -0
  50. package/docs/ubuntu-sync.md +357 -0
  51. package/docs/worker-contract.md +67 -0
  52. package/examples/fleet.json +5 -0
  53. package/llms.txt +36 -0
  54. package/mise.lock +29 -0
  55. package/mise.toml +15 -2
  56. package/package.json +11 -2
  57. package/schema/fleet.schema.json +137 -15
  58. package/src/accounts/checks.ts +220 -0
  59. package/src/accounts/context.ts +76 -0
  60. package/src/accounts/propagate.ts +62 -0
  61. package/src/accounts/roster.ts +85 -0
  62. package/src/accounts/selection.ts +67 -0
  63. package/src/accounts/sessions.ts +120 -0
  64. package/src/agents/account-relay.ts +176 -26
  65. package/src/agents/fleet-verify.ts +9 -0
  66. package/src/agents/launch.ts +84 -5
  67. package/src/agents/native-verify.ts +293 -22
  68. package/src/agents/provider-errors.ts +9 -2
  69. package/src/agents/registry.ts +7 -1
  70. package/src/cli/agent-guide.ts +39 -5
  71. package/src/cli/app.ts +11 -4
  72. package/src/cli/cli.ts +7 -0
  73. package/src/cli/command-index.ts +7 -0
  74. package/src/cli/fleet-accounts.ts +253 -0
  75. package/src/cli/fleet-state.ts +499 -0
  76. package/src/cli/fleet-sync-accounts.ts +58 -0
  77. package/src/cli/fleet-sync-checkout.ts +56 -17
  78. package/src/cli/fleet-sync-enrollment.ts +19 -23
  79. package/src/cli/fleet-sync-forward.ts +1 -1
  80. package/src/cli/fleet-sync-manager.ts +84 -12
  81. package/src/cli/fleet-sync-monitoring.ts +5 -5
  82. package/src/cli/fleet-sync-output.ts +7 -3
  83. package/src/cli/fleet-sync-remote.ts +31 -18
  84. package/src/cli/fleet-sync-state.ts +86 -0
  85. package/src/cli/fleet-sync.ts +159 -57
  86. package/src/cli/fleet.ts +4 -2
  87. package/src/cli/managed-health.ts +4 -4
  88. package/src/cli/node-sync.ts +7 -0
  89. package/src/cli/program.ts +20 -4
  90. package/src/cli/project.ts +51 -0
  91. package/src/cli/register-fleet.ts +86 -1
  92. package/src/cli/registry.ts +41 -1
  93. package/src/cli/roster.ts +46 -30
  94. package/src/cli/settings.ts +15 -12
  95. package/src/cli/source-operations.ts +27 -3
  96. package/src/cli/tailnet.ts +2 -2
  97. package/src/definition.ts +116 -4
  98. package/src/diagnostics.ts +1 -1
  99. package/src/fleet-config.ts +105 -19
  100. package/src/ide/definition.ts +86 -0
  101. package/src/ide/ide.ts +43 -0
  102. package/src/ide/index.ts +5 -0
  103. package/src/ide/orchestrate.ts +50 -0
  104. package/src/ide/t3.ts +111 -0
  105. package/src/machine/source.ts +3 -3
  106. package/src/machine/state.ts +15 -27
  107. package/src/manager-sync.ts +9 -18
  108. package/src/monitoring/agent-evidence.ts +146 -0
  109. package/src/monitoring/apply.ts +15 -3
  110. package/src/monitoring/beszel.ts +11 -4
  111. package/src/monitoring/enroll.ts +2 -1
  112. package/src/monitoring/hub-access.ts +4 -3
  113. package/src/monitoring/hub-backup.ts +121 -0
  114. package/src/monitoring/hub-viewer.ts +8 -8
  115. package/src/monitoring/hub.ts +3 -3
  116. package/src/monitoring/manager-hub.ts +30 -14
  117. package/src/node-converge-linux.ts +30 -7
  118. package/src/package-info.ts +45 -0
  119. package/src/platform/ansible.ts +19 -15
  120. package/src/platform/darwin.ts +0 -13
  121. package/src/platform/fallback.ts +0 -8
  122. package/src/platform/linux.ts +0 -10
  123. package/src/platform/resource-verify.ts +8 -4
  124. package/src/platform/t3-policy.ts +33 -6
  125. package/src/platform/t3-service.ts +104 -0
  126. package/src/platform/types.ts +0 -4
  127. package/src/platform/ubuntu-audit.ts +5 -2
  128. package/src/platform/windows.ts +0 -14
  129. package/src/remote-bootstrap.ts +17 -19
  130. package/src/remote-entry.ts +15 -1
  131. package/src/repair-catalog.ts +1 -1
  132. package/src/runner.ts +1 -1
  133. package/src/service-auth.ts +15 -5
  134. package/src/state/bucket.ts +208 -0
  135. package/src/state/check.ts +74 -0
  136. package/src/state/connection.ts +92 -0
  137. package/src/state/enrollment.ts +45 -0
  138. package/src/state/legacy.ts +89 -0
  139. package/src/state/litestream.ts +143 -0
  140. package/src/state/operator.ts +20 -0
  141. package/src/state/paths.ts +40 -0
  142. package/src/state/roster.ts +62 -0
  143. package/src/state/store.ts +169 -0
  144. package/src/state/sync.ts +352 -0
  145. package/src/t3/accounts.ts +7 -1
  146. package/src/t3/models.ts +133 -0
  147. package/src/t3/pairing.ts +31 -0
  148. package/src/t3/profile.ts +203 -0
  149. package/src/t3/version.ts +119 -0
  150. package/src/t3-endpoint.ts +64 -8
  151. package/src/t3-linked-check.ts +24 -2
  152. package/src/toolchain-checks.ts +25 -0
  153. package/src/transport.ts +3 -2
  154. package/src/types.ts +4 -0
  155. package/src/worker-access-checks.ts +56 -3
  156. package/src/worker-host-checks.ts +41 -13
  157. package/src/workstation-checks.ts +1 -1
  158. package/src/index.ts +0 -6
  159. package/src/test-fixtures.ts +0 -36
@@ -0,0 +1,253 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { FleetUsageError } from '../errors';
5
+ import { fleet } from '../fleet-config';
6
+ import { runCommand } from '../runner';
7
+ import { blocking, type Check, type Runner } from '../types';
8
+ import {
9
+ nativeLoginCheck,
10
+ nativeLogins,
11
+ providerAccountChecks,
12
+ rosterCurrentCheck,
13
+ } from '../accounts/checks';
14
+ import { exposedVersion, t3Settings } from '../accounts/context';
15
+ import { parseRoster, type ProxyRoster } from '../accounts/roster';
16
+ import { exposedInstances } from '../accounts/sessions';
17
+ import { forwardSourceOperation, ownerCheckout } from './source-operations';
18
+ import type { CommandOutcome } from './contracts';
19
+ import type { Settings } from './settings';
20
+ import { fleetSync } from './fleet-sync';
21
+
22
+ export interface FleetAccountsOptions {
23
+ diff?: boolean;
24
+ add?: boolean;
25
+ relogin?: boolean;
26
+ remove?: boolean;
27
+ removeNativeLogins?: boolean;
28
+ yes?: boolean;
29
+ }
30
+
31
+ export interface AccountsDependencies {
32
+ run?: Runner;
33
+ /** Runs a proxy command attached to the terminal (a sign-in). */
34
+ operate?: (args: string[]) => Promise<number>;
35
+ /** `fleet-sync --only accounts`. */
36
+ propagate?: () => Promise<CommandOutcome>;
37
+ home?: string;
38
+ settingsFile?: string;
39
+ }
40
+
41
+ const PROVIDER = /^[a-z][a-z0-9-]{0,31}$/;
42
+
43
+ /** `devfleet fleet-accounts`: the provider account roster the proxy holds,
44
+ * each account's state, and the account checks. `--add`, `--relogin` and
45
+ * `--remove` change one account on the proxy and propagate the result to
46
+ * every node at once.
47
+ */
48
+ export async function fleetAccounts(
49
+ options: FleetAccountsOptions,
50
+ args: string[],
51
+ settings: Settings,
52
+ dependencies: AccountsDependencies = {}
53
+ ): Promise<CommandOutcome> {
54
+ const home = dependencies.home ?? homedir();
55
+ if (options.removeNativeLogins)
56
+ return removeNativeLogins(Boolean(options.yes), home);
57
+ const change = lifecycle(options, args);
58
+ const changed = change
59
+ ? await applyChange(change, settings, dependencies)
60
+ : undefined;
61
+ if (changed && !changed.data) return failedChange();
62
+ return report(options, settings, dependencies, { change, changed });
63
+ }
64
+
65
+ async function report(
66
+ options: FleetAccountsOptions,
67
+ settings: Settings,
68
+ dependencies: AccountsDependencies,
69
+ {
70
+ change,
71
+ changed,
72
+ }: { change: string[] | null; changed: CommandOutcome | undefined }
73
+ ): Promise<CommandOutcome> {
74
+ const home = dependencies.home ?? homedir();
75
+ const roster = await readRoster(
76
+ dependencies.run ?? runCommand,
77
+ settings.workspace
78
+ );
79
+ const checks: Check[] = providerAccountChecks(
80
+ roster,
81
+ fleet.services.proxy.accounts?.minimumUsable
82
+ );
83
+ if (options.diff)
84
+ checks.push(...(await thisMachine(roster, home, dependencies)));
85
+ const ok = !checks.some(blocking) && !changed?.exitCode;
86
+ return {
87
+ exitCode: ok ? 0 : 1,
88
+ data: {
89
+ version: 1,
90
+ command: 'fleet-accounts',
91
+ ok,
92
+ ...listing(roster),
93
+ checks,
94
+ ...(change && {
95
+ changed: change.slice(0, 2),
96
+ propagation: changed?.data,
97
+ }),
98
+ },
99
+ };
100
+ }
101
+
102
+ /** The roster as listed: no routing prefix, no file. */
103
+ function listing(roster: ProxyRoster) {
104
+ return {
105
+ rosterVersion: roster.rosterVersion,
106
+ managementApi: roster.managementApi,
107
+ accounts: roster.accounts.map(
108
+ ({ provider, index, label, state, until }) => ({
109
+ provider,
110
+ index,
111
+ label,
112
+ state,
113
+ until,
114
+ })
115
+ ),
116
+ };
117
+ }
118
+
119
+ /** Runs the change on the proxy, then propagates it to every node. A change
120
+ * the proxy did not complete answers with no data: nothing was propagated.
121
+ */
122
+ async function applyChange(
123
+ change: string[],
124
+ settings: Settings,
125
+ dependencies: AccountsDependencies
126
+ ): Promise<CommandOutcome> {
127
+ const operate =
128
+ dependencies.operate ??
129
+ ((forwarded: string[]) =>
130
+ forwardSourceOperation(['cliproxy', ...forwarded], settings));
131
+ if ((await operate(change)) !== 0) return { exitCode: 1, data: null };
132
+ return (
133
+ dependencies.propagate ??
134
+ (() => fleetSync({ only: 'accounts', json: true }, settings))
135
+ )();
136
+ }
137
+
138
+ function failedChange(): CommandOutcome {
139
+ return {
140
+ exitCode: 1,
141
+ data: {
142
+ version: 1,
143
+ command: 'fleet-accounts',
144
+ ok: false,
145
+ error: 'ACCOUNT_CHANGE_FAILED',
146
+ summary:
147
+ 'The proxy did not complete the change; an interrupted sign-in leaves no credential behind. Nothing was propagated.',
148
+ },
149
+ };
150
+ }
151
+
152
+ /** `--diff`: what this machine offers against the roster. */
153
+ async function thisMachine(
154
+ roster: ProxyRoster,
155
+ home: string,
156
+ dependencies: AccountsDependencies
157
+ ): Promise<Check[]> {
158
+ const exposed = await exposedInstances(
159
+ dependencies.settingsFile ?? t3Settings()
160
+ );
161
+ return [
162
+ rosterCurrentCheck(roster.rosterVersion, exposedVersion(exposed)),
163
+ nativeLoginCheck(nativeLogins(home)),
164
+ ];
165
+ }
166
+
167
+ /** The proxy command a lifecycle flag stands for, or null for a listing. */
168
+ function lifecycle(
169
+ options: FleetAccountsOptions,
170
+ [provider, index]: string[]
171
+ ): string[] | null {
172
+ const chosen = (['add', 'relogin', 'remove'] as const).filter(
173
+ (flag) => options[flag]
174
+ );
175
+ if (chosen.length === 0) return null;
176
+ if (chosen.length > 1)
177
+ throw new FleetUsageError('Choose one of --add, --relogin and --remove.');
178
+ const flag = chosen[0]!;
179
+ if (!provider || !PROVIDER.test(provider))
180
+ throw new FleetUsageError(
181
+ `--${flag} needs a provider, such as claude or codex.`
182
+ );
183
+ if (flag === 'add') return ['add-account', provider];
184
+ if (!index || !/^[1-9][0-9]{0,3}$/.test(index))
185
+ throw new FleetUsageError(
186
+ `--${flag} needs the account's index, as devfleet fleet-accounts lists it.`
187
+ );
188
+ return [flag === 'relogin' ? 'relogin' : 'remove-account', provider, index];
189
+ }
190
+
191
+ /** The roster as the proxy on this manager reports it. */
192
+ async function readRoster(
193
+ run: Runner,
194
+ workspace: string
195
+ ): Promise<ProxyRoster> {
196
+ const checkout = ownerCheckout(workspace);
197
+ if (!checkout)
198
+ throw new FleetUsageError(
199
+ 'The account roster is kept by the proxy on the manager. Run devfleet fleet-accounts there, after a first fleet-sync.'
200
+ );
201
+ const result = await run(
202
+ [
203
+ 'python3',
204
+ '-B',
205
+ join(checkout, 'cliproxy/scripts/cli.py'),
206
+ 'accounts',
207
+ '--json',
208
+ ],
209
+ { cwd: checkout, timeoutMs: 60_000 }
210
+ );
211
+ try {
212
+ if (!result.ok) throw new Error();
213
+ return parseRoster(result.stdout);
214
+ } catch {
215
+ throw new FleetUsageError(
216
+ 'The proxy did not report its accounts. Check it with devfleet cliproxy status.'
217
+ );
218
+ }
219
+ }
220
+
221
+ /** Opt-in: deletes the provider logins agent CLIs keep outside the proxy. */
222
+ async function removeNativeLogins(
223
+ yes: boolean,
224
+ home: string
225
+ ): Promise<CommandOutcome> {
226
+ const found = nativeLogins(home);
227
+ if (!yes)
228
+ return {
229
+ exitCode: found.length ? 1 : 0,
230
+ data: {
231
+ version: 1,
232
+ command: 'fleet-accounts',
233
+ ok: found.length === 0,
234
+ applied: false,
235
+ checks: [nativeLoginCheck(found)],
236
+ next: found.length
237
+ ? 'Add --yes to remove these logins from this machine.'
238
+ : undefined,
239
+ },
240
+ };
241
+ for (const login of found) await rm(login.path, { force: true });
242
+ return {
243
+ exitCode: 0,
244
+ data: {
245
+ version: 1,
246
+ command: 'fleet-accounts',
247
+ ok: true,
248
+ applied: true,
249
+ removed: [...new Set(found.map((login) => login.cli))].sort(),
250
+ checks: [nativeLoginCheck(nativeLogins(home))],
251
+ },
252
+ };
253
+ }