@crvouga/dev-fleet 0.2.0 → 0.4.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 (129) hide show
  1. package/README.md +50 -16
  2. package/ansible/github/git.yml +66 -0
  3. package/ansible/macos/manager.yml +3 -0
  4. package/ansible/macos/policy.json +11 -1
  5. package/ansible/macos/t3-policy.yml +49 -0
  6. package/ansible/monitoring/compose.yml +10 -0
  7. package/ansible/monitoring/container-watchdog.py +118 -0
  8. package/ansible/monitoring/hub-darwin.yml +146 -0
  9. package/ansible/net/remote-probe.sh +5 -0
  10. package/ansible/ubuntu/baseline.yml +2 -0
  11. package/ansible/ubuntu/disk-gc.py +25 -3
  12. package/ansible/ubuntu/disk-guard.py +24 -5
  13. package/ansible/ubuntu/policy.json +51 -1
  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/tasks/browser.yml +56 -0
  19. package/ansible/ubuntu/tasks/docker.yml +52 -0
  20. package/ansible/ubuntu/tasks/recovery.yml +60 -1
  21. package/ansible/ubuntu/tasks/resources.yml +2 -2
  22. package/ansible/ubuntu/tasks/services.yml +13 -0
  23. package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
  24. package/ansible/ubuntu/wip-guard.py +121 -0
  25. package/bootstrap/doctor.ts +32 -6
  26. package/bootstrap/fleet-remote-bootstrap.sh +31 -4
  27. package/bootstrap/native-t3.ts +22 -11
  28. package/bootstrap/setup.ts +23 -8
  29. package/bootstrap/worker-tools.ts +118 -0
  30. package/cliproxy/README.md +4 -4
  31. package/cliproxy/compose.yaml +4 -0
  32. package/cliproxy/scripts/accounts.py +216 -0
  33. package/cliproxy/scripts/checkpoint.py +30 -2
  34. package/cliproxy/scripts/cli.py +1 -1
  35. package/cliproxy/scripts/clientkeys.py +99 -0
  36. package/cliproxy/scripts/lease.py +80 -0
  37. package/cliproxy/scripts/ops +137 -25
  38. package/cliproxy/scripts/restore-verify +15 -0
  39. package/docs/accounts.md +154 -0
  40. package/docs/authentication.md +90 -0
  41. package/docs/declared.md +5 -1
  42. package/docs/durability.md +26 -0
  43. package/docs/machine-setup.md +26 -15
  44. package/docs/operations.md +87 -31
  45. package/docs/runbooks.md +380 -0
  46. package/docs/state.md +13 -7
  47. package/docs/topology.md +2 -0
  48. package/docs/ubuntu-sync.md +35 -28
  49. package/examples/fleet.json +19 -1
  50. package/llms.txt +3 -0
  51. package/mise.toml +10 -1
  52. package/package.json +2 -2
  53. package/schema/check-ids.json +522 -0
  54. package/schema/fleet.schema.json +244 -1
  55. package/schema/output/check.schema.json +23 -0
  56. package/schema/output/error.schema.json +14 -0
  57. package/schema/output/fleet-report.schema.json +79 -0
  58. package/schema/output/fleet-sync.schema.json +112 -0
  59. package/schema/output/report.schema.json +38 -0
  60. package/src/accounts/checks.ts +220 -0
  61. package/src/accounts/context.ts +76 -0
  62. package/src/accounts/propagate.ts +62 -0
  63. package/src/accounts/roster.ts +85 -0
  64. package/src/accounts/selection.ts +67 -0
  65. package/src/accounts/sessions.ts +120 -0
  66. package/src/agents/account-relay.ts +176 -26
  67. package/src/agents/launch.ts +84 -5
  68. package/src/agents/native-verify.ts +128 -2
  69. package/src/auth-checks.ts +3 -0
  70. package/src/cli/agent-guide.ts +5 -3
  71. package/src/cli/fleet-accounts.ts +253 -0
  72. package/src/cli/fleet-report.ts +126 -0
  73. package/src/cli/fleet-sync-accounts.ts +58 -0
  74. package/src/cli/fleet-sync-manager.ts +31 -4
  75. package/src/cli/fleet-sync-output.ts +3 -2
  76. package/src/cli/fleet-sync-plan.ts +200 -0
  77. package/src/cli/fleet-sync-remote.ts +23 -14
  78. package/src/cli/fleet-sync-schedule.ts +142 -35
  79. package/src/cli/fleet-sync.ts +126 -8
  80. package/src/cli/fleet-watch.ts +198 -0
  81. package/src/cli/fleet.ts +4 -0
  82. package/src/cli/register-fleet.ts +86 -0
  83. package/src/cli/registry.ts +26 -0
  84. package/src/cli/schedule-logs.ts +36 -0
  85. package/src/clock.ts +63 -0
  86. package/src/definition.ts +226 -7
  87. package/src/git-identity.ts +160 -0
  88. package/src/ide/definition.ts +86 -0
  89. package/src/ide/ide.ts +43 -0
  90. package/src/ide/index.ts +5 -0
  91. package/src/ide/orchestrate.ts +50 -0
  92. package/src/ide/t3.ts +111 -0
  93. package/src/machine-health.ts +51 -4
  94. package/src/manager-health.ts +205 -0
  95. package/src/manager-sync.ts +13 -17
  96. package/src/monitoring/hub-backup.ts +63 -17
  97. package/src/monitoring/manager-hub.ts +44 -2
  98. package/src/node-converge-linux.ts +44 -12
  99. package/src/notify.ts +239 -0
  100. package/src/platform/darwin.ts +0 -13
  101. package/src/platform/fallback.ts +0 -8
  102. package/src/platform/linux.ts +0 -10
  103. package/src/platform/t3-policy.ts +33 -6
  104. package/src/platform/t3-service.ts +104 -0
  105. package/src/platform/types.ts +0 -4
  106. package/src/platform/windows.ts +0 -14
  107. package/src/remote-bootstrap.ts +87 -25
  108. package/src/remote-entry.ts +15 -1
  109. package/src/remote-operation.ts +213 -0
  110. package/src/schedule-check.ts +40 -0
  111. package/src/secrets.ts +26 -8
  112. package/src/service-auth.ts +15 -5
  113. package/src/ssh.ts +10 -2
  114. package/src/state/history.ts +172 -0
  115. package/src/state/store.ts +11 -4
  116. package/src/t3/accounts.ts +7 -1
  117. package/src/t3/models.ts +133 -0
  118. package/src/t3/pairing.ts +31 -0
  119. package/src/t3/profile.ts +203 -0
  120. package/src/t3/version.ts +119 -0
  121. package/src/t3-linked-check.ts +1 -1
  122. package/src/tailscale.ts +51 -0
  123. package/src/toolchain-checks.ts +8 -3
  124. package/src/transport.ts +3 -2
  125. package/src/worker-capability-checks.ts +167 -0
  126. package/src/worker-capacity.ts +19 -7
  127. package/src/worker-host-checks.ts +68 -13
  128. package/src/worker-policy.ts +33 -0
  129. package/src/worker-readiness.ts +10 -1
@@ -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
+ }
@@ -0,0 +1,126 @@
1
+ import { FleetUsageError } from '../errors';
2
+ import { readHistory, type SyncRun } from '../state/history';
3
+ import type { CommandOutcome } from './contracts';
4
+
5
+ export interface FleetReportOptions {
6
+ since?: string;
7
+ json?: boolean;
8
+ }
9
+
10
+ const UNITS: Record<string, number> = {
11
+ m: 60_000,
12
+ h: 3_600_000,
13
+ d: 86_400_000,
14
+ };
15
+
16
+ /** `30d`, `12h` or `90m`: how far back the report looks. */
17
+ export function parseSince(value = '30d'): number {
18
+ const match = /^([1-9]\d{0,4})([mhd])$/.exec(value);
19
+ if (!match)
20
+ throw new FleetUsageError(
21
+ '--since takes a duration such as 30d, 12h or 90m.'
22
+ );
23
+ return Number(match[1]) * UNITS[match[2]!]!;
24
+ }
25
+
26
+ export interface Reliability {
27
+ runs: number;
28
+ succeeded: number;
29
+ successRate: number | null;
30
+ meanDurationMs: number | null;
31
+ failingChecks: { id: string; runs: number }[];
32
+ notReady: { node: string; ms: number }[];
33
+ }
34
+
35
+ /** How long each machine was not ready: from the end of a run that left it
36
+ * not ready to the end of the next run that found it ready (or until now).
37
+ */
38
+ function notReadyTime(runs: SyncRun[], now: Date): Map<string, number> {
39
+ const since = new Map<string, number>();
40
+ const total = new Map<string, number>();
41
+ const add = (node: string, until: number) => {
42
+ total.set(node, (total.get(node) ?? 0) + until - since.get(node)!);
43
+ since.delete(node);
44
+ };
45
+ for (const run of runs) {
46
+ const at = Date.parse(run.finishedAt);
47
+ for (const node of [...since.keys()])
48
+ if (!run.notReady.includes(node)) add(node, at);
49
+ for (const node of run.notReady) if (!since.has(node)) since.set(node, at);
50
+ }
51
+ for (const node of [...since.keys()]) add(node, now.getTime());
52
+ return total;
53
+ }
54
+
55
+ export function reliability(runs: SyncRun[], now: Date): Reliability {
56
+ const succeeded = runs.filter((run) => run.ok).length;
57
+ const counts = new Map<string, number>();
58
+ for (const run of runs)
59
+ for (const id of run.failing) counts.set(id, (counts.get(id) ?? 0) + 1);
60
+ return {
61
+ runs: runs.length,
62
+ succeeded,
63
+ successRate: runs.length ? succeeded / runs.length : null,
64
+ meanDurationMs: runs.length
65
+ ? Math.round(
66
+ runs.reduce((sum, run) => sum + run.durationMs, 0) / runs.length
67
+ )
68
+ : null,
69
+ failingChecks: [...counts]
70
+ .map(([id, count]) => ({ id, runs: count }))
71
+ .sort((a, b) => b.runs - a.runs || a.id.localeCompare(b.id))
72
+ .slice(0, 10),
73
+ notReady: [...notReadyTime(runs, now)]
74
+ .map(([node, ms]) => ({ node, ms }))
75
+ .sort((a, b) => b.ms - a.ms || a.node.localeCompare(b.node)),
76
+ };
77
+ }
78
+
79
+ /** Read-only: the reliability of fleet-sync over a period, from the run
80
+ * history kept in the fleet state.
81
+ */
82
+ export function fleetReport(
83
+ options: FleetReportOptions,
84
+ env = process.env,
85
+ now = new Date()
86
+ ): CommandOutcome {
87
+ const since = options.since ?? '30d';
88
+ const from = now.getTime() - parseSince(since);
89
+ const runs = readHistory(env).runs.filter(
90
+ (run) => Date.parse(run.finishedAt) >= from
91
+ );
92
+ const summary = reliability(runs, now);
93
+ const data = {
94
+ version: 1,
95
+ command: 'fleet-report',
96
+ ok: true,
97
+ since,
98
+ ...summary,
99
+ entries: runs,
100
+ };
101
+ return { exitCode: 0, data, text: render(since, summary) };
102
+ }
103
+
104
+ function minutes(ms: number): string {
105
+ return `${Math.round(ms / 60_000)} min`;
106
+ }
107
+
108
+ function render(since: string, summary: Reliability): string {
109
+ if (!summary.runs) return `No fleet-sync run in the last ${since}.`;
110
+ const lines = [
111
+ `fleet-sync over the last ${since}: ${summary.runs} runs, ${summary.succeeded} succeeded (${Math.round((summary.successRate ?? 0) * 100)}%), ${Math.round((summary.meanDurationMs ?? 0) / 1000)} s on average.`,
112
+ ];
113
+ if (summary.failingChecks.length)
114
+ lines.push(
115
+ 'Most frequent failing checks:',
116
+ ...summary.failingChecks.map(
117
+ (check) => ` ${check.id} ${check.runs} runs`
118
+ )
119
+ );
120
+ if (summary.notReady.length)
121
+ lines.push(
122
+ 'Time not ready:',
123
+ ...summary.notReady.map((node) => ` ${node.node} ${minutes(node.ms)}`)
124
+ );
125
+ return lines.join('\n');
126
+ }
@@ -0,0 +1,58 @@
1
+ import { enrolledRoster } from '../definition';
2
+ import { selectNodes } from '../config';
3
+ import { fleet as definition } from '../fleet-config';
4
+ import { bundleProbe } from '../transport';
5
+ import { machineProviders } from '../t3/providers';
6
+ import { accountContext, nodeAccountChecks } from '../accounts/context';
7
+ import { propagateAccounts } from '../accounts/propagate';
8
+ import { loadRoster } from './roster';
9
+ import { ownerCheckout } from './source-operations';
10
+ import type { Check, FleetNode, Runner } from '../types';
11
+ import type { FleetSyncOptions } from './fleet-sync';
12
+ import type { Settings } from './settings';
13
+ import type { CommandOutcome } from './contracts';
14
+
15
+ /** `fleet-sync --only accounts` on the manager. */
16
+ export async function syncAccounts(
17
+ options: FleetSyncOptions,
18
+ settings: Settings,
19
+ { revision, run }: { revision: string; run: Runner }
20
+ ): Promise<CommandOutcome> {
21
+ let nodes: FleetNode[] = [];
22
+ try {
23
+ nodes = selectNodes(
24
+ enrolledRoster(definition, await loadRoster(settings.config)),
25
+ options.node ?? []
26
+ );
27
+ } catch {
28
+ // No enrolled worker yet: the manager alone.
29
+ }
30
+ return propagateAccounts({
31
+ nodes,
32
+ revision,
33
+ source: await bundleProbe(),
34
+ manager: () => managerAccounts(settings.workspace),
35
+ run,
36
+ });
37
+ }
38
+
39
+ /** The manager's own account instances, from the checkout it runs. */
40
+ export async function managerAccounts(workspace: string): Promise<Check[]> {
41
+ const checkout = ownerCheckout(workspace);
42
+ if (!checkout)
43
+ return [
44
+ {
45
+ id: 't3:providers',
46
+ status: 'fail',
47
+ summary: 'No revision is applied on the manager yet.',
48
+ remedy: {
49
+ action: 'Run a full fleet-sync first.',
50
+ command: 'devfleet fleet-sync',
51
+ },
52
+ },
53
+ ];
54
+ return [
55
+ await machineProviders(definition.manager.id, checkout, true),
56
+ ...(await nodeAccountChecks(await accountContext())),
57
+ ];
58
+ }
@@ -6,14 +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 { convergeGitIdentity, gitChecks } from '../git-identity';
14
17
  import macos from '../../ansible/macos/policy.json';
15
18
  import { ansibleCommand } from '../platform/ansible';
16
19
  import { PACKAGE_ROOT } from '../package-info';
20
+ import { managerHealth } from '../manager-health';
17
21
  import { readFileSync } from 'node:fs';
18
22
  import { join } from 'node:path';
19
23
 
@@ -39,8 +43,16 @@ export async function convergeManager(
39
43
  if (input.apply) installConfig();
40
44
  checks.push(await managerPower(run, input.workspace));
41
45
  checks.push(await managerDocker(run));
42
- checks.push(await machineProviders(id, input.workspace, input.apply));
43
- checks.push(await providerNames(id, input.workspace, input.apply));
46
+ // Each enabled IDE: its settings, pinned version and policy.
47
+ checks.push(
48
+ ...(await managerChecks(enabled(idesOf(definition), IDES), {
49
+ id,
50
+ workspace: input.workspace,
51
+ apply: input.apply,
52
+ run,
53
+ }))
54
+ );
55
+ checks.push(...(await managerHealth(run)));
44
56
  if (input.apply) {
45
57
  // Same provider profiles and the same Vault gateway credential as every
46
58
  // worker: the instances above launch from these profiles, and the check
@@ -50,15 +62,30 @@ export async function convergeManager(
50
62
  managerAgentProfiles(id, input.workspace, run)
51
63
  ))
52
64
  );
65
+ // The same Git identity and GitHub credentials as every worker.
66
+ checks.push(
67
+ ...(await attempt('git:configuration', () =>
68
+ convergeGitIdentity(input.workspace, definition.git, run)
69
+ ))
70
+ );
71
+ if (definition.git)
72
+ checks.push(...(await gitChecks(definition.git, run)));
53
73
  const providers = await attempt('provider:verification', () =>
54
74
  verifyFleetProviders(id)
55
75
  );
56
76
  checks.push(...providers);
57
77
  // Same agents, versions and shared accounts as every worker.
78
+ const accounts = await accountContext();
79
+ checks.push(
80
+ ...(await attempt('accounts:verification', () =>
81
+ nodeAccountChecks(accounts)
82
+ ))
83
+ );
58
84
  checks.push(
59
85
  ...(await attempt('agent:verification', () =>
60
86
  verifyNativeAgents(id, input.workspace, run, {
61
87
  outOfQuota: quotaLimited(providers),
88
+ ...(accounts ? { accounts } : {}),
62
89
  })
63
90
  ))
64
91
  );
@@ -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',