@homeflare/alchemy 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/credentials.d.ts +8 -8
- package/dist/proxmox/credentials.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 +1 -1
- package/dist/proxmox/generated/pve.d.ts +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 +6 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +356 -142
- package/dist/proxmox/index.js.map +59 -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-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-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 +2 -2
- 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/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/docs/adopt-verify.md +21 -5
- package/docs/adopted-deploys.md +1 -1
- package/docs/ownership.md +26 -1
- 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 +6 -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/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/credentials.test.ts +6 -8
- package/src/proxmox/credentials.ts +8 -8
- package/src/proxmox/firewall-alias-form.ts +1 -1
- package/src/proxmox/firewall-alias.ts +1 -1
- package/src/proxmox/generated/pbs.ts +1 -1
- package/src/proxmox/generated/pve.ts +1 -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 +7 -0
- package/src/proxmox/index.ts +14 -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-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-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 +3 -3
- 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/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
|
@@ -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,9 @@ 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
|
+
- ★ **Measurements in the source name a reference cluster with placeholders**: `C1`, three nodes
|
|
19
|
+
`node-b`, `node-c` and `node-d` with Ceph on a Thunderbolt mesh, and documentation addresses
|
|
20
|
+
(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.12.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;
|
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
|
/**
|