@homeflare/alchemy 0.38.0 → 0.40.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 (353) hide show
  1. package/README.md +32 -8
  2. package/dist/ceph/auth-entity.d.ts +34 -0
  3. package/dist/ceph/auth-entity.d.ts.map +1 -0
  4. package/dist/ceph/ceph-argv-config.d.ts +10 -0
  5. package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
  6. package/dist/ceph/ceph-argv.d.ts +19 -0
  7. package/dist/ceph/ceph-argv.d.ts.map +1 -0
  8. package/dist/ceph/ceph-auth-form.d.ts +54 -0
  9. package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
  10. package/dist/ceph/ceph-auth-parse.d.ts +56 -0
  11. package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
  12. package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
  13. package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
  14. package/dist/ceph/ceph-config-options.d.ts +23 -0
  15. package/dist/ceph/ceph-config-options.d.ts.map +1 -0
  16. package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
  17. package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
  18. package/dist/ceph/ceph-transport.d.ts +37 -0
  19. package/dist/ceph/ceph-transport.d.ts.map +1 -0
  20. package/dist/ceph/fake-ceph-dial.d.ts +35 -0
  21. package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
  22. package/dist/ceph/index.d.ts +13 -0
  23. package/dist/ceph/index.d.ts.map +1 -0
  24. package/dist/ceph/index.js +1609 -0
  25. package/dist/ceph/index.js.map +38 -0
  26. package/dist/forgejo/index.js +4 -2
  27. package/dist/forgejo/index.js.map +3 -3
  28. package/dist/forgejo/team-member.d.ts +25 -0
  29. package/dist/forgejo/team-member.d.ts.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  33. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
  34. package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
  35. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
  36. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
  37. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
  38. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
  39. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
  40. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
  41. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
  42. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
  43. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
  44. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
  45. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
  46. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
  47. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
  48. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
  49. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
  50. package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
  51. package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
  52. package/dist/proxmox/index.d.ts +14 -3
  53. package/dist/proxmox/index.d.ts.map +1 -1
  54. package/dist/proxmox/index.js +574 -133
  55. package/dist/proxmox/index.js.map +33 -23
  56. package/dist/proxmox/qemu-form.d.ts +27 -18
  57. package/dist/proxmox/qemu-form.d.ts.map +1 -1
  58. package/dist/proxmox/qemu-judge.d.ts +30 -0
  59. package/dist/proxmox/qemu-judge.d.ts.map +1 -0
  60. package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
  61. package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
  62. package/dist/proxmox/qemu-net.d.ts +9 -0
  63. package/dist/proxmox/qemu-net.d.ts.map +1 -0
  64. package/dist/proxmox/qemu-props.d.ts +112 -0
  65. package/dist/proxmox/qemu-props.d.ts.map +1 -0
  66. package/dist/proxmox/qemu-volume.d.ts +27 -0
  67. package/dist/proxmox/qemu-volume.d.ts.map +1 -0
  68. package/dist/proxmox/qemu.d.ts +14 -27
  69. package/dist/proxmox/qemu.d.ts.map +1 -1
  70. package/dist/proxmox/storage-download-errors.d.ts +5 -0
  71. package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
  72. package/dist/proxmox/storage-download-identity.d.ts +27 -0
  73. package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
  74. package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
  75. package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
  76. package/dist/proxmox/storage-download-props.d.ts +36 -0
  77. package/dist/proxmox/storage-download-props.d.ts.map +1 -0
  78. package/dist/proxmox/storage-download-task.d.ts +17 -0
  79. package/dist/proxmox/storage-download-task.d.ts.map +1 -0
  80. package/dist/proxmox/storage-download.d.ts +44 -0
  81. package/dist/proxmox/storage-download.d.ts.map +1 -0
  82. package/dist/talos/credentials-write.d.ts +46 -0
  83. package/dist/talos/credentials-write.d.ts.map +1 -0
  84. package/dist/talos/credentials.d.ts +51 -9
  85. package/dist/talos/credentials.d.ts.map +1 -1
  86. package/dist/talos/fake-process.d.ts +56 -0
  87. package/dist/talos/fake-process.d.ts.map +1 -0
  88. package/dist/talos/index.d.ts +19 -4
  89. package/dist/talos/index.d.ts.map +1 -1
  90. package/dist/talos/index.js +357 -173
  91. package/dist/talos/index.js.map +16 -10
  92. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  93. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  94. package/dist/talos/kubeconfig.d.ts +41 -23
  95. package/dist/talos/kubeconfig.d.ts.map +1 -1
  96. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  97. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  98. package/dist/talos/machine-config-poll.d.ts +43 -0
  99. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  100. package/dist/talos/machine-config-read.d.ts +49 -0
  101. package/dist/talos/machine-config-read.d.ts.map +1 -0
  102. package/dist/talos/resource.d.ts +18 -0
  103. package/dist/talos/resource.d.ts.map +1 -1
  104. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  105. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  106. package/dist/talos/talos-bootstrap.d.ts +87 -0
  107. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  108. package/dist/talos/talos-cluster-health.d.ts +43 -1
  109. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  110. package/dist/talos/talos-errors.d.ts +68 -0
  111. package/dist/talos/talos-errors.d.ts.map +1 -0
  112. package/dist/talos/talos-machine-config.d.ts +51 -15
  113. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  114. package/dist/talos/talosctl.d.ts +7 -0
  115. package/dist/talos/talosctl.d.ts.map +1 -1
  116. package/dist/talos/values.d.ts +23 -3
  117. package/dist/talos/values.d.ts.map +1 -1
  118. package/dist/telemetry/fake-collector.d.ts +36 -0
  119. package/dist/telemetry/fake-collector.d.ts.map +1 -0
  120. package/dist/telemetry/index.d.ts +11 -0
  121. package/dist/telemetry/index.d.ts.map +1 -0
  122. package/dist/telemetry/index.js +223 -0
  123. package/dist/telemetry/index.js.map +12 -0
  124. package/dist/telemetry/layer.d.ts +86 -0
  125. package/dist/telemetry/layer.d.ts.map +1 -0
  126. package/dist/telemetry/redact-serialization.d.ts +21 -0
  127. package/dist/telemetry/redact-serialization.d.ts.map +1 -0
  128. package/dist/telemetry/redact.d.ts +46 -0
  129. package/dist/telemetry/redact.d.ts.map +1 -0
  130. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  131. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  132. package/dist/unifi/acl-rule-form.d.ts +90 -0
  133. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  134. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  135. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  136. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  137. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  138. package/dist/unifi/acl-rule.d.ts +49 -0
  139. package/dist/unifi/acl-rule.d.ts.map +1 -0
  140. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  141. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  142. package/dist/unifi/dns-policy-form.d.ts +97 -0
  143. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  144. package/dist/unifi/dns-policy.d.ts +55 -0
  145. package/dist/unifi/dns-policy.d.ts.map +1 -0
  146. package/dist/unifi/drift.d.ts +27 -0
  147. package/dist/unifi/drift.d.ts.map +1 -0
  148. package/dist/unifi/fake-unifi.d.ts +5 -1
  149. package/dist/unifi/fake-unifi.d.ts.map +1 -1
  150. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  151. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  152. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  153. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  154. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  155. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  156. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  157. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  158. package/dist/unifi/firewall-policy.d.ts +57 -0
  159. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  160. package/dist/unifi/firewall-zone-form.d.ts +6 -0
  161. package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
  162. package/dist/unifi/firewall-zone.d.ts +1 -1
  163. package/dist/unifi/firewall-zone.d.ts.map +1 -1
  164. package/dist/unifi/index.d.ts +11 -3
  165. package/dist/unifi/index.d.ts.map +1 -1
  166. package/dist/unifi/index.js +725 -60
  167. package/dist/unifi/index.js.map +31 -9
  168. package/dist/unifi/network-drift.d.ts +26 -0
  169. package/dist/unifi/network-drift.d.ts.map +1 -0
  170. package/dist/unifi/network-form.d.ts +40 -7
  171. package/dist/unifi/network-form.d.ts.map +1 -1
  172. package/dist/unifi/network.d.ts +1 -0
  173. package/dist/unifi/network.d.ts.map +1 -1
  174. package/dist/unifi/paginate.d.ts +24 -0
  175. package/dist/unifi/paginate.d.ts.map +1 -0
  176. package/dist/unifi/providers.d.ts +1 -1
  177. package/dist/unifi/providers.d.ts.map +1 -1
  178. package/dist/unifi/resource.d.ts +36 -1
  179. package/dist/unifi/resource.d.ts.map +1 -1
  180. package/dist/unifi/scan-source.d.ts +11 -0
  181. package/dist/unifi/scan-source.d.ts.map +1 -0
  182. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  183. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  184. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  185. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  186. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  187. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  188. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  189. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  190. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  191. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  192. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  193. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  194. package/docs/telemetry-spike.md +94 -0
  195. package/docs/telemetry.md +109 -0
  196. package/docs/unifi-acl-rule.md +122 -0
  197. package/docs/unifi-dns-policy.md +75 -0
  198. package/docs/unifi-firewall-policy.md +109 -0
  199. package/docs/unifi-traffic-matching-list.md +82 -0
  200. package/docs/unifi-wifi.md +120 -0
  201. package/docs/unifi.md +64 -3
  202. package/package.json +10 -2
  203. package/src/ceph/auth-entity.ts +74 -0
  204. package/src/ceph/ceph-argv-config.ts +78 -0
  205. package/src/ceph/ceph-argv.test.ts +226 -0
  206. package/src/ceph/ceph-argv.ts +189 -0
  207. package/src/ceph/ceph-auth-form.test.ts +103 -0
  208. package/src/ceph/ceph-auth-form.ts +107 -0
  209. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  210. package/src/ceph/ceph-auth-parse.ts +121 -0
  211. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  212. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  213. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  214. package/src/ceph/ceph-config-options.test.ts +44 -0
  215. package/src/ceph/ceph-config-options.ts +36 -0
  216. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  217. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  218. package/src/ceph/ceph-transport.test.ts +125 -0
  219. package/src/ceph/ceph-transport.ts +117 -0
  220. package/src/ceph/fake-ceph-dial.ts +55 -0
  221. package/src/ceph/index.ts +12 -0
  222. package/src/forgejo/team-member.test.ts +14 -0
  223. package/src/forgejo/team-member.ts +28 -1
  224. package/src/index.ts +1 -0
  225. package/src/proxmox/constraints-required.test.ts +1 -0
  226. package/src/proxmox/generated/constraints/index.ts +3 -1
  227. package/src/proxmox/generated/constraints/pve-access.ts +1 -1
  228. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
  229. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
  230. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
  231. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
  232. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
  233. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
  234. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
  235. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
  236. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
  237. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
  238. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
  239. package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
  240. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
  241. package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
  242. package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
  243. package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
  244. package/src/proxmox/index.test.ts +11 -6
  245. package/src/proxmox/index.ts +19 -3
  246. package/src/proxmox/qemu-form.ts +49 -13
  247. package/src/proxmox/qemu-identity.test.ts +30 -0
  248. package/src/proxmox/qemu-judge.test.ts +65 -0
  249. package/src/proxmox/qemu-judge.ts +84 -0
  250. package/src/proxmox/qemu-lifecycle.test.ts +78 -19
  251. package/src/proxmox/qemu-lifecycle.ts +131 -32
  252. package/src/proxmox/qemu-net.test.ts +48 -0
  253. package/src/proxmox/qemu-net.ts +78 -0
  254. package/src/proxmox/qemu-ownership.test.ts +44 -0
  255. package/src/proxmox/qemu-props.test.ts +87 -0
  256. package/src/proxmox/qemu-props.ts +157 -0
  257. package/src/proxmox/qemu-volume.test.ts +229 -0
  258. package/src/proxmox/qemu-volume.ts +178 -0
  259. package/src/proxmox/qemu.ts +15 -29
  260. package/src/proxmox/storage-download-errors.ts +7 -0
  261. package/src/proxmox/storage-download-identity.test.ts +54 -0
  262. package/src/proxmox/storage-download-identity.ts +50 -0
  263. package/src/proxmox/storage-download-lifecycle.ts +226 -0
  264. package/src/proxmox/storage-download-ownership.test.ts +50 -0
  265. package/src/proxmox/storage-download-props.ts +38 -0
  266. package/src/proxmox/storage-download-task.ts +72 -0
  267. package/src/proxmox/storage-download.test.ts +216 -0
  268. package/src/proxmox/storage-download.ts +58 -0
  269. package/src/talos/credentials-write.test.ts +81 -0
  270. package/src/talos/credentials-write.ts +95 -0
  271. package/src/talos/credentials.test.ts +157 -0
  272. package/src/talos/credentials.ts +160 -90
  273. package/src/talos/fake-process.ts +94 -0
  274. package/src/talos/index.ts +39 -4
  275. package/src/talos/kubeconfig-attrs.ts +55 -0
  276. package/src/talos/kubeconfig.test.ts +256 -0
  277. package/src/talos/kubeconfig.ts +170 -90
  278. package/src/talos/machine-config-fixtures.ts +86 -0
  279. package/src/talos/machine-config-poll.test.ts +57 -0
  280. package/src/talos/machine-config-poll.ts +82 -0
  281. package/src/talos/machine-config-read.ts +118 -0
  282. package/src/talos/resource.ts +19 -0
  283. package/src/talos/secrets-gate.test.ts +63 -0
  284. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  285. package/src/talos/talos-bootstrap.test.ts +218 -0
  286. package/src/talos/talos-bootstrap.ts +151 -45
  287. package/src/talos/talos-cluster-health.test.ts +143 -0
  288. package/src/talos/talos-cluster-health.ts +91 -28
  289. package/src/talos/talos-errors.ts +85 -0
  290. package/src/talos/talos-machine-config.test.ts +189 -0
  291. package/src/talos/talos-machine-config.ts +102 -88
  292. package/src/talos/talosctl.ts +8 -0
  293. package/src/talos/values.test.ts +34 -6
  294. package/src/talos/values.ts +41 -7
  295. package/src/telemetry/fake-collector.ts +59 -0
  296. package/src/telemetry/http-client-sealing.test.ts +82 -0
  297. package/src/telemetry/index.ts +10 -0
  298. package/src/telemetry/layer.test.ts +204 -0
  299. package/src/telemetry/layer.ts +152 -0
  300. package/src/telemetry/redact-serialization.ts +178 -0
  301. package/src/telemetry/redact.test.ts +124 -0
  302. package/src/telemetry/redact.ts +147 -0
  303. package/src/unifi/acl-rule-drift.ts +57 -0
  304. package/src/unifi/acl-rule-form.ts +135 -0
  305. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  306. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  307. package/src/unifi/acl-rule-ordering.ts +70 -0
  308. package/src/unifi/acl-rule.test.ts +183 -0
  309. package/src/unifi/acl-rule.ts +61 -0
  310. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  311. package/src/unifi/dns-policy-drift.ts +48 -0
  312. package/src/unifi/dns-policy-form.ts +142 -0
  313. package/src/unifi/dns-policy.test.ts +154 -0
  314. package/src/unifi/dns-policy.ts +67 -0
  315. package/src/unifi/drift.test.ts +78 -0
  316. package/src/unifi/drift.ts +56 -0
  317. package/src/unifi/errors-and-secrets.test.ts +127 -0
  318. package/src/unifi/fake-unifi.ts +5 -1
  319. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  320. package/src/unifi/firewall-policy-drift.ts +55 -0
  321. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  322. package/src/unifi/firewall-policy-form.ts +146 -0
  323. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  324. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  325. package/src/unifi/firewall-policy-ordering.ts +87 -0
  326. package/src/unifi/firewall-policy.test.ts +183 -0
  327. package/src/unifi/firewall-policy.ts +73 -0
  328. package/src/unifi/firewall-zone-form.ts +31 -2
  329. package/src/unifi/firewall-zone.test.ts +21 -1
  330. package/src/unifi/firewall-zone.ts +1 -1
  331. package/src/unifi/get-only-guard.test.ts +165 -0
  332. package/src/unifi/index.ts +78 -1
  333. package/src/unifi/network-drift.test.ts +207 -0
  334. package/src/unifi/network-drift.ts +80 -0
  335. package/src/unifi/network-form.ts +11 -39
  336. package/src/unifi/network.test.ts +7 -82
  337. package/src/unifi/network.ts +6 -6
  338. package/src/unifi/paginate.test.ts +252 -0
  339. package/src/unifi/paginate.ts +141 -0
  340. package/src/unifi/providers.ts +19 -1
  341. package/src/unifi/resource.ts +73 -3
  342. package/src/unifi/scan-source.ts +64 -0
  343. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  344. package/src/unifi/traffic-matching-list-form.ts +77 -0
  345. package/src/unifi/traffic-matching-list.test.ts +202 -0
  346. package/src/unifi/traffic-matching-list.ts +78 -0
  347. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  348. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  349. package/src/unifi/wifi-broadcast-form.ts +211 -0
  350. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  351. package/src/unifi/wifi-broadcast.test.ts +155 -0
  352. package/src/unifi/wifi-broadcast.ts +90 -0
  353. package/src/unifi/write-op-reference.test.ts +190 -0
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/ceph/auth-entity.ts", "../src/openbao/mount-path.ts", "../src/ceph/ceph-auth-form.ts", "../src/ceph/ceph-auth-reconcile.ts", "../src/openbao/bao-http.ts", "../src/openbao/bao-address.ts", "../src/openbao/bao-status.ts", "../src/openbao/digest.ts", "../src/ceph/ceph-config-options.ts", "../src/ceph/ceph-argv-config.ts", "../src/ceph/ceph-argv.ts", "../src/ceph/ceph-auth-parse.ts", "../src/ceph/ceph-transport.ts", "../src/linux/ssh-command.ts", "../src/linux/ssh-runner.ts", "../src/launchd/host-lookup.ts", "../src/linux/ssh-scripts.ts", "../src/launchd/job-form.ts", "../src/launchd/plist.ts", "../src/linux/unit-text.ts", "../src/linux/unit-form.ts", "../src/launchd/secret-tripwire.ts", "../src/launchd/host-file-form.ts", "../src/launchd/job-validate.ts", "../src/launchd/launchctl.ts", "../src/launchd/sudo-allowlist.ts", "../src/linux/sudo-allowlist-dir.ts", "../src/linux/sudo-allowlist-chown.ts", "../src/linux/sudo-allowlist.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `Ceph.AuthEntity` (K-A4) — a Ceph client auth entity (`client.k8s-*`), minted over the ssh\n * mon-command transport and stored only in OpenBao. Design: docs/plans/2026-09-26-ceph-mon-\n * transport.md, \"`Ceph.AuthEntity` (K-A4)\"; the loop itself lives in ceph-auth-reconcile.ts, the\n * transport in ceph-transport.ts, the allowlist in ceph-argv.ts.\n *\n * ⛔ `read` AND `diff` NEVER SSH. \"A plan never elevates\" (../linux/sudo-runner.ts's own header\n * makes the same rule for the file family): this family's props/state comparison is the whole\n * of `diff`, and `read` hands back exactly the attributes state already holds. Live drift is\n * caught only at `reconcile` (which does elevate, on the admin lane) and by the post-deploy\n * operator check — this family cannot prove live convergence at plan time, and says so rather\n * than pretending an unprivileged probe exists where none does.\n * ⛔ `delete` IS REFUSED UNCONDITIONALLY. `defaultRemovalPolicy: 'retain'` keeps Alchemy from ever\n * calling it on an ordinary `destroy`, and the handler itself refuses too — belt and suspenders\n * with ceph-argv.ts's own refusal of the `auth del` shape, so there is no path, `--adopt` or a\n * resource-level removal-policy override included, that ever reaches it (D3).\n * ⛔ NEVER ADOPTED. See ceph-auth-reconcile.ts's header for why the generic `refuseTakeover` is not\n * used here.\n * ⛔ ROWS ARE CREATES THROUGH THE FIRST-CREATE GATE, NO `adopt()` — `list` answers empty, the same\n * as every Ceph family in this kit (ceph-pool.ts's header has the reason a non-empty list would\n * invite exactly the takeover this family refuses).\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport type { CephAuthEntityAttributes, CephAuthEntityProps } from './ceph-auth-form.ts';\nimport { planCephAuthEntity } from './ceph-auth-form.ts';\nimport { reconcileCephAuthEntity } from './ceph-auth-reconcile.ts';\n\nexport type { CephAuthEntityAttributes, CephAuthEntityProps } from './ceph-auth-form.ts';\nexport type { CephCaps } from './ceph-auth-parse.ts';\n\nexport interface CephAuthEntity extends Resource<\n 'Ceph.AuthEntity',\n CephAuthEntityProps,\n CephAuthEntityAttributes,\n never,\n HttpClient.HttpClient\n> {}\n\n/** ★ `retain` — the delete handler below refuses regardless, but this stops Alchemy calling it. */\nexport const CephAuthEntity = Resource<CephAuthEntity>('Ceph.AuthEntity', {\n defaultRemovalPolicy: 'retain',\n});\n\nconst refusedDelete = (entity: string): Error =>\n new Error(\n `Ceph.AuthEntity ${entity}: 'ceph auth del' is refused entirely — lockout safety, never ` +\n 'auto-delete (D3). Nothing was sent to any mon. If the entity truly must go, remove it by ' +\n 'hand, deliberately, along with its OpenBao key — this family will never do it for you.',\n );\n\nexport const CephAuthEntityProvider = () =>\n Provider.effect(\n CephAuthEntity,\n Effect.succeed(\n CephAuthEntity.Provider.of({\n list: () => Effect.succeed([]),\n\n /** State only — see the header. Never `Unowned`: this family is never adopted. */\n read: ({ output }) => Effect.succeed(output),\n\n /** State only — see the header. `output === undefined` lets reconcile decide (a create). */\n diff: ({ news, output }) =>\n Effect.succeed(isResolved(news) ? planCephAuthEntity(news, output) : undefined),\n\n reconcile: ({ news, output }) => reconcileCephAuthEntity(news, output, {}),\n\n delete: ({ output }) => Effect.die(refusedDelete(output.entity)),\n }),\n ),\n );\n",
6
+ "/**\n * Trimming runs of one character from caller-supplied paths and names.\n *\n * ★ LOOPS, NOT `/^\\/+|\\/+$/g`, `/\\/+$/` OR `/^-+|-+$/g`. A trailing `x+$` backtracks polynomially\n * on a long run of `x` followed by anything else (CodeQL js/polynomial-redos), and mount paths,\n * namespaces and slugs are caller input. Every loop here is linear in the input length.\n */\n\n/** `text` without a trailing run of `char` (one UTF-16 unit). */\nexport const trimTrailing = (text: string, char: string): string => {\n let end = text.length;\n while (end > 0 && text[end - 1] === char) end -= 1;\n return text.slice(0, end);\n};\n\n/** `text` without a leading or trailing run of `char` (one UTF-16 unit). */\nexport const trimRuns = (text: string, char: string): string => {\n let start = 0;\n while (start < text.length && text[start] === char) start += 1;\n return trimTrailing(text.slice(start), char);\n};\n\n/** `text` without trailing `/`. */\nexport const trimTrailingSlashes = (text: string): string => trimTrailing(text, '/');\n\n/** `text` without leading or trailing `/`. */\nexport const trimSlashes = (text: string): string => trimRuns(text, '/');\n\n/** SSH mount path without a trailing slash; `ssh` when the caller did not say. */\nexport const sshMountOf = (mount: string | undefined): string =>\n trimTrailingSlashes(mount ?? 'ssh');\n",
7
+ "/**\n * `Ceph.AuthEntity`'s props/attributes shapes, the OpenBao paths it writes to, and the pure plan\n * function `diff` uses. Design: docs/plans/2026-09-26-ceph-mon-transport.md, \"`Ceph.AuthEntity`\n * (K-A4)\".\n *\n * ⛔ `entity` AND `mount` ARE IDENTITY — WHICH MEANS A CHANGE TO EITHER IS `replace`, NEVER\n * `update`. The original header here argued a different entity/mount is simply a different\n * resource id in the consuming stack, so this family would never need `replace`. That assumed\n * the id and the identity move together; nothing enforces that they do — a declaration can keep\n * its Alchemy id and edit `entity` or `mount` in place. LAND red team (2026-09-26), CONFIRMED:\n * with `output` still naming the OLD entity, comparing caps alone answered `update`, and\n * reconcile then ran `auth get`/`auth caps` against the NEW entity — a foreign or unrelated\n * live entity — because `output !== undefined` skipped the \"never adopted\" refusal (which only\n * fires when state is absent). `identityMoved` below is the guard, mirroring the\n * `rename-identity.ts` pattern `Bao.SshRole` and its siblings use for the same shape of bug —\n * except this family cannot ask `judgeMove`'s occupancy question at plan time (that would ssh),\n * so there is no `replace`-onto-an-occupied-identity refusal here; reconcile's own\n * `neverAdopted` check is what catches a moved identity landing on a live foreign entity,\n * because a `replace`'s new generation always reconciles with `output: undefined`.\n */\nimport { trimTrailingSlashes } from '../openbao/mount-path.ts';\nimport type { CephCaps } from './ceph-auth-parse.ts';\n\nexport type { CephCaps };\n\nexport type CephAuthEntityProps = {\n /** `client.k8s-<name>` — bounded by ceph-argv.ts's allowlist, checked on every transport call. */\n readonly entity: string;\n readonly caps: CephCaps;\n /** TB4 mon ssh destinations, tried in this order (D1). At least one. */\n readonly nodes: readonly string[];\n /** OpenBao KV-v2 mount holding Talos/Ceph material, e.g. `talos-c1` (D2). */\n readonly mount: string;\n};\n\nexport type CephAuthEntityAttributes = {\n readonly entity: string;\n readonly caps: CephCaps;\n /** `sha256(key)`, hex — never the key itself. */\n readonly fingerprint: string;\n /** CLI-style path (`<mount>/ceph/<entity>`) — safe to persist and log; the key is not here. */\n readonly baoPath: string;\n /** Which mon actually answered the write that produced this row. Never key material. */\n readonly node: string;\n};\n\nexport const capsEqual = (a: CephCaps, b: CephCaps): boolean =>\n a.mon === b.mon && a.osd === b.osd && a.mgr === b.mgr;\n\n/** The CLI-style path an operator's `bao kv get` would use — what `baoPath` records. */\nexport const cephCliPath = (mount: string, entity: string): string =>\n `${trimTrailingSlashes(mount)}/ceph/${entity}`;\n\n/**\n * The raw KV-v2 HTTP API path `../openbao/bao-http.ts`'s `baoRead`/`baoWrite` take — the CLI path\n * with `data/` inserted, the same segment `bao kv get`/`bao kv put` insert themselves\n * (`../talos/credentials.ts`'s header has the shipped-default bug this convention avoids: never\n * include `data/` in the CLI-style path itself, only here).\n */\nexport const cephDataPath = (mount: string, entity: string): string =>\n `${trimTrailingSlashes(mount)}/data/ceph/${entity}`;\n\nexport type CephAuthEntityPlan = { readonly action: 'noop' | 'update' | 'replace' };\n\n/**\n * Whether `output` (if any) names a different entity/mount than `news` declares — the one\n * question both `planCephAuthEntity` and `reconcileCephAuthEntity`'s guard ask, so the comparison\n * lives in exactly one place (rename-identity.ts's own header: \"one description of which object,\n * not twelve copies of the check\").\n */\nexport const identityMoved = (\n news: Pick<CephAuthEntityProps, 'entity' | 'mount'>,\n output: CephAuthEntityAttributes | undefined,\n): boolean =>\n output !== undefined &&\n (output.entity !== news.entity || output.baoPath !== cephCliPath(news.mount, news.entity));\n\n/**\n * ⛔ STATE ONLY, NO SSH — the design's own rule for this family (\"a plan never elevates\"; diff and\n * verify compare props against state, never the live cluster). `output === undefined` returns\n * `undefined`: a brand-new row plans as a create the same way every other family in this kit\n * does, and reconcile is where the actual ssh work — and the key material — ever appears.\n * A moved identity plans `replace` before caps are even considered: `defaultRemovalPolicy:\n * 'retain'` means the old generation is simply left alone (Apply.ts skips its delete under\n * retain), and the new generation reconciles with `output: undefined`, which is exactly the\n * \"never adopted\" path if the new identity already has a live entity.\n */\nexport const planCephAuthEntity = (\n news: CephAuthEntityProps,\n output: CephAuthEntityAttributes | undefined,\n): CephAuthEntityPlan | undefined => {\n if (output === undefined) return undefined;\n if (identityMoved(news, output)) return { action: 'replace' };\n return capsEqual(news.caps, output.caps) ? { action: 'noop' } : { action: 'update' };\n};\n\nexport const attributesOf = (\n props: Pick<CephAuthEntityProps, 'caps' | 'entity' | 'mount'>,\n fingerprint: string,\n node: string,\n): CephAuthEntityAttributes => ({\n baoPath: cephCliPath(props.mount, props.entity),\n caps: props.caps,\n entity: props.entity,\n fingerprint,\n node,\n});\n",
8
+ "/**\n * The observe -> ensure -> sync loop `Ceph.AuthEntity`'s reconcile runs (mon-transport doc,\n * `Ceph.AuthEntity`). Split from auth-entity.ts so the loop itself — the part a fake-runner test\n * actually exercises — is a plain function, the same split ceph-pool.ts and ssh-role.ts use.\n *\n * ⛔ NEVER ADOPTS. A live entity found present while this stack holds no attributes for it is\n * refused outright, `--adopt` included — the generic `refuseTakeover` (../ownership/adopt.ts)\n * is deliberately NOT used here, because it honours `--adopt` and this family's own design says\n * plainly: \"Existing auth entities are recorded as observed inventory only — never adopted:\n * adopting one would mean reading and fingerprinting its key, and a key is only ever handled for\n * an entity this stack owns.\"\n * ⛔ THE OBSERVE STEP READS `auth get`'S UNFILTERED STDOUT ON EVERY RECONCILE (decision 65,\n * 2026-09-26, amending the design after LAND finding 5 — no node-side filter narrows it first).\n * The key that stdout carries is dropped in the same breath: `parseAuthGet` has no `key` field in\n * its return type at all, so there is no variable here ever holding it past that call. Only `auth\n * get-or-create`, run once on the create branch, yields a `key` value, and it goes straight to\n * OpenBao — never assigned to a variable this function returns, logs, or puts in the attributes\n * it hands back — `attributesOf` takes a fingerprint, never a key.\n * ★ CAPS DRIFT NEVER RE-MINTS THE KEY. `auth caps` changes only what an entity may do; the returned\n * attributes reuse `output.fingerprint` unchanged.\n * ⛔ A MOVED IDENTITY IS REFUSED HERE TOO, BELT AND SUSPENDERS WITH `planCephAuthEntity`'s\n * `replace`. `isResolved(news)` in auth-entity.ts's `diff` can defer to the engine's own\n * `update` fallback while any prop is still an Output (rename.ts's own header documents this\n * exact Alchemy behaviour for every other family this pattern protects) — a case in which\n * `planCephAuthEntity` never even runs. Reached with `output` naming a different entity/mount\n * than `props`, this refuses before any ssh call, the same shape of guard `guardRename`\n * (openbao/rename-identity.ts) runs first in every `Bao.*` reconcile.\n */\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { baoWrite } from '../openbao/bao-http.ts';\nimport { sha256 } from '../openbao/digest.ts';\nimport { authCapsArgv, authGetArgv, authGetOrCreateArgv } from './ceph-argv.ts';\nimport { parseAuthGet, parseAuthGetOrCreate } from './ceph-auth-parse.ts';\nimport {\n type CephAuthEntityAttributes,\n type CephAuthEntityProps,\n attributesOf,\n capsEqual,\n cephDataPath,\n identityMoved,\n} from './ceph-auth-form.ts';\nimport { type CephTransportOptions, assertFreshQuorum, runCephCommand } from './ceph-transport.ts';\n\nexport type CephAuthDeps = Pick<CephTransportOptions, 'dial' | 'log'>;\n\nconst asError = (cause: unknown): Error =>\n cause instanceof Error ? cause : new Error(String(cause));\n\nconst runCeph = (transport: CephTransportOptions, argv: readonly string[]) =>\n Effect.tryPromise({ catch: asError, try: () => runCephCommand(transport, argv) });\n\nconst syncQuorum = (transport: CephTransportOptions) =>\n Effect.tryPromise({ catch: asError, try: () => assertFreshQuorum(transport) });\n\nconst neverAdopted = (entity: string): Error =>\n new Error(\n `Ceph.AuthEntity ${entity}: already exists on the mon cluster and this stack holds no state ` +\n 'for it. This family is never adopted — not even under --adopt — because taking it over ' +\n 'would mean fingerprinting a key this stack did not mint. Remove it by hand first, or bring ' +\n 'it under management through the coordinator’s own read-only inventory process.',\n );\n\nconst movedIdentity = (from: string, to: string): Error =>\n new Error(\n `Ceph.AuthEntity: planned as an update, but its identity moved from ${from} to ${to}. ` +\n `Acting on ${to} now, with ${from}'s attributes, could re-cap an entity this generation ` +\n 'never minted. Nothing was sent to any mon. Run the deploy again: once entity/mount fully ' +\n 'resolve at plan time this plans as a replace instead.',\n );\n\nexport const reconcileCephAuthEntity = (\n props: CephAuthEntityProps,\n output: CephAuthEntityAttributes | undefined,\n deps: CephAuthDeps,\n): Effect.Effect<CephAuthEntityAttributes, Error, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n if (identityMoved(props, output))\n return yield* Effect.fail(movedIdentity(output?.entity ?? '', props.entity));\n\n const transport: CephTransportOptions = { nodes: props.nodes, ...deps };\n\n const observed = yield* runCeph(transport, authGetArgv(props.entity)).pipe(\n Effect.map((result) => parseAuthGet(result, props.entity)),\n );\n\n if (observed.kind === 'absent') {\n // ⛔ THE VAULT WRITE IS CHECKED BEFORE ANYTHING IS MINTED. LAND red team (2026-09-26),\n // CONFIRMED: minting first and writing second means a write refused by policy (the likely\n // first-deploy shape — the `data/ceph/*` write grant landing after this stack's first\n // run) strands a key that only ever existed in memory. The next reconcile's `auth get`\n // then finds the entity present, `output` is still undefined, and `neverAdopted` refuses\n // it — permanently, since `auth del` is refused too (D3). A placeholder write proves the\n // path is writable first; nothing has touched the mon if it fails, so a retry starts\n // clean. ⚠️ KV-v2 keeps this placeholder as a version in the secret's history (no version\n // is ever skipped) — harmless, since it never carries anything key-shaped, but visible to\n // `bao kv get -version=N` until the mount's `max_versions` rolls it off.\n yield* baoWrite('POST', cephDataPath(props.mount, props.entity), {\n data: { pending: true },\n });\n const result = yield* runCeph(transport, authGetOrCreateArgv(props.entity, props.caps));\n const created = parseAuthGetOrCreate(result, props.entity);\n // ⛔ THE ONE PLACE THE KEY CROSSES INTO A BAO CALL — as the HTTP request body's JSON, never\n // argv, never a temp file. `created.key` is not referenced again after this.\n yield* baoWrite('POST', cephDataPath(props.mount, props.entity), {\n data: { caps: created.caps, key: created.key },\n });\n yield* syncQuorum(transport);\n return attributesOf(props, sha256(created.key), result.node);\n }\n\n if (output === undefined) return yield* Effect.fail(neverAdopted(props.entity));\n\n if (capsEqual(observed.caps, props.caps)) return output;\n\n const result = yield* runCeph(transport, authCapsArgv(props.entity, props.caps));\n yield* syncQuorum(transport);\n return attributesOf(props, output.fingerprint, result.node);\n });\n",
9
+ "/**\n * One call to OpenBao's HTTP API through Effect's `HttpClient` — the transport every Bao.* family\n * uses since it stopped shelling out to `bao`.\n *\n * ★ `HttpClient`, THE SAME PATTERN <estate>/proxmox and <estate>/forgejo USE AGAINST THEIR APIs, decided\n * 2026-09-14. It buys a typed error channel, interruption when a plan is cancelled, the runtime's\n * tracing, and a client a test can point at a fake server — and it needs no Alchemy session, so\n * every call here works from all four operations. (`CommandExecutor.run` was tried on Bao.Policy\n * first and blocked read/diff: Alchemy passes a session to reconcile/delete but NOT to read/diff,\n * Provider.ts:258-289. The ChildProcessSpawner that fixed that is gone with the CLI.)\n * ⚠️ THE STACK MUST PROVIDE `FetchHttpClient.layer`. alchemy.run.ts does, the way\n * <estate>/proxmox/alchemy.run.ts does.\n */\nimport * as Cause from 'effect/Cause';\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\nimport * as Schedule from 'effect/Schedule';\nimport * as Semaphore from 'effect/Semaphore';\nimport * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';\nimport * as Headers from 'effect/unstable/http/Headers';\nimport * as HttpClient from 'effect/unstable/http/HttpClient';\nimport * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';\nimport { type BaoEnvironment, headersFor, resolveAddress } from './bao-address.ts';\nimport { BaoError, type BaoIntent, settle } from './bao-status.ts';\n\n/**\n * The environment a call resolves BAO_ADDR, BAO_NAMESPACE and BAO_TOKEN from — `process.env`,\n * read at call time, unless a test provides another.\n */\nexport const BaoEnv = Context.Reference<BaoEnvironment>('homeflare/openbao/BaoEnv', {\n defaultValue: () => process.env,\n});\n\n/**\n * ⛔ AT MOST EIGHT EXCHANGES WITH OPENBAO AT ONCE — ACROSS EVERY Bao.* FAMILY, IN ONE PROCESS.\n *\n * 🔴 MEASURED 2026-09-14 10:41: A DEPLOY OF THIS STACK TOOK OPENBAO's API OFF ITS HOST. Alchemy fans\n * resources out with a hard-coded `concurrency: \"unbounded\"` (alchemy Plan.ts:476, Apply.ts:259,\n * :263, :303 — no option reaches it), and 555 Bao.CloudflareRole resources each read, diff,\n * reconcile and read back: well over a thousand requests at one loopback listener within seconds.\n * OpenBao logged\n * HTTP server (listening on 127.0.0.1:8200) exited with error: set tcp\n * 127.0.0.1:8200->127.0.0.1:57974: setsockopt: invalid argument\n * and KEPT RUNNING with 8200 closed. launchd saw a live process and restarted nothing. Everything\n * on the vault host that talks to 127.0.0.1:8200 was cut off, starting with the agent's templates, until\n * a manual restart.\n * ★ THE LISTENER DYING IS A GO-ON-DARWIN FAILURE, NOT OURS TO FIX. Traefik lost its entrypoints the\n * same way on darwin/arm64 (traefik issue 8841). The BURST is ours. The adoption deploy an hour\n * earlier made the same calls and survived, so a cap narrows a race; it does not prove it closed.\n * ★ A SEMAPHORE, NOT `HttpClient.withRateLimiter`. That limits requests per window and needs a\n * RateLimiter service. The failure is simultaneous connections, and only a cap on in-flight\n * exchanges bounds those whatever each one's latency.\n * ⚠️ A Context.Reference SO A TEST CAN PROVIDE ITS OWN GATE. The default is built ONCE and cached on\n * the reference (effect Context.ts:1582-1588), so every call in the process shares one gate.\n * ⚠️ EIGHT IS A CHOICE, NOT A MEASUREMENT OF WHAT THE LISTENER TOLERATES. Nothing here can find that\n * number without re-running the failure against production.\n */\nexport const MAX_IN_FLIGHT = 8;\n\nexport const BaoGate = Context.Reference<Semaphore.Semaphore>('homeflare/openbao/BaoGate', {\n defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT),\n});\n\n/**\n * ⚠️ THE CLI's DEFAULT CLIENT TIMEOUT (openbao v2.6.2 api/client.go:324). A hung agent or a server\n * that accepts and never answers would otherwise hang the plan with no line saying why.\n */\nexport const BAO_TIMEOUT = '60 seconds';\n\n/**\n * Mesh in front of a remote OpenBao drops connections under Alchemy's unbounded\n * fan-out. Status 0 is transport, not an OpenBao 4xx — retry twice. Measured\n * 2026-09-16 against bao.example.internal: a 585-role plan died mid-diff with\n * `no response: (no errors given)` while the vault stayed unsealed.\n */\nexport const retryTransport = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n Effect.retry(effect, {\n schedule: Schedule.spaced('750 millis'),\n times: 2,\n while: (error) =>\n (error instanceof BaoError && error.status === 0) || Cause.isTimeoutError(error),\n });\n\n/**\n * ⛔ EFFECT RECORDS EVERY REQUEST HEADER ON THE CLIENT SPAN and redacts only the names in\n * `Headers.CurrentRedactedNames` — by default authorization, cookie, set-cookie and x-api-key\n * (effect Headers.ts:753-762; HttpClient.ts:842-849, with `TracerHeaderFilter` defaulting to\n * true). `X-Vault-Token` is not on that list, so under any tracer the token would be exported as\n * a span attribute. It is added for every call made here.\n */\nexport const redactingToken = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n Effect.gen(function* () {\n const names = yield* Headers.CurrentRedactedNames;\n return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [\n ...names,\n 'x-vault-token',\n ]);\n });\n\n/**\n * Route the exchange over a unix socket when the address is one.\n *\n * ★ BUN's `fetch` TAKES A `unix` OPTION (bun-types globals.d.ts BunFetchRequestInit.unix), AND\n * `FetchHttpClient.RequestInit` IS EFFECT'S DOCUMENTED WAY TO PASS FETCH OPTIONS. The fetch client\n * spreads that service into every call (effect FetchHttpClient.ts:59-72), reading it from the\n * CALLING fiber's context — and `layerMergedContext` merges the caller's context over the layer's\n * (HttpClient.ts:2065-2076), so providing it around one call routes that call alone. Any defaults\n * already provided are kept.\n * ⛔ IT ONLY WORKS ON BUN, SO ANYWHERE ELSE IT REFUSES. Node's fetch ignores `unix` and would send\n * the request to http://localhost:80 — a silent wrong server, which is the one outcome this\n * package exists to prevent. It also assumes the provided `HttpClient` is the fetch one.\n */\nexport const overSocket =\n (socket: string | undefined, method: string, path: string) =>\n <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E | BaoError, R> => {\n if (socket === undefined) return effect;\n if (typeof Bun === 'undefined') {\n return Effect.fail(\n new BaoError(0, method, path, ['a unix:// address needs Bun fetch; this is not Bun']),\n );\n }\n return Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (defaults) => {\n const init: BunFetchRequestInit = { ...Option.getOrElse(defaults, () => ({})), unix: socket };\n return Effect.provideService(effect, FetchHttpClient.RequestInit, init);\n });\n };\n\n/** One exchange, classified. The body is the parsed JSON object, or undefined for absent / 204. */\nexport const baoCall = (\n intent: BaoIntent,\n method: 'DELETE' | 'GET' | 'POST' | 'PUT',\n path: string,\n body?: Readonly<Record<string, unknown>>,\n) =>\n Effect.gen(function* () {\n const env = yield* BaoEnv;\n const address = resolveAddress(env);\n const client = yield* HttpClient.HttpClient;\n // ⚠️ The content type goes on `bodyText`, never in the header map: `bodyText` overwrites it.\n // <estate>/proxmox/src/client.ts has the scar.\n const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(\n HttpClientRequest.setHeaders(headersFor(address, env)),\n body === undefined\n ? (self) => self\n : HttpClientRequest.bodyText(JSON.stringify(body), 'application/json'),\n );\n // ★ Status first, body read exactly once, as text — an error body is not reliably JSON.\n const exchange = client.execute(request).pipe(\n Effect.flatMap((response) =>\n Effect.map(response.text, (text) => ({ status: response.status, text })),\n ),\n Effect.timeout(BAO_TIMEOUT),\n // ⛔ `.message` ONLY: the HttpClientError message is the reason, method and URL. The error\n // object itself holds the request, whose headers hold the token.\n Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])),\n );\n // ★ THE PERMIT WRAPS THE TIMEOUT, NOT THE REVERSE: a call queued behind the gate is not yet\n // talking to OpenBao, so its wait must not count against the 60 seconds.\n const gate = yield* BaoGate;\n const { status, text } = yield* retryTransport(\n exchange.pipe(\n overSocket(address.socket, method, path),\n redactingToken,\n Semaphore.withPermits(gate, 1),\n ),\n );\n const outcome = settle(intent, method, path, status, text);\n return 'error' in outcome ? yield* Effect.fail(outcome.error) : outcome.body;\n });\n\n/**\n * The `data` of a read, or undefined when OpenBao says the object is ABSENT (404).\n *\n * ★ `bao read -format=json` printed this same response body; the old helper took `data` when it was\n * an object and the whole body otherwise, and so does this.\n */\nexport const baoRead = (path: string) =>\n Effect.map(baoCall('read', 'GET', path), (body) => {\n if (body === undefined) return undefined;\n const data = body['data'];\n return typeof data === 'object' && data !== null\n ? (data as Record<string, unknown>)\n : (body as Record<string, unknown>);\n });\n\n/**\n * A write whose SUCCESS MATTERS. Any non-2xx fails, 404 included.\n *\n * ★ `PUT` IS WHAT `bao write` SENT (openbao v2.6.2 api/logical.go Write); `sys/mounts` enable and\n * tune are `POST` in the same client (api/sys_mounts.go:57, :201).\n * ⚠️ ROLE BODIES STAY ALL-STRING ON PURPOSE. `bao write k=v` put every value in the JSON body as a\n * string — MEASURED with `-output-curl-string`, see ssh-role-form.ts — and the form files still\n * build exactly that body, so the server parses the same request it was always sent.\n * ⛔ EXCEPT A MAP FIELD: sdk v2.6.2 refuses a string for a TypeMap with a 400, so Bao.SshRole\n * sends its two maps as objects (the ⛔ on mapValue in ssh-role-form.ts, measured 2026-09-21).\n */\nexport const baoWrite = (\n method: 'POST' | 'PUT',\n path: string,\n body: Readonly<Record<string, unknown>>,\n) => Effect.asVoid(baoCall('write', method, path, body));\n\n/** A delete: idempotent — already gone (404) is success — and refused (403, 5xx) is a failure. */\nexport const baoDelete = (path: string) => Effect.asVoid(baoCall('delete', 'DELETE', path));\n",
10
+ "/**\n * Where an OpenBao call goes and which headers it carries — resolved from the environment the way\n * the `bao` CLI resolves it, so the provider and `bao kv get` can never disagree about which server\n * or namespace this machine is talking to.\n *\n * ★ READ FROM openbao v2.6.2, NOT RECALLED:\n * · api/client.go:34-66 — BAO_ADDR, BAO_AGENT_ADDR, BAO_NAMESPACE, BAO_TOKEN, and the header\n * names `X-Vault-Namespace` and `X-Vault-Token`.\n * · api/client.go:370 — the default address, `https://127.0.0.1:8200`.\n * · api/client.go:503-507, 617-618, 761-762 — BAO_AGENT_ADDR, when set, WINS over BAO_ADDR.\n * · api/env.go:22-34 — every BAO_* falls back to its VAULT_* twin, but a BAO_* that is present\n * and EMPTY still wins over the VAULT_* one.\n * · api/client.go:776-785 — the token and namespace are applied only when non-empty, and every\n * request carries `X-Vault-Request: true`.\n * · api/client.go:1349-1354 — the request path is joined onto the address's own path.\n *\n * ⛔ WHAT THE CLI DOES AND THIS DELIBERATELY DOES NOT: with BAO_TOKEN unset, `bao` falls back to its\n * token helper (a token file in $HOME). This never reads that file. No BAO_TOKEN means AGENT\n * MODE — see `headersFor`.\n * ⚠️ NOT CARRIED OVER: BAO_CACERT / BAO_SKIP_VERIFY / client certificates, SRV lookup, and the CLI's\n * retries on 5xx. An https address needs a certificate the runtime already trusts; a TLS failure\n * fails the call loudly rather than being skipped.\n */\n\nexport type BaoEnvironment = Readonly<Record<string, string | undefined>>;\n\nexport type BaoAddress = {\n /** Scheme, host and any path prefix, no trailing slash. `http://localhost` for a socket. */\n readonly base: string;\n /** The socket path when the address is `unix:///path`, otherwise undefined. */\n readonly socket: string | undefined;\n /** The namespace to send, when there is one. */\n readonly namespace: string | undefined;\n};\n\nexport const DEFAULT_ADDRESS = 'https://127.0.0.1:8200';\n\nconst UNIX = 'unix://';\n\n/** api/env.go ReadBaoVariable: present (even empty) BAO_* wins, else the VAULT_* twin. */\nexport const readBaoVariable = (env: BaoEnvironment, name: `BAO_${string}`): string =>\n env[name] ?? env[`VAULT_${name.slice('BAO_'.length)}`] ?? '';\n\n/**\n * The address and namespace for this environment. Carries no token, so it is safe to log.\n *\n * ★ UNIX SOCKETS, BECAUSE AN AGENT LISTENER CAN BE ONE. OpenBao Agent's `listener \"unix\"` serves\n * the same API on a socket (website/content/docs/agent-and-proxy/agent/index.mdx:382-384), and the\n * agent advertises such a listener as `unix://<path>` (command/agent.go:565-566). The CLI handles\n * that address by dialling the socket and speaking plain HTTP to host `localhost`\n * (api/client.go:654-667); `base` and `socket` are exactly those two halves.\n */\nexport const resolveAddress = (env: BaoEnvironment): BaoAddress => {\n const agent = readBaoVariable(env, 'BAO_AGENT_ADDR');\n const direct = readBaoVariable(env, 'BAO_ADDR');\n const raw = agent !== '' ? agent : direct !== '' ? direct : DEFAULT_ADDRESS;\n const namespace = readBaoVariable(env, 'BAO_NAMESPACE');\n const socket = raw.startsWith(UNIX) ? raw.slice(UNIX.length) : undefined;\n return {\n base: socket === undefined ? raw.replace(/\\/+$/, '') : 'http://localhost',\n namespace: namespace === '' ? undefined : namespace,\n socket,\n };\n};\n\n/**\n * The headers one call sends.\n *\n * ★ AGENT MODE IS \"NO TOKEN HEADER AT ALL\", AND THAT IS THE AGENT'S OWN CONTRACT. Read in openbao\n * v2.6.2 command/agentproxyshared/cache/handler.go:29-38: the proxy handler takes the request's\n * `X-Vault-Token`, and only when it is EMPTY and an auto-auth sink exists does it substitute the\n * agent's own token. command/agent.go:517-541 creates that sink exactly when\n * `api_proxy { use_auto_auth_token = true }` is set; command/agent/config/config.go:288-292 and\n * 635-637 map the older `cache { use_auto_auth_token }` onto the same setting; and\n * command/proxy.go:495-518 wires OpenBao Proxy identically. The docs say the same in words\n * (website/content/docs/agent-and-proxy/agent/apiproxy.mdx:57-62): a request without a token is\n * forwarded with the auto-auth token, a request that bears one keeps its own, and `\"force\"`\n * overwrites it.\n * ⛔ SO AN EMPTY `X-Vault-Token: ` WOULD BE WRONG IN BOTH DIRECTIONS: harmless to the agent,\n * which reads empty as absent, but an explicit unauthenticated request to a server. The header\n * is omitted, never blanked.\n * ★ THE NAMESPACE RIDES THROUGH THE AGENT UNTOUCHED. The proxy clones its client and replaces that\n * client's headers with the incoming request's (cache/api_proxy.go:50-54 and :78), so the\n * `X-Vault-Namespace` sent here is the one OpenBao sees.\n * ⚠️ `X-Vault-Request: true` IS NOT DECORATION. An agent listener with `require_request_header`\n * refuses any request without it (command/agent.go:544-548); the CLI always sends it.\n *\n * ⛔ THE RETURN VALUE HOLDS THE TOKEN. Hand it to the request and nowhere else: never to an error,\n * a log line, an attribute, or a span — bao-http.ts adds the header to the redacted names.\n */\nexport const headersFor = (address: BaoAddress, env: BaoEnvironment): Record<string, string> => {\n const headers: Record<string, string> = { 'X-Vault-Request': 'true' };\n if (address.namespace !== undefined) headers['X-Vault-Namespace'] = address.namespace;\n const token = readBaoVariable(env, 'BAO_TOKEN');\n if (token !== '') headers['X-Vault-Token'] = token;\n return headers;\n};\n",
11
+ "/**\n * How a finished OpenBao HTTP exchange reads: a body, a genuinely absent object, or an error.\n *\n * ★ STATUS CODES, NEVER WORDS. The `bao` CLI this replaced reported every outcome as stderr text,\n * and deciding by that text broke twice: every failure once read as absent (a bad token planned 33\n * updates), and then `bao policy read` turned out to say \"absent\" in different words from\n * `bao read`, so a new policy could not be created (b3c18a33e). The API itself answers 404 for\n * absent, 403 for denied and 503 for sealed — openbao v2.6.2 sdk/logical/response_util.go:\n * RespondErrorCommon (a read that finds nothing is 404; ErrPermissionDenied is 403) and\n * AdjustErrorStatusCode (ErrSealed is 503).\n */\n\nexport type BaoIntent = 'read' | 'write' | 'delete';\n\n/**\n * A failed call, naming the method, the path and OpenBao's own `errors` array.\n *\n * ⛔ IT NEVER CARRIES THE TOKEN, AND IT NEVER CARRIES A RESPONSE BODY IT DID NOT PARSE. It is built\n * from the method, the API path and OpenBao's error strings only — no request, no headers, no\n * `cause` — because an Effect `HttpClientRequest` inspects its headers through a redaction list\n * that does not name `x-vault-token`. A 2xx body can hold a credential, and a foreign error page\n * can echo the request back, so a body that is not OpenBao's is described, never quoted.\n */\nexport class BaoError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n readonly errors: readonly string[],\n ) {\n const code = status === 0 ? 'no response' : String(status);\n super(`OpenBao ${method} /v1/${path} -> ${code}: ${errors.join('; ') || '(no errors given)'}`);\n this.name = 'BaoError';\n }\n}\n\nexport type Settled =\n | { readonly body: Record<string, unknown> | undefined }\n | { readonly error: BaoError };\n\nconst objectOf = (text: string): Record<string, unknown> | undefined => {\n try {\n const parsed: unknown = JSON.parse(text);\n return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * OpenBao's `errors` array, or undefined for a body that is not OpenBao's.\n *\n * ★ EVERY ERROR OPENBAO WRITES IS `{\"errors\": [...]}` WITH A JSON CONTENT TYPE, 404 included —\n * sdk/logical/response_util.go RespondError:180-196 — and every entry is a STRING.\n * ⛔ AN ARRAY WITH ANY NON-STRING ENTRY IS NOT OPENBAO. The first version filtered the non-strings\n * out, which turned a JSON gateway's `{\"errors\":[{\"code\":1000,\"message\":\"Not Found\"}]}` into an\n * empty OpenBao error list — and a 404 carrying it into \"absent\" on a read and \"done\" on a delete,\n * the plan-CREATE-over-something-that-exists failure arriving through a wrong BAO_ADDR. Found by\n * grok's review on 2026-09-14. An EMPTY array is still OpenBao's: a plain 404 carries exactly that.\n */\nconst errorsOf = (text: string): readonly string[] | undefined => {\n const raw = objectOf(text)?.['errors'];\n return Array.isArray(raw) && raw.every((entry) => typeof entry === 'string')\n ? (raw as string[])\n : undefined;\n};\n\n/**\n * Classify one exchange.\n *\n * ★ PURE AND EXPORTED SO THE ONE DECISION EVERY READ AND DELETE HERE RESTS ON CAN BE TESTED WITHOUT\n * A VAULT. It has been wrong in both directions already — every failure once read as absent, and\n * one command's absence wording was assumed for another's — see bao-status.test.ts.\n *\n * ⛔ 404 IS ABSENCE ONLY ON A READ, AND ONLY WHEN OPENBAO SAID IT. MEASURED 2026-09-13 with the CLI,\n * and the discriminator is real but NARROW: a genuinely absent object, read by a token that IS\n * allowed to read that path, is a 404. ⛔ AND THE SAME READ UNDER A NARROWER TOKEN IS A DENIAL:\n * under the agent lane the identical read answered `Code: 403 … permission denied`, because the\n * policy grants named paths and an unnamed one is refused rather than reported missing. So\n * \"absent\" is only knowable to a caller that could have read it — which is exactly why 403 is an\n * ERROR rather than a shrug. A resource that cannot tell absence from denial plans CREATE for\n * something that exists, which is the failure this estate has now found four separate times.\n * ⚠️ A 404 WHOSE BODY IS NOT `{\"errors\": […strings]}` IS NOT OPENBAO. A BAO_ADDR pointing at the\n * wrong server — a reverse proxy's HTML 404, a gateway's JSON one — must not read as \"nothing\".\n *\n * 🔴 THE SHELL HELPER USED TO RETURN `''` FOR EVERY NON-ZERO EXIT, AND THAT IS A LIE WITH\n * CONSEQUENCES. An expired BAO_TOKEN, a sealed server or a missing grant all came back as \"the\n * mount is not there\", so `diff` answered `update`, `reconcile` ran `secrets enable`, and the plan\n * reported a CREATE on infrastructure that exists. The failure the operator sees is about the\n * wrong thing entirely, at the worst possible moment — a sealed vault. ⚠️ SO 403, 5xx AND ANYTHING\n * ELSE THAT IS NOT 2xx FAILS. The cost is that a genuinely-absent read under a too-narrow token\n * becomes an error instead of a create, and that is the right way round: \"I could not tell\" is a\n * true statement, and \"it is not there\" was not.\n *\n * 🔴 THE DELETES ALL USED THE READ HELPER, WHICH DISCARDED THE EXIT CODE. A refused\n * `secrets disable` was reported as a successful delete: the resource left Alchemy's state while\n * the mount stayed live, so the next plan showed nothing and the object was orphaned in silence.\n * ⚠️ ABSENCE IS STILL NOT AN ERROR FOR A DELETE, because a delete must be idempotent: Alchemy\n * retries, and the second attempt legitimately finds nothing. So a delete's 404 is success and\n * its 403 is not.\n * ★ DISABLING A MOUNT THAT IS ALREADY GONE IS NOT EVEN A 404. A review on 2026-09-14 said it failed\n * with `no matching mount` and would wedge a retried delete. The server says otherwise: it answers\n * success for a missing mount so as not to reveal whether one existed — openbao v2.6.2\n * vault/logical_system.go:1211-1216 — and the CLI printed \"Disabled the secrets engine (if it\n * existed)\" (command/secrets_disable.go:87).\n */\nexport const settle = (\n intent: BaoIntent,\n method: string,\n path: string,\n status: number,\n text: string,\n): Settled => {\n const fail = (errors: readonly string[]) => ({\n error: new BaoError(status, method, path, errors),\n });\n if (status < 200 || status >= 300) {\n const errors = errorsOf(text);\n if (errors === undefined) {\n // ⛔ DESCRIBED, NEVER QUOTED. A proxy or gateway error page can echo the request back, headers\n // and all, so quoting it could carry X-Vault-Token into an error that Alchemy logs (grok,\n // 2026-09-14). mint in <estate>/proxmox/src/credentials.ts already did it this way.\n return fail([`not an OpenBao error body (${String(text.length)} bytes)`]);\n }\n if (status === 404 && intent !== 'write') return { body: undefined };\n return fail(errors);\n }\n // ★ A write or delete answering 204 has nothing to say. A READ with no body has told us nothing,\n // and OpenBao never does that — a read that finds nothing is a 404 (RespondErrorCommon).\n if (text.trim() === '') {\n return intent === 'read' ? fail(['a successful read with an empty body']) : { body: undefined };\n }\n const body = objectOf(text);\n return body === undefined ? fail(['a success body that is not a JSON object']) : { body };\n};\n",
12
+ "/**\n * The digest every Bao.* family persists instead of a value — moved here from the retired\n * bao-shell.ts, unchanged, when the package stopped shelling out to `bao`.\n */\n\n/** ⚠️ Bun's hasher, not node:crypto — this runs under `bun --bun alchemy`. */\n/**\n * The only form a policy body may be hashed or compared in.\n *\n * ⛔ OPENBAO STRIPS THE TRAILING NEWLINE AND THAT ALONE WAS A FOREVER-DIFF. MEASURED on the live\n * server: the assembler emits each fragment followed by a newline, so `app-llm` goes in as\n * 41 lines and `bao policy read` hands back 40 — identical text, one `\\n` apart, two different\n * digests. Every plan therefore reported `update`, every deploy rewrote the same policy, and the\n * stack could never settle. It was invisible until the stack planned for the first time today.\n * ★ THE HTTP API HANDS BACK THE SAME STRIPPED TEXT. `bao policy read` printed `data.policy` of\n * `GET /v1/sys/policies/acl/<name>` (openbao v2.6.2 api/sys_policy.go:60-81), which is what\n * policy-wire.ts now reads directly — so the canonicalisation is still load-bearing.\n *\n * ⚠️ `trimEnd` ONLY. Leading and interior whitespace are part of the HCL and must not be touched —\n * a policy that differs by an indent really is a different document, and quietly folding that\n * away would hide an edit rather than a formatting artifact.\n */\nexport const canonical = (text: string) => text.trimEnd();\n\nexport const sha256 = (text: string) =>\n new Bun.CryptoHasher('sha256').update(canonical(text)).digest('hex');\n",
13
+ "/**\n * The named Ceph config-option list — the only options `ceph config get/set/rm` may ever touch\n * through the mon-command transport (ceph-argv.ts's `configProblem`). Design:\n * docs/plans/2026-09-26-ceph-config-options.md, D-C1.\n *\n * ⛔ THE LIST STARTS EMPTY, AND EACH ADDITION IS ITS OWN REVIEWED PR LINE. Nothing in K-A4 adds to\n * it — `CephConfigOption` (K-D) is a separate, later family; this file exists now only so the\n * transport's allowlist has something to bound `config` against from day one, per the accepted\n * design (\"config get/set for the named option list which starts EMPTY\").\n * ⛔ NEVER ADD ONE OF THE LOCKOUT CLASSES BELOW. `mon_host`, `public_network`, `cluster_network`,\n * `public_addr`, `cluster_addr`, every `auth_*_required` and every `key`/`keyring`-bearing\n * option re-address the mons or de-authenticate the whole cluster — cutting every VM disk — and\n * `isLockoutConfigOption` is a structural guard a test runs against this file's own list, not a\n * suggestion for the reviewer of the next PR line.\n * ⛔ NEVER ADD A SECRET-VALUED OPTION. mgr module options can hold passwords (REASONED — no live\n * cluster to measure this against yet); a secret-bearing setting belongs in a vault-backed flow,\n * never a `ceph config set` argv, which lands in the runner's log and the node's sudo journal.\n */\n\n/** Starts empty. Add one reviewed option name per PR line, never a batch. */\nexport const NAMED_CONFIG_OPTIONS: ReadonlySet<string> = new Set([]);\n\nconst LOCKOUT_EXACT: ReadonlySet<string> = new Set([\n 'mon_host',\n 'public_network',\n 'cluster_network',\n 'public_addr',\n 'cluster_addr',\n]);\n\n/** `auth_*_required` (cephx enforcement) and anything key/keyring-shaped, either side of `_`. */\nconst LOCKOUT_PATTERN = /^auth_\\w*_required$|(^|_)key(ring)?($|_)/;\n\n/** Whether `name` is a class this transport must never read a value for, adopt, or write. */\nexport const isLockoutConfigOption = (name: string): boolean =>\n LOCKOUT_EXACT.has(name) || LOCKOUT_PATTERN.test(name);\n",
14
+ "/**\n * The `ceph config get/set/rm` half of the argv allowlist — split out of ceph-argv.ts (which\n * re-exports `configProblem`) purely to keep that file under the house's 250-line file cap; the\n * split follows the same line the design docs already draw (2026-09-26-ceph-config-options.md is\n * its own companion to 2026-09-26-ceph-mon-transport.md).\n *\n * ⚠️ `<who>` IS NOT YET BOUNDED TO K-D's \"declared daemons\" (the config-options design's own\n * acceptance test #1). `client.admin` still matches this shape. Latent today:\n * `NAMED_CONFIG_OPTIONS` starts empty, so `namedOptionProblem` refuses every `config get/set/rm`\n * regardless of `who` — LAND red team, 2026-09-26, flagged this as armed only once K-D's first\n * named option lands, and narrowing it needs that family's own declared-daemon list, which does\n * not exist yet. Tracked for `CephConfigOption` (K-D), not fixed here.\n */\nimport { NAMED_CONFIG_OPTIONS, isLockoutConfigOption } from './ceph-config-options.ts';\n\n/** `config`'s `<who>`: a daemon class, optionally `.id` — never a path, never free text. */\nconst WHO = /^(global|mon|osd|mgr|mds|client)(\\.[A-Za-z0-9_-]{1,32})?$/;\n/** A ceph option name's own shape — snake_case, lowercase. Independent of the named list below. */\nconst OPTION_NAME = /^[a-z][a-z0-9_]{0,63}$/;\n/**\n * No shell metacharacters; the named list (below) is what actually decides secrecy, not this.\n * ⛔ NO LEADING `-` EXCEPT A NEGATIVE INTEGER, AND NEVER EMPTY. LAND red team (2026-09-26),\n * CONFIRMED: the prior pattern let a config VALUE start with `-`, and this argv reaches\n * `sudo -n /usr/bin/ceph` with no shell in between — no shell means no quoting boundary, so\n * `ceph config set global x -o/etc/ceph/ceph.conf` parses as ceph's own `-o <file>` flag, not\n * as the string `-o/etc/ceph/ceph.conf`. Accepted flag-shaped values included\n * `--admin-daemon=/var/run/ceph/ceph-mon.a.asok`, `--conf=/tmp/x`, `-n`, and the empty string.\n * The sudoers entry has no argument restriction of its own (that is D4, a later step), so this\n * allowlist is the only thing standing between a config value and an arbitrary ceph CLI flag.\n * Latent while `NAMED_CONFIG_OPTIONS` is empty; K-D's first entry arms it.\n */\nconst CONFIG_VALUE = /^(?:[A-Za-z0-9][A-Za-z0-9 ._:/=,-]{0,255}|-[0-9]{1,255})$/;\n\n/**\n * `undefined` when `value` is a safe ceph config value — exported (like `boundedEntityProblem`)\n * so the shape itself is directly testable, since `NAMED_CONFIG_OPTIONS` being empty today means\n * `cephCommandProblem` never reaches this check on any real argv (`namedOptionProblem` refuses\n * first) — this stays exercised until K-D's first named option arms the path for real.\n */\nexport const configValueProblem = (value: string): string | undefined =>\n CONFIG_VALUE.test(value)\n ? undefined\n : `config value ${JSON.stringify(value)} contains characters outside the safe set`;\n\n/**\n * `undefined` when `name` may be declared, adopted, read by value or written — see\n * ceph-config-options.ts's header for why the list starts empty and stays reviewed line by line.\n */\nconst namedOptionProblem = (name: string): string | undefined => {\n if (!OPTION_NAME.test(name)) return `${JSON.stringify(name)} is not a plain ceph option name`;\n if (isLockoutConfigOption(name))\n return `${name} is a lockout-class option and can never be declared`;\n return NAMED_CONFIG_OPTIONS.has(name)\n ? undefined\n : `${name} is not on the named option list (it starts empty — see ceph-config-options.ts)`;\n};\n\n/** `config get|set|rm <who> <name> [value]`, called with the operands after `config` stripped. */\nexport const configProblem = (rest: readonly string[]): string | undefined => {\n const [verb, who, name, ...extra] = rest;\n if (who === undefined || !WHO.test(who)) {\n return 'config takes a bounded `<who>` (global/mon/osd/mgr/mds/client[.id])';\n }\n if (name === undefined) return 'config takes a `<name>` from the named option list';\n if (verb === 'get' || verb === 'rm') {\n return extra.length === 0\n ? namedOptionProblem(name)\n : `config ${String(verb)} takes exactly \\`<who> <name>\\``;\n }\n if (verb === 'set') {\n const [value, ...more] = extra;\n if (value === undefined || more.length !== 0)\n return 'config set takes exactly `<who> <name> <value>`';\n const named = namedOptionProblem(name);\n return named !== undefined ? named : configValueProblem(value);\n }\n return `ceph config ${String(verb)} is not on the allowlist`;\n};\n",
15
+ "/**\n * The client-side argv allowlist for the Ceph mon-command transport (K-A4) — an allowlist of\n * SHAPES, not of subcommands, the same discipline `../linux/sudo-allowlist.ts` uses for the file\n * family. Pure: no I/O, so every shape is tested without ssh (ceph-argv.test.ts).\n *\n * Design: docs/plans/2026-09-26-ceph-mon-transport.md (\"Transport options\", T-A) and\n * docs/plans/2026-09-26-ceph-config-options.md (the named option list this file bounds `config`\n * to). Both are REASONED against public Ceph docs, not measured on this estate's cluster — no\n * mon exists yet for a live check, which is exactly why every shape here is bounded as tightly as\n * the design allows rather than merely \"shaped like a ceph command\".\n *\n * ⛔ `auth ls` IS NEVER ALLOWED. It prints every key on the cluster, and the estate's own\n * 2026-09-25 lesson is that an exclusion filter is how a token leaks — allowlist extraction\n * only, never \"everything except\". A caller that wants an inventory uses the committed\n * read-only script this design defers to, never this transport.\n * ⛔ `auth del` IS NEVER ALLOWED, PERIOD — not behind a flag, not for any entity. The lockout-safety\n * rule is \"never auto-delete\" (D3): a wrong delete here de-authenticates whatever the entity's\n * caps still let VM disks depend on, and it does not ask twice.\n * ⛔ THE ENTITY OPERAND IS BOUNDED TO THE `client.k8s-` PREFIX. `client.admin`, `mon.*`, `osd.*`,\n * `mgr.*` and the PVE storage client are the keyrings the cluster and every VM disk run on;\n * nothing this allowlist accepts can name one.\n * ⛔ EVERY CHECK RUNS AGAINST THE FINAL ARGV — `/usr/bin/ceph`, the real subcommand, and the\n * trailing `-f json` the transport always appends — never a pre-transform version, so what is\n * validated is exactly what reaches `sudo -n`.\n */\nimport { configProblem } from './ceph-argv-config.ts';\n\nexport { configValueProblem } from './ceph-argv-config.ts';\n\n/** ★ Absolute, so neither sudo nor `ceph` is looked up on the operator's PATH. */\nexport const CEPH_BIN = '/usr/bin/ceph';\n\n/** Every entity this transport may ever touch starts with this. */\nexport const ENTITY_PREFIX = 'client.k8s-';\n\n/** ⚠️ Bounded length too — a cap operand this long is not a plausible pool-scoped rbd profile. */\nconst ENTITY = /^client\\.k8s-[a-z0-9][a-z0-9-]{0,61}$/;\n\n/** `undefined` when `entity` is a name this transport may ever create, get or re-cap. */\nexport const boundedEntityProblem = (entity: string): string | undefined =>\n ENTITY.test(entity)\n ? undefined\n : `entity must match \\`${ENTITY_PREFIX}<name>\\` (lowercase, digits, hyphens), got ${JSON.stringify(entity)}`;\n\n/**\n * ⛔ THE DESIGN'S OWN CAPS, LITERALLY — NOT A GENERAL \"LOOKS LIKE A CAP STRING\" GRAMMAR. LAND red\n * team (2026-09-26), CONFIRMED: an earlier, wider pattern here (any of `[A-Za-z0-9 _.=,-]`, no\n * per-daemon shape) also matched `allow rwx`, bare `profile osd` / `profile bootstrap-osd`,\n * comma-chained grants (`profile rbd pool=k8s-rbd, allow rwx`), and — the sharpest one — an osd\n * `profile rbd` with NO pool, which grants rbd access to every pool on the cluster, the VM-disk\n * pool included. The design (2026-09-26-ceph-mon-transport.md, \"Props\") only ever declares\n * `mon: 'profile rbd'` and `osd`/`mgr: 'profile rbd pool=<name>'`; these are that shape, exactly,\n * pool name bounded the same way an entity name is.\n */\nconst MON_CAP = /^profile rbd$/;\nconst POOL_SCOPED_CAP = /^profile rbd pool=[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;\n\nconst DAEMONS = ['mon', 'osd', 'mgr'] as const;\n\n/** `undefined` when `value` is `daemon`'s one allowed cap shape (the design's own caps map). */\nconst capProblem = (\n daemon: (typeof DAEMONS)[number],\n value: string | undefined,\n): string | undefined => {\n if (value === undefined) return `${daemon} cap is missing`;\n const shape = daemon === 'mon' ? MON_CAP : POOL_SCOPED_CAP;\n return shape.test(value)\n ? undefined\n : `${daemon} cap ${JSON.stringify(value)} is not the design's plain pool-scoped rbd profile`;\n};\n\n/** `auth get <entity>`: the read `Ceph.AuthEntity` uses to observe caps, never the key. */\nconst authGetProblem = (ops: readonly string[]): string | undefined => {\n const [entity, ...extra] = ops;\n if (entity === undefined || extra.length !== 0) return 'auth get takes exactly `<entity>`';\n return boundedEntityProblem(entity);\n};\n\n/**\n * `auth get-or-create <entity> mon <cap> osd <cap> mgr <cap>` and `auth caps` (same operand\n * shape). ★ FIXED ORDER, EXACTLY THREE PAIRS — the design's own caps map is always\n * `{mon, osd, mgr}`; a variable-length daemon list is more allowlist than this family needs,\n * and less allowlist is less to get wrong.\n */\nconst capsTripleProblem = (ops: readonly string[]): string | undefined => {\n const [entity, ...pairs] = ops;\n if (entity === undefined)\n return `takes exactly \\`<entity> ${DAEMONS.map((d) => `${d} <cap>`).join(' ')}\\``;\n const entityProblem = boundedEntityProblem(entity);\n if (entityProblem !== undefined) return entityProblem;\n if (pairs.length !== DAEMONS.length * 2) {\n return `takes exactly \\`<entity> ${DAEMONS.map((d) => `${d} <cap>`).join(' ')}\\``;\n }\n for (const [index, daemon] of DAEMONS.entries()) {\n const key = pairs[index * 2];\n const value = pairs[index * 2 + 1];\n if (key !== daemon)\n return `expected \\`${daemon}\\` in position ${String(index + 1)}, got ${JSON.stringify(key)}`;\n const problem = capProblem(daemon, value);\n if (problem !== undefined) return problem;\n }\n return undefined;\n};\n\nconst authProblem = (rest: readonly string[]): string | undefined => {\n const [verb, ...ops] = rest;\n if (verb === 'ls') {\n return (\n 'auth ls prints every key on the cluster; use the committed read-only inventory ' +\n 'script instead, never this transport'\n );\n }\n if (verb === 'del')\n return 'auth del is refused entirely — lockout safety, never auto-delete (D3)';\n if (verb === 'get') return authGetProblem(ops);\n if (verb === 'get-or-create') return capsTripleProblem(ops);\n if (verb === 'caps') return capsTripleProblem(ops);\n return `ceph auth ${String(verb)} is not on the allowlist`;\n};\n\n/** Every call this allowlist accepts ends in this — the transport appends it, never the caller. */\nconst JSON_FORMAT = ['-f', 'json'] as const;\n\nconst withJsonSuffix = (\n rest: readonly string[],\n check: (ops: readonly string[]) => string | undefined,\n): string | undefined => {\n const suffix = rest.slice(-2);\n if (suffix[0] !== JSON_FORMAT[0] || suffix[1] !== JSON_FORMAT[1]) {\n return 'every ceph call in this allowlist ends `-f json`';\n }\n return check(rest.slice(0, -2));\n};\n\n/**\n * `undefined` when `argv` is exactly an allowed ceph shape (the FULL argv `sudo -n` would run,\n * `/usr/bin/ceph` included), else the reason it is not.\n */\nexport const cephCommandProblem = (argv: readonly string[]): string | undefined => {\n const [bin, group, ...rest] = argv;\n if (bin !== CEPH_BIN) return `${JSON.stringify(bin)} is not the allowlisted ceph binary`;\n if (group === 'quorum_status') {\n return rest.length === 2 && rest[0] === JSON_FORMAT[0] && rest[1] === JSON_FORMAT[1]\n ? undefined\n : 'quorum_status takes exactly `-f json`';\n }\n if (group === 'auth') return withJsonSuffix(rest, authProblem);\n if (group === 'config') return withJsonSuffix(rest, configProblem);\n return `ceph ${String(group)} is not on the allowlist`;\n};\n\n/** The exact argv for each allowed shape, so callers never hand-assemble one that could drift. */\nexport const authGetArgv = (entity: string): readonly string[] => [\n CEPH_BIN,\n 'auth',\n 'get',\n entity,\n ...JSON_FORMAT,\n];\n\nexport const authGetOrCreateArgv = (\n entity: string,\n caps: Readonly<Record<(typeof DAEMONS)[number], string>>,\n): readonly string[] => [\n CEPH_BIN,\n 'auth',\n 'get-or-create',\n entity,\n ...DAEMONS.flatMap((daemon) => [daemon, caps[daemon]]),\n ...JSON_FORMAT,\n];\n\nexport const authCapsArgv = (\n entity: string,\n caps: Readonly<Record<(typeof DAEMONS)[number], string>>,\n): readonly string[] => [\n CEPH_BIN,\n 'auth',\n 'caps',\n entity,\n ...DAEMONS.flatMap((daemon) => [daemon, caps[daemon]]),\n ...JSON_FORMAT,\n];\n\nexport const quorumStatusArgv = (): readonly string[] => [\n CEPH_BIN,\n 'quorum_status',\n ...JSON_FORMAT,\n];\n",
16
+ "/**\n * Parsing for the three ceph `-f json` shapes `Ceph.AuthEntity` reads: `auth get`, `auth\n * get-or-create` and `quorum_status`. Pure: no I/O, tested with fixture stdout\n * (ceph-auth-parse.test.ts) — there is no live mon to capture real output from yet, so every shape\n * below is REASONED from public Ceph documentation, not measured, and an unexpected shape is\n * always an error, never a guess.\n *\n * ⛔ `parseAuthGet`'S RETURN TYPE HAS NO `key` FIELD. Decision 65 (2026-09-26, LAND finding 5)\n * amended the design to let the observe step read `auth get`'s unfiltered stdout on every\n * reconcile, not only on create — but the key it carries is dropped in the same breath:\n * `parseKeyringEntry` reads it into a local binding and `parseAuthGet` never puts it in the\n * object it returns. Not a convention, a type: there is no field here to forget to strip.\n * ⛔ `parseQuorumStatus` FAILS CLOSED. A JSON parse error, a missing `quorum` array or an empty one\n * all answer `{ healthy: false }`; nothing here ever calls an unparseable answer healthy.\n */\n\nexport type CephCaps = { readonly mon: string; readonly osd: string; readonly mgr: string };\n\nexport type CephExecOutcome = {\n readonly exitCode: number;\n readonly stdout: string;\n readonly stderr: string;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null;\n\nconst parseCaps = (value: unknown, entity: string): CephCaps => {\n if (!isRecord(value)) throw new Error(`ceph auth: ${entity}'s entry has no caps object`);\n const { mon, osd, mgr } = value;\n if (typeof mon !== 'string' || typeof osd !== 'string' || typeof mgr !== 'string') {\n throw new Error(`ceph auth: ${entity}'s caps are missing a mon, osd or mgr string`);\n }\n return { mgr, mon, osd };\n};\n\n/**\n * REASONED shape (public `ceph auth get -f json` / `auth get-or-create -f json` docs): a JSON\n * array with one object per entity, `{entity, key, caps: {mon, osd, mgr}}`.\n */\nconst parseKeyringEntry = (\n stdout: string,\n entity: string,\n): { readonly caps: CephCaps; readonly key: string } => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(stdout);\n } catch {\n // ⛔ NEVER INTERPOLATE THE CATCH VALUE. LAND red team (2026-09-26), CONFIRMED: both Bun's and\n // Node's `JSON.parse` errors quote a slice of the offending input — a bare (unquoted) or\n // comma-adjacent key in this stdout put a slice of a real cephx key straight into this\n // thrown error (measured: median 32/40 chars of a 1000-key sample). `stdout` carries the\n // key on every reconcile since decision 65, so this message names only what failed, never\n // any part of what it was reading.\n throw new Error(`ceph auth ${entity}: stdout did not parse as JSON`);\n }\n const row = Array.isArray(parsed)\n ? parsed.find((entry) => isRecord(entry) && entry['entity'] === entity)\n : undefined;\n if (!isRecord(row)) throw new Error(`ceph auth ${entity}: no matching entry in the JSON output`);\n const { key } = row;\n if (typeof key !== 'string' || key.trim() === '') {\n throw new Error(`ceph auth ${entity}: the entry has no key field`);\n }\n return { caps: parseCaps(row['caps'], entity), key };\n};\n\nexport type CephAuthObserved =\n | { readonly kind: 'present'; readonly caps: CephCaps }\n | { readonly kind: 'absent' };\n\n/**\n * ⚠️ ABSENCE IS RECOGNISED ONLY BY `ENOENT` IN STDERR (REASONED from `ceph auth get`'s documented\n * error text). Any other nonzero exit — a permission problem, a malformed entity ceph itself\n * refused, a mon mid-election — propagates as an error, never as \"absent\": the transient-read\n * test every family in this kit carries.\n */\nexport const parseAuthGet = (result: CephExecOutcome, entity: string): CephAuthObserved => {\n if (result.exitCode !== 0) {\n if (result.stderr.includes('ENOENT')) return { kind: 'absent' };\n throw new Error(`ceph auth get ${entity} failed: ${result.stderr.trim().slice(0, 300)}`);\n }\n const { caps } = parseKeyringEntry(result.stdout, entity);\n return { caps, kind: 'present' };\n};\n\n/**\n * ⚠️ `get-or-create` NEVER READS AS ABSENT — REASONED: it either creates, returns the existing\n * entity unchanged, or fails outright (e.g. an existing entity with different caps). A nonzero\n * exit here is always an error.\n */\nexport const parseAuthGetOrCreate = (\n result: CephExecOutcome,\n entity: string,\n): { readonly caps: CephCaps; readonly key: string } => {\n if (result.exitCode !== 0) {\n throw new Error(\n `ceph auth get-or-create ${entity} failed: ${result.stderr.trim().slice(0, 300)}`,\n );\n }\n return parseKeyringEntry(result.stdout, entity);\n};\n\nexport type CephQuorum =\n | { readonly healthy: true }\n | { readonly healthy: false; readonly reason: string };\n\n/** REASONED shape (public `ceph quorum_status -f json` docs): an object with a `quorum` array. */\nexport const parseQuorumStatus = (stdout: string): CephQuorum => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(stdout);\n } catch (cause) {\n return { healthy: false, reason: `stdout did not parse as JSON (${String(cause)})` };\n }\n if (!isRecord(parsed)) return { healthy: false, reason: 'stdout was not a JSON object' };\n const quorum = parsed['quorum'];\n return Array.isArray(quorum) && quorum.length > 0\n ? { healthy: true }\n : { healthy: false, reason: 'no non-empty `quorum` array in the quorum_status output' };\n};\n",
17
+ "/**\n * The Ceph mon-command transport (K-A4, T-A): ssh to one of the TB4 mon nodes, `sudo -n\n * /usr/bin/ceph …`, tried in node order. Every call is checked against ceph-argv.ts's allowlist\n * BEFORE anything dials — a refused shape never opens a connection, never mind runs a command.\n *\n * ★ BUILT ON THE SAME WIRE FORMAT sshSudoRunner USES, NOT A SECOND ONE. `runRemote` (the framed\n * script + nonce marker `../linux/ssh-command.ts` and `../linux/ssh-runner.ts` already ship and\n * test) is reused directly: same fail-closed behaviour on a dropped connection, same\n * `BatchMode=yes`, same host-key verification left to the operator's own ssh config.\n * ⛔ THE DIAL IS AN INJECTED SEAM, NOT child_process MOCKED. `CephDial` is the same idea as\n * `HostRunner` (../launchd/runner.ts): production code gets `sshCephDial`, tests hand a fake\n * function directly (fake-ceph-dial.ts) — no ssh binary, no spawned process, ever, in a test.\n * ⛔ A TRANSPORT FAILURE (no mon reachable) IS NEVER \"ABSENT\". Only a mon that actually ran the\n * command and answered gets to say whether an entity exists; every node failing to connect\n * propagates as an Error, so a down anchor can never read as \"recreate it\".\n * ⛔ ONLY ARGV IS EVER LOGGED. `log` is called with the shape being attempted, never with a\n * result's stdout or stderr — `auth get`/`auth get-or-create`'s stdout carries the entity's key.\n */\nimport { randomBytes } from 'node:crypto';\nimport { quoteArgv } from '../linux/ssh-command.ts';\nimport { runRemote } from '../linux/ssh-runner.ts';\nimport { SUDO, SudoRefusedError } from '../linux/sudo-allowlist.ts';\nimport { cephCommandProblem, quorumStatusArgv } from './ceph-argv.ts';\nimport { parseQuorumStatus } from './ceph-auth-parse.ts';\n\nexport type CephExecResult = {\n readonly exitCode: number;\n readonly stdout: string;\n readonly stderr: string;\n};\n\n/** One attempt against one mon: run `argv` (already sudo-wrapped by the caller) and report. */\nexport type CephDial = (node: string, argv: readonly string[]) => Promise<CephExecResult>;\n\nconst CONNECT_TIMEOUT_SEC = 10;\nconst EXEC_TIMEOUT_MS = 30_000;\n\n/** ★ A fresh nonce per attempt — this dial never reuses a connection or a marker across calls. */\nexport const sshCephDial: CephDial = async (node, argv) => {\n const nonce = randomBytes(8).toString('hex');\n const script = `${SUDO} -n -- ${quoteArgv(argv)}`;\n return runRemote(\n { connectTimeoutSec: CONNECT_TIMEOUT_SEC, execTimeoutMs: EXEC_TIMEOUT_MS, host: node },\n nonce,\n script,\n );\n};\n\nexport type CephTransportOptions = {\n /** TB4 mon ssh destinations, tried in this order. Required; at least one. */\n readonly nodes: readonly string[];\n readonly dial?: CephDial;\n /** One line per attempted argv, before it dials. @default a line on stderr */\n readonly log?: (line: string) => void;\n};\n\nconst defaultLog = (line: string) => process.stderr.write(`${line}\\n`);\n\n/**\n * Run one allowed ceph shape against the first mon that answers.\n *\n * ⛔ THE ALLOWLIST CHECK HAPPENS BEFORE THE FIRST NODE IS EVEN TRIED — a refused argv dials no\n * ssh at all, so a bug that builds a bad shape cannot leak so much as a connection attempt.\n */\nexport const runCephCommand = async (\n options: CephTransportOptions,\n argv: readonly string[],\n): Promise<CephExecResult & { readonly node: string }> => {\n const problem = cephCommandProblem(argv);\n if (problem !== undefined) {\n throw new SudoRefusedError(`ceph transport refused ${JSON.stringify(argv)}: ${problem}`);\n }\n if (options.nodes.length === 0) throw new Error('ceph transport: no mon nodes were declared');\n const dial = options.dial ?? sshCephDial;\n const log = options.log ?? defaultLog;\n const attempts: { readonly node: string; readonly message: string }[] = [];\n for (const node of options.nodes) {\n log(`homeflare/ceph sudo -n ${JSON.stringify(argv)} @ ${node}`);\n try {\n const result = await dial(node, argv);\n return { ...result, node };\n } catch (cause) {\n attempts.push({ message: cause instanceof Error ? cause.message : String(cause), node });\n }\n }\n // ⛔ NEVER \"ABSENT\". Every mon this call knew about failed at the transport layer; that is a\n // down anchor (or a network problem), not proof the entity does not exist.\n const detail = attempts.map((a) => `${a.node}: ${a.message}`).join('; ');\n throw new Error(`ceph transport: every mon node failed (never treated as absent) — ${detail}`);\n};\n\n/**\n * ★ LOCKOUT SAFETY, THE UFW-ESTABLISHED PATTERN: after every write, re-check quorum on a FRESH\n * connection (a new dial, never the one the write used) and fail the row if it degraded. Never\n * auto-repair, never continue — the caller's write is reported as failed even though it may\n * itself have succeeded, because a write that broke quorum is not a success this transport will\n * call one.\n * ⚠️ THE SHAPE OF `quorum_status -f json` IS REASONED FROM PUBLIC CEPH DOCS, NOT MEASURED against\n * this estate's Ceph release — there is no mon yet. An unparseable or missing `quorum` array\n * fails closed as \"degraded\", never as \"healthy\"; see ceph-auth-parse.ts.\n */\nexport const assertFreshQuorum = async (options: CephTransportOptions): Promise<void> => {\n const result = await runCephCommand(options, quorumStatusArgv());\n if (result.exitCode !== 0) {\n throw new Error(\n `ceph quorum_status failed after a write (mon ${result.node}): ` +\n `${result.stderr.trim().slice(0, 300)}`,\n );\n }\n const quorum = parseQuorumStatus(result.stdout);\n if (!quorum.healthy) {\n throw new Error(\n `ceph quorum degraded after a write (mon ${result.node}): ${quorum.reason} — the write is ` +\n 'reported as failed even if it landed; nothing here auto-repairs a quorum problem.',\n );\n }\n};\n",
18
+ "/**\n * The wire format the ssh runner speaks: how an argv becomes one remote `sh` script, and how that\n * script's real exit code comes back. Pure functions, so every quoting and framing rule is tested\n * without opening a connection.\n *\n * ⛔ `ssh host <words>` IS NOT AN EXECVE. ssh joins its trailing arguments with spaces and the\n * REMOTE LOGIN SHELL parses the result, so an unquoted path with a space, a `$`, a backtick or a\n * `;` is re-parsed as shell syntax on the other side. Everything therefore goes through\n * `shellQuote`, and the remote command is always `/bin/sh -c '<script>'` — a POSIX shell named\n * explicitly, so a csh or fish login shell only ever execs it.\n * ⛔ AN EXIT CODE FROM ssh IS AMBIGUOUS. ssh exits 255 for its own failures (connection refused,\n * host key changed, auth) and otherwise passes the remote exit code through — and a remote\n * command may itself exit 255. A runner that read the code alone would report \"the file is not\n * there\" for a dropped connection, which is the exact \"never infer success from an empty result\"\n * failure. So every script ends by printing its own status on stderr behind a PER-RUNNER NONCE,\n * and a result without that line is a transport failure, never data.\n * ⚠️ THE NONCE IS NOT DECORATION. Without it a remote program that happens to print the marker on\n * stderr could claim any exit code it liked; with a fresh random nonce per runner, the only thing\n * that can produce the line is the script we sent.\n */\n\n/** A script whose work found nothing at the path. ★ Chosen from the 64–113 range sh leaves free. */\nexport const ABSENT = 66;\n\n/**\n * One argument, safe for a POSIX shell. ⛔ Single quotes, because they quote EVERYTHING; the\n * `'\\''` dance is how a literal quote gets through.\n */\nexport const shellQuote = (arg: string): string => {\n if (arg.includes('\\x00')) throw new Error('ssh: an argument contains NUL');\n return `'${arg.replaceAll(\"'\", `'\\\\''`)}'`;\n};\n\n/** An argv as one shell command line. */\nexport const quoteArgv = (argv: readonly string[]): string => argv.map(shellQuote).join(' ');\n\nconst marker = (nonce: string) => `__HF_RC_${nonce}=`;\n\n/**\n * Wrap a script so its status comes back unambiguously.\n *\n * ⛔ THE SCRIPT RUNS IN A SUBSHELL, AND THAT IS NOT COSMETIC. Several of the scripts end a branch\n * with `exit <code>` to say \"nothing was there\"; at the top level that exit leaves the shell\n * immediately and the marker line NEVER PRINTS — so the caller sees a framed result missing its\n * frame and, correctly, calls it a transport failure. 🔴 MEASURED 2026-09-22 against a live\n * Debian 13 host: `stat` of a missing path came back as \"the remote command did not report a\n * status (ssh exit 66)\" instead of \"nothing is at this path\". Inside `( … )` the exit leaves only\n * the subshell, `$?` carries the code out, and the frame always runs.\n * ⚠️ The leading `\\n` matters too: a program whose last stderr line has no newline would otherwise\n * glue itself to the marker and the marker would not be found.\n */\nexport const frameScript = (script: string, nonce: string): string =>\n `(\\n${script}\\n)\\n__hf=$?\\nprintf '\\\\n${marker(nonce)}%s\\\\n' \"$__hf\" >&2\\nexit \"$__hf\"`;\n\nexport type Framed = { readonly exitCode: number; readonly stderr: string };\n\n/**\n * The remote exit code and the stderr that belongs to the command, or `undefined` when the marker\n * is absent — which means the script never ran to its end: ssh failed, the connection dropped, or\n * the remote shell could not parse what we sent. ⛔ The caller must treat `undefined` as an error,\n * never as an answer.\n */\nexport const parseFramed = (stderr: string, nonce: string): Framed | undefined => {\n const at = stderr.lastIndexOf(`\\n${marker(nonce)}`);\n if (at < 0) return undefined;\n const digits = /^(\\d+)\\s*$/.exec(stderr.slice(at + marker(nonce).length + 1));\n if (digits?.[1] === undefined) return undefined;\n return { exitCode: Number(digits[1]), stderr: stderr.slice(0, at) };\n};\n\n/**\n * The fixed ssh options, in front of anything a caller adds.\n * ⛔ `BatchMode=yes` — a deploy that stops to ask for a passphrase is the failure \"no password\n * prompts\" names, and with BatchMode ssh fails at once instead.\n * ⛔ NOTHING HERE WEAKENS HOST VERIFICATION. No `StrictHostKeyChecking`, no `UserKnownHostsFile`:\n * the operator's own ssh config and known_hosts decide who the host is, and a changed key stays\n * a refusal. A runner that turned that off would make a man-in-the-middle a deploy target.\n * ★ `-T`: no pseudo-terminal, so the remote shell's stdout stays the bytes the program wrote.\n */\nexport const sshArgv = (\n host: string,\n script: string,\n options: { readonly connectTimeoutSec: number; readonly sshArgs?: readonly string[] },\n): readonly string[] => {\n if (host === '' || host.startsWith('-')) {\n throw new Error(`ssh: ${JSON.stringify(host)} is not a usable destination`);\n }\n return [\n 'ssh',\n '-T',\n '-o',\n 'BatchMode=yes',\n '-o',\n `ConnectTimeout=${String(options.connectTimeoutSec)}`,\n ...(options.sshArgs ?? []),\n host,\n `/bin/sh -c ${shellQuote(script)}`,\n ];\n};\n",
19
+ "/**\n * `sshRunner()` — the estate's Linux hosts behind the SAME HostRunner seam the launchd family\n * already drives a Mac through (launchd/runner.ts). Every provider in this directory reaches its\n * host only through it, so the whole Linux lifecycle runs against a fake in tests.\n *\n * ★ WHY THE SAME SEAM AND NOT A SECOND ONE. HostRunner is nine promise-returning methods with no\n * macOS in them; what was Mac-specific was launchctl and the plist, not the seam. Reusing it\n * means HostFile, the ownership rules and `checkWrite` already work on Linux, and a stack can\n * hand the same runner to a file, a directory and a unit.\n * ⛔ NO PASSWORDS AND NO SILENT SUDO. Every connection is `BatchMode=yes` (ssh-command.ts), and\n * `privileged` is false: this runner never calls sudo. A root-owned path therefore needs a\n * destination whose ssh user IS root — which is how the estate already reaches its hypervisors —\n * or a privileged runner of the caller's own. See docs/linux-host.md; the Linux twin of\n * sudoRunner is deliberately a later, separate change with its own allowlist.\n * ⛔ FAIL CLOSED. A connection that drops, a host key that changed, a shell that cannot parse the\n * script: none of them produces a \"not found\". Without the framing marker the result is an\n * Error, never data (ssh-command.ts parseFramed).\n * ★ EXERCISED READ-ONLY AGAINST A LIVE HOST, 2026-09-22 (Debian 13, systemd 257, ssh user uid\n * 1001): `stat` of a file, of a missing path and of a symlink (reported `symlink`, so the lstat\n * contract holds over the wire); `readFile` of a file and of a missing path; `getent` user and\n * group; a failing `exec`; and an argument of `a b '$HOME` + backtick + ` ;rm` arriving verbatim,\n * which is the quoting proven rather than argued. Nothing was written.\n * ⚠️ ONE CONNECTION PER CALL. No multiplexing is configured here: a deploy over a slow link pays a\n * handshake per read. Pass `sshArgs: ['-o', 'ControlMaster=auto', …]` to opt into your own\n * ControlPersist socket — the kit does not create one behind your back, because a stale socket\n * outliving the deploy is a surprise nobody declared.\n */\nimport { spawn } from 'node:child_process';\nimport { randomBytes } from 'node:crypto';\nimport { groupQuery, parseGroup, parseUser, userQuery } from '../launchd/host-lookup.ts';\nimport type { ExecResult, HostRunner } from '../launchd/runner.ts';\nimport { ABSENT, frameScript, parseFramed, sshArgv } from './ssh-command.ts';\nimport {\n execScript,\n parseStat,\n readScript,\n removeScript,\n statScript,\n writeScript,\n} from './ssh-scripts.ts';\n\nexport type SshRunnerOptions = {\n /** The destination exactly as the operator's own ssh config names it (`Host` block or user@host). */\n readonly host: string;\n /** Extra ssh options, after the fixed safety set and before the destination. */\n readonly sshArgs?: readonly string[];\n /** Kill a connection after this long. @default 120_000 */\n readonly execTimeoutMs?: number;\n /** ssh's own `ConnectTimeout`. @default 10 */\n readonly connectTimeoutSec?: number;\n};\n\ntype Raw = { exitCode: number; stdout: string; stderr: string; signal: string | null };\n\nconst spawnSsh = (argv: readonly string[], timeoutMs: number, stdin?: Uint8Array): Promise<Raw> =>\n new Promise((resolve, reject) => {\n const [program, ...args] = argv as [string, ...string[]];\n const child = spawn(program, args, {\n shell: false,\n stdio: [stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],\n timeout: timeoutMs,\n });\n const out: Buffer[] = [];\n const err: Buffer[] = [];\n child.stdout?.on('data', (chunk: Buffer) => out.push(chunk));\n child.stderr?.on('data', (chunk: Buffer) => err.push(chunk));\n child.on('error', (cause) => {\n reject(new Error(`ssh: ${cause.message}`));\n });\n if (stdin !== undefined && child.stdin !== null) {\n // ⚠️ EPIPE is normal here: the remote `cat` can die before we finish writing, and the real\n // failure is the exit code we are about to read — not a crash in this process.\n child.stdin.on('error', () => undefined);\n child.stdin.end(stdin);\n }\n child.on('close', (exitCode, signal) => {\n resolve({\n exitCode: exitCode ?? -1,\n signal,\n stderr: Buffer.concat(err).toString('utf8'),\n stdout: Buffer.concat(out).toString('utf8'),\n });\n });\n });\n\n/** What the process on the far end did — or an Error, when we cannot say that it ran at all. */\nexport const runRemote = async (\n options: Required<Pick<SshRunnerOptions, 'host' | 'connectTimeoutSec' | 'execTimeoutMs'>> &\n Pick<SshRunnerOptions, 'sshArgs'>,\n nonce: string,\n script: string,\n stdin?: Uint8Array,\n): Promise<ExecResult> => {\n const argv = sshArgv(options.host, frameScript(script, nonce), {\n connectTimeoutSec: options.connectTimeoutSec,\n ...(options.sshArgs === undefined ? {} : { sshArgs: options.sshArgs }),\n });\n const raw = await spawnSsh(argv, options.execTimeoutMs, stdin);\n const framed = parseFramed(raw.stderr, nonce);\n if (framed === undefined) {\n // ⛔ The command may or may not have run. Saying \"absent\" here is how a dropped link becomes a\n // deleted config, so it is an error with ssh's own words (truncated, never the stdout).\n throw new Error(\n `ssh ${options.host}: the remote command did not report a status (ssh exit ` +\n `${String(raw.exitCode)}${raw.signal === null ? '' : `, killed by ${raw.signal}`}): ` +\n `${raw.stderr.trim().slice(0, 300)}`,\n );\n }\n return { exitCode: framed.exitCode, stderr: framed.stderr, stdout: raw.stdout };\n};\n\nconst decodeBase64 = (text: string): Uint8Array =>\n new Uint8Array(Buffer.from(text.replaceAll('\\n', ''), 'base64'));\n\nconst failed = (host: string, what: string, result: ExecResult): Error =>\n new Error(\n `ssh ${host}: ${what} -> ${String(result.exitCode)}: ${result.stderr.trim().slice(0, 300)}`,\n );\n\n/**\n * Connect once to learn who we are, then hand back the runner.\n * ⛔ THE PROBE IS THE FAIL-CLOSED GATE. `effectiveUid()` is synchronous on HostRunner, so the uid\n * must be known before the first provider call; and `uname -s` refuses a host whose `stat -c`,\n * `base64` and `mv -f` are not the Linux spellings this runner sends. A host that cannot answer\n * both is a thrown Error at construction, not a stack that half-deploys.\n */\nexport const sshRunner = async (options: SshRunnerOptions): Promise<HostRunner> => {\n const settings = {\n connectTimeoutSec: options.connectTimeoutSec ?? 10,\n execTimeoutMs: options.execTimeoutMs ?? 120_000,\n host: options.host,\n ...(options.sshArgs === undefined ? {} : { sshArgs: options.sshArgs }),\n };\n const nonce = randomBytes(8).toString('hex');\n const run = (script: string, stdin?: Uint8Array) => runRemote(settings, nonce, script, stdin);\n const probe = await run('uname -s; id -u');\n if (probe.exitCode !== 0) throw failed(options.host, 'uname -s; id -u', probe);\n const [system, uid] = probe.stdout.trim().split('\\n');\n if (system !== 'Linux' || uid === undefined || !/^\\d+$/.test(uid)) {\n throw new Error(\n `ssh ${options.host}: expected a Linux host, got ${JSON.stringify(probe.stdout.trim())}. ` +\n 'sshRunner() sends GNU/BusyBox argv (stat -c, base64, mv -f); for a Mac use localRunner().',\n );\n }\n const euid = Number(uid);\n return {\n effectiveUid: () => euid,\n exec: (argv) => run(execScript(argv)),\n lookupGroup: async (nameOrId) => {\n const result = await run(execScript(groupQuery('linux', nameOrId)));\n return result.exitCode === 0 ? parseGroup('linux', result.stdout) : undefined;\n },\n lookupUser: async (nameOrId) => {\n const result = await run(execScript(userQuery('linux', nameOrId)));\n return result.exitCode === 0 ? parseUser('linux', result.stdout) : undefined;\n },\n privileged: false,\n readFile: async (path) => {\n const result = await run(readScript(path));\n if (result.exitCode === ABSENT) return undefined;\n if (result.exitCode !== 0) throw failed(options.host, `read ${path}`, result);\n return decodeBase64(result.stdout);\n },\n removeFile: async (path) => {\n const result = await run(removeScript(path));\n if (result.exitCode !== 0) throw failed(options.host, `remove ${path}`, result);\n },\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n stat: async (path) => {\n const result = await run(statScript(path));\n if (result.exitCode === ABSENT) return undefined;\n if (result.exitCode !== 0) throw failed(options.host, `stat ${path}`, result);\n const parsed = parseStat(result.stdout);\n // ⛔ Unparseable is an error, not \"nothing there\": a `stat` that printed something we do not\n // understand is a host this runner does not know, and guessing would write over a file.\n if (parsed === undefined) {\n throw new Error(`ssh ${options.host}: stat ${path} printed an unexpected shape`);\n }\n return parsed;\n },\n writeFileAtomic: async (path, bytes, write) => {\n const at = path.lastIndexOf('/');\n const temp = `${path.slice(0, at)}/.${path.slice(at + 1)}.${randomBytes(6).toString('hex')}.tmp`;\n const result = await run(writeScript(path, temp, write), bytes);\n if (result.exitCode !== 0) throw failed(options.host, `write ${path}`, result);\n },\n };\n};\n",
20
+ "/**\n * User and group lookups, as argv plus a parser — split out so the parsing is tested against the\n * real output shapes without running anything.\n *\n * ★ macOS FIRST, getent ELSEWHERE. macOS has no `getent`; its directory service answers through\n * `dscacheutil`. Shape MEASURED 2026-09-21 on macOS 27.2 (read-only query):\n *\n * name: someone\n * password: ********\n * uid: 501\n * gid: 20\n * dir: /Users/someone\n * shell: /bin/zsh\n *\n * An unknown uid printed NOTHING and exited 0 — so \"no record\" is an empty parse, never an\n * exit code. `getent` exits 2 for an unknown key; that also parses as no record.\n * ⛔ NEVER PARSE OR KEEP THE `password:` LINE. It is a placeholder on macOS, but a parser that\n * copies every field would carry a hash from any host where it is not.\n */\nimport type { HostUser } from './runner.ts';\n\nconst NUMERIC = /^\\d+$/;\n\n/** argv for looking up a user, by name or numeric id. */\nexport const userQuery = (platform: string, nameOrId: string): readonly string[] =>\n platform === 'darwin'\n ? [\n '/usr/bin/dscacheutil',\n '-q',\n 'user',\n '-a',\n NUMERIC.test(nameOrId) ? 'uid' : 'name',\n nameOrId,\n ]\n : ['getent', 'passwd', nameOrId];\n\n/** argv for looking up a group, by name or numeric id. */\nexport const groupQuery = (platform: string, nameOrId: string): readonly string[] =>\n platform === 'darwin'\n ? [\n '/usr/bin/dscacheutil',\n '-q',\n 'group',\n '-a',\n NUMERIC.test(nameOrId) ? 'gid' : 'name',\n nameOrId,\n ]\n : ['getent', 'group', nameOrId];\n\n/** The first `key: value` record in dscacheutil output, minus any password field. */\nconst firstRecord = (stdout: string): Map<string, string> => {\n const fields = new Map<string, string>();\n for (const line of stdout.split('\\n')) {\n if (line.trim() === '') {\n if (fields.size > 0) break; // ⚠️ records are blank-line separated; keep the first only\n continue;\n }\n const at = line.indexOf(': ');\n if (at <= 0) continue;\n const key = line.slice(0, at);\n if (key === 'password') continue;\n fields.set(key, line.slice(at + 2).trim());\n }\n return fields;\n};\n\nconst toId = (text: string | undefined): number | undefined =>\n text !== undefined && NUMERIC.test(text) ? Number(text) : undefined;\n\nexport const parseUser = (platform: string, stdout: string): HostUser | undefined => {\n if (platform === 'darwin') {\n const record = firstRecord(stdout);\n const uid = toId(record.get('uid'));\n const gid = toId(record.get('gid'));\n const home = record.get('dir');\n return uid === undefined || gid === undefined || home === undefined\n ? undefined\n : { gid, home, uid };\n }\n // getent passwd: name:x:uid:gid:gecos:home:shell\n const fields = (stdout.split('\\n')[0] ?? '').split(':');\n const uid = toId(fields[2]);\n const gid = toId(fields[3]);\n const home = fields[5];\n return uid === undefined || gid === undefined || home === undefined || home === ''\n ? undefined\n : { gid, home, uid };\n};\n\nexport const parseGroup = (platform: string, stdout: string): number | undefined => {\n if (platform === 'darwin') return toId(firstRecord(stdout).get('gid'));\n // getent group: name:x:gid:members\n return toId((stdout.split('\\n')[0] ?? '').split(':')[2]);\n};\n",
21
+ "/**\n * The four remote scripts a HostRunner needs, and the parser for the one that returns data.\n * Pure: every shape here is asserted in ssh-scripts.test.ts without a connection.\n *\n * ★ MEASURED 2026-09-22 on Debian 13 (systemd 257, kernel 7.0.14-pve), read-only over ssh:\n * $ stat -c '%f %a %u %g %s' /etc/hostname → 81a4 644 0 0 4 (exit 0)\n * $ stat -c '%f %a %u %g %s' /nonexistent → \"cannot statx\" (exit 1)\n * `%f` is the RAW mode in hex, so the file type is in it (0x8000 regular, 0x4000 directory,\n * 0xa000 symlink) and `%a` is the permission bits alone. ⛔ GNU `stat` does NOT follow symlinks\n * without `-L`, which is what makes this an lstat — the FileStat contract (runner.ts) requires it,\n * because writing through a symlink changes a file the resource never named.\n * ⚠️ COREUTILS, NOT POSIX. `stat -c`, `base64` and `mv -f` are GNU/BusyBox spellings, not POSIX\n * ones; this runner is for Linux hosts and refuses anything else at the probe (ssh-runner.ts).\n */\nimport { ABSENT, quoteArgv, shellQuote } from './ssh-command.ts';\nimport type { FileStat, WriteOptions } from '../launchd/runner.ts';\n\n/** ★ `-e` is false for a dangling symlink and `-L` is true for it, so both tests together mean\n * \"something is at this path\" — the question stat(2) answers and `-e` alone does not. */\nconst IF_PRESENT = (path: string) => `if [ -e ${path} ] || [ -L ${path} ]; then`;\n\nexport const statScript = (path: string): string => {\n const quoted = shellQuote(path);\n return `${IF_PRESENT(quoted)} stat -c '%f %a %u %g %s' -- ${quoted}; else exit ${String(ABSENT)}; fi`;\n};\n\n/** ★ base64 on the wire: stdout then carries no byte that a shell, a pty or a locale can alter. */\nexport const readScript = (path: string): string => {\n const quoted = shellQuote(path);\n return `${IF_PRESENT(quoted)} base64 -- ${quoted}; else exit ${String(ABSENT)}; fi`;\n};\n\n/** ⛔ `rm -f` is idempotent for an absent path and still FAILS for a directory, which is what keeps\n * HostRunner.removeFile from quietly removing something that is not a file. */\nexport const removeScript = (path: string): string => `rm -f -- ${shellQuote(path)}`;\n\nexport const execScript = (argv: readonly string[]): string => quoteArgv(argv);\n\n/**\n * Write bytes (fed on stdin) so `path` holds the old file or the new one and never a torn mix.\n *\n * ⛔ NOT `install`. GNU coreutils `install` opens the destination and writes THROUGH it — unlike\n * macOS's, which stages and renames — so a deploy interrupted mid-write would leave a truncated\n * config where a whole one used to be. Staging beside the target and `mv` (rename(2), same\n * directory so never EXDEV) is the only atomic spelling available over a shell.\n * ⛔ `set -C` and `umask 077`: the staged file is created O_EXCL, so a symlink or a file planted at\n * the temp name is a refusal rather than a write through it, and it is unreadable by anyone else\n * in the moment before its mode is set.\n * ⚠️ chown BEFORE chmod: chown by root clears setuid/setgid, so the other order silently drops a\n * declared 4755 — the same ordering localRunner keeps for the same reason.\n * ⚠️ Each step removes the staged file before it fails, so a failed write leaves no litter beside\n * a file someone else owns.\n */\nexport const writeScript = (path: string, temp: string, options: WriteOptions): string => {\n const target = shellQuote(path);\n const staged = shellQuote(temp);\n const bail = `{ rm -f -- ${staged}; exit`;\n /**\n * ⛔ NEVER `chown <uid>:` — GNU chown reads a trailing colon as \"and that user's LOGIN GROUP\",\n * which is a gid nobody declared. Each half is spelled only when it was asked for.\n */\n const owner =\n options.uid === undefined\n ? options.gid === undefined\n ? undefined\n : `:${String(options.gid)}`\n : options.gid === undefined\n ? String(options.uid)\n : `${String(options.uid)}:${String(options.gid)}`;\n const chown = owner === undefined ? [] : [`chown ${owner} -- ${staged} || ${bail} 72; }`];\n return [\n 'set -C',\n 'umask 077',\n `cat > ${staged} || ${bail} 71; }`,\n ...chown,\n `chmod ${options.mode.toString(8).padStart(3, '0')} -- ${staged} || ${bail} 73; }`,\n `mv -f -- ${staged} ${target} || ${bail} 74; }`,\n ].join('\\n');\n};\n\nconst TYPE_BITS = 0xf000;\nconst KINDS = new Map<number, FileStat['kind']>([\n [0x8000, 'file'],\n [0x4000, 'directory'],\n [0xa000, 'symlink'],\n]);\n\n/** `stat -c '%f %a %u %g %s'` output → a FileStat, or `undefined` when it is not that shape. */\nexport const parseStat = (stdout: string): FileStat | undefined => {\n const fields = stdout.trim().split(/\\s+/);\n if (fields.length !== 5) return undefined;\n const [rawMode, perms, uid, gid, size] = fields as [string, string, string, string, string];\n const raw = Number.parseInt(rawMode, 16);\n const mode = Number.parseInt(perms, 8);\n const numbers = [raw, mode, Number(uid), Number(gid), Number(size)];\n if (numbers.some((value) => !Number.isSafeInteger(value) || value < 0)) return undefined;\n return {\n gid: Number(gid),\n kind: KINDS.get(raw & TYPE_BITS) ?? 'other',\n // ⚠️ `%a` alone, never `%f & 0o7777`: they agree, and the permission field is the documented one.\n mode,\n size: Number(size),\n uid: Number(uid),\n };\n};\n",
22
+ "/**\n * LaunchdJob's props, attributes and the pure half of the provider: domain parsing, the plist path,\n * and the props → plist mapping whose SHA-256 is the whole diff.\n *\n * ★ KEY NAMES AND MEANINGS FROM launchd.plist(5), checked against the 148 nix-darwin daemons on a\n * macOS 27.2 host on 2026-09-21 (read-only, from /nix/store): Label, ProgramArguments, RunAtLoad,\n * KeepAlive (boolean or {SuccessfulExit}), ThrottleInterval, StandardOutPath/StandardErrorPath,\n * EnvironmentVariables, UserName, StartInterval and StartCalendarInterval are the keys in real\n * use. Everything rarer goes through `extraKeys`.\n */\nimport { createHash } from 'node:crypto';\nimport { type PlistDict, type PlistValue, renderPlist } from './plist.ts';\n\n/** `system` (LaunchDaemons, root) or one user's login session, `gui/<uid>` (LaunchAgents). */\nexport type LaunchdDomain = 'system' | `gui/${number}`;\n\n/** One StartCalendarInterval entry. An omitted field is a wildcard, as in cron. */\nexport type CalendarInterval = {\n readonly minute?: number;\n readonly hour?: number;\n readonly day?: number;\n /** 0 and 7 are both Sunday. */\n readonly weekday?: number;\n readonly month?: number;\n};\n\n/** `true` = always restart; the object form restarts only on the named conditions. */\nexport type KeepAlive = boolean | { readonly successfulExit?: boolean; readonly crashed?: boolean };\n\nexport interface LaunchdJobProps {\n /** Reverse-DNS label. Unique per domain; also the plist's file name. */\n label: string;\n domain: LaunchdDomain;\n /** argv; the first element must be an absolute path. */\n programArguments: readonly string[];\n /**\n * ⛔ NON-SECRET VALUES ONLY. Props are stored unencrypted in Alchemy state, and these also land in\n * the plist on disk and in `launchctl print`. Pass a secret as a FILE PATH that a secret renderer\n * (openbao-agent) maintains — secret-tripwire.ts refuses the obvious mistakes.\n */\n environment?: Readonly<Record<string, string>>;\n runAtLoad?: boolean;\n keepAlive?: KeepAlive;\n /** Seconds between starts. */\n startInterval?: number;\n startCalendarInterval?: CalendarInterval | readonly CalendarInterval[];\n /** Minimum seconds between (re)starts; launchd's default is 10. */\n throttleInterval?: number;\n standardOutPath?: string;\n standardErrorPath?: string;\n workingDirectory?: string;\n /** Run as this user. ⚠️ System domain only — launchd ignores it for agents, so it is refused. */\n userName?: string;\n /** Run as this group. ⚠️ System domain only, like `userName`. */\n groupName?: string;\n /**\n * Any other launchd.plist(5) key, verbatim (`ProcessType`, `SoftResourceLimits`, …).\n * ⛔ A key the typed props own is refused, so one job can never say two things.\n */\n extraKeys?: PlistDict;\n}\n\nexport interface LaunchdJobAttributes {\n label: string;\n domain: LaunchdDomain;\n /** `<domain>/<label>` — what `launchctl print` / `bootout` take. */\n serviceTarget: string;\n plistPath: string;\n /** SHA-256 of the plist bytes last bootstrapped (reconcile) or found on disk (read). */\n plistSha256: string;\n /** Bootstrapped into the domain — not necessarily running. */\n loaded: boolean;\n /** launchd's own word: `running`, `not running`, … */\n state?: string;\n pid?: number;\n lastExitCode?: number;\n}\n\nexport type ParsedDomain =\n | { readonly kind: 'system' }\n | { readonly kind: 'gui'; readonly uid: number };\n\nexport const parseDomain = (domain: string): ParsedDomain | undefined => {\n if (domain === 'system') return { kind: 'system' };\n const match = /^gui\\/(\\d+)$/.exec(domain);\n return match?.[1] === undefined ? undefined : { kind: 'gui', uid: Number(match[1]) };\n};\n\nexport const serviceTarget = (label: string, domain: LaunchdDomain): string => `${domain}/${label}`;\n\n/**\n * Where launchd expects the job's plist. ★ Derived, never declared: a daemon's plist outside\n * /Library/LaunchDaemons is not bootstrapped at boot, so a free-form path would be a job that\n * silently stops existing after the next restart.\n */\nexport const plistPathFor = (\n label: string,\n domain: ParsedDomain,\n home: string | undefined,\n): string => {\n if (domain.kind === 'system') return `/Library/LaunchDaemons/${label}.plist`;\n // ⛔ No `~` fallback: the provider may run as root, whose `~` is not the agent owner's home.\n if (home === undefined || !home.startsWith('/')) {\n throw new Error(`gui/${String(domain.uid)}: no home directory for that uid on this host`);\n }\n return `${home}/Library/LaunchAgents/${label}.plist`;\n};\n\n/** Plist keys the typed props own — `extraKeys` may not name them. `Program` is refused too. */\nexport const OWNED_KEYS: readonly string[] = [\n 'EnvironmentVariables',\n 'GroupName',\n 'KeepAlive',\n 'Label',\n 'Program',\n 'ProgramArguments',\n 'RunAtLoad',\n 'StandardErrorPath',\n 'StandardOutPath',\n 'StartCalendarInterval',\n 'StartInterval',\n 'ThrottleInterval',\n 'UserName',\n 'WorkingDirectory',\n];\n\nconst calendarDict = (entry: CalendarInterval): PlistDict => {\n const dict: Record<string, PlistValue> = {};\n if (entry.minute !== undefined) dict['Minute'] = entry.minute;\n if (entry.hour !== undefined) dict['Hour'] = entry.hour;\n if (entry.day !== undefined) dict['Day'] = entry.day;\n if (entry.weekday !== undefined) dict['Weekday'] = entry.weekday;\n if (entry.month !== undefined) dict['Month'] = entry.month;\n return dict;\n};\n\nconst keepAliveValue = (keepAlive: KeepAlive): PlistValue => {\n if (typeof keepAlive === 'boolean') return keepAlive;\n const dict: Record<string, PlistValue> = {};\n if (keepAlive.successfulExit !== undefined) dict['SuccessfulExit'] = keepAlive.successfulExit;\n if (keepAlive.crashed !== undefined) dict['Crashed'] = keepAlive.crashed;\n return dict;\n};\n\n/** The plist dict for a job. ⚠️ Assumes jobProblems() came back empty. */\nexport const jobPlistDict = (props: LaunchdJobProps): PlistDict => {\n const dict: Record<string, PlistValue> = { ...props.extraKeys };\n dict['Label'] = props.label;\n dict['ProgramArguments'] = [...props.programArguments];\n const env = props.environment ?? {};\n if (Object.keys(env).length > 0) dict['EnvironmentVariables'] = { ...env };\n if (props.runAtLoad !== undefined) dict['RunAtLoad'] = props.runAtLoad;\n if (props.keepAlive !== undefined) dict['KeepAlive'] = keepAliveValue(props.keepAlive);\n if (props.startInterval !== undefined) dict['StartInterval'] = props.startInterval;\n const calendar = props.startCalendarInterval;\n if (calendar !== undefined) {\n dict['StartCalendarInterval'] = Array.isArray(calendar)\n ? (calendar as readonly CalendarInterval[]).map(calendarDict)\n : calendarDict(calendar as CalendarInterval);\n }\n if (props.throttleInterval !== undefined) dict['ThrottleInterval'] = props.throttleInterval;\n if (props.standardOutPath !== undefined) dict['StandardOutPath'] = props.standardOutPath;\n if (props.standardErrorPath !== undefined) dict['StandardErrorPath'] = props.standardErrorPath;\n if (props.workingDirectory !== undefined) dict['WorkingDirectory'] = props.workingDirectory;\n if (props.userName !== undefined) dict['UserName'] = props.userName;\n if (props.groupName !== undefined) dict['GroupName'] = props.groupName;\n return dict;\n};\n\nexport const sha256Hex = (bytes: Uint8Array | string): string =>\n createHash('sha256').update(bytes).digest('hex');\n\n/** The rendered plist and its digest — the only thing LaunchdJob compares. */\nexport const renderJob = (\n props: LaunchdJobProps,\n): { text: string; bytes: Uint8Array; sha256: string } => {\n const text = renderPlist(jobPlistDict(props));\n const bytes = new TextEncoder().encode(text);\n return { bytes, sha256: sha256Hex(bytes), text };\n};\n",
23
+ "/**\n * An XML property list serializer — the subset launchd.plist(5) reads, and nothing else.\n *\n * ★ WHY NOT AN npm LIBRARY. Nothing in this workspace depends on one (checked 2026-09-21: no\n * `plist`, `xmlbuilder` or `@xmldom/xmldom` anywhere under node_modules), and the common one pulls\n * an XML DOM in for PARSING, which a provider that only RENDERS never does. The subset a launchd\n * job needs is five node types, so this is ~100 lines plus exhaustive tests (plist.test.ts), which\n * also round-trip every case through Apple's own `plutil` when the tests run on macOS.\n *\n * ⛔ THE OUTPUT MUST BE DETERMINISTIC. LaunchdJob diffs by the SHA-256 of these bytes, so two\n * renders of one declaration must be byte-identical: dict keys are sorted, indentation is fixed,\n * and nothing depends on insertion order. A non-deterministic render is a forever-`update`.\n * ⚠️ AND A CHANGE TO THIS FILE'S OUTPUT RESTARTS EVERY JOB. A new header, indent or escape changes\n * every digest, so the next deploy after the kit upgrade updates — restarts — every declared\n * job at once. Treat any byte change here as a breaking change and say so in its changeset.\n *\n * ⛔ REFUSE, NEVER COERCE. `undefined`, `null`, a fraction, NaN, a class instance or a character XML\n * 1.0 cannot carry is a thrown PlistError naming the path, not a silently dropped key. A dropped\n * `KeepAlive` is a daemon that never restarts, and nothing downstream would say why.\n */\n\n/** Every value launchd reads. ⚠️ No `<real>`, `<date>` or `<data>`: no launchd key needs one. */\nexport type PlistValue =\n | string\n | number\n | boolean\n | readonly PlistValue[]\n | { readonly [key: string]: PlistValue };\n\nexport type PlistDict = { readonly [key: string]: PlistValue };\n\nexport class PlistError extends Error {\n constructor(\n readonly path: string,\n detail: string,\n ) {\n super(`plist ${path === '' ? '(root)' : path}: ${detail}`);\n this.name = 'PlistError';\n }\n}\n\nconst HEADER =\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" ' +\n '\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\\n' +\n '<plist version=\"1.0\">\\n';\n\n/** ⚠️ A cycle, or a runaway generator, would otherwise recurse until the stack dies. */\nconst MAX_DEPTH = 32;\n\n/**\n * ⛔ CHARACTERS XML 1.0 CANNOT CARRY AT ALL — not even escaped (XML 1.0 §2.2 `Char`). C0 controls\n * other than tab, LF and CR; U+FFFE / U+FFFF; and an unpaired surrogate, which has no UTF-8 form.\n */\n// ★ The `u` flag makes a VALID surrogate pair one code point above U+FFFF, outside this class, so\n// only an unpaired half matches the \\uD800-\\uDFFF range.\n// oxlint-disable-next-line no-control-regex -- matching control characters is the point\nconst FORBIDDEN = /[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\uD800-\\uDFFF\\uFFFE\\uFFFF]/u;\n\n/**\n * Escape text content.\n * ⚠️ CR IS A CHARACTER REFERENCE, NOT A LITERAL. An XML parser normalises a literal CR (and CRLF)\n * to LF before the application sees it (XML 1.0 §2.11), so `a\\r\\nb` would come back as `a\\nb` and\n * the argument launchd passes would differ from the one declared. `&#13;` survives normalisation.\n */\nexport const escapeText = (text: string, path: string): string => {\n const bad = FORBIDDEN.exec(text);\n if (bad !== null) {\n const code = bad[0].codePointAt(0) ?? 0;\n throw new PlistError(\n path,\n `U+${code.toString(16).toUpperCase().padStart(4, '0')} cannot appear in XML 1.0`,\n );\n }\n return text\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\\r', '&#13;');\n};\n\nconst isPlainObject = (value: object): boolean => {\n const proto = Object.getPrototypeOf(value) as unknown;\n return proto === Object.prototype || proto === null;\n};\n\nconst renderValue = (value: unknown, path: string, depth: number, out: string[]): void => {\n const pad = '\\t'.repeat(depth);\n if (depth > MAX_DEPTH) throw new PlistError(path, `nested deeper than ${String(MAX_DEPTH)}`);\n if (typeof value === 'string') {\n out.push(`${pad}<string>${escapeText(value, path)}</string>`);\n return;\n }\n if (typeof value === 'boolean') {\n out.push(`${pad}<${value ? 'true' : 'false'}/>`);\n return;\n }\n if (typeof value === 'number') {\n // ⛔ launchd reads every numeric key as an integer; 1.5 would be a silently different job.\n if (!Number.isSafeInteger(value)) {\n throw new PlistError(path, `${String(value)} is not a safe integer (no <real> support)`);\n }\n out.push(`${pad}<integer>${String(value === 0 ? 0 : value)}</integer>`);\n return;\n }\n if (Array.isArray(value)) {\n if (value.length === 0) {\n out.push(`${pad}<array/>`);\n return;\n }\n out.push(`${pad}<array>`);\n for (const [index, item] of (value as readonly unknown[]).entries()) {\n renderValue(item, `${path}[${String(index)}]`, depth + 1, out);\n }\n out.push(`${pad}</array>`);\n return;\n }\n if (typeof value === 'object' && value !== null && isPlainObject(value)) {\n renderDict(value as Record<string, unknown>, path, depth, out);\n return;\n }\n const kind = value === null ? 'null' : typeof value;\n throw new PlistError(path, `${kind} has no plist form — omit the key instead`);\n};\n\nconst renderDict = (\n dict: Record<string, unknown>,\n path: string,\n depth: number,\n out: string[],\n): void => {\n const pad = '\\t'.repeat(depth);\n // ★ Code-unit order, not localeCompare: the same bytes on every machine and every locale.\n const keys = Object.keys(dict).sort();\n if (keys.length === 0) {\n out.push(`${pad}<dict/>`);\n return;\n }\n out.push(`${pad}<dict>`);\n for (const key of keys) {\n const child = path === '' ? key : `${path}.${key}`;\n out.push(`${pad}\\t<key>${escapeText(key, child)}</key>`);\n renderValue(dict[key], child, depth + 1, out);\n }\n out.push(`${pad}</dict>`);\n};\n\n/** Render a top-level dict as a complete XML plist document, ending in a newline. */\nexport const renderPlist = (dict: PlistDict): string => {\n if (typeof dict !== 'object' || dict === null || Array.isArray(dict) || !isPlainObject(dict)) {\n throw new PlistError('', 'the top level of a launchd plist must be a dict');\n }\n const out: string[] = [];\n renderDict(dict as Record<string, unknown>, '', 0, out);\n return `${HEADER}${out.join('\\n')}\\n</plist>\\n`;\n};\n",
24
+ "/**\n * The parts of `unit-form.ts`'s validation that inspect the unit file's TEXT rather than its\n * props struct — split out so unit-form.ts stays under the file cap, and because these are one\n * coherent concern: is this INI shape something systemd would actually load. Takes plain strings,\n * never `SystemdUnitProps`, so it never needs to import back from unit-form.ts.\n *\n * ⛔ `[Install]` KEYS ARE `unit-form.ts`'s OWN LIST, NOT SYSTEMD'S FULL VOCABULARY — WantedBy,\n * RequiredBy, UpsertedBy, Also, Alias, per `systemd.unit(5)` §[Install]. `container-form.ts`'s\n * own header notes it deliberately does NOT reuse this set: a `.container` file's `[Install]`\n * supports only Alias/WantedBy/RequiredBy (`podman-systemd.unit(5)`), narrower than this.\n */\n\nexport const INSTALL_KEYS = new Set(['WantedBy', 'RequiredBy', 'UpsertedBy', 'Also', 'Alias']);\n\n/** Whether `text` has an `[Install]` section carrying at least one key systemd actually links on. */\nexport const hasInstallSection = (text: string): boolean => {\n if (!text.includes('[Install]')) return false;\n const after = text.slice(text.indexOf('[Install]'));\n return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, 'm').test(after));\n};\n\n/**\n * The first line that is neither blank nor a comment — the line whose shape actually decides\n * whether the file \"begins with a [Section]\", per `textProblems` below.\n * ★ systemd.syntax(7): \"Empty lines and lines starting with '#' or ';' are ignored\", with no\n * carve-out for the position before the first section — a comment header is ordinary content\n * there, same as anywhere else in the file.\n */\nexport const firstSubstantiveLine = (text: string): string | undefined =>\n text\n .split('\\n')\n .find((line) => !(line.trim() === '' || line.startsWith('#') || line.startsWith(';')));\n\n/**\n * 🔴 MEASURED FALSE, CT100 deploy 2026-09-26 00:11Z: the openbao-agent.service asset's first eight\n * lines are `#`-comments recording where the file was recovered from (verbatim from the live\n * host — homeflare-ct100's `src/openbao-agent/files/openbao-agent.service`), and the file is\n * byte-identical to what systemd already loads there. The old check tested the WHOLE text\n * against `/^\\s*\\[[A-Za-z]+]/` — `\\s*` skips blank lines and indentation, never a `#`/`;`\n * comment — so a leading comment line failed it outright. This checks only the first\n * SUBSTANTIVE line, so any number of leading blank lines and comments are fine and a genuine\n * `key=value` before any section — the one real error this exists to catch — still fails, since\n * that line is neither blank nor a comment either.\n */\nexport const textProblems = (text: string): string[] => {\n const found: string[] = [];\n if (text.trim() === '') found.push('the unit file is empty');\n if (text.includes('\\u0000')) found.push('the unit file contains NUL');\n const first = firstSubstantiveLine(text);\n // ★ `\\s*` kept from the original regex — leading indentation on the SECTION line itself was\n // already tolerated before this fix and stays that way; only the leading-comment shape changes.\n if (first === undefined || !/^\\s*\\[[A-Za-z]+]/.test(first)) {\n found.push(\n 'a unit file must begin with a [Section] (blank lines and #/; comments before it are fine)',\n );\n }\n return found;\n};\n",
25
+ "/**\n * Systemd.Unit / Systemd.Timer props, attributes, the unit-file renderer and the validation — the\n * pure half of the family.\n *\n * ⛔ THE DIRECTIVE SET IS SYSTEMD'S, NOT THIS FILE'S. There is no machine-readable schema of\n * `systemd.unit(5)` / `systemd.service(5)` directives to generate from, so this renders SECTIONS\n * AND LINES VERBATIM and claims to know nothing about what the keys mean. What it validates is\n * only what an INI file must be true of — a section name, a key shape, no embedded newline —\n * plus the two structural rules systemd's own output proves (see `unitProblems`). ★ That is the\n * honest boundary: invent no vendor shapes, and let `systemd-analyze verify` be the deep check a\n * host stack runs on the host.\n * ⛔ `content` IS NEVER A SECRET. Props sit unencrypted in Alchemy state and the unit file is\n * world-readable at 0644; a unit that needs a secret takes `EnvironmentFile=` pointing at a file\n * a secret renderer maintains.\n * ★ THE TEXT-SHAPE CHECKS (comments/blank lines before [Section], [Install] key lookup) live in\n * `unit-text.ts`, split out to keep this file under the house line cap.\n */\nimport { sha256Hex } from '../launchd/job-form.ts';\nimport { hasInstallSection, textProblems } from './unit-text.ts';\n\nexport type UnitSection = {\n /** `Unit`, `Service`, `Timer`, `Install`, … — verbatim, including its capitalisation. */\n readonly name: string;\n /**\n * The section's lines, in order. A repeated key is allowed and meaningful in systemd\n * (`ExecStartPre=` twice runs twice), so this is a list of pairs, never an object.\n */\n readonly lines: readonly (readonly [key: string, value: string])[];\n};\n\nexport type SystemdUnitProps = {\n /** Unit name WITH its suffix, e.g. `thing.service`. The unit file's name on disk. */\n name: string;\n /** The whole unit file, verbatim. Mutually exclusive with `sections`. */\n content?: string;\n /** The unit file as sections, rendered in the order given. Mutually exclusive with `content`. */\n sections?: readonly UnitSection[];\n /** Directory the unit file goes in. @default '/etc/systemd/system' */\n directory?: string;\n /** `systemctl enable` / `disable`. @default true */\n enabled?: boolean;\n /** `systemctl start` / `stop`. @default true */\n started?: boolean;\n /**\n * Digests of things this unit READS that live outside its unit file — a rendered config file's\n * `sha256` attribute, most often. A change here restarts the unit; nothing else does.\n * ⚠️ A hand edit of that file is NOT noticed: the digest in state still matches what the stack\n * declared, so the unit reads converged. That limit is the same one the Mac side records.\n */\n restartOn?: readonly string[];\n};\n\nexport type SystemdUnitAttributes = {\n name: string;\n unitPath: string;\n /** SHA-256 of the unit file this resource wrote. */\n unitSha256: string;\n /** SHA-256 of `restartOn`, so a config change is a diff. */\n configSha256: string;\n loadState: string;\n activeState: string;\n unitFileState?: string;\n enabled: boolean;\n active: boolean;\n};\n\n/** ★ systemd's admin drop-in directory, ahead of the vendor's in its search order (measured). */\nexport const DEFAULT_UNIT_DIRECTORY = '/etc/systemd/system';\n\n/**\n * Directories systemd looks in for a unit file it can enable.\n * ★ MEASURED 2026-09-22 with `systemd-analyze unit-paths` on Debian 13 / systemd 257, minus the\n * per-user and generator directories, which are not places a stack writes. `/lib/systemd/system`\n * is the same directory before the /usr merge, where the measured host has only the merged name.\n * ⛔ WHY IT IS ENFORCED. `systemctl enable <name>` looks the unit up BY NAME; a unit file outside\n * these directories is invisible to that lookup, so an enabled unit declared into, say, `/opt`\n * writes its file, reloads, and then fails at `enable` with the file already on disk.\n */\nexport const UNIT_SEARCH_DIRECTORIES = [\n '/etc/systemd/system',\n '/run/systemd/system',\n '/usr/local/lib/systemd/system',\n '/usr/lib/systemd/system',\n '/lib/systemd/system',\n] as const;\n\n/** ⛔ The unit file's mode and owner are systemd's rule, not a choice: readable, never writable. */\nexport const UNIT_WRITE = { gid: 0, mode: 0o644, uid: 0 } as const;\n\nexport const digestOf = (text: string): string => sha256Hex(new TextEncoder().encode(text));\n\n/** The digests of everything outside the unit file that should restart it, as one value. */\nexport const configDigest = (restartOn: readonly string[] | undefined): string =>\n digestOf((restartOn ?? []).join('\\n'));\n\n/**\n * Whether `activeState`/`subState` count as \"the unit is running\" against a declared `started` —\n * the ONE place this decision is made, so `diffUnit` (unit-lifecycle.ts) and `settle`\n * (unit-settle.ts) can never disagree about it.\n *\n * ⛔ WHY THIS EXISTS: a `Type=oneshot` service driven by a `.timer` (no `[Install]`, declared\n * `started: false, enabled: false` — its timer starts it, not this resource) reports\n * `ActiveState=activating` for the run's whole duration, not an instant. Written for the PVE\n * node-check unit that first hit this: ~1.2s, every 60s. A plan taken mid-run must not read that\n * run as drift, and a deploy must not `systemctl stop` a check that is already in flight.\n * ★ systemd.service(5) §`Type=`, `oneshot` (man7.org mirror, read 2026-09-23): with no\n * `RemainAfterExit=`, the service manager holds the unit `activating` for as long as\n * `ExecStart=` runs, then moves it straight to `dead`/`inactive` (or `failed`) — it never passes\n * through `active`. So for `started: false`, `activating` is exactly a run in progress, and\n * `active` is the one state a oneshot never reaches on its own: that is the drift, and the only\n * thing settle stops.\n * ⛔ CRASH-LOOP GUARD, added on adversarial review: `ActiveState=activating` is not only \"starting\n * for the first time\" — a service with `Restart=` waiting out its `RestartSec=` backoff between\n * crash attempts is ALSO `activating`, with `SubState=auto-restart` (or `auto-restart-queued`).\n * A unit declared `started: false` that is crash-looping sits there for nearly all of its\n * wall-clock time. Without this guard the oneshot exemption above would read that loop as\n * converged forever, and a deploy would never again issue the `stop` that ends it — silently\n * defeating `started: false` on exactly the unit that most needs it enforced. So only a genuine\n * start (`SubState` anything else — `start`, `start-pre`, `start-post`, …) gets the exemption;\n * an auto-restart substate counts as running regardless of `started`, same as `active` does.\n * ★ SOURCE, not measured against a live host: systemd's own `service.c`\n * `state_translation_table` (github.com/systemd/systemd, read 2026-09-23) maps both\n * `SERVICE_AUTO_RESTART` and `SERVICE_AUTO_RESTART_QUEUED` to `UNIT_ACTIVATING`, and\n * `unit-def.c`'s `service_state_table` names their `SubState` strings `\"auto-restart\"` and\n * `\"auto-restart-queued\"`. ⚠️ REASONED NOT MEASURED: no unit was actually crash-looped and read\n * back with `systemctl show` for this change — the systemd source is the citation, unlike\n * systemctl.ts's own header, which is a live-host trace.\n * ⚠️ THE TRADE-OFF THIS ACCEPTS: a `Type=notify` (or other long-running type) unit stuck in a\n * genuine `activating`/`start` with `started: false` — never reaching `active` and never\n * crash-looping either — reads as converged, not drift, until it reaches `active`. A unit hung\n * like that is exactly the case a human should look at, not one this predicate auto-corrects by\n * stopping it out from under whatever else depends on it.\n * ★ `started: true` (the default, when omitted) is unchanged: `active` or `activating` (any\n * `subState`) both count as running, so a start already in flight is never started twice.\n */\nexport const isUnitRunning = (\n activeState: string,\n subState: string | undefined,\n started: boolean,\n): boolean => {\n if (activeState === 'active') return true;\n if (activeState !== 'activating') return false;\n if (subState === 'auto-restart' || subState === 'auto-restart-queued') return true;\n return started;\n};\n\n/** Sections → an INI unit file. ★ Keys and values pass through untouched; see the file header. */\nexport const renderUnit = (sections: readonly UnitSection[]): string =>\n `${sections\n .map((section) =>\n [`[${section.name}]`, ...section.lines.map(([key, value]) => `${key}=${value}`)].join('\\n'),\n )\n .join('\\n\\n')}\\n`;\n\nexport const unitText = (props: SystemdUnitProps): string =>\n props.content ?? renderUnit(props.sections ?? []);\n\nexport const unitPathFor = (props: SystemdUnitProps): string =>\n `${props.directory ?? DEFAULT_UNIT_DIRECTORY}/${props.name}`;\n\n/** ★ Exported so sudo-allowlist.ts validates a systemctl unit operand against the same pattern. */\nexport const NAME = /^[A-Za-z0-9][A-Za-z0-9_.@:-]*\\.([a-z]+)$/;\n/** systemd's own unit types. ★ Fixed vocabulary from `systemd.unit(5)`, not a guess at directives. */\nconst TYPES = new Set([\n 'automount',\n 'device',\n 'mount',\n 'path',\n 'scope',\n 'service',\n 'slice',\n 'socket',\n 'swap',\n 'target',\n 'timer',\n]);\n\n/**\n * Every refusal at once, so one plan shows the whole list.\n * ⛔ THE TWO STRUCTURAL RULES, both proven by systemd's own output rather than assumed:\n * - `enabled: true` needs an `[Install]` section. `systemctl enable` has nothing to link without\n * one, and systemd's name for a unit file that has none is `UnitFileState=static` — measured on\n * a live host, where the journal daemon reads `static` and the ssh daemon reads `enabled`.\n * - A `.timer` needs a `[Timer]` section, and a timer's `[Install]` is what arms it at boot.\n */\nexport const unitProblems = (props: SystemdUnitProps, expect?: string): string[] => {\n const found: string[] = [];\n const type = NAME.exec(props.name)?.[1];\n if (type === undefined || !TYPES.has(type)) {\n found.push(\n `name must be <unit>.<type> with a systemd unit type, got ${JSON.stringify(props.name)}`,\n );\n } else if (expect !== undefined && type !== expect) {\n found.push(`this resource declares a .${expect} unit; ${props.name} is a .${type}`);\n }\n if ((props.content === undefined) === (props.sections === undefined)) {\n found.push('declare exactly one of content or sections');\n }\n const directory = props.directory ?? DEFAULT_UNIT_DIRECTORY;\n if (!directory.startsWith('/') || directory.endsWith('/') || directory.includes('/..')) {\n found.push('directory must be an absolute, normalised path with no trailing slash');\n }\n found.push(...textProblems(unitText(props)));\n if (props.enabled !== false && !UNIT_SEARCH_DIRECTORIES.includes(directory as never)) {\n found.push(\n `an enabled unit must live where systemd looks for it by name (${UNIT_SEARCH_DIRECTORIES.join(', ')}); ` +\n `${directory} is not one of them. Move it, or declare enabled: false.`,\n );\n }\n if (props.enabled !== false && !hasInstallSection(unitText(props))) {\n found.push(\n 'enabled units need an [Install] section with WantedBy/RequiredBy/Also/Alias; without one ' +\n 'systemd calls the unit \"static\" and `systemctl enable` has nothing to link. Add one, or ' +\n 'declare enabled: false and let something else pull it in.',\n );\n }\n if (type === 'timer' && !unitText(props).includes('[Timer]')) {\n found.push('a .timer needs a [Timer] section');\n }\n return found;\n};\n",
26
+ "/**\n * A tripwire for the obvious ways a secret ends up in Alchemy state through these providers.\n *\n * ⛔ WHY IT EXISTS. Alchemy persists every prop UNENCRYPTED in its state store — the same reason\n * the OpenBao families are metadata-only (openbao/policy.ts). A LaunchdJob's `environment` and\n * `programArguments`, and a HostFile's `content`, are props. A token declared there is a token\n * copied into the state store, into every plan diff, and into the plist or file on disk.\n * ★ THE ALTERNATIVE, NAMED IN EVERY REFUSAL: let a secret renderer (openbao-agent, or any other)\n * write the secret to a root-owned file, and declare only its PATH — `FOO_TOKEN_FILE=/path`,\n * `--token-file /path`. The job reads the file at start; the state holds a path.\n *\n * ⚠️ A TRIPWIRE, NOT A SCANNER. It catches the names and shapes people actually type. A token in\n * an innocently named variable passes; nothing here can make a secret safe to declare.\n */\nimport type { PlistDict, PlistValue } from './plist.ts';\n\nconst ALTERNATIVE =\n 'props are stored unencrypted in Alchemy state. Have a secret renderer (e.g. openbao-agent) ' +\n 'write it to a root-owned file and declare the file path instead (FOO_FILE=/path, --foo-file /path).';\n\n/** Names that end in a word meaning \"this is the secret itself\". `*_FILE`, `*_PATH` never match. */\nconst SECRET_NAME =\n /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API_?KEY|ACCESS_KEY|PRIVATE_KEY|SECRET_KEY|CREDENTIALS?)$/i;\n\n/** `--token=…` / `-password=…` style flags carrying their value inline. */\nconst INLINE_FLAG = /^--?[a-z0-9-]*(?:token|password|passwd|passphrase|secret|api-?key)=./i;\n\n/** A flag whose NEXT argv element is the secret: `--token abc`. */\nconst BARE_FLAG = /^--?(?:[a-z0-9]+-)*(?:token|password|passwd|passphrase|secret|api-?key)$/i;\n\nexport const PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/;\n\n/** Paths of every string leaf in a plist value holding a private key. */\nconst keyLeaves = (value: PlistValue, path: string): string[] => {\n if (typeof value === 'string') return PRIVATE_KEY.test(value) ? [path] : [];\n if (Array.isArray(value)) {\n return (value as readonly PlistValue[]).flatMap((item, index) =>\n keyLeaves(item, `${path}[${String(index)}]`),\n );\n }\n if (typeof value === 'object' && value !== null) {\n return Object.entries(value).flatMap(([key, item]) => keyLeaves(item, `${path}.${key}`));\n }\n return [];\n};\n\n/**\n * Refusals for a job's environment, argv and extraKeys. Empty means nothing tripped.\n * ★ extraKeys too: it is a prop like the others, so it lands in state just the same.\n */\nexport const jobSecretProblems = (\n environment: Readonly<Record<string, string>> | undefined,\n argv: readonly string[],\n extraKeys?: PlistDict,\n): string[] => {\n const found: string[] = [];\n for (const [name, value] of Object.entries(environment ?? {})) {\n if (SECRET_NAME.test(name))\n found.push(`environment ${name} looks like a secret: ${ALTERNATIVE}`);\n else if (PRIVATE_KEY.test(value))\n found.push(`environment ${name} holds a private key: ${ALTERNATIVE}`);\n }\n for (const [index, arg] of argv.entries()) {\n // ⛔ Never echo the argument itself: if it IS a secret, the refusal would print it.\n if (INLINE_FLAG.test(arg)) {\n found.push(`programArguments[${String(index)}] passes a secret inline: ${ALTERNATIVE}`);\n } else if (BARE_FLAG.test(arg) && index + 1 < argv.length) {\n found.push(`programArguments[${String(index + 1)}] follows a secret flag: ${ALTERNATIVE}`);\n } else if (PRIVATE_KEY.test(arg)) {\n found.push(`programArguments[${String(index)}] holds a private key: ${ALTERNATIVE}`);\n }\n }\n for (const path of keyLeaves(extraKeys ?? {}, 'extraKeys')) {\n found.push(`${path} holds a private key: ${ALTERNATIVE}`);\n }\n return found;\n};\n\n/** Refusals for a HostFile's content. */\nexport const fileSecretProblems = (content: string): string[] =>\n PRIVATE_KEY.test(content) ? [`content holds a private key: ${ALTERNATIVE}`] : [];\n",
27
+ "/**\n * HostFile's props, attributes and validation — the pure half of the provider.\n *\n * ⛔ `content` IS NEVER A SECRET. Alchemy stores props unencrypted, so the content of every HostFile\n * sits in the state store and in every plan diff. A secret file stays RENDERED BY A SECRET\n * RENDERER (openbao-agent templates, on this estate) that fetches it at run time; the stack\n * declares only non-secret config, and at most the PATH the renderer writes to.\n * secret-tripwire.ts refuses the obvious case (a PEM private key); nothing can catch all of them.\n */\nimport { fileSecretProblems } from './secret-tripwire.ts';\n\nexport interface HostFileProps {\n /** Absolute path. The parent directory must already exist. */\n path: string;\n /** UTF-8 text. ⛔ Never a secret — see the file header. */\n content: string;\n /** Permission bits. @default 0o644 */\n mode?: number;\n /** User name or numeric uid. Omitted: whoever the runner writes as (a rewrite keeps no owner). */\n owner?: string | number;\n /** Group name or numeric gid. Omitted: the directory's default group. */\n group?: string | number;\n}\n\nexport interface HostFileAttributes {\n path: string;\n /** SHA-256 of the bytes on disk. */\n sha256: string;\n mode: number;\n uid: number;\n gid: number;\n size: number;\n}\n\nexport const DEFAULT_MODE = 0o644;\n\n/** A user or group as declared: a non-negative id, or a name the host could have. */\nexport const identityProblems = (name: string, value: string | number | undefined): string[] => {\n if (value === undefined) return [];\n if (typeof value === 'number') {\n return Number.isSafeInteger(value) && value >= 0\n ? []\n : [`${name} must be a non-negative integer id`];\n }\n return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value) || /^\\d+$/.test(value)\n ? []\n : [`${name} ${JSON.stringify(value)} is not a valid user or group name`];\n};\n\n/**\n * An absolute, normalised path, or the reasons it is not. ★ Shared with sudo-allowlist.ts, so a\n * path the sudo runner may touch as root obeys exactly the rule a declared file does.\n */\nexport const pathProblems = (path: string): string[] => {\n const found: string[] = [];\n if (!path.startsWith('/')) found.push('path must be absolute');\n // ★ One spelling per file: `a/./b`, `a//b` and `a/../b` would each be a second resource for the\n // same inode, and `..` is how a derived path walks out of the directory it was meant for.\n if (\n path.endsWith('/') ||\n path\n .split('/')\n .slice(1)\n .some((part) => part === '' || part === '.' || part === '..')\n ) {\n found.push('path must be normalised (no trailing slash, no empty, \".\" or \"..\" segments)');\n }\n if (path.includes('\\x00')) found.push('path contains NUL');\n return found;\n};\n\nexport const fileProblems = (props: HostFileProps): string[] => {\n const found = pathProblems(props.path);\n const mode = props.mode ?? DEFAULT_MODE;\n if (!Number.isSafeInteger(mode) || mode < 0 || mode > 0o7777) found.push('mode must be 0–0o7777');\n found.push(...identityProblems('owner', props.owner));\n found.push(...identityProblems('group', props.group));\n found.push(...fileSecretProblems(props.content));\n return found;\n};\n",
28
+ "/**\n * Everything LaunchdJob refuses before it touches the host. Run at plan time (diff) and again at\n * reconcile, so a bad declaration fails the plan instead of half-applying.\n *\n * ★ REFUSE WHAT launchd WOULD SILENTLY IGNORE OR MISREAD. launchd loads a plist with an out-of-range\n * calendar field or a UserName on an agent without complaint and then simply never does what the\n * declaration says; a plan that fails is cheaper than a job that quietly does nothing.\n */\nimport {\n type CalendarInterval,\n type LaunchdJobProps,\n OWNED_KEYS,\n parseDomain,\n} from './job-form.ts';\nimport { jobSecretProblems } from './secret-tripwire.ts';\n\n/** ★ Labels become file names: no `/`, no leading dot, nothing a path could re-interpret. */\nexport const LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;\n\n/**\n * ⛔ LABELS OTHER TOOLS OWN ARE NEVER DECLARED HERE. nix-darwin (`org.nixos.*`) rewrites its plists\n * on every activation and unloads any it no longer lists; macOS owns `com.apple.*`; `brew\n * services` owns `homebrew.mxcl.*`. Declaring one of those makes two tools fight over one file.\n * To move a job off such a tool, declare a NEW label and cut over (docs/launchd.md).\n */\nexport const RESERVED_LABEL_PREFIXES: readonly string[] = [\n 'org.nixos.',\n 'com.apple.',\n 'homebrew.mxcl.',\n];\n\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nconst RANGES: ReadonlyArray<readonly [keyof CalendarInterval, number, number]> = [\n ['minute', 0, 59],\n ['hour', 0, 23],\n ['day', 1, 31],\n ['weekday', 0, 7],\n ['month', 1, 12],\n];\n\nconst isInt = (value: number, min: number, max = Number.MAX_SAFE_INTEGER) =>\n Number.isSafeInteger(value) && value >= min && value <= max;\n\nconst calendarProblems = (entries: readonly CalendarInterval[]): string[] => {\n if (entries.length === 0) return ['startCalendarInterval is an empty list; omit it instead'];\n const found: string[] = [];\n for (const [index, entry] of entries.entries()) {\n for (const [field, min, max] of RANGES) {\n const value = entry[field];\n if (value !== undefined && !isInt(value, min, max)) {\n found.push(\n `startCalendarInterval[${String(index)}].${field} must be ${String(min)}–${String(max)}`,\n );\n }\n }\n }\n return found;\n};\n\nconst absolute = (name: string, value: string | undefined): string[] =>\n value === undefined || value.startsWith('/') ? [] : [`${name} must be an absolute path`];\n\n/**\n * The label and domain alone — the job's identity. ★ Split out because a rename is a delete-first\n * replace, and it must be refusable at plan time even while other props are still unresolved\n * Outputs (see job.ts diff): otherwise the old job is deleted and only then is the new label\n * found to be invalid.\n */\nexport const identityProblems = (label: string, domainText: string): string[] => {\n const found: string[] = [];\n if (!LABEL.test(label)) {\n found.push('label must be 1–200 of [A-Za-z0-9._-], starting with a letter or digit');\n }\n const reserved = RESERVED_LABEL_PREFIXES.find((prefix) => label.startsWith(prefix));\n if (reserved !== undefined) {\n found.push(`label ${label} is under ${reserved}, which another tool owns; declare a new label`);\n }\n if (parseDomain(domainText) === undefined)\n found.push(`domain must be 'system' or 'gui/<uid>', not ${domainText}`);\n return found;\n};\n\nexport const jobProblems = (props: LaunchdJobProps): string[] => {\n const found = identityProblems(props.label, props.domain);\n const domain = parseDomain(props.domain);\n\n const [program] = props.programArguments;\n // ★ Absolute, because launchd's PATH for a job is not the deploying shell's PATH.\n if (program === undefined) found.push('programArguments must name a program');\n else if (!program.startsWith('/')) found.push('programArguments[0] must be an absolute path');\n\n for (const name of Object.keys(props.environment ?? {})) {\n if (!ENV_NAME.test(name))\n found.push(`environment name ${JSON.stringify(name)} is not a valid variable name`);\n }\n found.push(...jobSecretProblems(props.environment, props.programArguments, props.extraKeys));\n\n if (props.startInterval !== undefined && !isInt(props.startInterval, 1)) {\n found.push('startInterval must be a positive whole number of seconds');\n }\n if (props.throttleInterval !== undefined && !isInt(props.throttleInterval, 0)) {\n found.push('throttleInterval must be a non-negative whole number of seconds');\n }\n const calendar = props.startCalendarInterval;\n if (calendar !== undefined) {\n found.push(\n ...calendarProblems(Array.isArray(calendar) ? calendar : [calendar as CalendarInterval]),\n );\n }\n const keepAlive = props.keepAlive;\n if (\n typeof keepAlive === 'object' &&\n keepAlive.successfulExit === undefined &&\n keepAlive.crashed === undefined\n ) {\n found.push('keepAlive {} names no condition; use true, false, or a condition');\n }\n\n found.push(...absolute('standardOutPath', props.standardOutPath));\n found.push(...absolute('standardErrorPath', props.standardErrorPath));\n found.push(...absolute('workingDirectory', props.workingDirectory));\n\n if (domain?.kind === 'gui' && (props.userName !== undefined || props.groupName !== undefined)) {\n found.push(\n 'userName/groupName apply only to the system domain; launchd ignores them for agents',\n );\n }\n for (const key of Object.keys(props.extraKeys ?? {})) {\n if (OWNED_KEYS.includes(key))\n found.push(`extraKeys.${key} is owned by a typed prop; use that instead`);\n }\n return found;\n};\n",
29
+ "/**\n * The four `launchctl` subcommands LaunchdJob uses — print, print-disabled, bootstrap, bootout —\n * over a HostRunner, plus the parsers for the two read-only ones.\n *\n * ★ MEASURED 2026-09-21 ON macOS 27.2 (read-only `print` / `print-disabled` only; nothing was\n * bootstrapped or booted out to write this):\n * - `print system/<unknown>` exits **113** with \"Could not find service … in domain for system\"\n * on stderr. 113 is therefore \"not loaded\", not an error.\n * - `print` of a loaded job is a `<target> = {` block. Its own fields sit one tab deep\n * (`\\tstate = running`, `\\tpid = 1219`, `\\tlast exit code = 0` or `(never exited)`); nested\n * blocks (`resource coalition`, `semaphores`) repeat names like `state` two tabs deep.\n * - `print-disabled <domain>` lists `\"label\" => enabled|disabled`.\n * - reading needs no root: a uid-501 shell printed system-domain jobs.\n * - `print gui/<uid>/<label>` for a uid with NO LOGIN SESSION exits **112** with \"Could not find\n * domain for user gui: <uid>\" (and so does `print-disabled gui/<uid>`); a malformed target is\n * 64, a missing service in a live gui domain is 113. So 112 + that text is \"the domain does\n * not exist\", which means the job cannot be loaded — not an error for a read or a delete.\n * ⚠️ REASONED, NOT MEASURED: bootstrap/bootout exit codes. nix-darwin's own activation script on the\n * same host notes \"`bootout` on an already-absent job is a no-op that returns non-zero\", which is\n * why bootout here is always preceded by a print and followed by a poll, never trusted alone.\n * ⛔ NEVER PUT `print` STDOUT IN AN ERROR. It contains the job's whole environment.\n */\nimport type { HostRunner } from './runner.ts';\n\nexport const LAUNCHCTL = '/bin/launchctl';\n/** `launchctl print` of an unknown service — measured, see above. */\nexport const NOT_FOUND = 113;\n/** `launchctl print` into a gui domain with no login session — measured, see above. */\nexport const NO_DOMAIN = 112;\nconst NO_DOMAIN_TEXT = 'Could not find domain';\n\nexport class LaunchctlError extends Error {\n constructor(command: string, exitCode: number, stderr: string) {\n super(`launchctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);\n this.name = 'LaunchctlError';\n }\n}\n\nexport type ServiceStatus = {\n readonly loaded: boolean;\n /** The domain itself is absent (a gui/<uid> with no login session). Implies `loaded: false`. */\n readonly domainMissing?: true;\n readonly state?: string;\n readonly pid?: number;\n readonly lastExitCode?: number;\n};\n\n/**\n * The job block's own `key = value` lines, ignoring nested blocks.\n * ⚠️ A multi-line ProgramArguments entry is printed raw inside `arguments = {`, so a close is only\n * accepted when its indentation matches the open. An argument that itself contains a line of\n * exactly one tab and `}` would still desynchronise this; no real job has one.\n */\nexport const printFields = (stdout: string): Map<string, string> => {\n const fields = new Map<string, string>();\n const closers: string[] = [];\n for (const line of stdout.split('\\n')) {\n if (closers.length === 0) {\n if (fields.size === 0 && line.endsWith(' = {')) closers.push('}');\n continue;\n }\n if (line === closers[closers.length - 1]) {\n closers.pop();\n continue;\n }\n const opened = /^(\\t+)\\S.* = \\{$/.exec(line);\n if (opened?.[1] !== undefined && opened[1].length === closers.length) {\n closers.push(`${opened[1]}}`);\n continue;\n }\n const field = closers.length === 1 ? /^\\t([a-z][a-z ]*[a-z]) = (.*)$/.exec(line) : null;\n if (field?.[1] !== undefined && field[2] !== undefined && !fields.has(field[1])) {\n fields.set(field[1], field[2]);\n }\n }\n return fields;\n};\n\nconst leadingInt = (text: string | undefined): number | undefined => {\n const match = text === undefined ? null : /^(-?\\d+)/.exec(text);\n return match?.[1] === undefined ? undefined : Number(match[1]);\n};\n\nexport const parsePrint = (stdout: string): ServiceStatus => {\n const fields = printFields(stdout);\n const state = fields.get('state');\n const pid = leadingInt(fields.get('pid'));\n // \"(never exited)\" parses to undefined; \"78: EX_CONFIG\" to 78.\n const lastExitCode = leadingInt(fields.get('last exit code'));\n return {\n loaded: true,\n ...(state === undefined ? {} : { state }),\n ...(pid === undefined ? {} : { pid }),\n ...(lastExitCode === undefined ? {} : { lastExitCode }),\n };\n};\n\n/** Labels `print-disabled` reports as disabled. Older releases printed `=> true` for disabled. */\nexport const parseDisabled = (stdout: string): Set<string> => {\n const disabled = new Set<string>();\n for (const match of stdout.matchAll(/^\\s*\"([^\"]+)\" => (\\w+)\\s*$/gm)) {\n if (match[1] !== undefined && (match[2] === 'disabled' || match[2] === 'true')) {\n disabled.add(match[1]);\n }\n }\n return disabled;\n};\n\nexport const printService = async (runner: HostRunner, target: string): Promise<ServiceStatus> => {\n const result = await runner.exec([LAUNCHCTL, 'print', target]);\n if (result.exitCode === NOT_FOUND) return { loaded: false };\n // ⚠️ Without this, deleting an agent while its user is logged out fails on the read, and the\n // plist stays in LaunchAgents — where launchd loads it again at that user's next login.\n if (result.exitCode === NO_DOMAIN && result.stderr.includes(NO_DOMAIN_TEXT)) {\n return { domainMissing: true, loaded: false };\n }\n if (result.exitCode !== 0)\n throw new LaunchctlError(`print ${target}`, result.exitCode, result.stderr);\n return parsePrint(result.stdout);\n};\n\nexport const isDisabled = async (\n runner: HostRunner,\n domain: string,\n label: string,\n): Promise<boolean> => {\n const result = await runner.exec([LAUNCHCTL, 'print-disabled', domain]);\n if (result.exitCode !== 0) {\n throw new LaunchctlError(`print-disabled ${domain}`, result.exitCode, result.stderr);\n }\n return parseDisabled(result.stdout).has(label);\n};\n\nexport const bootstrap = async (\n runner: HostRunner,\n domain: string,\n plistPath: string,\n): Promise<void> => {\n const result = await runner.exec([LAUNCHCTL, 'bootstrap', domain, plistPath]);\n if (result.exitCode !== 0) {\n throw new LaunchctlError(`bootstrap ${domain} ${plistPath}`, result.exitCode, result.stderr);\n }\n};\n\n/**\n * Poll `print` until the service is gone. ⚠️ bootout can return before launchd lets go of it.\n * ★ 30 s, because a job gets SIGTERM and then its ExitTimeOut before SIGKILL: launchd.plist(5)\n * documents a 20 s default (a macOS 27.2 `print` showed `exit timeout = 5` for a job that sets\n * none). Giving up sooner would fail a deploy over a job that was, correctly, still exiting.\n */\nexport const waitUnloaded = async (\n runner: HostRunner,\n target: string,\n attempts = 120,\n intervalMs = 250,\n): Promise<void> => {\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n if (!(await printService(runner, target)).loaded) return;\n await runner.sleep(intervalMs);\n }\n throw new Error(\n `${target} is still loaded ${String((attempts * intervalMs) / 1000)}s after bootout`,\n );\n};\n\n/** Boot a service out if it is loaded, and wait until launchd has let go. Idempotent. */\nexport const bootoutIfLoaded = async (runner: HostRunner, target: string): Promise<void> => {\n if (!(await printService(runner, target)).loaded) return;\n const result = await runner.exec([LAUNCHCTL, 'bootout', target]);\n /**\n * ⚠️ A non-zero bootout is only an error if the service is STILL there after the wait (see\n * header) — so wait FIRST, then judge. A job slower to exit than launchctl's own wait is widely\n * reported to fail bootout with `36: Operation now in progress` (EINPROGRESS) while it is\n * still going away; REASONED from those reports, not measured here. Judging on an immediate\n * print would fail the deploy with the new plist written and the old job already exiting —\n * the job ends up down either way, and the error would blame the wrong step.\n */\n try {\n await waitUnloaded(runner, target);\n } catch (cause) {\n if (result.exitCode === 0) throw cause;\n throw new LaunchctlError(\n `bootout ${target}`,\n result.exitCode,\n `${result.stderr.trim()} (and still loaded after waiting)`,\n );\n }\n};\n",
30
+ "/**\n * Everything sudo-runner.ts may run as root, as exact argv shapes, plus the rule for which calls\n * need root at all. Pure: no I/O, so every shape is tested without sudo (sudo-allowlist.test.ts).\n *\n * ⛔ AN ALLOWLIST OF SHAPES, NOT OF PROGRAMS. Allowing \"launchctl\" would allow `bootout system` with\n * no service, which removes the whole system domain, and `bootstrap system <directory>`, which\n * loads every plist in that directory (launchctl(1), read on macOS 27.2 on 2026-09-21). Each shape\n * below fixes the subcommand, the domain, the flags and the number of operands, so neither of\n * those can be written.\n * ⛔ PATHS ARE CHECKED LEXICALLY HERE: absolute, normalised (the same rule a HostFile path obeys,\n * host-file-form.ts) and STRICTLY under a prefix the stack declared. sudo-guard.ts then checks\n * the host with lstat: the prefix a root-only directory, no symlinks, and only regular files.\n * ⛔ `install` COPIES ONLY THE FILE THIS RUNNER STAGED. Any other source would let the runner copy\n * a file only root can read (/etc/master.passwd, say) to a path the deploying user can read.\n */\nimport { pathProblems } from './host-file-form.ts';\nimport { plistPathFor } from './job-form.ts';\nimport { LABEL, RESERVED_LABEL_PREFIXES } from './job-validate.ts';\nimport { LAUNCHCTL } from './launchctl.ts';\n\n/**\n * A privileged call that was refused before it ran: by the allowlist, by a host check\n * (sudo-guard.ts), or by sudo itself. ★ Here, beside the allowlist, so the guard and the runner\n * throw the same class and a caller can tell \"nothing ran as root\" from \"it ran and failed\".\n */\nexport class SudoRefusedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SudoRefusedError';\n }\n}\n\n/** ★ Absolute program paths, so neither sudo nor the command is looked up on the caller's PATH. */\nexport const SUDO = '/usr/bin/sudo';\nexport const INSTALL = '/usr/bin/install';\nexport const RM = '/bin/rm';\n\n/** The launchctl subcommands that change a domain. Everything else launchctl does is a read. */\nconst WRITES: ReadonlySet<string> = new Set(['bootstrap', 'bootout', 'kickstart']);\n\n/** A mode as `install -m` takes it: four octal digits, so `0644` and `4755` are both exact. */\nexport const octalMode = (mode: number): string | undefined =>\n Number.isSafeInteger(mode) && mode >= 0 && mode <= 0o7777\n ? mode.toString(8).padStart(4, '0')\n : undefined;\n\n/** Validate the stack's prefixes once, when the runner is built. */\nexport const checkPrefixes = (prefixes: readonly string[]): readonly string[] => {\n if (prefixes.length === 0) {\n throw new Error(\n 'sudoRunner: no prefixes. Name the directories root may write, e.g. /Library/LaunchDaemons.',\n );\n }\n for (const prefix of prefixes) {\n const found = prefix === '/' ? ['\"/\" would allow every path'] : pathProblems(prefix);\n if (found.length > 0) {\n throw new Error(`sudoRunner: prefix ${JSON.stringify(prefix)}: ${found.join('; ')}`);\n }\n }\n return [...prefixes];\n};\n\n/**\n * The declared prefix `path` is strictly under, or `undefined`.\n * ⚠️ `${prefix}/`, never a bare startsWith: `/opt/example` must not admit `/opt/example-other`.\n */\nexport const prefixOf = (path: string, prefixes: readonly string[]): string | undefined =>\n pathProblems(path).length > 0\n ? undefined\n : prefixes.find((prefix) => path.startsWith(`${prefix}/`));\n\nconst labelOk = (label: string): boolean =>\n LABEL.test(label) && !RESERVED_LABEL_PREFIXES.some((prefix) => label.startsWith(prefix));\n\n/** The label of `system/<label>`, when it is one LaunchdJob itself would accept. */\nconst systemLabel = (target: string | undefined): string | undefined => {\n const label = target?.startsWith('system/') === true ? target.slice('system/'.length) : undefined;\n return label !== undefined && labelOk(label) ? label : undefined;\n};\n\n/**\n * A label's daemon plist, when that path is under a declared prefix.\n * ★ ONLY `/Library/LaunchDaemons/<label>.plist`, derived by the same plistPathFor LaunchdJob uses:\n * it is the one directory launchd loads daemons from at boot, so a plist anywhere else under a\n * prefix would be a root daemon that silently vanishes at the next restart. Root never loads one.\n */\nconst daemonPlist = (label: string, prefixes: readonly string[]): string | undefined => {\n const path = plistPathFor(label, { kind: 'system' }, undefined);\n return prefixOf(path, prefixes) === undefined ? undefined : path;\n};\n\nconst NO_DAEMON_PREFIX =\n 'needs /Library/LaunchDaemons among the prefixes, where the job’s plist is written and removed';\n\nconst launchctlProblem = (\n args: readonly string[],\n prefixes: readonly string[],\n): string | undefined => {\n const [sub, ...rest] = args;\n if (sub === 'bootstrap') {\n const given = rest[1] ?? '';\n const name = given.slice(given.lastIndexOf('/') + 1);\n const label = name.endsWith('.plist') ? name.slice(0, -'.plist'.length) : '';\n const shaped = rest.length === 2 && rest[0] === 'system' && labelOk(label);\n if (!shaped) return 'bootstrap takes exactly `system /Library/LaunchDaemons/<label>.plist`';\n const plist = daemonPlist(label, prefixes);\n if (plist === undefined) return `bootstrap ${NO_DAEMON_PREFIX}`;\n return given === plist ? undefined : `bootstrap loads only ${plist}, never another path`;\n }\n if (sub === 'bootout') {\n const label = rest.length === 1 ? systemLabel(rest[0]) : undefined;\n if (label === undefined) return 'bootout takes exactly `system/<label>`, never a bare domain';\n /**\n * ⚠️ WITHOUT THE PREFIX, A DELETE STOPS HALFWAY. deleteJob boots the job out, then removes its\n * plist; with /Library/LaunchDaemons undeclared the removal is refused AFTER the bootout, the\n * plist stays, and launchd loads the job again at the next boot. Refused here, nothing moves.\n */\n return daemonPlist(label, prefixes) === undefined ? `bootout ${NO_DAEMON_PREFIX}` : undefined;\n }\n if (sub === 'kickstart') {\n const flags = rest.slice(0, -1);\n const flagsOk =\n flags.every((flag) => flag === '-k' || flag === '-p') && new Set(flags).size === flags.length;\n return flagsOk && systemLabel(rest[rest.length - 1]) !== undefined\n ? undefined\n : 'kickstart takes only -k / -p, then `system/<label>`';\n }\n return `launchctl ${String(sub)} is never run as root`;\n};\n\nconst ID = /^\\d+$/;\n\n/** `install`'s optional owner flags: none, `-o N`, `-g N`, or `-o N -g N`. Numeric ids only. */\nconst idsOk = (ids: readonly string[]): boolean => {\n const [flag, id, second, secondId] = ids;\n if (ids.length === 0) return true;\n if (ids.length === 2) return (flag === '-o' || flag === '-g') && ID.test(id ?? '');\n return (\n ids.length === 4 &&\n flag === '-o' &&\n ID.test(id ?? '') &&\n second === '-g' &&\n ID.test(secondId ?? '')\n );\n};\n\n/** The one source `install` may copy and the prefixes, for the call being checked. */\nexport type AllowContext = { readonly prefixes: readonly string[]; readonly staged?: string };\n\n/** `undefined` when `argv` is exactly an allowed privileged shape, else the reason it is not. */\nexport const privilegedProblem = (\n argv: readonly string[],\n context: AllowContext,\n): string | undefined => {\n const [program, ...args] = argv;\n const under = (path: string | undefined) =>\n path !== undefined && prefixOf(path, context.prefixes) !== undefined;\n if (program === LAUNCHCTL) return launchctlProblem(args, context.prefixes);\n if (program === INSTALL) {\n // -S -m MODE [-o UID] [-g GID] STAGED DEST: nothing else, in this order.\n const [safe, m, mode, ...rest] = args;\n if (safe !== '-S' || m !== '-m' || mode === undefined || !/^[0-7]{4}$/.test(mode)) {\n return 'install takes exactly `-S -m <4 octal digits> [-o <uid>] [-g <gid>] <staged> <dest>`';\n }\n if (rest.length < 2 || !idsOk(rest.slice(0, -2))) return 'install takes only -o <uid> -g <gid>';\n const [source, dest] = rest.slice(-2);\n if (source === undefined || source !== context.staged) {\n return 'install copies only the file this runner staged';\n }\n return under(dest) ? undefined : 'install destination is not under a declared prefix';\n }\n if (program === RM) {\n const [force, end, path, ...extra] = args;\n return force === '-f' && end === '--' && extra.length === 0 && under(path)\n ? undefined\n : 'rm takes exactly `-f -- <path under a declared prefix>`';\n }\n return `${String(program)} is not on the sudo allowlist`;\n};\n\n/** Where one exec goes: run as the deploying user, run as root through the allowlist, or refused. */\nexport type Route =\n | { readonly as: 'operator' }\n | { readonly as: 'root' }\n | { readonly refuse: string };\n\n/**\n * ★ WHY THIS REFUSES AS WELL AS ROUTES. sudoRunner reports `privileged: true`, which switches off\n * the providers' own \"another user's gui domain needs root\" refusal (job-preflight.ts). This\n * runner elevates only the system domain, so it gives that refusal back itself. (A write into\n * another user's LaunchAgents is refused by the writer first: outside every prefix, a foreign\n * owner needs root. This covers the delete, which reaches launchctl before any file.)\n * ⚠️ Reads (`print`, `print-disabled`) always run as the deploying user: launchctl(1) says anyone\n * may read or query the system domain, and launchctl.ts measured it on 2026-09-21.\n */\nexport const routeExec = (argv: readonly string[], operatorUid: number): Route => {\n const [program, sub] = argv;\n if (program === 'sudo' || program?.endsWith('/sudo') === true) {\n return {\n refuse: 'sudoRunner never passes a sudo argv through; it elevates only its allowlist',\n };\n }\n if (program !== LAUNCHCTL || sub === undefined || !WRITES.has(sub)) return { as: 'operator' };\n const target = sub === 'kickstart' ? argv[argv.length - 1] : argv[2];\n if (target === 'system' || target?.startsWith('system/') === true) return { as: 'root' };\n const owner = /^(?:gui|user)\\/(\\d+)(?:\\/|$)/.exec(target ?? '')?.[1];\n if (owner === undefined || Number(owner) === operatorUid) return { as: 'operator' };\n return {\n refuse:\n `launchctl ${sub} ${String(target)}: that domain belongs to uid ${owner}, and sudoRunner ` +\n 'elevates only the system domain. Deploy as that user, or as root.',\n };\n};\n",
31
+ "/**\n * The `mkdir` / `chmod` / `chown` / `rmdir` half of sudo-allowlist.ts, split out to keep that file\n * under the house's 250-line cap. These four are `Host.Directory`'s whole write surface\n * (directory-lifecycle.ts): this file fixes their argv to the EXACT shapes that file emits.\n *\n * ⛔ `chown` TAKES ONLY NUMERIC IDS, NEVER A NAME. `directory-lifecycle.ts`'s `ownerArg()` already\n * resolves a name to a uid/gid before building argv, so a name here would mean some other code\n * path built this argv — refused rather than trusted.\n * 🔴 MEASURED (adversarial review, 2026-09-23, against this file's first shipped version): NEITHER\n * the mode nor the owner was checked for anything but shape. `mkdir -m 0777 -- <path>` and\n * `chown 1001:1001 -- <path>` both passed the allowlist untouched. A stack could declare\n * `HostDirectory({ path: '/etc/systemd/system/x.service.d', mode: 0o777 })`, drop a\n * `ExecStart=` override into it, and the runner's own next `daemon-reload` + `restart` would run\n * it as root — a local privilege escalation reachable through this runner's own allowlist, not a\n * bug in the guard (which never runs against a path this permissive by design).\n * ⛔ SO: NO SPECIAL BITS, NO GROUP/OTHER WRITE, EVER — mirroring the file half's `modeProblem`\n * (`../launchd/sudo-guard.ts`) but WITHOUT its \"handed to another owner\" exemption. A directory's\n * OWNER always has write access through the owner bits alone, so no mode restriction can make a\n * non-root-owned directory here safe — which is why `chown` below is refused to anything but\n * root outright, not merely mode-restricted.\n * 🔴 MEASURED (adversarial review, round 2, 2026-09-23, cited against gnulib's `lib/userspec.c`,\n * used by GNU `chown`): a purely-numeric owner string is looked up as a NAME first —\n * `*u == '+' ? NULL : getpwnam(u)` — and only parsed as a raw number when no such account\n * exists. If a host ever had a user or group literally NAMED `\"0\"`, `chown 0 -- path` would\n * silently chown to THAT account, not uid 0 — bypassing this file's own root-only restriction\n * without tripping it. `+` skips the name lookup outright (`if it starts with '+', skip the\n * look-up`, `userspec.c`), so the allowlist now requires it: `sudo-allowlist.ts`'s\n * `canonicalize()` rewrites the bare `0`/`:0`/`0:0` argv `directory-lifecycle.ts` actually sends\n * into the forced-numeric form below before this check ever runs, so the unprivileged caller\n * never has to know this syntax exists.\n */\nimport { prefixOf } from '../launchd/sudo-allowlist.ts';\n\nexport const MKDIR = '/usr/bin/mkdir';\nexport const CHMOD = '/usr/bin/chmod';\nexport const CHOWN = '/usr/bin/chown';\nexport const RMDIR = '/usr/bin/rmdir';\n\n/**\n * `mkdir -m` / `chmod`'s mode, exactly as `directory-lifecycle.ts`'s own `octal()` formats it:\n * `mode.toString(8).padStart(3, '0')` — 3 digits, or 4 when a special bit makes it longer.\n * Parsed to a number so the setuid/setgid/group/other-write bits can be checked directly, the same\n * mask `modeProblem` uses (`0o6000 | 0o022`).\n */\nconst parseDirMode = (text: string): number | undefined =>\n /^([0-7]{3}|[1-7][0-7]{3})$/.test(text) ? Number.parseInt(text, 8) : undefined;\n\nconst DANGEROUS_MODE_BITS = 0o6022;\n\n/** `undefined` when `mode` is a safe directory mode: no setuid/setgid, no group/other write. */\nconst dirModeProblem = (mode: number | undefined): string | undefined => {\n if (mode === undefined) return 'mode';\n return (mode & DANGEROUS_MODE_BITS) !== 0\n ? 'a mode with setuid/setgid or group/other write (this runner installs only 0–0o777 minus 0o022, no special bits)'\n : undefined;\n};\n\n/**\n * `chown`'s owner argument — root only, and `+`-forced so a same-named account can never be\n * resolved instead of uid/gid `0`. `canonicalize()` builds these from the bare `0`/`:0`/`0:0`\n * `directory-lifecycle.ts`'s `ownerArg()` sends; this checks what actually reaches `sudo`.\n */\nconst ROOT_OWNER = /^(\\+0|:\\+0|\\+0:\\+0)$/;\n\nexport const dirProgramProblem = (\n program: string,\n args: readonly string[],\n prefixes: readonly string[],\n): string | undefined => {\n const under = (path: string | undefined) =>\n path !== undefined && prefixOf(path, prefixes) !== undefined;\n if (program === MKDIR) {\n const [m, modeText, end, path, ...extra] = args;\n const mode = modeText === undefined ? undefined : parseDirMode(modeText);\n if (m !== '-m' || mode === undefined || end !== '--' || extra.length !== 0 || !under(path)) {\n return 'mkdir takes exactly `-m <octal> -- <path under a declared prefix>`';\n }\n return dirModeProblem(mode);\n }\n if (program === CHMOD) {\n const [modeText, end, path, ...extra] = args;\n const mode = modeText === undefined ? undefined : parseDirMode(modeText);\n if (mode === undefined || end !== '--' || extra.length !== 0 || !under(path)) {\n return 'chmod takes exactly `<octal> -- <path under a declared prefix>`';\n }\n return dirModeProblem(mode);\n }\n if (program === CHOWN) {\n const [owner, end, path, ...extra] = args;\n return owner !== undefined &&\n ROOT_OWNER.test(owner) &&\n end === '--' &&\n extra.length === 0 &&\n under(path)\n ? undefined\n : 'chown takes exactly `+0`, `:+0` or `+0:+0` -- <path under a declared prefix>` — root ' +\n 'only, forced numeric; a directory this runner elevates never hands ownership to anyone else';\n }\n const [end, path, ...extra] = args;\n return end === '--' && extra.length === 0 && under(path)\n ? undefined\n : 'rmdir takes exactly `-- <path under a declared prefix>`';\n};\n",
32
+ "/**\n * `chown`'s two privileged shapes, split out of sudo-allowlist.ts to keep it under the house's\n * 250-line cap: the ROOT-ONLY directory form (sudo-allowlist-dir.ts owns that check) and the\n * FILE-ownership form used only on the derived temp file `sudo-write.ts` `install`s into, for a\n * `RemoteFile` whose declared owner or group is not root.\n *\n * ⛔ `install`'S OWN `-o`/`-g` ARE NEVER USED, ON EITHER PLATFORM'S ALLOWLIST NOW. 🔴 MEASURED\n * (adversarial review, round 2, 2026-09-23, against coreutils 9.7 `src/install.c` `get_ids()`):\n * unlike `chown`, `install` does NOT go through gnulib's `+`-aware `userspec.c` at all — its own\n * `get_ids()` calls `getpwnam(owner_name)` / `getgrnam(group_name)` FIRST for ANY string,\n * falling back to `xstrtoumax` only when no such account exists, with no `+`-prefix escape of\n * any kind. So `install -o 0` on a host with a user or group literally named `\"0\"` installs\n * owned by THAT account, not uid 0 — and there is no argv shape that closes this for `install`\n * itself. The fix is to never let `install` set ownership: it always runs with no `-o`/`-g` (so\n * the fresh temp file lands owned by whoever `sudo` runs it as — root:root, with no name lookup\n * at all, since omitting the flags skips `get_ids` entirely), and this file's `chown` shape sets\n * the FINAL ownership afterward, on the same temp file, before `mv`. `chown` DOES support `+`\n * (gnulib `userspec.c`: `*u == '+' ? NULL : getpwnam(u)`, \"if it starts with '+', skip the\n * look-up\") — repeated on each side of a `user:group` spec independently — so this is where the\n * fix actually lands: `+<uid>`, `:+<gid>`, or `+<uid>:+<gid>`, never a bare digit string.\n */\nimport { CHOWN, dirProgramProblem } from './sudo-allowlist-dir.ts';\n\n/** Bare `directory-lifecycle.ts` argv (`0`, `:0`, `0:0`, …) → the `+`-forced form sudo may run. */\nexport const forceNumericOwner = (spec: string): string => {\n const [uidPart, gidPart] = spec.includes(':') ? spec.split(':') : [spec, undefined];\n const uid = uidPart === undefined || uidPart === '' ? '' : `+${uidPart}`;\n const gid = gidPart === undefined ? '' : `:+${gidPart}`;\n return `${uid}${gid}`;\n};\n\n/** `+<uid>`, `:+<gid>`, or `+<uid>:+<gid>` — numeric, `+`-forced, either or both sides. */\nconst SAFE_OWNER = /^(\\+\\d+(:\\+\\d+)?|:\\+\\d+)$/;\n\n/**\n * `chown <safe spec> -- <temp>`, bound to the exact temp `install` just wrote — the file-ownership\n * half of `writeUnderPrefix`'s sequence. Any owner/group is allowed (a `RemoteFile` may declare\n * one), as long as it is numeric and forced; the temp binding is what stops it reaching anywhere\n * else, the same way `mv`'s own binding does.\n */\nconst fileChownProblem = (args: readonly string[], temp: string): string | undefined => {\n const [owner, end, path, ...extra] = args;\n if (\n owner !== undefined &&\n SAFE_OWNER.test(owner) &&\n end === '--' &&\n extra.length === 0 &&\n path === temp\n ) {\n return undefined;\n }\n return (\n 'chown on the staged temp takes exactly `+<uid>`, `:+<gid>` or `+<uid>:+<gid>` -- ' +\n '<the exact temp install just wrote>, numeric and +-forced'\n );\n};\n\n/**\n * `chown`'s dispatch: a call bound to `context.temp` (its last argument equals it) is the\n * file-ownership form above; everything else is the directory form, root-only\n * (`sudo-allowlist-dir.ts`'s own `ROOT_OWNER`, `+`-forced the same way).\n */\nexport const chownProblem = (\n args: readonly string[],\n context: { readonly prefixes: readonly string[]; readonly temp?: string },\n): string | undefined => {\n const path = args[args.length - 1];\n if (context.temp !== undefined && path === context.temp)\n return fileChownProblem(args, context.temp);\n return dirProgramProblem(CHOWN, args, context.prefixes);\n};\n",
33
+ "/**\n * Everything sshSudoRunner may run as root, as exact argv shapes, plus the rule for which calls\n * need root at all. Pure: no I/O, so every shape is tested without ssh (sudo-allowlist.test.ts).\n * The Linux twin of `../launchd/sudo-allowlist.ts` — reused directly where the rule is generic\n * (prefixes, octal 4-digit modes, `SudoRefusedError`), rewritten where GNU coreutils and systemd\n * need a different shape than macOS's `install -S` and `launchctl`.\n *\n * ⛔ AN ALLOWLIST OF SHAPES, NOT OF PROGRAMS. `chown` unqualified would let a bug hand any file to\n * any uid; `systemctl` unqualified would let `disable --global` reach every user's units. Each\n * shape below fixes the flags, the operand count and (for install/mv) which paths may appear.\n * ⛔ `install` COPIES ONLY THE FILE THIS RUNNER STAGED, into ONLY the temp path this runner derived\n * for the current write — never argv's own idea of either. Binding both stops a bug (or a future\n * change) from turning this into \"install any file the operator can read to any path root can\n * reach\".\n * ⛔ GNU `install` WRITES THROUGH ITS DESTINATION (ssh-scripts.ts's header), unlike macOS's, which\n * stages and renames on its own. So there is no `-S`: this allowlist's `install` writes a FRESH\n * temp file (never the live path), and only `mv` — a rename(2), atomic by construction — ever\n * touches the path a provider declared.\n */\nimport { NAME as UNIT_NAME } from './unit-form.ts';\nimport {\n INSTALL,\n SUDO,\n SudoRefusedError,\n checkPrefixes,\n octalMode,\n prefixOf,\n} from '../launchd/sudo-allowlist.ts';\nimport { chownProblem, forceNumericOwner } from './sudo-allowlist-chown.ts';\nimport { CHMOD, CHOWN, MKDIR, RMDIR, dirProgramProblem } from './sudo-allowlist-dir.ts';\n\nexport { INSTALL, SUDO, SudoRefusedError, checkPrefixes, octalMode, prefixOf };\nexport { CHMOD, CHOWN, MKDIR, RMDIR } from './sudo-allowlist-dir.ts';\n\n/** ★ Absolute program paths, so neither sudo nor the command is looked up on the operator's PATH. */\nexport const MV = '/usr/bin/mv';\nexport const RM = '/usr/bin/rm';\nexport const SYSTEMCTL_ABS = '/usr/bin/systemctl';\n\n/** The bare names `directory-lifecycle.ts` and `systemctl.ts` already send through `exec()`. */\nexport const DIRECTORY_PROGRAMS: ReadonlySet<string> = new Set([\n 'mkdir',\n 'chmod',\n 'chown',\n 'rmdir',\n]);\nconst DIRECTORY_PROGRAMS_ABS: ReadonlySet<string> = new Set([MKDIR, CHMOD, CHOWN, RMDIR]);\nconst ABS_OF: Readonly<Record<string, string>> = {\n chmod: CHMOD,\n chown: CHOWN,\n mkdir: MKDIR,\n rmdir: RMDIR,\n systemctl: SYSTEMCTL_ABS,\n};\n/**\n * The bare argv a provider sends → the absolute argv sudo may run. Unknown programs pass through.\n * ★ `chown`'s owner argument is ALSO rewritten here, to the `+`-forced form `sudo-allowlist-dir.ts`\n * and `sudo-allowlist-chown.ts` now require — `directory-lifecycle.ts` never has to know that\n * syntax exists, and the allowlist checks exactly what will actually reach `sudo`.\n */\nexport const canonicalize = (argv: readonly string[]): readonly string[] => {\n const abs = ABS_OF[argv[0] ?? ''];\n const program = abs ?? argv[0];\n if (program === CHOWN) {\n const [owner, ...rest] = argv.slice(1);\n return [CHOWN, owner === undefined ? '' : forceNumericOwner(owner), ...rest];\n }\n return abs === undefined ? argv : [abs, ...argv.slice(1)];\n};\n\nconst dirOf = (path: string): string => path.slice(0, path.lastIndexOf('/')) || '/';\n\n/** The one source `install` may copy, the one temp `install`/`mv` may touch, and the prefixes. */\nexport type AllowContext = {\n readonly prefixes: readonly string[];\n readonly staged?: string;\n readonly temp?: string;\n};\n\n/**\n * ⛔ NEVER `-o`/`-g` — see sudo-allowlist-chown.ts's header. `install` always writes the fresh temp\n * owned by whoever `sudo` runs it as (root:root), with no name lookup at all; a non-root owner or\n * group is set afterward by a separate, `+`-forced `chown` on that same temp, before `mv`.\n */\nconst installProblem = (args: readonly string[], context: AllowContext): string | undefined => {\n const [m, mode, t, dd, source, dest, ...extra] = args;\n if (\n m !== '-m' ||\n mode === undefined ||\n !/^[0-7]{4}$/.test(mode) ||\n t !== '-T' ||\n dd !== '--' ||\n extra.length !== 0\n ) {\n return 'install takes exactly `-m <4 octal digits> -T -- <staged> <temp>`';\n }\n if (source === undefined || source !== context.staged) {\n return 'install copies only the file this runner staged';\n }\n if (dest === undefined || dest !== context.temp) {\n return 'install writes only the derived temp path for this write';\n }\n return prefixOf(dest, context.prefixes) === undefined\n ? 'install destination is not under a declared prefix'\n : undefined;\n};\n\nconst mvProblem = (args: readonly string[], context: AllowContext): string | undefined => {\n const [f, t, dd, source, dest] = args;\n if (f !== '-f' || t !== '-T' || dd !== '--' || args.length !== 5) {\n return 'mv takes exactly `-f -T -- <temp> <dest>`';\n }\n if (source === undefined || source !== context.temp) {\n return 'mv moves only the temp path this runner just installed to';\n }\n if (dest === undefined || prefixOf(dest, context.prefixes) === undefined) {\n return 'mv destination is not under a declared prefix';\n }\n return dirOf(source) === dirOf(dest)\n ? undefined\n : 'mv must stay inside the destination’s directory';\n};\n\nconst rmProblem = (args: readonly string[], context: AllowContext): string | undefined => {\n const [force, end, path, ...extra] = args;\n return force === '-f' &&\n end === '--' &&\n extra.length === 0 &&\n path !== undefined &&\n prefixOf(path, context.prefixes) !== undefined\n ? undefined\n : 'rm takes exactly `-f -- <path under a declared prefix>`';\n};\n\nconst BANNED_SYSTEMCTL_FLAGS: ReadonlySet<string> = new Set([\n '--user',\n '--global',\n '-H',\n '-M',\n '--root',\n]);\nconst UNIT_WRITE_VERBS: ReadonlySet<string> = new Set([\n 'enable',\n 'disable',\n 'start',\n 'stop',\n 'restart',\n]);\n\nconst systemctlProblem = (\n args: readonly string[],\n prefixes: readonly string[],\n): string | undefined => {\n if (args.some((arg) => BANNED_SYSTEMCTL_FLAGS.has(arg))) {\n return 'systemctl --user/--global/-H/-M/--root is never run as root';\n }\n const [verb, ...rest] = args;\n if (verb === 'daemon-reload')\n return rest.length === 0 ? undefined : 'daemon-reload takes no operands';\n if (verb !== undefined && UNIT_WRITE_VERBS.has(verb)) {\n const [dd, unit, ...extra] = rest;\n return dd === '--' && unit !== undefined && UNIT_NAME.test(unit) && extra.length === 0\n ? undefined\n : `${verb} takes exactly \\`-- <unit>\\``;\n }\n return `systemctl ${String(verb)} is never run as root`;\n};\n\n/** `undefined` when `argv` is exactly an allowed privileged shape, else the reason it is not. */\nexport const privilegedProblem = (\n argv: readonly string[],\n context: AllowContext,\n): string | undefined => {\n const [program, ...args] = argv;\n if (program === INSTALL) return installProblem(args, context);\n if (program === MV) return mvProblem(args, context);\n if (program === RM) return rmProblem(args, context);\n if (program === SYSTEMCTL_ABS) return systemctlProblem(args, context.prefixes);\n if (program === CHOWN) return chownProblem(args, context);\n if (program === MKDIR || program === CHMOD || program === RMDIR) {\n return dirProgramProblem(program, args, context.prefixes);\n }\n return `${String(program)} is not on the sudo allowlist`;\n};\n\n/** Where one exec goes: run as the operator, run as root through the allowlist, or refused. */\nexport type Route =\n | { readonly as: 'operator' }\n | { readonly as: 'root' }\n | { readonly refuse: string };\n\n/**\n * ★ ROUTING NEVER TOUCHES THE HOST. mkdir/chmod/chown/rmdir go to root only under a declared\n * prefix — outside one they run as the operator, who may well own that path (H3/rule 7).\n * systemctl's write verbs always ATTEMPT root; the FragmentPath check that can still refuse one\n * needs a host read, so it runs in the runner's `before` hook, not here.\n */\nexport const routeExec = (argv: readonly string[], prefixes: readonly string[]): Route => {\n const [program, ...args] = argv;\n if (program === 'sudo' || program?.endsWith('/sudo') === true) {\n return {\n refuse: 'sshSudoRunner never passes a sudo argv through; it elevates only its allowlist',\n };\n }\n if (program === 'systemctl' || program === SYSTEMCTL_ABS) {\n if (args.some((arg) => BANNED_SYSTEMCTL_FLAGS.has(arg))) {\n return { refuse: 'sshSudoRunner refuses systemctl --user/--global/-H/-M/--root' };\n }\n const [verb] = args;\n return verb === 'daemon-reload' || (verb !== undefined && UNIT_WRITE_VERBS.has(verb))\n ? { as: 'root' }\n : { as: 'operator' };\n }\n // ★ Both spellings route the same way — a caller that happened to pass the absolute program\n // (nothing in this repo does today) must not silently fall through to the operator branch.\n if (DIRECTORY_PROGRAMS.has(program ?? '') || DIRECTORY_PROGRAMS_ABS.has(program ?? '')) {\n const path = args[args.length - 1];\n return path !== undefined && prefixOf(path, prefixes) !== undefined\n ? { as: 'root' }\n : { as: 'operator' };\n }\n return { as: 'operator' };\n};\n"
34
+ ],
35
+ "mappings": "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",
36
+ "debugId": "B185E625634786EC64756E2164756E21",
37
+ "names": []
38
+ }
@@ -371,7 +371,9 @@ import * as Provider7 from "alchemy/Provider";
371
371
  import * as organization5 from "@distilled.cloud/forgejo/organization";
372
372
  import * as Data2 from "effect/Data";
373
373
  import * as Effect8 from "effect/Effect";
374
- var ForgejoTeamMember = Resource7("Forgejo.TeamMember");
374
+ var ForgejoTeamMember = Resource7("Forgejo.TeamMember", {
375
+ defaultRemovalPolicy: "retain"
376
+ });
375
377
 
376
378
  class ForgejoTeamNotFoundError extends Data2.TaggedError("ForgejoTeamNotFoundError") {
377
379
  }
@@ -411,5 +413,5 @@ export {
411
413
  ForgejoTeamMemberProvider
412
414
  };
413
415
 
414
- //# debugId=D0F244891AADE98C64756E2164756E21
416
+ //# debugId=6A88B7970CFE610264756E2164756E21
415
417
  //# sourceMappingURL=index.js.map