@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.
- package/README.md +3 -3
- package/dist/caddy/admin-guard.d.ts +1 -1
- package/dist/caddy/index.js.map +2 -2
- package/dist/cloudflare/client.d.ts +1 -1
- package/dist/cloudflare/index.js.map +2 -2
- package/dist/cloudflare/lock-rules.d.ts +1 -1
- package/dist/forgejo/branch-protection-form.d.ts +1 -1
- package/dist/forgejo/branch-protection.d.ts +1 -1
- package/dist/forgejo/index.js.map +10 -10
- package/dist/forgejo/org-actions-secrets.d.ts +1 -1
- package/dist/forgejo/org-label.d.ts +1 -1
- package/dist/forgejo/org-team.d.ts +1 -1
- package/dist/forgejo/repo-webhook.d.ts +1 -1
- package/dist/forgejo/repository.d.ts +2 -2
- package/dist/forgejo/repository.d.ts.map +1 -1
- package/dist/forgejo/resource.d.ts +2 -2
- package/dist/forgejo/team-member.d.ts +1 -1
- package/dist/openbao/auth-role.d.ts +1 -1
- package/dist/openbao/bao-http.d.ts +4 -4
- package/dist/openbao/cloudflare-permission-groups.d.ts +1 -1
- package/dist/openbao/cloudflare-role-form.d.ts +2 -2
- package/dist/openbao/cloudflare-role-form.d.ts.map +1 -1
- package/dist/openbao/cloudflare-roles-expand.d.ts +1 -1
- package/dist/openbao/digest.d.ts +1 -1
- package/dist/openbao/forgejo-bootstrap-parse.d.ts +2 -2
- package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -1
- package/dist/openbao/forgejo-bootstrap.d.ts +1 -1
- package/dist/openbao/index.js.map +14 -14
- package/dist/openbao/mount.d.ts +1 -1
- package/dist/openbao/pki-role.d.ts +1 -1
- package/dist/openbao/policy.d.ts +2 -2
- package/dist/openbao/proxmox-role-form.d.ts +2 -2
- package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
- package/dist/openbao/proxmox-role.d.ts +10 -10
- package/dist/openbao/ssh-role.d.ts +3 -3
- package/dist/proxmox/acl.d.ts +2 -2
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/alertmanager-body.d.ts +54 -0
- package/dist/proxmox/alertmanager-body.d.ts.map +1 -0
- package/dist/proxmox/api-token-form.d.ts +2 -2
- package/dist/proxmox/api-token.d.ts +11 -11
- package/dist/proxmox/ceph-daemon-form.d.ts +3 -3
- package/dist/proxmox/ceph-daemon.d.ts +19 -19
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
- package/dist/proxmox/ceph-flag.d.ts +5 -5
- package/dist/proxmox/ceph-fs-wire.d.ts +5 -5
- package/dist/proxmox/ceph-fs.d.ts +16 -16
- package/dist/proxmox/ceph-osd-tree.d.ts +16 -16
- package/dist/proxmox/ceph-osd.d.ts +5 -5
- package/dist/proxmox/ceph-pool-form.d.ts +13 -13
- package/dist/proxmox/ceph-pool-form.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool.d.ts +3 -3
- package/dist/proxmox/client.d.ts +2 -2
- package/dist/proxmox/credential-literals.d.ts +36 -0
- package/dist/proxmox/credential-literals.d.ts.map +1 -0
- package/dist/proxmox/credentials.d.ts +8 -8
- package/dist/proxmox/credentials.d.ts.map +1 -1
- package/dist/proxmox/fake-pbs-notify.d.ts +23 -0
- package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -0
- package/dist/proxmox/fake-pve.d.ts +5 -0
- package/dist/proxmox/fake-pve.d.ts.map +1 -1
- package/dist/proxmox/firewall-alias-form.d.ts +1 -1
- package/dist/proxmox/firewall-alias.d.ts +1 -1
- package/dist/proxmox/generated/pbs.d.ts +279 -1
- package/dist/proxmox/generated/pbs.d.ts.map +1 -1
- package/dist/proxmox/generated/pve.d.ts +58 -1
- package/dist/proxmox/generated/pve.d.ts.map +1 -1
- package/dist/proxmox/group.d.ts +8 -8
- package/dist/proxmox/ha-resource.d.ts +3 -2
- package/dist/proxmox/ha-resource.d.ts.map +1 -1
- package/dist/proxmox/ha-rule-form.d.ts +5 -5
- package/dist/proxmox/ha-rule.d.ts +3 -3
- package/dist/proxmox/index.d.ts +14 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +935 -223
- package/dist/proxmox/index.js.map +69 -56
- package/dist/proxmox/lease-cache.d.ts +4 -4
- package/dist/proxmox/lxc-harness.d.ts +1 -1
- package/dist/proxmox/lxc-props.d.ts +2 -2
- package/dist/proxmox/lxc.d.ts +3 -3
- package/dist/proxmox/members.d.ts +3 -3
- package/dist/proxmox/network-apply-read.d.ts +5 -5
- package/dist/proxmox/network-apply.d.ts +9 -9
- package/dist/proxmox/node-network-form.d.ts +6 -6
- package/dist/proxmox/node-network.d.ts +17 -17
- package/dist/proxmox/node-network.d.ts.map +1 -1
- package/dist/proxmox/notification-matcher-form.d.ts +100 -0
- package/dist/proxmox/notification-matcher-form.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher.d.ts +46 -0
- package/dist/proxmox/notification-matcher.d.ts.map +1 -0
- package/dist/proxmox/notification-target.d.ts +3 -2
- package/dist/proxmox/notification-target.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-guard.d.ts +1 -1
- package/dist/proxmox/pbs-datastore.d.ts +10 -10
- package/dist/proxmox/pbs-notification-matcher.d.ts +53 -0
- package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-form.d.ts +63 -0
- package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +100 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-wire.d.ts +30 -0
- package/dist/proxmox/pbs-notification-target-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target.d.ts +123 -0
- package/dist/proxmox/pbs-notification-target.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job.d.ts +2 -2
- package/dist/proxmox/pbs-sync-job.d.ts +4 -4
- package/dist/proxmox/pbs-verify-job.d.ts +1 -1
- package/dist/proxmox/pool.d.ts +4 -4
- package/dist/proxmox/provision-baseline.d.ts +74 -0
- package/dist/proxmox/provision-baseline.d.ts.map +1 -0
- package/dist/proxmox/provision-bootstrap.d.ts +33 -0
- package/dist/proxmox/provision-bootstrap.d.ts.map +1 -0
- package/dist/proxmox/provision-cli-fake.d.ts +38 -0
- package/dist/proxmox/provision-cli-fake.d.ts.map +1 -0
- package/dist/proxmox/provision-declare.d.ts +25 -0
- package/dist/proxmox/provision-declare.d.ts.map +1 -0
- package/dist/proxmox/replication-job.d.ts +9 -8
- package/dist/proxmox/replication-job.d.ts.map +1 -1
- package/dist/proxmox/resource.d.ts +6 -5
- package/dist/proxmox/resource.d.ts.map +1 -1
- package/dist/proxmox/sdn-apply-read.d.ts +7 -7
- package/dist/proxmox/sdn-apply.d.ts +1 -1
- package/dist/proxmox/sdn-subnet-form.d.ts +1 -1
- package/dist/proxmox/sdn-subnet.d.ts +5 -5
- package/dist/proxmox/sdn-vnet.d.ts +5 -6
- package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
- package/dist/proxmox/storage.d.ts +4 -3
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/proxmox/user.d.ts +3 -2
- package/dist/proxmox/user.d.ts.map +1 -1
- package/dist/proxmox/values.d.ts +1 -1
- package/dist/proxmox/write-only.d.ts +35 -0
- package/dist/proxmox/write-only.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-write.d.ts +1 -1
- package/dist/proxmox/zfs-pool.d.ts +5 -5
- package/dist/talos/credentials.d.ts +1 -1
- package/dist/talos/credentials.d.ts.map +1 -1
- package/dist/talos/index.js.map +1 -1
- package/dist/verify/fake-engine.d.ts +5 -0
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/adopt-verify.md +21 -5
- package/docs/adopted-deploys.md +9 -1
- package/docs/ownership.md +26 -1
- package/docs/pbs-alertmanager-body.md +73 -0
- package/docs/pbs-notifications.md +163 -0
- package/docs/provision-baseline.md +106 -0
- package/docs/proxmox-lxc-adopt.md +2 -2
- package/docs/proxmox-lxc.md +5 -4
- package/docs/proxmox.md +9 -0
- package/package.json +1 -1
- package/src/caddy/admin-guard.ts +1 -1
- package/src/caddy/local-admin.ts +1 -1
- package/src/cloudflare/client.ts +1 -1
- package/src/cloudflare/lock-rules.ts +1 -1
- package/src/forgejo/branch-protection-form.ts +1 -1
- package/src/forgejo/branch-protection.ts +1 -1
- package/src/forgejo/client.ts +1 -1
- package/src/forgejo/org-actions-secrets.ts +1 -1
- package/src/forgejo/org-label.ts +1 -1
- package/src/forgejo/org-team.ts +1 -1
- package/src/forgejo/repo-webhook.ts +1 -1
- package/src/forgejo/repository.ts +2 -2
- package/src/forgejo/resource.ts +2 -2
- package/src/forgejo/team-member.ts +1 -1
- package/src/openbao/README.md +4 -4
- package/src/openbao/auth-role.ts +1 -1
- package/src/openbao/bao-http.test.ts +2 -2
- package/src/openbao/bao-http.ts +6 -6
- package/src/openbao/bao-status.test.ts +1 -1
- package/src/openbao/bao-status.ts +1 -1
- package/src/openbao/cloudflare-group-scope.test.ts +1 -1
- package/src/openbao/cloudflare-permission-groups.test.ts +1 -1
- package/src/openbao/cloudflare-permission-groups.ts +3 -3
- package/src/openbao/cloudflare-policy.ts +1 -1
- package/src/openbao/cloudflare-role-form.ts +2 -2
- package/src/openbao/cloudflare-roles-expand.ts +1 -1
- package/src/openbao/digest.ts +1 -1
- package/src/openbao/forgejo-bootstrap-parse.test.ts +3 -3
- package/src/openbao/forgejo-bootstrap-parse.ts +2 -2
- package/src/openbao/forgejo-bootstrap.ts +4 -4
- package/src/openbao/kubernetes-role-form.test.ts +5 -5
- package/src/openbao/mount.ts +1 -1
- package/src/openbao/pki-role.ts +1 -1
- package/src/openbao/policy-wire.test.ts +3 -3
- package/src/openbao/policy.ts +2 -2
- package/src/openbao/proxmox-role-form.ts +5 -5
- package/src/openbao/proxmox-role.ts +11 -11
- package/src/openbao/ssh-role.ts +3 -3
- package/src/proxmox/acl.ts +4 -4
- package/src/proxmox/adopt-noop.test.ts +6 -6
- package/src/proxmox/alertmanager-body.ts +125 -0
- package/src/proxmox/api-token-form.ts +4 -4
- package/src/proxmox/api-token.ts +13 -13
- package/src/proxmox/ceph-daemon-form.ts +3 -3
- package/src/proxmox/ceph-daemon.ts +20 -20
- package/src/proxmox/ceph-flag.ts +7 -7
- package/src/proxmox/ceph-fs-wire.ts +7 -7
- package/src/proxmox/ceph-fs.ts +18 -18
- package/src/proxmox/ceph-osd-tree.ts +16 -16
- package/src/proxmox/ceph-osd.ts +6 -6
- package/src/proxmox/ceph-pool-adopt.test.ts +14 -14
- package/src/proxmox/ceph-pool-form.ts +13 -13
- package/src/proxmox/ceph-pool.ts +6 -6
- package/src/proxmox/client.test.ts +3 -3
- package/src/proxmox/client.ts +3 -3
- package/src/proxmox/credential-literals.test.ts +148 -0
- package/src/proxmox/credential-literals.ts +74 -0
- package/src/proxmox/credentials.test.ts +6 -8
- package/src/proxmox/credentials.ts +8 -8
- package/src/proxmox/fake-pbs-notify.ts +108 -0
- package/src/proxmox/fake-pve.ts +12 -2
- package/src/proxmox/firewall-alias-form.ts +1 -1
- package/src/proxmox/firewall-alias.ts +1 -1
- package/src/proxmox/generated/pbs.ts +330 -1
- package/src/proxmox/generated/pve.ts +63 -1
- package/src/proxmox/group.ts +10 -10
- package/src/proxmox/ha-resource.ts +3 -2
- package/src/proxmox/ha-rule-form.ts +5 -5
- package/src/proxmox/ha-rule.ts +3 -3
- package/src/proxmox/index.test.ts +10 -0
- package/src/proxmox/index.ts +32 -0
- package/src/proxmox/lease-cache.test.ts +19 -19
- package/src/proxmox/lease-cache.ts +4 -4
- package/src/proxmox/lxc-adopt.test.ts +3 -3
- package/src/proxmox/lxc-create-form.ts +1 -1
- package/src/proxmox/lxc-guards.test.ts +4 -4
- package/src/proxmox/lxc-harness.ts +3 -3
- package/src/proxmox/lxc-judge.test.ts +5 -5
- package/src/proxmox/lxc-lifecycle.ts +1 -1
- package/src/proxmox/lxc-props.ts +2 -2
- package/src/proxmox/lxc-read.test.ts +3 -3
- package/src/proxmox/lxc-strict-adopt.test.ts +1 -1
- package/src/proxmox/lxc-volume.test.ts +9 -9
- package/src/proxmox/lxc-volume.ts +1 -1
- package/src/proxmox/lxc-wire.ts +1 -1
- package/src/proxmox/lxc.ts +3 -3
- package/src/proxmox/members.test.ts +1 -1
- package/src/proxmox/members.ts +3 -3
- package/src/proxmox/metric-server-otel.ts +2 -2
- package/src/proxmox/network-apply-read.ts +10 -10
- package/src/proxmox/network-apply.ts +10 -10
- package/src/proxmox/node-network-form.ts +7 -7
- package/src/proxmox/node-network.ts +18 -18
- package/src/proxmox/notification-matcher-form.ts +221 -0
- package/src/proxmox/notification-matcher.test.ts +234 -0
- package/src/proxmox/notification-matcher.ts +76 -0
- package/src/proxmox/notification-target.ts +3 -2
- package/src/proxmox/pbs-datastore-guard.ts +1 -1
- package/src/proxmox/pbs-datastore.ts +10 -10
- package/src/proxmox/pbs-notification-matcher.ts +81 -0
- package/src/proxmox/pbs-notification-target-drift.test.ts +83 -0
- package/src/proxmox/pbs-notification-target-form.ts +229 -0
- package/src/proxmox/pbs-notification-target-lifecycle.ts +177 -0
- package/src/proxmox/pbs-notification-target-state.test.ts +191 -0
- package/src/proxmox/pbs-notification-target-wire.ts +112 -0
- package/src/proxmox/pbs-notification-target.test.ts +226 -0
- package/src/proxmox/pbs-notification-target.ts +139 -0
- package/src/proxmox/pbs-notifications-stack.test.ts +94 -0
- package/src/proxmox/pbs-prune-job.ts +2 -2
- package/src/proxmox/pbs-sync-job.ts +4 -4
- package/src/proxmox/pbs-verify-job.ts +1 -1
- package/src/proxmox/pool.test.ts +2 -2
- package/src/proxmox/pool.ts +4 -4
- package/src/proxmox/provision-baseline.test.ts +87 -0
- package/src/proxmox/provision-baseline.ts +162 -0
- package/src/proxmox/provision-bootstrap.test.ts +132 -0
- package/src/proxmox/provision-bootstrap.ts +135 -0
- package/src/proxmox/provision-cli-fake.ts +217 -0
- package/src/proxmox/provision-declare.test.ts +111 -0
- package/src/proxmox/provision-declare.ts +103 -0
- package/src/proxmox/replication-job.ts +9 -8
- package/src/proxmox/resource.ts +9 -9
- package/src/proxmox/sdn-apply-read.test.ts +16 -16
- package/src/proxmox/sdn-apply-read.ts +7 -7
- package/src/proxmox/sdn-apply.ts +3 -3
- package/src/proxmox/sdn-subnet-form.ts +1 -1
- package/src/proxmox/sdn-subnet.ts +5 -5
- package/src/proxmox/sdn-vnet.ts +5 -6
- package/src/proxmox/storage.ts +4 -3
- package/src/proxmox/user.ts +3 -2
- package/src/proxmox/values.test.ts +2 -2
- package/src/proxmox/values.ts +1 -1
- package/src/proxmox/write-only.test.ts +75 -0
- package/src/proxmox/write-only.ts +106 -0
- package/src/proxmox/zfs-pool-write.ts +1 -1
- package/src/proxmox/zfs-pool.ts +6 -6
- package/src/talos/credentials.ts +3 -3
- package/src/talos/values.test.ts +9 -9
- package/src/verify/cli.test.ts +1 -1
- package/src/verify/fake-engine.ts +8 -1
package/docs/adopted-deploys.md
CHANGED
|
@@ -38,6 +38,7 @@ API token on the cluster, just as it does for `alchemy plan`.
|
|
|
38
38
|
| `Proxmox.BackupJob` | `cluster/backup/{id}` | read | shared path — no write |
|
|
39
39
|
| `Proxmox.MetricServer` | `cluster/metrics/server/{id}` | read | shared path — no write |
|
|
40
40
|
| `Proxmox.NotificationTarget` ² | `cluster/notifications/endpoints/{type}/{name}` | read | shared path — no write |
|
|
41
|
+
| `Proxmox.NotificationMatcher` | `cluster/notifications/matchers/{name}` | read | shared path — no write |
|
|
41
42
|
| `Proxmox.HaResource` | `cluster/ha/resources/{sid}` | read | shared path — no write |
|
|
42
43
|
| `Proxmox.HaRule` | `cluster/ha/rules/{rule}` | read | shared path — no write |
|
|
43
44
|
| `Proxmox.SdnZone` | `cluster/sdn/zones/{zone}` | provision ¹ | shared path — no write |
|
|
@@ -47,6 +48,8 @@ API token on the cluster, just as it does for `alchemy plan`.
|
|
|
47
48
|
| `Pbs.PruneJob` | `config/prune/{id}` | read (PBS) | shared path — no write |
|
|
48
49
|
| `Pbs.SyncJob` | `config/sync/{id}` | read (PBS) | shared path — no write |
|
|
49
50
|
| `Pbs.VerifyJob` | `config/verify/{id}` | read (PBS) | shared path — no write |
|
|
51
|
+
| `Pbs.NotificationMatcher` | `config/notifications/matchers/{name}` | read (PBS) | shared path — no write |
|
|
52
|
+
| `Pbs.NotificationTarget` ⁵ | `config/notifications/endpoints/{type}/{name}` | read (PBS) | own: GET, PUT only for a stale group, GET — no write |
|
|
50
53
|
| `Proxmox.Acl` | `access/acl` (whole list, filtered) | provision ¹ | shared path, then its own `bound` check — no write |
|
|
51
54
|
| `Pbs.Datastore` | `config/datastore/{name}` | read (PBS) | own: GET, path/backend guards, PUT skipped on `matches`, settle GET — no write |
|
|
52
55
|
| `Proxmox.SdnApply` | 5 × `cluster/sdn/*?pending=1` + fabrics `?pending` / `?running` | read | own: counts staged objects; zero returns at once — **no `PUT /cluster/sdn`** |
|
|
@@ -65,13 +68,18 @@ no-write row applies. Pinned by `src/proxmox/lxc-adopt.test.ts` ("deploys with n
|
|
|
65
68
|
declared ratio that differs from the live one is never written on a no-op adoption and never
|
|
66
69
|
reported as a diff.
|
|
67
70
|
|
|
71
|
+
⁵ Its own `diff` and `reconcile`, because a secret's value is not in the live object: the plan
|
|
72
|
+
compares a seal kept in the previous state ([pbs-notifications.md](./pbs-notifications.md)). An
|
|
73
|
+
adopted target has no seal, so its secret values are presence-only and adoption writes nothing.
|
|
74
|
+
Pinned by `src/proxmox/pbs-notification-target-state.test.ts`.
|
|
75
|
+
|
|
68
76
|
## Before the fix: `Proxmox.CephPool`
|
|
69
77
|
|
|
70
78
|
`reconcile` read the pool and, whenever it existed, PUT the declared `size`, `min_size`,
|
|
71
79
|
`pg_autoscale_mode`, `crush_rule` (and any declared hints) to `nodes/{node}/ceph/pool/{name}`.
|
|
72
80
|
That PUT forks a `cephsetpool` worker under the provision token. PVE's `set_pool` skips each
|
|
73
81
|
setting whose value is unchanged (Tools.pm:293-300), so a matching pool was not re-tuned. It was
|
|
74
|
-
still a provision-lease write and a task on the node every time the pool was adopted.
|
|
82
|
+
still a provision-lease write and a task on the node every time the pool was adopted. C1's task
|
|
75
83
|
list shows six such tasks (2026-09-13 and 2026-09-20), one per declared pool per deploy.
|
|
76
84
|
|
|
77
85
|
## How it is pinned
|
package/docs/ownership.md
CHANGED
|
@@ -4,7 +4,8 @@ A live object this stack holds **no state for** is never taken over silently. Th
|
|
|
4
4
|
`Bao.*` family, `HostFile`, `LaunchdJob`, `CaddyConfig` and `ProxmoxLxc`, and it holds even when the
|
|
5
5
|
live object is **identical** to the declaration: identical is not ours. Another stack, a person or an old
|
|
6
6
|
script put it there, and once state claims it, a delete under `RemovalPolicy.destroy()` removes it
|
|
7
|
-
from its real owner. (Decided 2026-09-21; the Bao families adopted silently until 0.9.0.
|
|
7
|
+
from its real owner. (Decided 2026-09-21; the Bao families adopted silently until 0.9.0. 0.9.0 was
|
|
8
|
+
never published, so 0.10.0 is the first release with the rule.)
|
|
8
9
|
|
|
9
10
|
To take an object over, say so:
|
|
10
11
|
|
|
@@ -92,6 +93,30 @@ read, a resume reads as a fresh create and asks for `--adopt`. `alchemy deploy`
|
|
|
92
93
|
session both share one. An orphan's interrupted create (no longer declared) cannot be compared with
|
|
93
94
|
a declaration, so Apply leaves its object in place with a note instead of deleting it.
|
|
94
95
|
|
|
96
|
+
## After a wiped store or `state delete`, it needs `--adopt`
|
|
97
|
+
|
|
98
|
+
Alchemy's documented repair for a bad record is `alchemy state delete`, then `alchemy deploy`,
|
|
99
|
+
escalating to `--adopt` only on `OwnedBySomeoneElse`
|
|
100
|
+
([inspecting state](https://alchemy.run/cli/inspecting-state#recover-from-bad-state)). For the
|
|
101
|
+
families above that escalation always comes (decided 2026-09-21): an object whose row is gone is
|
|
102
|
+
`Unowned`, so the deploy needs `--adopt`, or `adopt(true)` on its declaration. `MeshNode`
|
|
103
|
+
([mesh-node.md](./mesh-node.md)) and `R2BucketLock` (`src/cloudflare/r2-bucket-lock.ts`) need it
|
|
104
|
+
too. A `CaddyConfig` running the declared config re-imports without it, and so does every family
|
|
105
|
+
whose `read` never answers `Unowned`: the `pveHandlers` resources (Limits), the `forgejoHandlers`
|
|
106
|
+
resources and the Talos resources. Read the takeover first with `hf-adopt-verify`
|
|
107
|
+
([adopt-verify.md](./adopt-verify.md)).
|
|
108
|
+
|
|
109
|
+
★ This departs from Alchemy's docs, not from its code. The docs say a provider with no ownership
|
|
110
|
+
marker answers plain attributes, so its objects re-import silently (`AdoptPolicy.ts`,
|
|
111
|
+
[adopting resources](https://alchemy.run/cli/adopting-resources),
|
|
112
|
+
[provider](https://alchemy.run/infrastructure-as-code/provider#read)). Many of Alchemy's own
|
|
113
|
+
marker-less providers answer `Unowned` to a read with no state instead, as the kit's families do
|
|
114
|
+
(beta.79: `Cloudflare/Snippets/Snippet.ts`, `Cloudflare/Tunnel/WarpConnector.ts`,
|
|
115
|
+
`Cloudflare/Zone/Hold.ts`). After a wiped store those need `--adopt` too.
|
|
116
|
+
|
|
117
|
+
⚠️ **That page's `alchemy state delete myapp/pr-42` fails.** A stage or a whole stack is a
|
|
118
|
+
directory, and deleting one needs `-r` (beta.79 `State/Tree.ts`). A single row does not.
|
|
119
|
+
|
|
95
120
|
## Limits
|
|
96
121
|
|
|
97
122
|
- ⚠️ **A check, then a write: not a lock.** Two resources that declare the same name in **one**
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# A PBS webhook body that posts an Alertmanager alert
|
|
2
|
+
|
|
3
|
+
`alertmanagerAlertBody()` ([alertmanager-body.ts](../src/proxmox/alertmanager-body.ts)) is the
|
|
4
|
+
body of a PBS webhook target that posts one alert to Alertmanager's `POST /api/v2/alerts`.
|
|
5
|
+
Declaring the target and the matcher that routes to it: [pbs-notifications.md](./pbs-notifications.md).
|
|
6
|
+
|
|
7
|
+
With the default options it returns exactly this text:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
[
|
|
11
|
+
{
|
|
12
|
+
"labels": {
|
|
13
|
+
{{#if fields.type}}"job_type": {{ json fields.type }},{{/if}}
|
|
14
|
+
{{#if fields.job-id}}"job_id": {{ json fields.job-id }},{{/if}}
|
|
15
|
+
{{#if fields.datastore}}"datastore": {{ json fields.datastore }},{{/if}}
|
|
16
|
+
{{#if fields.hostname}}"hostname": {{ json fields.hostname }},{{/if}}
|
|
17
|
+
"severity": {{ json severity }},
|
|
18
|
+
"alertname": "PbsNotification",
|
|
19
|
+
"source": "pbs"
|
|
20
|
+
},
|
|
21
|
+
"annotations": {
|
|
22
|
+
"summary": {{ json title }},
|
|
23
|
+
"description": {{ json message }},
|
|
24
|
+
"timestamp": "{{ timestamp }}"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The fence is `text` on purpose. As a `handlebars` fence the formatter reflowed it into
|
|
31
|
+
something the function does not return.
|
|
32
|
+
|
|
33
|
+
Options: `alertname`, `source` and an optional `generatorURL`. Braces and backslashes are
|
|
34
|
+
refused in all three. `generatorURL` must also be an absolute `http` or `https` URL, because
|
|
35
|
+
Alertmanager validates it as a URI and rejects the whole post if it is not one.
|
|
36
|
+
|
|
37
|
+
## Why it is valid JSON for every PBS notification
|
|
38
|
+
|
|
39
|
+
Each point is from the proxmox-notify, proxmox-backup and Alertmanager source, read 2026-09-22.
|
|
40
|
+
|
|
41
|
+
- **The webhook renderer has no escaping** (`register_escape_fn(no_escape)`). Every value that
|
|
42
|
+
comes from the notification goes through `json`, which is `serde_json::to_string`. It writes
|
|
43
|
+
a complete, quoted JSON string with quotes, backslashes and control characters escaped. A task
|
|
44
|
+
log with a quote, a tab or a newline stays valid JSON.
|
|
45
|
+
- **`escape` on a missing field fails the whole render**, and the notification is lost.
|
|
46
|
+
`json` on a missing field writes `null`, which is valid JSON but not a label value. So every
|
|
47
|
+
`fields.*` label sits inside `{{#if}}` and ends with its own comma, and the constant labels
|
|
48
|
+
close the object. The UI's **Test** button sends a notification with no fields at all.
|
|
49
|
+
- **Every field value is a string.** proxmox-notify turns each metadata field into a JSON string
|
|
50
|
+
before rendering. `severity` serialises as `info`, `notice`, `warning`, `error` or `unknown`,
|
|
51
|
+
and `timestamp` is an integer (epoch seconds). Nothing else in the data is rendered.
|
|
52
|
+
- **The helpers are `json`, `escape` and `url-encode`, and nothing else.** The mail templates'
|
|
53
|
+
`timestamp` and `duration` helpers are not registered for webhooks, so `timestamp` is sent as
|
|
54
|
+
epoch seconds in an annotation.
|
|
55
|
+
- **Alertmanager takes an array** (`postableAlerts`). `labels` is required and must not be empty.
|
|
56
|
+
Every label and annotation value must be a string. It drops empty labels.
|
|
57
|
+
|
|
58
|
+
`tests/alertmanager-body.test.ts` renders the template with handlebars.js set up the way
|
|
59
|
+
proxmox-notify sets up its renderer. It covers the field set of every event type in PBS's
|
|
60
|
+
`src/server/notifications/mod.rs`, the Test notification, forwarded system mail, and hostile
|
|
61
|
+
strings (quotes, backslashes, control characters, `{{`, U+2028). Each result must parse as an
|
|
62
|
+
Alertmanager v2 alert array. A scratch build of handlebars-rust 5.1.2, the major version
|
|
63
|
+
proxmox-notify pins, rendered the same cases to valid JSON. The first real notification is
|
|
64
|
+
still the check that counts: press **Test** on the target, then read what Alertmanager received.
|
|
65
|
+
|
|
66
|
+
## What it does not do
|
|
67
|
+
|
|
68
|
+
- **With no `endsAt`, Alertmanager resolves the alert itself** after `resolve_timeout` (5 min by
|
|
69
|
+
default), and PBS never sends a resolve. Treat a "resolved" notification as a timeout, not a
|
|
70
|
+
recovery.
|
|
71
|
+
- **`severity` uses PBS's names.** Route on `severity="error"`, or relabel in Alertmanager.
|
|
72
|
+
- **Alertmanager deduplicates by label set.** Two failures of the same job within
|
|
73
|
+
`resolve_timeout` are one alert, not two.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# PBS and PVE notifications — targets, matchers, and an Alertmanager webhook
|
|
2
|
+
|
|
3
|
+
`@homeflare/alchemy/proxmox` declares both halves of the Proxmox notification system:
|
|
4
|
+
|
|
5
|
+
| family | what it is | source |
|
|
6
|
+
| ----------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
7
|
+
| `Pbs.NotificationTarget` | where PBS sends: `webhook`, `smtp`, `sendmail` | [pbs-notification-target.ts](../src/proxmox/pbs-notification-target.ts) |
|
|
8
|
+
| `Pbs.NotificationMatcher` | which PBS notifications go to which targets | [pbs-notification-matcher.ts](../src/proxmox/pbs-notification-matcher.ts) |
|
|
9
|
+
| `Proxmox.NotificationMatcher` | the same on a PVE cluster | [notification-matcher.ts](../src/proxmox/notification-matcher.ts) |
|
|
10
|
+
| `Proxmox.NotificationTarget` | PVE targets (existing; declares no secret) | [notification-target.ts](../src/proxmox/notification-target.ts) |
|
|
11
|
+
| `alertmanagerAlertBody()` | a webhook body that posts one Alertmanager v2 alert | [pbs-alertmanager-body.md](./pbs-alertmanager-body.md) |
|
|
12
|
+
|
|
13
|
+
A target that no matcher names receives nothing. Declare both.
|
|
14
|
+
|
|
15
|
+
## Paging on a failed verify, sync, prune or garbage collection
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import * as Effect from 'effect/Effect';
|
|
19
|
+
import {
|
|
20
|
+
PbsNotificationMatcher,
|
|
21
|
+
PbsNotificationTarget,
|
|
22
|
+
alertmanagerAlertBody,
|
|
23
|
+
} from '@homeflare/alchemy/proxmox';
|
|
24
|
+
|
|
25
|
+
const pbs = {
|
|
26
|
+
api: 'https://pbs.example.com:8007/api2/json',
|
|
27
|
+
mount: 'pbs-mount',
|
|
28
|
+
scheme: 'pbs',
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export const notifications = Effect.gen(function* () {
|
|
32
|
+
const hook = yield* PbsNotificationTarget('alertmanager', {
|
|
33
|
+
target: pbs,
|
|
34
|
+
type: 'webhook',
|
|
35
|
+
name: 'alertmanager',
|
|
36
|
+
method: 'post',
|
|
37
|
+
url: 'https://alertmanager.example.com/api/v2/alerts',
|
|
38
|
+
header: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
Authorization: 'Bearer {{ secrets.token }}', // template text, not the token
|
|
41
|
+
},
|
|
42
|
+
secret: { token: { fromEnv: 'PBS_ALERTMANAGER_TOKEN' } }, // the token, by variable name
|
|
43
|
+
body: alertmanagerAlertBody(),
|
|
44
|
+
});
|
|
45
|
+
yield* PbsNotificationMatcher('page-on-failure', {
|
|
46
|
+
target: pbs,
|
|
47
|
+
name: 'page-on-failure',
|
|
48
|
+
targets: [hook.name], // the Output, not a string: it orders create and delete
|
|
49
|
+
'match-severity': ['error'],
|
|
50
|
+
'match-field': ['exact:type=gc,prune,sync,verify'],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Drop the `Authorization` header and the `secret` when Alertmanager takes unauthenticated posts
|
|
56
|
+
on a trusted network. The provider layer is `PbsNotificationTargetProvider()` and
|
|
57
|
+
`PbsNotificationMatcherProvider()`. `targets: [hook.name]` matters: PBS refuses a matcher that
|
|
58
|
+
names a missing target, and refuses to delete a target a matcher still names. The Output gives
|
|
59
|
+
Alchemy that ordering; a bare string does not (`pbs-notifications-stack.test.ts`).
|
|
60
|
+
|
|
61
|
+
⛔ **A datastore in `legacy-sendmail` mode never reaches a matcher.** PBS mails its `notify-user`
|
|
62
|
+
directly. `notification-system` is the default, but a datastore created before PBS 3.2 can say
|
|
63
|
+
otherwise. Check with `proxmox-backup-manager datastore show <name>`.
|
|
64
|
+
|
|
65
|
+
⚠️ **A verification job's event `type` is `verify`**, not `verification`. PBS's own event table
|
|
66
|
+
says `verification`, and the source (`send_verify_status`) sets `verify`. The other types are
|
|
67
|
+
`gc`, `prune`, `sync`, `tape-backup`, `tape-load`, `package-updates`, `acme`, `thresholds` and
|
|
68
|
+
`system-mail`. Garbage collection has no `job-id`.
|
|
69
|
+
|
|
70
|
+
⚠️ **`match-severity: ['warning', 'error']` under the default `mode: 'all'` matches nothing.**
|
|
71
|
+
Each item is combined with `mode`. The severities inside one item are OR-ed, so write
|
|
72
|
+
`['warning,error']`.
|
|
73
|
+
|
|
74
|
+
## Write-only values: `{ fromEnv }`
|
|
75
|
+
|
|
76
|
+
Alchemy stores props and attributes unencrypted, and a `Redacted` value is tagged, not
|
|
77
|
+
encrypted. A secret prop would be a secret in the state database. So `secret`, the smtp
|
|
78
|
+
`password` and any header value that is a credential are declared as `{ fromEnv: 'NAME' }`.
|
|
79
|
+
The deploying process reads the variable at call time.
|
|
80
|
+
|
|
81
|
+
| what | in state | how a plan compares it |
|
|
82
|
+
| --------------- | ---------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
83
|
+
| secret values | names (PBS returns names only), and a **seal** | seal of what this provider last wrote, checked against the env value |
|
|
84
|
+
| smtp password | the seal only (PBS returns nothing) | the same seal |
|
|
85
|
+
| header values | names, and a scrypt digest of the live pairs | digest of the declared values (env-resolved) against the digest of the live ones |
|
|
86
|
+
| everything else | the value | by value, like every family here |
|
|
87
|
+
|
|
88
|
+
The seal is `scrypt:<salt>:<digest>`, with a random salt per write. It proves "unchanged" and
|
|
89
|
+
holds nothing a reader could send to PBS. A guessable secret stays guessable, so use a random
|
|
90
|
+
token.
|
|
91
|
+
|
|
92
|
+
- **A plan without the variables is presence-only.** Names are compared and values are
|
|
93
|
+
`unknown`. `unknown` plans `noop`, never an invented update.
|
|
94
|
+
- **A write carries only the stale groups.** A PUT without `secret` or `header` leaves those on
|
|
95
|
+
the server as they are, so a changed comment deploys without the secrets.
|
|
96
|
+
- **A write that needs a missing variable fails by name**, before any request is sent.
|
|
97
|
+
- **Adoption is free.** A webhook adopted with secrets has no seal. Its values stay unverified
|
|
98
|
+
(presence-only) until the first write that carries them: a create, a changed secret name, or
|
|
99
|
+
a value rotated after that first write. A value the server already holds is never replaced by
|
|
100
|
+
adoption alone. To write yours, rename the secret once, together with its `{{ secrets.* }}`
|
|
101
|
+
references.
|
|
102
|
+
- **Rotation.** Change the variable's value and plan: the seal no longer matches, and the deploy
|
|
103
|
+
PUTs the secret group alone.
|
|
104
|
+
|
|
105
|
+
⛔ **A literal credential in a plain prop is refused.** A plain prop is stored as written, so an
|
|
106
|
+
`Authorization`, `Proxy-Authorization` or `Cookie` header, a header or URL query parameter
|
|
107
|
+
named like `token`, `key`, `secret`, `password` or `signature`, or a password in the URL's
|
|
108
|
+
userinfo must be `{ fromEnv }` or read `{{ secrets.<name> }}`. The plan fails and names the
|
|
109
|
+
field, never the value.
|
|
110
|
+
|
|
111
|
+
⚠️ **What the guard cannot see.** A token in a URL path (Slack-style webhooks), in `body` or in
|
|
112
|
+
`comment` is still a plain prop. Put it in `secret` and reference it from the template. The
|
|
113
|
+
check runs at plan only while every prop is resolved. If a prop is an unresolved Output, Alchemy
|
|
114
|
+
records the props before `reconcile` refuses them.
|
|
115
|
+
|
|
116
|
+
⚠️ **Prefer `secret` over a `{ fromEnv }` header for a credential.** PBS returns header values
|
|
117
|
+
to anyone with `Sys.Audit`, and prints a header's value unmasked when it fails to render. It
|
|
118
|
+
masks only `secret` values.
|
|
119
|
+
|
|
120
|
+
## The body template
|
|
121
|
+
|
|
122
|
+
`alertmanagerAlertBody()` has its own page, with the exact text it returns and why it is valid
|
|
123
|
+
JSON for every notification PBS sends: [pbs-alertmanager-body.md](./pbs-alertmanager-body.md).
|
|
124
|
+
|
|
125
|
+
## Adopting the built-in `default-matcher`
|
|
126
|
+
|
|
127
|
+
A matcher is compared as its whole rule: `match-*`, `mode`, `invert-match` and `disable`,
|
|
128
|
+
declared or not. Only `comment` is left alone when undeclared. Adopting a built-in means
|
|
129
|
+
declaring what it holds. Measured 2026-09-22:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// PVE 9.2.11: everything to mail-to-root
|
|
133
|
+
ProxmoxNotificationMatcher('default-matcher', {
|
|
134
|
+
target: cluster,
|
|
135
|
+
name: 'default-matcher',
|
|
136
|
+
targets: ['mail-to-root'],
|
|
137
|
+
});
|
|
138
|
+
// PBS 4.2: everything except successful prune jobs
|
|
139
|
+
PbsNotificationMatcher('default-matcher', {
|
|
140
|
+
target: pbs,
|
|
141
|
+
name: 'default-matcher',
|
|
142
|
+
targets: ['mail-to-root'],
|
|
143
|
+
'invert-match': true,
|
|
144
|
+
'match-field': ['exact:type=prune'],
|
|
145
|
+
'match-severity': ['info'],
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Both plan `noop` and deploy with no write (`notification-matcher.test.ts`). ⛔ **Deleting a
|
|
150
|
+
built-in reverts it to the shipped rule**, and the server reports success. The attributes show
|
|
151
|
+
`origin` so you can see which kind you have.
|
|
152
|
+
|
|
153
|
+
## Privileges
|
|
154
|
+
|
|
155
|
+
| server | read | write |
|
|
156
|
+
| ------ | --------------------------------------------------------------- | -------------------------------------------- |
|
|
157
|
+
| PBS | `Sys.Audit` on `/system/notifications` | `Sys.Modify` on `/system/notifications` |
|
|
158
|
+
| PVE | `Mapping.Audit` or `Mapping.Modify` on `/mapping/notifications` | `Mapping.Modify` on `/mapping/notifications` |
|
|
159
|
+
|
|
160
|
+
⚠️ A missing read privilege looks like "absent". The plan says `create` and the server answers
|
|
161
|
+
that the object already exists. Grant the read before changing anything else. On PVE,
|
|
162
|
+
`Mapping.Use` is not enough: it opens the matcher list, not the per-matcher read this package
|
|
163
|
+
makes (`get_matcher` in pve-manager's `Notifications.pm`).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Provisioning baseline — every cluster, every node
|
|
2
|
+
|
|
3
|
+
Every `@homeflare/alchemy/proxmox` resource runs as a short-lived PVE token that OpenBao mints for
|
|
4
|
+
a **provision user**. That user, and the access it holds, is the baseline a cluster needs before a
|
|
5
|
+
stack can manage anything on it. This subpath ships it as one description with two readers:
|
|
6
|
+
|
|
7
|
+
| export | what it is |
|
|
8
|
+
| --------------------------- | ------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `PROVISION_PRIVILEGES` | the provision role's privileges: 27, sorted, the exact set |
|
|
10
|
+
| `PROVISION_DEFAULTS` | the generic names in the last table, each overridable |
|
|
11
|
+
| `provisionBaseline(names)` | the resolved baseline both readers use; refuses names that are not PVE-shaped |
|
|
12
|
+
| `declareProvisionBaseline` | the baseline as `Proxmox.Role`, `Proxmox.Group`, `Proxmox.User` and `Proxmox.Acl` resources |
|
|
13
|
+
| `provisionBootstrap(names)` | the one-time root commands for a new cluster or node, as a `sh` script |
|
|
14
|
+
|
|
15
|
+
## The chicken and the egg
|
|
16
|
+
|
|
17
|
+
⛔ **The provision lane cannot create itself.** A plan needs a minted token, and a mint needs the
|
|
18
|
+
provision user to exist, be in the mint group, and hold the role on `/`. On a new cluster none of
|
|
19
|
+
that exists, so no plan can run, including the plan that would create it.
|
|
20
|
+
|
|
21
|
+
So root makes it once, and the stack takes it over from there:
|
|
22
|
+
|
|
23
|
+
1. **Bootstrap, as root, once per cluster.** Generate the script and run it on any one node:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bun -e "import { provisionBootstrap } from '@homeflare/alchemy/proxmox';
|
|
27
|
+
process.stdout.write(provisionBootstrap())" | ssh root@node-a sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Read it first: it is plain text with no secret in it. It checks each object and changes only
|
|
31
|
+
what differs, naming each step (`role HfProvisioner: adding`, `user hf-read@pve: ok`). It
|
|
32
|
+
never creates a token and never sets a password.
|
|
33
|
+
|
|
34
|
+
2. **Point OpenBao at the users.** The proxmox secrets engine mints for them through a
|
|
35
|
+
`Bao.ProxmoxRole` per lane (`mintUser: PROVISION_DEFAULTS.provisionUser`, and the read user).
|
|
36
|
+
The engine's own credential, a user allowed to manage tokens for members of the mint group, is
|
|
37
|
+
set up with the engine. It is not part of this baseline.
|
|
38
|
+
|
|
39
|
+
3. **Declare the baseline in the stack, and adopt it once:**
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import * as Effect from 'effect/Effect';
|
|
43
|
+
import { declareProvisionBaseline } from '@homeflare/alchemy/proxmox';
|
|
44
|
+
|
|
45
|
+
export const access = Effect.gen(function* () {
|
|
46
|
+
return yield* declareProvisionBaseline('pve', target);
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The first deploy finds everything live with no state. Run it with `--adopt`, or pass
|
|
51
|
+
`{ adopt: true }`. Because the script made exactly what the declaration holds, the plan is a
|
|
52
|
+
clean adoption and the deploy writes nothing. From then on, a hand edit (a privilege added in
|
|
53
|
+
the UI, a user dropped from the group) shows up as a diff on the next plan.
|
|
54
|
+
|
|
55
|
+
## Every cluster, every node
|
|
56
|
+
|
|
57
|
+
- **A new cluster:** run the bootstrap on one node, then deploy.
|
|
58
|
+
- **A node joining a cluster:** nothing to do. Users, groups, roles and ACLs live in
|
|
59
|
+
`/etc/pve/user.cfg`, which the cluster shares. Running the script there prints `ok` for every
|
|
60
|
+
step and writes nothing.
|
|
61
|
+
- **A standalone node:** it is its own cluster. Bootstrap it.
|
|
62
|
+
|
|
63
|
+
## Your own names
|
|
64
|
+
|
|
65
|
+
Every name has a generic default, and a site passes its own to both readers:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const names = { role: 'Provisioner', mintGroup: 'mint', readUser: null };
|
|
69
|
+
provisionBootstrap(names); // the script
|
|
70
|
+
declareProvisionBaseline('pve', target, names); // the resources
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `readUser: null` gives a baseline with no read lane.
|
|
74
|
+
- `readRole` must already exist (`PVEAuditor` is built in). The script stops before any write if
|
|
75
|
+
it does not.
|
|
76
|
+
- A name that is not PVE-shaped, or that would need quoting in a shell, is refused by both
|
|
77
|
+
readers before either one runs.
|
|
78
|
+
|
|
79
|
+
## What the baseline holds
|
|
80
|
+
|
|
81
|
+
| object | detail |
|
|
82
|
+
| -------------------------------- | ---------------------------------------------------------- |
|
|
83
|
+
| role `HfProvisioner` | exactly `PROVISION_PRIVILEGES`, nothing more, nothing less |
|
|
84
|
+
| group `hf-mint` | the fence: OpenBao may mint only for its members |
|
|
85
|
+
| user `hf-provision@pve` | in `hf-mint` only, enabled, no password, no expiry |
|
|
86
|
+
| user `hf-read@pve` | the same, for the read lane |
|
|
87
|
+
| `/`, `hf-provision@pve`, role | `HfProvisioner`, propagated |
|
|
88
|
+
| `/`, `hf-read@pve`, `PVEAuditor` | propagated |
|
|
89
|
+
|
|
90
|
+
★ **One privilege list for every family.** It is the union of what each resource's reconcile
|
|
91
|
+
needs. That includes managing this baseline itself (`Sys.Modify`, `Realm.AllocateUser`,
|
|
92
|
+
`User.Modify`, `Group.Allocate`, `Permissions.Modify`), so declaring it can never narrow the lane
|
|
93
|
+
out of its own repair. It does not include writes PVE keeps for `root@pam` (device passthrough,
|
|
94
|
+
bind mounts). Those are refused at plan with the command to run instead.
|
|
95
|
+
|
|
96
|
+
⚠️ **The role is the exact set.** Both the script and `Proxmox.Role` replace the privileges, never
|
|
97
|
+
add to them. A privilege added by hand is removed on the next run or deploy. If you need more,
|
|
98
|
+
declare your own role and grant.
|
|
99
|
+
|
|
100
|
+
⛔ **Everything retains.** Dropping `declareProvisionBaseline` from a stack drops its state and
|
|
101
|
+
leaves the role, users, group and grants in place. Deleting the provision user would revoke every
|
|
102
|
+
lease at once, including the one the next plan needs.
|
|
103
|
+
|
|
104
|
+
⚠️ **Adoption today.** `Proxmox.Role`, `Group`, `User` and `Acl` still read a live object with no
|
|
105
|
+
state as their own (the `pveHandlers` limit in [ownership.md](./ownership.md)). The `adopt`
|
|
106
|
+
option says what the stack means, so it keeps holding when those families follow the rule.
|
|
@@ -12,7 +12,7 @@ The adoption half of [proxmox-lxc.md](./proxmox-lxc.md). Two rules, both decided
|
|
|
12
12
|
With adoption on, a declaration that differs from the live config in any key fails the plan:
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
CT
|
|
15
|
+
CT 900 on pve1: adopting it would change memory, net1. An adoption never changes a guest, so
|
|
16
16
|
nothing is written. Declare what is live for these keys (the plan names keys, never values) and
|
|
17
17
|
plan again; change them after the adoption, as an update.
|
|
18
18
|
```
|
|
@@ -26,7 +26,7 @@ plan again; change them after the adoption, as an update.
|
|
|
26
26
|
ordinary `update`, and its plan warns with each key the deploy writes:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
Proxmox.Lxc pve1/
|
|
29
|
+
Proxmox.Lxc pve1/900: live config differs from the declaration in memory -- a deploy writes these.
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
- **The deploy asks again.** A key edited by hand between the plan and the deploy is refused at
|
package/docs/proxmox-lxc.md
CHANGED
|
@@ -16,7 +16,7 @@ export const guests = Effect.gen(function* () {
|
|
|
16
16
|
const ct = yield* ProxmoxLxc('ct', {
|
|
17
17
|
target,
|
|
18
18
|
node: 'pve1',
|
|
19
|
-
vmid:
|
|
19
|
+
vmid: 900,
|
|
20
20
|
hostname: 'ct-example',
|
|
21
21
|
cores: 4,
|
|
22
22
|
memory: 8192,
|
|
@@ -24,8 +24,8 @@ export const guests = Effect.gen(function* () {
|
|
|
24
24
|
onboot: 1,
|
|
25
25
|
unprivileged: 1,
|
|
26
26
|
features: 'nesting=1',
|
|
27
|
-
rootfs: 'local-zfs:subvol-
|
|
28
|
-
mp0: 'tank:subvol-
|
|
27
|
+
rootfs: 'local-zfs:subvol-900-disk-0,size=40G',
|
|
28
|
+
mp0: 'tank:subvol-900-disk-0,mp=/data,backup=0,size=200G',
|
|
29
29
|
net0: 'name=eth0,bridge=vmbr0,hwaddr=00:00:5E:00:53:01,ip=192.0.2.10/24,type=veth',
|
|
30
30
|
dev0: '/dev/dri/renderD128,uid=0,gid=44,mode=0660',
|
|
31
31
|
}).pipe(adopt(true));
|
|
@@ -129,7 +129,8 @@ guest that needs `/dev/net/tun` (a Mesh door) or `/dev/dri` is created in two st
|
|
|
129
129
|
1. Declare the guest without the device, and deploy.
|
|
130
130
|
2. Run the `pct set` on the node, then add `dev0` to the declaration.
|
|
131
131
|
|
|
132
|
-
`nesting` on an unprivileged guest is allowed. It needs `VM.Allocate`, which
|
|
132
|
+
`nesting` on an unprivileged guest is allowed. It needs `VM.Allocate`, which the
|
|
133
|
+
[provisioning baseline](./provision-baseline.md)'s role has.
|
|
133
134
|
|
|
134
135
|
### Gaps
|
|
135
136
|
|
package/docs/proxmox.md
CHANGED
|
@@ -12,3 +12,12 @@ or creates one from a template.
|
|
|
12
12
|
plan, named without its value ([proxmox-lxc-adopt.md](./proxmox-lxc-adopt.md)).
|
|
13
13
|
- ⛔ **root@pam-only keys** (`devN`, bind mounts, features beyond `nesting`) are refused at plan,
|
|
14
14
|
with the `pct set` to run instead. Guide: [proxmox-lxc.md](./proxmox-lxc.md).
|
|
15
|
+
- ★ **One provisioning baseline for every cluster and node**: the privilege list, a helper that
|
|
16
|
+
declares it, and the one-time root commands that bootstrap it
|
|
17
|
+
([provision-baseline.md](./provision-baseline.md)).
|
|
18
|
+
- ★ **Notifications that page**: PBS webhook, smtp and sendmail targets with write-only secrets
|
|
19
|
+
(`{ fromEnv }`, never in state), PBS and PVE matchers, and a webhook body that posts an
|
|
20
|
+
Alertmanager v2 alert ([pbs-notifications.md](./pbs-notifications.md)).
|
|
21
|
+
- ★ **Measurements in the source name a reference cluster with placeholders**: `C1`, three nodes
|
|
22
|
+
`node-b`, `node-c` and `node-d` with Ceph on a Thunderbolt mesh, and documentation addresses
|
|
23
|
+
(RFC 5737). The shapes and counts are measured; the names and addresses are not the real ones.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
package/src/caddy/admin-guard.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Refused unless the transport is at that default; declaring `admin <address>` is the fix.
|
|
21
21
|
* · a Host the new config does not allow: `origins` without it, or — with no `origins` — a Host
|
|
22
22
|
* outside Caddy's loopback defaults (`localhost`, `[::1]`, `127.0.0.1` at the listen port).
|
|
23
|
-
* Every later call answers 403 (admin.go allowedOrigins / checkHost; MEASURED on
|
|
23
|
+
* Every later call answers 403 (admin.go allowedOrigins / checkHost; MEASURED on a Mac host's
|
|
24
24
|
* Caddy 2.11.4 — `host not allowed`). The transport also sends `Origin: http://<Host>`, which
|
|
25
25
|
* Caddy checks against the same list (originAllowed), so one check covers both.
|
|
26
26
|
* · `enforce_origin` over a unix socket: this transport, like Caddy's CLI (cmd/commandfuncs.go
|
package/src/caddy/local-admin.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* ★ HOST AND ORIGIN, AS CADDY's OWN CLI SENDS THEM (cmd/commandfuncs.go AdminAPIRequest): over
|
|
13
13
|
* TCP, `Host` and `Origin: http://<host>`; over a socket, `Host: 127.0.0.1` and no Origin, because
|
|
14
14
|
* Caddy skips the Host check on unix listeners (admin.go allowedOrigins). MEASURED against the
|
|
15
|
-
*
|
|
15
|
+
* a Mac host's Caddy 2.11.4: any other Host gets 403 `host not allowed`, a forwarded port included —
|
|
16
16
|
* hence `hostHeader`.
|
|
17
17
|
*/
|
|
18
18
|
import { request as httpRequest } from 'node:http';
|
package/src/cloudflare/client.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* ⛔ THE SDK WAS ALREADY IN THE STORE AND DECLARED BY NOBODY. Measured 2026-09-15:
|
|
6
6
|
* `node_modules/.pnpm/cloudflare@4.5.0` exists as a transitive dependency of `alchemy`, and
|
|
7
7
|
* `pnpm ls cloudflare --depth 0` at the root and in every workspace package returns nothing.
|
|
8
|
-
*
|
|
8
|
+
* `<estate>/local/scripts/vendor-sdk-ratchet.ts` row R2 says exactly that — "not declared anywhere
|
|
9
9
|
* in the workspace yet … the row adds the dependency once, then converts the TS callers" — so
|
|
10
10
|
* this package is that row's first half, and the twenty ratcheted offences keep their rows.
|
|
11
11
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The vocabulary of an R2 lock rule, and the comparison that decides whether to write one.
|
|
3
3
|
*
|
|
4
|
-
* ★ ITS OWN MODULE FOR THE REASON
|
|
4
|
+
* ★ ITS OWN MODULE FOR THE REASON `<estate>/litellm/src/equal.ts` IS ONE: the comparison is the
|
|
5
5
|
* riskiest function in the provider and it deserves to be readable and testable without the
|
|
6
6
|
* lifecycle around it. An equality that returned true for everything would make every reconcile
|
|
7
7
|
* a no-op and the resource would silently never write anything — which looks exactly like a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create/PATCH bodies for `Forgejo.BranchProtection` — extracted for the 250-line cap.
|
|
3
3
|
*
|
|
4
|
-
* ★ READ OFF
|
|
4
|
+
* ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json` — create posts
|
|
5
5
|
* `CreateBranchProtectionOption`; update sends `EditBranchProtectionOption` fields only.
|
|
6
6
|
*/
|
|
7
7
|
import type { BranchProtectionProps } from './branch-protection.ts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.BranchProtection` — one rule at `/repos/{owner}/{repo}/branch_protections/{name}`.
|
|
3
3
|
*
|
|
4
|
-
* ★ READ OFF
|
|
4
|
+
* ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json`: create is POST collection;
|
|
5
5
|
* read/update/delete use the branch/rule name in the path segment `{name}`. Response carries
|
|
6
6
|
* `rule_name` (preferred) and deprecated `branch_name`.
|
|
7
7
|
*
|
package/src/forgejo/client.ts
CHANGED
|
@@ -87,7 +87,7 @@ export const forgejo = <T>(
|
|
|
87
87
|
* (src/Hetzner/Providers.ts builds on `FetchHttpClient.layer`; there is no bare fetch in its
|
|
88
88
|
* Docker, Kubernetes or GitHub providers). It buys a typed error channel, interruption when
|
|
89
89
|
* a plan is cancelled, the runtime's tracing, and a client a test can replace without
|
|
90
|
-
* monkey-patching a global.
|
|
90
|
+
* monkey-patching a global. <estate>/proxmox/src/resource.ts carries the same note.
|
|
91
91
|
*/
|
|
92
92
|
const client = yield* HttpClient.HttpClient;
|
|
93
93
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `Forgejo.OrgSecret` — org-level Actions secret metadata at `/orgs/{org}/actions/secrets/{name}`.
|
|
3
3
|
*
|
|
4
4
|
* ★ CHOICE: AUTHORED WITH WRITE-ONLY VALUE VIA ENV, NOT REFUSED. The OpenAPI in
|
|
5
|
-
*
|
|
5
|
+
* `<estate>/mcp-servers/docs/api/upstream/forgejo.json` shows `Secret` returns only `name` and
|
|
6
6
|
* `created_at` — never the `data` field — so attributes stay metadata-only. Create/update is PUT
|
|
7
7
|
* with body `{ data }`, which does not fit POST/PATCH; this family uses the `upsert` seam on
|
|
8
8
|
* `ForgejoSpec` instead of hand-rolling provider handlers.
|
package/src/forgejo/org-label.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.OrgLabel` — one org-level issue label shared across every repository in the org.
|
|
3
3
|
*
|
|
4
|
-
* ★ RETIRES `
|
|
4
|
+
* ★ RETIRES the estate's hand-run `labels-reconcile.sh`, which
|
|
5
5
|
* converged the whole `labels.yaml` file in one shot. That script CREATED and PATCHED labels
|
|
6
6
|
* from the spec and REPORTED `EXTRA` labels present live but absent from the file — it never
|
|
7
7
|
* deleted them, because removing a label strips it from every issue that carries it.
|
package/src/forgejo/org-team.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.OrgTeam` — one organization team under `/orgs/{org}/teams`.
|
|
3
3
|
*
|
|
4
|
-
* ★ READ OFF
|
|
4
|
+
* ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json` (Forgejo 16.x OpenAPI): create is
|
|
5
5
|
* `POST /orgs/{org}/teams`; read/update/delete use numeric id at `/teams/{id}` — not the name.
|
|
6
6
|
*
|
|
7
7
|
* ⚠️ PATCH AND DELETE USE `/teams/{id}` — same seam as org labels. `locate` lists by name;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.RepoWebhook` — one repository webhook under `/repos/{owner}/{repo}/hooks`.
|
|
3
3
|
*
|
|
4
|
-
* ★ READ OFF
|
|
4
|
+
* ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json`: create is POST collection; read/update/delete
|
|
5
5
|
* use numeric hook id at `/repos/{owner}/{repo}/hooks/{id}`. `locate` lists and matches on `name`.
|
|
6
6
|
*
|
|
7
7
|
* ⛔ NO WEBHOOK `secret` IN PROPS OR ATTRIBUTES. Optional HMAC material is env-only at write time
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.Repository` — an organization-owned git repository.
|
|
3
3
|
*
|
|
4
|
-
* ★ MEASURED 2026-09-13 against the live daemon (`GET /api/v1/repos
|
|
4
|
+
* ★ MEASURED 2026-09-13 against the live daemon (`GET /api/v1/repos/<org>/<repo>`):
|
|
5
5
|
* responses are flat JSON with `private`, `has_issues`, `default_branch`, etc. Create is
|
|
6
6
|
* `POST /orgs/{org}/repos`; read/update/delete use `/repos/{owner}/{repo}`.
|
|
7
7
|
*
|
|
@@ -15,7 +15,7 @@ import { type ForgejoRequirements, forgejoHandlers } from './resource.ts';
|
|
|
15
15
|
import { bool, text } from './values.ts';
|
|
16
16
|
|
|
17
17
|
export interface RepositoryProps {
|
|
18
|
-
/** Organization login — the owner segment in
|
|
18
|
+
/** Organization login — the owner segment in `<org>/<repo>`. */
|
|
19
19
|
org: string;
|
|
20
20
|
/** Repository name. Changing it is a replace, not an update. */
|
|
21
21
|
name: string;
|