@homeflare/alchemy 0.11.0 → 0.12.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 (240) 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/api-token-form.d.ts +2 -2
  39. package/dist/proxmox/api-token.d.ts +11 -11
  40. package/dist/proxmox/ceph-daemon-form.d.ts +3 -3
  41. package/dist/proxmox/ceph-daemon.d.ts +19 -19
  42. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  43. package/dist/proxmox/ceph-flag.d.ts +5 -5
  44. package/dist/proxmox/ceph-fs-wire.d.ts +5 -5
  45. package/dist/proxmox/ceph-fs.d.ts +16 -16
  46. package/dist/proxmox/ceph-osd-tree.d.ts +16 -16
  47. package/dist/proxmox/ceph-osd.d.ts +5 -5
  48. package/dist/proxmox/ceph-pool-form.d.ts +13 -13
  49. package/dist/proxmox/ceph-pool-form.d.ts.map +1 -1
  50. package/dist/proxmox/ceph-pool.d.ts +3 -3
  51. package/dist/proxmox/client.d.ts +2 -2
  52. package/dist/proxmox/credentials.d.ts +8 -8
  53. package/dist/proxmox/credentials.d.ts.map +1 -1
  54. package/dist/proxmox/firewall-alias-form.d.ts +1 -1
  55. package/dist/proxmox/firewall-alias.d.ts +1 -1
  56. package/dist/proxmox/generated/pbs.d.ts +1 -1
  57. package/dist/proxmox/generated/pve.d.ts +1 -1
  58. package/dist/proxmox/group.d.ts +8 -8
  59. package/dist/proxmox/ha-resource.d.ts +3 -2
  60. package/dist/proxmox/ha-resource.d.ts.map +1 -1
  61. package/dist/proxmox/ha-rule-form.d.ts +5 -5
  62. package/dist/proxmox/ha-rule.d.ts +3 -3
  63. package/dist/proxmox/index.d.ts +6 -0
  64. package/dist/proxmox/index.d.ts.map +1 -1
  65. package/dist/proxmox/index.js +356 -142
  66. package/dist/proxmox/index.js.map +59 -56
  67. package/dist/proxmox/lease-cache.d.ts +4 -4
  68. package/dist/proxmox/lxc-harness.d.ts +1 -1
  69. package/dist/proxmox/lxc-props.d.ts +2 -2
  70. package/dist/proxmox/lxc.d.ts +3 -3
  71. package/dist/proxmox/members.d.ts +3 -3
  72. package/dist/proxmox/network-apply-read.d.ts +5 -5
  73. package/dist/proxmox/network-apply.d.ts +9 -9
  74. package/dist/proxmox/node-network-form.d.ts +6 -6
  75. package/dist/proxmox/node-network.d.ts +17 -17
  76. package/dist/proxmox/node-network.d.ts.map +1 -1
  77. package/dist/proxmox/notification-target.d.ts +3 -2
  78. package/dist/proxmox/notification-target.d.ts.map +1 -1
  79. package/dist/proxmox/pbs-datastore-guard.d.ts +1 -1
  80. package/dist/proxmox/pbs-datastore.d.ts +10 -10
  81. package/dist/proxmox/pbs-prune-job.d.ts +2 -2
  82. package/dist/proxmox/pbs-sync-job.d.ts +4 -4
  83. package/dist/proxmox/pbs-verify-job.d.ts +1 -1
  84. package/dist/proxmox/pool.d.ts +4 -4
  85. package/dist/proxmox/provision-baseline.d.ts +74 -0
  86. package/dist/proxmox/provision-baseline.d.ts.map +1 -0
  87. package/dist/proxmox/provision-bootstrap.d.ts +33 -0
  88. package/dist/proxmox/provision-bootstrap.d.ts.map +1 -0
  89. package/dist/proxmox/provision-cli-fake.d.ts +38 -0
  90. package/dist/proxmox/provision-cli-fake.d.ts.map +1 -0
  91. package/dist/proxmox/provision-declare.d.ts +25 -0
  92. package/dist/proxmox/provision-declare.d.ts.map +1 -0
  93. package/dist/proxmox/replication-job.d.ts +9 -8
  94. package/dist/proxmox/replication-job.d.ts.map +1 -1
  95. package/dist/proxmox/resource.d.ts +2 -2
  96. package/dist/proxmox/resource.d.ts.map +1 -1
  97. package/dist/proxmox/sdn-apply-read.d.ts +7 -7
  98. package/dist/proxmox/sdn-apply.d.ts +1 -1
  99. package/dist/proxmox/sdn-subnet-form.d.ts +1 -1
  100. package/dist/proxmox/sdn-subnet.d.ts +5 -5
  101. package/dist/proxmox/sdn-vnet.d.ts +5 -6
  102. package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
  103. package/dist/proxmox/storage.d.ts +4 -3
  104. package/dist/proxmox/storage.d.ts.map +1 -1
  105. package/dist/proxmox/user.d.ts +3 -2
  106. package/dist/proxmox/user.d.ts.map +1 -1
  107. package/dist/proxmox/values.d.ts +1 -1
  108. package/dist/proxmox/zfs-pool-write.d.ts +1 -1
  109. package/dist/proxmox/zfs-pool.d.ts +5 -5
  110. package/dist/talos/credentials.d.ts +1 -1
  111. package/dist/talos/credentials.d.ts.map +1 -1
  112. package/dist/talos/index.js.map +1 -1
  113. package/docs/adopt-verify.md +21 -5
  114. package/docs/adopted-deploys.md +1 -1
  115. package/docs/ownership.md +26 -1
  116. package/docs/provision-baseline.md +106 -0
  117. package/docs/proxmox-lxc-adopt.md +2 -2
  118. package/docs/proxmox-lxc.md +5 -4
  119. package/docs/proxmox.md +6 -0
  120. package/package.json +1 -1
  121. package/src/caddy/admin-guard.ts +1 -1
  122. package/src/caddy/local-admin.ts +1 -1
  123. package/src/cloudflare/client.ts +1 -1
  124. package/src/cloudflare/lock-rules.ts +1 -1
  125. package/src/forgejo/branch-protection-form.ts +1 -1
  126. package/src/forgejo/branch-protection.ts +1 -1
  127. package/src/forgejo/client.ts +1 -1
  128. package/src/forgejo/org-actions-secrets.ts +1 -1
  129. package/src/forgejo/org-label.ts +1 -1
  130. package/src/forgejo/org-team.ts +1 -1
  131. package/src/forgejo/repo-webhook.ts +1 -1
  132. package/src/forgejo/repository.ts +2 -2
  133. package/src/forgejo/resource.ts +2 -2
  134. package/src/forgejo/team-member.ts +1 -1
  135. package/src/openbao/README.md +4 -4
  136. package/src/openbao/auth-role.ts +1 -1
  137. package/src/openbao/bao-http.test.ts +2 -2
  138. package/src/openbao/bao-http.ts +6 -6
  139. package/src/openbao/bao-status.test.ts +1 -1
  140. package/src/openbao/bao-status.ts +1 -1
  141. package/src/openbao/cloudflare-group-scope.test.ts +1 -1
  142. package/src/openbao/cloudflare-permission-groups.test.ts +1 -1
  143. package/src/openbao/cloudflare-permission-groups.ts +3 -3
  144. package/src/openbao/cloudflare-policy.ts +1 -1
  145. package/src/openbao/cloudflare-role-form.ts +2 -2
  146. package/src/openbao/cloudflare-roles-expand.ts +1 -1
  147. package/src/openbao/digest.ts +1 -1
  148. package/src/openbao/forgejo-bootstrap-parse.test.ts +3 -3
  149. package/src/openbao/forgejo-bootstrap-parse.ts +2 -2
  150. package/src/openbao/forgejo-bootstrap.ts +4 -4
  151. package/src/openbao/kubernetes-role-form.test.ts +5 -5
  152. package/src/openbao/mount.ts +1 -1
  153. package/src/openbao/pki-role.ts +1 -1
  154. package/src/openbao/policy-wire.test.ts +3 -3
  155. package/src/openbao/policy.ts +2 -2
  156. package/src/openbao/proxmox-role-form.ts +5 -5
  157. package/src/openbao/proxmox-role.ts +11 -11
  158. package/src/openbao/ssh-role.ts +3 -3
  159. package/src/proxmox/acl.ts +4 -4
  160. package/src/proxmox/adopt-noop.test.ts +6 -6
  161. package/src/proxmox/api-token-form.ts +4 -4
  162. package/src/proxmox/api-token.ts +13 -13
  163. package/src/proxmox/ceph-daemon-form.ts +3 -3
  164. package/src/proxmox/ceph-daemon.ts +20 -20
  165. package/src/proxmox/ceph-flag.ts +7 -7
  166. package/src/proxmox/ceph-fs-wire.ts +7 -7
  167. package/src/proxmox/ceph-fs.ts +18 -18
  168. package/src/proxmox/ceph-osd-tree.ts +16 -16
  169. package/src/proxmox/ceph-osd.ts +6 -6
  170. package/src/proxmox/ceph-pool-adopt.test.ts +14 -14
  171. package/src/proxmox/ceph-pool-form.ts +13 -13
  172. package/src/proxmox/ceph-pool.ts +6 -6
  173. package/src/proxmox/client.test.ts +3 -3
  174. package/src/proxmox/client.ts +3 -3
  175. package/src/proxmox/credentials.test.ts +6 -8
  176. package/src/proxmox/credentials.ts +8 -8
  177. package/src/proxmox/firewall-alias-form.ts +1 -1
  178. package/src/proxmox/firewall-alias.ts +1 -1
  179. package/src/proxmox/generated/pbs.ts +1 -1
  180. package/src/proxmox/generated/pve.ts +1 -1
  181. package/src/proxmox/group.ts +10 -10
  182. package/src/proxmox/ha-resource.ts +3 -2
  183. package/src/proxmox/ha-rule-form.ts +5 -5
  184. package/src/proxmox/ha-rule.ts +3 -3
  185. package/src/proxmox/index.test.ts +7 -0
  186. package/src/proxmox/index.ts +14 -0
  187. package/src/proxmox/lease-cache.test.ts +19 -19
  188. package/src/proxmox/lease-cache.ts +4 -4
  189. package/src/proxmox/lxc-adopt.test.ts +3 -3
  190. package/src/proxmox/lxc-create-form.ts +1 -1
  191. package/src/proxmox/lxc-guards.test.ts +4 -4
  192. package/src/proxmox/lxc-harness.ts +3 -3
  193. package/src/proxmox/lxc-judge.test.ts +5 -5
  194. package/src/proxmox/lxc-lifecycle.ts +1 -1
  195. package/src/proxmox/lxc-props.ts +2 -2
  196. package/src/proxmox/lxc-read.test.ts +3 -3
  197. package/src/proxmox/lxc-strict-adopt.test.ts +1 -1
  198. package/src/proxmox/lxc-volume.test.ts +9 -9
  199. package/src/proxmox/lxc-volume.ts +1 -1
  200. package/src/proxmox/lxc-wire.ts +1 -1
  201. package/src/proxmox/lxc.ts +3 -3
  202. package/src/proxmox/members.test.ts +1 -1
  203. package/src/proxmox/members.ts +3 -3
  204. package/src/proxmox/metric-server-otel.ts +2 -2
  205. package/src/proxmox/network-apply-read.ts +10 -10
  206. package/src/proxmox/network-apply.ts +10 -10
  207. package/src/proxmox/node-network-form.ts +7 -7
  208. package/src/proxmox/node-network.ts +18 -18
  209. package/src/proxmox/notification-target.ts +3 -2
  210. package/src/proxmox/pbs-datastore-guard.ts +1 -1
  211. package/src/proxmox/pbs-datastore.ts +10 -10
  212. package/src/proxmox/pbs-prune-job.ts +2 -2
  213. package/src/proxmox/pbs-sync-job.ts +4 -4
  214. package/src/proxmox/pbs-verify-job.ts +1 -1
  215. package/src/proxmox/pool.test.ts +2 -2
  216. package/src/proxmox/pool.ts +4 -4
  217. package/src/proxmox/provision-baseline.test.ts +87 -0
  218. package/src/proxmox/provision-baseline.ts +162 -0
  219. package/src/proxmox/provision-bootstrap.test.ts +132 -0
  220. package/src/proxmox/provision-bootstrap.ts +135 -0
  221. package/src/proxmox/provision-cli-fake.ts +217 -0
  222. package/src/proxmox/provision-declare.test.ts +111 -0
  223. package/src/proxmox/provision-declare.ts +103 -0
  224. package/src/proxmox/replication-job.ts +9 -8
  225. package/src/proxmox/resource.ts +3 -3
  226. package/src/proxmox/sdn-apply-read.test.ts +16 -16
  227. package/src/proxmox/sdn-apply-read.ts +7 -7
  228. package/src/proxmox/sdn-apply.ts +3 -3
  229. package/src/proxmox/sdn-subnet-form.ts +1 -1
  230. package/src/proxmox/sdn-subnet.ts +5 -5
  231. package/src/proxmox/sdn-vnet.ts +5 -6
  232. package/src/proxmox/storage.ts +4 -3
  233. package/src/proxmox/user.ts +3 -2
  234. package/src/proxmox/values.test.ts +2 -2
  235. package/src/proxmox/values.ts +1 -1
  236. package/src/proxmox/zfs-pool-write.ts +1 -1
  237. package/src/proxmox/zfs-pool.ts +6 -6
  238. package/src/talos/credentials.ts +3 -3
  239. package/src/talos/values.test.ts +9 -9
  240. package/src/verify/cli.test.ts +1 -1
@@ -36,7 +36,7 @@ export const fold = (name: string) => name.trim().toLowerCase();
36
36
  /**
37
37
  * A CIDR reduced to the one form PVE will hand back.
38
38
  *
39
- * ⛔ MEASURED by running the cluster's own `PVE::Firewall::parse_alias` on n2 (pve-manager 9.2.11,
39
+ * ⛔ MEASURED by running the cluster's own `PVE::Firewall::parse_alias` on node-b (pve-manager 9.2.11,
40
40
  * 2026-09-13) — a pure function over a string, no config read and none written:
41
41
  * `10.1.1.5/32` -> `10.1.1.5` a /32 host suffix is STRIPPED
42
42
  * `2001:0DB8::1/128`-> `2001:0DB8::1` a /128 is stripped, and the case is NOT folded
@@ -61,7 +61,7 @@
61
61
  * ⚠️ PRIVILEGES, AND FOR ONCE NOTHING NEEDS WIDENING. MEASURED 2026-09-13 from the cluster's own
62
62
  * schema and its live ACL: the item GET checks `["perm","/",["Sys.Audit"]]`, POST/PUT/DELETE
63
63
  * check `["perm","/",["Sys.Modify"]]`, `hf-read@pve` holds `PVEAuditor` at `/` with propagate and
64
- * `hf-provision@pve` holds `LXCProvisioner` at `/` with propagate. Both privileges are already
64
+ * `hf-provision@pve` holds the provision role (provision-baseline.ts) at `/` with propagate. Both privileges are already
65
65
  * held, so `readRole` stays the default `read` lane — unlike storage/sdn-zone/sdn-vnet, whose
66
66
  * ITEM reads are gated on an allocate privilege the auditor cannot have.
67
67
  * ⚠️ `Sys.Modify` ON `/` IS STILL A BIG HAMMER — it is the same grant `metric-server.ts` warns
@@ -2,7 +2,7 @@
2
2
  * Generated PBS API types — DO NOT EDIT BY HAND.
3
3
  *
4
4
  * Run: bun codegen/generate.ts
5
- * Source: house/proxmox/apidoc/ — see README there for provenance.
5
+ * Source: the cluster's own `apidoc.js` (pve-docs), extracted outside this repo.
6
6
  */
7
7
 
8
8
  /** GET /config/datastore — `data` payload after client unwrap. */
@@ -2,7 +2,7 @@
2
2
  * Generated PVE API types — DO NOT EDIT BY HAND.
3
3
  *
4
4
  * Run: bun codegen/generate.ts
5
- * Source: house/proxmox/apidoc/ — see README there for provenance.
5
+ * Source: the cluster's own `apidoc.js` (pve-docs), extracted outside this repo.
6
6
  */
7
7
 
8
8
  /** GET /cluster/replication — `data` payload after client unwrap. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `Proxmox.Group` — a PVE group: a name, a comment, and the set of users PVE hangs off it.
3
3
  *
4
- * ★ DECLARING `hf-mint` IS THE WHOLE POINT OF THIS FAMILY. `vaultmint@pve` holds the `HFMint` role
4
+ * ★ DECLARING `hf-mint` IS THE WHOLE POINT OF THIS FAMILY. `mint@pve` holds the `MintTokens` role
5
5
  * ON THE PATH `/access/groups/hf-mint` — measured from `GET /access/acl`, 2026-09-13 — which is
6
6
  * what confines the OpenBao proxmox engine to minting inside that one group. The group is the
7
7
  * fence around the credential every other resource in this package runs on, and until now it was
@@ -17,7 +17,7 @@
17
17
  * in this file could ever settle.
18
18
  *
19
19
  * ⛔ THE INDEX AND THE ITEM DISAGREE ABOUT BOTH THE NAME AND THE TYPE, and this file reads only the
20
- * item. MEASURED against n2, 2026-09-13:
20
+ * item. MEASURED against node-b, 2026-09-13:
21
21
  * GET /access/groups -> [{"groupid":"hf-mint","comment":"…",
22
22
  * "users":"hf-provision@pve,hf-read@pve"}, …]
23
23
  * GET /access/groups/hf-mint -> {"comment":"…","members":["hf-provision@pve","hf-read@pve"]}
@@ -27,8 +27,8 @@
27
27
  *
28
28
  * ⛔ AND THE ITEM'S ORDER IS NOT STABLE BETWEEN TWO CONSECUTIVE CALLS. MEASURED, seconds apart, on
29
29
  * the same endpoint:
30
- * ["root@pam","tim@pve","tim@pam","tim@<account>.com@Schenanigans"]
31
- * ["tim@pam","tim@pve","tim@<account>.com@Schenanigans","root@pam"]
30
+ * ["root@pam","alice@pve","alice@pam","alice@example.com@corp"]
31
+ * ["alice@pam","alice@pve","alice@example.com@corp","root@pam"]
32
32
  * `read_group` builds it as `[keys %{ $data->{users} }]` — a bare Perl hash key list, whose order
33
33
  * is randomised per process — while the INDEX sorts (`join(',', sort keys …)`). So the array is
34
34
  * sorted on the way into `attributes` as well as being kept out of `matches`: unsorted, Alchemy's
@@ -45,7 +45,7 @@
45
45
  * So `readRole` is left at the default `read` — unlike storage/sdn-zone/sdn-vnet, whose ITEM
46
46
  * reads PVE gates on an allocate privilege. See the ⛔ on `readRole` in `resource.ts`.
47
47
  * · POST, PUT and DELETE each check `Group.Allocate` on `/access/groups`, and
48
- * `hf-provision@pve` holds it via `LXCProvisioner`.
48
+ * `hf-provision@pve` holds it via the provision role (`PROVISION_PRIVILEGES`).
49
49
  *
50
50
  * ⛔ `retain` BY DEFAULT, BECAUSE DELETING A GROUP DESTROYS TWO THINGS THIS FILE CANNOT PUT BACK.
51
51
  * Measured in `PVE::AccessControl` on the node:
@@ -53,8 +53,8 @@
53
53
  * <comment>:`) and DERIVES each user's `groups` map from it at parse time. Deleting the group
54
54
  * deletes the list, and this resource has no `members` prop to restore it from.
55
55
  * · `delete_group_acl` then walks the whole ACL tree and drops every grant where the group is
56
- * the SUBJECT. On this cluster that is `Schenanigans -> Administrator on /` and
57
- * `HomeAssistant -> PVEAuditor on /`: whole populations of access, gone in one call, with no
56
+ * the SUBJECT. On this cluster that is `admins -> Administrator on /` and
57
+ * `automation -> PVEAuditor on /`: whole populations of access, gone in one call, with no
58
58
  * confirmation and nothing in the plan to suggest it.
59
59
  * `delete` is FULLY IMPLEMENTED — `DELETE /access/groups/{groupid}` exists, unlike the ACL
60
60
  * family's — so `.pipe(RemovalPolicy.destroy())` really removes the group. See the ★ in
@@ -120,7 +120,7 @@ export const ProxmoxGroup = Resource<ProxmoxGroup>('Proxmox.Group', {
120
120
  * back as absent. Left alone, `matches` would be false on every plan and every deploy would
121
121
  * rewrite the same value into the same hole. Normalising BOTH the comparison and the form is how
122
122
  * `values.ts` already treats `''` as the absence of a boolean rather than as `false`.
123
- * ⚠️ MEASURED IN THE SOURCE ON n2, NOT ON THE WIRE — writing `'0'` to the cluster to watch it
123
+ * ⚠️ MEASURED IN THE SOURCE ON node-b, NOT ON THE WIRE — writing `'0'` to the cluster to watch it
124
124
  * vanish would have been a write, and this agent had read access only.
125
125
  * ⚠️ `pool.ts` HAS THE SAME LATENT HOLE — `pool:$pool:$comment:…` is written by that same
126
126
  * truthiness test and is not guarded. Not fixed from here; flagged so it is fixed on purpose.
@@ -160,7 +160,7 @@ const handlers = pveHandlers<GroupProps, GroupAttributes>({
160
160
  * without one answers `{"members":[…]}` and nothing else. THAT ONE IS READ OFF THE HANDLER'S
161
161
  * SOURCE, not off the wire — every group on this cluster happens to carry a comment, so the
162
162
  * commentless shape was not there to measure. The empty-MEMBERS shape WAS measured —
163
- * `HomeAssistant` answers `{"comment":"…","members":[]}`. `''` is what an undeclared
163
+ * `automation` answers `{"comment":"…","members":[]}`. `''` is what an undeclared
164
164
  * `comment` prop normalises to, so the two sides meet either way.
165
165
  */
166
166
  comment: text(live['comment']),
@@ -190,7 +190,7 @@ const handlers = pveHandlers<GroupProps, GroupAttributes>({
190
190
 
191
191
  /**
192
192
  * ⛔ THE EMPTY `list` IS INHERITED FROM `pveHandlers` AND IT MATTERS HERE. `GET /access/groups`
193
- * answers with every group on the cluster — on this one that is `Schenanigans`, which grants
193
+ * answers with every group on the cluster — on this one that is `admins`, which grants
194
194
  * `Administrator` on `/` to every human who logs in. Handing it to Alchemy would invite adoption,
195
195
  * and therefore one day a delete that takes the whole admin group's access with it. Adoption
196
196
  * stays an explicit act, here as everywhere else in this package.
@@ -35,10 +35,11 @@
35
35
  * (node-affinity and resource-affinity) and that is the next family to add; do not port the old
36
36
  * groups endpoint back just because the docs still describe it.
37
37
  *
38
- * ⚠️ RECONCILE NEEDS `Sys.Console` ON `/`, WHICH `LXCProvisioner` DOES NOT HOLD. Read and diff are
38
+ * ⚠️ RECONCILE NEEDS `Sys.Console` ON `/`, WHICH THE PROVISION ROLE LACKED WHEN THIS WAS WRITTEN
39
+ * (`PROVISION_PRIVILEGES` carries it now — ha-rule.ts has the measurement). Read and diff are
39
40
  * fine — `GET /cluster/ha/resources/{sid}` checks `Sys.Audit`, which the role already has — while
40
41
  * POST, PUT and DELETE all check `Sys.Console` on `/` and will answer "Permission check failed
41
- * (/, Sys.Console)" until the role is widened. Widen it knowingly: `Sys.Console` is also what
42
+ * (/, Sys.Console)" on a role without it. Widen it knowingly: `Sys.Console` is also what
42
43
  * opens a root shell on every node (`/nodes/{node}/vncshell`, `termproxy`), so granting it to the
43
44
  * provision credential buys HA membership at the price of node console access. A separate role
44
45
  * for HA writes is the narrower answer if that trade is not wanted.
@@ -16,21 +16,21 @@ import { csv } from './values.ts';
16
16
  * A `<node>[:<pri>]` list, spelled the way PVE gives it back.
17
17
  *
18
18
  * ⛔ THIS IS NOT `csv`, AND THE DIFFERENCE IS A FOREVER-DIFF. MEASURED by reading
19
- * `/usr/share/perl5/PVE/HA/Rules/NodeAffinity.pm` on n2 (2026-09-13): `decode_plugin_value`
19
+ * `/usr/share/perl5/PVE/HA/Rules/NodeAffinity.pm` on node-b (2026-09-13): `decode_plugin_value`
20
20
  * turns the list into a HASH keyed by node name, and `encode_plugin_value` rebuilds it with
21
21
  * `for my $node (sort keys %$value)` — sorted by NODE NAME, never by the token — emitting the
22
22
  * bare name whenever the priority is falsy, because `PVE::HA::Tools::parse_node_priority` reads
23
- * a missing priority as 0. So a declared `n3:1,n2:0` is handed back as `n2,n3:1`: re-ordered AND
23
+ * a missing priority as 0. So a declared `node-c:1,node-b:0` is handed back as `node-b,node-c:1`: re-ordered AND
24
24
  * re-spelled. Compare the raw strings and every plan reports an update, forever.
25
25
  *
26
26
  * ⚠️ SORTED BY NAME, NOT BY TOKEN, AND THE TWO GENUINELY DIVERGE. `csv` sorts whole tokens. Node
27
27
  * names may contain `-` — `parse_node_priority`'s own regex is
28
- * `[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?` — and `-` (0x2D) sorts before `:` (0x3A), so `n2-a`
29
- * and `n2:5` come out in one order by token and the other by name. PVE sorts by the name, so
28
+ * `[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?` — and `-` (0x2D) sorts before `:` (0x3A), so `node-b-a`
29
+ * and `node-b:5` come out in one order by token and the other by name. PVE sorts by the name, so
30
30
  * this does too.
31
31
  *
32
32
  * ⚠️ LAST ENTRY WINS FOR A REPEATED NODE, because PVE's decode is a hash assignment and a hash
33
- * keeps one value per key. A declaration saying `n2:1,n2:5` is stored as `n2:5`; matching that
33
+ * keeps one value per key. A declaration saying `node-b:1,node-b:5` is stored as `node-b:5`; matching that
34
34
  * is what a Map gives for free.
35
35
  */
36
36
  export const nodeList = (value: readonly string[] | string | undefined) => {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `Proxmox.HaRule` — where the CRM may place a guest, and which guests must not share a node.
3
3
  *
4
- * ★ THIS IS THE PVE 9 SUCCESSOR TO HA GROUPS, AND THE GROUPS ENDPOINT IS GONE. MEASURED on n2
4
+ * ★ THIS IS THE PVE 9 SUCCESSOR TO HA GROUPS, AND THE GROUPS ENDPOINT IS GONE. MEASURED on node-b
5
5
  * (pve-manager/9.2.11, 2026-09-13): `pvesh get /cluster/ha/groups` answers `cannot index groups:
6
6
  * ha groups have been migrated to rules`, and `/etc/pve/ha/` holds `rules.cfg` with no
7
7
  * `groups.cfg` beside it. The published schema still documents groups and still offers a `group`
@@ -48,8 +48,8 @@
48
48
  * ⚠️ BOTH LANES ALREADY HAVE THE PRIVILEGES, WHICH IS NEW. MEASURED 2026-09-13:
49
49
  * GET (collection and item) checks `Sys.Audit` on `/` and the built-in `PVEAuditor` holds it, so
50
50
  * `readRole` stays the default `read` lease — unlike storage.ts and the SDN families. POST, PUT
51
- * and DELETE all check `Sys.Console` on `/`, and `pvesh get /access/roles/LXCProvisioner` now
52
- * returns `Sys.Console` among its 27 privileges. The warning in ha-resource.ts's header — that
51
+ * and DELETE all check `Sys.Console` on `/`, and the provision role now returns `Sys.Console` among
52
+ * its 27 privileges (`PROVISION_PRIVILEGES`, provision-baseline.ts). The warning in ha-resource.ts's header — that
53
53
  * `Sys.Console` is missing — is stale; the role was widened, and the price it named still stands:
54
54
  * `Sys.Console` is also what opens a root shell on every node.
55
55
  *
@@ -40,6 +40,13 @@ test('control-plane Resource constructors are on the public barrel', () => {
40
40
  }
41
41
  });
42
42
 
43
+ test('the provisioning baseline is on the barrel: the list, the declaration, the bootstrap', async () => {
44
+ const barrel = await import('./index.ts');
45
+ expect(barrel.PROVISION_PRIVILEGES).toHaveLength(27);
46
+ expect(typeof barrel.declareProvisionBaseline).toBe('function');
47
+ expect(barrel.provisionBootstrap()).toStartWith('#!/bin/sh\n');
48
+ });
49
+
43
50
  test('QEMU and NIC apply stay Provider-only', () => {
44
51
  expect(src).not.toMatch(/export \{ ProxmoxVm[, }]/);
45
52
  expect(src).not.toMatch(/export \{ ProxmoxNodeNetwork[, }]/);
@@ -12,6 +12,9 @@
12
12
  * The provider-only version diffed four fields and would have planned a create over any read
13
13
  * failure. The Resource now adopts from the live config, refuses every change PVE cannot make
14
14
  * in place, and retains on destroy (lxc.ts).
15
+ * ★ THE PROVISIONING BASELINE JOINED 2026-09-21: one privilege list (`PROVISION_PRIVILEGES`), the
16
+ * helper that declares it (`declareProvisionBaseline`) and the pure generator of the one-time
17
+ * root commands that bootstrap it (`provisionBootstrap`) — docs/provision-baseline.md.
15
18
  * ★ Anything else unlisted is still reachable by path if you genuinely need
16
19
  * it — that is a deliberate, visible act rather than an accident of barrelling.
17
20
  */
@@ -40,6 +43,17 @@ export { PbsPruneJob, PbsPruneJobProvider } from './pbs-prune-job.ts';
40
43
  export { PbsSyncJob, PbsSyncJobProvider } from './pbs-sync-job.ts';
41
44
  export { PbsVerifyJob, PbsVerifyJobProvider } from './pbs-verify-job.ts';
42
45
  export { ProxmoxPool, ProxmoxPoolProvider } from './pool.ts';
46
+ export {
47
+ PROVISION_DEFAULTS,
48
+ PROVISION_PRIVILEGES,
49
+ type ProvisionBaseline,
50
+ type ProvisionLane,
51
+ type ProvisionNames,
52
+ type ResolvedProvisionNames,
53
+ provisionBaseline,
54
+ } from './provision-baseline.ts';
55
+ export { provisionBootstrap } from './provision-bootstrap.ts';
56
+ export { type DeclareProvisionOptions, declareProvisionBaseline } from './provision-declare.ts';
43
57
  export { ProxmoxVmProvider } from './qemu.ts';
44
58
  export { ProxmoxReplicationJobProvider } from './replication-job.ts';
45
59
  export { ProxmoxRole, ProxmoxRoleProvider } from './role.ts';
@@ -17,13 +17,13 @@ import * as Exit from 'effect/Exit';
17
17
  import type { PveCredential } from './credentials.ts';
18
18
  import { type LeaseKey, makeLeases, timeToLive } from './lease-cache.ts';
19
19
 
20
- const TB4: LeaseKey = {
21
- mount: 'proxmox-tb4',
20
+ const C1: LeaseKey = {
21
+ mount: 'proxmox-c1',
22
22
  role: 'read',
23
23
  scheme: 'pve',
24
24
  };
25
- /** ⚠️ Same role as TB4, DIFFERENT mount — the collision the key has to survive. */
26
- const OPS: LeaseKey = { ...TB4, mount: 'proxmox-ops' };
25
+ /** ⚠️ Same role as C1, DIFFERENT mount — the collision the key has to survive. */
26
+ const C2: LeaseKey = { ...C1, mount: 'proxmox-c2' };
27
27
 
28
28
  const cred = (tokenId: string, leaseSeconds: number): PveCredential => ({
29
29
  leaseSeconds,
@@ -85,10 +85,10 @@ describe('lease cache under concurrency', () => {
85
85
  await Effect.runPromise(
86
86
  Effect.gen(function* () {
87
87
  const leases = yield* makeLeases(mint.mintFor);
88
- const exits = yield* concurrently(20, Cache.get(leases, TB4));
88
+ const exits = yield* concurrently(20, Cache.get(leases, C1));
89
89
  assert.equal(exits.filter(Exit.isSuccess).length, 20);
90
90
  // An equal but separately built key is the same entry.
91
- yield* Cache.get(leases, { ...TB4 });
91
+ yield* Cache.get(leases, { ...C1 });
92
92
  }),
93
93
  );
94
94
  assert.equal(mint.calls(), 1);
@@ -102,9 +102,9 @@ describe('lease cache under concurrency', () => {
102
102
  await Effect.runPromise(
103
103
  Effect.gen(function* () {
104
104
  const leases = yield* makeLeases(mint.mintFor);
105
- const first = yield* concurrently(20, Cache.get(leases, TB4));
105
+ const first = yield* concurrently(20, Cache.get(leases, C1));
106
106
  assert.equal(first.filter(Exit.isFailure).length, 20, 'the waiters share the failure');
107
- const second = yield* concurrently(20, Cache.get(leases, TB4));
107
+ const second = yield* concurrently(20, Cache.get(leases, C1));
108
108
  assert.equal(second.filter(Exit.isSuccess).length, 20);
109
109
  }),
110
110
  );
@@ -116,8 +116,8 @@ describe('lease cache under concurrency', () => {
116
116
  await Effect.runPromise(
117
117
  Effect.gen(function* () {
118
118
  const leases = yield* makeLeases(mint.mintFor);
119
- yield* concurrently(20, Cache.get(leases, TB4));
120
- yield* Cache.get(leases, TB4);
119
+ yield* concurrently(20, Cache.get(leases, C1));
120
+ yield* Cache.get(leases, C1);
121
121
  }),
122
122
  );
123
123
  assert.equal(mint.calls(), 2);
@@ -125,25 +125,25 @@ describe('lease cache under concurrency', () => {
125
125
 
126
126
  it('does not serve one cluster the credential of another with the same mount and role', async () => {
127
127
  const mint = countingMint((call) => cred(`hf-read@pve!${String(call)}`, 3600));
128
- const [tb4, ops] = await Effect.runPromise(
128
+ const [c1, c2] = await Effect.runPromise(
129
129
  Effect.gen(function* () {
130
130
  const leases = yield* makeLeases(mint.mintFor);
131
- return [yield* Cache.get(leases, TB4), yield* Cache.get(leases, OPS)] as const;
131
+ return [yield* Cache.get(leases, C1), yield* Cache.get(leases, C2)] as const;
132
132
  }),
133
133
  );
134
- assert.notEqual(tb4.tokenId, ops.tokenId);
134
+ assert.notEqual(c1.tokenId, c2.tokenId);
135
135
  assert.equal(mint.calls(), 2);
136
136
  });
137
137
 
138
138
  it('shares one lease across two members of the same cluster mount', async () => {
139
139
  const mint = countingMint((call) => cred(`hf-read@pve!${String(call)}`, 3600));
140
- const N2: LeaseKey = { mount: 'proxmox-tb4', role: 'read', scheme: 'pve' };
141
- const N3: LeaseKey = { mount: 'proxmox-tb4', role: 'read', scheme: 'pve' };
140
+ const NODE_B: LeaseKey = { mount: 'proxmox-c1', role: 'read', scheme: 'pve' };
141
+ const NODE_C: LeaseKey = { mount: 'proxmox-c1', role: 'read', scheme: 'pve' };
142
142
  await Effect.runPromise(
143
143
  Effect.gen(function* () {
144
144
  const leases = yield* makeLeases(mint.mintFor);
145
- yield* Cache.get(leases, N2);
146
- yield* Cache.get(leases, N3);
145
+ yield* Cache.get(leases, NODE_B);
146
+ yield* Cache.get(leases, NODE_C);
147
147
  }),
148
148
  );
149
149
  assert.equal(mint.calls(), 1);
@@ -151,11 +151,11 @@ describe('lease cache under concurrency', () => {
151
151
 
152
152
  it('keeps read and provision apart', async () => {
153
153
  const mint = countingMint((call) => cred(`hf@pve!${String(call)}`, 3600));
154
- const PROVISION: LeaseKey = { ...TB4, role: 'provision' };
154
+ const PROVISION: LeaseKey = { ...C1, role: 'provision' };
155
155
  const [read, provision] = await Effect.runPromise(
156
156
  Effect.gen(function* () {
157
157
  const leases = yield* makeLeases(mint.mintFor);
158
- return [yield* Cache.get(leases, TB4), yield* Cache.get(leases, PROVISION)] as const;
158
+ return [yield* Cache.get(leases, C1), yield* Cache.get(leases, PROVISION)] as const;
159
159
  }),
160
160
  );
161
161
  assert.notEqual(read.tokenId, provision.tokenId);
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * One credential per (target, role) for the life of a run, instead of one per API call.
3
3
  *
4
- * 🔴 THE MEASUREMENT THAT MADE THIS NECESSARY. A single `alchemy plan` plus `deploy` of the TB4
5
- * stack — 98 resources — left **760 API token entries in `/etc/pve/user.cfg`**, counted on n2
4
+ * 🔴 THE MEASUREMENT THAT MADE THIS NECESSARY. A single `alchemy plan` plus `deploy` of the C1
5
+ * stack — 98 resources — left **760 API token entries in `/etc/pve/user.cfg`**, counted on node-b
6
6
  * immediately afterwards. `mint()` asked OpenBao for a new credential on EVERY call, and each
7
7
  * mint creates a real Proxmox token: read, diff, reconcile and read-back each got their own. They
8
8
  * drain as the leases expire (760 down to 712 over five minutes), so nothing was broken — but
@@ -66,9 +66,9 @@ const REMINT_MARGIN_SECONDS = 60;
66
66
  * What a credential is cached under.
67
67
  *
68
68
  * ⛔ KEYED ON THE OPENBAO MOUNT AND ROLE, NOT ON A MEMBER HOSTNAME. A PVE API token is
69
- * cluster-wide — `user.cfg` is replicated — so n2, n3 and n4 share one credential. Keying on
69
+ * cluster-wide — `user.cfg` is replicated — so node-b, node-c and node-d share one credential. Keying on
70
70
  * `api` would mint three identical tokens when failover rotates members.
71
- * ★ THE MOUNT CANNOT COLLIDE ACROSS CLUSTERS: `proxmox-tb4`, `proxmox-ops` and `pbs-tb4` are
71
+ * ★ THE MOUNT CANNOT COLLIDE ACROSS CLUSTERS: `proxmox-c1`, `proxmox-c2` and `pbs-c1` are
72
72
  * distinct OpenBao mounts with distinct policies; two estates never share a mount name.
73
73
  * ★ A PLAIN OBJECT IS A SAFE KEY BECAUSE EFFECT COMPARES KEYS STRUCTURALLY — two separately built
74
74
  * literals with the same fields are one entry, which the probe above measured.
@@ -100,7 +100,7 @@ describe('drift after the adoption is named at plan and written once', () => {
100
100
  test('a larger rootfs is a resize after the config, never a new volume', async () => {
101
101
  const pve = live();
102
102
  const stack = await adoptedAsIs(pve);
103
- const props: LxcProps = { ...pasted(), rootfs: 'local-zfs:subvol-100-disk-0,size=64G' };
103
+ const props: LxcProps = { ...pasted(), rootfs: 'local-zfs:subvol-900-disk-0,size=64G' };
104
104
  expect((await stack.deploy(ct(props))).failure).toBe('');
105
105
  expect(writesOf(pve)).toEqual([`PUT nodes/${NODE}/lxc/${String(VMID)}/resize`]);
106
106
  expect(pve.guests.get(`${NODE}/${String(VMID)}`)?.['rootfs']).toContain('size=64G');
@@ -109,10 +109,10 @@ describe('drift after the adoption is named at plan and written once', () => {
109
109
 
110
110
  describe('changes PVE cannot make in place fail the plan and write nothing', () => {
111
111
  test.each([
112
- ['a smaller mount point', { mp0: 'tank:subvol-100-disk-0,mp=/data,size=100G' }, /shrink/],
112
+ ['a smaller mount point', { mp0: 'tank:subvol-900-disk-0,mp=/data,size=100G' }, /shrink/],
113
113
  ['a storage move', { rootfs: 'other:40' }, /pct move-volume/],
114
114
  ['an unprivileged flip', { unprivileged: 0 as const }, /read-only option/],
115
- ['a device passthrough change', { dev1: '/dev/fuse' }, /pct set 100 --dev1/],
115
+ ['a device passthrough change', { dev1: '/dev/fuse' }, /pct set 900 --dev1/],
116
116
  ['a feature beyond nesting', { features: 'nesting=1,keyctl=1' }, /root@pam/],
117
117
  ['detaching a mount point', { mp0: '' }, /unusedN/],
118
118
  ])('%s', async (_, change, reason) => {
@@ -23,7 +23,7 @@ import { bool } from './values.ts';
23
23
  /**
24
24
  * ⛔ A CREATE ALLOCATES NEW VOLUMES ONLY. pve-container's `create_disks` allocates `storage:GiB`
25
25
  * and takes anything else as "use specified/existing volid", then `restore_archive` unpacks the
26
- * template into the mounted rootfs. So a create naming `local-zfs:subvol-100-disk-0` — a pasted
26
+ * template into the mounted rootfs. So a create naming `local-zfs:subvol-900-disk-0` — a pasted
27
27
  * adoption, re-created after its config vanished while the volume survived (a `pct destroy`
28
28
  * only WARNS when a volume will not delete) — unpacks a template over the data on it.
29
29
  */
@@ -63,8 +63,8 @@ describe('a guest the cluster no longer lists, with state still held', () => {
63
63
  expect((await stack.deploy(declared())).failure).toBe('');
64
64
  pve.guests.delete(KEY);
65
65
  const run = await stack.deploy(declared());
66
- expect(run.failure).toContain('rootfs: local-zfs:subvol-100-disk-0,size=40G names an existing');
67
- expect(run.failure).toContain('mp0: tank:subvol-100-disk-0');
66
+ expect(run.failure).toContain('rootfs: local-zfs:subvol-900-disk-0,size=40G names an existing');
67
+ expect(run.failure).toContain('mp0: tank:subvol-900-disk-0');
68
68
  expect(writesOf(pve)).toEqual([]);
69
69
  });
70
70
 
@@ -130,13 +130,13 @@ describe('identity and volumes', () => {
130
130
  const pve = cluster();
131
131
  seed(pve, NODE, VMID);
132
132
  const stack = await adoptedAsIs(pve);
133
- const mp0 = 'tank:subvol-100-disk-0,mp=/data,backup=1,size=300G';
133
+ const mp0 = 'tank:subvol-900-disk-0,mp=/data,backup=1,size=300G';
134
134
  const run = await stack.deploy(ct(pasted({ mp0 })));
135
135
  expect(run.failure).toBe('');
136
136
  const path = `nodes/${NODE}/lxc/${String(VMID)}`;
137
137
  expect(writesOf(pve)).toEqual([`PUT ${path}/config`, `PUT ${path}/resize`]);
138
138
  const put = pve.seen.find((call) => call.path.endsWith('/config') && call.method === 'PUT');
139
- expect(put?.form.get('mp0')).toBe('tank:subvol-100-disk-0,mp=/data,backup=1,size=200G');
139
+ expect(put?.form.get('mp0')).toBe('tank:subvol-900-disk-0,mp=/data,backup=1,size=200G');
140
140
  expect(pve.guests.get(KEY)?.['mp0']).toContain('size=300G');
141
141
  });
142
142
 
@@ -54,7 +54,7 @@ export const LIVE: Readonly<Record<string, unknown>> = {
54
54
  hostname: 'ct-example',
55
55
  lxc: [['lxc.prlimit.memlock', 'unlimited']],
56
56
  memory: 8192,
57
- mp0: 'tank:subvol-100-disk-0,mp=/data,backup=0,size=200G',
57
+ mp0: 'tank:subvol-900-disk-0,mp=/data,backup=0,size=200G',
58
58
  nameserver: '192.0.2.254',
59
59
  net0:
60
60
  'name=eth0,bridge=vmbr0,firewall=0,gw=192.0.2.1,hwaddr=00:00:5E:00:53:01,' +
@@ -62,7 +62,7 @@ export const LIVE: Readonly<Record<string, unknown>> = {
62
62
  net1: 'name=eth1,bridge=vmbr1,hwaddr=00:00:5E:00:53:02,ip=198.51.100.4/24,mtu=1500,type=veth',
63
63
  onboot: 1,
64
64
  ostype: 'debian',
65
- rootfs: 'local-zfs:subvol-100-disk-0,size=40G',
65
+ rootfs: 'local-zfs:subvol-900-disk-0,size=40G',
66
66
  searchdomain: 'example.com',
67
67
  swap: 0,
68
68
  tags: 'web',
@@ -71,7 +71,7 @@ export const LIVE: Readonly<Record<string, unknown>> = {
71
71
 
72
72
  /** Where `seed` puts the production-shaped guest by default, and its `node/vmid` key. */
73
73
  export const NODE = 'pve1';
74
- export const VMID = 100;
74
+ export const VMID = 900;
75
75
  export const KEY = `${NODE}/${String(VMID)}`;
76
76
 
77
77
  /** The live config as a declaration: every key, minus what pvesh adds that is not a prop. */
@@ -13,7 +13,7 @@ import { judgeVolume, parseVolume, sizeBytes } from './lxc-volume.ts';
13
13
  const props = (over: Partial<LxcProps>): LxcProps => ({
14
14
  node: 'pve1',
15
15
  target: TARGET,
16
- vmid: 100,
16
+ vmid: 900,
17
17
  ...over,
18
18
  });
19
19
  const live = (config: Record<string, unknown>) => ({ unprivileged: 1, ...config });
@@ -36,7 +36,7 @@ describe('sizes and volumes, as pve-container parses them', () => {
36
36
 
37
37
  test('an options change on a volume declared as new is written with the LIVE volume id', () => {
38
38
  const verdict = judgeVolume('mp0', 'tank:200,mp=/data,backup=1', LIVE_MP);
39
- expect(verdict).toEqual({ put: 'tank:subvol-100-disk-0,mp=/data,backup=1,size=200G' });
39
+ expect(verdict).toEqual({ put: 'tank:subvol-900-disk-0,mp=/data,backup=1,size=200G' });
40
40
  });
41
41
 
42
42
  test('a fractional new-disk size grows by the same GiB', () => {
@@ -44,7 +44,7 @@ describe('sizes and volumes, as pve-container parses them', () => {
44
44
  });
45
45
  });
46
46
 
47
- const LIVE_MP = 'tank:subvol-100-disk-0,mp=/data,backup=0,size=200G';
47
+ const LIVE_MP = 'tank:subvol-900-disk-0,mp=/data,backup=0,size=200G';
48
48
 
49
49
  describe('scalars compare the way PVE stores them', () => {
50
50
  test.each([
@@ -106,7 +106,7 @@ describe('features follow check_ct_modify_config_perm', () => {
106
106
 
107
107
  test('clearing features that hold more than nesting is root@pam only, as PVE counts keys', () => {
108
108
  const change = judge(props({ features: '' }), live({ features: 'nesting=1,keyctl=0' }));
109
- expect(change.refuse[0]).toMatch(/pct set 100 --delete features/);
109
+ expect(change.refuse[0]).toMatch(/pct set 900 --delete features/);
110
110
  });
111
111
  });
112
112
 
@@ -147,7 +147,7 @@ describe('create', () => {
147
147
  ostemplate: 'local:vztmpl/t.tar.zst',
148
148
  start: '1',
149
149
  unprivileged: '1',
150
- vmid: '100',
150
+ vmid: '900',
151
151
  });
152
152
  });
153
153
 
@@ -72,7 +72,7 @@ export const readLive = (where: LxcWhere) =>
72
72
  'GET',
73
73
  'cluster/resources?type=vm',
74
74
  );
75
- // ⚠️ `Number()`, NOT `===` ON THE RAW VALUE: a vmid that came back as `"100"` would otherwise
75
+ // ⚠️ `Number()`, NOT `===` ON THE RAW VALUE: a vmid that came back as `"900"` would otherwise
76
76
  // match nothing, and "listed nowhere" is the one answer that turns into a create.
77
77
  const found = (rows ?? []).find((row) => Number(row.vmid) === where.vmid);
78
78
  if (found !== undefined && found.node === where.node && found.type === 'lxc') {
@@ -58,7 +58,7 @@ export interface LxcProps extends WithTarget {
58
58
  /** MiB. */
59
59
  swap?: number;
60
60
  /**
61
- * `local-zfs:subvol-100-disk-0,size=40G` as read, or `local-zfs:40` (40 GiB, new volume) to
61
+ * `local-zfs:subvol-900-disk-0,size=40G` as read, or `local-zfs:40` (40 GiB, new volume) to
62
62
  * create. See lxc-volume.ts for what may change: options in place, size up only.
63
63
  */
64
64
  rootfs?: string;
@@ -79,7 +79,7 @@ export interface LxcProps extends WithTarget {
79
79
  console?: PveFlag;
80
80
  tty?: number;
81
81
  cmode?: string;
82
- /** `tank:subvol-100-disk-1,mp=/data,backup=1,size=100G`, or `tank:100,mp=/data` to create. */
82
+ /** `tank:subvol-900-disk-1,mp=/data,backup=1,size=100G`, or `tank:100,mp=/data` to create. */
83
83
  [mount: `mp${number}`]: string | undefined;
84
84
  /** `name=eth0,bridge=vmbr0,ip=dhcp`. ⚠️ An undeclared `hwaddr` keeps the live MAC. */
85
85
  [nic: `net${number}`]: string | undefined;
@@ -40,10 +40,10 @@ describe('absent means the cluster does not list the vmid', () => {
40
40
 
41
41
  test('a 500 for a guest the cluster lists on this node fails with PVE’s error', async () => {
42
42
  fake = fakePve();
43
- seed(fake, NODE, 100, LIVE);
43
+ seed(fake, NODE, 900, LIVE);
44
44
  const message = 'got timeout\n';
45
- fake.configErrors.set(`${NODE}/100`, { body: { data: null, message }, status: 500 });
46
- const run = await lxcEngine(fake).plan(declared(100));
45
+ fake.configErrors.set(`${NODE}/900`, { body: { data: null, message }, status: 500 });
46
+ const run = await lxcEngine(fake).plan(declared(900));
47
47
  expect(run.failure).toContain('got timeout');
48
48
  expect(run.actions).toEqual({});
49
49
  expect(writesOf(fake)).toEqual([]);
@@ -59,7 +59,7 @@ describe('a cold adoption that differs from the live guest fails the plan', () =
59
59
  ['an unprivileged flip', { unprivileged: 0 as const }, 'unprivileged', ['live 1']],
60
60
  [
61
61
  'a smaller mount point',
62
- { mp0: 'tank:subvol-100-disk-0,mp=/data,size=100G' },
62
+ { mp0: 'tank:subvol-900-disk-0,mp=/data,size=100G' },
63
63
  'mp0',
64
64
  ['100G'],
65
65
  ],
@@ -14,17 +14,17 @@ import { judge } from './lxc-judge.ts';
14
14
  import { type LxcProps, storedConfig } from './lxc-props.ts';
15
15
  import { judgeVolume } from './lxc-volume.ts';
16
16
 
17
- const LIVE_MP = 'tank:subvol-100-disk-0,mp=/data,size=200G';
17
+ const LIVE_MP = 'tank:subvol-900-disk-0,mp=/data,size=200G';
18
18
  const props = (over: Record<string, unknown>): LxcProps =>
19
- ({ node: 'pve1', target: TARGET, vmid: 100, ...over }) as unknown as LxcProps;
19
+ ({ node: 'pve1', target: TARGET, vmid: 900, ...over }) as unknown as LxcProps;
20
20
  const live = (config: Record<string, unknown>) => ({ unprivileged: 1, ...config });
21
21
 
22
22
  describe('mount points', () => {
23
23
  test('another volume on the same storage is refused, never written', () => {
24
- const verdict = judgeVolume('mp0', 'tank:subvol-100-disk-7,mp=/data,size=200G', LIVE_MP);
24
+ const verdict = judgeVolume('mp0', 'tank:subvol-900-disk-7,mp=/data,size=200G', LIVE_MP);
25
25
  expect(verdict.refuse).toMatch(/detach the live volume to unusedN/);
26
26
  expect(verdict.put).toBeUndefined();
27
- const change = judge(props({ mp0: 'tank:subvol-100-disk-7,mp=/data' }), live({ mp0: LIVE_MP }));
27
+ const change = judge(props({ mp0: 'tank:subvol-900-disk-7,mp=/data' }), live({ mp0: LIVE_MP }));
28
28
  expect(change.put).toEqual({});
29
29
  expect(change.refuse[0]).toMatch(/unusedN/);
30
30
  });
@@ -32,7 +32,7 @@ describe('mount points', () => {
32
32
  test('a bind mount is root@pam only, new or changed', () => {
33
33
  const added = judge(props({ mp1: '/srv/share,mp=/share' }), live({}));
34
34
  expect(added.put).toEqual({});
35
- expect(added.refuse[0]).toMatch(/pct set 100 --mp1/);
35
+ expect(added.refuse[0]).toMatch(/pct set 900 --mp1/);
36
36
  const bind = '/srv/share,mp=/share';
37
37
  expect(judgeVolume('mp1', `${bind},ro=1`, bind)).toEqual({ rootOnly: true });
38
38
  expect(judgeVolume('mp1', bind, bind)).toEqual({});
@@ -41,13 +41,13 @@ describe('mount points', () => {
41
41
  test('replicate defaults ON: an explicit replicate=0 is a value, replicate=1 is not', () => {
42
42
  expect(judgeVolume('mp0', `${LIVE_MP},replicate=1`, LIVE_MP)).toEqual({});
43
43
  const off = judgeVolume('mp0', LIVE_MP, `${LIVE_MP},replicate=0`);
44
- expect(off.put).toBe('tank:subvol-100-disk-0,mp=/data,size=200G');
44
+ expect(off.put).toBe('tank:subvol-900-disk-0,mp=/data,size=200G');
45
45
  });
46
46
 
47
47
  test('options and growth together: the PUT carries the live size, the resize the new one', () => {
48
48
  const verdict = judgeVolume('mp0', 'tank:300,mp=/data,backup=1', LIVE_MP);
49
49
  expect(verdict).toEqual({
50
- put: 'tank:subvol-100-disk-0,mp=/data,backup=1,size=200G',
50
+ put: 'tank:subvol-900-disk-0,mp=/data,backup=1,size=200G',
51
51
  resize: '300G',
52
52
  });
53
53
  });
@@ -57,7 +57,7 @@ describe('a create allocates new volumes only', () => {
57
57
  test('an existing volume id in a create is refused; the new-disk spelling is not', () => {
58
58
  const base = { ostemplate: 'local:vztmpl/t.tar.zst' };
59
59
  const existing = createRefusals(
60
- props({ ...base, rootfs: 'local-zfs:subvol-100-disk-0,size=8G' }),
60
+ props({ ...base, rootfs: 'local-zfs:subvol-900-disk-0,size=8G' }),
61
61
  );
62
62
  expect(existing.join('\n')).toMatch(/rootfs: .* names an existing volume/);
63
63
  expect(
@@ -84,7 +84,7 @@ describe('keys the resource does not manage', () => {
84
84
  mp0: LIVE_MP,
85
85
  parent: 'snap1',
86
86
  unprivileged: 1,
87
- unused0: 'tank:subvol-100-disk-9',
87
+ unused0: 'tank:subvol-900-disk-9',
88
88
  };
89
89
  expect(storedConfig(read)).toEqual({
90
90
  hostname: 'x',
@@ -14,7 +14,7 @@
14
14
  * ⛔ AN EXISTING VOLUME IS NEVER WRITTEN WITH THE NEW-DISK SPELLING. `tank:100` in a PUT means
15
15
  * "allocate a fresh 100 GiB volume", and PVE then moves the volume the guest was using to
16
16
  * `unusedN`. So a declared `tank:100,mp=/data` that matches a live
17
- * `tank:subvol-100-disk-1,mp=/data,size=100G` compares EQUAL, and an options change on it is
17
+ * `tank:subvol-900-disk-1,mp=/data,size=100G` compares EQUAL, and an options change on it is
18
18
  * written with the LIVE volume id — the declaration's spelling is never sent back for a volume
19
19
  * that exists.
20
20
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * ★ EVERY RULE HERE IS READ OFF pve-container 6.1.14 (the PVE 9 line, `src/PVE/LXC/Config.pm` at
5
5
  * git.proxmox.com HEAD, 2026-09-21) or off a live guest's config read with
6
- * `pvesh get /nodes/<node>/lxc/100/config`. A rule nobody measured is a forever-diff waiting to
6
+ * `pvesh get /nodes/<node>/lxc/<vmid>/config`. A rule nobody measured is a forever-diff waiting to
7
7
  * happen: the plan reports an update, the deploy writes the same value back, and the next plan
8
8
  * reports it again.
9
9
  *