@crvouga/dev-fleet 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +157 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +128 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +17 -0
  33. package/ansible/monitoring/README.md +154 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +134 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/lock.json +11 -0
  40. package/ansible/net/remote-probe.sh +13 -0
  41. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  42. package/ansible/ubuntu/baseline.yml +89 -0
  43. package/ansible/ubuntu/disk-guard.py +104 -0
  44. package/ansible/ubuntu/policy.json +43 -0
  45. package/ansible/ubuntu/recovery.yml +15 -0
  46. package/ansible/ubuntu/t3/policy.json +7 -0
  47. package/ansible/ubuntu/t3-desktop.yml +220 -0
  48. package/ansible/ubuntu/t3-health.sh +33 -0
  49. package/ansible/ubuntu/t3-policy.yml +76 -0
  50. package/ansible/ubuntu/t3-route.yml +28 -0
  51. package/ansible/ubuntu/tailscale.yml +75 -0
  52. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  53. package/ansible/ubuntu/tasks/docker.yml +31 -0
  54. package/ansible/ubuntu/tasks/packages.yml +56 -0
  55. package/ansible/ubuntu/tasks/power.yml +51 -0
  56. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  57. package/ansible/ubuntu/tasks/resources.yml +252 -0
  58. package/ansible/ubuntu/tasks/services.yml +31 -0
  59. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  60. package/ansible/ubuntu/tasks/user.yml +120 -0
  61. package/bin/devfleet.mjs +36 -0
  62. package/bootstrap/bootstrap.ps1 +50 -0
  63. package/bootstrap/bootstrap.sh +73 -0
  64. package/bootstrap/devfleet +7 -0
  65. package/bootstrap/docker-desktop.sh +33 -0
  66. package/bootstrap/doctor.ts +189 -0
  67. package/bootstrap/fleet-manager-run.sh +76 -0
  68. package/bootstrap/fleet-remote-bootstrap.sh +185 -0
  69. package/bootstrap/global-config.ts +113 -0
  70. package/bootstrap/install-launcher.sh +7 -0
  71. package/bootstrap/native-aws.ts +86 -0
  72. package/bootstrap/native-t3.ts +154 -0
  73. package/bootstrap/npm.ts +350 -0
  74. package/bootstrap/setup-status.ts +51 -0
  75. package/bootstrap/setup.ts +88 -0
  76. package/bootstrap/shell-path.ts +105 -0
  77. package/bootstrap/verify-t3-archive.py +49 -0
  78. package/bootstrap/worker-auth.ts +65 -0
  79. package/bootstrap/worker-github.ts +47 -0
  80. package/bootstrap/worker-tools.ts +24 -0
  81. package/cliproxy/AGENTS.md +11 -0
  82. package/cliproxy/README.md +269 -0
  83. package/cliproxy/clients/bootstrap.py +143 -0
  84. package/cliproxy/clients/launcher.py +57 -0
  85. package/cliproxy/compose.yaml +90 -0
  86. package/cliproxy/config.template.yaml +70 -0
  87. package/cliproxy/docs/cloud-adoption.md +16 -0
  88. package/cliproxy/docs/deployment.png +0 -0
  89. package/cliproxy/docs/deployment.svg +109 -0
  90. package/cliproxy/docs/health-workflow.example.yml +185 -0
  91. package/cliproxy/docs/migration.md +131 -0
  92. package/cliproxy/docs/model-health-report.schema.json +364 -0
  93. package/cliproxy/docs/model-health.md +393 -0
  94. package/cliproxy/image.lock.json +5 -0
  95. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  96. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  97. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  98. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  99. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  101. package/cliproxy/models.json +1618 -0
  102. package/cliproxy/requirements.lock +78 -0
  103. package/cliproxy/scripts/accounts.py +38 -0
  104. package/cliproxy/scripts/audit.py +123 -0
  105. package/cliproxy/scripts/backup +234 -0
  106. package/cliproxy/scripts/check.py +34 -0
  107. package/cliproxy/scripts/checkpoint.py +226 -0
  108. package/cliproxy/scripts/cli.py +64 -0
  109. package/cliproxy/scripts/cliproxy_host.py +257 -0
  110. package/cliproxy/scripts/cloud_state.py +150 -0
  111. package/cliproxy/scripts/container-health +6 -0
  112. package/cliproxy/scripts/docker +24 -0
  113. package/cliproxy/scripts/external-health.py +79 -0
  114. package/cliproxy/scripts/fleet_config.py +43 -0
  115. package/cliproxy/scripts/fleet_sync.py +274 -0
  116. package/cliproxy/scripts/initialize +84 -0
  117. package/cliproxy/scripts/install-dependencies +48 -0
  118. package/cliproxy/scripts/install.py +406 -0
  119. package/cliproxy/scripts/model_health.py +16 -0
  120. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  121. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  122. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  123. package/cliproxy/scripts/modelhealth/config.py +186 -0
  124. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  125. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  126. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  127. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  128. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  129. package/cliproxy/scripts/modelhealth/report.py +150 -0
  130. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  131. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  132. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  133. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  134. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  135. package/cliproxy/scripts/monitor +68 -0
  136. package/cliproxy/scripts/ops +527 -0
  137. package/cliproxy/scripts/public-endpoint +127 -0
  138. package/cliproxy/scripts/recover_node.py +177 -0
  139. package/cliproxy/scripts/restore-verify +60 -0
  140. package/cliproxy/scripts/retention.py +144 -0
  141. package/cliproxy/scripts/service-start +7 -0
  142. package/cliproxy/scripts/validate-config +56 -0
  143. package/cliproxy/scripts/vault-access.py +57 -0
  144. package/cliproxy/scripts/vault-backup-login +55 -0
  145. package/cliproxy/scripts/vault-recovery +56 -0
  146. package/cliproxy/scripts/vault-renew +30 -0
  147. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  148. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  149. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  150. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  151. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  152. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  153. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  155. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  156. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  157. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  158. package/cliproxy/vendor/LICENSE +22 -0
  159. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  160. package/examples/fleet.json +95 -0
  161. package/examples/roster.json +13 -0
  162. package/mise.lock +1492 -0
  163. package/mise.toml +99 -0
  164. package/package.json +65 -4
  165. package/schema/fleet.schema.json +364 -0
  166. package/schema/roster.schema.json +77 -0
  167. package/src/agents/account-relay.ts +111 -0
  168. package/src/agents/accounts.ts +85 -0
  169. package/src/agents/audit.ts +77 -0
  170. package/src/agents/cli.ts +13 -0
  171. package/src/agents/command.ts +204 -0
  172. package/src/agents/converge.ts +123 -0
  173. package/src/agents/doctor.ts +107 -0
  174. package/src/agents/errors.ts +2 -0
  175. package/src/agents/files.ts +73 -0
  176. package/src/agents/fleet-verify.ts +156 -0
  177. package/src/agents/launch.ts +290 -0
  178. package/src/agents/native-verify.ts +226 -0
  179. package/src/agents/provider-errors.ts +50 -0
  180. package/src/agents/registry.ts +55 -0
  181. package/src/agents/render.ts +110 -0
  182. package/src/agents/round-trip.ts +192 -0
  183. package/src/agents/secrets.ts +13 -0
  184. package/src/agents/toolchain.ts +34 -0
  185. package/src/agents/verify-cache.ts +103 -0
  186. package/src/agents/verify.ts +127 -0
  187. package/src/auth-checks.ts +163 -0
  188. package/src/cli/agent-guide.ts +95 -0
  189. package/src/cli/app.ts +115 -0
  190. package/src/cli/cli.ts +21 -0
  191. package/src/cli/command-index.ts +50 -0
  192. package/src/cli/contracts.ts +25 -0
  193. package/src/cli/fleet-reference.ts +17 -0
  194. package/src/cli/fleet-sync-checkout.ts +91 -0
  195. package/src/cli/fleet-sync-enrollment.ts +144 -0
  196. package/src/cli/fleet-sync-forward.ts +134 -0
  197. package/src/cli/fleet-sync-manager.ts +193 -0
  198. package/src/cli/fleet-sync-monitoring.ts +185 -0
  199. package/src/cli/fleet-sync-output.ts +314 -0
  200. package/src/cli/fleet-sync-privilege.ts +27 -0
  201. package/src/cli/fleet-sync-progress.ts +109 -0
  202. package/src/cli/fleet-sync-remote.ts +236 -0
  203. package/src/cli/fleet-sync-report.ts +26 -0
  204. package/src/cli/fleet-sync-schedule.ts +138 -0
  205. package/src/cli/fleet-sync-vault.ts +166 -0
  206. package/src/cli/fleet-sync.ts +309 -0
  207. package/src/cli/fleet.ts +163 -0
  208. package/src/cli/managed-health.ts +219 -0
  209. package/src/cli/node-deployment.ts +34 -0
  210. package/src/cli/node-sync.ts +114 -0
  211. package/src/cli/node-t3.ts +31 -0
  212. package/src/cli/options.ts +141 -0
  213. package/src/cli/probe-source.ts +7 -0
  214. package/src/cli/program.ts +169 -0
  215. package/src/cli/register-fleet.ts +246 -0
  216. package/src/cli/registry.ts +158 -0
  217. package/src/cli/repair-apply.ts +67 -0
  218. package/src/cli/repair-guard.ts +11 -0
  219. package/src/cli/repair-inspect.ts +150 -0
  220. package/src/cli/repair-script.ts +76 -0
  221. package/src/cli/repair.ts +209 -0
  222. package/src/cli/roster.ts +89 -0
  223. package/src/cli/search.ts +35 -0
  224. package/src/cli/settings.ts +64 -0
  225. package/src/cli/source-operations.ts +78 -0
  226. package/src/cli/t3/command.ts +90 -0
  227. package/src/cli/t3/register.ts +55 -0
  228. package/src/cli/tailnet.ts +127 -0
  229. package/src/cli/ui-actions.ts +133 -0
  230. package/src/cli/ui-controller.ts +113 -0
  231. package/src/cli/ui-data.ts +106 -0
  232. package/src/cli/ui-execute.ts +163 -0
  233. package/src/cli/ui-repairs.ts +34 -0
  234. package/src/cli/ui-reports.ts +211 -0
  235. package/src/cli/ui-targets.ts +100 -0
  236. package/src/cli/ui-terminal.ts +109 -0
  237. package/src/cli/ui-types.ts +49 -0
  238. package/src/cli/ui-wizard.ts +101 -0
  239. package/src/cli/ui.ts +9 -0
  240. package/src/cli/worker-health-output.ts +37 -0
  241. package/src/config.ts +186 -0
  242. package/src/configure.ts +255 -0
  243. package/src/definition.ts +260 -0
  244. package/src/diagnostics.ts +92 -0
  245. package/src/doctor-check.ts +10 -0
  246. package/src/errors.ts +1 -0
  247. package/src/fingerprint.ts +120 -0
  248. package/src/fleet-config.ts +88 -0
  249. package/src/index.ts +6 -0
  250. package/src/input-hashes.ts +73 -0
  251. package/src/machine/local-source.ts +52 -0
  252. package/src/machine/lock-error.ts +2 -0
  253. package/src/machine/platform.ts +44 -0
  254. package/src/machine/source.ts +68 -0
  255. package/src/machine/state.ts +185 -0
  256. package/src/machine-health.ts +95 -0
  257. package/src/managed-source.ts +47 -0
  258. package/src/manager-failure.ts +48 -0
  259. package/src/manager-sync.ts +400 -0
  260. package/src/manager.ts +62 -0
  261. package/src/monitoring/apply.ts +171 -0
  262. package/src/monitoring/beszel.ts +191 -0
  263. package/src/monitoring/enroll.ts +110 -0
  264. package/src/monitoring/hub-access.ts +113 -0
  265. package/src/monitoring/hub-viewer.ts +142 -0
  266. package/src/monitoring/hub.ts +93 -0
  267. package/src/monitoring/manager-agent.ts +73 -0
  268. package/src/monitoring/manager-hub.ts +232 -0
  269. package/src/monitoring/remote.ts +65 -0
  270. package/src/network-definition.ts +83 -0
  271. package/src/node-converge-linux.ts +372 -0
  272. package/src/node-converge-shared.ts +83 -0
  273. package/src/node-converge.ts +40 -0
  274. package/src/onboard.ts +35 -0
  275. package/src/platform/ansible-receipt.ts +128 -0
  276. package/src/platform/ansible.ts +62 -0
  277. package/src/platform/darwin.ts +203 -0
  278. package/src/platform/detect.ts +11 -0
  279. package/src/platform/fallback.ts +110 -0
  280. package/src/platform/index.ts +38 -0
  281. package/src/platform/launchd.ts +98 -0
  282. package/src/platform/linux.ts +139 -0
  283. package/src/platform/ps.ts +45 -0
  284. package/src/platform/resource-verify.ts +128 -0
  285. package/src/platform/systemd-user.ts +95 -0
  286. package/src/platform/t3-policy.ts +137 -0
  287. package/src/platform/t3-route.ts +68 -0
  288. package/src/platform/t3-service.ts +75 -0
  289. package/src/platform/types.ts +123 -0
  290. package/src/platform/ubuntu-audit.ts +63 -0
  291. package/src/platform/ubuntu-verify.ts +105 -0
  292. package/src/platform/windows.ts +148 -0
  293. package/src/power-recovery.ts +55 -0
  294. package/src/probe.ts +51 -0
  295. package/src/protocol.ts +119 -0
  296. package/src/proxy-converge.ts +85 -0
  297. package/src/remote-bootstrap.ts +212 -0
  298. package/src/remote-entry.ts +89 -0
  299. package/src/remote-lock.ts +12 -0
  300. package/src/repair-catalog.ts +206 -0
  301. package/src/repair-plan.ts +78 -0
  302. package/src/repair-runner.ts +84 -0
  303. package/src/report.ts +71 -0
  304. package/src/runner.ts +104 -0
  305. package/src/secrets.ts +169 -0
  306. package/src/service-auth.ts +115 -0
  307. package/src/ssh.ts +135 -0
  308. package/src/sync.ts +104 -0
  309. package/src/t3/accounts.ts +81 -0
  310. package/src/t3/desired.ts +165 -0
  311. package/src/t3/files.ts +81 -0
  312. package/src/t3/names-check.ts +53 -0
  313. package/src/t3/names.ts +155 -0
  314. package/src/t3/plan.ts +82 -0
  315. package/src/t3/providers.ts +74 -0
  316. package/src/t3/reconcile.ts +123 -0
  317. package/src/t3-endpoint.ts +48 -0
  318. package/src/t3-linked-check.ts +101 -0
  319. package/src/tailnet-health.ts +231 -0
  320. package/src/tailnet-matching.ts +37 -0
  321. package/src/tailscale.ts +141 -0
  322. package/src/test-fixtures.ts +36 -0
  323. package/src/toolchain-checks.ts +83 -0
  324. package/src/topology.ts +24 -0
  325. package/src/transport.ts +122 -0
  326. package/src/types.ts +95 -0
  327. package/src/vault-config.ts +38 -0
  328. package/src/worker-access-checks.ts +115 -0
  329. package/src/worker-capacity.ts +34 -0
  330. package/src/worker-host-checks.ts +176 -0
  331. package/src/worker-policy.ts +64 -0
  332. package/src/worker-readiness.ts +64 -0
  333. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,63 @@
1
+ import policy from '../../ansible/ubuntu/policy.json';
2
+ import type { Runner } from '../types';
3
+
4
+ export interface UbuntuAudit {
5
+ supported: boolean;
6
+ release: string | null;
7
+ architecture: string | null;
8
+ packages: Record<string, string>;
9
+ desktop: boolean;
10
+ services: Record<string, boolean>;
11
+ passwordlessSudo: boolean;
12
+ rebootPending: boolean;
13
+ }
14
+ const PACKAGES = [...Object.keys(policy.packages), 'tailscale'];
15
+
16
+ /** Read-only audit: never emit package sources, environment, accounts or raw errors. */
17
+ export async function auditUbuntu(run: Runner): Promise<UbuntuAudit> {
18
+ const release = await run(['cat', '/etc/os-release']);
19
+ const arch = await run(['dpkg', '--print-architecture']);
20
+ const packages = await run([
21
+ 'dpkg-query',
22
+ '-W',
23
+ '-f=${Package}\t${Version}\n',
24
+ ...PACKAGES,
25
+ ]);
26
+ const services: Record<string, boolean> = {};
27
+ for (const name of ['ssh', 'tailscaled', 'beszel-agent', 'docker']) {
28
+ services[name] = (
29
+ await run(['systemctl', 'is-active', `${name}.service`])
30
+ ).ok;
31
+ }
32
+ const version =
33
+ /^VERSION_ID="?([0-9]+\.[0-9]+)"?$/m.exec(release.stdout)?.[1] ?? null;
34
+ const architecture = /^(amd64|arm64)\s*$/.exec(arch.stdout)?.[1] ?? null;
35
+ return {
36
+ supported:
37
+ release.ok &&
38
+ /^ID=ubuntu$/m.test(release.stdout) &&
39
+ version === '26.04' &&
40
+ architecture !== null,
41
+ release: version,
42
+ architecture,
43
+ packages: packageVersions(packages.stdout),
44
+ desktop: (await run(['test', '-d', '/usr/share/xsessions'])).ok,
45
+ services,
46
+ passwordlessSudo: (await run(['sudo', '-n', 'true'])).ok,
47
+ rebootPending: (await run(['test', '-f', '/var/run/reboot-required'])).ok,
48
+ };
49
+ }
50
+ function packageVersions(output: string): Record<string, string> {
51
+ const versions: Record<string, string> = {};
52
+ for (const line of output.split('\n')) {
53
+ const [name, version] = line.split('\t');
54
+ if (
55
+ name &&
56
+ PACKAGES.includes(name) &&
57
+ version &&
58
+ /^[a-zA-Z0-9.+:~_-]{1,100}$/.test(version)
59
+ )
60
+ versions[name] = version;
61
+ }
62
+ return versions;
63
+ }
@@ -0,0 +1,105 @@
1
+ import type { Check, Runner } from '../types';
2
+
3
+ const LOGIND = {
4
+ HandleLidSwitch: 's "ignore"',
5
+ HandleLidSwitchDocked: 's "ignore"',
6
+ HandleLidSwitchExternalPower: 's "ignore"',
7
+ IdleAction: 's "ignore"',
8
+ KillUserProcesses: 'b false',
9
+ StopIdleSessionUSec: 't 18446744073709551615',
10
+ };
11
+ const POWER_FILES = [
12
+ '/etc/systemd/logind.conf.d/60-personal-fleet.conf',
13
+ '/etc/systemd/sleep.conf.d/60-personal-fleet.conf',
14
+ ];
15
+ /** Inspect the running manager, not an installer receipt. Reboot acceptance
16
+ * additionally requires the managed files to predate this kernel boot.
17
+ */
18
+ export async function verifyUbuntuBoot(
19
+ user: string,
20
+ run: Runner
21
+ ): Promise<Check[]> {
22
+ for (const [property, expected] of Object.entries(LOGIND)) {
23
+ const observed = await run([
24
+ 'busctl',
25
+ 'get-property',
26
+ 'org.freedesktop.login1',
27
+ '/org/freedesktop/login1',
28
+ 'org.freedesktop.login1.Manager',
29
+ property,
30
+ ]);
31
+ if (!observed.ok || observed.stdout.trim() !== expected)
32
+ return pending(
33
+ 'UBUNTU_SESSION_POLICY_PENDING',
34
+ 'The running login manager does not expose the declared ' +
35
+ property +
36
+ '. Keep active work intact and schedule an explicit maintenance reboot; do not restart logind during sessions.'
37
+ );
38
+ }
39
+ for (const name of [
40
+ 'sleep',
41
+ 'suspend',
42
+ 'hibernate',
43
+ 'hybrid-sleep',
44
+ 'suspend-then-hibernate',
45
+ ]) {
46
+ const observed = await run(['systemctl', 'is-enabled', name + '.target']);
47
+ if (observed.stdout.trim() !== 'masked')
48
+ return pending(
49
+ 'UBUNTU_SLEEP_POLICY_DRIFT',
50
+ 'A declared sleep transition is not masked. Reapply the approved Ubuntu baseline; preserve user sessions.'
51
+ );
52
+ }
53
+ const linger = await run([
54
+ 'loginctl',
55
+ 'show-user',
56
+ user,
57
+ '--property=Linger',
58
+ '--value',
59
+ ]);
60
+ if (!linger.ok || linger.stdout.trim() !== 'yes')
61
+ return pending(
62
+ 'UBUNTU_LINGER_REQUIRED',
63
+ 'The designated user is not observed as lingering. Reapply the approved user policy before claiming logout survival.'
64
+ );
65
+ if (!(await filesPredateBoot(run)))
66
+ return pending(
67
+ 'UBUNTU_MAINTENANCE_REBOOT_PENDING',
68
+ 'Effective session policy matches, but the managed power files were not observed before this boot. Perform an explicit maintenance reboot and rerun the manager command.'
69
+ );
70
+ return [
71
+ {
72
+ id: 'ubuntu:boot-policy',
73
+ status: 'pass',
74
+ summary:
75
+ 'Managed power files predate this boot; the running login manager exposes the declared no-sleep/session policy, sleep transitions are masked, and designated-user lingering is active.',
76
+ },
77
+ ];
78
+ }
79
+ async function filesPredateBoot(run: Runner): Promise<boolean> {
80
+ const boot = await run(['cat', '/proc/stat']);
81
+ const seconds = Number(/^btime (\d+)$/m.exec(boot.stdout)?.[1]);
82
+ if (!boot.ok || !Number.isSafeInteger(seconds) || seconds <= 0) return false;
83
+ for (const path of POWER_FILES) {
84
+ const observed = await run(['stat', '--format=%Y', path]);
85
+ const modified = Number(observed.stdout.trim());
86
+ if (
87
+ !observed.ok ||
88
+ !Number.isSafeInteger(modified) ||
89
+ modified <= 0 ||
90
+ modified >= seconds
91
+ )
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+ function pending(code: string, action: string): Check[] {
97
+ return [
98
+ {
99
+ id: 'ubuntu:boot-policy',
100
+ status: 'fail',
101
+ summary: code,
102
+ remedy: { action },
103
+ },
104
+ ];
105
+ }
@@ -0,0 +1,148 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { execFileSync, spawnSync } from 'node:child_process';
4
+
5
+ import type {
6
+ NotificationKind,
7
+ NotifierCapability,
8
+ NotifierTestResult,
9
+ Platform,
10
+ ProcessInfo,
11
+ ScheduledJob,
12
+ ScheduledJobStatus,
13
+ } from './types';
14
+ import { existsSync } from 'node:fs';
15
+
16
+ export class WindowsPlatform implements Platform {
17
+ readonly name = 'windows' as const;
18
+ readonly label = 'Windows';
19
+
20
+ opencodeDir(): string {
21
+ const base =
22
+ process.env['APPDATA']?.trim() || join(homedir(), 'AppData/Roaming');
23
+ return join(base, 'opencode');
24
+ }
25
+
26
+ cacheDir(): string {
27
+ const base =
28
+ process.env['LOCALAPPDATA']?.trim() || join(homedir(), 'AppData/Local');
29
+ return join(base, 'opencode-cache');
30
+ }
31
+
32
+ globalBinDir(): string {
33
+ return join(homedir(), '.local/bin');
34
+ }
35
+
36
+ canBuildSwiftNotifier(): boolean {
37
+ return false;
38
+ }
39
+
40
+ shellRcFiles(): readonly string[] {
41
+ return [];
42
+ }
43
+
44
+ notifierCapabilities(): NotifierCapability {
45
+ return {
46
+ native: true,
47
+ sounds: false,
48
+ clickToFocus: false,
49
+ reason:
50
+ 'Windows uses a PowerShell toast fallback (no sounds, no click-to-focus); the macOS OpenCodeNotifier app is macOS-only.',
51
+ };
52
+ }
53
+
54
+ async postNotification(input: {
55
+ title: string;
56
+ message: string;
57
+ kind: NotificationKind;
58
+ }): Promise<NotifierTestResult> {
59
+ void input.kind;
60
+ try {
61
+ const ps = [
62
+ '-NoProfile',
63
+ '-NonInteractive',
64
+ '-Command',
65
+ `New-BurntToastNotification -Text ${JSON.stringify(input.title)}, ${JSON.stringify(input.message)}`,
66
+ ];
67
+ execFileSync('powershell', ps, { stdio: 'ignore' });
68
+ return { ok: true, detail: 'posted via BurntToast' };
69
+ } catch {
70
+ // Fall through to msg.exe.
71
+ }
72
+ const r = spawnSync('msg', ['*', `${input.title}: ${input.message}`], {
73
+ stdio: 'ignore',
74
+ });
75
+ if (r.status === 0) return { ok: true, detail: 'posted via msg' };
76
+ return {
77
+ ok: false,
78
+ detail:
79
+ 'no notifier found (install BurntToast: Install-Module BurntToast)',
80
+ };
81
+ }
82
+
83
+ isNotifierDaemonRunning(): boolean {
84
+ return false;
85
+ }
86
+
87
+ restartNotifierDaemon(): void {
88
+ // No daemon on Windows — no-op.
89
+ }
90
+
91
+ availableSystemSounds(): string[] {
92
+ return [];
93
+ }
94
+
95
+ async playSystemSound(name: string): Promise<NotifierTestResult> {
96
+ void name;
97
+ return {
98
+ ok: false,
99
+ detail: 'sounds unsupported on Windows (toast fallback has no sound API)',
100
+ };
101
+ }
102
+
103
+ listProcesses(): ProcessInfo[] | null {
104
+ return null;
105
+ }
106
+
107
+ tailscaleAppBinaries(): readonly string[] {
108
+ return ['C:\\Program Files\\Tailscale\\tailscale.exe'].filter((p) =>
109
+ existsSync(p)
110
+ );
111
+ }
112
+
113
+ vscodeSettingsPath(): string | null {
114
+ const base =
115
+ process.env['APPDATA']?.trim() || join(homedir(), 'AppData/Roaming');
116
+ return join(base, 'Code/User/settings.json');
117
+ }
118
+
119
+ vscodeCliBinaries(): readonly string[] {
120
+ const base =
121
+ process.env['LOCALAPPDATA']?.trim() || join(homedir(), 'AppData/Local');
122
+ return [join(base, 'Programs/Microsoft VS Code/bin/code.cmd')].filter((p) =>
123
+ existsSync(p)
124
+ );
125
+ }
126
+
127
+ installScheduledJob(job: ScheduledJob): NotifierTestResult {
128
+ return {
129
+ ok: false,
130
+ detail: `scheduled jobs are not supported on ${this.label} (${job.label})`,
131
+ };
132
+ }
133
+
134
+ removeScheduledJob(label: string): NotifierTestResult {
135
+ return {
136
+ ok: true,
137
+ detail: `nothing to remove for ${label} on ${this.label}`,
138
+ };
139
+ }
140
+
141
+ scheduledJobStatus(label: string): ScheduledJobStatus {
142
+ return {
143
+ supported: false,
144
+ installed: false,
145
+ detail: `not supported on ${this.label} (${label})`,
146
+ };
147
+ }
148
+ }
@@ -0,0 +1,55 @@
1
+ import { record } from './config';
2
+ import type { Check, Runner } from './types';
3
+
4
+ /** Only recognize named firmware settings; an enabled OS service is not proof of AC recovery. */
5
+ export async function powerRecoveryCheck(run: Runner): Promise<Check> {
6
+ const result = await run(
7
+ ['sudo', '-n', 'fwupdmgr', 'get-bios-settings', '--json'],
8
+ { timeoutMs: 15_000 }
9
+ );
10
+ try {
11
+ const data: unknown = JSON.parse(result.stdout);
12
+ const settings =
13
+ record(data) && Array.isArray(data['BiosSettings'])
14
+ ? data['BiosSettings']
15
+ : [];
16
+ const observed = settings.find(
17
+ (item) =>
18
+ record(item) &&
19
+ [
20
+ 'RestoreACPowerLoss',
21
+ 'AfterPowerLoss',
22
+ 'PowerOnAfterPowerFail',
23
+ 'ACPowerRecovery',
24
+ ].includes(String(item['Name']))
25
+ );
26
+ if (result.ok && record(observed)) {
27
+ const enabled = ['Power On', 'Always On', 'PowerOn', 'On'].includes(
28
+ String(observed['CurrentValue'])
29
+ );
30
+ return {
31
+ id: 'worker:power-loss-recovery',
32
+ status: enabled ? 'pass' : 'fail',
33
+ summary: enabled
34
+ ? 'Firmware reports an enabled restore-after-AC-loss setting; a real power-loss test remains distinct from configuration evidence.'
35
+ : 'Firmware restore-after-AC-loss is disabled.',
36
+ remedy: {
37
+ action:
38
+ 'Enable the machine firmware restore-after-AC-loss setting or configure its VM host power recovery.',
39
+ },
40
+ };
41
+ }
42
+ } catch {
43
+ /* Firmware output may contain hardware identifiers; never expose it. */
44
+ }
45
+ return {
46
+ id: 'worker:power-loss-recovery',
47
+ status: 'skip',
48
+ summary:
49
+ 'Automatic power-on after AC loss is not observable through a supported firmware adapter on this machine.',
50
+ remedy: {
51
+ action:
52
+ 'Enable and test restore-on-AC-power in the firmware or VM host during initial enrollment. An OS unit cannot power on a switched-off machine.',
53
+ },
54
+ };
55
+ }
package/src/probe.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { dependencyCheck, fingerprint } from './fingerprint';
2
+ import { CAPABILITIES } from './config';
3
+ import { authCheck } from './auth-checks';
4
+ import type { Capability, Check, Runner, Snapshot } from './types';
5
+
6
+ export async function probe(
7
+ root: string,
8
+ required: Capability[],
9
+ run: Runner,
10
+ nodeId?: string
11
+ ): Promise<Snapshot> {
12
+ const current = await fingerprint(root, run);
13
+ const checks: Check[] = [
14
+ {
15
+ id: 'workspace',
16
+ status:
17
+ Object.values(current.files).every(Boolean) && current.head
18
+ ? 'pass'
19
+ : 'fail',
20
+ summary:
21
+ 'Checkout revision and shared dependency/toolchain input availability.',
22
+ },
23
+ ];
24
+ const dirty = await run(['git', 'status', '--porcelain'], { cwd: root });
25
+ checks.push({
26
+ id: 'git:clean',
27
+ status: dirty.ok ? (dirty.stdout.trim() ? 'warn' : 'pass') : 'fail',
28
+ summary:
29
+ dirty.ok && !dirty.stdout.trim()
30
+ ? 'Working tree is clean.'
31
+ : 'Working tree is dirty or unreadable; sync --pull will refuse.',
32
+ });
33
+ checks.push(await dependencyCheck(root, current));
34
+ for (const capability of CAPABILITIES) {
35
+ if (required.includes(capability))
36
+ checks.push(...(await authCheck(capability, run, root, nodeId)));
37
+ else
38
+ checks.push({
39
+ id: `auth:${capability}`,
40
+ status: 'skip',
41
+ required: false,
42
+ summary: 'Not required by this node configuration.',
43
+ });
44
+ }
45
+ return {
46
+ version: 1,
47
+ platform: process.platform,
48
+ fingerprint: current,
49
+ checks,
50
+ };
51
+ }
@@ -0,0 +1,119 @@
1
+ import { record } from './config';
2
+ import { FINGERPRINT_FILES } from './input-hashes';
3
+ import type { Check, Snapshot } from './types';
4
+
5
+ const STATUSES = ['pass', 'warn', 'fail', 'skip'];
6
+
7
+ export function validCheck(value: unknown): value is Check {
8
+ if (
9
+ !record(value) ||
10
+ typeof value['id'] !== 'string' ||
11
+ !/^[a-z0-9:._-]{1,100}$/.test(value['id']) ||
12
+ !STATUSES.includes(String(value['status'])) ||
13
+ typeof value['summary'] !== 'string' ||
14
+ value['summary'].length > 2000
15
+ )
16
+ return false;
17
+ if (!validDuration(value['phaseDurationMs'])) return false;
18
+ if (!validRequired(value['required'])) return false;
19
+ const remedy = value['remedy'];
20
+ return remedy === undefined || validRemedy(remedy);
21
+ }
22
+
23
+ function validRemedy(value: unknown): boolean {
24
+ return (
25
+ record(value) &&
26
+ typeof value['action'] === 'string' &&
27
+ value['action'].length < 2000 &&
28
+ (value['command'] === undefined || typeof value['command'] === 'string')
29
+ );
30
+ }
31
+
32
+ export function validSnapshot(value: unknown): value is Snapshot {
33
+ if (
34
+ !record(value) ||
35
+ value['version'] !== 1 ||
36
+ typeof value['platform'] !== 'string' ||
37
+ !Array.isArray(value['checks']) ||
38
+ value['checks'].length > 200 ||
39
+ !value['checks'].every(validCheck)
40
+ )
41
+ return false;
42
+ return validFingerprint(value['fingerprint']);
43
+ }
44
+
45
+ function validFingerprint(value: unknown): boolean {
46
+ if (!record(value) || !record(value['files'])) return false;
47
+ if (
48
+ !Object.keys(value['files']).every((key) => FINGERPRINT_FILES.includes(key))
49
+ )
50
+ return false;
51
+ const head = value['head'];
52
+ return (
53
+ (head === null ||
54
+ (typeof head === 'string' && /^[a-f0-9]{40,64}$/.test(head))) &&
55
+ Object.values(value['files']).every(validHash)
56
+ );
57
+ }
58
+
59
+ function validHash(value: unknown): boolean {
60
+ return (
61
+ value === null ||
62
+ (typeof value === 'string' && /^[a-f0-9]{64}$/.test(value))
63
+ );
64
+ }
65
+
66
+ export function decodeSnapshot(output: string): Snapshot | null {
67
+ try {
68
+ const data: unknown = JSON.parse(output);
69
+ return validSnapshot(data) ? cleanSnapshot(data) : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export function cleanCheck(check: Check): Check {
76
+ return {
77
+ id: check.id,
78
+ status: check.status,
79
+ summary: check.summary,
80
+ ...(check.required !== undefined ? { required: check.required } : {}),
81
+ ...(check.phaseDurationMs !== undefined
82
+ ? { phaseDurationMs: check.phaseDurationMs }
83
+ : {}),
84
+ ...(check.remedy
85
+ ? {
86
+ remedy: {
87
+ action: check.remedy.action,
88
+ ...(check.remedy.command ? { command: check.remedy.command } : {}),
89
+ },
90
+ }
91
+ : {}),
92
+ };
93
+ }
94
+
95
+ function cleanSnapshot(snapshot: Snapshot): Snapshot {
96
+ return {
97
+ version: 1,
98
+ platform: snapshot.platform,
99
+ fingerprint: {
100
+ head: snapshot.fingerprint.head,
101
+ files: snapshot.fingerprint.files,
102
+ },
103
+ checks: snapshot.checks.map(cleanCheck),
104
+ };
105
+ }
106
+
107
+ function validDuration(value: unknown): boolean {
108
+ return (
109
+ value === undefined ||
110
+ (typeof value === 'number' &&
111
+ Number.isSafeInteger(value) &&
112
+ value >= 0 &&
113
+ value <= 86_400_000)
114
+ );
115
+ }
116
+
117
+ function validRequired(value: unknown): boolean {
118
+ return value === undefined || typeof value === 'boolean';
119
+ }
@@ -0,0 +1,85 @@
1
+ import { record } from './config';
2
+ import { configPath } from './fleet-config';
3
+ import type { Check, Runner } from './types';
4
+
5
+ /** The proxy runs only on the manager, which performs singleton/state/provenance
6
+ * checks before starting a stopped install; on a worker this verifies the
7
+ * client-only role. Routine sync cannot upgrade or restore it.
8
+ */
9
+ export async function convergeProxy(
10
+ node: string,
11
+ revision: string,
12
+ run: Runner,
13
+ options: { apply?: boolean; cwd?: string } = {}
14
+ ): Promise<Check[]> {
15
+ const result = await run(
16
+ [
17
+ 'python3',
18
+ '-B',
19
+ 'cliproxy/scripts/fleet_sync.py',
20
+ '--definition',
21
+ configPath(),
22
+ '--node',
23
+ node,
24
+ '--revision',
25
+ revision,
26
+ ...(options.apply === false ? [] : ['--apply']),
27
+ ],
28
+ {
29
+ timeoutMs: 180_000,
30
+ ...(options.cwd ? { cwd: options.cwd } : {}),
31
+ }
32
+ );
33
+ try {
34
+ const data: unknown = JSON.parse(result.stdout);
35
+ if (
36
+ !record(data) ||
37
+ data['version'] !== 1 ||
38
+ !Array.isArray(data['phases']) ||
39
+ !data['phases'].length
40
+ )
41
+ throw new Error();
42
+ return data['phases'].map((phase): Check => {
43
+ if (
44
+ !record(phase) ||
45
+ typeof phase['code'] !== 'string' ||
46
+ !/^PROXY_[A-Z0-9_]+$/.test(phase['code'])
47
+ )
48
+ throw new Error();
49
+ return {
50
+ id: `proxy:${phase['code'].slice(6).toLowerCase().replaceAll('_', '-')}`,
51
+ status: phase['status'] === 'unchanged' ? 'pass' : 'fail',
52
+ summary: phase['code'],
53
+ remedy: { action: phaseAction(phase['remedy']) },
54
+ };
55
+ });
56
+ } catch {
57
+ return [
58
+ {
59
+ id: 'proxy:configuration',
60
+ status: 'fail',
61
+ summary: 'PROXY_RECONCILE_UNVERIFIED',
62
+ remedy: {
63
+ action:
64
+ 'Inspect the CLIProxyAPI install on the manager and its deployment manifest; runtime data and credentials were not copied.',
65
+ },
66
+ },
67
+ ];
68
+ }
69
+ }
70
+
71
+ const GENERIC_ACTION =
72
+ 'The proxy runs only on the manager. Inspect it there with its audit/recovery runbook; missing state requires fencing and encrypted recovery, never a second identity.';
73
+
74
+ /** The proxy's own remedies are authored text from tracked source (never
75
+ * subprocess output); anything else falls back to the generic runbook.
76
+ */
77
+ function phaseAction(remedy: unknown): string {
78
+ const action = record(remedy) ? remedy['action'] : undefined;
79
+ return typeof action === 'string' &&
80
+ action.length > 0 &&
81
+ action.length <= 400 &&
82
+ !/[\u0000-\u001f]/.test(action)
83
+ ? action
84
+ : GENERIC_ACTION;
85
+ }