@homeflare/alchemy 0.11.0 → 0.13.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 (290) hide show
  1. package/README.md +3 -3
  2. package/dist/caddy/admin-guard.d.ts +1 -1
  3. package/dist/caddy/index.js.map +2 -2
  4. package/dist/cloudflare/client.d.ts +1 -1
  5. package/dist/cloudflare/index.js.map +2 -2
  6. package/dist/cloudflare/lock-rules.d.ts +1 -1
  7. package/dist/forgejo/branch-protection-form.d.ts +1 -1
  8. package/dist/forgejo/branch-protection.d.ts +1 -1
  9. package/dist/forgejo/index.js.map +10 -10
  10. package/dist/forgejo/org-actions-secrets.d.ts +1 -1
  11. package/dist/forgejo/org-label.d.ts +1 -1
  12. package/dist/forgejo/org-team.d.ts +1 -1
  13. package/dist/forgejo/repo-webhook.d.ts +1 -1
  14. package/dist/forgejo/repository.d.ts +2 -2
  15. package/dist/forgejo/repository.d.ts.map +1 -1
  16. package/dist/forgejo/resource.d.ts +2 -2
  17. package/dist/forgejo/team-member.d.ts +1 -1
  18. package/dist/openbao/auth-role.d.ts +1 -1
  19. package/dist/openbao/bao-http.d.ts +4 -4
  20. package/dist/openbao/cloudflare-permission-groups.d.ts +1 -1
  21. package/dist/openbao/cloudflare-role-form.d.ts +2 -2
  22. package/dist/openbao/cloudflare-role-form.d.ts.map +1 -1
  23. package/dist/openbao/cloudflare-roles-expand.d.ts +1 -1
  24. package/dist/openbao/digest.d.ts +1 -1
  25. package/dist/openbao/forgejo-bootstrap-parse.d.ts +2 -2
  26. package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -1
  27. package/dist/openbao/forgejo-bootstrap.d.ts +1 -1
  28. package/dist/openbao/index.js.map +14 -14
  29. package/dist/openbao/mount.d.ts +1 -1
  30. package/dist/openbao/pki-role.d.ts +1 -1
  31. package/dist/openbao/policy.d.ts +2 -2
  32. package/dist/openbao/proxmox-role-form.d.ts +2 -2
  33. package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
  34. package/dist/openbao/proxmox-role.d.ts +10 -10
  35. package/dist/openbao/ssh-role.d.ts +3 -3
  36. package/dist/proxmox/acl.d.ts +2 -2
  37. package/dist/proxmox/acl.d.ts.map +1 -1
  38. package/dist/proxmox/alertmanager-body.d.ts +54 -0
  39. package/dist/proxmox/alertmanager-body.d.ts.map +1 -0
  40. package/dist/proxmox/api-token-form.d.ts +2 -2
  41. package/dist/proxmox/api-token.d.ts +11 -11
  42. package/dist/proxmox/ceph-daemon-form.d.ts +3 -3
  43. package/dist/proxmox/ceph-daemon.d.ts +19 -19
  44. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  45. package/dist/proxmox/ceph-flag.d.ts +5 -5
  46. package/dist/proxmox/ceph-fs-wire.d.ts +5 -5
  47. package/dist/proxmox/ceph-fs.d.ts +16 -16
  48. package/dist/proxmox/ceph-osd-tree.d.ts +16 -16
  49. package/dist/proxmox/ceph-osd.d.ts +5 -5
  50. package/dist/proxmox/ceph-pool-form.d.ts +13 -13
  51. package/dist/proxmox/ceph-pool-form.d.ts.map +1 -1
  52. package/dist/proxmox/ceph-pool.d.ts +3 -3
  53. package/dist/proxmox/client.d.ts +2 -2
  54. package/dist/proxmox/credential-literals.d.ts +36 -0
  55. package/dist/proxmox/credential-literals.d.ts.map +1 -0
  56. package/dist/proxmox/credentials.d.ts +8 -8
  57. package/dist/proxmox/credentials.d.ts.map +1 -1
  58. package/dist/proxmox/fake-pbs-notify.d.ts +23 -0
  59. package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -0
  60. package/dist/proxmox/fake-pve.d.ts +5 -0
  61. package/dist/proxmox/fake-pve.d.ts.map +1 -1
  62. package/dist/proxmox/firewall-alias-form.d.ts +1 -1
  63. package/dist/proxmox/firewall-alias.d.ts +1 -1
  64. package/dist/proxmox/generated/pbs.d.ts +279 -1
  65. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  66. package/dist/proxmox/generated/pve.d.ts +58 -1
  67. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  68. package/dist/proxmox/group.d.ts +8 -8
  69. package/dist/proxmox/ha-resource.d.ts +3 -2
  70. package/dist/proxmox/ha-resource.d.ts.map +1 -1
  71. package/dist/proxmox/ha-rule-form.d.ts +5 -5
  72. package/dist/proxmox/ha-rule.d.ts +3 -3
  73. package/dist/proxmox/index.d.ts +14 -0
  74. package/dist/proxmox/index.d.ts.map +1 -1
  75. package/dist/proxmox/index.js +935 -223
  76. package/dist/proxmox/index.js.map +69 -56
  77. package/dist/proxmox/lease-cache.d.ts +4 -4
  78. package/dist/proxmox/lxc-harness.d.ts +1 -1
  79. package/dist/proxmox/lxc-props.d.ts +2 -2
  80. package/dist/proxmox/lxc.d.ts +3 -3
  81. package/dist/proxmox/members.d.ts +3 -3
  82. package/dist/proxmox/network-apply-read.d.ts +5 -5
  83. package/dist/proxmox/network-apply.d.ts +9 -9
  84. package/dist/proxmox/node-network-form.d.ts +6 -6
  85. package/dist/proxmox/node-network.d.ts +17 -17
  86. package/dist/proxmox/node-network.d.ts.map +1 -1
  87. package/dist/proxmox/notification-matcher-form.d.ts +100 -0
  88. package/dist/proxmox/notification-matcher-form.d.ts.map +1 -0
  89. package/dist/proxmox/notification-matcher.d.ts +46 -0
  90. package/dist/proxmox/notification-matcher.d.ts.map +1 -0
  91. package/dist/proxmox/notification-target.d.ts +3 -2
  92. package/dist/proxmox/notification-target.d.ts.map +1 -1
  93. package/dist/proxmox/pbs-datastore-guard.d.ts +1 -1
  94. package/dist/proxmox/pbs-datastore.d.ts +10 -10
  95. package/dist/proxmox/pbs-notification-matcher.d.ts +53 -0
  96. package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -0
  97. package/dist/proxmox/pbs-notification-target-form.d.ts +63 -0
  98. package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -0
  99. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +100 -0
  100. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -0
  101. package/dist/proxmox/pbs-notification-target-wire.d.ts +30 -0
  102. package/dist/proxmox/pbs-notification-target-wire.d.ts.map +1 -0
  103. package/dist/proxmox/pbs-notification-target.d.ts +123 -0
  104. package/dist/proxmox/pbs-notification-target.d.ts.map +1 -0
  105. package/dist/proxmox/pbs-prune-job.d.ts +2 -2
  106. package/dist/proxmox/pbs-sync-job.d.ts +4 -4
  107. package/dist/proxmox/pbs-verify-job.d.ts +1 -1
  108. package/dist/proxmox/pool.d.ts +4 -4
  109. package/dist/proxmox/provision-baseline.d.ts +74 -0
  110. package/dist/proxmox/provision-baseline.d.ts.map +1 -0
  111. package/dist/proxmox/provision-bootstrap.d.ts +33 -0
  112. package/dist/proxmox/provision-bootstrap.d.ts.map +1 -0
  113. package/dist/proxmox/provision-cli-fake.d.ts +38 -0
  114. package/dist/proxmox/provision-cli-fake.d.ts.map +1 -0
  115. package/dist/proxmox/provision-declare.d.ts +25 -0
  116. package/dist/proxmox/provision-declare.d.ts.map +1 -0
  117. package/dist/proxmox/replication-job.d.ts +9 -8
  118. package/dist/proxmox/replication-job.d.ts.map +1 -1
  119. package/dist/proxmox/resource.d.ts +6 -5
  120. package/dist/proxmox/resource.d.ts.map +1 -1
  121. package/dist/proxmox/sdn-apply-read.d.ts +7 -7
  122. package/dist/proxmox/sdn-apply.d.ts +1 -1
  123. package/dist/proxmox/sdn-subnet-form.d.ts +1 -1
  124. package/dist/proxmox/sdn-subnet.d.ts +5 -5
  125. package/dist/proxmox/sdn-vnet.d.ts +5 -6
  126. package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
  127. package/dist/proxmox/storage.d.ts +4 -3
  128. package/dist/proxmox/storage.d.ts.map +1 -1
  129. package/dist/proxmox/user.d.ts +3 -2
  130. package/dist/proxmox/user.d.ts.map +1 -1
  131. package/dist/proxmox/values.d.ts +1 -1
  132. package/dist/proxmox/write-only.d.ts +35 -0
  133. package/dist/proxmox/write-only.d.ts.map +1 -0
  134. package/dist/proxmox/zfs-pool-write.d.ts +1 -1
  135. package/dist/proxmox/zfs-pool.d.ts +5 -5
  136. package/dist/talos/credentials.d.ts +1 -1
  137. package/dist/talos/credentials.d.ts.map +1 -1
  138. package/dist/talos/index.js.map +1 -1
  139. package/dist/verify/fake-engine.d.ts +5 -0
  140. package/dist/verify/fake-engine.d.ts.map +1 -1
  141. package/docs/adopt-verify.md +21 -5
  142. package/docs/adopted-deploys.md +9 -1
  143. package/docs/ownership.md +26 -1
  144. package/docs/pbs-alertmanager-body.md +73 -0
  145. package/docs/pbs-notifications.md +163 -0
  146. package/docs/provision-baseline.md +106 -0
  147. package/docs/proxmox-lxc-adopt.md +2 -2
  148. package/docs/proxmox-lxc.md +5 -4
  149. package/docs/proxmox.md +9 -0
  150. package/package.json +1 -1
  151. package/src/caddy/admin-guard.ts +1 -1
  152. package/src/caddy/local-admin.ts +1 -1
  153. package/src/cloudflare/client.ts +1 -1
  154. package/src/cloudflare/lock-rules.ts +1 -1
  155. package/src/forgejo/branch-protection-form.ts +1 -1
  156. package/src/forgejo/branch-protection.ts +1 -1
  157. package/src/forgejo/client.ts +1 -1
  158. package/src/forgejo/org-actions-secrets.ts +1 -1
  159. package/src/forgejo/org-label.ts +1 -1
  160. package/src/forgejo/org-team.ts +1 -1
  161. package/src/forgejo/repo-webhook.ts +1 -1
  162. package/src/forgejo/repository.ts +2 -2
  163. package/src/forgejo/resource.ts +2 -2
  164. package/src/forgejo/team-member.ts +1 -1
  165. package/src/openbao/README.md +4 -4
  166. package/src/openbao/auth-role.ts +1 -1
  167. package/src/openbao/bao-http.test.ts +2 -2
  168. package/src/openbao/bao-http.ts +6 -6
  169. package/src/openbao/bao-status.test.ts +1 -1
  170. package/src/openbao/bao-status.ts +1 -1
  171. package/src/openbao/cloudflare-group-scope.test.ts +1 -1
  172. package/src/openbao/cloudflare-permission-groups.test.ts +1 -1
  173. package/src/openbao/cloudflare-permission-groups.ts +3 -3
  174. package/src/openbao/cloudflare-policy.ts +1 -1
  175. package/src/openbao/cloudflare-role-form.ts +2 -2
  176. package/src/openbao/cloudflare-roles-expand.ts +1 -1
  177. package/src/openbao/digest.ts +1 -1
  178. package/src/openbao/forgejo-bootstrap-parse.test.ts +3 -3
  179. package/src/openbao/forgejo-bootstrap-parse.ts +2 -2
  180. package/src/openbao/forgejo-bootstrap.ts +4 -4
  181. package/src/openbao/kubernetes-role-form.test.ts +5 -5
  182. package/src/openbao/mount.ts +1 -1
  183. package/src/openbao/pki-role.ts +1 -1
  184. package/src/openbao/policy-wire.test.ts +3 -3
  185. package/src/openbao/policy.ts +2 -2
  186. package/src/openbao/proxmox-role-form.ts +5 -5
  187. package/src/openbao/proxmox-role.ts +11 -11
  188. package/src/openbao/ssh-role.ts +3 -3
  189. package/src/proxmox/acl.ts +4 -4
  190. package/src/proxmox/adopt-noop.test.ts +6 -6
  191. package/src/proxmox/alertmanager-body.ts +125 -0
  192. package/src/proxmox/api-token-form.ts +4 -4
  193. package/src/proxmox/api-token.ts +13 -13
  194. package/src/proxmox/ceph-daemon-form.ts +3 -3
  195. package/src/proxmox/ceph-daemon.ts +20 -20
  196. package/src/proxmox/ceph-flag.ts +7 -7
  197. package/src/proxmox/ceph-fs-wire.ts +7 -7
  198. package/src/proxmox/ceph-fs.ts +18 -18
  199. package/src/proxmox/ceph-osd-tree.ts +16 -16
  200. package/src/proxmox/ceph-osd.ts +6 -6
  201. package/src/proxmox/ceph-pool-adopt.test.ts +14 -14
  202. package/src/proxmox/ceph-pool-form.ts +13 -13
  203. package/src/proxmox/ceph-pool.ts +6 -6
  204. package/src/proxmox/client.test.ts +3 -3
  205. package/src/proxmox/client.ts +3 -3
  206. package/src/proxmox/credential-literals.test.ts +148 -0
  207. package/src/proxmox/credential-literals.ts +74 -0
  208. package/src/proxmox/credentials.test.ts +6 -8
  209. package/src/proxmox/credentials.ts +8 -8
  210. package/src/proxmox/fake-pbs-notify.ts +108 -0
  211. package/src/proxmox/fake-pve.ts +12 -2
  212. package/src/proxmox/firewall-alias-form.ts +1 -1
  213. package/src/proxmox/firewall-alias.ts +1 -1
  214. package/src/proxmox/generated/pbs.ts +330 -1
  215. package/src/proxmox/generated/pve.ts +63 -1
  216. package/src/proxmox/group.ts +10 -10
  217. package/src/proxmox/ha-resource.ts +3 -2
  218. package/src/proxmox/ha-rule-form.ts +5 -5
  219. package/src/proxmox/ha-rule.ts +3 -3
  220. package/src/proxmox/index.test.ts +10 -0
  221. package/src/proxmox/index.ts +32 -0
  222. package/src/proxmox/lease-cache.test.ts +19 -19
  223. package/src/proxmox/lease-cache.ts +4 -4
  224. package/src/proxmox/lxc-adopt.test.ts +3 -3
  225. package/src/proxmox/lxc-create-form.ts +1 -1
  226. package/src/proxmox/lxc-guards.test.ts +4 -4
  227. package/src/proxmox/lxc-harness.ts +3 -3
  228. package/src/proxmox/lxc-judge.test.ts +5 -5
  229. package/src/proxmox/lxc-lifecycle.ts +1 -1
  230. package/src/proxmox/lxc-props.ts +2 -2
  231. package/src/proxmox/lxc-read.test.ts +3 -3
  232. package/src/proxmox/lxc-strict-adopt.test.ts +1 -1
  233. package/src/proxmox/lxc-volume.test.ts +9 -9
  234. package/src/proxmox/lxc-volume.ts +1 -1
  235. package/src/proxmox/lxc-wire.ts +1 -1
  236. package/src/proxmox/lxc.ts +3 -3
  237. package/src/proxmox/members.test.ts +1 -1
  238. package/src/proxmox/members.ts +3 -3
  239. package/src/proxmox/metric-server-otel.ts +2 -2
  240. package/src/proxmox/network-apply-read.ts +10 -10
  241. package/src/proxmox/network-apply.ts +10 -10
  242. package/src/proxmox/node-network-form.ts +7 -7
  243. package/src/proxmox/node-network.ts +18 -18
  244. package/src/proxmox/notification-matcher-form.ts +221 -0
  245. package/src/proxmox/notification-matcher.test.ts +234 -0
  246. package/src/proxmox/notification-matcher.ts +76 -0
  247. package/src/proxmox/notification-target.ts +3 -2
  248. package/src/proxmox/pbs-datastore-guard.ts +1 -1
  249. package/src/proxmox/pbs-datastore.ts +10 -10
  250. package/src/proxmox/pbs-notification-matcher.ts +81 -0
  251. package/src/proxmox/pbs-notification-target-drift.test.ts +83 -0
  252. package/src/proxmox/pbs-notification-target-form.ts +229 -0
  253. package/src/proxmox/pbs-notification-target-lifecycle.ts +177 -0
  254. package/src/proxmox/pbs-notification-target-state.test.ts +191 -0
  255. package/src/proxmox/pbs-notification-target-wire.ts +112 -0
  256. package/src/proxmox/pbs-notification-target.test.ts +226 -0
  257. package/src/proxmox/pbs-notification-target.ts +139 -0
  258. package/src/proxmox/pbs-notifications-stack.test.ts +94 -0
  259. package/src/proxmox/pbs-prune-job.ts +2 -2
  260. package/src/proxmox/pbs-sync-job.ts +4 -4
  261. package/src/proxmox/pbs-verify-job.ts +1 -1
  262. package/src/proxmox/pool.test.ts +2 -2
  263. package/src/proxmox/pool.ts +4 -4
  264. package/src/proxmox/provision-baseline.test.ts +87 -0
  265. package/src/proxmox/provision-baseline.ts +162 -0
  266. package/src/proxmox/provision-bootstrap.test.ts +132 -0
  267. package/src/proxmox/provision-bootstrap.ts +135 -0
  268. package/src/proxmox/provision-cli-fake.ts +217 -0
  269. package/src/proxmox/provision-declare.test.ts +111 -0
  270. package/src/proxmox/provision-declare.ts +103 -0
  271. package/src/proxmox/replication-job.ts +9 -8
  272. package/src/proxmox/resource.ts +9 -9
  273. package/src/proxmox/sdn-apply-read.test.ts +16 -16
  274. package/src/proxmox/sdn-apply-read.ts +7 -7
  275. package/src/proxmox/sdn-apply.ts +3 -3
  276. package/src/proxmox/sdn-subnet-form.ts +1 -1
  277. package/src/proxmox/sdn-subnet.ts +5 -5
  278. package/src/proxmox/sdn-vnet.ts +5 -6
  279. package/src/proxmox/storage.ts +4 -3
  280. package/src/proxmox/user.ts +3 -2
  281. package/src/proxmox/values.test.ts +2 -2
  282. package/src/proxmox/values.ts +1 -1
  283. package/src/proxmox/write-only.test.ts +75 -0
  284. package/src/proxmox/write-only.ts +106 -0
  285. package/src/proxmox/zfs-pool-write.ts +1 -1
  286. package/src/proxmox/zfs-pool.ts +6 -6
  287. package/src/talos/credentials.ts +3 -3
  288. package/src/talos/values.test.ts +9 -9
  289. package/src/verify/cli.test.ts +1 -1
  290. package/src/verify/fake-engine.ts +8 -1
@@ -0,0 +1,25 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import { ProxmoxAcl } from './acl.ts';
3
+ import type { PveTarget } from './credentials.ts';
4
+ import { ProxmoxGroup } from './group.ts';
5
+ import { type ProvisionNames } from './provision-baseline.ts';
6
+ import { ProxmoxRole } from './role.ts';
7
+ import { ProxmoxUser } from './user.ts';
8
+ export interface DeclareProvisionOptions {
9
+ /** `true`/`false` pipe `adopt(…)` onto every resource; omitted, the deploy's policy decides. */
10
+ readonly adopt?: boolean;
11
+ }
12
+ /**
13
+ * The baseline for one cluster. `id` prefixes every logical id (`<id>-role`, `<id>-mint-group`,
14
+ * `<id>-provision-user`, `<id>-provision-grant`, and the `read` pair), so one stack can declare a
15
+ * baseline per cluster. `users` and `grants` come back in `baseline.users` / `baseline.grants`
16
+ * order: provision, then read. Fails, before declaring anything, on a name the baseline refuses.
17
+ */
18
+ export declare const declareProvisionBaseline: (id: string, target: PveTarget, names?: ProvisionNames, options?: DeclareProvisionOptions) => Effect.Effect<{
19
+ baseline: import("./provision-baseline.ts").ProvisionBaseline;
20
+ grants: ProxmoxAcl[];
21
+ group: ProxmoxGroup;
22
+ role: ProxmoxRole;
23
+ users: ProxmoxUser[];
24
+ }, Error, import("effect/unstable/http/HttpClient").HttpClient>;
25
+ //# sourceMappingURL=provision-declare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provision-declare.d.ts","sourceRoot":"","sources":["../../src/proxmox/provision-declare.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,KAAK,cAAc,EAAqB,MAAM,yBAAyB,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,MAAM,WAAW,uBAAuB;IACtC,gGAAgG;IAChG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,OAC/B,MAAM,UACF,SAAS,UACV,cAAc,YACZ,uBAAuB;;;;;;+DAgD9B,CAAC"}
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * `Proxmox.ReplicationJob` — a guest's ZFS storage replication to a second node, declared.
3
3
  *
4
- * ⛔ NOTHING ON TB4 CAN USE THIS FAMILY, AND THAT IS SAID HERE RATHER THAN FOUND OUT LATER. PVE
4
+ * ⛔ NOTHING ON C1 CAN USE THIS FAMILY, AND THAT IS SAID HERE RATHER THAN FOUND OUT LATER. PVE
5
5
  * storage replication is ZFS-only: `POST /cluster/replication` calls `get_replicatable_volumes`
6
- * and refuses a guest whose disks are anywhere else with "No replicatable volumes found". TB4 is
6
+ * and refuses a guest whose disks are anywhere else with "No replicatable volumes found". C1 is
7
7
  * Ceph/RBD-backed — shared storage, where replication means nothing because every node already
8
- * sees the volume — so `GET /cluster/replication` answers `[]` (MEASURED on n2, 2026-09-13) and
9
- * on this cluster always will. The file is written for a ZFS cluster; against TB4 a declaration
8
+ * sees the volume — so `GET /cluster/replication` answers `[]` (MEASURED on node-b, 2026-09-13) and
9
+ * on this cluster always will. The file is written for a ZFS cluster; against C1 a declaration
10
10
  * is a create that fails with PVE's own sentence, not a plan that quietly does nothing.
11
11
  *
12
- * ★ WHAT IS MEASURED HERE AND WHAT IS REASONED. Measured on n2 (pve-manager 9.2.11), read-only:
12
+ * ★ WHAT IS MEASURED HERE AND WHAT IS REASONED. Measured on node-b (pve-manager 9.2.11), read-only:
13
13
  * the published schema for all five methods; `PVE/ReplicationConfig.pm` and
14
14
  * `PVE/API2/ReplicationConfig.pm` line by line; `GET /cluster/replication` -> `[]`; and the ACL
15
15
  * row `{"path":"/","propagate":1,"roleid":"PVEAuditor","ugid":"hf-read@pve"}`. NOT measured: any
@@ -45,12 +45,13 @@
45
45
  * declarations differing only in `jobnum` but naming the same target are not two jobs; the second
46
46
  * is a create that fails.
47
47
  *
48
- * ⚠️ RECONCILE NEEDS `VM.Replicate` ON `/vms/<guest>`, WHICH `LXCProvisioner` DOES NOT HOLD. All
48
+ * ⚠️ RECONCILE NEEDS `VM.Replicate` ON `/vms/<guest>`, WHICH THE PROVISION ROLE DOES NOT HOLD. All
49
49
  * three writes check it (MEASURED in API2/ReplicationConfig.pm: create, update and delete each
50
50
  * call `$rpcenv->check($authuser, "/vms/$vmid", ['VM.Replicate'])`), and the role's 27 privileges
51
51
  * include `VM.Allocate`, `VM.Audit`, `VM.Backup`, `VM.Config.*` and `VM.PowerMgmt` but not
52
- * `VM.Replicate`. Widen it deliberately over SSH, preserving what is there, as `pool.ts` records:
53
- * `pveum role modify LXCProvisioner --privs "<existing>,VM.Replicate"`.
52
+ * `VM.Replicate` (`PROVISION_PRIVILEGES`). Do NOT widen it by hand: the baseline declares the
53
+ * role's exact set, so a hand-added privilege is removed on the next deploy. Grant a second role
54
+ * holding `VM.Replicate` on `/vms/<guest>` instead.
54
55
  * ★ THE READ LANE NEEDS NOTHING EXTRA, which is why there is no `readRole` below. The ITEM read
55
56
  * checks `VM.Audit` on `/vms/<guest>`, `hf-read@pve` holds the built-in `PVEAuditor` at `/`
56
57
  * with propagate — both MEASURED — and `PVEAuditor` carries `VM.Audit`. This family is not a
@@ -1 +1 @@
1
- {"version":3,"file":"replication-job.d.ts","sourceRoot":"","sources":["../../src/proxmox/replication-job.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmFG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAG7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAGnF,MAAM,WAAW,mBAAoB,SAAQ,UAAU;IACrD,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,iGAAiG;IACjG,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+FAA+F;IAC/F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,QAAQ,CACrD,wBAAwB,EACxB,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,qBAAqB,wDAA4D,CAAC;AAwE/F;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,2IAIvC,CAAC"}
1
+ {"version":3,"file":"replication-job.d.ts","sourceRoot":"","sources":["../../src/proxmox/replication-job.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAG7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAGnF,MAAM,WAAW,mBAAoB,SAAQ,UAAU;IACrD,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,iGAAiG;IACjG,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+FAA+F;IAC/F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,QAAQ,CACrD,wBAAwB,EACxB,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,qBAAqB,wDAA4D,CAAC;AAwE/F;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,2IAIvC,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import type { Input } from 'alchemy/Input';
2
2
  import * as Effect from 'effect/Effect';
3
3
  import type * as HttpClient from 'effect/unstable/http/HttpClient';
4
+ import { type PveForm } from './client.ts';
4
5
  import type { ApiTarget, PbsTarget, PveRole, PveTarget } from './credentials.ts';
5
6
  /**
6
7
  * ★ WHY SEVERAL FAMILIES HERE DECLARE `defaultRemovalPolicy: 'retain'`, WRITTEN ONCE.
@@ -52,21 +53,21 @@ export type WithApiTarget = {
52
53
  target: ApiTarget;
53
54
  };
54
55
  export type PveSpec<Props extends WithApiTarget, Attributes> = {
55
- /** `pools/house`, `nodes/n2/lxc/101` — where ONE object is read, updated and deleted. */
56
+ /** `pools/lab`, `nodes/node-b/lxc/101` — where ONE object is read, updated and deleted. */
56
57
  readonly path: (props: Props) => string;
57
- /** `pools`, `nodes/n2/lxc` — where a NEW one is POSTed. */
58
+ /** `pools`, `nodes/node-b/lxc` — where a NEW one is POSTed. */
58
59
  readonly collection: (props: Props) => string;
59
60
  /** Live JSON to attributes. Returning undefined means "this is not really there". */
60
61
  readonly attributes: (live: Record<string, unknown>, props: Props) => Attributes | undefined;
61
- /** The form PVE wants on create. ⚠️ PVE takes form encoding, not JSON. */
62
- readonly createForm: (props: Props) => Record<string, string>;
62
+ /** The form PVE wants on create. ⚠️ PVE takes form encoding, not JSON. Arrays: client.ts. */
63
+ readonly createForm: (props: Props) => PveForm;
63
64
  /**
64
65
  * The form for an update, or undefined when the object has no mutable fields.
65
66
  *
66
67
  * ⚠️ SOME PVE OBJECTS CANNOT BE UPDATED AT ALL. Returning undefined makes a changed prop a
67
68
  * REPLACE rather than a silent no-op, which is the honest answer for an immutable object.
68
69
  */
69
- readonly updateForm?: (props: Props) => Record<string, string>;
70
+ readonly updateForm?: (props: Props) => PveForm;
70
71
  /** True when live already matches props. Decides noop vs update. */
71
72
  readonly matches: (attributes: Attributes, props: Props) => boolean;
72
73
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/proxmox/resource.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAEnE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGjF;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;GAKG;AACH;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,UAAU,CAAC;AAEpD,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAC/C,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAClD,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,OAAO,CAAC,KAAK,SAAS,aAAa,EAAE,UAAU,IAAI;IAC7D,yFAAyF;IACzF,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IACxC,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IAC9C,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,GAAG,SAAS,CAAC;IAC7F,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/D,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,aAAa,EAAE,UAAU,QAC7D,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC;kBAGX,KAAK;iBAiBX,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBASZ,QAAQ;;yBACF,MAAM;;yBAGtC,SAAS;;sBAIV,KAAK;IA4DvB;;;;OAIG;oBACa,KAAK;CAExB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,SAAS,aAAa,EAAE,UAAU,SAAS,MAAM,QAC1E,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC;IAI9B,IAAI;IACJ,IAAI,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IAChC,IAAI,qBAAqB;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBArHlC,QAAQ;;yBACF,MAAM;;yBAGtC,SAAS;;IAmH5B,SAAS,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACrC,MAAM,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAErC,CAAC"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/proxmox/resource.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,KAAK,OAAO,EAAO,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGjF;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;GAKG;AACH;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,UAAU,CAAC;AAEpD,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAC/C,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAClD,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,OAAO,CAAC,KAAK,SAAS,aAAa,EAAE,UAAU,IAAI;IAC7D,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IACxC,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IAC9C,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,GAAG,SAAS,CAAC;IAC7F,6FAA6F;IAC7F,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IAC/C;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IAChD,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,aAAa,EAAE,UAAU,QAC7D,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC;kBAGX,KAAK;iBAiBX,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBASZ,QAAQ;;yBACF,MAAM;;yBAGtC,SAAS;;sBAIV,KAAK;IA4DvB;;;;OAIG;oBACa,KAAK;CAExB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,SAAS,aAAa,EAAE,UAAU,SAAS,MAAM,QAC1E,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC;IAI9B,IAAI;IACJ,IAAI,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IAChC,IAAI,qBAAqB;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBArHlC,QAAQ;;yBACF,MAAM;;yBAGtC,SAAS;;IAmH5B,SAAS,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACrC,MAAM,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAErC,CAAC"}
@@ -3,12 +3,12 @@
3
3
  * collections this package declares resources for.
4
4
  *
5
5
  * 🔴 THE GAP THIS FILE CLOSES, AND IT WAS FOUND BY ALMOST DECLARING THE APPLY. `pendingCount` in
6
- * sdn-apply.ts counted `cluster/sdn/zones` and `cluster/sdn/vnets`. TB4 has zero of each — and a
7
- * staged OSPF FABRIC that nothing counted. MEASURED 2026-09-13 on n2:
6
+ * sdn-apply.ts counted `cluster/sdn/zones` and `cluster/sdn/vnets`. C1 has zero of each — and a
7
+ * staged OSPF FABRIC that nothing counted. MEASURED 2026-09-13 on node-b:
8
8
  *
9
- * /etc/pve/sdn/fabrics.cfg ospf_fabric: tb4, area 1, ip_prefix 10.100.0.0/24
10
- * ospf_node: tb4_n2/n3/n4, interfaces en05 + en06,
11
- * ip 10.100.0.102 / .103 / .104
9
+ * /etc/pve/sdn/fabrics.cfg ospf_fabric: c1, area 1, ip_prefix 203.0.113.0/24
10
+ * ospf_node: c1_n2/node-c/node-d, interfaces tb0 + tb1,
11
+ * ip 203.0.113.102 / .103 / .104
12
12
  * /etc/network/interfaces.d/sdn the file PVE GENERATES from it, carrying those /32s
13
13
  *
14
14
  * Those addresses are Ceph's `cluster_network`. So `diff` would have answered `noop` for ever
@@ -38,7 +38,7 @@ import type { PveTarget } from './credentials.ts';
38
38
  *
39
39
  * ⚠️ NAMED RATHER THAN SILENTLY SKIPPED. `ipams` and `dns` answer HTTP 400 to both `?pending=1` and
40
40
  * `?running=1`, so there is no way to ask them what is staged. They are a REAL blind spot: an
41
- * ipam edited by hand is published by the apply and cannot be seen from here. TB4 has one ipam
41
+ * ipam edited by hand is published by the apply and cannot be seen from here. C1 has one ipam
42
42
  * (`pve`, the built-in) and zero dns entries, so the blind spot is currently empty — which is a
43
43
  * fact about today, not a property of the design.
44
44
  */
@@ -74,7 +74,7 @@ export declare const canonical: (value: unknown) => string;
74
74
  *
75
75
  * ⛔ ZERO HERE IS THE ONLY THING THAT MAKES DECLARING THE APPLY SAFE. `reconcile` runs on CREATE —
76
76
  * Alchemy always calls it the first time a resource appears — and `PUT /cluster/sdn` regenerates
77
- * `/etc/network/interfaces.d/sdn` on every node. On TB4 that file carries Ceph's cluster network.
77
+ * `/etc/network/interfaces.d/sdn` on every node. On C1 that file carries Ceph's cluster network.
78
78
  */
79
79
  export declare const sdnPendingCount: (target: PveTarget) => Effect.Effect<number, Error, import("effect/unstable/http/HttpClient").HttpClient>;
80
80
  //# sourceMappingURL=sdn-apply-read.d.ts.map
@@ -6,7 +6,7 @@
6
6
  * is the apply, and it is CLUSTER-WIDE — it publishes every staged object at once, not the one
7
7
  * that happened to reconcile.
8
8
  *
9
- * ★ MEASURED ON TB4 ON 2026-09-13, NOT INFERRED. A probe zone was created, inspected and removed:
9
+ * ★ MEASURED ON C1 ON 2026-09-13, NOT INFERRED. A probe zone was created, inspected and removed:
10
10
  *
11
11
  * pvesh create /cluster/sdn/zones --zone zzprobe --type simple
12
12
  * GET /cluster/sdn/zones/zzprobe -> {"digest":"c438…","type":"simple","zone":"zzprobe"}
@@ -22,7 +22,7 @@ export type DhcpRange = {
22
22
  *
23
23
  * ⛔ MEASURED from `/usr/share/perl5/PVE/API2/Network/SDN/Subnets.pm` on pve-manager 9.2.11, in
24
24
  * the create handler: `my $id = $cidr =~ s/\//-/r; $id = "$zoneid-$id";`. So `subnet=10.0.0.0/24`
25
- * goes out and `house-10.0.0.0-24` is what exists afterwards. A CIDR cannot be a path segment
25
+ * goes out and `lab-10.0.0.0-24` is what exists afterwards. A CIDR cannot be a path segment
26
26
  * anyway — it carries a slash — so there is no spelling of this endpoint that takes the CIDR.
27
27
  *
28
28
  * ⚠️ `.replace`, NOT `.replaceAll`, AND THE PARITY IS DELIBERATE. Perl's `s/\//-/r` has no `/g`, so
@@ -11,7 +11,7 @@
11
11
  * `vnet`; a literal buys no ordering, and here it also buys a wrong path.
12
12
  *
13
13
  * ⛔ THE ID PVE FILES A SUBNET UNDER IS NOT THE ONE YOU WRITE, AND IT IS THIS FAMILY'S SHARPEST
14
- * EDGE. `POST` takes `subnet=10.0.0.0/24` while GET, PUT and DELETE address `house-10.0.0.0-24`.
14
+ * EDGE. `POST` takes `subnet=10.0.0.0/24` while GET, PUT and DELETE address `lab-10.0.0.0-24`.
15
15
  * One PVE parameter name, `subnet`, carrying two different values — so they are named APART here,
16
16
  * `cidr` for the declared one and `subnetId` for the derived one, for the reason values.ts gives
17
17
  * about `flag`: a name that means two things is a bug waiting for whoever reads one meaning. The
@@ -50,11 +50,11 @@
50
50
  * seven privileges include SDN.Audit — at `/` with propagate=1. So the default 3600s auditor
51
51
  * lease reads this family, and a `readRole: 'provision'` here would be privilege nobody needed.
52
52
  * Writes are a different answer: POST, PUT and DELETE all require SDN.Allocate on
53
- * `/sdn/zones/<zone>/<vnet>`, which `LXCProvisioner` gained on 2026-09-13.
53
+ * `/sdn/zones/<zone>/<vnet>`, which the provision role gained on 2026-09-13 (`PROVISION_PRIVILEGES`).
54
54
  *
55
- * ⚠️ NO LIVE SUBNET EXISTS ON TB4 TO ROUND-TRIP AGAINST — there are no SDN zones and no vnets, so
55
+ * ⚠️ NO LIVE SUBNET EXISTS ON C1 TO ROUND-TRIP AGAINST — there are no SDN zones and no vnets, so
56
56
  * there can be no subnets. Every claim above is read off PVE 9.2.11's own schema and Perl sources
57
- * on n2, and the noop argument below rests on those rather than on a plan this file has watched.
57
+ * on node-b, and the noop argument below rests on those rather than on a plan this file has watched.
58
58
  *
59
59
  * ⚠️ TWO REFUSALS TO EXPECT, both from `SubnetPlugin::on_update_hook`: PVE will not put a subnet on
60
60
  * a vnet with `vlanaware` set ("you can't add a subnet on a vlanaware vnet"), and it rejects a
@@ -88,7 +88,7 @@ export interface SdnSubnetProps extends WithTarget {
88
88
  gateway?: string;
89
89
  /**
90
90
  * Masquerade this subnet. ⚠️ INERT UNTIL THE CLUSTER FIREWALL IS ON — PVE's own description is
91
- * "enable masquerade for this subnet if pve-firewall", and TB4's cluster firewall was empty and
91
+ * "enable masquerade for this subnet if pve-firewall", and C1's cluster firewall was empty and
92
92
  * disabled when this was written. It still round-trips, so declaring it does not diff.
93
93
  */
94
94
  snat?: boolean;
@@ -35,12 +35,11 @@
35
35
  * than an error — see the ⛔ in sdn-zone.ts and docs/privileges.md.
36
36
  *
37
37
  * ⚠️ PRIVILEGES, AND THE ESTATE'S CREDENTIAL HELD NONE OF THEM UNTIL 2026-09-13. reconcile and
38
- * delete mint `provision` and need SDN.Allocate on `/sdn/zones`, or on the one zone if scoped. `LXCProvisioner` grants
39
- * SDN.Use, which is what a GUEST needs in order to attach to a vnet and NOT what creating one
40
- * needs, so a container declared on this vnet works today while the vnet itself 403s. Widen
41
- * deliberately, the way Pool.Allocate was:
42
- * `pveum role modify LXCProvisioner --privs "<existing>,SDN.Allocate,SDN.Audit"` over SSH,
43
- * preserving every existing privilege.
38
+ * delete mint `provision` and need SDN.Allocate on `/sdn/zones`, or on the one zone if scoped. The provision role granted
39
+ * only SDN.Use then, which is what a GUEST needs in order to attach to a vnet and NOT what
40
+ * creating one needs, so a container on a vnet worked while the vnet itself 403'd. It was widened
41
+ * the way Pool.Allocate was, and `PROVISION_PRIVILEGES` (provision-baseline.ts) now carries
42
+ * SDN.Allocate and SDN.Audit.
44
43
  */
45
44
  import { Resource } from 'alchemy';
46
45
  import * as Provider from 'alchemy/Provider';
@@ -1 +1 @@
1
- {"version":3,"file":"sdn-vnet.d.ts","sourceRoot":"","sources":["../../src/proxmox/sdn-vnet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAGnF,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,cAAc,iDAA8C,CAAC;AAyG1E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,oIACoD,CAAC"}
1
+ {"version":3,"file":"sdn-vnet.d.ts","sourceRoot":"","sources":["../../src/proxmox/sdn-vnet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAGnF,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,cAAc,iDAA8C,CAAC;AAyG1E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,oIACoD,CAAC"}
@@ -21,9 +21,10 @@
21
21
  * messages point away from the cause.
22
22
  * ★ SO WIDEN OR RE-POINT BEFORE THE FIRST PLAN, outside this file: give the credential mount's
23
23
  * `read` role `Datastore.Allocate` on `/storage`, or read this family with `provision`. The
24
- * provision role in the estate this was written for holds `Datastore.AllocateSpace` and
24
+ * provision role in the estate this was written for held only `Datastore.AllocateSpace` and
25
25
  * `Datastore.Audit`, and NEITHER IS IT — AllocateSpace writes volumes INTO a storage, Allocate
26
- * DEFINES one — so it 403s here until widened the way `Pool.Allocate` was for `Proxmox.Pool`.
26
+ * DEFINES one — so it 403'd here until widened the way `Pool.Allocate` was for `Proxmox.Pool`.
27
+ * The provisioning baseline (`provision-baseline.ts`) carries `Datastore.Allocate`.
27
28
  *
28
29
  * ⛔ THERE IS NO `password` PROP AND THERE MUST NEVER BE ONE. PBS and CIFS storages take one at
29
30
  * create time, and Alchemy persists attributes UNENCRYPTED — a secret in props is one careless
@@ -64,7 +65,7 @@ export interface StorageProps extends WithTarget {
64
65
  /** `images,rootdir,vztmpl,iso,backup,snippets`. PVE reports `none` for an empty set. */
65
66
  content?: string;
66
67
  /**
67
- * Node restriction, e.g. `n1,n2`. ⚠️ A STORAGE WITH A `nodes` LIST DOES NOT EXIST ANYWHERE ELSE:
68
+ * Node restriction, e.g. `node-a,node-b`. ⚠️ A STORAGE WITH A `nodes` LIST DOES NOT EXIST ANYWHERE ELSE:
68
69
  * an LXC declared on a node outside it fails at create with "storage not available on node".
69
70
  */
70
71
  nodes?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAGnF;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACpD,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,OAAO,CAAC,EAAE,KAAK,CAAC;IAChB,gBAAgB,CAAC,EAAE,KAAK,CAAC;CAC1B,CAAC;AAEF,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,qGAAqG;AACrG,eAAO,MAAM,cAAc,iDAEzB,CAAC;AAgHH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,oIACoD,CAAC"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAGnF;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACpD,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,OAAO,CAAC,EAAE,KAAK,CAAC;IAChB,gBAAgB,CAAC,EAAE,KAAK,CAAC;CAC1B,CAAC;AAEF,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,qGAAqG;AACrG,eAAO,MAAM,cAAc,iDAEzB,CAAC;AAgHH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,oIACoD,CAAC"}
@@ -29,8 +29,9 @@
29
29
  * can then say what a delete would take with it — and kept OUT of `matches`, exactly like
30
30
  * `pool.members`. Reported, never declared.
31
31
  *
32
- * ⚠️ RECONCILE NEEDS PRIVILEGES `LXCProvisioner` DOES NOT HOLD, so state them rather than meet a
33
- * 403 in the middle of a deploy (pool.ts records how that went the last time):
32
+ * ⚠️ RECONCILE NEEDS PRIVILEGES A GUEST-ONLY ROLE DOES NOT HOLD, so they are stated rather than met
33
+ * as a 403 in the middle of a deploy (pool.ts records how that went the last time).
34
+ * `PROVISION_PRIVILEGES` (provision-baseline.ts) carries every one of them:
34
35
  * · `Realm.AllocateUser` on `/access/realm/<realm>` — create and delete.
35
36
  * · `User.Modify` on `/access/groups`, and on `/access/groups/<group>` for EVERY group named in
36
37
  * `groups` — create, update and delete. PVE checks the groups you are granting, not just the
@@ -1 +1 @@
1
- {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/proxmox/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAGnF,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mGAAmG;IACnG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAAwC,CAAC;AAiGjE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,iIACiD,CAAC"}
1
+ {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/proxmox/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAGnF,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mGAAmG;IACnG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAAwC,CAAC;AAiGjE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,iIACiD,CAAC"}
@@ -108,7 +108,7 @@ export declare const int: (value: unknown, fallback: number) => number;
108
108
  * A PVE update form with its clear-list attached, and the two guaranteed not to overlap.
109
109
  *
110
110
  * ⛔ PVE DIES IF A KEY IS SET AND DELETED IN ONE CALL, AND SIX FAMILIES HERE BUILT THAT FORM BY
111
- * HAND. Measured in the shipped Perl on n2 — `PVE::SectionConfig::delete_from_config`, line 1860:
111
+ * HAND. Measured in the shipped Perl on node-b — `PVE::SectionConfig::delete_from_config`, line 1860:
112
112
  *
113
113
  * die "cannot set and delete property '$k' at the same time!\n"
114
114
  * if defined($new_options->{$k});
@@ -0,0 +1,35 @@
1
+ /** A write-only value, by the NAME of the environment variable holding it. Never the value. */
2
+ export interface FromEnv {
3
+ readonly fromEnv: string;
4
+ }
5
+ /** Where a value is looked up. Defaults to `process.env`, read at call time, never at load. */
6
+ export type Environment = Readonly<Record<string, string | undefined>>;
7
+ /**
8
+ * Each `name → FromEnv` resolved, or the variable names that could not be.
9
+ *
10
+ * ⚠️ AN EMPTY VARIABLE IS A MISSING ONE. A secret renderer that failed writes `NAME=` rather than
11
+ * omitting the line, and sending `''` would store an empty secret on the server — a webhook that
12
+ * authenticates with nothing, reported as a successful deploy.
13
+ * ⚠️ THE VALUE IS NOT TRIMMED. A token with a trailing newline is a broken header either way, and
14
+ * trimming here would make the seal disagree with a value written by any other tool.
15
+ */
16
+ export declare const resolveAll: (refs: Readonly<Record<string, FromEnv>>, env?: Environment) => {
17
+ readonly values: Readonly<Record<string, string>>;
18
+ readonly missing: readonly string[];
19
+ };
20
+ /**
21
+ * `scrypt:<salt>:<digest>`, or `''` when there is nothing to seal.
22
+ *
23
+ * ★ A RANDOM SALT BY DEFAULT, FOR WHAT THIS PROVIDER WROTE. Pass a fixed one only for a value the
24
+ * server returns anyway (a header), where a fresh salt per read would churn the state row on
25
+ * every plan without protecting anything the server does not already show its auditors.
26
+ */
27
+ export declare const seal: (values: Readonly<Record<string, string>>, salt?: string) => string;
28
+ /**
29
+ * Whether `values` are the ones `sealed` was made from.
30
+ *
31
+ * ⚠️ A MALFORMED OR EMPTY SEAL ANSWERS FALSE, never throws: callers decide what "no evidence"
32
+ * means, and every caller here treats it as "cannot tell" rather than as drift.
33
+ */
34
+ export declare const sealMatches: (sealed: string, values: Readonly<Record<string, string>>) => boolean;
35
+ //# sourceMappingURL=write-only.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-only.d.ts","sourceRoot":"","sources":["../../src/proxmox/write-only.ts"],"names":[],"mappings":"AA2BA,+FAA+F;AAC/F,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,+FAA+F;AAC/F,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEvE;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,SACf,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,QAClC,WAAW,KACf;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAS1F,CAAC;AAqBF;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,WAAY,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,SAAS,MAAM,KAAG,MAI9E,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,WAAY,MAAM,UAAU,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAG,OAStF,CAAC"}
@@ -37,7 +37,7 @@ export declare const createForm: (props: ZfsPoolProps) => Record<string, string>
37
37
  /**
38
38
  * Create the pool if it is absent, and otherwise leave it completely alone.
39
39
  *
40
- * ⛔ THE POST ONLY FORKS A WORKER. MEASURED in `/usr/share/perl5/PVE/API2/Disks/ZFS.pm` on n2: the
40
+ * ⛔ THE POST ONLY FORKS A WORKER. MEASURED in `/usr/share/perl5/PVE/API2/Disks/ZFS.pm` on node-b: the
41
41
  * create handler ends in `$rpcenv->fork_worker('zfscreate', ...)` and its HTTP answer is a UPID
42
42
  * returned the instant the worker is forked — before `zpool create` has run, let alone finished.
43
43
  * A read-back taken immediately, as `pveOperations.reconcile` takes it, finds nothing and would
@@ -22,22 +22,22 @@
22
22
  * that one line turns a text edit into an unrecoverable one.
23
23
  *
24
24
  * ⛔ THERE IS NO PUT ON THIS FAMILY AT ALL. MEASURED from the cluster's own published schema
25
- * (`/usr/share/pve-docs/api-viewer/apidoc.js`, read on n2 2026-09-13): `/nodes/{node}/disks/zfs`
25
+ * (`/usr/share/pve-docs/api-viewer/apidoc.js`, read on node-b 2026-09-13): `/nodes/{node}/disks/zfs`
26
26
  * has GET and POST, `/nodes/{node}/disks/zfs/{name}` has GET and DELETE, and that is the whole
27
27
  * surface. So `updateForm` is omitted — which, per `resource.ts`, makes any `matches` = false a
28
28
  * REPLACE. A replace is a delete followed by a create, and on this family the delete is the
29
29
  * thing above. Read the ⛔ on `matches` before adding anything to it.
30
30
  *
31
31
  * ⛔ NOT ONE CREATE PARAMETER COMES BACK ON READ, SO `matches` COMPARES NOTHING. MEASURED against
32
- * the live cluster: `GET /nodes/n2/disks/zfs/rpool` answers exactly
32
+ * the live cluster: `GET /nodes/node-b/disks/zfs/rpool` answers exactly
33
33
  * `{action, children, errors, leaf, name, scan, state, status}` — no `ashift`, no `compression`,
34
34
  * no `raidlevel`, no `devices`, no `add_storage`. Every create parameter is write-only. There is
35
35
  * therefore no field a declaration and a live pool can both be asked about, and a `matches` that
36
36
  * invented one would plan a replace — i.e. a `zpool destroy` — over a difference it could never
37
37
  * verify in the first place.
38
38
  *
39
- * ⚠️ AND WHAT THE INDEX RETURNS IS LIVE TELEMETRY, NOT CONFIGURATION. `GET /nodes/n2/disks/zfs`
40
- * gives `alloc`, `free`, `frag`, `dedup`, `size`, `health`. MEASURED: two reads of n2 seconds
39
+ * ⚠️ AND WHAT THE INDEX RETURNS IS LIVE TELEMETRY, NOT CONFIGURATION. `GET /nodes/node-b/disks/zfs`
40
+ * gives `alloc`, `free`, `frag`, `dedup`, `size`, `health`. MEASURED: two reads of node-b seconds
41
41
  * apart returned `alloc` 14474944512 then 14472740864 — it moves on its own, with no declaration
42
42
  * anywhere near it. `scan` in the detail read moves the same way (it carries the last scrub), and
43
43
  * `status`/`action` appear and vanish as ZFS feature flags and faults come and go. None of them
@@ -133,7 +133,7 @@ export interface ZfsPoolAttributes {
133
133
  *
134
134
  * ⚠️ THIS IS WHAT ZFS RESOLVED, NOT WHAT WAS DECLARED, and the two normally differ: PVE rewrites
135
135
  * a `/dev/sdb` into a by-id link before creating, and ZFS reports partition paths (`…-part3`)
136
- * for a pool built on partitions — MEASURED, that is exactly what n2's `rpool` reports. Never
136
+ * for a pool built on partitions — MEASURED, that is exactly what node-b's `rpool` reports. Never
137
137
  * compare it with `props.devices`.
138
138
  */
139
139
  devices: string;
@@ -2,7 +2,7 @@ import * as Effect from 'effect/Effect';
2
2
  import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
3
3
  /** Where credentials come from. HomeFlare-specific mount names live in the stack, not here. */
4
4
  export type TalosTarget = {
5
- /** OpenBao KV mount holding Talos material, e.g. `talos-tb4`. */
5
+ /** OpenBao KV mount holding Talos material, e.g. `talos-c1`. */
6
6
  readonly mount: string;
7
7
  /** Logical cluster name — safe to persist and log. */
8
8
  readonly cluster: string;
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/talos/credentials.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAK,mBAAmB,MAAM,6CAA6C,CAAC;AAEnF,+FAA+F;AAC/F,MAAM,MAAM,WAAW,GAAG;IACxB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,WAAY,WAAW;;iGAsFhD,CAAC"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/talos/credentials.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAK,mBAAmB,MAAM,6CAA6C,CAAC;AAEnF,+FAA+F;AAC/F,MAAM,MAAM,WAAW,GAAG;IACxB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,WAAY,WAAW;;iGAsFhD,CAAC"}
@@ -3,7 +3,7 @@
3
3
  "sources": ["../src/talos/kubeconfig.ts", "../src/talos/credentials.ts", "../src/talos/talosctl.ts", "../src/talos/values.ts", "../src/talos/talos-bootstrap.ts", "../src/talos/talos-cluster-health.ts", "../src/talos/talos-machine-config.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * `Talos.Kubeconfig` — fetch admin kubeconfig so `Kubernetes.*` workloads can connect.\n *\n * ★ REASONED FROM talosctl kubeconfig (Talos v1.13 CLI reference): writes a kubeconfig file;\n * `--merge=false` keeps it isolated; `-f/--force` overwrites an existing file.\n *\n * ⛔ THE KUBECONFIG FILE CONTAINS A CLIENT CERTIFICATE — IT MUST NOT BE AN ATTRIBUTE. Alchemy\n * persists attributes unencrypted. This resource:\n * - writes the file to `runtimePath` at reconcile time (re-minted each deploy),\n * - persists ONLY public metadata + fingerprints,\n * - exposes a `connection` field referencing the runtime path for `Kubernetes.ClusterAdapter`.\n *\n * ⚠️ `runtimePath` IS PERSISTED AS A STRING, NOT AS FILE CONTENTS. The path is a contract between\n * this resource and the host filesystem — same pattern as pointing Kubernetes.KubeConfig at\n * `/opt/homeflare/...` rendered outside Postgres.\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport type { Connection } from 'alchemy/Kubernetes/Connection';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { mintTalosconfig } from './credentials.ts';\nimport type { TalosRequirements, WithTarget } from './resource.ts';\nimport { talosctl } from './talosctl.ts';\nimport { kubeconfigMetadata, sha256 } from './values.ts';\n\nexport interface KubeconfigProps extends WithTarget {\n /** Node that serves `talosctl kubeconfig`. */\n node: string;\n /** Context name inside the generated kubeconfig — not secret. */\n context: string;\n /**\n * Host path where talosctl writes the kubeconfig each reconcile.\n *\n * ⛔ Must be outside Alchemy state and rotated by redeploy, never copied into attributes.\n */\n runtimePath: string;\n after?: readonly unknown[];\n}\n\nexport interface KubeconfigAttributes {\n context: string;\n endpoint: string;\n certificateAuthorityFingerprint: string;\n clientCertificateFingerprint: string;\n /** Digest of endpoint+context+fingerprints — detects credential rotation without storing PEM. */\n credentialGeneration: string;\n /**\n * Serializable `Kubernetes.Connection` for downstream workloads.\n *\n * ★ auth.kind `kubeconfig` is the stock ClusterAdapter — provider-roadmap.md names this seam.\n * Only `path` and `context` appear here; the adapter reads credentials from disk at call time.\n */\n connection: Connection;\n}\n\nexport interface TalosKubeconfig extends Resource<\n 'Talos.Kubeconfig',\n KubeconfigProps,\n KubeconfigAttributes,\n never,\n TalosRequirements\n> {}\n\nexport const TalosKubeconfig = Resource<TalosKubeconfig>('Talos.Kubeconfig');\n\nconst generation = (meta: {\n endpoint: string;\n caFingerprint: string;\n clientFingerprint: string;\n context: string;\n}) => sha256(`${meta.context}\\n${meta.endpoint}\\n${meta.caFingerprint}\\n${meta.clientFingerprint}`);\n\nconst toConnection = (props: KubeconfigProps): Connection => ({\n auth: { context: props.context, kind: 'kubeconfig', path: props.runtimePath },\n});\n\nconst readFileMeta = (props: KubeconfigProps) =>\n Effect.gen(function* () {\n const text = yield* Effect.tryPromise({\n try: () => Bun.file(props.runtimePath).text(),\n catch: (cause) => new Error(String(cause)),\n }).pipe(Effect.orElseSucceed(() => undefined as string | undefined));\n if (text === undefined) return undefined;\n const meta = kubeconfigMetadata(text, props.context);\n if (meta === undefined) return undefined;\n return {\n certificateAuthorityFingerprint: meta.caFingerprint,\n clientCertificateFingerprint: meta.clientFingerprint,\n connection: toConnection(props),\n context: props.context,\n credentialGeneration: generation({ ...meta, context: props.context }),\n endpoint: meta.endpoint,\n };\n });\n\nconst read = (props: KubeconfigProps) =>\n readFileMeta(props).pipe(\n Effect.map((meta) =>\n meta === undefined\n ? {\n certificateAuthorityFingerprint: '',\n clientCertificateFingerprint: '',\n connection: toConnection(props),\n context: props.context,\n credentialGeneration: '',\n endpoint: '',\n }\n : meta,\n ),\n );\n\nconst diff = (news: Input<KubeconfigProps>, output: KubeconfigAttributes | undefined) =>\n Effect.gen(function* () {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* read(news);\n if (\n live.credentialGeneration !== '' &&\n output.credentialGeneration === live.credentialGeneration &&\n output.context === live.context &&\n output.endpoint === live.endpoint\n ) {\n return { action: 'noop' } as const;\n }\n return { action: 'update' } as const;\n });\n\nconst reconcile = (props: KubeconfigProps) =>\n Effect.gen(function* () {\n const credential = yield* mintTalosconfig(props.target);\n yield* talosctl(\n [\n 'kubeconfig',\n props.runtimePath,\n '--merge=false',\n '--force',\n '--force-context-name',\n props.context,\n ],\n { nodes: [props.node], talosconfigPath: credential.talosconfigPath },\n );\n const meta = yield* readFileMeta(props);\n if (meta === undefined) {\n return yield* Effect.die(\n new Error(\n `${props.runtimePath}: talosctl kubeconfig returned no error but the file is missing ` +\n 'or unparsable. Read back rather than trusting the exit code alone.',\n ),\n );\n }\n return meta;\n });\n\nconst handlers = {\n delete: () => Effect.void,\n diff: ({\n news,\n output,\n }: {\n news: Input<KubeconfigProps>;\n output: KubeconfigAttributes | undefined;\n }) => diff(news, output),\n list: () => Effect.succeed([]),\n read: ({ olds }: { olds: KubeconfigProps }) => read(olds),\n reconcile: ({ news }: { news: KubeconfigProps }) => reconcile(news),\n};\n\nexport const TalosKubeconfigProvider = () =>\n Provider.effect(TalosKubeconfig, Effect.succeed(TalosKubeconfig.Provider.of(handlers)));\n",
6
- "/**\n * The Talos client credential a reconcile runs with: minted from OpenBao, written to a temp file.\n *\n * ⛔ TALOSCONFIG AND secrets.yaml HOLD CLIENT CERTIFICATES AND CLUSTER CA PRIVATE KEYS. Alchemy\n * persists resource attributes WITHOUT encryption — StateEncoding.ts writes `Redacted` as\n * `{\"@redacted\": <plaintext>}` — and this estate's state store is the `alchemy` Postgres that\n * pg-backup.sh dumps nightly. Neither file may be a prop, an attribute, or a log line.\n *\n * ★ MODELED ON house/proxmox/src/credentials.ts: mint at call time through `ChildProcessSpawner`,\n * use the material to build a client, drop it when the scope ends. The difference is Talos wants a\n * FILE (`--talosconfig` / `TALOSCONFIG`), so mint writes a temp path and registers cleanup.\n *\n * ⚠️ REASONED FROM THE PUBLISHED TALOS WORKFLOW, NOT MEASURED ON THIS ESTATE. There is no Talos\n * cluster here yet; paths below follow the same `mount/data/…` shape other infra mounts use.\n */\nimport { closeSync, openSync, writeFileSync } from 'node:fs';\nimport * as Effect from 'effect/Effect';\nimport * as Stream from 'effect/Stream';\nimport * as ChildProcess from 'effect/unstable/process/ChildProcess';\nimport * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';\n\n/** Where credentials come from. HomeFlare-specific mount names live in the stack, not here. */\nexport type TalosTarget = {\n /** OpenBao KV mount holding Talos material, e.g. `talos-tb4`. */\n readonly mount: string;\n /** Logical cluster name — safe to persist and log. */\n readonly cluster: string;\n /**\n * Path under the mount for the talosconfig YAML.\n *\n * ⚠️ DEFAULT IS REASONED. The first real deploy will confirm or correct the path; a wrong path\n * fails closed with a bao error rather than writing an empty talosconfig.\n */\n readonly talosconfigKey?: string;\n};\n\nexport type TalosCredential = {\n /** Absolute path to a temp talosconfig. ⛔ NEVER PERSIST, NEVER LOG CONTENTS. */\n readonly talosconfigPath: string;\n};\n\nconst defaultKey = (target: TalosTarget) => target.talosconfigKey ?? 'data/talosconfig';\n\n/**\n * Mint one talosconfig file for `target`.\n *\n * ★ SHELLS OUT TO `bao` RATHER THAN SPEAKING THE HTTP API — same reasoning as proxmox credentials.\n * The estate's BAO_ADDR / BAO_NAMESPACE / approle contract already lives in that binary.\n */\nexport const mintTalosconfig = (target: TalosTarget) =>\n Effect.scoped(\n Effect.gen(function* () {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;\n const key = defaultKey(target);\n const result = yield* ChildProcess.make(\n 'bao',\n ['kv', 'get', '-format=json', `${target.mount}/${key}`],\n { detached: false, extendEnv: true, stderr: 'pipe', stdin: 'ignore', stdout: 'pipe' },\n ).pipe(\n spawner.spawn,\n Effect.flatMap((child) =>\n Effect.all(\n {\n exitCode: child.exitCode,\n stderr: child.stderr.pipe(\n Stream.decodeText,\n Stream.mkString,\n Effect.map((text) => text.trim()),\n ),\n stdout: child.stdout.pipe(\n Stream.decodeText,\n Stream.mkString,\n Effect.map((text) => text.trim()),\n ),\n },\n { concurrency: 'unbounded' },\n ),\n ),\n Effect.scoped,\n );\n\n if (result.exitCode !== 0) {\n // ⛔ stderr ONLY — stdout may hold talosconfig YAML with client credentials.\n return yield* Effect.fail(\n new Error(\n `bao kv get ${target.mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`,\n ),\n );\n }\n\n const parsed = JSON.parse(result.stdout) as { data?: { data?: Record<string, unknown> } };\n const data = parsed.data?.data;\n const raw = data?.['talosconfig'] ?? data?.['config'];\n if (typeof raw !== 'string' || raw.trim() === '') {\n return yield* Effect.fail(\n new Error(\n `${target.mount}/${key} returned no talosconfig field. Expected a KV key named 'talosconfig' or 'config' — never store the value in Alchemy props.`,\n ),\n );\n }\n\n const talosconfigPath = `${Bun.env['TMPDIR'] ?? '/tmp'}/hf-talos-${target.cluster}-${Bun.randomUUIDv7()}.yaml`;\n /**\n * ⛔ 0600, CREATED EXCLUSIVELY, AND `Bun.write` CANNOT DO EITHER. This file holds the\n * cluster's admin client certificate — everything talosctl needs to reconfigure or wipe a\n * node. `Bun.write` takes no mode, so it lands at the process umask, which on this estate\n * is 0644: world-readable, in a world-readable directory, for as long as the resource\n * runs. The name is unguessable, and \"unguessable\" is not a permission.\n * ⚠️ `wx` IS THE OTHER HALF. Opening with O_EXCL means this cannot be made to write through\n * a path an attacker pre-created — the classic /tmp symlink race — and with a UUIDv7 name\n * a collision is a genuine error rather than something to paper over.\n * ⚠️ THE FINALIZER BELOW REMOVES IT ON EVERY EXIT PATH, but a hard kill (SIGKILL, power\n * loss) leaves it behind. 0600 is what makes that survivable rather than a disclosure.\n */\n yield* Effect.try({\n try: () => {\n const fd = openSync(talosconfigPath, 'wx', 0o600);\n try {\n writeFileSync(fd, raw);\n } finally {\n closeSync(fd);\n }\n },\n catch: (cause) => new Error(`writing temp talosconfig: ${String(cause)}`),\n });\n\n yield* Effect.addFinalizer(() =>\n Effect.tryPromise({\n try: () => Bun.file(talosconfigPath).delete(),\n catch: () => undefined,\n }).pipe(Effect.orElseSucceed(() => undefined)),\n );\n\n return { talosconfigPath } satisfies TalosCredential;\n }),\n );\n",
6
+ "/**\n * The Talos client credential a reconcile runs with: minted from OpenBao, written to a temp file.\n *\n * ⛔ TALOSCONFIG AND secrets.yaml HOLD CLIENT CERTIFICATES AND CLUSTER CA PRIVATE KEYS. Alchemy\n * persists resource attributes WITHOUT encryption — StateEncoding.ts writes `Redacted` as\n * `{\"@redacted\": <plaintext>}` — and this estate's state store is the `alchemy` Postgres that\n * a nightly job dumps. Neither file may be a prop, an attribute, or a log line.\n *\n * ★ MODELED ON <estate>/proxmox/src/credentials.ts: mint at call time through `ChildProcessSpawner`,\n * use the material to build a client, drop it when the scope ends. The difference is Talos wants a\n * FILE (`--talosconfig` / `TALOSCONFIG`), so mint writes a temp path and registers cleanup.\n *\n * ⚠️ REASONED FROM THE PUBLISHED TALOS WORKFLOW, NOT MEASURED ON THIS ESTATE. There is no Talos\n * cluster here yet; paths below follow the same `mount/data/…` shape other infra mounts use.\n */\nimport { closeSync, openSync, writeFileSync } from 'node:fs';\nimport * as Effect from 'effect/Effect';\nimport * as Stream from 'effect/Stream';\nimport * as ChildProcess from 'effect/unstable/process/ChildProcess';\nimport * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';\n\n/** Where credentials come from. HomeFlare-specific mount names live in the stack, not here. */\nexport type TalosTarget = {\n /** OpenBao KV mount holding Talos material, e.g. `talos-c1`. */\n readonly mount: string;\n /** Logical cluster name — safe to persist and log. */\n readonly cluster: string;\n /**\n * Path under the mount for the talosconfig YAML.\n *\n * ⚠️ DEFAULT IS REASONED. The first real deploy will confirm or correct the path; a wrong path\n * fails closed with a bao error rather than writing an empty talosconfig.\n */\n readonly talosconfigKey?: string;\n};\n\nexport type TalosCredential = {\n /** Absolute path to a temp talosconfig. ⛔ NEVER PERSIST, NEVER LOG CONTENTS. */\n readonly talosconfigPath: string;\n};\n\nconst defaultKey = (target: TalosTarget) => target.talosconfigKey ?? 'data/talosconfig';\n\n/**\n * Mint one talosconfig file for `target`.\n *\n * ★ SHELLS OUT TO `bao` RATHER THAN SPEAKING THE HTTP API — same reasoning as proxmox credentials.\n * The estate's BAO_ADDR / BAO_NAMESPACE / approle contract already lives in that binary.\n */\nexport const mintTalosconfig = (target: TalosTarget) =>\n Effect.scoped(\n Effect.gen(function* () {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;\n const key = defaultKey(target);\n const result = yield* ChildProcess.make(\n 'bao',\n ['kv', 'get', '-format=json', `${target.mount}/${key}`],\n { detached: false, extendEnv: true, stderr: 'pipe', stdin: 'ignore', stdout: 'pipe' },\n ).pipe(\n spawner.spawn,\n Effect.flatMap((child) =>\n Effect.all(\n {\n exitCode: child.exitCode,\n stderr: child.stderr.pipe(\n Stream.decodeText,\n Stream.mkString,\n Effect.map((text) => text.trim()),\n ),\n stdout: child.stdout.pipe(\n Stream.decodeText,\n Stream.mkString,\n Effect.map((text) => text.trim()),\n ),\n },\n { concurrency: 'unbounded' },\n ),\n ),\n Effect.scoped,\n );\n\n if (result.exitCode !== 0) {\n // ⛔ stderr ONLY — stdout may hold talosconfig YAML with client credentials.\n return yield* Effect.fail(\n new Error(\n `bao kv get ${target.mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`,\n ),\n );\n }\n\n const parsed = JSON.parse(result.stdout) as { data?: { data?: Record<string, unknown> } };\n const data = parsed.data?.data;\n const raw = data?.['talosconfig'] ?? data?.['config'];\n if (typeof raw !== 'string' || raw.trim() === '') {\n return yield* Effect.fail(\n new Error(\n `${target.mount}/${key} returned no talosconfig field. Expected a KV key named 'talosconfig' or 'config' — never store the value in Alchemy props.`,\n ),\n );\n }\n\n const talosconfigPath = `${Bun.env['TMPDIR'] ?? '/tmp'}/hf-talos-${target.cluster}-${Bun.randomUUIDv7()}.yaml`;\n /**\n * ⛔ 0600, CREATED EXCLUSIVELY, AND `Bun.write` CANNOT DO EITHER. This file holds the\n * cluster's admin client certificate — everything talosctl needs to reconfigure or wipe a\n * node. `Bun.write` takes no mode, so it lands at the process umask, which on this estate\n * is 0644: world-readable, in a world-readable directory, for as long as the resource\n * runs. The name is unguessable, and \"unguessable\" is not a permission.\n * ⚠️ `wx` IS THE OTHER HALF. Opening with O_EXCL means this cannot be made to write through\n * a path an attacker pre-created — the classic /tmp symlink race — and with a UUIDv7 name\n * a collision is a genuine error rather than something to paper over.\n * ⚠️ THE FINALIZER BELOW REMOVES IT ON EVERY EXIT PATH, but a hard kill (SIGKILL, power\n * loss) leaves it behind. 0600 is what makes that survivable rather than a disclosure.\n */\n yield* Effect.try({\n try: () => {\n const fd = openSync(talosconfigPath, 'wx', 0o600);\n try {\n writeFileSync(fd, raw);\n } finally {\n closeSync(fd);\n }\n },\n catch: (cause) => new Error(`writing temp talosconfig: ${String(cause)}`),\n });\n\n yield* Effect.addFinalizer(() =>\n Effect.tryPromise({\n try: () => Bun.file(talosconfigPath).delete(),\n catch: () => undefined,\n }).pipe(Effect.orElseSucceed(() => undefined)),\n );\n\n return { talosconfigPath } satisfies TalosCredential;\n }),\n );\n",
7
7
  "/**\n * One `talosctl` invocation — stdout/stderr captured, secrets never echoed.\n *\n * ★ SHELL OUT LIKE ALCHEMY'S DOCKER PROVIDER (ChildProcessSpawner), NOT A SECOND SDK. Talos speaks\n * gRPC; the published, stable surface for operators is `talosctl`. No Talos npm package is added.\n *\n * ⚠️ REASONED FROM docs.siderolabs.com/talos/v1.13/reference/cli — nothing here was measured on a\n * live cluster in this session.\n *\n * ⛔ DO NOT LOG STDOUT ON FAILURE. `talosctl kubeconfig` and error paths have been observed in the\n * wild to include credential material; stderr is the safe diagnostic channel.\n */\nimport * as Effect from 'effect/Effect';\nimport * as Stream from 'effect/Stream';\nimport * as ChildProcess from 'effect/unstable/process/ChildProcess';\nimport * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';\n\nexport class TalosError extends Error {\n constructor(\n readonly command: string,\n readonly exitCode: number,\n detail: string,\n ) {\n super(`talosctl ${command} -> ${String(exitCode)}: ${detail}`);\n this.name = 'TalosError';\n }\n}\n\nexport type TalosRunOptions = {\n readonly talosconfigPath: string;\n readonly nodes?: readonly string[];\n readonly endpoints?: readonly string[];\n};\n\n/** Run `talosctl <args…>`. Returns trimmed stdout, or '' when empty. */\nexport const talosctl = (args: readonly string[], options: TalosRunOptions) =>\n Effect.gen(function* () {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;\n const argv = [\n ...args,\n '--talosconfig',\n options.talosconfigPath,\n ...(options.nodes === undefined ? [] : ['--nodes', options.nodes.join(',')]),\n ...(options.endpoints === undefined ? [] : ['--endpoints', options.endpoints.join(',')]),\n ];\n const result = yield* ChildProcess.make('talosctl', argv, {\n detached: false,\n extendEnv: true,\n stderr: 'pipe',\n stdin: 'ignore',\n stdout: 'pipe',\n }).pipe(\n spawner.spawn,\n Effect.flatMap((child) =>\n Effect.all(\n {\n exitCode: child.exitCode,\n stderr: child.stderr.pipe(\n Stream.decodeText,\n Stream.mkString,\n Effect.map((text) => text.trim()),\n ),\n stdout: child.stdout.pipe(\n Stream.decodeText,\n Stream.mkString,\n Effect.map((text) => text.trim()),\n ),\n },\n { concurrency: 'unbounded' },\n ),\n ),\n Effect.scoped,\n );\n\n const command = args.join(' ');\n if (result.exitCode !== 0) {\n return yield* Effect.fail(\n new TalosError(command, result.exitCode, result.stderr.slice(0, 300)),\n );\n }\n return result.stdout;\n });\n\n/** Like `talosctl`, but exit code 1 with \"already\" in stderr is treated as success. */\nexport const talosctlOrAlready = (args: readonly string[], options: TalosRunOptions) =>\n talosctl(args, options).pipe(\n Effect.catchIf(\n (cause): cause is TalosError =>\n cause instanceof TalosError &&\n cause.exitCode !== 0 &&\n /already|exists|bootstrapped/i.test(cause.message),\n () => Effect.succeed(''),\n ),\n );\n",
8
8
  "/**\n * Coercions and digests for Talos resources — every function here exists because a trap does.\n *\n * ⚠️ NOTHING IN THIS FILE TOUCHES A LIVE CLUSTER. Digests are computed from repo files or from\n * kubeconfig YAML already on disk at reconcile time; tests use fixtures only.\n */\nimport { createHash } from 'node:crypto';\n\n/** Trim trailing whitespace only — leading space in YAML is meaningful. */\nexport const canonicalText = (text: string) => text.trimEnd();\n\n/** SHA-256 hex digest of canonical text. Safe to persist — it is not reversible to the input. */\nexport const sha256 = (text: string) =>\n createHash('sha256').update(canonicalText(text)).digest('hex');\n\n/** Digest of a machine config file read from the repo. ⛔ Never persist the file body itself. */\nexport const configDigest = (text: string) => sha256(text);\n\ntype KubeconfigDoc = {\n clusters?: {\n cluster?: { server?: string; 'certificate-authority-data'?: string };\n name?: string;\n }[];\n contexts?: { context?: { cluster?: string; user?: string }; name?: string }[];\n users?: {\n name?: string;\n user?: { 'client-certificate-data'?: string; 'client-key-data'?: string };\n }[];\n 'current-context'?: string;\n};\n\n/**\n * Public metadata extracted from a kubeconfig file on disk.\n *\n * ⛔ RETURNS FINGERPRINTS ONLY. The admin kubeconfig contains a client certificate; Alchemy state\n * may hold the API server URL and CA/client cert digests, never the PEM bytes.\n */\nexport const kubeconfigMetadata = (\n yamlText: string,\n contextName: string,\n): { endpoint: string; caFingerprint: string; clientFingerprint: string } | undefined => {\n let doc: KubeconfigDoc;\n try {\n doc = Bun.YAML.parse(yamlText) as KubeconfigDoc;\n } catch {\n return undefined;\n }\n\n const ctx = doc.contexts?.find((row) => row.name === contextName)?.context;\n const clusterName = ctx?.cluster;\n const userName = ctx?.user;\n if (clusterName === undefined || userName === undefined) return undefined;\n\n const cluster = doc.clusters?.find((row) => row.name === clusterName)?.cluster;\n const user = doc.users?.find((row) => row.name === userName)?.user;\n const endpoint = cluster?.server;\n const ca = cluster?.['certificate-authority-data'];\n const cert = user?.['client-certificate-data'] ?? user?.['client-key-data'];\n if (endpoint === undefined || ca === undefined || cert === undefined) return undefined;\n\n return {\n caFingerprint: sha256(ca),\n clientFingerprint: sha256(cert),\n endpoint,\n };\n};\n\n/** Resolve a config file path relative to the declaring stack file. */\nexport const resolveConfigPath = (stackDir: string, configFile: string) =>\n configFile.startsWith('/') ? configFile : `${stackDir}/${configFile}`;\n",
9
9
  "/**\n * `Talos.Bootstrap` — initialize etcd once on ONE control-plane node.\n *\n * ★ REASONED FROM talosctl bootstrap (Talos v1.13 CLI reference): one node aborts the etcd join\n * loop and forms the initial cluster; other control-plane nodes join after Kubernetes starts on\n * the bootstrap node. This command must run exactly once per cluster.\n *\n * ⛔ NOT A FACTORY RESOURCE — bootstrap cannot be deleted or updated in place; `delete` is a no-op.\n */\nimport { Resource } from 'alchemy';\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { mintTalosconfig } from './credentials.ts';\nimport type { TalosRequirements, WithTarget } from './resource.ts';\nimport { talosctl, talosctlOrAlready } from './talosctl.ts';\n\nexport interface BootstrapProps extends WithTarget {\n /** The ONE control-plane node that receives `talosctl bootstrap`. */\n node: string;\n /** Machine configs for this node (and peers) must be applied first. */\n after?: readonly unknown[];\n}\n\nexport interface BootstrapAttributes {\n node: string;\n bootstrapped: boolean;\n}\n\nexport interface TalosBootstrap extends Resource<\n 'Talos.Bootstrap',\n BootstrapProps,\n BootstrapAttributes,\n never,\n TalosRequirements\n> {}\n\nexport const TalosBootstrap = Resource<TalosBootstrap>('Talos.Bootstrap', {\n /** etcd data is irreplaceable — opt into destroy explicitly if Talos ever adds an undo. */\n defaultRemovalPolicy: 'retain',\n});\n\n/**\n * REASONED: `talosctl get etcdmembers -o json` should list members after bootstrap.\n * ⚠️ NOT MEASURED — first live deploy confirms the resource type name.\n */\nconst isBootstrapped = (props: BootstrapProps, talosconfigPath: string) =>\n talosctl(['get', 'etcdmembers', '-o', 'json'], {\n nodes: [props.node],\n talosconfigPath,\n }).pipe(\n Effect.map((text) => text.includes('\"id\"') || text.includes('\"member\"')),\n Effect.orElseSucceed(() => false),\n );\n\nconst read = (props: BootstrapProps) =>\n Effect.gen(function* () {\n const credential = yield* mintTalosconfig(props.target);\n const bootstrapped = yield* isBootstrapped(props, credential.talosconfigPath);\n return { bootstrapped, node: props.node };\n });\n\nconst diff = (news: Input<BootstrapProps>, output: BootstrapAttributes | undefined) =>\n Effect.gen(function* () {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* read(news);\n if (live.bootstrapped) return { action: 'noop' } as const;\n return { action: 'update' } as const;\n });\n\nconst reconcile = (props: BootstrapProps) =>\n Effect.gen(function* () {\n const credential = yield* mintTalosconfig(props.target);\n const before = yield* isBootstrapped(props, credential.talosconfigPath);\n if (!before) {\n yield* talosctlOrAlready(['bootstrap'], {\n nodes: [props.node],\n talosconfigPath: credential.talosconfigPath,\n });\n }\n const after = yield* isBootstrapped(props, credential.talosconfigPath);\n if (!after) {\n return yield* Effect.die(\n new Error(\n `${props.node}: bootstrap returned no error but etcd members are still absent. ` +\n 'Read back rather than trusting the exit code alone.',\n ),\n );\n }\n return { bootstrapped: true, node: props.node };\n });\n\nconst handlers = {\n delete: () => Effect.void,\n diff: ({\n news,\n output,\n }: {\n news: Input<BootstrapProps>;\n output: BootstrapAttributes | undefined;\n }) => diff(news, output),\n list: () => Effect.succeed([]),\n read: ({ olds }: { olds: BootstrapProps }) => read(olds),\n reconcile: ({ news }: { news: BootstrapProps }) => reconcile(news),\n};\n\nexport const TalosBootstrapProvider = () =>\n Provider.effect(TalosBootstrap, Effect.succeed(TalosBootstrap.Provider.of(handlers)));\n",
@@ -53,6 +53,11 @@ export interface FakeEngine {
53
53
  readonly deploy: (body: Body) => Promise<Record<string, string>>;
54
54
  /** Verify `body` against what the earlier deploys recorded. */
55
55
  readonly verify: (body: Body, options?: VerifyOptions) => Promise<AdoptReport>;
56
+ /**
57
+ * Every state row the store holds, as JSON — what a Postgres store would persist. ★ For tests
58
+ * that must prove a value NEVER reaches state (proxmox/pbs-notification-target-state.test.ts).
59
+ */
60
+ readonly stored: () => string;
56
61
  }
57
62
  /**
58
63
  * One stack over one state store and any providers, shared by every call — a family's own test
@@ -1 +1 @@
1
- {"version":3,"file":"fake-engine.d.ts","sourceRoot":"","sources":["../../src/verify/fake-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAKnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAiB,MAAM,aAAa,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D,4EAA4E;AAC5E,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,QAAS,SAAQ,OAAO,CAAC,QAAQ,CAAC,eAAe,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AACnG,eAAO,MAAM,QAAQ,iCAA8C,CAAC;AAEpE,gGAAgG;AAChG,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,eAAO,MAAM,OAAO,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAQ,KAI1D,CAAC;AAEH,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,UAAW,KAAK,KAAG,MAAM,EAC2C,CAAC;AAyB1F,4FAA4F;AAC5F,eAAO,MAAM,cAAc,UAAW,KAAK,8GAmCxC,CAAC;AAEJ,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAK5D,MAAM,WAAW,UAAU;IACzB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CAChF;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,IAAI,EAAE,CAAC,EAAE,GAAG,SAAS,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,KAAG,UAuC3E,CAAC;AAEF,oCAAoC;AACpC,eAAO,MAAM,UAAU,UAAW,KAAK,KAAG,UAA+C,CAAC"}
1
+ {"version":3,"file":"fake-engine.d.ts","sourceRoot":"","sources":["../../src/verify/fake-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAKnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAiB,MAAM,aAAa,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D,4EAA4E;AAC5E,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,QAAS,SAAQ,OAAO,CAAC,QAAQ,CAAC,eAAe,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AACnG,eAAO,MAAM,QAAQ,iCAA8C,CAAC;AAEpE,gGAAgG;AAChG,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,eAAO,MAAM,OAAO,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAQ,KAI1D,CAAC;AAEH,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,UAAW,KAAK,KAAG,MAAM,EAC2C,CAAC;AAyB1F,4FAA4F;AAC5F,eAAO,MAAM,cAAc,UAAW,KAAK,8GAmCxC,CAAC;AAEJ,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAK5D,MAAM,WAAW,UAAU;IACzB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/E;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,IAAI,EAAE,CAAC,EAAE,GAAG,SAAS,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,KAAG,UAyC3E,CAAC;AAEF,oCAAoC;AACpC,eAAO,MAAM,UAAU,UAAW,KAAK,KAAG,UAA+C,CAAC"}
@@ -9,8 +9,8 @@ bunx --bun hf-adopt-verify --config alchemy.run.ts --stage live # rows
9
9
  bunx --bun hf-adopt-verify --config alchemy.pbs.ts --stage live --all # every row + deletions
10
10
  ```
11
11
 
12
- Wrap it in the same credential lane the deploy uses (`bun ./scripts/with-mini-bao.ts --admin …`
13
- in the Proxmox stacks): a lane that cannot read an object sees it as absent, and the verifier
12
+ Wrap it in the same credential lane the deploy uses (the wrapper that mints
13
+ its OpenBao token): a lane that cannot read an object sees it as absent, and the verifier
14
14
  then reports `create` for something that exists.
15
15
 
16
16
  ## Why a plan line is not enough
@@ -24,8 +24,8 @@ Measured in alchemy `2.0.0-beta.79`:
24
24
  - So `alchemy plan` prints `(+) adopted` for a matching object **and** for a drifting one, and
25
25
  the deploy reconciles both. Whether that writes is up to each family's `reconcile`.
26
26
 
27
- Measured on TB4, `pvesh get /cluster/tasks`, 2026-09-21: six `cephsetpool` tasks by
28
- `hf-provision@pve`, one per pool, on 2026-09-13 and again on 2026-09-20 — the deploys that
27
+ Measured on C1, `pvesh get /cluster/tasks`, 2026-09-21: six `cephsetpool` tasks by
28
+ the provision user, one per pool, on 2026-09-13 and again on 2026-09-20 — the deploys that
29
29
  adopted the three Ceph pools. `Proxmox.CephPool` sent `setpool` whenever the pool existed,
30
30
  whatever its diff said. It was the one PVE/PBS family that wrote on a no-op adoption, and it is
31
31
  fixed. [adopted-deploys.md](./adopted-deploys.md) shows what every family does.
@@ -37,10 +37,26 @@ fixed. [adopted-deploys.md](./adopted-deploys.md) shows what every family does.
37
37
  | `alchemy plan` (CLI) | the forced action only; `--detailed` is declared YAML, no JSON |
38
38
  | `Plan.describePlan` | serializable rows, again only the forced action |
39
39
  | `forceUpdateAfterAdoption` | a local variable; no log, no warning, no field on the node |
40
- | `alchemy drift` | rows **with** state only, and its non-dry run reconciles |
40
+ | `alchemy drift` | rows **with** state only, against the last deploy; exits `0` |
41
41
  | `deploy --adopt --dry-run` | the adoption plan (`plan` has no `--adopt`), same forced action |
42
42
  | **the provider service** | the raw `read` / `diff` answer, per FQN — used |
43
43
 
44
+ `alchemy drift` has no dry run to gate on (beta.79, `Cli/commands/drift.ts`, `Drift.ts`). Its
45
+ flags are `--repair`, `--config`, `--env-file`, `--stage` and `--profile`: no `--dry-run`, and no
46
+ `--yes`, although [its page](https://alchemy.run/cli/drift) lists one (measured 2026-09-21:
47
+ `Unrecognized flag: --yes in command alchemy drift`, exit `1`). Without `--repair`, a terminal
48
+ run asks Repair or Cancel (default Cancel). A non-interactive run (no TTY, `CI`, `--no-input`,
49
+ `ALCHEMY_PLAIN=1` or `ALCHEMY_NO_TUI=1`, or an agent such as Claude Code) prints the repair plan
50
+ and exits `0` even when something drifted. It skips a row whose provider has no `read`, or whose
51
+ status is not `created` / `updated`. `--repair` restores the props saved at the last deploy, not
52
+ what the code declares now, and recreates a missing object. `--all` (below) asks each
53
+ provider's `diff` about the declaration, as a deploy does, and reads every row again.
54
+
55
+ ⛔ **Repair is a write outside the deploy gate.** Choosing Repair, or passing `--repair` (which
56
+ never prompts), calls each drifted row's `reconcile` directly (`Drift.ts`): no `hf-adopt-verify`,
57
+ no approved plan of the declaration. A HomeFlare gate never runs it. Deploy the declaration
58
+ through the gate instead.
59
+
44
60
  The engine finds every provider in the Effect context by resource type. The verifier opens the
45
61
  stack the way `alchemy plan` does (`Alchemist.open`). It swaps each provider for a watched copy
46
62
  (`src/verify/spy.ts`) and runs `Plan.make` itself, so output resolution, state lookup, renames,