@crvouga/dev-fleet 0.0.0-stage → 0.1.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 (333) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +157 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +128 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +17 -0
  33. package/ansible/monitoring/README.md +154 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +134 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/lock.json +11 -0
  40. package/ansible/net/remote-probe.sh +13 -0
  41. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  42. package/ansible/ubuntu/baseline.yml +89 -0
  43. package/ansible/ubuntu/disk-guard.py +104 -0
  44. package/ansible/ubuntu/policy.json +43 -0
  45. package/ansible/ubuntu/recovery.yml +15 -0
  46. package/ansible/ubuntu/t3/policy.json +7 -0
  47. package/ansible/ubuntu/t3-desktop.yml +220 -0
  48. package/ansible/ubuntu/t3-health.sh +33 -0
  49. package/ansible/ubuntu/t3-policy.yml +76 -0
  50. package/ansible/ubuntu/t3-route.yml +28 -0
  51. package/ansible/ubuntu/tailscale.yml +75 -0
  52. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  53. package/ansible/ubuntu/tasks/docker.yml +31 -0
  54. package/ansible/ubuntu/tasks/packages.yml +56 -0
  55. package/ansible/ubuntu/tasks/power.yml +51 -0
  56. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  57. package/ansible/ubuntu/tasks/resources.yml +252 -0
  58. package/ansible/ubuntu/tasks/services.yml +31 -0
  59. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  60. package/ansible/ubuntu/tasks/user.yml +120 -0
  61. package/bin/devfleet.mjs +36 -0
  62. package/bootstrap/bootstrap.ps1 +50 -0
  63. package/bootstrap/bootstrap.sh +73 -0
  64. package/bootstrap/devfleet +7 -0
  65. package/bootstrap/docker-desktop.sh +33 -0
  66. package/bootstrap/doctor.ts +189 -0
  67. package/bootstrap/fleet-manager-run.sh +76 -0
  68. package/bootstrap/fleet-remote-bootstrap.sh +185 -0
  69. package/bootstrap/global-config.ts +113 -0
  70. package/bootstrap/install-launcher.sh +7 -0
  71. package/bootstrap/native-aws.ts +86 -0
  72. package/bootstrap/native-t3.ts +154 -0
  73. package/bootstrap/npm.ts +350 -0
  74. package/bootstrap/setup-status.ts +51 -0
  75. package/bootstrap/setup.ts +88 -0
  76. package/bootstrap/shell-path.ts +105 -0
  77. package/bootstrap/verify-t3-archive.py +49 -0
  78. package/bootstrap/worker-auth.ts +65 -0
  79. package/bootstrap/worker-github.ts +47 -0
  80. package/bootstrap/worker-tools.ts +24 -0
  81. package/cliproxy/AGENTS.md +11 -0
  82. package/cliproxy/README.md +269 -0
  83. package/cliproxy/clients/bootstrap.py +143 -0
  84. package/cliproxy/clients/launcher.py +57 -0
  85. package/cliproxy/compose.yaml +90 -0
  86. package/cliproxy/config.template.yaml +70 -0
  87. package/cliproxy/docs/cloud-adoption.md +16 -0
  88. package/cliproxy/docs/deployment.png +0 -0
  89. package/cliproxy/docs/deployment.svg +109 -0
  90. package/cliproxy/docs/health-workflow.example.yml +185 -0
  91. package/cliproxy/docs/migration.md +131 -0
  92. package/cliproxy/docs/model-health-report.schema.json +364 -0
  93. package/cliproxy/docs/model-health.md +393 -0
  94. package/cliproxy/image.lock.json +5 -0
  95. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  96. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  97. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  98. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  99. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  101. package/cliproxy/models.json +1618 -0
  102. package/cliproxy/requirements.lock +78 -0
  103. package/cliproxy/scripts/accounts.py +38 -0
  104. package/cliproxy/scripts/audit.py +123 -0
  105. package/cliproxy/scripts/backup +234 -0
  106. package/cliproxy/scripts/check.py +34 -0
  107. package/cliproxy/scripts/checkpoint.py +226 -0
  108. package/cliproxy/scripts/cli.py +64 -0
  109. package/cliproxy/scripts/cliproxy_host.py +257 -0
  110. package/cliproxy/scripts/cloud_state.py +150 -0
  111. package/cliproxy/scripts/container-health +6 -0
  112. package/cliproxy/scripts/docker +24 -0
  113. package/cliproxy/scripts/external-health.py +79 -0
  114. package/cliproxy/scripts/fleet_config.py +43 -0
  115. package/cliproxy/scripts/fleet_sync.py +274 -0
  116. package/cliproxy/scripts/initialize +84 -0
  117. package/cliproxy/scripts/install-dependencies +48 -0
  118. package/cliproxy/scripts/install.py +406 -0
  119. package/cliproxy/scripts/model_health.py +16 -0
  120. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  121. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  122. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  123. package/cliproxy/scripts/modelhealth/config.py +186 -0
  124. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  125. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  126. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  127. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  128. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  129. package/cliproxy/scripts/modelhealth/report.py +150 -0
  130. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  131. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  132. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  133. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  134. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  135. package/cliproxy/scripts/monitor +68 -0
  136. package/cliproxy/scripts/ops +527 -0
  137. package/cliproxy/scripts/public-endpoint +127 -0
  138. package/cliproxy/scripts/recover_node.py +177 -0
  139. package/cliproxy/scripts/restore-verify +60 -0
  140. package/cliproxy/scripts/retention.py +144 -0
  141. package/cliproxy/scripts/service-start +7 -0
  142. package/cliproxy/scripts/validate-config +56 -0
  143. package/cliproxy/scripts/vault-access.py +57 -0
  144. package/cliproxy/scripts/vault-backup-login +55 -0
  145. package/cliproxy/scripts/vault-recovery +56 -0
  146. package/cliproxy/scripts/vault-renew +30 -0
  147. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  148. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  149. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  150. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  151. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  152. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  153. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  155. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  156. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  157. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  158. package/cliproxy/vendor/LICENSE +22 -0
  159. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  160. package/examples/fleet.json +95 -0
  161. package/examples/roster.json +13 -0
  162. package/mise.lock +1492 -0
  163. package/mise.toml +99 -0
  164. package/package.json +65 -4
  165. package/schema/fleet.schema.json +364 -0
  166. package/schema/roster.schema.json +77 -0
  167. package/src/agents/account-relay.ts +111 -0
  168. package/src/agents/accounts.ts +85 -0
  169. package/src/agents/audit.ts +77 -0
  170. package/src/agents/cli.ts +13 -0
  171. package/src/agents/command.ts +204 -0
  172. package/src/agents/converge.ts +123 -0
  173. package/src/agents/doctor.ts +107 -0
  174. package/src/agents/errors.ts +2 -0
  175. package/src/agents/files.ts +73 -0
  176. package/src/agents/fleet-verify.ts +156 -0
  177. package/src/agents/launch.ts +290 -0
  178. package/src/agents/native-verify.ts +226 -0
  179. package/src/agents/provider-errors.ts +50 -0
  180. package/src/agents/registry.ts +55 -0
  181. package/src/agents/render.ts +110 -0
  182. package/src/agents/round-trip.ts +192 -0
  183. package/src/agents/secrets.ts +13 -0
  184. package/src/agents/toolchain.ts +34 -0
  185. package/src/agents/verify-cache.ts +103 -0
  186. package/src/agents/verify.ts +127 -0
  187. package/src/auth-checks.ts +163 -0
  188. package/src/cli/agent-guide.ts +95 -0
  189. package/src/cli/app.ts +115 -0
  190. package/src/cli/cli.ts +21 -0
  191. package/src/cli/command-index.ts +50 -0
  192. package/src/cli/contracts.ts +25 -0
  193. package/src/cli/fleet-reference.ts +17 -0
  194. package/src/cli/fleet-sync-checkout.ts +91 -0
  195. package/src/cli/fleet-sync-enrollment.ts +144 -0
  196. package/src/cli/fleet-sync-forward.ts +134 -0
  197. package/src/cli/fleet-sync-manager.ts +193 -0
  198. package/src/cli/fleet-sync-monitoring.ts +185 -0
  199. package/src/cli/fleet-sync-output.ts +314 -0
  200. package/src/cli/fleet-sync-privilege.ts +27 -0
  201. package/src/cli/fleet-sync-progress.ts +109 -0
  202. package/src/cli/fleet-sync-remote.ts +236 -0
  203. package/src/cli/fleet-sync-report.ts +26 -0
  204. package/src/cli/fleet-sync-schedule.ts +138 -0
  205. package/src/cli/fleet-sync-vault.ts +166 -0
  206. package/src/cli/fleet-sync.ts +309 -0
  207. package/src/cli/fleet.ts +163 -0
  208. package/src/cli/managed-health.ts +219 -0
  209. package/src/cli/node-deployment.ts +34 -0
  210. package/src/cli/node-sync.ts +114 -0
  211. package/src/cli/node-t3.ts +31 -0
  212. package/src/cli/options.ts +141 -0
  213. package/src/cli/probe-source.ts +7 -0
  214. package/src/cli/program.ts +169 -0
  215. package/src/cli/register-fleet.ts +246 -0
  216. package/src/cli/registry.ts +158 -0
  217. package/src/cli/repair-apply.ts +67 -0
  218. package/src/cli/repair-guard.ts +11 -0
  219. package/src/cli/repair-inspect.ts +150 -0
  220. package/src/cli/repair-script.ts +76 -0
  221. package/src/cli/repair.ts +209 -0
  222. package/src/cli/roster.ts +89 -0
  223. package/src/cli/search.ts +35 -0
  224. package/src/cli/settings.ts +64 -0
  225. package/src/cli/source-operations.ts +78 -0
  226. package/src/cli/t3/command.ts +90 -0
  227. package/src/cli/t3/register.ts +55 -0
  228. package/src/cli/tailnet.ts +127 -0
  229. package/src/cli/ui-actions.ts +133 -0
  230. package/src/cli/ui-controller.ts +113 -0
  231. package/src/cli/ui-data.ts +106 -0
  232. package/src/cli/ui-execute.ts +163 -0
  233. package/src/cli/ui-repairs.ts +34 -0
  234. package/src/cli/ui-reports.ts +211 -0
  235. package/src/cli/ui-targets.ts +100 -0
  236. package/src/cli/ui-terminal.ts +109 -0
  237. package/src/cli/ui-types.ts +49 -0
  238. package/src/cli/ui-wizard.ts +101 -0
  239. package/src/cli/ui.ts +9 -0
  240. package/src/cli/worker-health-output.ts +37 -0
  241. package/src/config.ts +186 -0
  242. package/src/configure.ts +255 -0
  243. package/src/definition.ts +260 -0
  244. package/src/diagnostics.ts +92 -0
  245. package/src/doctor-check.ts +10 -0
  246. package/src/errors.ts +1 -0
  247. package/src/fingerprint.ts +120 -0
  248. package/src/fleet-config.ts +88 -0
  249. package/src/index.ts +6 -0
  250. package/src/input-hashes.ts +73 -0
  251. package/src/machine/local-source.ts +52 -0
  252. package/src/machine/lock-error.ts +2 -0
  253. package/src/machine/platform.ts +44 -0
  254. package/src/machine/source.ts +68 -0
  255. package/src/machine/state.ts +185 -0
  256. package/src/machine-health.ts +95 -0
  257. package/src/managed-source.ts +47 -0
  258. package/src/manager-failure.ts +48 -0
  259. package/src/manager-sync.ts +400 -0
  260. package/src/manager.ts +62 -0
  261. package/src/monitoring/apply.ts +171 -0
  262. package/src/monitoring/beszel.ts +191 -0
  263. package/src/monitoring/enroll.ts +110 -0
  264. package/src/monitoring/hub-access.ts +113 -0
  265. package/src/monitoring/hub-viewer.ts +142 -0
  266. package/src/monitoring/hub.ts +93 -0
  267. package/src/monitoring/manager-agent.ts +73 -0
  268. package/src/monitoring/manager-hub.ts +232 -0
  269. package/src/monitoring/remote.ts +65 -0
  270. package/src/network-definition.ts +83 -0
  271. package/src/node-converge-linux.ts +372 -0
  272. package/src/node-converge-shared.ts +83 -0
  273. package/src/node-converge.ts +40 -0
  274. package/src/onboard.ts +35 -0
  275. package/src/platform/ansible-receipt.ts +128 -0
  276. package/src/platform/ansible.ts +62 -0
  277. package/src/platform/darwin.ts +203 -0
  278. package/src/platform/detect.ts +11 -0
  279. package/src/platform/fallback.ts +110 -0
  280. package/src/platform/index.ts +38 -0
  281. package/src/platform/launchd.ts +98 -0
  282. package/src/platform/linux.ts +139 -0
  283. package/src/platform/ps.ts +45 -0
  284. package/src/platform/resource-verify.ts +128 -0
  285. package/src/platform/systemd-user.ts +95 -0
  286. package/src/platform/t3-policy.ts +137 -0
  287. package/src/platform/t3-route.ts +68 -0
  288. package/src/platform/t3-service.ts +75 -0
  289. package/src/platform/types.ts +123 -0
  290. package/src/platform/ubuntu-audit.ts +63 -0
  291. package/src/platform/ubuntu-verify.ts +105 -0
  292. package/src/platform/windows.ts +148 -0
  293. package/src/power-recovery.ts +55 -0
  294. package/src/probe.ts +51 -0
  295. package/src/protocol.ts +119 -0
  296. package/src/proxy-converge.ts +85 -0
  297. package/src/remote-bootstrap.ts +212 -0
  298. package/src/remote-entry.ts +89 -0
  299. package/src/remote-lock.ts +12 -0
  300. package/src/repair-catalog.ts +206 -0
  301. package/src/repair-plan.ts +78 -0
  302. package/src/repair-runner.ts +84 -0
  303. package/src/report.ts +71 -0
  304. package/src/runner.ts +104 -0
  305. package/src/secrets.ts +169 -0
  306. package/src/service-auth.ts +115 -0
  307. package/src/ssh.ts +135 -0
  308. package/src/sync.ts +104 -0
  309. package/src/t3/accounts.ts +81 -0
  310. package/src/t3/desired.ts +165 -0
  311. package/src/t3/files.ts +81 -0
  312. package/src/t3/names-check.ts +53 -0
  313. package/src/t3/names.ts +155 -0
  314. package/src/t3/plan.ts +82 -0
  315. package/src/t3/providers.ts +74 -0
  316. package/src/t3/reconcile.ts +123 -0
  317. package/src/t3-endpoint.ts +48 -0
  318. package/src/t3-linked-check.ts +101 -0
  319. package/src/tailnet-health.ts +231 -0
  320. package/src/tailnet-matching.ts +37 -0
  321. package/src/tailscale.ts +141 -0
  322. package/src/test-fixtures.ts +36 -0
  323. package/src/toolchain-checks.ts +83 -0
  324. package/src/topology.ts +24 -0
  325. package/src/transport.ts +122 -0
  326. package/src/types.ts +95 -0
  327. package/src/vault-config.ts +38 -0
  328. package/src/worker-access-checks.ts +115 -0
  329. package/src/worker-capacity.ts +34 -0
  330. package/src/worker-host-checks.ts +176 -0
  331. package/src/worker-policy.ts +64 -0
  332. package/src/worker-readiness.ts +64 -0
  333. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,81 @@
1
+ import {
2
+ accountSlug as slug,
3
+ discoverAccounts,
4
+ type Account,
5
+ } from '../agents/accounts';
6
+ import { proxyKey } from '../agents/secrets';
7
+
8
+ export type Roster = () => Promise<Account[]>;
9
+
10
+ /** The accounts signed in on the CLI proxy, read with this node's own key.
11
+ * Throws when the proxy does not answer, so providers are never dropped
12
+ * because the roster was unreadable.
13
+ */
14
+ export const proxyAccounts: Roster = async () =>
15
+ discoverAccounts(await proxyKey());
16
+
17
+ /** How T3 runs each tool for one account. The launcher pins every request to
18
+ * the account named by WS_AGENT_ACCOUNT; no credential is ever placed here.
19
+ */
20
+ const TOOLS: Record<
21
+ string,
22
+ { driver: string; label: string; config: Record<string, string> }
23
+ > = {
24
+ claude: {
25
+ driver: 'claudeAgent',
26
+ label: 'Claude',
27
+ config: {
28
+ binaryPath: '${workspace}/agents/claude-proxy',
29
+ homePath: '${agents}/${node}/claude/account-${account}',
30
+ },
31
+ },
32
+ codex: {
33
+ driver: 'codex',
34
+ label: 'Codex',
35
+ config: {
36
+ binaryPath: '${workspace}/agents/codex-proxy.ts',
37
+ homePath: '${agents}/${node}/codex/account-${account}',
38
+ },
39
+ },
40
+ opencode: {
41
+ driver: 'opencode',
42
+ label: 'AntiGravity',
43
+ config: {
44
+ binaryPath: '${workspace}/agents/opencode-proxy.ts',
45
+ },
46
+ },
47
+ };
48
+
49
+ /** One `<Provider> <account email>` instance per signed-in account, identical
50
+ * on every machine because every machine reads the same proxy.
51
+ */
52
+ export function accountInstances(
53
+ accounts: Account[],
54
+ variables: Record<string, string>
55
+ ): Record<string, unknown> {
56
+ const instances: Record<string, unknown> = {};
57
+ for (const account of accounts) {
58
+ const tool = TOOLS[account.tool];
59
+ if (!tool) continue;
60
+ const values: Record<string, string> = {
61
+ ...variables,
62
+ account: slug(account.email),
63
+ };
64
+ instances[`personal_${slug(account.prefix)}`.slice(0, 64)] = {
65
+ driver: tool.driver,
66
+ displayName: `${tool.label} ${account.email}`,
67
+ enabled: true,
68
+ config: Object.fromEntries(
69
+ Object.entries(tool.config).map(([key, value]) => [
70
+ key,
71
+ value.replace(/\$\{(\w+)\}/g, (_, name: string) => values[name]!),
72
+ ])
73
+ ),
74
+ environment: [
75
+ { name: 'WS_AGENT_NODE', value: variables['node'], sensitive: false },
76
+ { name: 'WS_AGENT_ACCOUNT', value: account.email, sensitive: false },
77
+ ],
78
+ };
79
+ }
80
+ return instances;
81
+ }
@@ -0,0 +1,165 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { homedir, hostname } from 'node:os';
3
+ import { FleetUsageError } from '../errors';
4
+ import { object, read } from './files';
5
+ import { proxyName } from './names';
6
+ import { accountInstances, proxyAccounts, type Roster } from './accounts';
7
+
8
+ export interface T3Options {
9
+ id?: string;
10
+ t3Home?: string;
11
+ definition?: string;
12
+ planHash?: string;
13
+ dryRun?: boolean;
14
+ import?: string;
15
+ /** Internal: the proxy account roster (read live when omitted). */
16
+ roster?: Roster;
17
+ }
18
+
19
+ export function nodeId(value?: string): string {
20
+ const id =
21
+ value ??
22
+ hostname()
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9-]/g, '-')
25
+ .slice(0, 63);
26
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(id))
27
+ throw new FleetUsageError('Invalid node ID. Use --id NAME.');
28
+ return id;
29
+ }
30
+
31
+ export function paths(options: T3Options, workspace: string) {
32
+ const base = resolve(
33
+ options.t3Home ?? process.env['T3CODE_HOME'] ?? join(homedir(), '.t3')
34
+ );
35
+ return {
36
+ settings: join(base, 'userdata/settings.json'),
37
+ state: join(base, 'userdata/personal-managed.json'),
38
+ definition: resolve(
39
+ options.definition ?? join(workspace, 'agents/t3.json')
40
+ ),
41
+ };
42
+ }
43
+
44
+ /** The managed instances: the fixed ones in the definition plus one per
45
+ * provider account signed in on the CLI proxy (`roster`, read live).
46
+ */
47
+ export async function desired(
48
+ path: string,
49
+ workspace: string,
50
+ id: string,
51
+ roster: Roster = proxyAccounts
52
+ ) {
53
+ const raw = await read(path);
54
+ if (raw === null)
55
+ throw new FleetUsageError('T3 definition is missing in this checkout.');
56
+ const doc = object(raw);
57
+ if (
58
+ doc['version'] !== 1 ||
59
+ Object.keys(doc).some((key) => !['version', 'instances'].includes(key))
60
+ )
61
+ throw new FleetUsageError(
62
+ 'Unsupported T3 definition. Expected version 1 and instances.'
63
+ );
64
+ const instances = object(JSON.stringify(doc['instances'] ?? null));
65
+ for (const [key, value] of Object.entries(instances))
66
+ validateInstance(key, value);
67
+ const agents =
68
+ process.platform === 'win32'
69
+ ? join(homedir(), 'AppData/Local/personal/agents')
70
+ : join(homedir(), '.config/personal/agents');
71
+ const variables: Record<string, string> = {
72
+ workspace: resolve(workspace),
73
+ agents,
74
+ node: id,
75
+ };
76
+ // Substitute individual strings, not serialized JSON: paths may contain quotes/backslashes.
77
+ return {
78
+ ...(expand(instances, variables) as Record<string, unknown>),
79
+ ...accountInstances(await roster(), variables),
80
+ };
81
+ }
82
+
83
+ function expand(value: unknown, variables: Record<string, string>): unknown {
84
+ if (typeof value === 'string')
85
+ return value.replace(/\$\{([^}]+)\}/g, (_, key: string) => {
86
+ if (!(key in variables))
87
+ throw new FleetUsageError('Unknown variable in T3 definition.');
88
+ return variables[key]!;
89
+ });
90
+ if (Array.isArray(value)) return value.map((item) => expand(item, variables));
91
+ if (value && typeof value === 'object')
92
+ return Object.fromEntries(
93
+ Object.entries(value).map(([key, item]) => [key, expand(item, variables)])
94
+ );
95
+ return value;
96
+ }
97
+
98
+ /** Each fixed driver accepts only the adapter fields fleet sets, under its
99
+ * conventional `<Provider> CLI Proxy` name. Credentials never appear here (T3
100
+ * refuses a managed policy that holds one): wrappers read them from Vault at
101
+ * launch.
102
+ */
103
+ const DRIVERS: Record<
104
+ string,
105
+ { required: string[]; env?: Record<string, string> }
106
+ > = {
107
+ opencode: { required: ['binaryPath'] },
108
+ grok: { required: ['binaryPath'], env: { XAI_API_KEY: 'fleet-cli-proxy' } },
109
+ };
110
+
111
+ function validateInstance(id: string, value: unknown): void {
112
+ const instance = object(JSON.stringify(value));
113
+ const config = object(JSON.stringify(instance['config']));
114
+ const driver = DRIVERS[String(instance['driver'])];
115
+ if (
116
+ !/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id) ||
117
+ !driver ||
118
+ instance['displayName'] !== proxyName(String(instance['driver'])) ||
119
+ typeof instance['enabled'] !== 'boolean' ||
120
+ Object.keys(instance).some(
121
+ (key) =>
122
+ !['driver', 'displayName', 'enabled', 'config', 'environment'].includes(
123
+ key
124
+ )
125
+ ) ||
126
+ !validConfig(config, driver) ||
127
+ !validEnvironment(instance['environment'], driver.env ?? {})
128
+ )
129
+ throw new FleetUsageError(
130
+ 'Invalid T3 instance definition; only the supported managed adapter fields are accepted.'
131
+ );
132
+ }
133
+
134
+ function validConfig(
135
+ config: Record<string, unknown>,
136
+ driver: { required: string[] }
137
+ ): boolean {
138
+ return (
139
+ Object.keys(config).every((key) => driver.required.includes(key)) &&
140
+ driver.required.every((key) => typeof config[key] === 'string')
141
+ );
142
+ }
143
+
144
+ /** WS_AGENT_NODE first, then exactly the driver's fixed non-secret values. */
145
+ function validEnvironment(
146
+ env: unknown,
147
+ fixed: Record<string, string>
148
+ ): boolean {
149
+ const extra = Object.entries(fixed);
150
+ if (!Array.isArray(env) || env.length !== 1 + extra.length) return false;
151
+ const [node, ...rest] = env.map((item) => object(JSON.stringify(item)));
152
+ return (
153
+ node!['name'] === 'WS_AGENT_NODE' &&
154
+ node!['value'] === '${node}' &&
155
+ node!['sensitive'] === false &&
156
+ Object.keys(node!).length === 3 &&
157
+ rest.every(
158
+ (variable, index) =>
159
+ variable['name'] === extra[index]![0] &&
160
+ variable['value'] === extra[index]![1] &&
161
+ variable['sensitive'] === false &&
162
+ Object.keys(variable).length === 3
163
+ )
164
+ );
165
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ lstat,
3
+ mkdir,
4
+ readFile,
5
+ rename,
6
+ rm,
7
+ writeFile,
8
+ } from 'node:fs/promises';
9
+ import { dirname, parse, resolve } from 'node:path';
10
+ import { randomUUID, createHash } from 'node:crypto';
11
+ import { FleetUsageError } from '../errors';
12
+
13
+ export const hash = (value: string): string =>
14
+ createHash('sha256').update(value).digest('hex');
15
+
16
+ export async function safePath(path: string): Promise<void> {
17
+ for (
18
+ let current = resolve(path);
19
+ current !== parse(current).root;
20
+ current = dirname(current)
21
+ ) {
22
+ try {
23
+ if ((await lstat(current)).isSymbolicLink())
24
+ throw new FleetUsageError('Managed T3 paths cannot be symlinks.');
25
+ } catch (error) {
26
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
27
+ }
28
+ }
29
+ }
30
+
31
+ export async function read(path: string): Promise<string | null> {
32
+ await safePath(path);
33
+ try {
34
+ const info = await lstat(path);
35
+ if (!info.isFile() || info.size > 4 * 1024 * 1024)
36
+ throw new FleetUsageError(
37
+ 'T3 configuration must be a regular file smaller than 4 MiB.'
38
+ );
39
+ return await readFile(path, 'utf8');
40
+ } catch (error) {
41
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ export async function write(path: string, contents: string): Promise<void> {
47
+ await safePath(path);
48
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
49
+ const temporary = `${path}.${randomUUID()}.tmp`;
50
+ try {
51
+ await writeFile(temporary, contents, { mode: 0o600, flag: 'wx' });
52
+ await rename(temporary, path);
53
+ } finally {
54
+ await rm(temporary, { force: true });
55
+ }
56
+ }
57
+
58
+ export function object(raw: string | null): Record<string, unknown> {
59
+ if (raw === null) return {};
60
+ try {
61
+ const value: unknown = JSON.parse(raw);
62
+ if (value && typeof value === 'object' && !Array.isArray(value))
63
+ return value as Record<string, unknown>;
64
+ } catch {
65
+ /* No contents or parser details in errors. */
66
+ }
67
+ throw new FleetUsageError(
68
+ 'Invalid T3 JSON object. Repair the file before reconciliation.'
69
+ );
70
+ }
71
+
72
+ export function canonical(value: unknown): string {
73
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
74
+ if (value && typeof value === 'object') {
75
+ return `{${Object.entries(value)
76
+ .sort(([a], [b]) => a.localeCompare(b))
77
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`)
78
+ .join(',')}}`;
79
+ }
80
+ return JSON.stringify(value) ?? 'null';
81
+ }
@@ -0,0 +1,53 @@
1
+ import { desired, nodeId, paths } from './desired';
2
+ import { conformT3Names } from './names';
3
+ import type { Roster } from './accounts';
4
+ import type { Check } from '../types';
5
+
6
+ /** Fleet check: this machine's T3 lists every provider under the convention. */
7
+ export async function providerNames(
8
+ id: string,
9
+ workspace: string,
10
+ apply: boolean,
11
+ { t3Home, roster }: { t3Home?: string; roster?: Roster } = {}
12
+ ): Promise<Check> {
13
+ try {
14
+ const target = paths(t3Home ? { t3Home } : {}, workspace);
15
+ const managed = Object.fromEntries(
16
+ Object.entries(
17
+ await desired(target.definition, workspace, nodeId(id), roster)
18
+ ).map(([instance, value]) => [
19
+ instance,
20
+ String((value as { displayName?: unknown }).displayName),
21
+ ])
22
+ );
23
+ const { changed } = await conformT3Names(target.settings, managed, {
24
+ apply,
25
+ });
26
+ return changed && !apply
27
+ ? {
28
+ id: 't3:provider-names',
29
+ status: 'warn',
30
+ summary: 'T3_PROVIDER_NAMES_PENDING',
31
+ remedy: {
32
+ action:
33
+ 'fleet-sync renames these providers to the convention when applied.',
34
+ },
35
+ }
36
+ : {
37
+ id: 't3:provider-names',
38
+ status: 'pass',
39
+ summary:
40
+ 'Every T3 provider is named "<Provider> CLI Proxy" or "<Provider> <account email>".',
41
+ };
42
+ } catch {
43
+ return {
44
+ id: 't3:provider-names',
45
+ status: 'fail',
46
+ summary: 'T3_PROVIDER_NAMES_UNVERIFIED',
47
+ remedy: {
48
+ action:
49
+ 'T3 settings were unreadable or changed during the rename. Rerun fleet-sync; only provider display names are written.',
50
+ },
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,155 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { object, read, write } from './files';
5
+
6
+ /** One naming convention for every provider T3 lists, on every machine:
7
+ * `<Provider> CLI Proxy` for a fleet-managed gateway instance and
8
+ * `<Provider> <account email>` for a native login.
9
+ */
10
+ export const PROVIDER_NAMES: Record<string, string> = {
11
+ claudeAgent: 'Claude',
12
+ codex: 'Codex',
13
+ antigravity: 'AntiGravity',
14
+ grok: 'Grok',
15
+ opencode: 'OpenCode',
16
+ cursor: 'Cursor',
17
+ };
18
+
19
+ export const proxyName = (driver: string): string | undefined =>
20
+ PROVIDER_NAMES[driver] && `${PROVIDER_NAMES[driver]} CLI Proxy`;
21
+
22
+ /** The account email is the only identity shown; without one the provider
23
+ * name stands alone rather than a guess.
24
+ */
25
+ export const accountName = (
26
+ driver: string,
27
+ email?: string
28
+ ): string | undefined =>
29
+ PROVIDER_NAMES[driver] &&
30
+ (email ? `${PROVIDER_NAMES[driver]} ${email}` : PROVIDER_NAMES[driver]);
31
+
32
+ export type Accounts = (
33
+ driver: string,
34
+ homePath?: string
35
+ ) => Promise<string | undefined>;
36
+
37
+ const EMAIL = /^[^\s@]{1,64}@[^\s@]{1,255}$/;
38
+ const email = (value: unknown): string | undefined =>
39
+ typeof value === 'string' && EMAIL.test(value) ? value : undefined;
40
+
41
+ async function json(path: string): Promise<Record<string, unknown>> {
42
+ return object(await readFile(path, 'utf8'));
43
+ }
44
+
45
+ /** Reads only the signed-in address from each client's own login record; a
46
+ * token is never returned, logged or sent anywhere.
47
+ */
48
+ export const localAccounts: Accounts = async (driver, homePath) => {
49
+ try {
50
+ if (driver === 'claudeAgent') {
51
+ const account = (await json(join(homePath ?? homedir(), '.claude.json')))[
52
+ 'oauthAccount'
53
+ ];
54
+ return email((account as Record<string, unknown>)?.['emailAddress']);
55
+ }
56
+ if (driver === 'codex') {
57
+ const home =
58
+ homePath ?? process.env['CODEX_HOME'] ?? join(homedir(), '.codex');
59
+ const tokens = (await json(join(home, 'auth.json')))['tokens'];
60
+ const claims = String(
61
+ (tokens as Record<string, unknown>)?.['id_token']
62
+ ).split('.')[1];
63
+ return email(
64
+ object(Buffer.from(claims ?? '', 'base64url').toString('utf8'))['email']
65
+ );
66
+ }
67
+ } catch {
68
+ /* Signed out or unreadable: the provider name stands alone. */
69
+ }
70
+ return undefined;
71
+ };
72
+
73
+ type Instance = Record<string, unknown>;
74
+ const homeOf = (instance: Instance): string | undefined => {
75
+ const home = (instance['config'] as Record<string, unknown> | undefined)?.[
76
+ 'homePath'
77
+ ];
78
+ return typeof home === 'string' ? home : undefined;
79
+ };
80
+
81
+ /** Every instance T3 lists gets its conventional name. Managed instances
82
+ * (`managed`: ID to name) are named by their definition; every other instance is renamed after its own
83
+ * signed-in account, and a signed-in default login T3 lists without a settings
84
+ * entry gains one. Only `displayName` is ever written.
85
+ */
86
+ export async function conformNames(
87
+ current: Record<string, unknown>,
88
+ managed: Record<string, string>,
89
+ accounts: Accounts = localAccounts
90
+ ): Promise<Record<string, unknown>> {
91
+ const next: Record<string, unknown> = { ...current };
92
+ const named = new Set<string>();
93
+ for (const [id, value] of Object.entries(current)) {
94
+ // A worker's policy owns its managed instances; a copy left in its
95
+ // settings still shows until the policy loads, so it conforms too.
96
+ const renamed = Object.hasOwn(managed, id)
97
+ ? rename(value, () => managed[id])
98
+ : rename(value, accountName, await account(value, accounts));
99
+ if (!renamed) continue;
100
+ named.add(String(renamed['displayName']));
101
+ next[id] = renamed;
102
+ }
103
+ // T3 lists a driver's default instance (its ID is the driver) without an entry.
104
+ for (const driver of Object.keys(PROVIDER_NAMES)) {
105
+ const account = await accounts(driver);
106
+ const name = account && accountName(driver, account);
107
+ if (!name || named.has(name) || Object.hasOwn(current, driver)) continue;
108
+ next[driver] = { driver, displayName: name };
109
+ }
110
+ return next;
111
+ }
112
+
113
+ const account = (value: unknown, accounts: Accounts) =>
114
+ value && typeof value === 'object'
115
+ ? accounts(String((value as Instance)['driver']), homeOf(value as Instance))
116
+ : undefined;
117
+
118
+ /** The instance under its conventional name; undefined for an unknown driver. */
119
+ function rename(
120
+ value: unknown,
121
+ convention: (driver: string, email?: string) => string | undefined,
122
+ email?: string
123
+ ): Instance | undefined {
124
+ if (!value || typeof value !== 'object') return undefined;
125
+ const instance = value as Instance;
126
+ const name = convention(String(instance['driver']), email);
127
+ if (!name) return undefined;
128
+ return instance['displayName'] === name
129
+ ? instance
130
+ : { ...instance, displayName: name };
131
+ }
132
+
133
+ /** Applies the convention to this machine's T3 settings. T3 watches the file,
134
+ * so names change live; nothing is restarted. Returns whether it changed.
135
+ */
136
+ export async function conformT3Names(
137
+ settingsPath: string,
138
+ managed: Record<string, string>,
139
+ options: { apply: boolean; accounts?: Accounts }
140
+ ): Promise<{ changed: boolean }> {
141
+ const raw = await read(settingsPath);
142
+ const settings = object(raw);
143
+ const current = object(JSON.stringify(settings['providerInstances'] ?? {}));
144
+ const next = await conformNames(current, managed, options.accounts);
145
+ if (JSON.stringify(next) === JSON.stringify(current))
146
+ return { changed: false };
147
+ if (!options.apply) return { changed: true };
148
+ // T3 also writes this file: never overwrite a change made since the read.
149
+ if ((await read(settingsPath)) !== raw) throw new Error('T3_SETTINGS_BUSY');
150
+ await write(
151
+ settingsPath,
152
+ JSON.stringify({ ...settings, providerInstances: next }, null, 2) + '\n'
153
+ );
154
+ return { changed: true };
155
+ }
package/src/t3/plan.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { FleetUsageError } from '../errors';
2
+ import { canonical, hash, object, read } from './files';
3
+
4
+ export type Change = {
5
+ id: string;
6
+ action: 'create' | 'update' | 'delete' | 'unchanged' | 'conflict';
7
+ };
8
+ export type T3Paths = { settings: string; state: string };
9
+
10
+ export async function planT3(paths: T3Paths, desired: Record<string, unknown>) {
11
+ const raw = await read(paths.settings);
12
+ const stateRaw = await read(paths.state);
13
+ const settings = object(raw);
14
+ const current = object(JSON.stringify(settings['providerInstances'] ?? {}));
15
+ const owned = readOwnership(stateRaw, paths.settings);
16
+ const next = { ...current };
17
+ const changes: Change[] = [];
18
+ for (const [id, instance] of Object.entries(desired)) {
19
+ const exists = Object.hasOwn(current, id);
20
+ const action = actionFor(exists, owned.includes(id), current[id], instance);
21
+ changes.push({ id, action });
22
+ if (action !== 'conflict') next[id] = instance;
23
+ }
24
+ for (const id of owned.filter((id) => !Object.hasOwn(desired, id))) {
25
+ changes.push({
26
+ id,
27
+ action: Object.hasOwn(current, id) ? 'delete' : 'unchanged',
28
+ });
29
+ delete next[id];
30
+ }
31
+ const settingsChanged = canonical(next) !== canonical(current);
32
+ const state = ownership(paths.settings, Object.keys(desired));
33
+ return {
34
+ raw,
35
+ stateRaw,
36
+ settings: settingsChanged
37
+ ? { ...settings, providerInstances: next }
38
+ : settings,
39
+ state,
40
+ settingsChanged,
41
+ stateChanged: canonical(object(stateRaw)) !== canonical(state),
42
+ changes,
43
+ planHash: hash(
44
+ canonical({ target: paths.settings, raw, stateRaw, desired })
45
+ ),
46
+ };
47
+ }
48
+
49
+ export function ownership(target: string, ids: string[]) {
50
+ return { version: 1, target, instances: [...ids].sort() };
51
+ }
52
+
53
+ export function readOwnership(raw: string | null, target: string): string[] {
54
+ if (raw === null) return [];
55
+ const state = object(raw);
56
+ const ids = state['instances'];
57
+ if (
58
+ state['version'] !== 1 ||
59
+ state['target'] !== target ||
60
+ !Array.isArray(ids) ||
61
+ ids.some(
62
+ (id) =>
63
+ typeof id !== 'string' || !/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id)
64
+ ) ||
65
+ new Set(ids).size !== ids.length
66
+ )
67
+ throw new FleetUsageError(
68
+ 'Invalid T3 ownership state; restore its backup before reconciliation.'
69
+ );
70
+ return ids as string[];
71
+ }
72
+
73
+ function actionFor(
74
+ exists: boolean,
75
+ owned: boolean,
76
+ current: unknown,
77
+ desired: unknown
78
+ ): Change['action'] {
79
+ if (exists && !owned) return 'conflict';
80
+ if (!exists) return 'create';
81
+ return canonical(current) === canonical(desired) ? 'unchanged' : 'update';
82
+ }
@@ -0,0 +1,74 @@
1
+ import type { Check } from '../types';
2
+ import { desired, nodeId, paths } from './desired';
3
+ import type { Roster } from './accounts';
4
+ import { reconcileT3 } from './reconcile';
5
+ import { stablePath, stableWorkspace } from '../platform/t3-policy';
6
+
7
+ /** Every machine's T3 settings hold exactly the managed provider instances:
8
+ * the fixed ones plus one per account signed in on the CLI proxy. T3 watches
9
+ * its settings, so providers appear (and retired ones disappear) at once,
10
+ * without a restart. Workers additionally lock the same instances through T3
11
+ * administrator policy. The ownership-safe t3-apply engine preserves unrelated
12
+ * settings and unmanaged instances; an existing unowned instance of the same
13
+ * ID is reported, never overwritten.
14
+ */
15
+ export async function machineProviders(
16
+ id: string,
17
+ workspace: string,
18
+ apply: boolean,
19
+ options: { t3Home?: string; roster?: Roster } = {}
20
+ ): Promise<Check> {
21
+ const remedy = {
22
+ action:
23
+ 'Run devfleet t3-apply --dry-run on this machine and review any conflicting instance; adopt it with t3-apply --import ID only if it should be managed.',
24
+ };
25
+ try {
26
+ const target = paths(options, workspace);
27
+ // Providers launch from the stable path of the applied run checkout, not
28
+ // from this revision's own directory, which a later sync prunes.
29
+ const home = apply
30
+ ? await stableWorkspace(workspace)
31
+ : stablePath(workspace);
32
+ const instances = await desired(
33
+ target.definition,
34
+ home,
35
+ nodeId(id),
36
+ options.roster
37
+ );
38
+ const plan = await reconcileT3(target, instances, { apply: false });
39
+ const result =
40
+ apply && plan.ok && plan.changed
41
+ ? await reconcileT3(target, instances, {
42
+ apply: true,
43
+ planHash: plan.planHash,
44
+ })
45
+ : plan;
46
+ if (!result.ok)
47
+ return {
48
+ id: 't3:providers',
49
+ status: 'fail',
50
+ summary: 'T3_PROVIDER_CONFLICT',
51
+ remedy,
52
+ };
53
+ return result.changed && !apply
54
+ ? {
55
+ id: 't3:providers',
56
+ status: 'warn',
57
+ summary: 'T3_PROVIDERS_PENDING',
58
+ remedy,
59
+ }
60
+ : {
61
+ id: 't3:providers',
62
+ status: 'pass',
63
+ summary:
64
+ 'T3 on this machine offers exactly the managed provider instances every machine gets.',
65
+ };
66
+ } catch {
67
+ return {
68
+ id: 't3:providers',
69
+ status: 'fail',
70
+ summary: 'T3_PROVIDERS_UNVERIFIED',
71
+ remedy,
72
+ };
73
+ }
74
+ }