@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
@@ -1,6 +1,5 @@
1
- import { readFile, mkdir, writeFile, rename, rm } from 'node:fs/promises';
2
- import { randomUUID } from 'node:crypto';
3
- import { loadConfig } from '../config';
1
+ import { mkdir, rm } from 'node:fs/promises';
2
+ import { loadRoster } from './roster';
4
3
  import { enrolledRoster } from '../definition';
5
4
  import { FleetUsageError } from '../errors';
6
5
  import { dirname } from 'node:path';
@@ -10,6 +9,7 @@ import { matchComputers } from '../tailnet-matching';
10
9
  import { sshArgs, sshFailure } from '../ssh';
11
10
  import { ensurePrivateRoot } from '../machine/state';
12
11
  import type { FleetNode, Runner } from '../types';
12
+ import { STATE_ROSTER, readRosterText, writeRosterText } from '../state/roster';
13
13
 
14
14
  /** Interactive enrollment/resume only. Existing paths and explicit identities
15
15
  * are preserved. Tailnet candidates never enter the declared membership.
@@ -20,7 +20,7 @@ export async function enrollManager(path: string, run: Runner): Promise<void> {
20
20
  const saved =
21
21
  original === null
22
22
  ? []
23
- : enrolledRoster(canonical, await loadConfig(path)).nodes;
23
+ : enrolledRoster(canonical, await loadRoster(path)).nodes;
24
24
  const nodes: FleetNode[] = canonical.workers.map((node) => ({
25
25
  id: node.id,
26
26
  ssh: node.ssh,
@@ -55,8 +55,6 @@ export async function enrollManager(path: string, run: Runner): Promise<void> {
55
55
  process.stderr.write(
56
56
  `Enrolling verified SSH connections for canonical targets:\n${enrolled.map((node) => ` ${node.id} → ${node.ssh}; device ${enrollmentIdentity(node)}`).join('\n')}\nMissing targets remain visible and are never replaced by discovered peers.\n`
57
57
  );
58
- await ensurePrivateRoot(dirname(path));
59
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
60
58
  await persistEnrollment(path, original, merged);
61
59
  }
62
60
 
@@ -95,13 +93,8 @@ function ambiguous(ids: string[], matches: Map<string, FleetNode[]>): boolean {
95
93
  ids.length > 1 || (ids.length === 1 && matches.get(ids[0]!)?.length !== 1)
96
94
  );
97
95
  }
98
- async function existingEnrollment(path: string): Promise<string | null> {
99
- try {
100
- return await readFile(path, 'utf8');
101
- } catch (error) {
102
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
103
- throw error;
104
- }
96
+ function existingEnrollment(path: string): Promise<string | null> {
97
+ return readRosterText(path);
105
98
  }
106
99
  export function mergeEnrollment(
107
100
  saved: FleetNode[],
@@ -123,22 +116,25 @@ async function persistEnrollment(
123
116
  original: string | null,
124
117
  nodes: FleetNode[]
125
118
  ): Promise<void> {
126
- const lock = `${path}.enroll.lock`;
127
- await mkdir(lock, { mode: 0o700 });
128
- const temporary = `${path}.${randomUUID()}.tmp`;
129
- try {
119
+ const write = async (): Promise<void> => {
130
120
  if ((await existingEnrollment(path)) !== original)
131
121
  throw new FleetUsageError(
132
122
  'Enrollment changed during approval. Inspect it and rerun; no connection was overwritten.'
133
123
  );
134
- await writeFile(
135
- temporary,
136
- JSON.stringify({ version: 1, nodes }, null, 2) + '\n',
137
- { flag: 'wx', mode: 0o600 }
124
+ await writeRosterText(
125
+ path,
126
+ JSON.stringify({ version: 1, nodes }, null, 2) + '\n'
138
127
  );
139
- await rename(temporary, path);
128
+ };
129
+ // The fleet state serializes its own writers; a roster file needs a lock.
130
+ if (path === STATE_ROSTER) return write();
131
+ await ensurePrivateRoot(dirname(path));
132
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
133
+ const lock = `${path}.enroll.lock`;
134
+ await mkdir(lock, { mode: 0o700 });
135
+ try {
136
+ await write();
140
137
  } finally {
141
- await rm(temporary, { force: true });
142
138
  await rm(lock, { recursive: true });
143
139
  }
144
140
  }
@@ -91,7 +91,7 @@ export function managerCommand(
91
91
  revision,
92
92
  ...forwardedFlags(options, undefined),
93
93
  ]
94
- : ['personal', 'fleet-sync', ...forwardedFlags(options, revision)];
94
+ : ['devfleet', 'fleet-sync', ...forwardedFlags(options, revision)];
95
95
  const color = process.stdout.isTTY && !process.env['NO_COLOR'];
96
96
  // A login shell gives the manager's normal PATH (mise, Docker, Tailscale).
97
97
  const script = `export ${NO_FORWARD_ENV}=1${color ? ' FORCE_COLOR=1' : ''}; exec ${args.map(shellQuote).join(' ')}`;
@@ -6,11 +6,18 @@ import { convergeProxy } from '../proxy-converge';
6
6
  import { attempt } from '../node-converge-shared';
7
7
  import { quotaLimited, verifyFleetProviders } from '../agents/fleet-verify';
8
8
  import { verifyNativeAgents } from '../agents/native-verify';
9
+ import { accountContext, nodeAccountChecks } from '../accounts/context';
9
10
  import { convergeManagerHub } from '../monitoring/manager-hub';
10
11
  import { enrollManagerMonitoring } from './fleet-sync-monitoring';
11
12
  import { installConfig } from '../fleet-config';
12
- import { machineProviders } from '../t3/providers';
13
- import { providerNames } from '../t3/names-check';
13
+ import { IDES } from '../ide';
14
+ import { enabled, managerChecks } from '../ide/orchestrate';
15
+ import { idesOf } from '../ide/definition';
16
+ import macos from '../../ansible/macos/policy.json';
17
+ import { ansibleCommand } from '../platform/ansible';
18
+ import { PACKAGE_ROOT } from '../package-info';
19
+ import { readFileSync } from 'node:fs';
20
+ import { join } from 'node:path';
14
21
 
15
22
  /** The manager converges itself locally (never over SSH) before any worker:
16
23
  * it must be the declared manager, stay awake on AC power, and host every
@@ -32,9 +39,17 @@ export async function convergeManager(
32
39
  if (identity.status === 'pass') {
33
40
  // Launchers and scheduled jobs on the manager read the installed copy.
34
41
  if (input.apply) installConfig();
35
- checks.push(await managerPower(run));
36
- checks.push(await machineProviders(id, input.workspace, input.apply));
37
- checks.push(await providerNames(id, input.workspace, input.apply));
42
+ checks.push(await managerPower(run, input.workspace));
43
+ checks.push(await managerDocker(run));
44
+ // Each enabled IDE: its settings, pinned version and policy.
45
+ checks.push(
46
+ ...(await managerChecks(enabled(idesOf(definition), IDES), {
47
+ id,
48
+ workspace: input.workspace,
49
+ apply: input.apply,
50
+ run,
51
+ }))
52
+ );
38
53
  if (input.apply) {
39
54
  // Same provider profiles and the same Vault gateway credential as every
40
55
  // worker: the instances above launch from these profiles, and the check
@@ -49,10 +64,17 @@ export async function convergeManager(
49
64
  );
50
65
  checks.push(...providers);
51
66
  // Same agents, versions and shared accounts as every worker.
67
+ const accounts = await accountContext();
68
+ checks.push(
69
+ ...(await attempt('accounts:verification', () =>
70
+ nodeAccountChecks(accounts)
71
+ ))
72
+ );
52
73
  checks.push(
53
74
  ...(await attempt('agent:verification', () =>
54
75
  verifyNativeAgents(id, input.workspace, run, {
55
76
  outOfQuota: quotaLimited(providers),
77
+ ...(accounts ? { accounts } : {}),
56
78
  })
57
79
  ))
58
80
  );
@@ -170,12 +192,22 @@ export async function managerIdentity(id: string, run: Runner): Promise<Check> {
170
192
  };
171
193
  }
172
194
 
173
- /** Workers depend on the manager's services, so it never sleeps on AC power. */
174
- export async function managerPower(run: Runner): Promise<Check> {
195
+ /** Workers depend on the manager's services, so it never sleeps on AC power.
196
+ * Measures every setting `ansible/macos/manager.yml` declares, from the same
197
+ * policy file the playbook applies.
198
+ */
199
+ export async function managerPower(
200
+ run: Runner,
201
+ workspace = '.'
202
+ ): Promise<Check> {
175
203
  const result = await run(['pmset', '-g', 'custom']);
176
204
  const ac = result.ok ? (result.stdout.split('AC Power:')[1] ?? '') : '';
177
- const sleep = /^\s*sleep\s+(\d+)/m.exec(ac)?.[1];
178
- return sleep === '0'
205
+ const observed = (key: string) =>
206
+ new RegExp(`^\\s*${key}\\s+(\\S+)`, 'm').exec(ac)?.[1];
207
+ const drifted = Object.entries(macos.pmset)
208
+ .filter(([key, value]) => observed(key) !== value)
209
+ .map(([key]) => key);
210
+ return drifted.length === 0
179
211
  ? {
180
212
  id: 'manager:power',
181
213
  status: 'pass',
@@ -184,10 +216,50 @@ export async function managerPower(run: Runner): Promise<Check> {
184
216
  : {
185
217
  id: 'manager:power',
186
218
  status: 'fail',
187
- summary: 'MANAGER_SLEEPS_ON_POWER',
219
+ summary: drifted.includes('sleep')
220
+ ? 'MANAGER_SLEEPS_ON_POWER'
221
+ : `MANAGER_POWER_POLICY_DRIFT ${drifted.join(' ')}`,
188
222
  remedy: {
189
- action:
190
- 'Workers lose the CLI proxy and Beszel hub while the manager sleeps. Apply the manager power policy once: ansible-playbook -K ansible/macos/manager.yml',
223
+ action: `Workers lose the CLI proxy and Beszel hub while the manager sleeps. Apply the manager power policy once, as an administrator: cd ${workspace} && ${ansibleCommand().join(' ')} -K ansible/macos/manager.yml`,
191
224
  },
192
225
  };
193
226
  }
227
+
228
+ /** The Docker Desktop release `mise.toml` pins, as the engine names itself. */
229
+ export function pinnedDockerDesktop(root = PACKAGE_ROOT): string {
230
+ const { vars } = Bun.TOML.parse(
231
+ readFileSync(join(root, 'mise.toml'), 'utf8')
232
+ ) as { vars: Record<string, string> };
233
+ return `Docker Desktop ${vars['docker_desktop_version']} (${vars['docker_desktop_build']})`;
234
+ }
235
+
236
+ /** The manager's services run in Docker Desktop, which no playbook installs
237
+ * and which updates itself: the pinned release is measured, and a different
238
+ * one warns instead of failing the run.
239
+ */
240
+ export async function managerDocker(
241
+ run: Runner,
242
+ pinned = pinnedDockerDesktop()
243
+ ): Promise<Check> {
244
+ const result = await run(
245
+ ['docker', 'version', '--format', '{{.Server.Platform.Name}}'],
246
+ { timeoutMs: 20_000 }
247
+ );
248
+ const observed = result.ok ? result.stdout.trim() : '';
249
+ if (observed === pinned)
250
+ return {
251
+ id: 'manager:docker',
252
+ status: 'pass',
253
+ summary: `The manager runs the pinned ${pinned}.`,
254
+ };
255
+ return {
256
+ id: 'manager:docker',
257
+ status: 'warn',
258
+ summary: observed
259
+ ? 'MANAGER_DOCKER_DESKTOP_DRIFT'
260
+ : 'MANAGER_DOCKER_DESKTOP_UNOBSERVED',
261
+ remedy: {
262
+ action: `The manager's services are tested on ${pinned}. Install that release and turn off its automatic updates, or expect differences; fleet-sync does not install or replace Docker Desktop.`,
263
+ },
264
+ };
265
+ }
@@ -2,7 +2,7 @@ import type { FleetNode, Runner, Check } from '../types';
2
2
  import type { FleetDefinition } from '../definition';
3
3
  import { hubUrl, roleOf } from '../topology';
4
4
  import { discoverTailnet } from '../tailscale';
5
- import { BESZEL_FLEET_IDENTITY, hubAccess } from '../monitoring/hub-access';
5
+ import { fleetIdentity, hubAccess } from '../monitoring/hub-access';
6
6
  import { enrollBeszel } from '../monitoring/enroll';
7
7
  import { verifyBeszel } from '../monitoring/beszel';
8
8
  import { installRemoteAgent } from '../monitoring/remote';
@@ -142,15 +142,15 @@ async function hubReachable(hub: string, run: Runner): Promise<boolean> {
142
142
  return probe.ok;
143
143
  }
144
144
 
145
- const REMEDIES: Record<string, string> = {
146
- BESZEL_ACCOUNT_REQUIRED: `The hub rejected the fleet login ${BESZEL_FLEET_IDENTITY}. A new hub on the manager creates it on first start from BESZEL_FLEET_AUTH; an older hub needs it once, by its superuser, as a verified users record with that Vault password. Never paste the password on a command line.`,
145
+ const remedies = (): Record<string, string> => ({
146
+ BESZEL_ACCOUNT_REQUIRED: `The hub rejected the fleet login ${fleetIdentity()}. A new hub on the manager creates it on first start from BESZEL_FLEET_AUTH; an older hub needs it once, by its superuser, as a verified users record with that Vault password. Never paste the password on a command line.`,
147
147
  BESZEL_HUB_UNREACHABLE:
148
148
  'The hub runs on the manager; keep it awake and Docker Desktop running, then rerun fleet-sync from the manager so it converges the hub first.',
149
149
  BESZEL_NODE_TAILNET_REQUIRED:
150
150
  'The worker is not visible on the tailnet. Sign it in to Tailscale and record its tailscaleId in the local roster, then rerun fleet-sync.',
151
151
  BESZEL_NOT_ENROLLED:
152
152
  'Only workers declared in fleet.json run the agent over SSH; fleet-sync installs the manager’s own agent locally after its hub.',
153
- };
153
+ });
154
154
  function unavailable(code: string): Check[] {
155
155
  return [
156
156
  {
@@ -159,7 +159,7 @@ function unavailable(code: string): Check[] {
159
159
  summary: code,
160
160
  remedy: {
161
161
  action:
162
- REMEDIES[code] ??
162
+ remedies()[code] ??
163
163
  "Verify the manager's private hub is running and the manager has BESZEL_FLEET_AUTH through existing Vault authorization. The normal hub account must access these systems. Inspect duplicate registration/host conflicts before retry; no token or raw response is printed.",
164
164
  },
165
165
  },
@@ -19,9 +19,10 @@ const COLUMNS: { title: string; matches: (id: string) => boolean }[] = [
19
19
  matches: (id) =>
20
20
  [
21
21
  't3:service',
22
- 't3:policy',
22
+ 't3:policy-staged',
23
+ 't3:policy-loaded',
24
+ 't3:running-version',
23
25
  't3:providers',
24
- 't3:antigravity-key',
25
26
  't3:private-route',
26
27
  'worker:t3-recovery',
27
28
  'worker:t3-health',
@@ -30,7 +31,10 @@ const COLUMNS: { title: string; matches: (id: string) => boolean }[] = [
30
31
  },
31
32
  {
32
33
  title: 'LINK',
33
- matches: (id) => ['t3:manager-endpoint', 't3:paired-access'].includes(id),
34
+ matches: (id) =>
35
+ ['t3:manager-endpoint', 't3:manager-origin', 't3:paired-access'].includes(
36
+ id
37
+ ),
34
38
  },
35
39
  {
36
40
  title: 'PROXY',
@@ -1,7 +1,8 @@
1
- import { verifyT3Linked } from '../t3-linked-check';
2
- import { T3_RESTARTED_SUMMARY } from '../platform/t3-policy';
3
1
  import { createHash } from 'node:crypto';
4
- import { verifyT3Endpoint } from '../t3-endpoint';
2
+ import { IDES } from '../ide';
3
+ import { enabled, workerChecks } from '../ide/orchestrate';
4
+ import { idesOf } from '../ide/definition';
5
+ import { rememberEnvironment } from '../t3/pairing';
5
6
  import { authorizePrivilege } from './fleet-sync-privilege';
6
7
  import { authorizeVault } from './fleet-sync-vault';
7
8
  import { managerFailure } from '../manager-failure';
@@ -11,7 +12,8 @@ import { saveReport } from './fleet-sync-report';
11
12
  import { synchronizeMonitoring } from './fleet-sync-monitoring';
12
13
  import { convergeManager } from './fleet-sync-manager';
13
14
  import { FleetUsageError } from '../errors';
14
- import { loadConfig, selectNodes } from '../config';
15
+ import { selectNodes } from '../config';
16
+ import { loadRoster } from './roster';
15
17
  import { enrolledRoster } from '../definition';
16
18
  import type { FleetDefinition } from '../definition';
17
19
  import { fleet as definition } from '../fleet-config';
@@ -19,23 +21,27 @@ import { synchronizeManager } from '../manager-sync';
19
21
  import { inspectBootstrap } from '../manager-sync';
20
22
  import { prepareBootstrap, bootstrapRemote } from '../remote-bootstrap';
21
23
  import { machineRoot } from './fleet-sync';
22
- import type { Runner } from '../types';
23
- import type { DeployNode } from '../manager-sync';
24
+ import type { Check, Runner } from '../types';
25
+ import type { DeployNode, ManagerReport } from '../manager-sync';
24
26
  import type { FleetSyncOptions } from './fleet-sync';
25
27
  import type { Settings } from './settings';
26
28
  import type { CommandOutcome } from './contracts';
27
29
 
30
+ /** `settle` runs once the fleet's result is known and before it is reported:
31
+ * it records the result and returns the check that says so.
32
+ */
28
33
  export async function selected(
29
34
  options: FleetSyncOptions,
30
35
  settings: Settings,
31
- run: Runner
36
+ run: Runner,
37
+ settle?: (report: ManagerReport) => Promise<Check>
32
38
  ): Promise<CommandOutcome> {
33
39
  if (!options.revision)
34
40
  throw new FleetUsageError('An approved source revision is required.');
35
41
  const canonical = definition;
36
42
  let roster;
37
43
  try {
38
- roster = enrolledRoster(canonical, await loadConfig(settings.config));
44
+ roster = enrolledRoster(canonical, await loadRoster(settings.config));
39
45
  } catch {
40
46
  return enrollmentRequired(canonical, options.revision);
41
47
  }
@@ -87,8 +93,13 @@ export async function selected(
87
93
  ),
88
94
  },
89
95
  run,
90
- deployWithMonitoring(payload, canonical, run, ui)
96
+ deployWithMonitoring(payload, canonical, run, {
97
+ ...ui,
98
+ workspace: settings.workspace,
99
+ roster: settings.config,
100
+ })
91
101
  ).finally(() => ui.progress.stop());
102
+ if (settle) report.checks.push(await settle(report));
92
103
  return outcome(report, planFingerprint, started);
93
104
  }
94
105
 
@@ -151,6 +162,9 @@ function deployWithMonitoring(
151
162
  ui: {
152
163
  progress: SyncProgress;
153
164
  interactiveAuthorization: boolean;
165
+ /** The manager's checkout, and the roster an identity is recorded in. */
166
+ workspace: string;
167
+ roster: string;
154
168
  }
155
169
  ): DeployNode {
156
170
  return async (node, revision) => {
@@ -188,16 +202,15 @@ function deployWithMonitoring(
188
202
  // Workers are Linux by definition.
189
203
  ui.progress.step(node.id, 'Verifying SSH, OS and T3');
190
204
  result.checks.push(...(await inspectBootstrap(node, run, 'linux')));
191
- result.checks.push(...(await verifyT3Endpoint(node, run)));
192
- // A worker T3 restarted by this sync (idle policy heal) needs a minute or
193
- // two before the manager's link reconnects.
194
- const restarted = result.checks.some(
195
- (check) => check.summary === T3_RESTARTED_SUMMARY
196
- );
197
205
  result.checks.push(
198
- await verifyT3Linked(node, run, {
199
- reconnectMs: restarted ? 180_000 : 0,
200
- })
206
+ ...(await workerChecks(enabled(idesOf(canonical), IDES), node, {
207
+ run,
208
+ workspace: ui.workspace,
209
+ earlier: result.checks,
210
+ remember: async (id, identity) => {
211
+ await rememberEnvironment(ui.roster, id, identity);
212
+ },
213
+ }))
201
214
  );
202
215
  if (
203
216
  result.checks.some(
@@ -0,0 +1,86 @@
1
+ import type { Enrollment } from '../machine/state';
2
+ import { replicaCheck } from '../state/check';
3
+ import { storeEnrollment } from '../state/enrollment';
4
+ import { adoptEarlier } from '../state/legacy';
5
+ import { databasePath, stateRoot } from '../state/paths';
6
+ import { writeDocument } from '../state/store';
7
+ import { pending, push, type Standing, type StateContext } from '../state/sync';
8
+ import type { Check } from '../types';
9
+
10
+ /** What one fleet-sync run records in the fleet state: the revision it was
11
+ * approved for and the configuration it runs with, then what it applied, then
12
+ * an upload so the bucket holds all of it.
13
+ */
14
+ export interface RunRecord {
15
+ /** Before any machine changes: this revision is the approved one. */
16
+ approve(): void;
17
+ /** Once the result is known. The check says whether the bucket holds it. */
18
+ settle(converged: boolean): Promise<Check>;
19
+ /** For a run that stopped before its result: its approval is still uploaded. */
20
+ abandon(): Promise<void>;
21
+ }
22
+
23
+ export function runRecord(
24
+ previous: Enrollment | null,
25
+ revision: string,
26
+ state: {
27
+ context: StateContext;
28
+ standing: Standing;
29
+ env?: NodeJS.ProcessEnv;
30
+ /** The configuration this run reads from a project's fleet.json. Stored
31
+ * so that a run started anywhere else (the schedule, a hand-off from a
32
+ * worker, a replacement manager) uses the one last applied.
33
+ */
34
+ definition?: string;
35
+ }
36
+ ): RunRecord {
37
+ const env = state.env ?? process.env;
38
+ const approval: Enrollment = {
39
+ ...previous,
40
+ version: 1,
41
+ id: 'manager',
42
+ approvedRevision: revision,
43
+ };
44
+ let settled = false;
45
+ /** Uploads only what this run changed; otherwise the bucket stands as it
46
+ * did when the run started.
47
+ */
48
+ const publish = (): Promise<Standing> =>
49
+ pending(state.context)
50
+ ? push(state.context)
51
+ : Promise.resolve(state.standing);
52
+ return {
53
+ approve: () => {
54
+ storeEnrollment(approval, env);
55
+ if (state.definition === undefined) return;
56
+ adoptEarlier(env);
57
+ writeDocument(
58
+ databasePath(stateRoot(env)),
59
+ 'definition',
60
+ state.definition
61
+ );
62
+ },
63
+ async settle(converged) {
64
+ settled = true;
65
+ if (converged)
66
+ storeEnrollment(
67
+ {
68
+ ...approval,
69
+ appliedRevision: revision,
70
+ ...(previous?.appliedRevision &&
71
+ previous.appliedRevision !== revision
72
+ ? { previousRevision: previous.appliedRevision }
73
+ : {}),
74
+ },
75
+ env
76
+ );
77
+ // The fleet converged either way: a bucket problem is reported, not fatal.
78
+ return replicaCheck(await publish().catch(() => 'unreachable' as const), {
79
+ advisory: true,
80
+ });
81
+ },
82
+ async abandon() {
83
+ if (!settled) await publish().catch(() => undefined);
84
+ },
85
+ };
86
+ }