@crvouga/dev-fleet 0.0.0-stage → 0.2.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 (363) 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 +357 -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 +158 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +9 -0
  33. package/ansible/monitoring/README.md +215 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +172 -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/hub-route-darwin.yml +51 -0
  40. package/ansible/monitoring/lock.json +16 -0
  41. package/ansible/net/remote-probe.sh +13 -0
  42. package/ansible/pyproject.toml +12 -0
  43. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  44. package/ansible/ubuntu/baseline.yml +89 -0
  45. package/ansible/ubuntu/disk-gc.py +336 -0
  46. package/ansible/ubuntu/disk-guard.py +104 -0
  47. package/ansible/ubuntu/policy.json +52 -0
  48. package/ansible/ubuntu/recovery.yml +15 -0
  49. package/ansible/ubuntu/t3/policy.json +7 -0
  50. package/ansible/ubuntu/t3-desktop.yml +220 -0
  51. package/ansible/ubuntu/t3-health.sh +33 -0
  52. package/ansible/ubuntu/t3-policy.yml +76 -0
  53. package/ansible/ubuntu/t3-route.yml +28 -0
  54. package/ansible/ubuntu/tailscale.yml +78 -0
  55. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  56. package/ansible/ubuntu/tasks/docker.yml +31 -0
  57. package/ansible/ubuntu/tasks/packages.yml +56 -0
  58. package/ansible/ubuntu/tasks/power.yml +51 -0
  59. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  60. package/ansible/ubuntu/tasks/resources.yml +312 -0
  61. package/ansible/ubuntu/tasks/services.yml +31 -0
  62. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  63. package/ansible/ubuntu/tasks/user.yml +120 -0
  64. package/ansible/uv.lock +190 -0
  65. package/bin/devfleet.mjs +36 -0
  66. package/bootstrap/bootstrap.ps1 +50 -0
  67. package/bootstrap/bootstrap.sh +80 -0
  68. package/bootstrap/devfleet +7 -0
  69. package/bootstrap/docker-desktop.sh +33 -0
  70. package/bootstrap/doctor.ts +234 -0
  71. package/bootstrap/fleet-manager-run.sh +103 -0
  72. package/bootstrap/fleet-remote-bootstrap.sh +258 -0
  73. package/bootstrap/global-config.ts +171 -0
  74. package/bootstrap/install-launcher.sh +10 -0
  75. package/bootstrap/native-aws.ts +86 -0
  76. package/bootstrap/native-t3.ts +154 -0
  77. package/bootstrap/npm.ts +358 -0
  78. package/bootstrap/release-version.ts +157 -0
  79. package/bootstrap/setup-status.ts +51 -0
  80. package/bootstrap/setup.ts +88 -0
  81. package/bootstrap/shell-path.ts +105 -0
  82. package/bootstrap/verify-t3-archive.py +49 -0
  83. package/bootstrap/worker-auth.ts +65 -0
  84. package/bootstrap/worker-github.ts +47 -0
  85. package/bootstrap/worker-tools.ts +24 -0
  86. package/cliproxy/AGENTS.md +11 -0
  87. package/cliproxy/README.md +269 -0
  88. package/cliproxy/clients/bootstrap.py +143 -0
  89. package/cliproxy/clients/launcher.py +57 -0
  90. package/cliproxy/compose.yaml +90 -0
  91. package/cliproxy/config.template.yaml +70 -0
  92. package/cliproxy/docs/cloud-adoption.md +16 -0
  93. package/cliproxy/docs/deployment.png +0 -0
  94. package/cliproxy/docs/deployment.svg +109 -0
  95. package/cliproxy/docs/health-workflow.example.yml +185 -0
  96. package/cliproxy/docs/migration.md +131 -0
  97. package/cliproxy/docs/model-health-report.schema.json +364 -0
  98. package/cliproxy/docs/model-health.md +393 -0
  99. package/cliproxy/image.lock.json +5 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  101. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  102. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  103. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  104. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  105. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  106. package/cliproxy/models.json +1618 -0
  107. package/cliproxy/requirements.lock +78 -0
  108. package/cliproxy/scripts/accounts.py +38 -0
  109. package/cliproxy/scripts/audit.py +123 -0
  110. package/cliproxy/scripts/backup +234 -0
  111. package/cliproxy/scripts/check.py +34 -0
  112. package/cliproxy/scripts/checkpoint.py +226 -0
  113. package/cliproxy/scripts/cli.py +64 -0
  114. package/cliproxy/scripts/cliproxy_host.py +257 -0
  115. package/cliproxy/scripts/cloud_state.py +150 -0
  116. package/cliproxy/scripts/container-health +6 -0
  117. package/cliproxy/scripts/docker +24 -0
  118. package/cliproxy/scripts/external-health.py +79 -0
  119. package/cliproxy/scripts/fleet_config.py +43 -0
  120. package/cliproxy/scripts/fleet_sync.py +274 -0
  121. package/cliproxy/scripts/initialize +84 -0
  122. package/cliproxy/scripts/install-dependencies +48 -0
  123. package/cliproxy/scripts/install.py +406 -0
  124. package/cliproxy/scripts/model_health.py +16 -0
  125. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  126. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  127. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  128. package/cliproxy/scripts/modelhealth/config.py +186 -0
  129. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  130. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  131. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  132. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  133. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  134. package/cliproxy/scripts/modelhealth/report.py +150 -0
  135. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  136. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  137. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  138. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  139. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  140. package/cliproxy/scripts/monitor +68 -0
  141. package/cliproxy/scripts/ops +527 -0
  142. package/cliproxy/scripts/public-endpoint +127 -0
  143. package/cliproxy/scripts/recover_node.py +177 -0
  144. package/cliproxy/scripts/restore-verify +60 -0
  145. package/cliproxy/scripts/retention.py +144 -0
  146. package/cliproxy/scripts/service-start +7 -0
  147. package/cliproxy/scripts/validate-config +56 -0
  148. package/cliproxy/scripts/vault-access.py +57 -0
  149. package/cliproxy/scripts/vault-backup-login +55 -0
  150. package/cliproxy/scripts/vault-recovery +56 -0
  151. package/cliproxy/scripts/vault-renew +30 -0
  152. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  153. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  155. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  156. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  157. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  158. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  159. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  160. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  161. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  162. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  163. package/cliproxy/vendor/LICENSE +22 -0
  164. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  165. package/docs/consumer-setup.md +408 -0
  166. package/docs/declared.md +89 -0
  167. package/docs/machine-setup.md +399 -0
  168. package/docs/operations.md +276 -0
  169. package/docs/releasing.md +103 -0
  170. package/docs/state.md +254 -0
  171. package/docs/topology.md +67 -0
  172. package/docs/ubuntu-sync.md +351 -0
  173. package/docs/worker-contract.md +67 -0
  174. package/examples/fleet.json +100 -0
  175. package/examples/roster.json +13 -0
  176. package/llms.txt +35 -0
  177. package/mise.lock +1521 -0
  178. package/mise.toml +110 -0
  179. package/package.json +74 -4
  180. package/schema/fleet.schema.json +420 -0
  181. package/schema/roster.schema.json +77 -0
  182. package/src/agents/account-relay.ts +111 -0
  183. package/src/agents/accounts.ts +85 -0
  184. package/src/agents/audit.ts +77 -0
  185. package/src/agents/cli.ts +13 -0
  186. package/src/agents/command.ts +204 -0
  187. package/src/agents/converge.ts +123 -0
  188. package/src/agents/doctor.ts +107 -0
  189. package/src/agents/errors.ts +2 -0
  190. package/src/agents/files.ts +73 -0
  191. package/src/agents/fleet-verify.ts +165 -0
  192. package/src/agents/launch.ts +290 -0
  193. package/src/agents/native-verify.ts +371 -0
  194. package/src/agents/provider-errors.ts +57 -0
  195. package/src/agents/registry.ts +61 -0
  196. package/src/agents/render.ts +110 -0
  197. package/src/agents/round-trip.ts +192 -0
  198. package/src/agents/secrets.ts +13 -0
  199. package/src/agents/toolchain.ts +34 -0
  200. package/src/agents/verify-cache.ts +103 -0
  201. package/src/agents/verify.ts +127 -0
  202. package/src/auth-checks.ts +163 -0
  203. package/src/cli/agent-guide.ts +129 -0
  204. package/src/cli/app.ts +122 -0
  205. package/src/cli/cli.ts +28 -0
  206. package/src/cli/command-index.ts +57 -0
  207. package/src/cli/contracts.ts +25 -0
  208. package/src/cli/fleet-reference.ts +17 -0
  209. package/src/cli/fleet-state.ts +499 -0
  210. package/src/cli/fleet-sync-checkout.ts +130 -0
  211. package/src/cli/fleet-sync-enrollment.ts +140 -0
  212. package/src/cli/fleet-sync-forward.ts +134 -0
  213. package/src/cli/fleet-sync-manager.ts +249 -0
  214. package/src/cli/fleet-sync-monitoring.ts +185 -0
  215. package/src/cli/fleet-sync-output.ts +317 -0
  216. package/src/cli/fleet-sync-privilege.ts +27 -0
  217. package/src/cli/fleet-sync-progress.ts +109 -0
  218. package/src/cli/fleet-sync-remote.ts +242 -0
  219. package/src/cli/fleet-sync-report.ts +26 -0
  220. package/src/cli/fleet-sync-schedule.ts +138 -0
  221. package/src/cli/fleet-sync-state.ts +86 -0
  222. package/src/cli/fleet-sync-vault.ts +166 -0
  223. package/src/cli/fleet-sync.ts +375 -0
  224. package/src/cli/fleet.ts +165 -0
  225. package/src/cli/managed-health.ts +219 -0
  226. package/src/cli/node-deployment.ts +34 -0
  227. package/src/cli/node-sync.ts +121 -0
  228. package/src/cli/node-t3.ts +31 -0
  229. package/src/cli/options.ts +141 -0
  230. package/src/cli/probe-source.ts +7 -0
  231. package/src/cli/program.ts +185 -0
  232. package/src/cli/project.ts +51 -0
  233. package/src/cli/register-fleet.ts +296 -0
  234. package/src/cli/registry.ts +185 -0
  235. package/src/cli/repair-apply.ts +67 -0
  236. package/src/cli/repair-guard.ts +11 -0
  237. package/src/cli/repair-inspect.ts +150 -0
  238. package/src/cli/repair-script.ts +76 -0
  239. package/src/cli/repair.ts +209 -0
  240. package/src/cli/roster.ts +105 -0
  241. package/src/cli/search.ts +35 -0
  242. package/src/cli/settings.ts +67 -0
  243. package/src/cli/source-operations.ts +102 -0
  244. package/src/cli/t3/command.ts +90 -0
  245. package/src/cli/t3/register.ts +55 -0
  246. package/src/cli/tailnet.ts +127 -0
  247. package/src/cli/ui-actions.ts +133 -0
  248. package/src/cli/ui-controller.ts +113 -0
  249. package/src/cli/ui-data.ts +106 -0
  250. package/src/cli/ui-execute.ts +163 -0
  251. package/src/cli/ui-repairs.ts +34 -0
  252. package/src/cli/ui-reports.ts +211 -0
  253. package/src/cli/ui-targets.ts +100 -0
  254. package/src/cli/ui-terminal.ts +109 -0
  255. package/src/cli/ui-types.ts +49 -0
  256. package/src/cli/ui-wizard.ts +101 -0
  257. package/src/cli/ui.ts +9 -0
  258. package/src/cli/worker-health-output.ts +37 -0
  259. package/src/config.ts +186 -0
  260. package/src/configure.ts +255 -0
  261. package/src/definition.ts +290 -0
  262. package/src/diagnostics.ts +92 -0
  263. package/src/doctor-check.ts +10 -0
  264. package/src/errors.ts +1 -0
  265. package/src/fingerprint.ts +120 -0
  266. package/src/fleet-config.ts +174 -0
  267. package/src/input-hashes.ts +73 -0
  268. package/src/machine/local-source.ts +52 -0
  269. package/src/machine/lock-error.ts +2 -0
  270. package/src/machine/platform.ts +44 -0
  271. package/src/machine/source.ts +68 -0
  272. package/src/machine/state.ts +173 -0
  273. package/src/machine-health.ts +95 -0
  274. package/src/managed-source.ts +47 -0
  275. package/src/manager-failure.ts +48 -0
  276. package/src/manager-sync.ts +405 -0
  277. package/src/manager.ts +62 -0
  278. package/src/monitoring/agent-evidence.ts +146 -0
  279. package/src/monitoring/apply.ts +183 -0
  280. package/src/monitoring/beszel.ts +198 -0
  281. package/src/monitoring/enroll.ts +111 -0
  282. package/src/monitoring/hub-access.ts +114 -0
  283. package/src/monitoring/hub-backup.ts +121 -0
  284. package/src/monitoring/hub-viewer.ts +142 -0
  285. package/src/monitoring/hub.ts +93 -0
  286. package/src/monitoring/manager-agent.ts +73 -0
  287. package/src/monitoring/manager-hub.ts +248 -0
  288. package/src/monitoring/remote.ts +65 -0
  289. package/src/network-definition.ts +83 -0
  290. package/src/node-converge-linux.ts +374 -0
  291. package/src/node-converge-shared.ts +83 -0
  292. package/src/node-converge.ts +40 -0
  293. package/src/onboard.ts +35 -0
  294. package/src/package-info.ts +45 -0
  295. package/src/platform/ansible-receipt.ts +128 -0
  296. package/src/platform/ansible.ts +66 -0
  297. package/src/platform/darwin.ts +203 -0
  298. package/src/platform/detect.ts +11 -0
  299. package/src/platform/fallback.ts +110 -0
  300. package/src/platform/index.ts +38 -0
  301. package/src/platform/launchd.ts +98 -0
  302. package/src/platform/linux.ts +139 -0
  303. package/src/platform/ps.ts +45 -0
  304. package/src/platform/resource-verify.ts +132 -0
  305. package/src/platform/systemd-user.ts +95 -0
  306. package/src/platform/t3-policy.ts +137 -0
  307. package/src/platform/t3-route.ts +68 -0
  308. package/src/platform/t3-service.ts +75 -0
  309. package/src/platform/types.ts +123 -0
  310. package/src/platform/ubuntu-audit.ts +66 -0
  311. package/src/platform/ubuntu-verify.ts +105 -0
  312. package/src/platform/windows.ts +148 -0
  313. package/src/power-recovery.ts +55 -0
  314. package/src/probe.ts +51 -0
  315. package/src/protocol.ts +119 -0
  316. package/src/proxy-converge.ts +85 -0
  317. package/src/remote-bootstrap.ts +210 -0
  318. package/src/remote-entry.ts +89 -0
  319. package/src/remote-lock.ts +12 -0
  320. package/src/repair-catalog.ts +206 -0
  321. package/src/repair-plan.ts +78 -0
  322. package/src/repair-runner.ts +84 -0
  323. package/src/report.ts +71 -0
  324. package/src/runner.ts +104 -0
  325. package/src/secrets.ts +169 -0
  326. package/src/service-auth.ts +115 -0
  327. package/src/ssh.ts +135 -0
  328. package/src/state/bucket.ts +208 -0
  329. package/src/state/check.ts +74 -0
  330. package/src/state/connection.ts +92 -0
  331. package/src/state/enrollment.ts +45 -0
  332. package/src/state/legacy.ts +89 -0
  333. package/src/state/litestream.ts +143 -0
  334. package/src/state/operator.ts +20 -0
  335. package/src/state/paths.ts +40 -0
  336. package/src/state/roster.ts +62 -0
  337. package/src/state/store.ts +169 -0
  338. package/src/state/sync.ts +352 -0
  339. package/src/sync.ts +104 -0
  340. package/src/t3/accounts.ts +81 -0
  341. package/src/t3/desired.ts +165 -0
  342. package/src/t3/files.ts +81 -0
  343. package/src/t3/names-check.ts +53 -0
  344. package/src/t3/names.ts +155 -0
  345. package/src/t3/plan.ts +82 -0
  346. package/src/t3/providers.ts +74 -0
  347. package/src/t3/reconcile.ts +123 -0
  348. package/src/t3-endpoint.ts +104 -0
  349. package/src/t3-linked-check.ts +123 -0
  350. package/src/tailnet-health.ts +231 -0
  351. package/src/tailnet-matching.ts +37 -0
  352. package/src/tailscale.ts +141 -0
  353. package/src/toolchain-checks.ts +108 -0
  354. package/src/topology.ts +24 -0
  355. package/src/transport.ts +122 -0
  356. package/src/types.ts +99 -0
  357. package/src/vault-config.ts +38 -0
  358. package/src/worker-access-checks.ts +168 -0
  359. package/src/worker-capacity.ts +34 -0
  360. package/src/worker-host-checks.ts +181 -0
  361. package/src/worker-policy.ts +64 -0
  362. package/src/worker-readiness.ts +64 -0
  363. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,65 @@
1
+ import type { Check, FleetNode, Runner } from '../types';
2
+ import { shellQuote, sshArgs, sshFailure } from '../ssh';
3
+ import { validCheck, cleanCheck } from '../protocol';
4
+ import { record } from '../config';
5
+ import { isRevision } from '../machine/state';
6
+ import type { AgentEnrollment } from './enroll';
7
+ import { remoteLockPrelude } from '../remote-lock';
8
+
9
+ /** Installs the Beszel agent on a worker. Only node-specific agent credentials
10
+ * cross SSH; hub account credentials stay on the manager. Workers never host
11
+ * the hub (it runs only on the manager), so there is no remote hub mode.
12
+ * The installed source and remote operation lock are reused.
13
+ */
14
+ export async function installRemoteAgent(
15
+ node: FleetNode,
16
+ revision: string,
17
+ enrollment: AgentEnrollment,
18
+ run: Runner
19
+ ): Promise<Check[]> {
20
+ if (!isRevision(revision)) throw new Error('BESZEL_REVISION');
21
+ const root = `"$HOME/.local/share/personal/fleet/revisions/${revision}"`;
22
+ const locked = shellQuote(
23
+ JSON.stringify({
24
+ version: 1,
25
+ checks: [
26
+ {
27
+ id: 'beszel:lock',
28
+ status: 'fail',
29
+ summary: 'FLEET_REMOTE_LOCKED',
30
+ remedy: {
31
+ action:
32
+ 'Another remote operation owns the lock. Inspect its state and retry after it finishes; never delete the lock or start a competing installer.',
33
+ },
34
+ },
35
+ ],
36
+ })
37
+ );
38
+ const command = `cd ${root} && { ${remoteLockPrelude()} || { printf '%s\\n' ${locked}; exit 0; }; } && trap '' HUP && exec "$HOME/.local/bin/mise" exec --locked bun -- bun ${shellQuote('src/monitoring/apply.ts')}`;
39
+ const result = await run([...sshArgs(node), command], {
40
+ input: JSON.stringify({ ...enrollment, node: node.id, revision }),
41
+ timeoutMs: 360_000,
42
+ });
43
+ if (!result.ok) return [sshFailure(result)];
44
+ try {
45
+ const value: unknown = JSON.parse(result.stdout);
46
+ if (
47
+ record(value) &&
48
+ value['version'] === 1 &&
49
+ Array.isArray(value['checks']) &&
50
+ value['checks'].length > 0 &&
51
+ value['checks'].length <= 20 &&
52
+ value['checks'].every(validCheck)
53
+ )
54
+ return value['checks'].map(cleanCheck);
55
+ } catch {
56
+ /* Never expose native output or credentials. */
57
+ }
58
+ return [
59
+ {
60
+ id: 'beszel:protocol',
61
+ status: 'fail',
62
+ summary: 'BESZEL_REMOTE_EVIDENCE_INVALID',
63
+ },
64
+ ];
65
+ }
@@ -0,0 +1,83 @@
1
+ import { type FleetDefinition, type MachineDefinition } from './definition';
2
+ import { record } from './config';
3
+ import { FleetUsageError } from './errors';
4
+
5
+ export interface NetworkHost {
6
+ alias: string;
7
+ tailscaleName: string;
8
+ sshPort: number;
9
+ user: string;
10
+ identityFile: string;
11
+ os: 'linux' | 'macOS' | 'windows';
12
+ t3Port: number;
13
+ }
14
+ export function networkProjection(
15
+ definition: FleetDefinition,
16
+ identities: Record<string, string>
17
+ ): NetworkHost[] {
18
+ return definition.workers.flatMap((node) =>
19
+ node.network
20
+ ? [
21
+ {
22
+ alias: node.ssh,
23
+ ...node.network,
24
+ os: node.os,
25
+ identityFile: identities[node.id] ?? '',
26
+ },
27
+ ]
28
+ : []
29
+ );
30
+ }
31
+ function compatible(
32
+ node: MachineDefinition | undefined,
33
+ host: Record<string, unknown>
34
+ ): node is MachineDefinition {
35
+ return Boolean(
36
+ node?.network &&
37
+ Object.entries(node.network).every(([key, value]) => host[key] === value) &&
38
+ node.os === host['os']
39
+ );
40
+ }
41
+ /** Immutable migration input supplies old manager paths only, never desired hosts. */
42
+ export function legacyIdentities(
43
+ definition: FleetDefinition,
44
+ raw: unknown
45
+ ): Record<string, string> {
46
+ if (!record(raw) || !Array.isArray(raw['hosts']))
47
+ throw new FleetUsageError('Invalid legacy Workstation inventory.');
48
+ const result: Record<string, string> = {};
49
+ for (const host of raw['hosts']) {
50
+ if (!record(host))
51
+ throw new FleetUsageError('Invalid legacy network host.');
52
+ const node = definition.workers.find((node) => node.ssh === host['alias']);
53
+ // A retired host has no manager identity to migrate into this fleet.
54
+ if (!node) continue;
55
+ if (!compatible(node, host))
56
+ throw new FleetUsageError(
57
+ 'Legacy Workstation and canonical Fleet definitions conflict. Review IDs, aliases and network settings before migration.'
58
+ );
59
+ const identity = host['identityFile'];
60
+ if (
61
+ typeof identity !== 'string' ||
62
+ !/^(~\/|\/)[^\x00-\x1f\x7f]+$/.test(identity)
63
+ )
64
+ throw new FleetUsageError('Legacy identity path is invalid.');
65
+ if (result[node.id] !== undefined)
66
+ throw new FleetUsageError('Duplicate legacy machine assignment.');
67
+ result[node.id] = identity;
68
+ }
69
+ return result;
70
+ }
71
+ /** `legacy` is the consumer's earlier inventory, when it has one: it only
72
+ * supplies identity paths the fleet configuration does not hold.
73
+ */
74
+ export function loadNetworkDefinition(
75
+ definition: FleetDefinition,
76
+ legacy: unknown,
77
+ overrides?: Record<string, string>
78
+ ): NetworkHost[] {
79
+ return networkProjection(
80
+ definition,
81
+ overrides ?? legacyIdentities(definition, legacy)
82
+ );
83
+ }
@@ -0,0 +1,374 @@
1
+ import { workerReadiness } from './worker-readiness';
2
+ import { runAnsible } from './platform/ansible';
3
+ import { runAnsibleWithReceipt } from './platform/ansible-receipt';
4
+ import { loadedT3Policy, stageT3Policy } from './platform/t3-policy';
5
+ import { verifyUbuntuBoot } from './platform/ubuntu-verify';
6
+ import { verifyResourceSafety } from './platform/resource-verify';
7
+ import { convergeT3Route, T3_ROUTE } from './platform/t3-route';
8
+ import { convergeProxy } from './proxy-converge';
9
+ import { providerNames } from './t3/names-check';
10
+ import { machineProviders } from './t3/providers';
11
+ import { quotaLimited, verifyFleetProviders } from './agents/fleet-verify';
12
+ import { verifyNativeAgents } from './agents/native-verify';
13
+ import { managedT3, managedT3Desktop } from '../bootstrap/native-t3';
14
+ import { convergeT3Service, t3StartedAt } from './platform/t3-service';
15
+ import { userInfo } from 'node:os';
16
+ import { resolve } from 'node:path';
17
+ import { auditUbuntu } from './platform/ubuntu-audit';
18
+ import {
19
+ applyAgentProfiles,
20
+ attempt,
21
+ configureWorkstation,
22
+ } from './node-converge-shared';
23
+ import type { Check, Runner } from './types';
24
+
25
+ /** Ubuntu worker convergence: Ansible owns OS policy, services and recovery; the
26
+ * native T3, proxy and provider owners run only after the baseline passes.
27
+ */
28
+ export async function convergeLinux(
29
+ root: string,
30
+ target: { node: string; revision: string },
31
+ run: Runner
32
+ ): Promise<Check[]> {
33
+ const { node, revision } = target;
34
+ const audit = await auditUbuntu(run);
35
+ if (!audit.supported || !audit.passwordlessSudo)
36
+ return [
37
+ {
38
+ id: 'ubuntu:prerequisites',
39
+ status: 'fail',
40
+ summary: audit.supported
41
+ ? 'SUDO_AUTHORIZATION_REQUIRED'
42
+ : 'UBUNTU_BASELINE_MISMATCH',
43
+ remedy: {
44
+ action:
45
+ 'Authorize designated-user sudo through the manager enrollment flow; unsupported releases require an explicit migration.',
46
+ },
47
+ },
48
+ ];
49
+ const checks: Check[] = [];
50
+ checks.push(
51
+ ...(await attempt('tailscale:configuration', async () => [
52
+ await applyTailscale(root, run),
53
+ ]))
54
+ );
55
+ const { baseline, verified } = await baselineStep(root, run);
56
+ checks.push(...baseline, ...verified);
57
+ checks.push(...(await t3AfterBaseline(root, { node, baseline, run })));
58
+ checks.push(
59
+ ...(await attempt('workstation:configuration', () =>
60
+ configureWorkstation(root, run)
61
+ ))
62
+ );
63
+ checks.push(
64
+ ...(await attempt('configuration:apply', () =>
65
+ applyAgentProfiles(root, node, revision, run)
66
+ ))
67
+ );
68
+ checks.push(
69
+ ...(await attempt('proxy:configuration', () =>
70
+ convergeProxy(node, revision, run)
71
+ ))
72
+ );
73
+ const providers = await attempt('provider:verification', () =>
74
+ verifyFleetProviders(node)
75
+ );
76
+ checks.push(...providers);
77
+ checks.push(
78
+ ...(await attempt('agent:verification', () =>
79
+ verifyNativeAgents(node, root, run, {
80
+ outOfQuota: quotaLimited(providers),
81
+ // Where T3 starts them: the environment of its user service.
82
+ t3Service: true,
83
+ })
84
+ ))
85
+ );
86
+ checks.push(
87
+ ...(await attempt('worker:readiness', () => workerReadiness(run, node)))
88
+ );
89
+ return checks;
90
+ }
91
+
92
+ /** Applies the baseline (skipped on an unchanged, verified receipt), then
93
+ * measures boot and resource policy. Self-healing: if a skipped baseline's
94
+ * effective policy no longer verifies, the baseline is applied in full and
95
+ * measured again in the same run.
96
+ */
97
+ async function baselineStep(
98
+ root: string,
99
+ run: Runner
100
+ ): Promise<{ baseline: Check[]; verified: Check[] }> {
101
+ const measure = async () => [
102
+ ...(await attempt('ubuntu:boot-policy', () =>
103
+ verifyUbuntuBoot(userInfo().username, run)
104
+ )),
105
+ ...(await attempt('ubuntu:resource-safety', () =>
106
+ verifyResourceSafety(userInfo().homedir, run)
107
+ )),
108
+ ];
109
+ let cached = false;
110
+ let baseline = await attempt('ubuntu:baseline', async () => {
111
+ const applied = await applyBaseline(root, run, false);
112
+ cached = applied.cached;
113
+ return applied.checks;
114
+ });
115
+ let verified = await measure();
116
+ if (cached && verified.some((check) => check.status === 'fail')) {
117
+ baseline = await attempt('ubuntu:baseline', async () => {
118
+ return (await applyBaseline(root, run, true)).checks;
119
+ });
120
+ verified = await measure();
121
+ }
122
+ return { baseline, verified };
123
+ }
124
+
125
+ async function applyBaseline(
126
+ root: string,
127
+ run: Runner,
128
+ force: boolean
129
+ ): Promise<{ cached: boolean; checks: Check[] }> {
130
+ const user = userInfo();
131
+ const ubuntu = resolve(root, 'ansible/ubuntu');
132
+ const result = await runAnsibleWithReceipt(
133
+ run,
134
+ resolve(ubuntu, 'baseline.yml'),
135
+ {
136
+ cwd: ubuntu,
137
+ timeoutMs: 900_000,
138
+ input: JSON.stringify({
139
+ development_source: root,
140
+ development_user: user.username,
141
+ development_uid: user.uid,
142
+ development_home: user.homedir,
143
+ }),
144
+ },
145
+ {
146
+ name: 'ubuntu-baseline',
147
+ sources: [ubuntu],
148
+ force,
149
+ verify: () => baselineServicesActive(run),
150
+ }
151
+ );
152
+ return {
153
+ cached: result.cached,
154
+ checks: [
155
+ {
156
+ id: 'ubuntu:baseline',
157
+ status: result.ok ? 'pass' : 'fail',
158
+ summary: result.ok
159
+ ? result.cached
160
+ ? 'Ubuntu baseline unchanged since its last successful apply and its services are active (re-applied in full at least daily).'
161
+ : 'Ubuntu baseline tasks completed, including always-on service recovery; effective boot/session policies require separate verification.'
162
+ : 'UBUNTU_BASELINE_INCOMPLETE',
163
+ },
164
+ ],
165
+ };
166
+ }
167
+
168
+ /** Cheap probe for what the baseline owns: system services and user timers. */
169
+ async function baselineServicesActive(run: Runner): Promise<boolean> {
170
+ const system = await run([
171
+ 'systemctl',
172
+ 'is-active',
173
+ '--quiet',
174
+ 'ssh.service',
175
+ 'tailscaled.service',
176
+ 'docker.service',
177
+ ]);
178
+ if (!system.ok) return false;
179
+ const user = await run([
180
+ 'systemctl',
181
+ '--user',
182
+ 'is-active',
183
+ '--quiet',
184
+ 'personal-worker-auth.timer',
185
+ ]);
186
+ return user.ok;
187
+ }
188
+
189
+ async function applyTailscale(root: string, run: Runner): Promise<Check> {
190
+ const ubuntu = resolve(root, 'ansible/ubuntu');
191
+ const tailscale = await runAnsibleWithReceipt(
192
+ run,
193
+ 'tailscale.yml',
194
+ { cwd: ubuntu, timeoutMs: 300_000, input: '{}' },
195
+ {
196
+ name: 'ubuntu-tailscale',
197
+ sources: [ubuntu],
198
+ verify: async () => {
199
+ const status = await run(['tailscale', 'status', '--json']);
200
+ return (
201
+ status.ok &&
202
+ (JSON.parse(status.stdout) as { BackendState?: unknown })
203
+ .BackendState === 'Running'
204
+ );
205
+ },
206
+ }
207
+ );
208
+ return {
209
+ id: 'tailscale:configuration',
210
+ status: tailscale.ok ? 'pass' : 'fail',
211
+ summary: tailscale.ok
212
+ ? 'Pinned native Tailscale service is enabled, running and authenticated; manager reachability remains separate.'
213
+ : 'TAILSCALE_SETUP_OR_LOGIN_REQUIRED',
214
+ };
215
+ }
216
+
217
+ async function convergeT3(
218
+ root: string,
219
+ node: string,
220
+ run: Runner
221
+ ): Promise<Check[]> {
222
+ const artifact = managedT3();
223
+ const startedAt = await t3StartedAt(run);
224
+ const checks = await convergeT3Service(
225
+ artifact,
226
+ run,
227
+ async () => {
228
+ const staged = await stageT3Policy(root, artifact.path, run, node);
229
+ return staged.status === 'pass' ? [] : [staged];
230
+ },
231
+ () => installT3Desktop(root, run)
232
+ );
233
+ if (checks.every((check) => check.status === 'pass'))
234
+ checks.push(
235
+ await loadedT3Policy(artifact.path, run, {
236
+ restarted: startedAt !== (await t3StartedAt(run)),
237
+ })
238
+ );
239
+ // Settings are watched by T3: providers arrive and stale ones leave at once.
240
+ checks.push(await machineProviders(node, root, true));
241
+ // Names live in the same settings, so they conform without a restart.
242
+ checks.push(await providerNames(node, root, true));
243
+ const serviceReady = checks.some(
244
+ (check) => check.id === 't3:service' && check.status === 'pass'
245
+ );
246
+ if (serviceReady)
247
+ checks.push(
248
+ ...(await convergeT3Route(run, () => publishT3Route(root, run)))
249
+ );
250
+ if (serviceReady) checks.push(...(await enableWorkerRecovery(root, run)));
251
+ return checks;
252
+ }
253
+
254
+ async function publishT3Route(root: string, run: Runner): Promise<boolean> {
255
+ const result = await runAnsible(run, 't3-route.yml', {
256
+ cwd: resolve(root, 'ansible/ubuntu'),
257
+ input: JSON.stringify({ t3_route_target: T3_ROUTE.proxy }),
258
+ timeoutMs: 60_000,
259
+ });
260
+ return result.ok;
261
+ }
262
+
263
+ async function installT3Desktop(root: string, run: Runner): Promise<boolean> {
264
+ const user = userInfo();
265
+ const desktop = managedT3Desktop();
266
+ const ubuntu = resolve(root, 'ansible/ubuntu');
267
+ const result = await runAnsibleWithReceipt(
268
+ run,
269
+ resolve(ubuntu, 't3-desktop.yml'),
270
+ {
271
+ cwd: root,
272
+ timeoutMs: 600_000,
273
+ input: JSON.stringify({
274
+ development_source: root,
275
+ development_user: user.username,
276
+ development_uid: user.uid,
277
+ development_home: user.homedir,
278
+ t3_desktop_url: desktop.url,
279
+ t3_desktop_name: desktop.name,
280
+ t3_desktop_sha256: desktop.sha256,
281
+ }),
282
+ },
283
+ {
284
+ name: 'ubuntu-t3-desktop',
285
+ sources: [ubuntu],
286
+ // A stopped or disabled app reruns the play, which starts it.
287
+ verify: async () =>
288
+ (
289
+ await run([
290
+ 'systemctl',
291
+ '--user',
292
+ 'is-enabled',
293
+ '--quiet',
294
+ 't3code-desktop.service',
295
+ ])
296
+ ).ok &&
297
+ (
298
+ await run([
299
+ 'systemctl',
300
+ '--user',
301
+ 'is-active',
302
+ '--quiet',
303
+ 't3code-desktop.service',
304
+ ])
305
+ ).ok,
306
+ }
307
+ );
308
+ return result.ok;
309
+ }
310
+
311
+ async function t3AfterBaseline(
312
+ root: string,
313
+ context: { node: string; baseline: Check[]; run: Runner }
314
+ ): Promise<Check[]> {
315
+ const { node, baseline, run } = context;
316
+ if (baseline.some((check) => check.status !== 'pass'))
317
+ return [
318
+ {
319
+ id: 't3:prerequisites',
320
+ status: 'skip',
321
+ summary: 'T3_BASELINE_PREREQUISITES_REQUIRED',
322
+ remedy: {
323
+ action:
324
+ 'Complete the designated-user baseline and lingering policy before changing native service lifecycle. Existing service state is preserved.',
325
+ },
326
+ },
327
+ ];
328
+ return attempt('t3:service', () => convergeT3(root, node, run));
329
+ }
330
+
331
+ async function enableWorkerRecovery(
332
+ root: string,
333
+ run: Runner
334
+ ): Promise<Check[]> {
335
+ const user = userInfo();
336
+ const ubuntu = resolve(root, 'ansible/ubuntu');
337
+ const result = await runAnsibleWithReceipt(
338
+ run,
339
+ resolve(ubuntu, 'recovery.yml'),
340
+ {
341
+ cwd: root,
342
+ timeoutMs: 60_000,
343
+ input: JSON.stringify({
344
+ development_source: root,
345
+ development_user: user.username,
346
+ development_uid: user.uid,
347
+ development_home: user.homedir,
348
+ }),
349
+ },
350
+ {
351
+ name: 'ubuntu-recovery',
352
+ sources: [ubuntu],
353
+ verify: async () =>
354
+ (
355
+ await run([
356
+ 'systemctl',
357
+ '--user',
358
+ 'is-active',
359
+ '--quiet',
360
+ 'personal-worker-recovery.timer',
361
+ ])
362
+ ).ok,
363
+ }
364
+ );
365
+ return [
366
+ {
367
+ id: 'worker:service-recovery',
368
+ status: result.ok ? 'pass' : 'fail',
369
+ summary: result.ok
370
+ ? 'T3 recovery enabled: a stopped desktop app is started and an unresponsive one restarted after three failed one-minute health checks.'
371
+ : 'WORKER_RECOVERY_SETUP_FAILED',
372
+ },
373
+ ];
374
+ }
@@ -0,0 +1,83 @@
1
+ import { fingerprint, writeReceipt } from './fingerprint';
2
+ import { configureWorkspace } from './configure';
3
+ import { stableWorkspace } from './platform/t3-policy';
4
+ import type { Check, Runner } from './types';
5
+
6
+ /** Phase wrapper: one failure code per phase, durations measured, no exception text. */
7
+ export async function attempt(
8
+ id: string,
9
+ operation: () => Promise<Check[]>
10
+ ): Promise<Check[]> {
11
+ const started = Date.now();
12
+ try {
13
+ const checks = await operation();
14
+ const phaseDurationMs = Date.now() - started;
15
+ return checks.map((check) => ({ ...check, phaseDurationMs }));
16
+ } catch {
17
+ return [
18
+ {
19
+ id,
20
+ status: 'fail',
21
+ summary: 'REMOTE_PHASE_FAILED',
22
+ phaseDurationMs: Date.now() - started,
23
+ remedy: {
24
+ action:
25
+ 'Inspect the named owner and approved source revision. Prior phase evidence is retained; no exception, environment or auth response is logged.',
26
+ },
27
+ },
28
+ ];
29
+ }
30
+ }
31
+
32
+ /** The `devfleet` launcher (linked to the applied revision, never copied) and
33
+ * this worker's own credential refresh. Identical on Linux and macOS.
34
+ */
35
+ export async function configureWorkstation(
36
+ root: string,
37
+ run: Runner
38
+ ): Promise<Check[]> {
39
+ const installed = await run(
40
+ // The stable path of the applied revision: it survives pruning.
41
+ ['sh', 'bootstrap/install-launcher.sh', await stableWorkspace(root)],
42
+ { cwd: root, timeoutMs: 30_000 }
43
+ );
44
+ if (!installed.ok)
45
+ return [
46
+ {
47
+ id: 'workstation:installation',
48
+ status: 'fail',
49
+ summary: 'LAUNCHER_INSTALLATION_FAILED',
50
+ },
51
+ ];
52
+ // This receipt certifies only frozen dependency inputs, never machine readiness.
53
+ await writeReceipt(root, await fingerprint(root, run));
54
+ await run(['bun', 'bootstrap/worker-auth.ts'], {
55
+ cwd: root,
56
+ timeoutMs: 90_000,
57
+ });
58
+ return [
59
+ {
60
+ id: 'workstation:configuration',
61
+ status: 'pass',
62
+ summary:
63
+ 'The devfleet launcher is linked to the applied revision; authentication remains a separate gate.',
64
+ },
65
+ ];
66
+ }
67
+
68
+ export function applyAgentProfiles(
69
+ root: string,
70
+ node: string,
71
+ revision: string,
72
+ run: Runner
73
+ ): Promise<Check[]> {
74
+ return configureWorkspace(
75
+ root,
76
+ { node, revision, tool: 'codex', profile: 'proxy' },
77
+ run,
78
+ {
79
+ additionalTools: ['claude', 'gemini', 'opencode', 'grok', 'antigravity'],
80
+ bootstrappedDeployment: true,
81
+ }
82
+ );
83
+ }
@@ -0,0 +1,40 @@
1
+ import { runLockedCommand as runCommand } from './runner';
2
+ import { isRevision } from './machine/state';
3
+ import { convergeLinux } from './node-converge-linux';
4
+ import type { Check } from './types';
5
+
6
+ /** Internal remote entry, run on a worker only. The manager owns the operation
7
+ * lock and source selection; this installs no pull daemon or timer. Never
8
+ * prints child output. Workers are Linux.
9
+ */
10
+ async function main(): Promise<Check[]> {
11
+ const [node, revision] = process.argv.slice(2);
12
+ if (!node || !/^[a-z][a-z0-9-]{0,62}$/.test(node) || !isRevision(revision))
13
+ throw new Error();
14
+ const root = process.cwd();
15
+ const head = await runCommand(['git', 'rev-parse', 'HEAD']);
16
+ if (!head.ok || head.stdout.trim() !== revision) throw new Error();
17
+ if (process.platform === 'linux')
18
+ return convergeLinux(root, { node, revision }, runCommand);
19
+ return [
20
+ {
21
+ id: 'platform:unsupported',
22
+ status: 'fail',
23
+ summary: 'PLATFORM_UNSUPPORTED',
24
+ remedy: {
25
+ action:
26
+ 'Workers are Ubuntu 26.04. The macOS manager converges itself locally in fleet-sync.',
27
+ },
28
+ },
29
+ ];
30
+ }
31
+ if (import.meta.main) {
32
+ const checks = await main().catch((): Check[] => [
33
+ {
34
+ id: 'configuration:operation',
35
+ status: 'fail',
36
+ summary: 'REMOTE_CONFIGURATION_INCOMPLETE',
37
+ },
38
+ ]);
39
+ process.stdout.write(JSON.stringify({ version: 1, checks }));
40
+ }
package/src/onboard.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { shellQuote } from './ssh';
2
+ import { syncPlan } from './sync';
3
+ import type { FleetNode } from './types';
4
+
5
+ export function onboarding(node: FleetNode, launcher = 'devfleet'): object {
6
+ return {
7
+ version: 1,
8
+ node: node.id,
9
+ applied: false,
10
+ steps: [
11
+ 'On the new node, review and run bootstrap/bootstrap.sh (or bootstrap.ps1 for native Windows). Use docs/machine-setup.md. This installs the shared locked toolchain; it does not enable SSH.',
12
+ 'Enable the OS SSH service as the operator on a trusted private network. Do not expose SSH publicly or create cloud resources.',
13
+ 'Create a distinct per-machine SSH key and authorize its PUBLIC key on the destination. Verify host fingerprints out of band. Keep private keys and auth tokens on their original machines.',
14
+ 'In the manager SSH config, configure this alias with the real HostName, User, IdentityFile, optional Port/ProxyJump and IdentitiesOnly yes. Fleet will not write or overwrite SSH configuration.',
15
+ 'Test ssh ALIAS manually once after fingerprint verification. Ensure noninteractive shell startup writes no stdout. Set runtime to an absolute Node/Bun executable/shim if SSH PATH omits it.',
16
+ 'Authenticate separately on the node: gh auth login; gh auth setup-git; vault login using the approved method. Configure coding-provider sessions independently; never copy auth directories.',
17
+ 'For T3 Connect, review t3 status and t3 service status, then complete t3 up --connect and operator pairing/linking. T3 pairing/auth state is not an SSH credential.',
18
+ 'For VS Code, install Remote-SSH in the LOCAL editor profile, use the same SSH alias/client/config, enable the login terminal temporarily, and read Output → Remote - SSH. Restart the editor after PATH/agent changes.',
19
+ 'VS Code needs TCP forwarding and compatible remote server prerequisites. Test with ssh -N -L 127.0.0.1:18080:127.0.0.1:REMOTE_PORT ALIAS against a known loopback service, then curl localhost:18080 from a second terminal. Review server policy; never loosen it automatically.',
20
+ 'Run devfleet fleet-status --node NODE --json from every intended manager. This is directed connectivity, not an all-pairs proof. Review skipped T3/VS Code end-to-end checks manually.',
21
+ ],
22
+ sshTemplate: `Host ${node.ssh}\n HostName REPLACE_WITH_PRIVATE_HOST\n User REPLACE_WITH_USER\n IdentityFile ~/.ssh/REPLACE_WITH_NODE_KEY\n IdentitiesOnly yes\n ForwardAgent no\n`,
23
+ vscodeSettings: {
24
+ 'remote.SSH.path': 'REPLACE_WITH_SAME_OPENSSH_BINARY',
25
+ 'remote.SSH.configFile': 'REPLACE_WITH_ABSOLUTE_SSH_CONFIG',
26
+ 'remote.SSH.showLoginTerminal': true,
27
+ },
28
+ workspaceCommand: `cd -- ${shellQuote(node.workspace)}`,
29
+ verifyCommand: `${launcher} fleet-status --node ${node.id} --json`,
30
+ sync: {
31
+ command: `${launcher} fleet-repair --node ${node.id} --repair workspace`,
32
+ steps: syncPlan(false),
33
+ },
34
+ };
35
+ }