@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,143 @@
1
+ #!/usr/bin/env python3
2
+ """Opt-in launchers, preserving all existing native client configuration."""
3
+
4
+ import hashlib
5
+ import subprocess
6
+ import argparse, getpass, json, os, pathlib, shutil, sys, urllib.request
7
+
8
+ p = argparse.ArgumentParser()
9
+ p.add_argument("--endpoint")
10
+ p.add_argument("--model")
11
+ p.add_argument("--claude-model")
12
+ p.add_argument("--uninstall", action="store_true")
13
+ p.add_argument("--validate", action="store_true")
14
+ a = p.parse_args()
15
+ launcher = pathlib.Path(__file__).resolve().with_name("launcher.py")
16
+ if not a.validate and not a.uninstall:
17
+ try:
18
+ source_repo = launcher.parents[2]
19
+ tracked = subprocess.run(
20
+ [
21
+ "git",
22
+ "-c",
23
+ "safe.directory=" + str(source_repo),
24
+ "-C",
25
+ str(source_repo),
26
+ "ls-files",
27
+ "--error-unmatch",
28
+ "--",
29
+ "cliproxy/clients/bootstrap.py",
30
+ "cliproxy/clients/launcher.py",
31
+ ],
32
+ capture_output=True,
33
+ )
34
+ if tracked.returncode or not launcher.is_file():
35
+ raise ValueError("Untracked launcher")
36
+ except (OSError, IndexError, ValueError):
37
+ sys.exit(
38
+ "Run bootstrap from the repository: bootstrap.py and launcher.py must be Git-tracked."
39
+ )
40
+ home = pathlib.Path.home()
41
+ config = home / ".config/cliproxy-fleet"
42
+ bins = home / ".local/bin"
43
+ if a.uninstall:
44
+ for name in ["codex-fleet", "claude-fleet", "opencode-fleet"]:
45
+ f = bins / name
46
+ if f.exists() and "cliproxy-fleet launcher" in f.read_text():
47
+ f.unlink()
48
+ # Keep the credential for deliberate reuse, not accidental credential loss.
49
+ print(
50
+ "Removed fleet launchers. Native settings unchanged. Remove ~/.config/cliproxy-fleet when no longer needed."
51
+ )
52
+ sys.exit()
53
+ os.umask(0o077)
54
+ config.mkdir(parents=True, exist_ok=True)
55
+ config.chmod(0o700)
56
+ bins.mkdir(parents=True, exist_ok=True)
57
+ if a.validate:
58
+ settings = json.loads((config / "settings.json").read_text())
59
+ key = (config / "client-key").read_text().strip()
60
+ req = urllib.request.Request(
61
+ settings["endpoint"].rstrip("/") + "/v1/models",
62
+ headers={"Authorization": "Bearer " + key, "User-Agent": "cliproxy-fleet/1.0"},
63
+ )
64
+ with urllib.request.urlopen(req, timeout=20) as r:
65
+ data = json.load(r)
66
+ print("Authenticated model listing passed; model IDs:")
67
+ print("\n".join(m["id"] for m in data.get("data", [])))
68
+ sys.exit()
69
+ if not a.endpoint or not a.model:
70
+ p.error("--endpoint and --model are required; choose actual available model IDs")
71
+ if not a.endpoint.startswith("https://"):
72
+ p.error("Endpoint must use HTTPS")
73
+ previous = (
74
+ json.loads((config / "settings.json").read_text())
75
+ if (config / "settings.json").exists()
76
+ else {}
77
+ )
78
+ generator_sha256 = hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest()
79
+ settings = {
80
+ "endpoint": a.endpoint.rstrip("/"),
81
+ "generator_repository": "https://github.com/crvouga/dev-fleet",
82
+ "generator_sha256": generator_sha256,
83
+ "model": a.model,
84
+ "claude_model": a.claude_model or previous.get("claude_model"),
85
+ }
86
+ keyfile = config / "client-key"
87
+ if not keyfile.exists():
88
+ key = getpass.getpass(
89
+ "Fleet client API key (hidden; NOT provider OAuth or management key): "
90
+ )
91
+ if len(key) < 32:
92
+ sys.exit("Client key must contain at least 32 characters")
93
+ keyfile.write_text(key + "\n")
94
+ keyfile.chmod(0o600)
95
+ (config / "settings.json").write_text(json.dumps(settings, indent=2) + "\n")
96
+ # Use SDK-specific protocols; users select the launcher explicitly.
97
+ opencode = {
98
+ "$schema": "https://opencode.ai/config.json",
99
+ "provider": {
100
+ "fleet-responses": {
101
+ "npm": "@ai-sdk/openai",
102
+ "name": "Fleet (Responses)",
103
+ "options": {
104
+ "baseURL": settings["endpoint"] + "/v1",
105
+ "apiKey": "{env:CLIPROXY_API_KEY}",
106
+ },
107
+ "models": {a.model: {"name": a.model}},
108
+ },
109
+ "fleet-claude": {
110
+ "npm": "@ai-sdk/anthropic",
111
+ "name": "Fleet (Claude)",
112
+ "options": {
113
+ "baseURL": settings["endpoint"] + "/v1",
114
+ "apiKey": "{env:CLIPROXY_API_KEY}",
115
+ },
116
+ "models": {a.claude_model: {"name": a.claude_model}},
117
+ },
118
+ },
119
+ "model": "fleet-responses/" + a.model,
120
+ }
121
+ if not settings["claude_model"]:
122
+ opencode["provider"].pop("fleet-claude")
123
+ else:
124
+ opencode["provider"]["fleet-claude"]["models"] = {
125
+ settings["claude_model"]: {"name": settings["claude_model"]}
126
+ }
127
+ (config / "opencode.json").write_text(json.dumps(opencode, indent=2) + "\n")
128
+ launcher = pathlib.Path(__file__).resolve().with_name("launcher.py")
129
+ if not launcher.is_file():
130
+ sys.exit(
131
+ "Run bootstrap from the repository checkout; tracked launcher.py is required."
132
+ )
133
+
134
+ for name in ["codex-fleet", "claude-fleet", "opencode-fleet"]:
135
+ f = bins / name
136
+ if f.exists() and "cliproxy-fleet launcher" not in f.read_text():
137
+ sys.exit("Refusing to overwrite unrelated launcher: " + str(f))
138
+ if f.exists() or f.is_symlink():
139
+ f.unlink()
140
+ f.symlink_to(launcher)
141
+ print(
142
+ "Installed opt-in launchers in ~/.local/bin. Native client settings preserved. Run bootstrap.py --validate."
143
+ )
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python3
2
+ # cliproxy-fleet launcher
3
+ # Tracked source: crvouga/dev-fleet cliproxy/clients/launcher.py
4
+ import json, os, pathlib, sys
5
+
6
+ r = pathlib.Path.home() / ".config/cliproxy-fleet"
7
+ s = json.loads((r / "settings.json").read_text())
8
+ os.environ["CLIPROXY_API_KEY"] = (r / "client-key").read_text().strip()
9
+ name = pathlib.Path(sys.argv[0]).name
10
+ args = sys.argv[1:]
11
+ if name == "codex-fleet":
12
+ args = [
13
+ "codex",
14
+ "-c",
15
+ 'model_provider="fleet"',
16
+ "-c",
17
+ 'model_providers.fleet.name="Fleet"',
18
+ "-c",
19
+ "model_providers.fleet.base_url=" + json.dumps(s["endpoint"] + "/v1"),
20
+ "-c",
21
+ 'model_providers.fleet.env_key="CLIPROXY_API_KEY"',
22
+ "-c",
23
+ 'model_providers.fleet.wire_api="responses"',
24
+ "-c",
25
+ "model_providers.fleet.requires_openai_auth=false",
26
+ "-c",
27
+ "model_providers.fleet.supports_websockets=false",
28
+ "-m",
29
+ s["model"],
30
+ *args,
31
+ ]
32
+ elif name == "claude-fleet":
33
+ if not s.get("claude_model"):
34
+ sys.exit(
35
+ "No Claude model configured. Add an authorized Claude account and rerun bootstrap with --claude-model MODEL_ID."
36
+ )
37
+ for k in [
38
+ "ANTHROPIC_API_KEY",
39
+ "CLAUDE_CODE_USE_BEDROCK",
40
+ "CLAUDE_CODE_USE_VERTEX",
41
+ "CLAUDE_CODE_USE_FOUNDRY",
42
+ ]:
43
+ os.environ.pop(k, None)
44
+ os.environ["ANTHROPIC_BASE_URL"] = s["endpoint"]
45
+ os.environ["ANTHROPIC_AUTH_TOKEN"] = os.environ["CLIPROXY_API_KEY"]
46
+ # Pin all default model families to the explicit approved model; no cost-changing fallback.
47
+ for k in [
48
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
49
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
50
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
51
+ ]:
52
+ os.environ[k] = s["claude_model"]
53
+ args = ["claude", "--model", s["claude_model"], *args]
54
+ else:
55
+ os.environ["OPENCODE_CONFIG"] = str(r / "opencode.json")
56
+ args = ["opencode", *args]
57
+ os.execvp(args[0], args)
@@ -0,0 +1,90 @@
1
+ name: cliproxy-fleet
2
+ services:
3
+ proxy:
4
+ image: ${CLI_PROXY_IMAGE:?image must be pinned}
5
+ restart: unless-stopped
6
+ user: '${CLIPROXY_UID:-1000}:${CLIPROXY_GID:-1000}'
7
+ working_dir: /state
8
+ command: ['/CLIProxyAPI/CLIProxyAPI', '-config', '/state/config.yaml']
9
+ environment:
10
+ HOME: /state
11
+ TZ: UTC
12
+ MANAGEMENT_STATIC_PATH: /state/static
13
+ ports:
14
+ - '127.0.0.1:8317:8317'
15
+ volumes:
16
+ - ./state/runtime:/state
17
+ - ./scripts/container-health:/health:ro
18
+ read_only: true
19
+ tmpfs:
20
+ - /tmp:rw,noexec,nosuid,size=64m,mode=1777
21
+ cap_drop: [ALL]
22
+ security_opt: [no-new-privileges:true]
23
+ mem_limit: 2g
24
+ cpus: 2
25
+ pids_limit: 256
26
+ stop_grace_period: 45s
27
+ healthcheck:
28
+ test: ['CMD', '/health']
29
+ interval: 30s
30
+ timeout: 5s
31
+ retries: 3
32
+ start_period: 30s
33
+ logging:
34
+ driver: local
35
+ options:
36
+ max-size: '10m'
37
+ max-file: '3'
38
+ tailnet:
39
+ profiles: [portable-endpoint]
40
+ user: '${CLIPROXY_UID:-1000}:${CLIPROXY_GID:-1000}'
41
+ image: ${TAILSCALE_IMAGE:?image must be pinned}
42
+ restart: unless-stopped
43
+ entrypoint: ['/usr/local/bin/tailscaled']
44
+ command:
45
+ [
46
+ '--tun=userspace-networking',
47
+ '--state=/var/lib/tailscale/tailscaled.state',
48
+ '--socket=/tmp/tailscaled.sock',
49
+ ]
50
+ volumes:
51
+ - ./state/tailscale:/var/lib/tailscale
52
+ cap_drop: [ALL]
53
+ security_opt: [no-new-privileges:true]
54
+ mem_limit: 256m
55
+ cpus: 0.5
56
+ pids_limit: 128
57
+ logging:
58
+ driver: local
59
+ options:
60
+ max-size: '5m'
61
+ max-file: '2'
62
+ cloudflared:
63
+ profiles: [public-endpoint]
64
+ image: ${CLOUDFLARED_IMAGE:?image must be pinned}
65
+ user: '${CLIPROXY_UID:-1000}:${CLIPROXY_GID:-1000}'
66
+ restart: unless-stopped
67
+ command:
68
+ [
69
+ 'tunnel',
70
+ '--no-autoupdate',
71
+ 'run',
72
+ '--token-file',
73
+ '/run/secrets/tunnel-token',
74
+ ]
75
+ volumes:
76
+ - ./state/secrets/tunnel-token:/run/secrets/tunnel-token:ro
77
+ depends_on:
78
+ proxy:
79
+ condition: service_healthy
80
+ read_only: true
81
+ cap_drop: [ALL]
82
+ security_opt: [no-new-privileges:true]
83
+ mem_limit: 256m
84
+ cpus: 0.5
85
+ pids_limit: 128
86
+ logging:
87
+ driver: local
88
+ options:
89
+ max-size: '5m'
90
+ max-file: '2'
@@ -0,0 +1,70 @@
1
+ config-version: 8
2
+ server:
3
+ host: '0.0.0.0'
4
+ port: 8317
5
+ max-request-body-bytes: 33554432
6
+ trusted-proxies: []
7
+ discovery:
8
+ enabled: false
9
+ management:
10
+ allow-remote: true
11
+ secret-key: REPLACE_MANAGEMENT_KEY
12
+ disable-control-panel: false
13
+ disable-auto-update-panel: true
14
+ panel-github-repository: https://github.com/router-for-me/Cli-Proxy-API-Management-Center
15
+ access:
16
+ api-keys:
17
+ - REPLACE_CLIENT_KEY
18
+ oauth:
19
+ auth-dir: /state/auths
20
+ # Fleet-owned (applied by `ops declared`): the Antigravity CLI sends its
21
+ # catalog ids; serve them from the Antigravity account's matching models.
22
+ model-alias:
23
+ antigravity:
24
+ - name: gemini-3.8-flash-high
25
+ alias: gemini-3.8-flash
26
+ fork: true
27
+ - name: gemini-3.1-flash-lite
28
+ alias: gemini-3.1-flash-lite-preview
29
+ fork: true
30
+ routing:
31
+ # Fill first: use one account until its quota is exhausted (it then cools down), then the next.
32
+ # Sessions stay bound to their account while it is available; new sessions start on the first
33
+ # available account, so usage is not spread across accounts.
34
+ strategy: fill-first
35
+ session-affinity: true
36
+ retry:
37
+ request-retry: 2
38
+ # 0 tries every eligible credential, so exhausting several accounts still reaches the next one.
39
+ max-retry-credentials: 0
40
+ max-retry-interval: 30
41
+ cooldown:
42
+ disable-cooling: false
43
+ save-cooldown-status: true
44
+ upstream:
45
+ claude:
46
+ # Fleet-owned (applied by `ops declared`): the proxy presents itself to
47
+ # Anthropic as this Claude Code version, and Anthropic refuses newer models
48
+ # to an older one. fleet-sync replaces the version with the `claude` pin in
49
+ # mise.toml on every run, so it follows the harness every node runs.
50
+ header-defaults:
51
+ user-agent: 'claude-cli/2.1.314 (external, cli)'
52
+ # T3 runs Claude Code under its own entrypoint label.
53
+ native-entrypoints: ['t3-code']
54
+ # Legacy compatibility field intentionally retained: never switch model capability/cost.
55
+ quota-exceeded:
56
+ switch-project: true
57
+ switch-preview-model: false
58
+ observability:
59
+ logs:
60
+ debug: false
61
+ logging-to-file: false
62
+ request-log: false
63
+ logs-max-total-size-mb: 50
64
+ error-logs-max-files: 2
65
+ usage:
66
+ usage-statistics-enabled: true
67
+ pprof:
68
+ enable: false
69
+ plugins:
70
+ enabled: false
@@ -0,0 +1,16 @@
1
+ # Cloud ownership and initial adoption
2
+
3
+ The initial workstation setup created runtime/cloud state before this package existed. Its operational code is now maintained here; installed copies are generated from committed blobs. The one-off Tailscale API provisioning and Vault secret-writing helpers are retired and removed during first package installation. They must not return as parallel infrastructure controllers.
4
+
5
+ Existing external resources are retained:
6
+
7
+ - Tailscale Service `svc:ai-proxy`, endpoint `tcp:443`, stable MagicDNS origin `https://ai-proxy.tail0a1b2c.ts.net`.
8
+ - Dedicated host tag `tag:ai-proxy` and optional client tag `tag:ai-client`, with owner/service approval and private access rules already in the tailnet. The original workstation identity/SSH/service route was preserved.
9
+ - Existing production R2 bucket and prefix `cliproxy-fleet/restic`; bucket and credentials already belong to the OpenTofu platform.
10
+ - Existing Vault `secret/personal/prd` fields `CLIPROXY_FLEET_RESTIC_PASSWORD`, `CLIPROXY_FLEET_CLIENT_API_KEY`, `CLIPROXY_FLEET_MANAGEMENT_KEY`, and operator-added `TAILSCALE_API_TOKEN`.
11
+
12
+ The new Vault field names still require reconciliation into `packages/infra/tofu/modules/inventory/inventory.tf.json` and the **complete** OpenTofu `vault_inputs` map before a later Vault apply. Preserve their existing values and every unrelated field. Use the established `bun run vault:fix`/`vault:edit` adoption workflow and a reviewed saved plan; never seed secrets directly from this package. The Tailscale admin token is no longer consumed by the runtime package and is not mounted into containers.
13
+
14
+ Before changing existing Tailscale cloud resources, adopt their identities and full policy through the platform's OpenTofu workflow. Do not replace the tailnet policy wholesale, recreate the Service/TailVIP, retag the workstation, or delete existing grants. Local `tailscale up` authorization and `serve` advertisement of an already defined resource are runtime operations; policy, tag ownership and Service definitions are infrastructure.
15
+
16
+ These are outstanding cloud-IaC reconciliation items, not hidden local scripts. Provider/authentication state, source-manifest metadata and the dedicated node identity are encrypted in R2 backups; recovery credentials are independently in Vault. Do not put live state, secrets, provider tokens or infrastructure plans into Git.
Binary file
@@ -0,0 +1,109 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1120" viewBox="0 0 1440 1120" role="img" aria-labelledby="title description">
2
+ <title id="title">CLIProxyAPI deployment and integrations</title>
3
+ <desc id="description">Clients reach Cloudflare HTTPS with API keys. An outbound tunnel connects to cloudflared and CLIProxyAPI on the workstation. CLIProxyAPI calls the upstream AI provider. GitHub supplies tracked runtime code and OpenTofu cloud configuration. Vault supplies credentials; Restic sends encrypted backups to R2. A private Tailscale endpoint also reaches the proxy.</desc>
4
+ <defs>
5
+ <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#667897"/></marker>
6
+ <marker id="blue-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/></marker>
7
+ <style>
8
+ text { font-family: Inter, Arial, sans-serif; fill: #15243b; }
9
+ .heading { font-size: 32px; font-weight: 700; }
10
+ .subtitle { font-size: 16px; fill: #53637b; }
11
+ .label { font-size: 18px; font-weight: 700; }
12
+ .detail { font-size: 15px; fill: #53637b; }
13
+ .edge-label { font-size: 14px; fill: #53637b; }
14
+ .box { fill: white; stroke: #ccd7e6; stroke-width: 1.5; }
15
+ .traffic { fill: none; stroke: #2563eb; stroke-width: 3; marker-end: url(#blue-arrow); }
16
+ .control { fill: none; stroke: #667897; stroke-width: 2; stroke-dasharray: 7 6; marker-end: url(#arrow); }
17
+ .storage { fill: none; stroke: #667897; stroke-width: 2; marker-end: url(#arrow); }
18
+ </style>
19
+ </defs>
20
+ <rect width="1440" height="1120" rx="24" fill="#f5f8fc"/>
21
+ <text x="54" y="65" class="heading">CLIProxyAPI · deployment &amp; integrations</text>
22
+ <text x="54" y="96" class="subtitle">Stable public HTTPS · API-key authentication · GitHub-owned configuration</text>
23
+
24
+ <rect x="54" y="130" width="1332" height="104" rx="16" class="box"/>
25
+ <text x="80" y="164" class="label">YOUR CLIENTS</text>
26
+ <text x="80" y="196" class="detail">Codex CLI · Claude Code · OpenCode · other API clients</text>
27
+ <text x="900" y="164" class="label">MANAGEMENT DASHBOARD</text>
28
+ <text x="900" y="196" class="detail">Browser + separate management key</text>
29
+
30
+ <path d="M 720 234 V 290" class="traffic"/>
31
+ <text x="742" y="269" class="edge-label">HTTPS + client API key / management key</text>
32
+ <rect x="395" y="290" width="650" height="110" rx="16" fill="#eaf1ff" stroke="#9dbaf5" stroke-width="1.5"/>
33
+ <text x="720" y="328" text-anchor="middle" class="label">cli-proxy.example.com</text>
34
+ <text x="720" y="359" text-anchor="middle" class="detail">Cloudflare DNS + HTTPS + managed Tunnel</text>
35
+ <text x="720" y="383" text-anchor="middle" class="detail">API: /v1 · Dashboard: /management.html</text>
36
+
37
+ <rect x="330" y="462" width="770" height="392" rx="20" fill="#edf5ff" stroke="#9dbaf5" stroke-width="1.5"/>
38
+ <text x="356" y="499" class="label">THIS WORKSTATION</text>
39
+ <text x="356" y="523" class="detail">systemd + Docker Compose · pinned Git code</text>
40
+
41
+ <path d="M 720 400 V 560" class="traffic"/>
42
+ <rect x="550" y="419" width="340" height="27" rx="6" fill="#f5f8fc"/>
43
+ <text x="720" y="438" text-anchor="middle" class="edge-label">Encrypted tunnel · initiated outbound by host</text>
44
+ <rect x="550" y="560" width="330" height="82" rx="13" class="box"/>
45
+ <text x="715" y="592" text-anchor="middle" class="label">cloudflared container</text>
46
+ <text x="715" y="619" text-anchor="middle" class="detail">Private token file · no added host port</text>
47
+ <path d="M 715 642 V 686" class="traffic"/>
48
+ <text x="735" y="669" class="edge-label">Docker network</text>
49
+ <rect x="550" y="686" width="330" height="85" rx="13" fill="#2563eb"/>
50
+ <text x="715" y="720" text-anchor="middle" style="fill:white;font-size:21px;font-weight:700">CLIProxyAPI</text>
51
+ <text x="715" y="748" text-anchor="middle" style="fill:#dbeafe;font-size:15px">Port 8317 · authentication + API routing</text>
52
+ <rect x="355" y="686" width="161" height="85" rx="13" class="box"/>
53
+ <text x="435" y="720" text-anchor="middle" class="label">Working cache</text>
54
+ <text x="435" y="746" text-anchor="middle" class="detail">OAuth + config</text>
55
+ <path d="M 550 728 H 516" class="storage" marker-start="url(#arrow)"/>
56
+ <text x="356" y="818" class="detail">Health monitoring · Vault renewal · cloud checkpoint every 15 minutes</text>
57
+
58
+ <rect x="1140" y="686" width="246" height="104" rx="15" class="box"/>
59
+ <text x="1263" y="719" text-anchor="middle" class="label">AI provider</text>
60
+ <text x="1263" y="746" text-anchor="middle" class="detail">Currently: Codex OAuth</text>
61
+ <text x="1263" y="771" text-anchor="middle" class="detail">Streaming + tool calls</text>
62
+ <path d="M 880 729 H 1140" class="traffic" marker-start="url(#blue-arrow)"/>
63
+ <text x="1008" y="714" text-anchor="middle" class="edge-label">Provider authentication</text>
64
+
65
+ <rect x="54" y="540" width="235" height="110" rx="15" class="box"/>
66
+ <text x="171" y="574" text-anchor="middle" class="label">Vault</text>
67
+ <text x="171" y="601" text-anchor="middle" class="detail">Tunnel, API + backup keys</text>
68
+ <text x="171" y="626" text-anchor="middle" class="detail">Current S3 credentials</text>
69
+ <path d="M 289 580 H 550" class="control"/>
70
+ <rect x="345" y="557" width="170" height="24" rx="5" fill="#edf5ff"/>
71
+ <text x="430" y="574" text-anchor="middle" class="edge-label">Token on startup</text>
72
+
73
+ <rect x="1140" y="540" width="246" height="110" rx="15" class="box"/>
74
+ <text x="1263" y="574" text-anchor="middle" class="label">Tailscale endpoint</text>
75
+ <text x="1263" y="601" text-anchor="middle" class="detail">Existing private alternative</text>
76
+ <text x="1263" y="626" text-anchor="middle" class="detail">ai-proxy.tail0a1b2c.ts.net</text>
77
+ <path d="M 1140 600 H 1048 V 704 H 880" class="storage"/>
78
+
79
+ <rect x="54" y="903" width="330" height="112" rx="15" class="box"/>
80
+ <text x="219" y="938" text-anchor="middle" class="label">GitHub · crvouga/dev-fleet</text>
81
+ <text x="219" y="967" text-anchor="middle" class="detail">Tracked runtime code + OpenTofu</text>
82
+ <text x="219" y="992" text-anchor="middle" class="detail">Reviewed main-branch CI deployment</text>
83
+ <path d="M 384 935 H 470 V 854" class="control"/>
84
+ <text x="488" y="900" class="edge-label">Pinned code install</text>
85
+ <path d="M 105 903 H 35 V 345 H 395" class="control"/>
86
+ <text x="122" y="330" class="edge-label">OpenTofu owns DNS + tunnel</text>
87
+ <path d="M 219 903 V 650" class="control"/>
88
+ <rect x="129" y="828" width="179" height="43" rx="6" fill="#f5f8fc"/>
89
+ <text x="219" y="847" text-anchor="middle" class="edge-label">OpenTofu owns</text>
90
+ <text x="219" y="865" text-anchor="middle" class="edge-label">credential delivery</text>
91
+
92
+ <rect x="680" y="903" width="220" height="112" rx="15" class="box"/>
93
+ <text x="790" y="938" text-anchor="middle" class="label">Restic</text>
94
+ <text x="790" y="967" text-anchor="middle" class="detail">15-minute checkpoints</text>
95
+ <text x="790" y="992" text-anchor="middle" class="detail">Runtime state + revision</text>
96
+ <path d="M 790 854 V 903" class="storage"/>
97
+ <rect x="1140" y="903" width="246" height="112" rx="15" class="box"/>
98
+ <text x="1263" y="938" text-anchor="middle" class="label">Cloudflare R2</text>
99
+ <text x="1263" y="967" text-anchor="middle" class="detail">Durable recovery source</text>
100
+ <text x="1263" y="992" text-anchor="middle" class="detail">cliproxy-fleet/restic</text>
101
+ <path d="M 900 959 H 1140" class="storage"/>
102
+ <text x="1020" y="941" text-anchor="middle" class="edge-label">S3-compatible API</text>
103
+
104
+ <path d="M 55 1065 H 107" class="traffic"/>
105
+ <text x="120" y="1070" class="detail">Inference traffic · responses return along the same path</text>
106
+ <path d="M 710 1065 H 762" class="control"/>
107
+ <text x="775" y="1070" class="detail">Configuration / credential delivery</text>
108
+ <text x="54" y="1100" class="detail">The public URL stays stable when the primary host moves. This workstation must remain online to serve requests.</text>
109
+ </svg>