@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,177 @@
1
+ #!/usr/bin/env python3
2
+ """Prepare a complete offline node cache atomically; never stop/start the live stack."""
3
+
4
+ import sys
5
+
6
+ sys.dont_write_bytecode = True
7
+
8
+ import pathlib as _pathlib
9
+
10
+ sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
11
+ import cliproxy_host
12
+
13
+ import argparse
14
+ import ctypes
15
+ import datetime
16
+ import json
17
+ import os
18
+ import pathlib
19
+ import stat
20
+ import shutil
21
+ import subprocess
22
+ import tempfile
23
+ import checkpoint
24
+ import cloud_state
25
+
26
+
27
+ def publish(candidate, destination):
28
+ """Atomic directory publication with no replacement, including races."""
29
+ library = ctypes.CDLL(None, use_errno=True)
30
+ if cliproxy_host.system() == "macos":
31
+ # renamex_np(from, to, RENAME_EXCL)
32
+ rename = library.renamex_np
33
+ rename.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint]
34
+ arguments = [os.fsencode(candidate), os.fsencode(destination), 0x4]
35
+ else:
36
+ # renameat2(AT_FDCWD, from, AT_FDCWD, to, RENAME_NOREPLACE)
37
+ rename = library.renameat2
38
+ rename.argtypes = [
39
+ ctypes.c_int,
40
+ ctypes.c_char_p,
41
+ ctypes.c_int,
42
+ ctypes.c_char_p,
43
+ ctypes.c_uint,
44
+ ]
45
+ arguments = [-100, os.fsencode(candidate), -100, os.fsencode(destination), 1]
46
+ rename.restype = ctypes.c_int
47
+ if rename(*arguments) != 0:
48
+ raise OSError(ctypes.get_errno(), "Recovery destination could not be published")
49
+
50
+
51
+ def verify_source(payload):
52
+ subprocess.run(
53
+ [
54
+ sys.executable,
55
+ "-B",
56
+ str(pathlib.Path(__file__).parent / "restore-verify"),
57
+ str(payload),
58
+ ],
59
+ check=True,
60
+ capture_output=True,
61
+ timeout=60,
62
+ )
63
+
64
+
65
+ def recover(
66
+ destination,
67
+ token_path,
68
+ restic_factory=cloud_state.client,
69
+ read_settings=cloud_state.settings,
70
+ ):
71
+ # Refuse even an empty existing directory: no user data or deployment can be overwritten.
72
+ live_root = cliproxy_host.default_root().resolve()
73
+ canonical_destination = destination.parent.resolve() / destination.name
74
+ if live_root.exists() and canonical_destination.is_relative_to(live_root):
75
+ raise ValueError("Recovery destination must be outside the existing deployment")
76
+ if destination.exists() or destination.is_symlink():
77
+ raise ValueError("Recovery destination already exists")
78
+ if not destination.parent.is_dir():
79
+ raise ValueError("Recovery parent directory must exist")
80
+ metadata = token_path.lstat()
81
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_mode & 0o077:
82
+ raise ValueError("Vault token must be a private regular file")
83
+ token = token_path.read_text().strip()
84
+ if not token:
85
+ raise ValueError("Existing Vault login required")
86
+ data = read_settings(token)
87
+ restic = restic_factory(data)
88
+ snapshot = cloud_state.latest(restic)
89
+ os.umask(0o077)
90
+ with tempfile.TemporaryDirectory(
91
+ prefix=".cliproxy-recover-", dir=destination.parent
92
+ ) as directory:
93
+ staging = pathlib.Path(directory)
94
+ restored = staging / "restored"
95
+ restic("restore", snapshot["id"], "--target", str(restored), "--verify")
96
+ payload = checkpoint.locate(restored)
97
+ # Validate the source manifest against trusted Git objects, never run restored code.
98
+ verify_source(payload)
99
+ repository = json.loads((payload / "state/repository.json").read_text())
100
+ if repository != {
101
+ key: data[key] for key in ("S3_ENDPOINT", "S3_BUCKET", "S3_REGION")
102
+ }:
103
+ raise ValueError("Recovered repository identity differs from Vault")
104
+ import yaml
105
+
106
+ config = yaml.safe_load((payload / "state/runtime/config.yaml").read_text())
107
+ if data["CLIPROXY_FLEET_CLIENT_API_KEY"] not in config["access"]["api-keys"]:
108
+ raise ValueError("Checkpoint client key differs from current Vault key")
109
+ candidate = staging / "candidate"
110
+ state = candidate / "state"
111
+ state.mkdir(parents=True, mode=0o700)
112
+ checkpoint.hydrate(payload, state)
113
+ secrets = state / "secrets"
114
+ secrets.mkdir(mode=0o700)
115
+ values = {
116
+ "vault-token": token,
117
+ "client-key": data["CLIPROXY_FLEET_CLIENT_API_KEY"],
118
+ "management-key": data["CLIPROXY_FLEET_MANAGEMENT_KEY"],
119
+ "restic-password": data["CLIPROXY_FLEET_RESTIC_PASSWORD"],
120
+ "recovery-confirmed": "Recovered independently from Vault.",
121
+ }
122
+ for name, value in values.items():
123
+ file = secrets / name
124
+ file.write_text(value + "\n")
125
+ file.chmod(0o600)
126
+ (state / "status").mkdir(mode=0o700)
127
+ gc_status = payload / "state/status/gc-success.json"
128
+ if gc_status.is_file():
129
+ shutil.copyfile(gc_status, state / "status/gc-success.json")
130
+ source = json.loads((payload / "INSTALLATION.json").read_text())
131
+ (candidate / "RECOVERY.json").write_text(
132
+ json.dumps(
133
+ {
134
+ "snapshot": snapshot["id"],
135
+ "source_revision": source["revision"],
136
+ "recovered_at": datetime.datetime.now(
137
+ datetime.timezone.utc
138
+ ).isoformat(),
139
+ "activated": False,
140
+ }
141
+ )
142
+ + "\n"
143
+ )
144
+ publish(candidate, destination)
145
+ return snapshot["id"]
146
+
147
+
148
+ def main():
149
+ parser = argparse.ArgumentParser(description=__doc__)
150
+ parser.add_argument(
151
+ "destination",
152
+ type=pathlib.Path,
153
+ help="New, nonexistent deployment/cache directory",
154
+ )
155
+ parser.add_argument(
156
+ "--vault-token-file",
157
+ type=pathlib.Path,
158
+ default=cliproxy_host.default_root()
159
+ / "state/secrets/vault-token",
160
+ )
161
+ args = parser.parse_args()
162
+ # Do not resolve the final component: an existing dangling symlink must be refused.
163
+ destination = args.destination.absolute()
164
+ recover(destination, args.vault_token_file)
165
+ print("Verified cloud recovery prepared at", destination)
166
+ print(
167
+ "No services or provider identities activated. RECOVERY.json records the source revision."
168
+ )
169
+
170
+
171
+ if __name__ == "__main__":
172
+ try:
173
+ main()
174
+ except Exception:
175
+ raise SystemExit(
176
+ "Offline recovery failed; live deployment unchanged. Credentials and upstream responses suppressed."
177
+ ) from None
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env python3
2
+ """Validate restored data with trusted repository code, without activating credentials."""
3
+
4
+ import hashlib
5
+ import importlib.util
6
+ import json
7
+ import pathlib
8
+ import subprocess
9
+ import sys
10
+
11
+ sys.dont_write_bytecode = True
12
+ root = pathlib.Path(sys.argv[1]).resolve()
13
+ source = pathlib.Path(__file__).resolve().parent
14
+ if not (root / "state/runtime/config.yaml").is_file():
15
+ import checkpoint
16
+ root = checkpoint.locate(root)
17
+ if (root / "CHECKPOINT.json").exists():
18
+ import checkpoint
19
+ checkpoint.validate(root)
20
+ if not (root / "state/runtime/config.yaml").is_file():
21
+ sys.exit("Pass the restored deployment root containing state/runtime/config.yaml.")
22
+ subprocess.run(
23
+ [
24
+ sys.executable,
25
+ "-B",
26
+ str(source / "validate-config"),
27
+ str(root / "state/runtime/config.yaml"),
28
+ ],
29
+ check=True,
30
+ )
31
+ count = 0
32
+ for file in (root / "state/runtime/auths").glob("*.json"):
33
+ try:
34
+ json.loads(file.read_text())
35
+ count += 1
36
+ except Exception:
37
+ sys.exit("Restored credential JSON is invalid; contents suppressed.")
38
+ manifest = root / "INSTALLATION.json"
39
+ if manifest.exists():
40
+ metadata = json.loads(manifest.read_text())
41
+ spec = importlib.util.spec_from_file_location("install", source / "install.py")
42
+ installer = importlib.util.module_from_spec(spec)
43
+ spec.loader.exec_module(installer)
44
+ _, files = installer.source_files(source.parents[1], metadata["revision"])
45
+ expected = {
46
+ name: hashlib.sha256(data).hexdigest() for name, (_, data) in files.items()
47
+ }
48
+ if expected != metadata["files"]:
49
+ sys.exit("Restored source manifest differs from its Git commit.")
50
+ locks = json.loads(files["image.lock.json"][1])
51
+ else:
52
+ locks = json.loads((root / "image.lock.json").read_text())
53
+ assert all("@sha256:" in image for image in locks.values())
54
+ assert (root / "state/runtime/static/management.html").is_file()
55
+ print(
56
+ "Restored config, Git source metadata, dashboard and",
57
+ count,
58
+ "credential files validated offline.",
59
+ )
60
+ print("No restored credentials or code were activated.")
@@ -0,0 +1,144 @@
1
+ """Bound disposable history. Call only while holding state/status/operation.lock."""
2
+
3
+ import datetime
4
+ import json
5
+ import pathlib
6
+ import re
7
+ import shutil
8
+ import stat
9
+ import subprocess
10
+ import sys
11
+ import time
12
+
13
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
14
+ import cliproxy_host
15
+
16
+
17
+ def forget(restic):
18
+ # Do not group by random staging paths or format-version tags: both grow forever.
19
+ restic(
20
+ "forget",
21
+ "--tag",
22
+ "cliproxy-fleet",
23
+ "--host",
24
+ "cliproxy-primary",
25
+ "--group-by",
26
+ "host",
27
+ "--keep-last",
28
+ "96",
29
+ "--keep-daily",
30
+ "7",
31
+ "--keep-weekly",
32
+ "5",
33
+ "--keep-monthly",
34
+ "12",
35
+ )
36
+
37
+
38
+ def prune(restic, now=None):
39
+ now = now or datetime.datetime.now(datetime.timezone.utc)
40
+ result = restic(
41
+ "snapshots",
42
+ "--json",
43
+ "--tag",
44
+ "cliproxy-checkpoint-v1",
45
+ "--host",
46
+ "cliproxy-primary",
47
+ stdout=subprocess.PIPE,
48
+ stderr=subprocess.PIPE,
49
+ text=True,
50
+ )
51
+ snapshots = json.loads(result.stdout)
52
+ times = [
53
+ datetime.datetime.fromisoformat(item["time"].replace("Z", "+00:00"))
54
+ for item in snapshots
55
+ ]
56
+ if not times or not 0 <= (now - max(times)).total_seconds() <= 3600:
57
+ raise ValueError(
58
+ "A successful cloud checkpoint within one hour is required for GC"
59
+ )
60
+ restic(
61
+ "check"
62
+ ) # Never delete history from a repository already failing integrity checks.
63
+ forget(restic)
64
+ # Physical collection applies only inside this dedicated Restic repository.
65
+ # No repack cap: a cap could let garbage arrive faster than it is reclaimed.
66
+ restic("prune", "--max-unused", "5%")
67
+ restic("check")
68
+
69
+
70
+ def staging(root, now=None):
71
+ now = time.time() if now is None else now
72
+ recovery = root / "recovery"
73
+ if not recovery.exists():
74
+ return 0
75
+ if recovery.is_symlink():
76
+ raise ValueError("Recovery directory must not be a link")
77
+ removed = 0
78
+ for path in recovery.iterdir():
79
+ if not re.fullmatch(r"(?:checkpoint|restore)-[a-z0-9_]{8}", path.name):
80
+ continue
81
+ metadata = path.lstat()
82
+ if stat.S_ISDIR(metadata.st_mode) and now - metadata.st_mtime > 86400:
83
+ # Python's fd-based rmtree does not traverse symlinks inside the tree.
84
+ if not shutil.rmtree.avoids_symlink_attacks:
85
+ raise ValueError("Safe directory deletion unavailable")
86
+ shutil.rmtree(path)
87
+ removed += 1
88
+ return removed
89
+
90
+
91
+ def revisions(root, references=None):
92
+ """Retain current and previous installed Git revisions; never force removal."""
93
+ current = json.loads((root / "INSTALLATION.json").read_text())
94
+ previous = root / "INSTALLATION.previous.json"
95
+ keep = {current["revision"]}
96
+ if previous.exists():
97
+ keep.add(json.loads(previous.read_text())["revision"])
98
+ parent = root / "revisions"
99
+ if references is None:
100
+ home = cliproxy_host.runtime_home()
101
+ units = cliproxy_host.backend().unit_directory
102
+ references = [
103
+ *(home / ".local/bin").glob("*-fleet"),
104
+ *units.glob("cliproxy-*"),
105
+ *units.glob("dev.devfleet.cliproxy-*"),
106
+ ]
107
+ for reference in references:
108
+ if reference.is_symlink():
109
+ try:
110
+ keep.add(reference.resolve().relative_to(parent.resolve()).parts[0])
111
+ except ValueError:
112
+ pass
113
+ active = parent / current["revision"]
114
+ if parent.is_symlink() or active.is_symlink() or not active.is_dir():
115
+ raise ValueError("Invalid installed worktree directory")
116
+
117
+ def git(path, *args):
118
+ return subprocess.check_output(
119
+ [
120
+ "git",
121
+ "--no-optional-locks",
122
+ "-c",
123
+ "safe.directory=" + str(path),
124
+ "-C",
125
+ str(path),
126
+ *args,
127
+ ],
128
+ stderr=subprocess.PIPE,
129
+ text=True,
130
+ ).strip()
131
+
132
+ removed = 0
133
+ for path in parent.iterdir():
134
+ if path.name in keep or not re.fullmatch(r"[0-9a-f]{40}", path.name):
135
+ continue
136
+ if path.is_symlink() or not path.is_dir():
137
+ continue
138
+ if git(path, "rev-parse", "HEAD") != path.name:
139
+ raise ValueError("Unexpected worktree revision")
140
+ if git(path, "status", "--porcelain", "--untracked-files=all", "--ignored"):
141
+ raise ValueError("Retired worktree has changes; refusing deletion")
142
+ git(active, "worktree", "remove", str(path))
143
+ removed += 1
144
+ return removed
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # Invoked as INSTALL_ROOT/scripts/service-start by systemd (Linux) or launchd (macOS).
4
+ cd "$(dirname "$0")/.."
5
+ export CLIPROXY_ROOT="$PWD"
6
+ ./scripts/audit.py
7
+ exec ./scripts/ops start
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+
4
+ sys.dont_write_bytecode = True
5
+
6
+ import pathlib as _pathlib
7
+
8
+ sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
9
+ import cliproxy_host
10
+
11
+ import pathlib, sys, yaml, os
12
+
13
+ arguments = [a for a in sys.argv[1:] if not a.startswith("--")]
14
+ require_fill_first = "--require-fill-first" in sys.argv[1:]
15
+ p = (
16
+ pathlib.Path(arguments[0])
17
+ if arguments
18
+ else cliproxy_host.default_root()
19
+ / "state/runtime/config.yaml"
20
+ )
21
+ try:
22
+ c = yaml.safe_load(p.read_text())
23
+
24
+ def require(condition):
25
+ if not condition:
26
+ raise ValueError("Unsafe configuration")
27
+
28
+ require(c["config-version"] == 8)
29
+ require(c["server"]["port"] == 8317)
30
+ require(c["oauth"]["auth-dir"] == "/state/auths")
31
+ management = c["management"]["secret-key"]
32
+ require(
33
+ isinstance(management, str)
34
+ and len(management) >= 32
35
+ and "REPLACE" not in management
36
+ )
37
+ keys = c["access"]["api-keys"]
38
+ require(
39
+ isinstance(keys, list)
40
+ and bool(keys)
41
+ and all(
42
+ isinstance(k, str) and len(k) >= 32 and "REPLACE" not in k for k in keys
43
+ )
44
+ )
45
+ require(management not in keys)
46
+ require(c["routing"]["strategy"] in ("round-robin", "weighted-round-robin", "fill-first"))
47
+ # Candidate configurations must keep the fill-first policy; the running config is only
48
+ # reported on drift (see scripts/monitor), so an older deployment still starts.
49
+ require(not require_fill_first or c["routing"]["strategy"] == "fill-first")
50
+ require(c.get("quota-exceeded", {}).get("switch-preview-model") is False)
51
+ require(c["observability"]["logs"]["debug"] is False)
52
+ require(c["observability"]["logs"]["request-log"] is False)
53
+ require(c["plugins"]["enabled"] is False)
54
+ except Exception:
55
+ sys.exit("Configuration failed YAML/security validation; values suppressed.")
56
+ print("YAML and deployment safety invariants passed (runtime staging still required).")
@@ -0,0 +1,57 @@
1
+ """Vault API helpers. Never print response bodies, tokens, or credentials."""
2
+
3
+ import sys
4
+
5
+ sys.dont_write_bytecode = True
6
+
7
+ import pathlib as _pathlib
8
+
9
+ sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
10
+ import cliproxy_host
11
+ import fleet_config
12
+
13
+
14
+ import json, pathlib, urllib.request, os
15
+
16
+ ROOT = cliproxy_host.default_root().resolve()
17
+
18
+
19
+ def request(path, token=None, data=None):
20
+ headers = {"Content-Type": "application/json"}
21
+ if token:
22
+ headers["X-Vault-Token"] = token
23
+ req = urllib.request.Request(
24
+ fleet_config.vault()["address"] + "/v1/" + path,
25
+ headers=headers,
26
+ data=json.dumps(data).encode() if data is not None else None,
27
+ )
28
+ with urllib.request.urlopen(req, timeout=30) as r:
29
+ return json.load(r)
30
+
31
+
32
+ def token():
33
+ return (ROOT / "state/secrets/vault-token").read_text().strip()
34
+
35
+
36
+ def credentials():
37
+ data = request(fleet_config.vault()["shared"], token())["data"]["data"]
38
+ fields = [
39
+ "S3_ENDPOINT",
40
+ "S3_REGION",
41
+ "S3_BUCKET",
42
+ "S3_ACCESS_KEY_ID",
43
+ "S3_SECRET_ACCESS_KEY",
44
+ ]
45
+ if any(not data.get(k) for k in fields):
46
+ raise ValueError("Missing S3 field")
47
+ return {k: data[k] for k in fields}
48
+
49
+
50
+ def renew():
51
+ t = token()
52
+ meta = request("auth/token/lookup-self", t)["data"]
53
+ if meta.get("ttl", 0) < 3600:
54
+ if not meta.get("renewable"):
55
+ raise ValueError("Token expiry requires login")
56
+ request("auth/token/renew-self", t, {})
57
+ return meta.get("ttl", 0)
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+
4
+ sys.dont_write_bytecode = True
5
+
6
+ import pathlib as _pathlib
7
+
8
+ sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
9
+ import cliproxy_host
10
+ import fleet_config
11
+
12
+ import getpass, importlib.util, os, pathlib, sys
13
+
14
+ root = cliproxy_host.default_root().resolve()
15
+ spec = importlib.util.spec_from_file_location(
16
+ "vault_access", root / "scripts/vault-access.py"
17
+ )
18
+ v = importlib.util.module_from_spec(spec)
19
+ spec.loader.exec_module(v)
20
+ os.umask(0o077)
21
+ for directory in [root / "state", root / "state/secrets", root / "state/status"]:
22
+ directory.mkdir(mode=0o700, parents=True, exist_ok=True)
23
+ if os.geteuid() == 0:
24
+ os.chown(directory, 1000, 1000)
25
+ try:
26
+ password = getpass.getpass("Vault password for " + fleet_config.vault()["user"] + " (hidden): ")
27
+ token = v.request("auth/userpass/login/" + fleet_config.vault()["user"], data={"password": password})[
28
+ "auth"
29
+ ]["client_token"]
30
+ password = None
31
+ data = v.request(fleet_config.vault()["shared"], token)["data"]["data"]
32
+ fields = [
33
+ "S3_ENDPOINT",
34
+ "S3_REGION",
35
+ "S3_BUCKET",
36
+ "S3_ACCESS_KEY_ID",
37
+ "S3_SECRET_ACCESS_KEY",
38
+ ]
39
+ if any(not data.get(k) for k in fields):
40
+ raise ValueError("Missing required S3 contract fields")
41
+ f = root / "state/secrets/vault-token"
42
+ new = f.with_suffix(".new")
43
+ new.write_text(token + "\n")
44
+ new.chmod(0o600)
45
+ os.replace(new, f)
46
+ if os.geteuid() == 0:
47
+ os.chown(f, 1000, 1000)
48
+ (root / "state/secrets/s3.json").unlink(missing_ok=True)
49
+ print(
50
+ "Saved Vault token securely. Each backup will fetch live S3 credentials; cached S3 credentials removed."
51
+ )
52
+ except Exception:
53
+ sys.exit(
54
+ "Vault login/read failed. Check credentials and access to the shared document; response suppressed."
55
+ )
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python3
2
+ """Read existing recovery secrets; all Vault writes belong to the OpenTofu root."""
3
+
4
+ import sys
5
+
6
+ sys.dont_write_bytecode = True
7
+
8
+ import pathlib as _pathlib
9
+
10
+ sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
11
+ import cliproxy_host
12
+ import fleet_config
13
+
14
+
15
+ import hmac
16
+ import importlib.util
17
+ import os
18
+ import pathlib
19
+ import sys
20
+
21
+ root = cliproxy_host.default_root().resolve()
22
+ spec = importlib.util.spec_from_file_location(
23
+ "vault_access", root / "scripts/vault-access.py"
24
+ )
25
+ vault = importlib.util.module_from_spec(spec)
26
+ spec.loader.exec_module(vault)
27
+ os.umask(0o077)
28
+ try:
29
+ if sys.argv[1:] not in [["recover"], ["verify"]]:
30
+ sys.exit(
31
+ "Usage: vault-recovery recover|verify. Manage secret values through bun run vault:edit."
32
+ )
33
+ secret = vault.request(fleet_config.vault()["shared"], vault.token())["data"]["data"][
34
+ "CLIPROXY_FLEET_RESTIC_PASSWORD"
35
+ ]
36
+ local = root / "state/secrets/restic-password"
37
+ if sys.argv[1] == "recover":
38
+ if local.exists() and not hmac.compare_digest(
39
+ local.read_text().strip(), secret
40
+ ):
41
+ sys.exit("Local recovery password differs; refusing to overwrite it.")
42
+ local.write_text(secret + "\n")
43
+ local.chmod(0o600)
44
+ if os.geteuid() == 0:
45
+ os.chown(local, 1000, 1000)
46
+ if not hmac.compare_digest(local.read_text().strip(), secret):
47
+ sys.exit("Recovery password mismatch; values suppressed.")
48
+ marker = root / "state/secrets/recovery-confirmed"
49
+ marker.write_text("Verified existing independent Vault recovery copy.\n")
50
+ if os.geteuid() == 0:
51
+ os.chown(marker, 1000, 1000)
52
+ print("Independent Vault recovery copy verified; no secrets printed.")
53
+ except Exception:
54
+ sys.exit(
55
+ "Vault recovery failed. Check login, registered key and read access; response suppressed."
56
+ )
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+
4
+ sys.dont_write_bytecode = True
5
+
6
+ import pathlib as _pathlib
7
+
8
+ sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
9
+ import cliproxy_host
10
+
11
+ import importlib.util, pathlib, sys, os
12
+
13
+ root = cliproxy_host.default_root().resolve()
14
+ spec = importlib.util.spec_from_file_location(
15
+ "vault_access", root / "scripts/vault-access.py"
16
+ )
17
+ v = importlib.util.module_from_spec(spec)
18
+ spec.loader.exec_module(v)
19
+ try:
20
+ v.renew()
21
+ (root / "state/status/vault-failed").unlink(missing_ok=True)
22
+ print("Vault token valid; renewal checked.")
23
+ except Exception:
24
+ f = root / "state/status/vault-failed"
25
+ f.write_text("Vault login/renewal required.\n")
26
+ if os.geteuid() == 0:
27
+ os.chown(f, 1000, 1000)
28
+ sys.exit(
29
+ "Vault token unavailable, expired, or unable to renew. Run vault-backup-login."
30
+ )
@@ -0,0 +1,10 @@
1
+ [Unit]
2
+ Description=Verify CLIProxyAPI encrypted backup integrity
3
+ After=network-online.target
4
+ [Service]
5
+ Environment=PYTHONDONTWRITEBYTECODE=1
6
+ Type=oneshot
7
+ ExecStart=/opt/cliproxy-fleet/scripts/backup check
8
+ UMask=0077
9
+ Nice=10
10
+ TimeoutStartSec=4h
@@ -0,0 +1,8 @@
1
+ [Unit]
2
+ Description=Weekly Restic repository and 10 percent data integrity check
3
+ [Timer]
4
+ OnCalendar=Sun *-*-* 05:00:00
5
+ RandomizedDelaySec=30m
6
+ Persistent=true
7
+ [Install]
8
+ WantedBy=timers.target
@@ -0,0 +1,13 @@
1
+ [Unit]
2
+ Description=Consistent encrypted CLIProxyAPI backup to independent R2 storage
3
+ Requires=docker.service
4
+ After=docker.service network-online.target
5
+ [Service]
6
+ Environment=PYTHONDONTWRITEBYTECODE=1
7
+ Type=oneshot
8
+ WorkingDirectory=/opt/cliproxy-fleet
9
+ ExecStart=/opt/cliproxy-fleet/scripts/backup backup
10
+ UMask=0077
11
+ Nice=10
12
+ IOSchedulingClass=idle
13
+ TimeoutStartSec=1h