@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
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `declareProvisionBaseline` through Alchemy's real plan and apply, over a fake PVE serving what the
3
+ * generated bootstrap made (provision-cli-fake.ts `apiView`) — the proof that the two halves are
4
+ * one baseline: after the bootstrap, the declaration adopts every object and writes NOTHING.
5
+ * Also pinned: a hand edit shows up as a diff; every resource retains; `adopt` is piped only when
6
+ * asked; and a refused name fails before anything is declared.
7
+ */
8
+ import { describe, expect, test } from 'bun:test';
9
+ import { push } from 'alchemy';
10
+ import { Stack } from 'alchemy/Stack';
11
+ import * as Effect from 'effect/Effect';
12
+ import * as Layer from 'effect/Layer';
13
+ import { engineOver } from '../verify/fake-engine.ts';
14
+ import { ProxmoxAclProvider } from './acl.ts';
15
+ import { FAKE_TARGET, fakePve, withoutBao } from './fake-pve.ts';
16
+ import { ProxmoxGroupProvider } from './group.ts';
17
+ import { type CliState, apiView, freshCluster, runScript } from './provision-cli-fake.ts';
18
+ import { provisionBootstrap } from './provision-bootstrap.ts';
19
+ import { type DeclareProvisionOptions, declareProvisionBaseline } from './provision-declare.ts';
20
+ import { ProxmoxRoleProvider } from './role.ts';
21
+ import { ProxmoxUserProvider } from './user.ts';
22
+
23
+ const ALL = [
24
+ 'pve-role',
25
+ 'pve-mint-group',
26
+ 'pve-provision-user',
27
+ 'pve-read-user',
28
+ 'pve-provision-grant',
29
+ 'pve-read-grant',
30
+ ];
31
+
32
+ const bootstrapped = async (): Promise<CliState> => {
33
+ const run = await runScript(provisionBootstrap(), freshCluster());
34
+ if (run.code !== 0) throw new Error(`bootstrap failed: ${run.stderr}`);
35
+ return run.state;
36
+ };
37
+
38
+ const engineOn = (state: CliState) => {
39
+ const live = apiView(state);
40
+ const fake = fakePve((call) => (call.method === 'GET' ? live[call.path] : 'UPID:fake'));
41
+ const providers = Layer.mergeAll(
42
+ ProxmoxAclProvider(),
43
+ ProxmoxGroupProvider(),
44
+ ProxmoxRoleProvider(),
45
+ ProxmoxUserProvider(),
46
+ );
47
+ return { engine: engineOver(providers.pipe(Layer.provideMerge(fake.layer))), fake };
48
+ };
49
+
50
+ describe('after the bootstrap, the declaration is a clean adoption', () => {
51
+ test('every object is adopted, and nothing is written', async () => {
52
+ const { engine, fake } = engineOn(await bootstrapped());
53
+ await withoutBao(async () => {
54
+ const report = await engine.verify(declareProvisionBaseline('pve', FAKE_TARGET));
55
+ expect(report.rows.map((row) => [row.fqn, row.diff, row.ok])).toEqual(
56
+ [...ALL].sort().map((fqn) => [fqn, 'noop', true]),
57
+ );
58
+ const deployed = await engine.deploy(declareProvisionBaseline('pve', FAKE_TARGET));
59
+ expect(deployed).toEqual(Object.fromEntries(ALL.map((fqn) => [fqn, 'adopted'])));
60
+ });
61
+ expect(fake.writes()).toEqual([]);
62
+ });
63
+
64
+ test('a privilege added by hand is a diff on the role, and nothing else', async () => {
65
+ const state = await bootstrapped();
66
+ state.roles['HfProvisioner']?.push('VM.Console');
67
+ const { engine } = engineOn(state);
68
+ await withoutBao(async () => {
69
+ const report = await engine.verify(declareProvisionBaseline('pve', FAKE_TARGET));
70
+ const changed = report.rows.filter((row) => row.diff !== 'noop');
71
+ expect(changed).toEqual([expect.objectContaining({ changed: ['privs'], fqn: 'pve-role' })]);
72
+ });
73
+ });
74
+ });
75
+
76
+ /** Register the declaration under a bare Stack and read back what Alchemy recorded per resource. */
77
+ const registered = async (options?: DeclareProvisionOptions) => {
78
+ const spec = { actions: {}, bindings: {}, name: 'test', resources: {}, stage: 'test' };
79
+ await Effect.runPromise(
80
+ push('lab', declareProvisionBaseline('pve', FAKE_TARGET, {}, options)).pipe(
81
+ Effect.provideService(Stack, spec as never),
82
+ ) as Effect.Effect<unknown>,
83
+ );
84
+ return spec.resources as Record<string, { Adopt?: boolean; RemovalPolicy?: string }>;
85
+ };
86
+
87
+ describe('what the helper registers', () => {
88
+ test('every resource retains, whatever the family default', async () => {
89
+ const resources = await registered();
90
+ expect(Object.keys(resources).sort()).toEqual(ALL.map((id) => `lab/${id}`).sort());
91
+ for (const each of Object.values(resources)) expect(each.RemovalPolicy).toBe('retain');
92
+ });
93
+
94
+ test.each([
95
+ [{ adopt: true }, true],
96
+ [{ adopt: false }, false],
97
+ [{}, undefined],
98
+ ] as const)('adopt %o is recorded as %p on every resource', async (options, expected) => {
99
+ for (const each of Object.values(await registered(options))) expect(each.Adopt).toBe(expected);
100
+ });
101
+
102
+ test('a refused name fails before anything is declared', async () => {
103
+ const spec = { actions: {}, bindings: {}, name: 'test', resources: {}, stage: 'test' };
104
+ const declared = declareProvisionBaseline('pve', FAKE_TARGET, { role: 'bad role' });
105
+ const exit = await Effect.runPromiseExit(
106
+ declared.pipe(Effect.provideService(Stack, spec as never)) as Effect.Effect<unknown, unknown>,
107
+ );
108
+ expect(String(exit)).toContain('role `bad role` is not a PVE role id');
109
+ expect(spec.resources).toEqual({});
110
+ });
111
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `declareProvisionBaseline` — the provisioning baseline (provision-baseline.ts) as resources, so a
3
+ * stack adopts what the bootstrap made and keeps it: the role, the mint group, one user per lane
4
+ * (its group membership included) and the grant binding each user to its role on `/`.
5
+ *
6
+ * const lane = yield* declareProvisionBaseline('pve', target);
7
+ * // names are generic by default; a site passes its own:
8
+ * const lane = yield* declareProvisionBaseline('pve', target, { role: 'Provisioner' });
9
+ *
10
+ * Provide `ProxmoxRoleProvider()`, `ProxmoxGroupProvider()`, `ProxmoxUserProvider()` and
11
+ * `ProxmoxAclProvider()` with `FetchHttpClient.layer`.
12
+ *
13
+ * ⛔ EVERY RESOURCE HERE RETAINS. This is the branch the whole package sits on (user.ts): dropping
14
+ * the call from a stack must drop state, never the role, the users or their grants — deleting the
15
+ * provision user revokes every lease at once, the plan that would recreate it included.
16
+ * ⛔ ADOPTION IS THE OPERATOR'S TO STATE, AS EVERYWHERE IN THE KIT (docs/ownership.md). The first
17
+ * deploy after the bootstrap finds all of this live with no state: run it with `--adopt`, or pass
18
+ * `{ adopt: true }`, which pipes `adopt(true)` onto each resource. `{ adopt: false }` pins them
19
+ * refused under a deploy-wide `--adopt`. Left out, the deploy's own policy decides.
20
+ * ⚠️ TODAY THE FOUR FAMILIES STILL READ A LIVE OBJECT WITH NO STATE AS OURS (the `pveHandlers`
21
+ * limit in ownership.md), so the flag is not yet what stops a claim. It is stated here so that
22
+ * when they follow the rule, a stack built on this helper already says what it means.
23
+ * ★ THE BOOTSTRAP MAKES EXACTLY THIS, so a clean adoption writes nothing: the same privileges, the
24
+ * same comment, the same single group per user (provision-bootstrap.ts reads the same baseline).
25
+ * ★ PLAIN NAMES, NOT ONE RESOURCE'S OUTPUT IN ANOTHER'S PROPS. An Output would order the creates,
26
+ * but Alchemy skips the adoption probe while any prop is one (Plan.ts), so the first deploy after
27
+ * the bootstrap would plan `create` for objects that are all there. Order never matters here:
28
+ * nothing in this package can run before the bootstrap, and after it everything exists.
29
+ */
30
+ import { adopt } from 'alchemy/AdoptPolicy';
31
+ import * as RemovalPolicy from 'alchemy/RemovalPolicy';
32
+ import * as Effect from 'effect/Effect';
33
+ import { ProxmoxAcl } from './acl.ts';
34
+ import type { PveTarget } from './credentials.ts';
35
+ import { ProxmoxGroup } from './group.ts';
36
+ import { type ProvisionNames, provisionBaseline } from './provision-baseline.ts';
37
+ import { ProxmoxRole } from './role.ts';
38
+ import { ProxmoxUser } from './user.ts';
39
+
40
+ export interface DeclareProvisionOptions {
41
+ /** `true`/`false` pipe `adopt(…)` onto every resource; omitted, the deploy's policy decides. */
42
+ readonly adopt?: boolean;
43
+ }
44
+
45
+ /**
46
+ * The baseline for one cluster. `id` prefixes every logical id (`<id>-role`, `<id>-mint-group`,
47
+ * `<id>-provision-user`, `<id>-provision-grant`, and the `read` pair), so one stack can declare a
48
+ * baseline per cluster. `users` and `grants` come back in `baseline.users` / `baseline.grants`
49
+ * order: provision, then read. Fails, before declaring anything, on a name the baseline refuses.
50
+ */
51
+ export const declareProvisionBaseline = (
52
+ id: string,
53
+ target: PveTarget,
54
+ names: ProvisionNames = {},
55
+ options: DeclareProvisionOptions = {},
56
+ ) =>
57
+ Effect.gen(function* () {
58
+ const baseline = yield* Effect.try({
59
+ catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
60
+ try: () => provisionBaseline(names),
61
+ });
62
+ const owned = <A, R>(resource: Effect.Effect<A, never, R>) => {
63
+ const kept = resource.pipe(RemovalPolicy.retain());
64
+ return options.adopt === undefined ? kept : kept.pipe(adopt(options.adopt));
65
+ };
66
+ const role = yield* owned(
67
+ ProxmoxRole(`${id}-role`, {
68
+ privs: [...baseline.role.privs],
69
+ roleid: baseline.role.roleid,
70
+ target,
71
+ }),
72
+ );
73
+ const group = yield* owned(ProxmoxGroup(`${id}-mint-group`, { ...baseline.group, target }));
74
+ // ★ `Effect.all` over mapped declarations (sequential), as cloudflare-permission-groups.ts does:
75
+ // oxlint's unicorn/no-array-for-each cannot tell `Effect.forEach` from `Array#forEach`.
76
+ const users = yield* Effect.all(
77
+ baseline.users.map((user) =>
78
+ owned(
79
+ ProxmoxUser(`${id}-${user.lane}-user`, {
80
+ comment: user.comment,
81
+ groups: [...user.groups],
82
+ target,
83
+ userid: user.userid,
84
+ }),
85
+ ),
86
+ ),
87
+ );
88
+ const grants = yield* Effect.all(
89
+ baseline.grants.map((grant) =>
90
+ owned(
91
+ ProxmoxAcl(`${id}-${grant.lane}-grant`, {
92
+ path: '/',
93
+ propagate: true,
94
+ roleid: grant.roleid,
95
+ target,
96
+ type: 'user',
97
+ ugid: grant.userid,
98
+ }),
99
+ ),
100
+ ),
101
+ );
102
+ return { baseline, grants, group, role, users };
103
+ });
@@ -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
@@ -71,9 +71,9 @@ export type WithPbsTarget = { target: PbsTarget };
71
71
  export type WithApiTarget = { target: ApiTarget };
72
72
 
73
73
  export type PveSpec<Props extends WithApiTarget, Attributes> = {
74
- /** `pools/house`, `nodes/n2/lxc/101` — where ONE object is read, updated and deleted. */
74
+ /** `pools/lab`, `nodes/node-b/lxc/101` — where ONE object is read, updated and deleted. */
75
75
  readonly path: (props: Props) => string;
76
- /** `pools`, `nodes/n2/lxc` — where a NEW one is POSTed. */
76
+ /** `pools`, `nodes/node-b/lxc` — where a NEW one is POSTed. */
77
77
  readonly collection: (props: Props) => string;
78
78
  /** Live JSON to attributes. Returning undefined means "this is not really there". */
79
79
  readonly attributes: (live: Record<string, unknown>, props: Props) => Attributes | undefined;
@@ -164,7 +164,7 @@ export const pveOperations = <Props extends WithApiTarget, Attributes>(
164
164
  * `Proxmox.NodeNetwork` it is worse than pointless: a PUT under `/nodes/{node}/network`
165
165
  * STAGES a change into `/etc/network/interfaces.new`, so merely adopting the interface
166
166
  * a node already has would leave that node with a pending network change waiting for an
167
- * apply — on a cluster whose Ceph traffic rides vmbr1.11. Adoption has to be free.
167
+ * apply — on a cluster whose Ceph traffic rides vmbr1.42. Adoption has to be free.
168
168
  *
169
169
  * ★ SO THE CONDITION IS THE SAME PREDICATE `diff` USES. If `matches` is true the object
170
170
  * already says what the declaration says, and there is nothing to write — by
@@ -2,7 +2,7 @@
2
2
  * The fabric comparison, which is what decides whether `Proxmox.SdnApply` publishes.
3
3
  *
4
4
  * ⛔ IT IS TESTED HERE RATHER THAN AGAINST THE CLUSTER BECAUSE STAGING A REAL FABRIC CHANGE MEANS
5
- * TOUCHING CEPH'S CLUSTER NETWORK. TB4's OSPF fabric pins 10.100.0.102/103/104 to en05 and en06,
5
+ * TOUCHING CEPH'S CLUSTER NETWORK. C1's OSPF fabric pins 203.0.113.102/103/104 to tb0 and tb1,
6
6
  * and those are the addresses `cluster_network` runs over. The staged-zone path IS proven end to
7
7
  * end against the live cluster (plan noop -> stage a zone -> plan update -> remove -> plan noop);
8
8
  * the fabric path is proven by construction, with the two ways it could lie pinned below.
@@ -20,20 +20,20 @@ import { UNDIFFABLE, canonical, fabricStaged, subsystemAbsent } from './sdn-appl
20
20
 
21
21
  /** The shape `cluster/sdn/fabrics/all` actually returns, trimmed to what matters. */
22
22
  const N2 = {
23
- fabric_id: 'tb4',
24
- interfaces: ['name=en05', 'name=en06'],
25
- ip: '10.100.0.102',
26
- node_id: 'n2',
23
+ fabric_id: 'c1',
24
+ interfaces: ['name=tb0', 'name=tb1'],
25
+ ip: '203.0.113.102',
26
+ node_id: 'node-b',
27
27
  };
28
28
  const N3 = {
29
- fabric_id: 'tb4',
30
- interfaces: ['name=en05', 'name=en06'],
31
- ip: '10.100.0.103',
32
- node_id: 'n3',
29
+ fabric_id: 'c1',
30
+ interfaces: ['name=tb0', 'name=tb1'],
31
+ ip: '203.0.113.103',
32
+ node_id: 'node-c',
33
33
  };
34
34
 
35
35
  const running = {
36
- fabrics: [{ area: '1', id: 'tb4', ip_prefix: '10.100.0.0/24', protocol: 'ospf' }],
36
+ fabrics: [{ area: '1', id: 'c1', ip_prefix: '203.0.113.0/24', protocol: 'ospf' }],
37
37
  nodes: [N2, N3],
38
38
  };
39
39
 
@@ -44,8 +44,8 @@ describe('fabric canonicalisation', () => {
44
44
  {
45
45
  area: '1',
46
46
  digest: '7b94d363ff69',
47
- id: 'tb4',
48
- ip_prefix: '10.100.0.0/24',
47
+ id: 'c1',
48
+ ip_prefix: '203.0.113.0/24',
49
49
  protocol: 'ospf',
50
50
  },
51
51
  ],
@@ -76,7 +76,7 @@ describe('fabric canonicalisation', () => {
76
76
  it('sees a changed node address', () => {
77
77
  const moved = {
78
78
  ...running,
79
- nodes: [{ ...N2, ip: '10.100.0.199' }, N3],
79
+ nodes: [{ ...N2, ip: '203.0.113.199' }, N3],
80
80
  };
81
81
  assert.notEqual(canonical(moved), canonical(running));
82
82
  });
@@ -86,7 +86,7 @@ describe('fabric canonicalisation', () => {
86
86
  ...running,
87
87
  nodes: [
88
88
  ...running.nodes,
89
- { fabric_id: 'tb4', interfaces: ['name=en05'], ip: '10.100.0.104', node_id: 'n4' },
89
+ { fabric_id: 'c1', interfaces: ['name=tb0'], ip: '203.0.113.104', node_id: 'node-d' },
90
90
  ],
91
91
  };
92
92
  assert.notEqual(canonical(added), canonical(running));
@@ -95,7 +95,7 @@ describe('fabric canonicalisation', () => {
95
95
  it('sees a changed interface list, which is what decides where the address lands', () => {
96
96
  const rewired = {
97
97
  ...running,
98
- nodes: [{ ...N2, interfaces: ['name=en05'] }, N3],
98
+ nodes: [{ ...N2, interfaces: ['name=tb0'] }, N3],
99
99
  };
100
100
  assert.notEqual(canonical(rewired), canonical(running));
101
101
  });
@@ -135,7 +135,7 @@ describe('what a failed read is allowed to mean', () => {
135
135
  });
136
136
 
137
137
  it('counts an unchanged fabric as nothing staged and a moved node as staged', () => {
138
- const moved = { ...running, nodes: [{ ...N2, ip: '10.100.0.199' }, N3] };
138
+ const moved = { ...running, nodes: [{ ...N2, ip: '203.0.113.199' }, N3] };
139
139
  assert.equal(fabricStaged(canonical(running), canonical(running)), 0);
140
140
  assert.equal(fabricStaged(canonical(moved), canonical(running)), 1);
141
141
  });
@@ -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
@@ -62,7 +62,7 @@ const MARKED = [
62
62
  *
63
63
  * ⚠️ NAMED RATHER THAN SILENTLY SKIPPED. `ipams` and `dns` answer HTTP 400 to both `?pending=1` and
64
64
  * `?running=1`, so there is no way to ask them what is staged. They are a REAL blind spot: an
65
- * ipam edited by hand is published by the apply and cannot be seen from here. TB4 has one ipam
65
+ * ipam edited by hand is published by the apply and cannot be seen from here. C1 has one ipam
66
66
  * (`pve`, the built-in) and zero dns entries, so the blind spot is currently empty — which is a
67
67
  * fact about today, not a property of the design.
68
68
  */
@@ -181,7 +181,7 @@ const sortDeep = (value: unknown): unknown => {
181
181
  *
182
182
  * ⛔ ZERO HERE IS THE ONLY THING THAT MAKES DECLARING THE APPLY SAFE. `reconcile` runs on CREATE —
183
183
  * Alchemy always calls it the first time a resource appears — and `PUT /cluster/sdn` regenerates
184
- * `/etc/network/interfaces.d/sdn` on every node. On TB4 that file carries Ceph's cluster network.
184
+ * `/etc/network/interfaces.d/sdn` on every node. On C1 that file carries Ceph's cluster network.
185
185
  */
186
186
  export const sdnPendingCount = (target: PveTarget) =>
187
187
  Effect.all([markedPending(target), fabricPending(target)], { concurrency: 'unbounded' }).pipe(
@@ -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"}
@@ -109,8 +109,8 @@ export const ProxmoxSdnApplyProvider = () =>
109
109
  *
110
110
  * 🔴 WHAT IT PREVENTS. `reconcile` runs on CREATE, the first time this resource appears
111
111
  * in a stack, before `diff` has ever been consulted. `PUT /cluster/sdn` regenerates
112
- * `/etc/network/interfaces.d/sdn` on EVERY node at once — and on TB4 that file carries
113
- * the OSPF fabric holding Ceph's `cluster_network` on en05/en06. Without this branch,
112
+ * `/etc/network/interfaces.d/sdn` on EVERY node at once — and on C1 that file carries
113
+ * the OSPF fabric holding Ceph's `cluster_network` on tb0/tb1. Without this branch,
114
114
  * adding one line to a stack file reloads networking on all three nodes simultaneously,
115
115
  * which is precisely what the serialised NetworkApply chain exists to prevent.
116
116
  *
@@ -22,7 +22,7 @@ export type DhcpRange = { start: string; end: string };
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
@@ -98,7 +98,7 @@ export interface SdnSubnetProps extends WithTarget {
98
98
  gateway?: string;
99
99
  /**
100
100
  * Masquerade this subnet. ⚠️ INERT UNTIL THE CLUSTER FIREWALL IS ON — PVE's own description is
101
- * "enable masquerade for this subnet if pve-firewall", and TB4's cluster firewall was empty and
101
+ * "enable masquerade for this subnet if pve-firewall", and C1's cluster firewall was empty and
102
102
  * disabled when this was written. It still round-trips, so declaring it does not diff.
103
103
  */
104
104
  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';
@@ -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
@@ -68,7 +69,7 @@ export interface StorageProps extends WithTarget {
68
69
  /** `images,rootdir,vztmpl,iso,backup,snippets`. PVE reports `none` for an empty set. */
69
70
  content?: string;
70
71
  /**
71
- * Node restriction, e.g. `n1,n2`. ⚠️ A STORAGE WITH A `nodes` LIST DOES NOT EXIST ANYWHERE ELSE:
72
+ * Node restriction, e.g. `node-a,node-b`. ⚠️ A STORAGE WITH A `nodes` LIST DOES NOT EXIST ANYWHERE ELSE:
72
73
  * an LXC declared on a node outside it fails at create with "storage not available on node".
73
74
  */
74
75
  nodes?: string;
@@ -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
@@ -99,11 +99,11 @@ describe('num and text', () => {
99
99
 
100
100
  describe('csv — a set PVE does not give back in the order it was handed', () => {
101
101
  it('is order insensitive, so two orderings compare equal', () => {
102
- assert.equal(csv(['n3', 'n2']), csv('n2,n3'));
102
+ assert.equal(csv(['node-c', 'node-b']), csv('node-b,node-c'));
103
103
  });
104
104
 
105
105
  it('trims and drops empties, so a trailing comma is not a phantom member', () => {
106
- assert.equal(csv('n2, n3,'), 'n2,n3');
106
+ assert.equal(csv('node-b, node-c,'), 'node-b,node-c');
107
107
  });
108
108
  });
109
109
 
@@ -168,7 +168,7 @@ export const int = (value: unknown, fallback: number) => {
168
168
  * A PVE update form with its clear-list attached, and the two guaranteed not to overlap.
169
169
  *
170
170
  * ⛔ PVE DIES IF A KEY IS SET AND DELETED IN ONE CALL, AND SIX FAMILIES HERE BUILT THAT FORM BY
171
- * HAND. Measured in the shipped Perl on n2 — `PVE::SectionConfig::delete_from_config`, line 1860:
171
+ * HAND. Measured in the shipped Perl on node-b — `PVE::SectionConfig::delete_from_config`, line 1860:
172
172
  *
173
173
  * die "cannot set and delete property '$k' at the same time!\n"
174
174
  * if defined($new_options->{$k});
@@ -51,7 +51,7 @@ const SETTLE_ATTEMPTS = 30;
51
51
  /**
52
52
  * Create the pool if it is absent, and otherwise leave it completely alone.
53
53
  *
54
- * ⛔ THE POST ONLY FORKS A WORKER. MEASURED in `/usr/share/perl5/PVE/API2/Disks/ZFS.pm` on n2: the
54
+ * ⛔ THE POST ONLY FORKS A WORKER. MEASURED in `/usr/share/perl5/PVE/API2/Disks/ZFS.pm` on node-b: the
55
55
  * create handler ends in `$rpcenv->fork_worker('zfscreate', ...)` and its HTTP answer is a UPID
56
56
  * returned the instant the worker is forked — before `zpool create` has run, let alone finished.
57
57
  * 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
@@ -155,7 +155,7 @@ export interface ZfsPoolAttributes {
155
155
  *
156
156
  * ⚠️ THIS IS WHAT ZFS RESOLVED, NOT WHAT WAS DECLARED, and the two normally differ: PVE rewrites
157
157
  * a `/dev/sdb` into a by-id link before creating, and ZFS reports partition paths (`…-part3`)
158
- * for a pool built on partitions — MEASURED, that is exactly what n2's `rpool` reports. Never
158
+ * for a pool built on partitions — MEASURED, that is exactly what node-b's `rpool` reports. Never
159
159
  * compare it with `props.devices`.
160
160
  */
161
161
  devices: string;
@@ -219,7 +219,7 @@ const spec: PveSpec<ZfsPoolProps, ZfsPoolAttributes> = {
219
219
  * can be compared on: every create parameter is write-only (see the ⛔ in the header) and
220
220
  * everything the read does return is telemetry that moves by itself. So declaring what is live
221
221
  * plans as `noop` — the only honest answer available, and the one the live cluster needs:
222
- * `rpool` exists on n2, n3 and n4 with nothing about its construction readable.
222
+ * `rpool` exists on node-b, node-c and node-d with nothing about its construction readable.
223
223
  * ⛔ ANYTHING ADDED HERE IS A REPLACE, NOT AN UPDATE. `updateForm` is omitted because PVE has
224
224
  * no PUT, so `resource.ts` turns a false into `{action:'replace'}` — delete then create — on
225
225
  * a family whose delete is `zpool destroy`. A comparison added here would be one plan away
@@ -4,9 +4,9 @@
4
4
  * ⛔ TALOSCONFIG AND secrets.yaml HOLD CLIENT CERTIFICATES AND CLUSTER CA PRIVATE KEYS. Alchemy
5
5
  * persists resource attributes WITHOUT encryption — StateEncoding.ts writes `Redacted` as
6
6
  * `{"@redacted": <plaintext>}` — and this estate's state store is the `alchemy` Postgres that
7
- * pg-backup.sh dumps nightly. Neither file may be a prop, an attribute, or a log line.
7
+ * a nightly job dumps. Neither file may be a prop, an attribute, or a log line.
8
8
  *
9
- * ★ MODELED ON house/proxmox/src/credentials.ts: mint at call time through `ChildProcessSpawner`,
9
+ * ★ MODELED ON <estate>/proxmox/src/credentials.ts: mint at call time through `ChildProcessSpawner`,
10
10
  * use the material to build a client, drop it when the scope ends. The difference is Talos wants a
11
11
  * FILE (`--talosconfig` / `TALOSCONFIG`), so mint writes a temp path and registers cleanup.
12
12
  *
@@ -21,7 +21,7 @@ import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawne
21
21
 
22
22
  /** Where credentials come from. HomeFlare-specific mount names live in the stack, not here. */
23
23
  export type TalosTarget = {
24
- /** OpenBao KV mount holding Talos material, e.g. `talos-tb4`. */
24
+ /** OpenBao KV mount holding Talos material, e.g. `talos-c1`. */
25
25
  readonly mount: string;
26
26
  /** Logical cluster name — safe to persist and log. */
27
27
  readonly cluster: string;