@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/src/forgejo/resource.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { ForgejoError, forgejo } from './client.ts';
|
|
|
28
28
|
/**
|
|
29
29
|
* ★ WHAT EVERY OPERATION HERE NEEDS FROM THE RUNTIME — and it is NOT what the first draft said.
|
|
30
30
|
*
|
|
31
|
-
* These resources declared `ChildProcessSpawner`, copied from
|
|
31
|
+
* These resources declared `ChildProcessSpawner`, copied from <estate>/proxmox, where it is there
|
|
32
32
|
* because a PVE credential is minted by shelling out to `bao`. Nothing in this package shells
|
|
33
33
|
* out: the token comes from `FORGEJO_TOKEN` in the environment. Declaring a service you never
|
|
34
34
|
* use is not harmless — it makes a stack provide a layer for nothing, and it hides the one
|
|
@@ -152,7 +152,7 @@ export const forgejoOperations = <Props extends object, Attributes>(
|
|
|
152
152
|
* The five provider handlers for a spec'd Forgejo object, wired once.
|
|
153
153
|
*
|
|
154
154
|
* ⛔ IT STOPS AT THE HANDLERS AND DOES NOT RETURN THE LAYER — same reasoning as pveHandlers in
|
|
155
|
-
*
|
|
155
|
+
* <estate>/proxmox: wrapping `Provider.effect` here needs casts against Alchemy's `Props<R>`.
|
|
156
156
|
*
|
|
157
157
|
* ⚠️ `list` answers empty — Forgejo index endpoints return the whole org; adoption stays explicit.
|
|
158
158
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ★ WHY IT EXISTS: forgejo-provision, the service account the Forgejo engine mints as, belongs to the
|
|
5
5
|
* HomeFlare Owners team — Tim, 2026-09-14. Membership is what its tokens can reach; the role's
|
|
6
|
-
* scopes only cap it (
|
|
6
|
+
* scopes only cap it (<estate>/openbao/src/forgejo-bootstrap.ts).
|
|
7
7
|
*
|
|
8
8
|
* ★ READ OFF THE LIVE 16.0.3 SWAGGER (127.0.0.1:3000/swagger.v1.json, 2026-09-14): GET, PUT and
|
|
9
9
|
* DELETE `/teams/{id}/members/{username}` answer 200, 204 and 204, and 404 when absent. The team
|
package/src/openbao/README.md
CHANGED
|
@@ -17,7 +17,7 @@ plus `FetchHttpClient.layer`.
|
|
|
17
17
|
- ⛔ A rename or move onto a name or path that already exists fails the plan, before anything is
|
|
18
18
|
written (every family but the mounts, whose moves are below). Move through a free name, in two
|
|
19
19
|
deploys, or remove the target first.
|
|
20
|
-
- ⛔ **Nothing is adopted without `--adopt`** (0.9.0), not even an object identical to the
|
|
20
|
+
- ⛔ **Nothing is adopted without `--adopt`** (0.9.0, first published as 0.10.0), not even an object identical to the
|
|
21
21
|
declaration: a live name with no state fails the plan, or the apply when a prop was an Output.
|
|
22
22
|
Our own interrupted create still resumes when its row can prove it — not one killed while a
|
|
23
23
|
prop was still an Output. [docs/ownership.md](../../docs/ownership.md). Change
|
|
@@ -105,10 +105,10 @@ export const machines = Effect.gen(function* () {
|
|
|
105
105
|
});
|
|
106
106
|
yield* BaoKubernetesRole('pod', {
|
|
107
107
|
aliasNameSource: 'serviceaccount_name',
|
|
108
|
-
boundServiceAccountNames: ['
|
|
108
|
+
boundServiceAccountNames: ['llm-gateway'],
|
|
109
109
|
boundServiceAccountNamespaces: ['ai'],
|
|
110
|
-
name: 'ai-
|
|
111
|
-
tokenPolicies: ['ai-
|
|
110
|
+
name: 'ai-gateway',
|
|
111
|
+
tokenPolicies: ['ai-gateway'],
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
114
|
```
|
package/src/openbao/auth-role.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ★ `defaultRemovalPolicy: 'retain'` — deleting a role locks out every host that logs in with it:
|
|
9
9
|
* its secret_ids go with it (the ⚠️ on `delete` below). Opt in with
|
|
10
|
-
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
10
|
+
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
11
11
|
*/
|
|
12
12
|
import { Resource } from 'alchemy';
|
|
13
13
|
import { isResolved } from 'alchemy/Diff';
|
|
@@ -31,11 +31,11 @@ describe('baoRead', () => {
|
|
|
31
31
|
() => reply,
|
|
32
32
|
async (bao) => {
|
|
33
33
|
const env = { BAO_ADDR: bao.address, BAO_NAMESPACE: 'homeflare', BAO_TOKEN: TOKEN };
|
|
34
|
-
const data = await run(env, baoRead('proxmox-
|
|
34
|
+
const data = await run(env, baoRead('proxmox-c1/roles/read'));
|
|
35
35
|
assert.deepEqual(data, { mint_user: 'hf-read@pve', ttl: 3600 });
|
|
36
36
|
const [seen] = bao.seen;
|
|
37
37
|
assert.equal(seen?.method, 'GET');
|
|
38
|
-
assert.equal(seen?.path, '/v1/proxmox-
|
|
38
|
+
assert.equal(seen?.path, '/v1/proxmox-c1/roles/read');
|
|
39
39
|
assert.equal(seen?.headers.get('x-vault-namespace'), 'homeflare');
|
|
40
40
|
assert.equal(seen?.headers.get('x-vault-token'), TOKEN);
|
|
41
41
|
assert.equal(seen?.headers.get('x-vault-request'), 'true');
|
package/src/openbao/bao-http.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* One call to OpenBao's HTTP API through Effect's `HttpClient` — the transport every Bao.* family
|
|
3
3
|
* uses since it stopped shelling out to `bao`.
|
|
4
4
|
*
|
|
5
|
-
* ★ `HttpClient`, THE SAME PATTERN
|
|
5
|
+
* ★ `HttpClient`, THE SAME PATTERN <estate>/proxmox and <estate>/forgejo USE AGAINST THEIR APIs, decided
|
|
6
6
|
* 2026-09-14. It buys a typed error channel, interruption when a plan is cancelled, the runtime's
|
|
7
7
|
* tracing, and a client a test can point at a fake server — and it needs no Alchemy session, so
|
|
8
8
|
* every call here works from all four operations. (`CommandExecutor.run` was tried on Bao.Policy
|
|
9
9
|
* first and blocked read/diff: Alchemy passes a session to reconcile/delete but NOT to read/diff,
|
|
10
10
|
* Provider.ts:258-289. The ChildProcessSpawner that fixed that is gone with the CLI.)
|
|
11
11
|
* ⚠️ THE STACK MUST PROVIDE `FetchHttpClient.layer`. alchemy.run.ts does, the way
|
|
12
|
-
*
|
|
12
|
+
* <estate>/proxmox/alchemy.run.ts does.
|
|
13
13
|
*/
|
|
14
14
|
import * as Context from 'effect/Context';
|
|
15
15
|
import * as Effect from 'effect/Effect';
|
|
@@ -34,7 +34,7 @@ export const BaoEnv = Context.Reference<BaoEnvironment>('homeflare/openbao/BaoEn
|
|
|
34
34
|
/**
|
|
35
35
|
* ⛔ AT MOST EIGHT EXCHANGES WITH OPENBAO AT ONCE — ACROSS EVERY Bao.* FAMILY, IN ONE PROCESS.
|
|
36
36
|
*
|
|
37
|
-
* 🔴 MEASURED 2026-09-14 10:41: A DEPLOY OF THIS STACK TOOK OPENBAO's API OFF
|
|
37
|
+
* 🔴 MEASURED 2026-09-14 10:41: A DEPLOY OF THIS STACK TOOK OPENBAO's API OFF ITS HOST. Alchemy fans
|
|
38
38
|
* resources out with a hard-coded `concurrency: "unbounded"` (alchemy Plan.ts:476, Apply.ts:259,
|
|
39
39
|
* :263, :303 — no option reaches it), and 555 Bao.CloudflareRole resources each read, diff,
|
|
40
40
|
* reconcile and read back: well over a thousand requests at one loopback listener within seconds.
|
|
@@ -42,7 +42,7 @@ export const BaoEnv = Context.Reference<BaoEnvironment>('homeflare/openbao/BaoEn
|
|
|
42
42
|
* HTTP server (listening on 127.0.0.1:8200) exited with error: set tcp
|
|
43
43
|
* 127.0.0.1:8200->127.0.0.1:57974: setsockopt: invalid argument
|
|
44
44
|
* and KEPT RUNNING with 8200 closed. launchd saw a live process and restarted nothing. Everything
|
|
45
|
-
* on the
|
|
45
|
+
* on the vault host that talks to 127.0.0.1:8200 was cut off, starting with the agent's templates, until
|
|
46
46
|
* a manual restart.
|
|
47
47
|
* ★ THE LISTENER DYING IS A GO-ON-DARWIN FAILURE, NOT OURS TO FIX. Traefik lost its entrypoints the
|
|
48
48
|
* same way on darwin/arm64 (traefik issue 8841). The BURST is ours. The adoption deploy an hour
|
|
@@ -70,7 +70,7 @@ const TIMEOUT = '60 seconds';
|
|
|
70
70
|
/**
|
|
71
71
|
* Mesh in front of a remote OpenBao drops connections under Alchemy's unbounded
|
|
72
72
|
* fan-out. Status 0 is transport, not an OpenBao 4xx — retry twice. Measured
|
|
73
|
-
* 2026-09-16 against
|
|
73
|
+
* 2026-09-16 against bao.example.internal: a 585-role plan died mid-diff with
|
|
74
74
|
* `no response: (no errors given)` while the vault stayed unsealed.
|
|
75
75
|
*/
|
|
76
76
|
const retryTransport = <A, R>(effect: Effect.Effect<A, BaoError, R>) =>
|
|
@@ -136,7 +136,7 @@ export const baoCall = (
|
|
|
136
136
|
const address = resolveAddress(env);
|
|
137
137
|
const client = yield* HttpClient.HttpClient;
|
|
138
138
|
// ⚠️ The content type goes on `bodyText`, never in the header map: `bodyText` overwrites it.
|
|
139
|
-
//
|
|
139
|
+
// <estate>/proxmox/src/client.ts has the scar.
|
|
140
140
|
const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(
|
|
141
141
|
HttpClientRequest.setHeaders(headersFor(address, env)),
|
|
142
142
|
body === undefined
|
|
@@ -86,7 +86,7 @@ describe('settle', () => {
|
|
|
86
86
|
|
|
87
87
|
// ⛔ A success body can hold a credential, so a malformed one is described and never quoted.
|
|
88
88
|
it('fails an unparseable success body without quoting it', () => {
|
|
89
|
-
const error = errorOf(settle('read', 'GET', 'pbs-
|
|
89
|
+
const error = errorOf(settle('read', 'GET', 'pbs-c1/creds/read', 200, 'secret=abc123 {'));
|
|
90
90
|
assert.ok(!error.message.includes('abc123'));
|
|
91
91
|
});
|
|
92
92
|
|
|
@@ -121,7 +121,7 @@ export const settle = (
|
|
|
121
121
|
if (errors === undefined) {
|
|
122
122
|
// ⛔ DESCRIBED, NEVER QUOTED. A proxy or gateway error page can echo the request back, headers
|
|
123
123
|
// and all, so quoting it could carry X-Vault-Token into an error that Alchemy logs (grok,
|
|
124
|
-
// 2026-09-14). mint in
|
|
124
|
+
// 2026-09-14). mint in <estate>/proxmox/src/credentials.ts already did it this way.
|
|
125
125
|
return fail([`not an OpenBao error body (${String(text.length)} bytes)`]);
|
|
126
126
|
}
|
|
127
127
|
if (status === 404 && intent !== 'write') return { body: undefined };
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* permission-groups`: `Logs Read` is listed twice, `6a315a56…` at `com.cloudflare.api.account`
|
|
11
11
|
* and `c4a30cd5…` at `com.cloudflare.api.account.zone`. The second is what the four live
|
|
12
12
|
* security roles carry on their ACCOUNT resource, and what
|
|
13
|
-
*
|
|
13
|
+
* <estate>/nix/scripts/cf-mint-analytics-tokens.py:80 pins as PG_LOGS_ZONE.
|
|
14
14
|
*/
|
|
15
15
|
import assert from 'node:assert/strict';
|
|
16
16
|
import { describe, it } from 'node:test';
|
|
@@ -143,7 +143,7 @@ describe('permissionGroupsFromEngine', () => {
|
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
|
-
* ⛔ r2-backup-
|
|
146
|
+
* ⛔ r2-backup-host's ENTRY IS BUCKET-SCOPED (`bucket: example-db-backup`), and the two groups it
|
|
147
147
|
* names are the only two the account lists at `com.cloudflare.edge.r2.bucket` — MEASURED
|
|
148
148
|
* 2026-09-15: 2 of 399. A unique name still resolves whatever scope asks, which is why the
|
|
149
149
|
* account-scoped `Workers R2 Storage Read` here answers a bucket-scoped question too.
|
|
@@ -103,7 +103,7 @@ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
|
103
103
|
* ⛔ A 403 IS A MISSING GRANT, AND IT SAYS SO. `BaoError` alone reports `GET /v1/<mount>/
|
|
104
104
|
* permission-groups -> 403: permission denied` — true, and it points nowhere: the path is
|
|
105
105
|
* deployed, the token is valid, and the one thing to change is a policy file. MEASURED 2026-09-15
|
|
106
|
-
* 10:19 on the
|
|
106
|
+
* 10:19 on the host's admin lane, where `bao write sys/capabilities-self` answered `[deny]` on
|
|
107
107
|
* this exact path. bao-status.ts argues that a failure must name what it is; this is that
|
|
108
108
|
* argument one level up.
|
|
109
109
|
*/
|
|
@@ -147,7 +147,7 @@ const engineGroups = (mount: string): Lookup =>
|
|
|
147
147
|
if (data === undefined) {
|
|
148
148
|
// ⚠️ 404 IS NOW A MOUNT-SHAPED ANSWER, NOT THE EXPECTED ONE. The path IS deployed
|
|
149
149
|
// estate-wide (measured 2026-09-15), so a 404 here means this mount is served by an older
|
|
150
|
-
// plugin binary or BAO_ADDR is not the
|
|
150
|
+
// plugin binary or BAO_ADDR is not the vault host — never "no groups".
|
|
151
151
|
const why = 'GET <mount>/permission-groups answered 404 — the endpoint is not deployed here';
|
|
152
152
|
return Effect.fail(new CloudflareGroupsError(mount, [why]));
|
|
153
153
|
}
|
|
@@ -231,7 +231,7 @@ export const permissionGroupsFromEngine = () =>
|
|
|
231
231
|
* undeclared live role carries IDs with no names to pair them with — and solves the account at
|
|
232
232
|
* once.
|
|
233
233
|
* 🔴 WHAT RETIRED IT, 2026-09-15 10:13: `alchemy deploy` could not create
|
|
234
|
-
* `cloudflare-{<account>,testing}-platform/roles/r2-backup-
|
|
234
|
+
* `cloudflare-{<account>,testing}-platform/roles/r2-backup-host` — `unknown group "Workers R2
|
|
235
235
|
* Storage Bucket Item Write" — no live role on account <account> carries it; a NEW group needs
|
|
236
236
|
* the engine endpoint`. That is this source's documented blind spot arriving, not a bug in it.
|
|
237
237
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* A Cloudflare policy document as the engine stores it — resolution from names, parsing from a
|
|
3
3
|
* read, and the one comparison every Bao.CloudflareRole plan rests on.
|
|
4
4
|
*
|
|
5
|
-
* MEASURED from the engine source (
|
|
5
|
+
* MEASURED from the engine source (<estate>/platform/secrets/vault/plugin/cloudflare/path_roles.go)
|
|
6
6
|
* and the 2026-09-14 snapshot of all 586 live roles:
|
|
7
7
|
* • `policies` is a STRING field holding a JSON array (path_roles.go:67-72). Each entry is
|
|
8
8
|
* `{effect, resources: map[string]string, permission_groups: [{id}]}` (:25-33).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Props, attributes and field comparison for Bao.CloudflareRole — the resource is
|
|
3
3
|
* cloudflare-role.ts, the policy document rules are cloudflare-policy.ts.
|
|
4
4
|
*
|
|
5
|
-
* MEASURED from the engine (
|
|
5
|
+
* MEASURED from the engine (<estate>/platform/secrets/vault/plugin/cloudflare/path_roles.go) and the
|
|
6
6
|
* 2026-09-14 snapshot of all 586 live roles:
|
|
7
7
|
* • A role is name, description, policies, ttl and max_ttl AND NOTHING ELSE (path_roles.go:14-20
|
|
8
8
|
* stores those; :204-210 returns them). Every snapshot read carries exactly those five keys, so
|
|
@@ -79,7 +79,7 @@ export interface RoleDifference {
|
|
|
79
79
|
readonly have: string;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** `cloudflare-<account>-dns/roles/
|
|
82
|
+
/** `cloudflare-<account>-dns/roles/example-dns-read` — read, write and delete use it. */
|
|
83
83
|
export const rolePath = (mount: string, name: string) => `${mountPath(mount)}/roles/${name}`;
|
|
84
84
|
|
|
85
85
|
const seconds = (value: unknown) =>
|
|
@@ -44,7 +44,7 @@ export interface DeclaredPolicy {
|
|
|
44
44
|
* ⛔ IT MOVES THE LOOKUP, NEVER THE RESOURCE. The entry still binds the groups to the resource
|
|
45
45
|
* above; only the id chosen for each name changes. That is the shape Cloudflare calls "all
|
|
46
46
|
* zones from an account" — the ACCOUNT resource carrying ZONE-scoped groups
|
|
47
|
-
* (
|
|
47
|
+
* (<estate>/nix/scripts/cf-mint-analytics-tokens.py:155-158, beside a working
|
|
48
48
|
* token). Without this field the resource is the only witness, and it says `account`.
|
|
49
49
|
*/
|
|
50
50
|
readonly scope?: string;
|
package/src/openbao/digest.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* The only form a policy body may be hashed or compared in.
|
|
9
9
|
*
|
|
10
10
|
* ⛔ OPENBAO STRIPS THE TRAILING NEWLINE AND THAT ALONE WAS A FOREVER-DIFF. MEASURED on the live
|
|
11
|
-
* server: the assembler emits each fragment followed by a newline, so `
|
|
11
|
+
* server: the assembler emits each fragment followed by a newline, so `app-llm` goes in as
|
|
12
12
|
* 41 lines and `bao policy read` hands back 40 — identical text, one `\n` apart, two different
|
|
13
13
|
* digests. Every plan therefore reported `update`, every deploy rewrote the same policy, and the
|
|
14
14
|
* stack could never settle. It was invisible until the stack planned for the first time today.
|
|
@@ -29,11 +29,11 @@ describe('usernamesFrom', () => {
|
|
|
29
29
|
test('reads the Username column of the measured table', () => {
|
|
30
30
|
const table = [
|
|
31
31
|
'ID Username Email IsActive IsAdmin 2FA',
|
|
32
|
-
'1
|
|
32
|
+
'1 alice alice@example.invalid true true false',
|
|
33
33
|
'7 forgejo-provision forgejo-provision@noreply.example true false false',
|
|
34
34
|
'',
|
|
35
35
|
].join('\n');
|
|
36
|
-
expect(usernamesFrom(table)).toEqual(['
|
|
36
|
+
expect(usernamesFrom(table)).toEqual(['alice', 'forgejo-provision']);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test('refuses a table it does not recognise rather than reporting nobody', () => {
|
|
@@ -63,7 +63,7 @@ describe('readCreateOutput', () => {
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
test('reports not created when the success line names someone else or is missing', () => {
|
|
66
|
-
expect(readCreateOutput("New user '
|
|
66
|
+
expect(readCreateOutput("New user 'alice' has been successfully created!", user).created).toBe(
|
|
67
67
|
false,
|
|
68
68
|
);
|
|
69
69
|
const noPassword = readCreateOutput(
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* read off the running 16.0.3 daemon or its source on 2026-09-14, not assumed.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/** Where the
|
|
8
|
+
/** Where the host's Forgejo keeps its state (its NixOS module and `app.ini`). */
|
|
9
9
|
export const FORGEJO_WORK_PATH = '/opt/homeflare/forgejo';
|
|
10
|
-
/** The file the launchd runner renders at every start
|
|
10
|
+
/** The file the launchd runner renders at every start. Readable by the account that runs Forgejo. */
|
|
11
11
|
export const FORGEJO_CONFIG = `${FORGEJO_WORK_PATH}/custom/conf/app.ini`;
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* straight to the Forgejo engine's role and nowhere else.
|
|
4
4
|
*
|
|
5
5
|
* ★ TIM'S CALL, 2026-09-14: "Random password, piped", and membership of the HomeFlare Owners team
|
|
6
|
-
* (
|
|
6
|
+
* (<estate>/forgejo, Forgejo.TeamMember). Forgejo has no service-account type and no app.ini setting
|
|
7
7
|
* that creates users — the bot type is database-only (codeberg issue 8136) — so this is an
|
|
8
8
|
* ordinary user made by `forgejo admin user create`.
|
|
9
9
|
*
|
|
@@ -34,12 +34,12 @@ import {
|
|
|
34
34
|
} from './forgejo-bootstrap-parse.ts';
|
|
35
35
|
|
|
36
36
|
export const PROVISION_USER = 'forgejo-provision';
|
|
37
|
-
/** `NO_REPLY_ADDRESS` in
|
|
37
|
+
/** `NO_REPLY_ADDRESS` in the host's Forgejo `app.ini` — a service account's mail goes nowhere. */
|
|
38
38
|
const PROVISION_EMAIL = `${PROVISION_USER}@noreply.lab.example.com`;
|
|
39
39
|
export const ROLE_PATH = 'forgejo/roles/provision';
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* ★ THE SCOPES ARE WHAT
|
|
42
|
+
* ★ THE SCOPES ARE WHAT <estate>/forgejo WRITES TODAY. Org labels, teams and org secrets need
|
|
43
43
|
* `write:organization`; repositories, webhooks and branch protection need `write:repository`.
|
|
44
44
|
* Membership decides what the tokens can reach, and the scopes cap it.
|
|
45
45
|
* ⚠️ FORGEJO TOKENS NEVER EXPIRE ON THE SERVER. The TTL is OpenBao's lease, and revoking the lease is
|
|
@@ -92,7 +92,7 @@ export const bootstrapProvisionAccount = Effect.gen(function* () {
|
|
|
92
92
|
const binary = runningForgejoBinary(ps.stdout);
|
|
93
93
|
if (binary === undefined) {
|
|
94
94
|
return yield* refuse(
|
|
95
|
-
'no running `forgejo web` found. Run this on the
|
|
95
|
+
'no running `forgejo web` found. Run this on the host where Forgejo runs.',
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
const cli = (subcommand: string, ...args: string[]) =>
|
|
@@ -14,16 +14,16 @@ import {
|
|
|
14
14
|
|
|
15
15
|
const POD: BaoKubernetesRoleProps = {
|
|
16
16
|
aliasNameSource: 'serviceaccount_name',
|
|
17
|
-
boundServiceAccountNames: ['
|
|
17
|
+
boundServiceAccountNames: ['llm-gateway'],
|
|
18
18
|
boundServiceAccountNamespaces: ['ai'],
|
|
19
|
-
name: 'ai-
|
|
20
|
-
tokenPolicies: ['ai-
|
|
19
|
+
name: 'ai-gateway',
|
|
20
|
+
tokenPolicies: ['ai-gateway'],
|
|
21
21
|
tokenTtl: '1h',
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const LIVE: Record<string, unknown> = {
|
|
25
25
|
alias_name_source: 'serviceaccount_name',
|
|
26
|
-
bound_service_account_names: ['
|
|
26
|
+
bound_service_account_names: ['llm-gateway'],
|
|
27
27
|
bound_service_account_namespace_selector: '',
|
|
28
28
|
bound_service_account_namespaces: ['ai'],
|
|
29
29
|
token_bound_cidrs: [],
|
|
@@ -32,7 +32,7 @@ const LIVE: Record<string, unknown> = {
|
|
|
32
32
|
token_no_default_policy: false,
|
|
33
33
|
token_num_uses: 0,
|
|
34
34
|
token_period: 0,
|
|
35
|
-
token_policies: ['ai-
|
|
35
|
+
token_policies: ['ai-gateway'],
|
|
36
36
|
token_ttl: 3600,
|
|
37
37
|
token_type: 'default',
|
|
38
38
|
};
|
package/src/openbao/mount.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* in OpenBao, not Alchemy state.
|
|
7
7
|
*
|
|
8
8
|
* ★ `defaultRemovalPolicy: 'retain'` — disabling a mount destroys every secret under it.
|
|
9
|
-
* Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
9
|
+
* Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
10
10
|
*
|
|
11
11
|
* ★ REPLACE SEMANTICS (audited 2026-09-21, see src/openbao/REPLACE.md):
|
|
12
12
|
* · `type` changed → plans `replace`, and the APPLY FAILS, destroying nothing: the new
|
package/src/openbao/pki-role.ts
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* never a policy one — but neither is a measurement.
|
|
37
37
|
*
|
|
38
38
|
* ★ `defaultRemovalPolicy: 'retain'` — see the delete comment below. Opt in with
|
|
39
|
-
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
39
|
+
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
40
40
|
*/
|
|
41
41
|
import { Resource } from 'alchemy';
|
|
42
42
|
import { isResolved } from 'alchemy/Diff';
|
|
@@ -28,11 +28,11 @@ describe('policy wire', () => {
|
|
|
28
28
|
it('round-trips through the stripped newline with equal digests', async () => {
|
|
29
29
|
await withFake(aclPolicies(), async (bao) => {
|
|
30
30
|
const env = { BAO_ADDR: bao.address };
|
|
31
|
-
await run(env, writePolicy('
|
|
31
|
+
await run(env, writePolicy('app-llm', ASSEMBLED));
|
|
32
32
|
assert.equal(bao.seen[0]?.method, 'PUT');
|
|
33
|
-
assert.equal(bao.seen[0]?.path, `${PREFIX}
|
|
33
|
+
assert.equal(bao.seen[0]?.path, `${PREFIX}app-llm`);
|
|
34
34
|
assert.deepEqual(JSON.parse(bao.seen[0]?.body ?? ''), { policy: ASSEMBLED });
|
|
35
|
-
const live = await run(env, readPolicy('
|
|
35
|
+
const live = await run(env, readPolicy('app-llm'));
|
|
36
36
|
assert.notEqual(live, ASSEMBLED, 'the fake strips the newline, as OpenBao does');
|
|
37
37
|
assert.equal(sha256(live), sha256(ASSEMBLED));
|
|
38
38
|
});
|
package/src/openbao/policy.ts
CHANGED
|
@@ -26,8 +26,8 @@ import { guardRename, judgeRename, nameIdentity } from './rename-identity.ts';
|
|
|
26
26
|
* ⛔ THERE IS NO `Bao.Secret`, AND THERE MUST NEVER BE ONE. Alchemy persists attributes to
|
|
27
27
|
* its state store and does NOT encrypt them: StateEncoding.ts unwraps `Redacted` to
|
|
28
28
|
* plaintext under a marker key, and nothing under alchemy/src/State encrypts. This
|
|
29
|
-
* estate's state store is the `alchemy` Postgres database, which
|
|
30
|
-
*
|
|
29
|
+
* estate's state store is the `alchemy` Postgres database, which a nightly job dumps
|
|
30
|
+
* (it enumerates EVERY non-template database), copies to a backup guest, and PBS backs up
|
|
31
31
|
* from there. A KV value here would exist in four places, none of them OpenBao.
|
|
32
32
|
*
|
|
33
33
|
* So this declares the SHAPE AROUND secrets. Every attribute it stores is a name, a
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* MEASURED 2026-09-13, read-only, against the live server (BAO_ADDR=http://127.0.0.1:8200,
|
|
5
5
|
* BAO_NAMESPACE=homeflare). No write of any kind was issued to a role or a mount:
|
|
6
6
|
*
|
|
7
|
-
* bao read -format=json proxmox-
|
|
7
|
+
* bao read -format=json proxmox-c1/roles/read
|
|
8
8
|
* → { max_ttl: 21600, mint_user: "hf-read@pve", name: "read", ttl: 3600 }
|
|
9
|
-
* bao read -format=json proxmox-
|
|
9
|
+
* bao read -format=json proxmox-c1/roles/provision
|
|
10
10
|
* → { max_ttl: 1800, mint_user: "hf-provision@pve", name: "provision", ttl: 300 }
|
|
11
|
-
* GET /v1/proxmox-
|
|
11
|
+
* GET /v1/proxmox-c1/?help=1 → components.schemas.WriteRolesNameRequest:
|
|
12
12
|
* mint_user string — the ONLY required field, described as "The `user@realm` whose
|
|
13
13
|
* standing ACL is the minted token's entire privilege set. Created
|
|
14
14
|
* out-of-band; this engine never grants ACLs."
|
|
@@ -24,7 +24,7 @@ import { sha256 } from './digest.ts';
|
|
|
24
24
|
import { mountPath, parseDuration, ttlSeconds } from './mount-form.ts';
|
|
25
25
|
|
|
26
26
|
export interface BaoProxmoxRoleProps {
|
|
27
|
-
/** Mount path of the proxmox secrets engine, e.g. `proxmox-
|
|
27
|
+
/** Mount path of the proxmox secrets engine, e.g. `proxmox-c1`. A trailing slash is fine. */
|
|
28
28
|
mount: string;
|
|
29
29
|
/**
|
|
30
30
|
* Role name, as `<mount>/roles/<name>` takes it.
|
|
@@ -105,7 +105,7 @@ const ttlText = (seconds: number | undefined, fallback = '0') => {
|
|
|
105
105
|
return `${String(seconds)}s`;
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
/** `proxmox-
|
|
108
|
+
/** `proxmox-c1/roles/read` — read, write and delete all address the role here. */
|
|
109
109
|
export const rolePath = (mount: string, name: string) => `${mountPath(mount)}/roles/${name}`;
|
|
110
110
|
|
|
111
111
|
export const attributesOf = (
|
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
* mint user, plus the lease ceilings that consumer's tokens get. METADATA ONLY.
|
|
4
4
|
*
|
|
5
5
|
* ★ WHY THIS FAMILY MATTERS MORE THAN IT LOOKS. Declaring these roles is how the estate
|
|
6
|
-
* replaces HAND-MADE PERMANENT PVE TOKENS with short-lived minted ones. On
|
|
7
|
-
* carry expire=0 and privsep=0 — root@pam!
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* replaces HAND-MADE PERMANENT PVE TOKENS with short-lived minted ones. On C1, SEVEN tokens
|
|
7
|
+
* carry expire=0 and privsep=0 — root@pam!inventory-sync, agent@pve!executor,
|
|
8
|
+
* metrics@pve!exporter, app@pve!app, iac@pve!ro, iac@pve!apply and
|
|
9
|
+
* mint@pve!engine. Six of those are one Bao.ProxmoxRole plus a scoped PVE user away from
|
|
10
10
|
* being a 5-minute lease instead of a forever key.
|
|
11
11
|
*
|
|
12
|
-
* The seventh cannot be.
|
|
12
|
+
* The seventh cannot be. mint@pve!engine is the credential the ENGINE ITSELF
|
|
13
13
|
* authenticates with in order to mint anything at all: the thing that issues short-lived
|
|
14
14
|
* tokens needs a standing one, and no role can resolve that chicken-and-egg. It stays
|
|
15
15
|
* permanent, which makes it the single most valuable token on the cluster and the one whose
|
|
16
16
|
* rotation must be a deliberate, hand-run procedure rather than a plan.
|
|
17
17
|
*
|
|
18
|
-
* MEASURED here: `bao read proxmox-
|
|
19
|
-
* `
|
|
18
|
+
* MEASURED here: `bao read proxmox-c1/config` returns parent_token_id
|
|
19
|
+
* `mint@pve!engine`, parent_secret_set true, parent_secret_tail `…eb01` — the engine's
|
|
20
20
|
* own key, present and, by the schema's own "Write-only: never returned by a read", not
|
|
21
21
|
* retrievable. The seven-token inventory was handed to this change and NOT re-counted against
|
|
22
22
|
* the PVE API in this session.
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
*
|
|
28
28
|
* ⛔ ISSUANCE IS NOT THIS RESOURCE, AND MUST NEVER BECOME ONE. `<mount>/creds/<name>` returns a
|
|
29
29
|
* LIVE PVE TOKEN. Alchemy persists attributes unencrypted (StateEncoding.ts tags Redacted
|
|
30
|
-
* values rather than encrypting them) into the `alchemy` Postgres, which
|
|
31
|
-
*
|
|
30
|
+
* values rather than encrypting them) into the `alchemy` Postgres, which a nightly job dumps
|
|
31
|
+
* to a backup guest, from where PBS backs it up. A minted token declared as a resource would
|
|
32
32
|
* outlive its own lease in at least four places, none of them OpenBao. This resource declares
|
|
33
33
|
* the DOOR; walking through it is a runtime call, not a plan.
|
|
34
34
|
*
|
|
35
35
|
* ★ `defaultRemovalPolicy: 'retain'` — deleting a role breaks every consumer that mints from
|
|
36
|
-
* it. Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
36
|
+
* it. Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
37
37
|
*/
|
|
38
38
|
import { Resource } from 'alchemy';
|
|
39
39
|
import { isResolved } from 'alchemy/Diff';
|
|
@@ -80,7 +80,7 @@ export const BaoProxmoxRoleProvider = () =>
|
|
|
80
80
|
BaoProxmoxRole.Provider.of({
|
|
81
81
|
/**
|
|
82
82
|
* ⛔ `bao list <mount>/roles` IS NOT A LIST OF THINGS THIS OWNS. MEASURED today:
|
|
83
|
-
* proxmox-
|
|
83
|
+
* proxmox-c1/roles answers `provision, read` and proxmox-c2/roles answers `read` —
|
|
84
84
|
* four roles created by hand at bringup, none of them declared anywhere yet.
|
|
85
85
|
* Returning them would invite Alchemy to adopt, and then delete, roles it never
|
|
86
86
|
* created, and deleting a role is how every consumer of it stops being able to mint.
|
package/src/openbao/ssh-role.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* ⛔ THE SIGNING KEY IS OUT OF SCOPE AND MUST STAY OUT. `ssh/config/ca` holds (or generates)
|
|
5
5
|
* the CA private key and `ssh/sign/<role>` issues certificates. Neither is declarable
|
|
6
6
|
* here, for the reason policy.ts spells out: Alchemy persists attributes to its state
|
|
7
|
-
* store WITHOUT encrypting them, that store is the `alchemy` Postgres, and
|
|
8
|
-
* dumps it
|
|
7
|
+
* store WITHOUT encrypting them, that store is the `alchemy` Postgres, and a nightly job
|
|
8
|
+
* dumps it to a backup guest. A CA key there would outlive the vault that guards it. This
|
|
9
9
|
* resource declares the SHAPE a signature must satisfy — principals, extensions, TTLs.
|
|
10
10
|
*
|
|
11
11
|
* ⛔ THE ROLE WRITE IS A FULL REPLACE, NOT A PATCH. OpenBao rebuilds the role entry from the
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* ★ `defaultRemovalPolicy: 'retain'`. Deleting this role locks every operator out of the
|
|
31
31
|
* estate — the memory note "operator SSH is homeflare-bot@<host>" is this CA. Opt in with
|
|
32
|
-
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
32
|
+
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
33
33
|
*/
|
|
34
34
|
import { Resource } from 'alchemy';
|
|
35
35
|
import { isResolved } from 'alchemy/Diff';
|
package/src/proxmox/acl.ts
CHANGED
|
@@ -41,14 +41,14 @@ export type AclSubjectType = 'user' | 'group' | 'token';
|
|
|
41
41
|
|
|
42
42
|
export interface AclProps extends WithTarget {
|
|
43
43
|
/**
|
|
44
|
-
* The PVE object path the grant is ON — `/`, `/pool/
|
|
44
|
+
* The PVE object path the grant is ON — `/`, `/pool/lab`, `/vms/101`, `/storage/local-zfs`.
|
|
45
45
|
* ⚠️ NOT THE API PATH: `spec.path` below is the endpoint (`access/acl`). PVE's API overloads the
|
|
46
46
|
* word, so this file does too rather than renaming a field the cluster calls `path`.
|
|
47
47
|
*/
|
|
48
48
|
path: string;
|
|
49
49
|
/** Which kind of subject `ugid` names. Identity: changing it is a different grant. */
|
|
50
50
|
type: AclSubjectType;
|
|
51
|
-
/** `
|
|
51
|
+
/** `alice@pve`, `admins`, `hf-provision@pve!hf-provision-…`. Identity. */
|
|
52
52
|
ugid: string;
|
|
53
53
|
/** The role bound here, e.g. `PVEAuditor`. ⚠️ It must exist — PVE refuses an unknown roleid. */
|
|
54
54
|
roleid: string;
|
|
@@ -85,9 +85,9 @@ export const ProxmoxAcl = Resource<ProxmoxAcl>('Proxmox.Acl');
|
|
|
85
85
|
/**
|
|
86
86
|
* ⚠️ PVE NORMALISES ACL PATHS AND RETURNS THE NORMALISED FORM, a forever-update trap when only one
|
|
87
87
|
* side of the comparison is normalised. `PVE::AccessControl::normalize_path` collapses repeated
|
|
88
|
-
* slashes and strips the trailing one, so a declared `/pool/
|
|
88
|
+
* slashes and strips the trailing one, so a declared `/pool/lab/` reads back as `/pool/lab`
|
|
89
89
|
* and a naive match never fires again. It also refuses a path with no leading slash, so one is
|
|
90
|
-
* added here rather than letting `pool/
|
|
90
|
+
* added here rather than letting `pool/lab` 400.
|
|
91
91
|
*/
|
|
92
92
|
const normalize = (raw: string) => `/${raw.split('/').filter(Boolean).join('/')}`;
|
|
93
93
|
|
|
@@ -32,8 +32,8 @@ type Case = {
|
|
|
32
32
|
|
|
33
33
|
const cases: readonly Case[] = [
|
|
34
34
|
{
|
|
35
|
-
declare: () => ProxmoxPool('
|
|
36
|
-
live: { 'pools/
|
|
35
|
+
declare: () => ProxmoxPool('lab', { comment: 'x', poolid: 'lab', target: FAKE_TARGET }),
|
|
36
|
+
live: { 'pools/lab': { comment: 'x', members: [] } },
|
|
37
37
|
name: 'Proxmox.Pool (pveHandlers: every factory family)',
|
|
38
38
|
provider: ProxmoxPoolProvider as never,
|
|
39
39
|
},
|
|
@@ -98,15 +98,15 @@ describe('adopting a matching object only reads', () => {
|
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
test('and the harness does see a write: a drifted Pool is PUT once', async () => {
|
|
101
|
-
const fake = clusterWith({ 'pools/
|
|
101
|
+
const fake = clusterWith({ 'pools/lab': { comment: 'old', members: [] } });
|
|
102
102
|
await withoutBao(async () => {
|
|
103
103
|
const engine = engineOver(ProxmoxPoolProvider().pipe(Layer.provideMerge(fake.layer)));
|
|
104
104
|
const declared = () =>
|
|
105
|
-
ProxmoxPool('
|
|
105
|
+
ProxmoxPool('lab', { comment: 'x', poolid: 'lab', target: FAKE_TARGET });
|
|
106
106
|
const report = await engine.verify(declared());
|
|
107
107
|
expect(report.rows[0]).toMatchObject({ changed: ['comment'], diff: 'update', ok: false });
|
|
108
|
-
expect(await engine.deploy(declared())).toEqual({
|
|
108
|
+
expect(await engine.deploy(declared())).toEqual({ lab: 'adopted' });
|
|
109
109
|
});
|
|
110
|
-
expect(fake.writes()).toEqual(['PUT pools/
|
|
110
|
+
expect(fake.writes()).toEqual(['PUT pools/lab']);
|
|
111
111
|
});
|
|
112
112
|
});
|