@homeflare/alchemy 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/README.md +3 -3
  2. package/dist/caddy/admin-guard.d.ts +1 -1
  3. package/dist/caddy/index.js.map +2 -2
  4. package/dist/cloudflare/client.d.ts +1 -1
  5. package/dist/cloudflare/index.js.map +2 -2
  6. package/dist/cloudflare/lock-rules.d.ts +1 -1
  7. package/dist/forgejo/branch-protection-form.d.ts +1 -1
  8. package/dist/forgejo/branch-protection.d.ts +1 -1
  9. package/dist/forgejo/index.js.map +10 -10
  10. package/dist/forgejo/org-actions-secrets.d.ts +1 -1
  11. package/dist/forgejo/org-label.d.ts +1 -1
  12. package/dist/forgejo/org-team.d.ts +1 -1
  13. package/dist/forgejo/repo-webhook.d.ts +1 -1
  14. package/dist/forgejo/repository.d.ts +2 -2
  15. package/dist/forgejo/repository.d.ts.map +1 -1
  16. package/dist/forgejo/resource.d.ts +2 -2
  17. package/dist/forgejo/team-member.d.ts +1 -1
  18. package/dist/openbao/auth-role.d.ts +1 -1
  19. package/dist/openbao/bao-http.d.ts +4 -4
  20. package/dist/openbao/cloudflare-permission-groups.d.ts +1 -1
  21. package/dist/openbao/cloudflare-role-form.d.ts +2 -2
  22. package/dist/openbao/cloudflare-role-form.d.ts.map +1 -1
  23. package/dist/openbao/cloudflare-roles-expand.d.ts +1 -1
  24. package/dist/openbao/digest.d.ts +1 -1
  25. package/dist/openbao/forgejo-bootstrap-parse.d.ts +2 -2
  26. package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -1
  27. package/dist/openbao/forgejo-bootstrap.d.ts +1 -1
  28. package/dist/openbao/index.js.map +14 -14
  29. package/dist/openbao/mount.d.ts +1 -1
  30. package/dist/openbao/pki-role.d.ts +1 -1
  31. package/dist/openbao/policy.d.ts +2 -2
  32. package/dist/openbao/proxmox-role-form.d.ts +2 -2
  33. package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
  34. package/dist/openbao/proxmox-role.d.ts +10 -10
  35. package/dist/openbao/ssh-role.d.ts +3 -3
  36. package/dist/proxmox/acl.d.ts +2 -2
  37. package/dist/proxmox/acl.d.ts.map +1 -1
  38. package/dist/proxmox/alertmanager-body.d.ts +54 -0
  39. package/dist/proxmox/alertmanager-body.d.ts.map +1 -0
  40. package/dist/proxmox/api-token-form.d.ts +2 -2
  41. package/dist/proxmox/api-token.d.ts +11 -11
  42. package/dist/proxmox/ceph-daemon-form.d.ts +3 -3
  43. package/dist/proxmox/ceph-daemon.d.ts +19 -19
  44. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  45. package/dist/proxmox/ceph-flag.d.ts +5 -5
  46. package/dist/proxmox/ceph-fs-wire.d.ts +5 -5
  47. package/dist/proxmox/ceph-fs.d.ts +16 -16
  48. package/dist/proxmox/ceph-osd-tree.d.ts +16 -16
  49. package/dist/proxmox/ceph-osd.d.ts +5 -5
  50. package/dist/proxmox/ceph-pool-form.d.ts +13 -13
  51. package/dist/proxmox/ceph-pool-form.d.ts.map +1 -1
  52. package/dist/proxmox/ceph-pool.d.ts +3 -3
  53. package/dist/proxmox/client.d.ts +2 -2
  54. package/dist/proxmox/credential-literals.d.ts +36 -0
  55. package/dist/proxmox/credential-literals.d.ts.map +1 -0
  56. package/dist/proxmox/credentials.d.ts +8 -8
  57. package/dist/proxmox/credentials.d.ts.map +1 -1
  58. package/dist/proxmox/fake-pbs-notify.d.ts +23 -0
  59. package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -0
  60. package/dist/proxmox/fake-pve.d.ts +5 -0
  61. package/dist/proxmox/fake-pve.d.ts.map +1 -1
  62. package/dist/proxmox/firewall-alias-form.d.ts +1 -1
  63. package/dist/proxmox/firewall-alias.d.ts +1 -1
  64. package/dist/proxmox/generated/pbs.d.ts +279 -1
  65. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  66. package/dist/proxmox/generated/pve.d.ts +58 -1
  67. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  68. package/dist/proxmox/group.d.ts +8 -8
  69. package/dist/proxmox/ha-resource.d.ts +3 -2
  70. package/dist/proxmox/ha-resource.d.ts.map +1 -1
  71. package/dist/proxmox/ha-rule-form.d.ts +5 -5
  72. package/dist/proxmox/ha-rule.d.ts +3 -3
  73. package/dist/proxmox/index.d.ts +14 -0
  74. package/dist/proxmox/index.d.ts.map +1 -1
  75. package/dist/proxmox/index.js +935 -223
  76. package/dist/proxmox/index.js.map +69 -56
  77. package/dist/proxmox/lease-cache.d.ts +4 -4
  78. package/dist/proxmox/lxc-harness.d.ts +1 -1
  79. package/dist/proxmox/lxc-props.d.ts +2 -2
  80. package/dist/proxmox/lxc.d.ts +3 -3
  81. package/dist/proxmox/members.d.ts +3 -3
  82. package/dist/proxmox/network-apply-read.d.ts +5 -5
  83. package/dist/proxmox/network-apply.d.ts +9 -9
  84. package/dist/proxmox/node-network-form.d.ts +6 -6
  85. package/dist/proxmox/node-network.d.ts +17 -17
  86. package/dist/proxmox/node-network.d.ts.map +1 -1
  87. package/dist/proxmox/notification-matcher-form.d.ts +100 -0
  88. package/dist/proxmox/notification-matcher-form.d.ts.map +1 -0
  89. package/dist/proxmox/notification-matcher.d.ts +46 -0
  90. package/dist/proxmox/notification-matcher.d.ts.map +1 -0
  91. package/dist/proxmox/notification-target.d.ts +3 -2
  92. package/dist/proxmox/notification-target.d.ts.map +1 -1
  93. package/dist/proxmox/pbs-datastore-guard.d.ts +1 -1
  94. package/dist/proxmox/pbs-datastore.d.ts +10 -10
  95. package/dist/proxmox/pbs-notification-matcher.d.ts +53 -0
  96. package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -0
  97. package/dist/proxmox/pbs-notification-target-form.d.ts +63 -0
  98. package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -0
  99. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +100 -0
  100. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -0
  101. package/dist/proxmox/pbs-notification-target-wire.d.ts +30 -0
  102. package/dist/proxmox/pbs-notification-target-wire.d.ts.map +1 -0
  103. package/dist/proxmox/pbs-notification-target.d.ts +123 -0
  104. package/dist/proxmox/pbs-notification-target.d.ts.map +1 -0
  105. package/dist/proxmox/pbs-prune-job.d.ts +2 -2
  106. package/dist/proxmox/pbs-sync-job.d.ts +4 -4
  107. package/dist/proxmox/pbs-verify-job.d.ts +1 -1
  108. package/dist/proxmox/pool.d.ts +4 -4
  109. package/dist/proxmox/provision-baseline.d.ts +74 -0
  110. package/dist/proxmox/provision-baseline.d.ts.map +1 -0
  111. package/dist/proxmox/provision-bootstrap.d.ts +33 -0
  112. package/dist/proxmox/provision-bootstrap.d.ts.map +1 -0
  113. package/dist/proxmox/provision-cli-fake.d.ts +38 -0
  114. package/dist/proxmox/provision-cli-fake.d.ts.map +1 -0
  115. package/dist/proxmox/provision-declare.d.ts +25 -0
  116. package/dist/proxmox/provision-declare.d.ts.map +1 -0
  117. package/dist/proxmox/replication-job.d.ts +9 -8
  118. package/dist/proxmox/replication-job.d.ts.map +1 -1
  119. package/dist/proxmox/resource.d.ts +6 -5
  120. package/dist/proxmox/resource.d.ts.map +1 -1
  121. package/dist/proxmox/sdn-apply-read.d.ts +7 -7
  122. package/dist/proxmox/sdn-apply.d.ts +1 -1
  123. package/dist/proxmox/sdn-subnet-form.d.ts +1 -1
  124. package/dist/proxmox/sdn-subnet.d.ts +5 -5
  125. package/dist/proxmox/sdn-vnet.d.ts +5 -6
  126. package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
  127. package/dist/proxmox/storage.d.ts +4 -3
  128. package/dist/proxmox/storage.d.ts.map +1 -1
  129. package/dist/proxmox/user.d.ts +3 -2
  130. package/dist/proxmox/user.d.ts.map +1 -1
  131. package/dist/proxmox/values.d.ts +1 -1
  132. package/dist/proxmox/write-only.d.ts +35 -0
  133. package/dist/proxmox/write-only.d.ts.map +1 -0
  134. package/dist/proxmox/zfs-pool-write.d.ts +1 -1
  135. package/dist/proxmox/zfs-pool.d.ts +5 -5
  136. package/dist/talos/credentials.d.ts +1 -1
  137. package/dist/talos/credentials.d.ts.map +1 -1
  138. package/dist/talos/index.js.map +1 -1
  139. package/dist/verify/fake-engine.d.ts +5 -0
  140. package/dist/verify/fake-engine.d.ts.map +1 -1
  141. package/docs/adopt-verify.md +21 -5
  142. package/docs/adopted-deploys.md +9 -1
  143. package/docs/ownership.md +26 -1
  144. package/docs/pbs-alertmanager-body.md +73 -0
  145. package/docs/pbs-notifications.md +163 -0
  146. package/docs/provision-baseline.md +106 -0
  147. package/docs/proxmox-lxc-adopt.md +2 -2
  148. package/docs/proxmox-lxc.md +5 -4
  149. package/docs/proxmox.md +9 -0
  150. package/package.json +1 -1
  151. package/src/caddy/admin-guard.ts +1 -1
  152. package/src/caddy/local-admin.ts +1 -1
  153. package/src/cloudflare/client.ts +1 -1
  154. package/src/cloudflare/lock-rules.ts +1 -1
  155. package/src/forgejo/branch-protection-form.ts +1 -1
  156. package/src/forgejo/branch-protection.ts +1 -1
  157. package/src/forgejo/client.ts +1 -1
  158. package/src/forgejo/org-actions-secrets.ts +1 -1
  159. package/src/forgejo/org-label.ts +1 -1
  160. package/src/forgejo/org-team.ts +1 -1
  161. package/src/forgejo/repo-webhook.ts +1 -1
  162. package/src/forgejo/repository.ts +2 -2
  163. package/src/forgejo/resource.ts +2 -2
  164. package/src/forgejo/team-member.ts +1 -1
  165. package/src/openbao/README.md +4 -4
  166. package/src/openbao/auth-role.ts +1 -1
  167. package/src/openbao/bao-http.test.ts +2 -2
  168. package/src/openbao/bao-http.ts +6 -6
  169. package/src/openbao/bao-status.test.ts +1 -1
  170. package/src/openbao/bao-status.ts +1 -1
  171. package/src/openbao/cloudflare-group-scope.test.ts +1 -1
  172. package/src/openbao/cloudflare-permission-groups.test.ts +1 -1
  173. package/src/openbao/cloudflare-permission-groups.ts +3 -3
  174. package/src/openbao/cloudflare-policy.ts +1 -1
  175. package/src/openbao/cloudflare-role-form.ts +2 -2
  176. package/src/openbao/cloudflare-roles-expand.ts +1 -1
  177. package/src/openbao/digest.ts +1 -1
  178. package/src/openbao/forgejo-bootstrap-parse.test.ts +3 -3
  179. package/src/openbao/forgejo-bootstrap-parse.ts +2 -2
  180. package/src/openbao/forgejo-bootstrap.ts +4 -4
  181. package/src/openbao/kubernetes-role-form.test.ts +5 -5
  182. package/src/openbao/mount.ts +1 -1
  183. package/src/openbao/pki-role.ts +1 -1
  184. package/src/openbao/policy-wire.test.ts +3 -3
  185. package/src/openbao/policy.ts +2 -2
  186. package/src/openbao/proxmox-role-form.ts +5 -5
  187. package/src/openbao/proxmox-role.ts +11 -11
  188. package/src/openbao/ssh-role.ts +3 -3
  189. package/src/proxmox/acl.ts +4 -4
  190. package/src/proxmox/adopt-noop.test.ts +6 -6
  191. package/src/proxmox/alertmanager-body.ts +125 -0
  192. package/src/proxmox/api-token-form.ts +4 -4
  193. package/src/proxmox/api-token.ts +13 -13
  194. package/src/proxmox/ceph-daemon-form.ts +3 -3
  195. package/src/proxmox/ceph-daemon.ts +20 -20
  196. package/src/proxmox/ceph-flag.ts +7 -7
  197. package/src/proxmox/ceph-fs-wire.ts +7 -7
  198. package/src/proxmox/ceph-fs.ts +18 -18
  199. package/src/proxmox/ceph-osd-tree.ts +16 -16
  200. package/src/proxmox/ceph-osd.ts +6 -6
  201. package/src/proxmox/ceph-pool-adopt.test.ts +14 -14
  202. package/src/proxmox/ceph-pool-form.ts +13 -13
  203. package/src/proxmox/ceph-pool.ts +6 -6
  204. package/src/proxmox/client.test.ts +3 -3
  205. package/src/proxmox/client.ts +3 -3
  206. package/src/proxmox/credential-literals.test.ts +148 -0
  207. package/src/proxmox/credential-literals.ts +74 -0
  208. package/src/proxmox/credentials.test.ts +6 -8
  209. package/src/proxmox/credentials.ts +8 -8
  210. package/src/proxmox/fake-pbs-notify.ts +108 -0
  211. package/src/proxmox/fake-pve.ts +12 -2
  212. package/src/proxmox/firewall-alias-form.ts +1 -1
  213. package/src/proxmox/firewall-alias.ts +1 -1
  214. package/src/proxmox/generated/pbs.ts +330 -1
  215. package/src/proxmox/generated/pve.ts +63 -1
  216. package/src/proxmox/group.ts +10 -10
  217. package/src/proxmox/ha-resource.ts +3 -2
  218. package/src/proxmox/ha-rule-form.ts +5 -5
  219. package/src/proxmox/ha-rule.ts +3 -3
  220. package/src/proxmox/index.test.ts +10 -0
  221. package/src/proxmox/index.ts +32 -0
  222. package/src/proxmox/lease-cache.test.ts +19 -19
  223. package/src/proxmox/lease-cache.ts +4 -4
  224. package/src/proxmox/lxc-adopt.test.ts +3 -3
  225. package/src/proxmox/lxc-create-form.ts +1 -1
  226. package/src/proxmox/lxc-guards.test.ts +4 -4
  227. package/src/proxmox/lxc-harness.ts +3 -3
  228. package/src/proxmox/lxc-judge.test.ts +5 -5
  229. package/src/proxmox/lxc-lifecycle.ts +1 -1
  230. package/src/proxmox/lxc-props.ts +2 -2
  231. package/src/proxmox/lxc-read.test.ts +3 -3
  232. package/src/proxmox/lxc-strict-adopt.test.ts +1 -1
  233. package/src/proxmox/lxc-volume.test.ts +9 -9
  234. package/src/proxmox/lxc-volume.ts +1 -1
  235. package/src/proxmox/lxc-wire.ts +1 -1
  236. package/src/proxmox/lxc.ts +3 -3
  237. package/src/proxmox/members.test.ts +1 -1
  238. package/src/proxmox/members.ts +3 -3
  239. package/src/proxmox/metric-server-otel.ts +2 -2
  240. package/src/proxmox/network-apply-read.ts +10 -10
  241. package/src/proxmox/network-apply.ts +10 -10
  242. package/src/proxmox/node-network-form.ts +7 -7
  243. package/src/proxmox/node-network.ts +18 -18
  244. package/src/proxmox/notification-matcher-form.ts +221 -0
  245. package/src/proxmox/notification-matcher.test.ts +234 -0
  246. package/src/proxmox/notification-matcher.ts +76 -0
  247. package/src/proxmox/notification-target.ts +3 -2
  248. package/src/proxmox/pbs-datastore-guard.ts +1 -1
  249. package/src/proxmox/pbs-datastore.ts +10 -10
  250. package/src/proxmox/pbs-notification-matcher.ts +81 -0
  251. package/src/proxmox/pbs-notification-target-drift.test.ts +83 -0
  252. package/src/proxmox/pbs-notification-target-form.ts +229 -0
  253. package/src/proxmox/pbs-notification-target-lifecycle.ts +177 -0
  254. package/src/proxmox/pbs-notification-target-state.test.ts +191 -0
  255. package/src/proxmox/pbs-notification-target-wire.ts +112 -0
  256. package/src/proxmox/pbs-notification-target.test.ts +226 -0
  257. package/src/proxmox/pbs-notification-target.ts +139 -0
  258. package/src/proxmox/pbs-notifications-stack.test.ts +94 -0
  259. package/src/proxmox/pbs-prune-job.ts +2 -2
  260. package/src/proxmox/pbs-sync-job.ts +4 -4
  261. package/src/proxmox/pbs-verify-job.ts +1 -1
  262. package/src/proxmox/pool.test.ts +2 -2
  263. package/src/proxmox/pool.ts +4 -4
  264. package/src/proxmox/provision-baseline.test.ts +87 -0
  265. package/src/proxmox/provision-baseline.ts +162 -0
  266. package/src/proxmox/provision-bootstrap.test.ts +132 -0
  267. package/src/proxmox/provision-bootstrap.ts +135 -0
  268. package/src/proxmox/provision-cli-fake.ts +217 -0
  269. package/src/proxmox/provision-declare.test.ts +111 -0
  270. package/src/proxmox/provision-declare.ts +103 -0
  271. package/src/proxmox/replication-job.ts +9 -8
  272. package/src/proxmox/resource.ts +9 -9
  273. package/src/proxmox/sdn-apply-read.test.ts +16 -16
  274. package/src/proxmox/sdn-apply-read.ts +7 -7
  275. package/src/proxmox/sdn-apply.ts +3 -3
  276. package/src/proxmox/sdn-subnet-form.ts +1 -1
  277. package/src/proxmox/sdn-subnet.ts +5 -5
  278. package/src/proxmox/sdn-vnet.ts +5 -6
  279. package/src/proxmox/storage.ts +4 -3
  280. package/src/proxmox/user.ts +3 -2
  281. package/src/proxmox/values.test.ts +2 -2
  282. package/src/proxmox/values.ts +1 -1
  283. package/src/proxmox/write-only.test.ts +75 -0
  284. package/src/proxmox/write-only.ts +106 -0
  285. package/src/proxmox/zfs-pool-write.ts +1 -1
  286. package/src/proxmox/zfs-pool.ts +6 -6
  287. package/src/talos/credentials.ts +3 -3
  288. package/src/talos/values.test.ts +9 -9
  289. package/src/verify/cli.test.ts +1 -1
  290. package/src/verify/fake-engine.ts +8 -1
@@ -0,0 +1,125 @@
1
+ /**
2
+ * A PBS webhook body that posts one Alertmanager v2 alert — `POST /api/v2/alerts` takes a JSON
3
+ * ARRAY of `postableAlert`, each with a required `labels` map and an optional `annotations` map,
4
+ * every value a string (Alertmanager's api/v2/openapi.yaml, read at main 2026-09-22).
5
+ *
6
+ * ★ PURE TEXT, NOT A RENDERER. PBS renders it, with Handlebars, at send time: `{{ severity }}`,
7
+ * `{{ title }}`, `{{ message }}`, `{{ timestamp }}` and `{{ fields.<name> }}` are the
8
+ * notification, and `json`, `escape` and `url-encode` are the only helpers
9
+ * (proxmox-notify endpoints/webhook.rs `setup_handlebars`, read at HEAD 2026-09-22).
10
+ *
11
+ * ⛔ THERE IS NO ESCAPING UNLESS THE TEMPLATE ASKS FOR IT. The webhook's Handlebars registers
12
+ * `no_escape` ("There is no escape."), so `"{{ message }}"` breaks the JSON the first time a
13
+ * task log holds a quote or a newline — and a failed job's message is exactly a task log. Every
14
+ * value below goes through `json`, which emits a complete, quoted JSON string.
15
+ * ⛔ A FIELD THE NOTIFICATION DOES NOT CARRY MUST NOT BE REACHED FOR BARE. `escape` on a missing
16
+ * field fails the whole render, and the notification is then lost — not degraded. MEASURED
17
+ * (2026-09-22) with handlebars-rust 5.1.2 and the webhook's three helpers restated from source:
18
+ * `{{ escape fields.job-id }}` over no fields errors "param at index 0 required but not found";
19
+ * `{{ json fields.job-id }}` renders `null`, valid JSON but not a label value. Garbage
20
+ * collection has no `job-id`, and the UI's "Test" button sends a notification with NO fields at
21
+ * all (lib.rs `test_target`). So every field-derived label sits inside
22
+ * `{{#if}}`, ends with its own comma, and the labels after them are always there: whichever
23
+ * fields are present, the object stays valid JSON.
24
+ *
25
+ * ⚠️ ALERTMANAGER RESOLVES IT BY ITSELF. With no `endsAt` it sets `now + resolve_timeout` (5m by
26
+ * default, api/v2/api.go `postAlertsHandler`), and PBS never sends a resolve — so a receiver
27
+ * with `send_resolved: true` reports "resolved" five minutes after a failure that nobody fixed.
28
+ * Page on the firing notification; do not read the resolve as a recovery.
29
+ * ⚠️ `severity` IS PBS'S WORD, not Alertmanager convention: `info`, `notice`, `warning`, `error`,
30
+ * `unknown`. Route on `severity="error"`, or relabel it in the Alertmanager config.
31
+ */
32
+
33
+ export interface AlertmanagerBodyOptions {
34
+ /** The `alertname` label. Default `PbsNotification`; `job_type` says which job it was. */
35
+ readonly alertname?: string;
36
+ /** The `source` label. Default `pbs`. */
37
+ readonly source?: string;
38
+ /** Optional `generatorURL` — an absolute link back to the PBS UI. */
39
+ readonly generatorURL?: string;
40
+ }
41
+
42
+ /**
43
+ * ⛔ AN OPTION IS PASTED INTO THE TEMPLATE, SO IT MAY NOT CARRY TEMPLATE SYNTAX. A `{{` in an
44
+ * alert name would be rendered by PBS as an expression; a brace or backslash is refused here
45
+ * rather than escaped, because Handlebars' escape rules are the renderer's, not this file's.
46
+ */
47
+ const literal = (label: string, value: string): string => {
48
+ if (/[{}\\]/.test(value) || value === '') {
49
+ throw new Error(
50
+ `alertmanagerAlertBody: ${label} must be non-empty and hold no '{', '}' or '\\'`,
51
+ );
52
+ }
53
+ return JSON.stringify(value);
54
+ };
55
+
56
+ /**
57
+ * ⛔ AN ABSOLUTE http(s) URL OR NOTHING. Alertmanager validates `generatorURL` as `format: uri`
58
+ * (api/v2/openapi.yaml) and answers 422 for the WHOLE post — so a typo here would not degrade
59
+ * one field, it would lose every notification, and only at send time.
60
+ */
61
+ const absoluteUrl = (value: string): string => {
62
+ const text = literal('generatorURL', value);
63
+ let protocol = '';
64
+ try {
65
+ protocol = new URL(value).protocol;
66
+ } catch {
67
+ // not a URL at all: refused below, like a relative one
68
+ }
69
+ if (protocol !== 'https:' && protocol !== 'http:') {
70
+ throw new Error('alertmanagerAlertBody: generatorURL must be an absolute http(s) URL');
71
+ }
72
+ return text;
73
+ };
74
+
75
+ /** Label name → notification field. ⚠️ Label names must match `[a-zA-Z_][a-zA-Z0-9_]*`. */
76
+ const FIELD_LABELS: readonly (readonly [label: string, field: string])[] = [
77
+ ['job_type', 'type'],
78
+ ['job_id', 'job-id'],
79
+ ['datastore', 'datastore'],
80
+ ['hostname', 'hostname'],
81
+ ];
82
+
83
+ /**
84
+ * The body template. Declare it on a webhook target together with `Content-Type:
85
+ * application/json`, method `post`, and the URL `<alertmanager>/api/v2/alerts`.
86
+ *
87
+ * ★ `fields.job-id` IS A VALID PATH: Handlebars-rust 5 (the version proxmox-notify pins) allows
88
+ * `-` in an identifier (grammar.pest `symbol_char`), as handlebars.js does. The same scratch
89
+ * build rendered this template for a GC failure, a verify failure and a field-less test
90
+ * notification, and each parsed as JSON with string-only labels. The adversarial review re-ran
91
+ * it over the test file's whole matrix — every PBS event's fields x five severities x hostile
92
+ * text, 840 renders with and without `generatorURL` — and every one parsed (2026-09-22).
93
+ * ⚠️ THAT BUILD IS NOT PBS. The first real notification is the measurement that counts: send one
94
+ * with the target's "Test" button and read what Alertmanager received.
95
+ */
96
+ export const alertmanagerAlertBody = (options: AlertmanagerBodyOptions = {}): string => {
97
+ const alertname = literal('alertname', options.alertname ?? 'PbsNotification');
98
+ const source = literal('source', options.source ?? 'pbs');
99
+ const labels = FIELD_LABELS.map(
100
+ ([label, field]) =>
101
+ ` {{#if fields.${field}}}"${label}": {{ json fields.${field} }},{{/if}}`,
102
+ );
103
+ const link =
104
+ options.generatorURL === undefined
105
+ ? ''
106
+ : `,\n "generatorURL": ${absoluteUrl(options.generatorURL)}`;
107
+ return [
108
+ '[',
109
+ ' {',
110
+ ' "labels": {',
111
+ ...labels,
112
+ ' "severity": {{ json severity }},',
113
+ ` "alertname": ${alertname},`,
114
+ ` "source": ${source}`,
115
+ ' },',
116
+ ' "annotations": {',
117
+ ' "summary": {{ json title }},',
118
+ ' "description": {{ json message }},',
119
+ ' "timestamp": "{{ timestamp }}"',
120
+ ` }${link}`,
121
+ ' }',
122
+ ']',
123
+ '',
124
+ ].join('\n');
125
+ };
@@ -12,7 +12,7 @@ import type { ApiTokenAttributes, ApiTokenProps } from './api-token.ts';
12
12
  *
13
13
  * ⛔ THE WRITE SEMANTICS BELOW ARE READ FROM THE CLUSTER'S OWN PERL RATHER THAN INFERRED FROM THE
14
14
  * API SCHEMA — `/usr/share/perl5/PVE/API2/User.pm`, subs `generate_token` and
15
- * `update_token_info`, read over SSH on n2 on 2026-09-13. The two disagree in exactly the place
15
+ * `update_token_info`, read over SSH on node-b on 2026-09-13. The two disagree in exactly the place
16
16
  * that costs a forever-diff: the schema gives `expire` the default "same as user", and the code
17
17
  * never implements it. api-token.ts records what that means.
18
18
  */
@@ -26,7 +26,7 @@ import { bool, int, text } from './values.ts';
26
26
  * `ugid`, so reporting it as an attribute is what lets a privilege-separated token be granted
27
27
  * anything at all without somebody retyping the join by hand.
28
28
  *
29
- * ⚠️ DERIVED, NOT READ. MEASURED: `GET /access/users/monitoring@pve/token/exporter` answers
29
+ * ⚠️ DERIVED, NOT READ. MEASURED: `GET /access/users/metrics@pve/token/exporter` answers
30
30
  * `{"expire":0,"privsep":0}` and echoes back neither half of its own identity. So this is built
31
31
  * from props, cannot disagree with the path the read used, and is therefore true by
32
32
  * construction — which is why it is reported and never compared.
@@ -106,8 +106,8 @@ export const apiTokenSpec: PveSpec<ApiTokenProps, ApiTokenAttributes> = {
106
106
  /**
107
107
  * ⚠️ EXACTLY THE THREE FIELDS A PUT CAN PUT BACK, WHICH IS ALSO EXACTLY WHAT THE GET REPORTS.
108
108
  * MEASURED, by replaying these two functions over the live GET bodies of all four token
109
- * shapes this cluster has: `monitoring@pve!exporter` (no comment), `tofu@pve!apply` and
110
- * `sablier@pve!sablier` (commented), and a live OpenBao lease under `hf-read@pve` (a real
109
+ * shapes this cluster has: `metrics@pve!exporter` (no comment), `iac@pve!apply` and
110
+ * `app@pve!app` (commented), and a live OpenBao lease under `hf-read@pve` (a real
111
111
  * non-zero `expire`, offered as both `1789327175` and `"1789327175"` since `int` must not care
112
112
  * which). All four answer noop; flipping `privsep` answers update; an empty body answers
113
113
  * absent. ⚠️ THAT IS THE COMPARISON, NOT THE ENGINE — no `alchemy plan` was run against this
@@ -2,11 +2,11 @@
2
2
  * `Proxmox.ApiToken` — a PVE API token: the thing a service actually authenticates as.
3
3
  *
4
4
  * ⛔ THE SECRET EXISTS FOR ONE HTTP RESPONSE AND THEN NOWHERE. MEASURED from the cluster's own
5
- * schema (`/usr/share/pve-docs/api-viewer/apidoc.js` on n2, 2026-09-13): POST returns
5
+ * schema (`/usr/share/pve-docs/api-viewer/apidoc.js` on node-b, 2026-09-13): POST returns
6
6
  * `["full-tokenid","info","value"]`; PUT returns `["comment","expire","full-tokenid","privsep",
7
7
  * "value"]` with `value` present ONLY when `regenerate` was set; GET returns
8
8
  * `["comment","expire","privsep"]` and never the secret. A live token agrees —
9
- * `GET /access/users/monitoring@pve/token/exporter` answers `{"expire":0,"privsep":0}`. PVE says
9
+ * `GET /access/users/metrics@pve/token/exporter` answers `{"expire":0,"privsep":0}`. PVE says
10
10
  * it plainly on the POST: the value "needs to be stored as it cannot be retrieved afterwards".
11
11
  *
12
12
  * ⛔ SO A TOKEN THIS RESOURCE CREATES IS UNUSABLE, AND SAYING SO IS THE POINT OF THIS BLOCK. The
@@ -19,7 +19,7 @@
19
19
  * ★ SO POINT THIS FAMILY AT TOKENS THAT ALREADY EXIST. `comment`, `expire` and `privsep` are the
20
20
  * whole of a token's policy, and declaring them is real work: it is how `expire` stops being
21
21
  * whatever somebody typed in 2024. Mint NEW tokens where the secret can be caught — OpenBao's
22
- * `proxmox-tb4` mount, or a human at `pveum user token add`.
22
+ * `proxmox-c1` mount, or a human at `pveum user token add`.
23
23
  * ⚠️ THE CREATE PATH IS LEFT REACHABLE RATHER THAN STUBBED. A create that silently did nothing
24
24
  * would be the same lie as the stubbed delete resource.ts's ★ refuses; the honest arrangement
25
25
  * is a create that works and a header that says what it produces.
@@ -31,8 +31,8 @@
31
31
  * that owns the lease, or to a human who is watching.
32
32
  *
33
33
  * ⚠️ THERE IS A SECOND WRITER TO THESE OBJECTS AND IT IS THE ONE THIS PROVIDER RUNS ON. OpenBao's
34
- * `proxmox-tb4` engine mints PVE tokens under `hf-read@pve` and `hf-provision@pve` — read from
35
- * `house/platform/secrets/vault/plugin-proxmox/proxmox/`: `privsep=0` (client.go), `expire` set
34
+ * `proxmox-c1` engine mints PVE tokens under `hf-read@pve` and `hf-provision@pve` — read from
35
+ * `<estate>/platform/secrets/vault/plugin-proxmox/proxmox/`: `privsep=0` (client.go), `expire` set
36
36
  * to the lease deadline, and an id the plugin CHOOSES,
37
37
  * `hf-<role>-<actor>-<entity6>-<stamp>-<nonce>` (tokenname.go). Revocation and WAL rollback look
38
38
  * up that one exact id (wal.go); neither sweeps a prefix. So the two systems cannot collide on
@@ -62,7 +62,7 @@ import { type PveRequirements, type WithTarget, pveHandlers, pveOperations } fro
62
62
 
63
63
  export interface ApiTokenProps extends WithTarget {
64
64
  /**
65
- * The account that owns the token, realm-qualified: `tofu@pve`, `root@pam`. Identity.
65
+ * The account that owns the token, realm-qualified: `iac@pve`, `root@pam`. Identity.
66
66
  *
67
67
  * ⚠️ THE ACCOUNT MUST ALREADY EXIST. Every method here runs `check_user_exist` first, so a
68
68
  * token named under a missing user fails the READ (folded to "absent") and then fails the
@@ -120,7 +120,7 @@ export interface ApiTokenProps extends WithTarget {
120
120
  * — `$privsep = $privsep ? 1 : 0` (AccessControl.pm:1622), so there is no unset state to
121
121
  * preserve. Were this optional, adopting a live token without mentioning `privsep` would plan
122
122
  * an update to 1 and STRIP a working credential of every privilege it has, silently: PVE
123
- * returns 401/403 to the service, nothing errors here, and `tofu@pve!apply` simply stops
123
+ * returns 401/403 to the service, nothing errors here, and `iac@pve!apply` simply stops
124
124
  * working. Required, so adopting a token is a sentence that states what it is.
125
125
  * ⚠️ AND `false` IS NOT A SHRUG. It is a token with its owner's whole privilege set; under a
126
126
  * provisioning account that is the account's full authority with a separate secret.
@@ -156,9 +156,9 @@ export interface ProxmoxApiToken extends Resource<
156
156
  * ★ `retain` BY DEFAULT, AND THIS IS THE PLAINEST CASE FOR IT IN THE PACKAGE. A token's contents
157
157
  * are one irreplaceable secret: delete it and the value is gone, a replacement is a DIFFERENT
158
158
  * value, and every holder loses access the instant `cfs_write_file` returns — with no error
159
- * raised anywhere near them. The live cluster's tokens are `monitoring@pve!exporter` (the PVE
160
- * exporter feeding VictoriaMetrics), `mcp@pve!executor`, `tofu@pve!apply`, `tofu@pve!ro`,
161
- * `sablier@pve!sablier` and `vaultmint@pve!engine` — the last being the parent credential the
159
+ * raised anywhere near them. The live cluster's tokens are `metrics@pve!exporter` (the PVE
160
+ * exporter feeding VictoriaMetrics), `agent@pve!executor`, `iac@pve!apply`, `iac@pve!ro`,
161
+ * `app@pve!app` and `mint@pve!engine` — the last being the parent credential the
162
162
  * OpenBao mount itself authenticates with, so orphaning that one would stop every plan in this
163
163
  * package, this resource included. `delete` is FULLY IMPLEMENTED (DELETE is a real method on
164
164
  * this path, measured) and runs the moment a caller opts in with `.pipe(RemovalPolicy.destroy())`.
@@ -184,7 +184,7 @@ const ops = pveOperations(apiTokenSpec);
184
184
  * `reconcile` refuses by name and says what to do instead.
185
185
  *
186
186
  * ★ EVERYTHING ELSE STILL WORKS, AND IT IS THE HALF WORTH HAVING. Adopting an existing token and
187
- * converging its `comment`, `expire` and `privsep` are real operations: TB4 carries SEVEN tokens
187
+ * converging its `comment`, `expire` and `privsep` are real operations: C1 carries SEVEN tokens
188
188
  * with `expire=0` and `privsep=0` made by clicks nobody recorded. Declaring those freezes the
189
189
  * set, and an eighth appearing shows up as drift.
190
190
  *
@@ -203,7 +203,7 @@ const handlers = {
203
203
  'the secret only in the create response and it cannot be stored, so a token made here ' +
204
204
  'would be a live credential nobody holds. Create it with `pveum user token add` and ' +
205
205
  'capture the value, or mint a short-lived one from the OpenBao proxmox mount ' +
206
- '(`bao read proxmox-tb4/creds/<role>`) -- then declare it here to manage it.',
206
+ '(`bao read proxmox-c1/creds/<role>`) -- then declare it here to manage it.',
207
207
  ),
208
208
  );
209
209
  }
@@ -213,7 +213,7 @@ const handlers = {
213
213
 
214
214
  /**
215
215
  * ⛔ `list` IS EMPTY, AND FOR THIS FAMILY THAT MATTERS MORE THAN THE GENERIC ARGUMENT IN
216
- * resource.ts. A token index handed to Alchemy would offer up `vaultmint@pve!engine` and every
216
+ * resource.ts. A token index handed to Alchemy would offer up `mint@pve!engine` and every
217
217
  * live OpenBao lease for adoption — and adoption is what makes a later plan willing to delete.
218
218
  * The mount's leases in particular appear and vanish on their own; anything that adopted one
219
219
  * would report drift against a credential that was never its to hold.
@@ -9,7 +9,7 @@
9
9
  * here decides a diff; `findRow` picks a row out of an answer its caller already fetched.
10
10
  *
11
11
  * ⛔ THE TWO PATHS ARE DIFFERENT AND THAT IS THE WHOLE REASON THIS FILE HAS TWO OF THEM. MEASURED
12
- * on n2, 2026-09-13: there is no GET on the id path — `pvesh get /nodes/n2/ceph/mon/n2` answers
12
+ * on node-b, 2026-09-13: there is no GET on the id path — `pvesh get /nodes/node-b/ceph/mon/node-b` answers
13
13
  * "No 'get' handler defined" — while POST and DELETE are registered ONLY there. So the read uses
14
14
  * `collectionPath` and the two writes use `daemonPath`, and swapping either one is a silent
15
15
  * 501: a create that never lands, or a destroy that reports failure while the daemon runs on.
@@ -41,8 +41,8 @@ export const daemonId = (props: CephDaemonProps) => props.name ?? props.node;
41
41
 
42
42
  /**
43
43
  * The read path. ⚠️ `{node}` HERE IS ONLY THE NODE BEING ASKED, not a filter. MEASURED:
44
- * `GET /nodes/n2/ceph/mon` returns n2, n3 AND n4, and `GET /nodes/n3/ceph/mon` returns the same
45
- * three IN A DIFFERENT ORDER — [n4,n2,n3] from n2 against [n3,n2,n4] from n3. A caller that took
44
+ * `GET /nodes/node-b/ceph/mon` returns node-b, node-c AND node-d, and `GET /nodes/node-c/ceph/mon` returns the same
45
+ * three IN A DIFFERENT ORDER — [node-d,node-b,node-c] from node-b against [node-c,node-b,node-d] from node-c. A caller that took
46
46
  * row zero, or assumed the list was this node's daemons, would be wrong on both counts.
47
47
  */
48
48
  export const collectionPath = (props: CephDaemonProps) => `nodes/${props.node}/ceph/${props.kind}`;
@@ -2,7 +2,7 @@
2
2
  * `Proxmox.CephDaemon` — a Ceph monitor, manager or metadata server on one node.
3
3
  *
4
4
  * ★ ONE RESOURCE FOR THREE KINDS, AND THE SCHEMAS WERE CHECKED BEFORE THAT WAS DECIDED. Parsed
5
- * from `/usr/share/pve-docs/api-viewer/apidoc.js` on n2, 2026-09-13: mon, mgr and mds each
5
+ * from `/usr/share/pve-docs/api-viewer/apidoc.js` on node-b, 2026-09-13: mon, mgr and mds each
6
6
  * expose GET on the collection and POST + DELETE on the id below it, each returns
7
7
  * name/host/state/addr/ceph_version*, and the only divergence is the create parameter
8
8
  * (`mon-address` / none / `hotstandby`) and the read-only extras (mon: quorum, rank; mds: rank,
@@ -10,11 +10,11 @@
10
10
  * times over and let them drift — the argument resource.ts already makes for the factory itself.
11
11
  *
12
12
  * ⛔ THE ONLY GET IS THE COLLECTION'S, AND IT ANSWERS FOR THE WHOLE CLUSTER. MEASURED:
13
- * `GET /nodes/n2/ceph/mon` returns n2, n3 AND n4, and `GET /nodes/n3/ceph/mon` returns the same
14
- * three IN A DIFFERENT ORDER — [n4,n2,n3] from n2, [n3,n2,n4] from n3 — so a row is found by
13
+ * `GET /nodes/node-b/ceph/mon` returns node-b, node-c AND node-d, and `GET /nodes/node-c/ceph/mon` returns the same
14
+ * three IN A DIFFERENT ORDER — [node-d,node-b,node-c] from node-b, [node-c,node-b,node-d] from node-c — so a row is found by
15
15
  * `name` and NEVER by position, and the `{node}` in the path is only the node being ASKED.
16
- * MEASURED too: there is no GET on the id path at all; `pvesh get /nodes/n2/ceph/mon/n2` answers
17
- * "No 'get' handler defined for '/nodes/n2/ceph/mon/n2'", and mgr and mds answer the same.
16
+ * MEASURED too: there is no GET on the id path at all; `pvesh get /nodes/node-b/ceph/mon/node-b` answers
17
+ * "No 'get' handler defined for '/nodes/node-b/ceph/mon/node-b'", and mgr and mds answer the same.
18
18
  * That is why `path()` below is the COLLECTION, and that in turn is why `delete` is the one
19
19
  * handler this file writes by hand: `pveOperations.destroy` sends its DELETE to `spec.path`,
20
20
  * which here would be the collection — a 501 reported as a failed destroy, with the daemon still
@@ -27,11 +27,11 @@
27
27
  * strips its section from ceph.conf. THREE MONS TOLERATE ONE LOSS; TWO TOLERATE NONE. Removing
28
28
  * two in one deploy — or removing the mon on the node whose API is serving the call — loses
29
29
  * quorum, and a cluster without mon quorum BLOCKS every RBD and CephFS I/O rather than erroring
30
- * it, so on this cluster every guest on `cephtb4` hangs.
30
+ * it, so on this cluster every guest on `rbd-c1` hangs.
31
31
  * `mds`: destroying a STANDBY is a non-event; destroying the ACTIVE one fails CephFS over,
32
- * and with no standby left `cephfs-tb4` goes unavailable. `mgr`: no guest I/O depends on it, but
32
+ * and with no standby left `cephfs-c1` goes unavailable. `mgr`: no guest I/O depends on it, but
33
33
  * the last one takes the PG autoscaler, the dashboard and PVE's own Ceph status with it.
34
- * Live on 2026-09-13: mon, mgr AND mds on each of n2, n3, n4.
34
+ * Live on 2026-09-13: mon, mgr AND mds on each of node-b, node-c, node-d.
35
35
  *
36
36
  * ★ AND THE ONLY WAY TO REACH THAT DESTROY IS TO DELETE THE DECLARATION. `matches` is deliberately
37
37
  * total (see its ⛔), so this resource CANNOT plan a `replace` — which is the one action that
@@ -41,7 +41,7 @@
41
41
  * `Sys.Audit`/`Datastore.Audit` on `/`, and PVEAuditor holds both (MEASURED:
42
42
  * `GET /access/roles/PVEAuditor`), so the mount's `read` role can see this family — unlike
43
43
  * `Proxmox.Storage`, which 403s under that lease. POST and DELETE both check `Sys.Modify` on
44
- * `/`, and MEASURED on this cluster `LXCProvisioner` already holds it, so the provision lane
44
+ * `/`, and MEASURED on this cluster the provision role already held it (it is in `PROVISION_PRIVILEGES`), so the provision lane
45
45
  * needs no widening for once. ⚠️ `Sys.Modify` on the ROOT path is broad — it also buys
46
46
  * datacenter options and every other cluster-wide config write — so it is worth knowing that
47
47
  * this credential already has it rather than discovering it the next time something is scoped.
@@ -86,9 +86,9 @@ export interface CephDaemonProps extends WithTarget {
86
86
  name?: string;
87
87
  /**
88
88
  * mon only. Overrides the autodetected monitor IP; must sit in Ceph's public network — on this
89
- * cluster that is 10.20.11.0/24, carried by `vmbr1.11`.
89
+ * cluster that is 198.51.100.0/24, carried by `vmbr1.42`.
90
90
  * ⛔ CREATE-ONLY AND NEVER COMPARED. PVE takes a bare ip-list on write and hands back `addr` as
91
- * `10.20.11.12:6789/0` — an address, a port and a nonce. The two are not the same string and
91
+ * `198.51.100.12:6789/0` — an address, a port and a nonce. The two are not the same string and
92
92
  * never will be, so diffing them would report a mismatch on every plan; with no PUT on this
93
93
  * family that mismatch becomes a REPLACE, and a replace of a mon is the quorum loss in the
94
94
  * header. Left out of `matches` on purpose.
@@ -98,7 +98,7 @@ export interface CephDaemonProps extends WithTarget {
98
98
  * mds only. Makes this standby replay the active MDS's journal for a faster failover.
99
99
  * ⛔ CREATE-ONLY AND NEVER COMPARED, AND THE FIELD THAT LOOKS LIKE ITS READBACK IS NOT ONE.
100
100
  * `standby_replay` in the list is the daemon's CURRENT state, not its configuration: MEASURED
101
- * 2026-09-13, all three mds report `standby_replay: false`, n3 included — and n3 is the ACTIVE
101
+ * 2026-09-13, all three mds report `standby_replay: false`, node-c included — and node-c is the ACTIVE
102
102
  * mds, which is not a standby at all and so can never report true however it was created.
103
103
  * Comparing `hotstandby` to it would plan a replace against whichever mds Ceph happens to have
104
104
  * elected, i.e. against a value no declaration controls.
@@ -128,22 +128,22 @@ export interface CephDaemonAttributes {
128
128
  host: string;
129
129
  /**
130
130
  * mon `running`/`stopped`/`unknown`, mgr `active`/`standby`, mds `up:active`/`up:standby`/…
131
- * ⛔ CEPH ELECTS THIS AND REWRITES IT WITHOUT ANYONE DECLARING ANYTHING. MEASURED: mgr n2 is
132
- * `active` while n3 and n4 are `standby`; mds n3 is `up:active` while n2 and n4 are
131
+ * ⛔ CEPH ELECTS THIS AND REWRITES IT WITHOUT ANYONE DECLARING ANYTHING. MEASURED: mgr node-b is
132
+ * `active` while node-c and node-d are `standby`; mds node-c is `up:active` while node-b and node-d are
133
133
  * `up:standby`. Restart a daemon and the roles move. It is reported so a plan can show what is
134
134
  * live and kept out of `matches` so that an election is never a diff.
135
135
  * ⚠️ IT ALSO MEANS THIS RESOURCE DOES NOT MANAGE WHETHER THE DAEMON IS RUNNING. A configured but
136
136
  * stopped daemon is still a row in the list, so it reads as present and plans `noop`.
137
137
  */
138
138
  state: string;
139
- /** Ceph-formatted, e.g. `10.20.11.12:6789/0`. Reported; see `mon-address` for why never compared. */
139
+ /** Ceph-formatted, e.g. `198.51.100.12:6789/0`. Reported; see `mon-address` for why never compared. */
140
140
  addr: string;
141
141
  /** `ceph_version_short`, e.g. `20.2.2`. Reported: a straggler after an upgrade is worth seeing. */
142
142
  version: string;
143
143
  /**
144
144
  * mon rank in the monmap, mds rank in the filesystem, `-1` when the daemon has neither.
145
- * ⛔ CEPH ASSIGNS IT AND NOTHING DECLARES IT. MEASURED: the mons are ranked n4=0, n3=1, n2=2 —
146
- * monmap order, not declaration order — and the mds ranks are 0 for the active n3 and -1 for
145
+ * ⛔ CEPH ASSIGNS IT AND NOTHING DECLARES IT. MEASURED: the mons are ranked node-d=0, node-c=1, node-b=2 —
146
+ * monmap order, not declaration order — and the mds ranks are 0 for the active node-c and -1 for
147
147
  * the two standbys. Rank moves when a daemon is added, removed or restarted.
148
148
  */
149
149
  rank: number;
@@ -152,8 +152,8 @@ export interface CephDaemonAttributes {
152
152
  /** mds only, and NOT the readback of `hotstandby` — see the ⛔ on that prop. */
153
153
  standbyReplay: boolean;
154
154
  /**
155
- * mds only: the CephFS this daemon currently serves — `cephfs-tb4` here.
156
- * ⛔ PRESENT ONLY ON THE ACTIVE MDS. MEASURED: n3 carries `fs_name`, n2 and n4 have no such key
155
+ * mds only: the CephFS this daemon currently serves — `cephfs-c1` here.
156
+ * ⛔ PRESENT ONLY ON THE ACTIVE MDS. MEASURED: node-c carries `fs_name`, node-b and node-d have no such key
157
157
  * at all. So it is empty for two of three identical declarations, and comparing it would
158
158
  * report work on exactly the daemons that are healthy standbys.
159
159
  */
@@ -234,7 +234,7 @@ export const ProxmoxCephDaemonProvider = () =>
234
234
  * problem. Everything else here is the factory's.
235
235
  * ⚠️ AND IT IS THE DANGEROUS ONE. Re-read the destroy ⛔ in the header before letting a
236
236
  * plan that removes a mon run: two gone at once is a cluster with no quorum and every
237
- * guest on `cephtb4` blocked on I/O.
237
+ * guest on `rbd-c1` blocked on I/O.
238
238
  */
239
239
  delete: ({ olds }: { olds: CephDaemonProps }) =>
240
240
  pve(olds.target, 'provision', 'DELETE', daemonPath(olds)),
@@ -2,7 +2,7 @@
2
2
  * `Proxmox.CephFlag` — one cluster-wide Ceph OSD flag, declared: `noout`, `pause`, `norebalance`.
3
3
  *
4
4
  * ★ ONE RESOURCE PER FLAG, NOT ONE CARRYING ALL ELEVEN, AND THE CLUSTER'S OWN API DECIDED IT.
5
- * MEASURED on TB4 2026-09-13 (pve-manager 9.2.11, ceph tentacle 20.2.2) by reading
5
+ * MEASURED on C1 2026-09-13 (pve-manager 9.2.11, ceph tentacle 20.2.2) by reading
6
6
  * `/usr/share/perl5/PVE/API2/Cluster/Ceph.pm` and the published schema in
7
7
  * `/usr/share/pve-docs/api-viewer/apidoc.js`:
8
8
  * · `PUT /cluster/ceph/flags` ends in `fork_worker('cephsetflags', ...)` and returns a STRING
@@ -30,7 +30,7 @@
30
30
  * defaulting to false would make `CephFlag('noout', { flag: 'noout', target })` mean "clear
31
31
  * it" — the dangerous direction, chosen by OMISSION. Here the dangerous sentence has to be
32
32
  * typed out by a person. Do not add a default, and do not declare a flag merely to document
33
- * that it is off: MEASURED 2026-09-13, all eleven flags on TB4 read 0, so eleven
33
+ * that it is off: MEASURED 2026-09-13, all eleven flags on C1 read 0, so eleven
34
34
  * `value: false` lines would plan green forever and do nothing except take the brake off
35
35
  * whenever somebody happens to apply them.
36
36
  *
@@ -58,8 +58,8 @@
58
58
  * and plans `noop` straight past it. It cannot express a half-pause, so never use it to prove
59
59
  * one is gone; `ceph osd dump | head -1` can.
60
60
  * ⛔ `pause` AND `noup` ARE ALSO THE TWO THAT TAKE THE CLUSTER DOWN FROM A TYPO. `pause: true`
61
- * stops all reads and writes, so every guest on `cephtb4` — the cluster's only rbd pool,
62
- * MEASURED 2026-09-13 — freezes on its root disk, and so does `cephfs-tb4`. `noup: true`
61
+ * stops all reads and writes, so every guest on `rbd-c1` — the cluster's only rbd pool,
62
+ * MEASURED 2026-09-13 — freezes on its root disk, and so does `cephfs-c1`. `noup: true`
63
63
  * stops a rebooted OSD from ever rejoining. Neither is undone by deleting the line.
64
64
  *
65
65
  * ⚠️ PRIVILEGES, AND THE WRITE ONE IS A BIG ASK. Both GETs check `Sys.Audit` on `/`; both PUTs
@@ -83,7 +83,7 @@ import { bool, flag } from './values.ts';
83
83
  *
84
84
  * ⚠️ THE ENUM IS CLOSED AND A TYPO IS A 400, WHICH IS WHY THIS IS A UNION AND NOT `string`. Both
85
85
  * the GET and the PUT declare `additionalProperties => 0` over exactly this list.
86
- * ⚠️ CEPH HAS FLAGS PVE DOES NOT MODEL, and four of them are always on. MEASURED on TB4:
86
+ * ⚠️ CEPH HAS FLAGS PVE DOES NOT MODEL, and four of them are always on. MEASURED on C1:
87
87
  * `ceph osd dump` reports `flags sortbitwise,recovery_deletes,purged_snapdirs,pglog_hardlimit`.
88
88
  * PVE reads that same string and answers only about its own eleven, so the others can neither
89
89
  * leak in here nor be set from here — `noautoscale` and `nosnaptrim` included.
@@ -167,7 +167,7 @@ const handlers = pveHandlers<CephFlagProps, CephFlagAttributes>({
167
167
  *
168
168
  * ⛔ IT NEVER ANSWERS undefined, WHICH MAKES THE CREATE BRANCH UNREACHABLE, AND ITS ERROR WILL
169
169
  * MISLEAD YOU. All eleven flags always exist; there is nothing for "absent" to mean. So a POST
170
- * to `collection` means the READ failed — an expired 300s lease, n2 down, or ceph simply not
170
+ * to `collection` means the READ failed — an expired 300s lease, node-b down, or ceph simply not
171
171
  * configured on the cluster (every one of these four handlers opens with
172
172
  * `check_ceph_configured()`, which dies). Read the resulting "Method 'POST /cluster/ceph/flags'
173
173
  * not implemented" as "the read failed" and go and look at the credential, not at ceph. Same
@@ -187,7 +187,7 @@ const handlers = pveHandlers<CephFlagProps, CephFlagAttributes>({
187
187
  * and `name` exist only on the bulk GET, which this file does not read, so neither can be
188
188
  * compared by accident. Nothing PVE returns here is rewritten, re-ordered or re-typed by the
189
189
  * cluster, because all PVE returns here is one boolean.
190
- * ★ MEASURED: on TB4, `GET /cluster/ceph/flags` answers value 0 for all eleven flags, so a
190
+ * ★ MEASURED: on C1, `GET /cluster/ceph/flags` answers value 0 for all eleven flags, so a
191
191
  * declaration of `value: false` reads back false and plans `noop`.
192
192
  */
193
193
  matches: (attributes, props) => attributes.value === props.value,
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * ⛔ POST AND DELETE ANSWER WITH A UPID, NOT A RESULT — THE WRITE IS ASYNCHRONOUS AND A FAILED ONE
18
18
  * IS STILL HTTP 200. Read from the cluster's own source, /usr/share/perl5/PVE/API2/Ceph/FS.pm on
19
- * n2: `createfs` and `destroyfs` both end `return $rpcenv->fork_worker(...)`. Two different lies
19
+ * node-b: `createfs` and `destroyfs` both end `return $rpcenv->fork_worker(...)`. Two different lies
20
20
  * come out of trusting that status code. A read-back straight after the POST reports "still
21
21
  * absent" about a filesystem that is being built as it says so — which is precisely the message
22
22
  * `pveOperations.reconcile` would print, pointing at the wrong cause. And a worker that dies
@@ -26,7 +26,7 @@
26
26
  * has not been observed; only the `fork_worker` that guarantees it is there.
27
27
  *
28
28
  * ⛔ A DELETE BODY IS SILENTLY DISCARDED BY PVE, SO `remove-pools` GOES IN THE QUERY STRING.
29
- * MEASURED in /usr/share/perl5/PVE/APIServer/AnyEvent.pm on n2: line 928 reads the request
29
+ * MEASURED in /usr/share/perl5/PVE/APIServer/AnyEvent.pm on node-b: line 928 reads the request
30
30
  * content into params only `if ($method eq 'PUT' || $method eq 'POST')`, and line 1653 sends
31
31
  * every other method down a branch that parses `$request->url->query()` and nothing else. So
32
32
  * `pve(target, role, 'DELETE', path, form)` would send a body PVE never looks at: the flags would
@@ -41,7 +41,7 @@
41
41
  * ⛔ THE POLLER IS NEVER THE TASK'S OWNER, WHICH COSTS A PRIVILEGE, AND THIS ONE IS MEASURED
42
42
  * RATHER THAN FEARED. client.ts mints a fresh credential per call and the mount vends a NEW
43
43
  * token id each time, so the token reading the status is never the token recorded in the UPID —
44
- * and PVE compares them exactly. /usr/share/perl5/PVE/API2/Tasks.pm on n2, `$check_task_user`:
44
+ * and PVE compares them exactly. /usr/share/perl5/PVE/API2/Tasks.pm on node-b, `$check_task_user`:
45
45
  * `return $user eq $fulltoken || $user eq $task->{user};`, above it the comment "token only sees
46
46
  * token tasks, user sees user + token tasks". Token B of the same user matches neither branch.
47
47
  * So the fallback is the schema's other clause for `GET /nodes/{node}/tasks/{upid}/status`: "The
@@ -128,8 +128,8 @@ const destroyPath = (props: CephFsProps) => {
128
128
  /**
129
129
  * Wait for a forked PVE task, and fail loudly rather than quietly.
130
130
  *
131
- * ⚠️ THE UPID IS PERCENT-ENCODED BECAUSE IT IS FULL OF COLONS — `UPID:n2:00396D3A:…:root@pam:`
132
- * (MEASURED shape, from `GET /nodes/n2/tasks`) is ONE path segment, not seven. Encoding is what
131
+ * ⚠️ THE UPID IS PERCENT-ENCODED BECAUSE IT IS FULL OF COLONS — `UPID:node-b:00396D3A:…:root@pam:`
132
+ * (MEASURED shape, from `GET /nodes/node-b/tasks`) is ONE path segment, not seven. Encoding is what
133
133
  * guarantees it arrives as one; PVE's router decodes each segment before matching. REASONED from
134
134
  * the URI handling rather than measured — no task of this provider's has been polled yet.
135
135
  */
@@ -178,8 +178,8 @@ const settle = (target: PveTarget, node: string, upid: string, what: string) =>
178
178
  *
179
179
  * ⚠️ A CREATE NEEDS A RUNNING *AND* A STANDBY MDS, and refuses BEFORE it forks -- "no running
180
180
  * Metadata Server (MDS) found!" / "no standby Metadata Server (MDS) found!" are synchronous, so
181
- * they surface as a failed POST rather than as a silent worker. TB4 has three (MEASURED: n3
182
- * `up:active` for cephfs-tb4, n2 and n4 `up:standby`), so this is a note for a smaller cluster.
181
+ * they surface as a failed POST rather than as a silent worker. C1 has three (MEASURED: node-c
182
+ * `up:active` for cephfs-c1, node-b and node-d `up:standby`), so this is a note for a smaller cluster.
183
183
  * ⚠️ BOTH WRITES CHECK `Sys.Modify` ON `/` -- the root, with the breadth metric-server.ts warns
184
184
  * about: granting it buys datacenter options and every other cluster-wide config write too.
185
185
  */
@@ -2,7 +2,7 @@
2
2
  * `Proxmox.CephFs` — a CephFS on the cluster's own Ceph. The filesystem a `cephfs` storage mounts.
3
3
  *
4
4
  * ⛔ A REPLACE OF THIS RESOURCE DESTROYS A LIVE FILESYSTEM, AND THERE IS NO PUT TO SAVE YOU FROM
5
- * IT. MEASURED from the published schema on n2, 2026-09-13: `/nodes/{node}/ceph/fs/{name}` has
5
+ * IT. MEASURED from the published schema on node-b, 2026-09-13: `/nodes/{node}/ceph/fs/{name}` has
6
6
  * exactly two methods, POST and DELETE. A CephFS cannot be edited in place, so `updateForm` is
7
7
  * left undefined and anything the factory can see as changed becomes a REPLACE — and Alchemy's
8
8
  * replace is create-then-delete, where the delete is `destroyfs`: the entry leaves the MDS map,
@@ -12,9 +12,9 @@
12
12
  * one it is a live filesystem destroyed on every deploy, forever, because a background daemon
13
13
  * moved a number nobody declared.
14
14
  *
15
- * ⛔ THERE IS NO SINGLE-OBJECT READ, WHICH INVERTS `path` AND `collection`. MEASURED on n2:
16
- * pvesh get /nodes/n2/ceph/fs/cephfs-tb4
17
- * -> No 'get' handler defined for '/nodes/n2/ceph/fs/cephfs-tb4'
15
+ * ⛔ THERE IS NO SINGLE-OBJECT READ, WHICH INVERTS `path` AND `collection`. MEASURED on node-b:
16
+ * pvesh get /nodes/node-b/ceph/fs/cephfs-c1
17
+ * -> No 'get' handler defined for '/nodes/node-b/ceph/fs/cephfs-c1'
18
18
  * `GET /nodes/{node}/ceph/fs` is a directory index and is the only read there is, so `path()`
19
19
  * below returns the INDEX and `attributes` picks this filesystem out of the array client-side —
20
20
  * the shape acl.ts uses for the one flat `GET /access/acl`. The POST goes to the `{name}` path,
@@ -28,9 +28,9 @@
28
28
  *
29
29
  * ⛔ CREATING A CephFS CREATES TWO CEPH POOLS, AND A `Proxmox.CephPool` MUST NOT ALSO DECLARE THEM.
30
30
  * `createfs` builds `<name>_data` and `<name>_metadata` itself and refuses outright if either
31
- * exists ("ceph pools '…_data' and/or '…_metadata' already exist"). MEASURED on TB4: the live
32
- * `cephfs-tb4` owns `cephfs-tb4_data` (id 6) and `cephfs-tb4_metadata` (id 7), both of which
33
- * `GET /nodes/n2/ceph/pool` lists like any other pool — and `/nodes/{node}/ceph/pool/{name}` DOES
31
+ * exists ("ceph pools '…_data' and/or '…_metadata' already exist"). MEASURED on C1: the live
32
+ * `cephfs-c1` owns `cephfs-c1_data` (id 6) and `cephfs-c1_metadata` (id 7), both of which
33
+ * `GET /nodes/node-b/ceph/pool` lists like any other pool — and `/nodes/{node}/ceph/pool/{name}` DOES
34
34
  * have GET, PUT and DELETE, so a pool resource would be a clean `pveHandlers` fit and would
35
35
  * adopt them without noticing. Declaring both is double management of one object: whichever
36
36
  * reconciles first wins, and the loser either fails its create or PUTs `size`/`pg_num` onto pools
@@ -38,16 +38,16 @@
38
38
  *
39
39
  * ⚠️ `add-storage` IS A SECOND SIDE EFFECT AND IT LANDS IN storage.cfg. Set, `createfs` also calls
40
40
  * `PVE::API2::Storage::Config->create` with type `cephfs`, content `backup,iso,vztmpl` and
41
- * `fs-name <name>`. MEASURED: /etc/pve/storage.cfg on n2 holds exactly that — `cephfs:
42
- * cephfs-tb4` / `content backup,vztmpl,iso` / `fs-name cephfs-tb4`. So a `Proxmox.Storage`
43
- * declaring `cephfs-tb4` would be managing an entry this resource created. Declare ONE of them:
41
+ * `fs-name <name>`. MEASURED: /etc/pve/storage.cfg on node-b holds exactly that — `cephfs:
42
+ * cephfs-c1` / `content backup,vztmpl,iso` / `fs-name cephfs-c1`. So a `Proxmox.Storage`
43
+ * declaring `cephfs-c1` would be managing an entry this resource created. Declare ONE of them:
44
44
  * either `add-storage` here and no storage resource, or `add-storage` off and a
45
45
  * `Proxmox.Storage` that reads this resource's `name` attribute — which also gets the ordering
46
46
  * right for free, because Alchemy orders by data flow.
47
47
  *
48
48
  * ⚠️ `node` IS A ROUTE, NOT IDENTITY, AND MUST NEVER BE DIFFED. A CephFS is cluster-wide; the node
49
- * in the path only says which node PVE proxies the call to. MEASURED: `GET /nodes/n2/ceph/fs`,
50
- * `…/n3/…` and `…/n4/…` returned the byte-identical array. So changing `node` plans `noop`, and
49
+ * in the path only says which node PVE proxies the call to. MEASURED: `GET /nodes/node-b/ceph/fs`,
50
+ * `…/node-c/…` and `…/node-d/…` returned the byte-identical array. So changing `node` plans `noop`, and
51
51
  * two resources naming the same `name` on different nodes are the SAME filesystem — the vmid
52
52
  * hazard from lxc.ts, with a filesystem on the end of it instead of a container.
53
53
  *
@@ -84,11 +84,11 @@ export interface CephFsProps extends WithTarget {
84
84
  * this is under 32. PVE's default is 128.
85
85
  *
86
86
  * ⛔ CREATE-TIME ONLY, UNREADABLE, AND OWNED BY THE AUTOSCALER FROM THE SECOND IT LANDS. It is
87
- * never compared — see the ⛔ on `matches` — and this is not caution, it is measured. TB4's
88
- * `cephfs-tb4` was created with the default 128 (the arithmetic proves it: `cephfs-tb4_metadata`
89
- * sits at 32, which is 128/4), and `cephfs-tb4_data` reads pg_num 32 TODAY. The autoscaler moved
87
+ * never compared — see the ⛔ on `matches` — and this is not caution, it is measured. C1's
88
+ * `cephfs-c1` was created with the default 128 (the arithmetic proves it: `cephfs-c1_metadata`
89
+ * sits at 32, which is 128/4), and `cephfs-c1_data` reads pg_num 32 TODAY. The autoscaler moved
90
90
  * it by a factor of four, and it is not finished: `pg_autoscale_mode` is `on` for every pool on
91
- * this cluster and `cephfs-tb4_metadata` already reports `pg_num_final: 16` against its live 32.
91
+ * this cluster and `cephfs-c1_metadata` already reports `pg_num_final: 16` against its live 32.
92
92
  */
93
93
  pg_num?: number;
94
94
  /**
@@ -155,7 +155,7 @@ const ops = pveOperations<CephFsProps, CephFsAttributes>({
155
155
  * only honest comparison is none at all.
156
156
  * ⚠️ THE TEMPTING ADDITION IS `pg_num`, AND IT WOULD BE THE WORST BUG THIS PACKAGE HAS SHIPPED.
157
157
  * It is not in the index at all, so it can only be read from the pool endpoint — where the
158
- * autoscaler owns it. MEASURED on TB4 today: created at 128, live at 32, with the metadata
158
+ * autoscaler owns it. MEASURED on C1 today: created at 128, live at 32, with the metadata
159
159
  * pool already scheduled down to 16. Compared, that is an eternal mismatch; and because
160
160
  * `updateForm` is undefined, the action it produces is REPLACE. The plan would destroy and
161
161
  * rebuild a live filesystem on every deploy, triggered by a daemon on its own schedule.
@@ -176,7 +176,7 @@ export const ProxmoxCephFsProvider = () =>
176
176
  ProxmoxCephFs.Provider.of({
177
177
  /**
178
178
  * ⛔ EMPTY, LIKE EVERY OTHER RESOURCE HERE. The index answers with every filesystem on the
179
- * cluster, `cephfs-tb4` included — the one holding this estate's ISOs and templates.
179
+ * cluster, `cephfs-c1` included — the one holding this estate's ISOs and templates.
180
180
  * Adopting it would put Alchemy one `destroy` away from it. Adoption stays explicit.
181
181
  */
182
182
  list: () => Effect.succeed([]),