refactor(workplan): split MASON-WP-0003 to the lane ops-mason owns
Trim MASON-WP-0003 to T01-T03 — decide, build the AppRole/policy/KV path structure, register the catalog entry — and route it through the four-phase pipeline via plans/state-hub-forge-derivation-read.md, which supplies the phase-3 executive summary the workplan was bypassing. T02 no longer mints or holds the token value: SCOPE.md puts secret values out of scope, so the forge owner mints and ops-warden's paste_once_provision desk delivers. Deployment plumbing, derive_from_forge(), and the fleet re-run move to state-hub/STATE-WP-0084. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 3377672@bnt-lap001 Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166
This commit is contained in:
parent
463c47faf6
commit
1fdd8896f4
2 changed files with 245 additions and 107 deletions
174
plans/state-hub-forge-derivation-read.md
Normal file
174
plans/state-hub-forge-derivation-read.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
id: state-hub-forge-derivation-read
|
||||
demand_source: ops-mason/workplans/MASON-WP-0003
|
||||
consumer_repo: state-hub
|
||||
credential_type: openbao-approle-kv
|
||||
status: draft
|
||||
created: "2026-08-26"
|
||||
updated: "2026-08-26"
|
||||
---
|
||||
|
||||
# Construction plan: State Hub forge-derivation read lane
|
||||
|
||||
## 1. Demand
|
||||
|
||||
`CUST-ADR-012` decision 1 makes the forge the projection source: central
|
||||
clones a repository's default branch and derives its work records from what
|
||||
Forgejo holds. The `state-hub` pod does this anonymously today, which works
|
||||
for public repositories and fails outright for private ones.
|
||||
|
||||
The first fleet-wide reset (`state-hub/STATE-WP-0083-T04`, 2026-08-26)
|
||||
covered 121 repositories. Nine failed identically:
|
||||
|
||||
```text
|
||||
fatal: could not read Username for 'https://forgejo.coulomb.social'
|
||||
```
|
||||
|
||||
`rapp-core-hub`, `rapp-issue-core`, `rapp-openbao`, `rapp-policy-nexus` and
|
||||
five others are invisible to derivation entirely.
|
||||
|
||||
The demand is therefore: a **read-only** credential the `state-hub` workload
|
||||
can authenticate for non-interactively (no operator present — derivation runs
|
||||
unattended in-cluster), sufficient for `git clone --depth 1` against private
|
||||
repositories and nothing more.
|
||||
|
||||
## 2. Existing-structure survey
|
||||
|
||||
**Checked:** `ops-warden/registry/routing/catalog.yaml` (on disk).
|
||||
**Not checked:** live `bao policy list` / `bao auth list` — no valid OpenBao
|
||||
token in this session. Whoever executes phase 4 must re-verify against live
|
||||
state before applying.
|
||||
|
||||
`warden route find "state hub read private repository"` returns **no lane**.
|
||||
|
||||
**Nearest existing entry: `forgejo-admin-api-token`**
|
||||
(`ops-warden/registry/routing/catalog.yaml:577`) — a site-admin PAT owned by
|
||||
`railiance-platform`, stored at `platform/workloads/forgejo/forgejo-admin`
|
||||
(field `API_TOKEN`), gated by policy `workload-kv-read-forgejo-admin`, marked
|
||||
`risk: high`, attended-minted, currently under interim delegation to
|
||||
`secrets-engine` (`SECRETS-WP-0006`).
|
||||
|
||||
**Reuse considered and declined.** Binding a `state-hub` AppRole to
|
||||
`workload-kv-read-forgejo-admin` is mechanically possible and is the wrong
|
||||
build: derivation performs a shallow clone and nothing else, and a projection
|
||||
that only reads must not hold an identity that can administer the forge. It
|
||||
would also make an unattended in-cluster workload a standing consumer of a
|
||||
lane that is explicitly attended-minted and mid-delegation. No compaction
|
||||
opportunity exists in the other direction either — the admin lane keeps its
|
||||
own consumers unchanged.
|
||||
|
||||
No second existing AppRole or policy is scoped to a Forgejo *read* credential.
|
||||
This is genuinely new structure.
|
||||
|
||||
## 3. Proposed changes
|
||||
|
||||
| # | Action | Object | Reuse-vs-new rationale |
|
||||
|---|---|---|---|
|
||||
| 1 | create | KV v2 secret path `platform/workloads/state-hub/forge-derivation` (field `FORGE_READ_TOKEN`) | New, holds one credential and nothing else — mirrors the `platform/workloads/<workload>/<purpose>` shape already used by `platform/workloads/forgejo/forgejo-admin` |
|
||||
| 2 | create | Policy `workload-kv-read-state-hub-forge-derivation` — read-only, scoped to exactly path #1 | New — same `workload-kv-read-<path-slug>` convention as `workload-kv-read-forgejo-admin`, scoped to one path |
|
||||
| 3 | create | AppRole `state-hub-forge-derivation`, bound to policy #2 | New — unattended in-cluster login; TTLs per §4 |
|
||||
| 4 | deliver | The Forgejo token value into path #1 via ops-warden's `paste_once_provision` desk | ops-mason never touches the value (`SCOPE.md`); the forge owner mints, the desk delivers |
|
||||
| 5 | propose | ops-warden catalog entry `state-hub-forge-derivation-read` — pointer-only, `warden_executes: false`, `status: draft` | New — no existing entry covers this need; `forgejo-admin-api-token` stays as-is, untouched |
|
||||
|
||||
No tear-down. Nothing existing becomes redundant.
|
||||
|
||||
**Not in this plan** (other repos' work, tracked as `state-hub/STATE-WP-0084`):
|
||||
mounting the credential into the `state-hub` deployment, teaching
|
||||
`derive_from_forge()` to use it, and re-running the fleet reset.
|
||||
|
||||
## 4. Review notes (phase 2)
|
||||
|
||||
- **Naming check:** path, policy, and AppRole names all follow the
|
||||
conventions visible in the existing catalog. No divergence proposed.
|
||||
- **TTL/scoping check:** short-lived tokens (`token_ttl=15m`,
|
||||
`token_max_ttl=30m`) matching the workload AppRole shape already in use.
|
||||
Derivation fetches once per pass, so a bounded token costs nothing.
|
||||
`secret_id_ttl` per current build-phase posture
|
||||
(`ops-warden/wiki/WorkloadSecurityPosture.md`) — no rotation schedule
|
||||
beyond what posture requires. **Explicit assumption, flagged in §5.**
|
||||
- **Redundancy check:** no existing policy or AppRole grants Forgejo read.
|
||||
- **Compaction opportunity:** none identified.
|
||||
- **Ease of use for the consumer:** the pod already consumes `state-hub-env`;
|
||||
the credential arrives the same way. Rotation must not require a chart
|
||||
change or a redeploy — that constraint belongs to `STATE-WP-0084`, but it
|
||||
is a consequence of this plan's delivery shape and is stated here so the
|
||||
two do not drift.
|
||||
- **Credential handling constraint (carried to the consumer):** the token
|
||||
must never reach the clone URL as embedded userinfo, where it lands in logs
|
||||
and process listings. Use a credential helper or an HTTP header.
|
||||
|
||||
### The breadth question, argued both ways
|
||||
|
||||
**Organisation-wide read.** New private repositories derive without further
|
||||
work, and no one has to remember this lane exists.
|
||||
|
||||
**Per-repository read.** Tighter, but needs maintenance each time a
|
||||
repository is added.
|
||||
|
||||
The original argument for organisation-wide read was that the narrow option
|
||||
fails *invisibly* — a forgotten grant looks exactly like a broken repository.
|
||||
That argument does not survive `STATE-WP-0084-T01`, which makes "cannot read"
|
||||
report as unreadable rather than as an error or, worse, as an empty
|
||||
derivation. Once a missing grant surfaces as a named condition, the narrow
|
||||
option's failure mode is visible and recoverable.
|
||||
|
||||
What remains is a straight blast-radius trade: an organisation-wide read
|
||||
token, if it leaks, reads **every private repository in the organisation** —
|
||||
source, history, and whatever those repositories contain. A per-repository
|
||||
grant leaks nine repositories' worth.
|
||||
|
||||
**Recommendation: organisation-wide read, contingent on `STATE-WP-0084-T01`
|
||||
landing first.** The maintenance burden of the narrow option is real and
|
||||
recurring, and the visibility objection to it is genuine until T01 exists.
|
||||
But this is the founder's call, not a mechanical one — §5 puts it as an
|
||||
explicit decision, not a default.
|
||||
|
||||
## 5. Executive summary (phase 3)
|
||||
|
||||
**One-line ask:** create a read-only Forgejo credential lane so the State Hub
|
||||
can derive projections for private repositories, which it cannot read today.
|
||||
|
||||
**Who/what gets access:** a new OpenBao AppRole `state-hub-forge-derivation`,
|
||||
used by the `state-hub` workload in-cluster. Nothing else — no existing role,
|
||||
human, or workload gains anything new.
|
||||
|
||||
**To what:** read-only access to exactly one KV path,
|
||||
`platform/workloads/state-hub/forge-derivation`, which does not exist today
|
||||
and will hold one Forgejo token. The AppRole cannot read the existing
|
||||
`forgejo-admin` path or any other path.
|
||||
|
||||
The Forgejo token itself grants **repository read only** — no push, no
|
||||
administration, no other Forgejo resource.
|
||||
|
||||
**For how long:** each login is a 15-minute token, renewable to a 30-minute
|
||||
cap. The AppRole credential itself does not expire on a schedule at current
|
||||
build-phase posture — the same choice already made for existing workload
|
||||
AppRoles. Override this if you want rotation now.
|
||||
|
||||
**Blast radius if the credential leaks:** whoever holds the
|
||||
`role_id`/`secret_id` pair can mint a short-lived token that reads one
|
||||
Forgejo read token — nothing else in OpenBao. What that Forgejo token then
|
||||
reads is the open decision:
|
||||
|
||||
- **organisation-wide read (recommended):** every private repository in the
|
||||
organisation is readable. No write, no admin, no persistence.
|
||||
- **per-repository read:** the nine listed repositories are readable, and
|
||||
every future private repository needs a grant added by hand.
|
||||
|
||||
**Cost to reverse:** revoke the Forgejo token, delete one AppRole, one
|
||||
policy, one KV path — four commands, no other lane or consumer affected. The
|
||||
State Hub returns to deriving public repositories only, which is what it does
|
||||
today.
|
||||
|
||||
**Sequencing note:** the recommendation above assumes `STATE-WP-0084-T01`
|
||||
(report unreadable repositories as unreadable, never as retirement work)
|
||||
lands first. That task is independently worth doing — today a clone that
|
||||
returned empty instead of failing would propose every record in that
|
||||
repository for retirement, the near-miss that `vergabe-teilnahme` produced.
|
||||
|
||||
**Decision needed:**
|
||||
|
||||
1. Approve the lane as proposed, reject, or send back to phase 1.
|
||||
2. Breadth: organisation-wide read, or per-repository read.
|
||||
3. Rotation: accept build-phase posture (no scheduled `secret_id` expiry), or
|
||||
require rotation now.
|
||||
|
|
@ -12,6 +12,7 @@ updated: "2026-08-26"
|
|||
related:
|
||||
- CUST-ADR-012
|
||||
- STATE-WP-0083
|
||||
- STATE-WP-0084
|
||||
state_hub_workstream_id: "4015b46d-02f1-56ac-853e-87e8542cf0dd"
|
||||
---
|
||||
|
||||
|
|
@ -19,9 +20,11 @@ state_hub_workstream_id: "4015b46d-02f1-56ac-853e-87e8542cf0dd"
|
|||
|
||||
## Goal
|
||||
|
||||
Give the State Hub read-only access to Forgejo so it can derive projections for
|
||||
private repositories, and register the lane so `warden route find` answers this
|
||||
question the next time someone asks it.
|
||||
Build the credential lane that lets the State Hub read private repositories
|
||||
from Forgejo, and register it so `warden route find` answers this question the
|
||||
next time someone asks it.
|
||||
|
||||
This workplan builds the lane. It does not consume it — see the handoff below.
|
||||
|
||||
## Why this exists
|
||||
|
||||
|
|
@ -42,22 +45,36 @@ others are invisible to the hub's derivation entirely.
|
|||
|
||||
This is a limit on the ADR's own premise. *The forge is the projection source*
|
||||
holds only for repositories central can read, and nothing in the current design
|
||||
says so. Worse, the failure is indistinguishable from a broken repository: the
|
||||
pass records an error, not a policy, so "cannot read" and "does not exist" look
|
||||
the same from the outside.
|
||||
says so.
|
||||
|
||||
## Why ops-mason owns it
|
||||
|
||||
`warden route find` returns no lane for this need. The nearest entry is
|
||||
`forgejo-admin-api-token` — an operator PAT owned by `railiance-platform` — which
|
||||
is far more authority than derivation requires and the wrong thing to reuse.
|
||||
`forgejo-admin-api-token` — an operator PAT owned by `railiance-platform` —
|
||||
which is far more authority than derivation requires and the wrong thing to
|
||||
reuse.
|
||||
|
||||
This repository builds "OpenBao AppRoles, policies, and KV secret paths so
|
||||
ops-warden always has something real to route to", which is exactly what is
|
||||
missing. ops-warden issues SSH certificates only and routes everything else; a
|
||||
Forgejo token is not its to vend.
|
||||
|
||||
## Decide the token's scope and breadth
|
||||
## What ops-mason does not own
|
||||
|
||||
Three parts of the end-to-end fix belong to other repositories and are tracked
|
||||
in `state-hub/STATE-WP-0084`:
|
||||
|
||||
- mounting the credential into the `state-hub` deployment;
|
||||
- teaching `derive_from_forge()` to use it, and to distinguish "cannot read"
|
||||
from "does not exist";
|
||||
- re-running the fleet reset and reviewing the nine first diffs.
|
||||
|
||||
`STATE-WP-0084-T01` (the unreadable-vs-missing distinction) is not merely
|
||||
downstream: the recommendation in this lane's construction plan depends on it,
|
||||
because it is what makes a narrow grant fail visibly. It is worth landing with
|
||||
or without this credential.
|
||||
|
||||
## Run the construction plan through the decision gate
|
||||
|
||||
```task
|
||||
id: MASON-WP-0003-T01
|
||||
|
|
@ -66,129 +83,76 @@ priority: high
|
|||
state_hub_task_id: "36d26000-ae71-5a58-a563-a07ff5be664f"
|
||||
```
|
||||
|
||||
Two choices to settle before anything is created, because they determine what
|
||||
the credential can do if it leaks.
|
||||
`plans/state-hub-forge-derivation-read.md` holds phases 1–3: the demand, the
|
||||
existing-structure survey (including why `forgejo-admin-api-token` is rejected
|
||||
rather than reused), the proposed AppRole/policy/KV path, the review pass, and
|
||||
the executive summary.
|
||||
|
||||
**Permission.** Derivation performs `git clone --depth 1` and nothing else, so
|
||||
the token needs repository *read* and no other scope. It must not be able to
|
||||
push, administer, or read other Forgejo resources. Reusing the existing admin PAT
|
||||
is explicitly rejected: a projection that only reads should not hold an
|
||||
identity that can write.
|
||||
Three things need deciding, and they determine what the credential can do if it
|
||||
leaks:
|
||||
|
||||
**Breadth.** Organisation-wide read means new private repositories derive without
|
||||
further work and no one has to remember this lane exists. A per-repository grant
|
||||
is tighter but needs maintenance every time a repository is added, and a
|
||||
forgotten grant reappears as the same silent "cannot read" this workplan exists
|
||||
to remove. Recommend organisation-wide read, on the grounds that the failure mode
|
||||
of the narrow option is invisible.
|
||||
1. **Approve, reject, or send back to phase 1.**
|
||||
2. **Breadth** — organisation-wide repository read, or per-repository. The plan
|
||||
recommends organisation-wide *contingent on `STATE-WP-0084-T01`*, and argues
|
||||
the counter-case: an organisation-wide token that leaks reads every private
|
||||
repository in the organisation.
|
||||
3. **Rotation** — accept build-phase posture (no scheduled `secret_id` expiry),
|
||||
or require rotation now.
|
||||
|
||||
Acceptance: scope and breadth recorded as a decision with the reasoning, not
|
||||
just a value.
|
||||
Permission is not in question: derivation performs `git clone --depth 1` and
|
||||
nothing else, so the token gets repository *read* and no other scope.
|
||||
|
||||
## Create the AppRole, policy, and KV path
|
||||
Acceptance: the plan reaches `status: reviewed`, the founder decision is
|
||||
recorded with its reasoning rather than as a bare value, and the plan carries
|
||||
`approved_by`/`approved_at`.
|
||||
|
||||
## Build the AppRole, policy, and KV path structure
|
||||
|
||||
```task
|
||||
id: MASON-WP-0003-T02
|
||||
status: todo
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "68a1c8e4-c12b-550e-962c-06a90ab8c1c2"
|
||||
```
|
||||
|
||||
Create the read-only Forgejo token, an OpenBao policy granting read on exactly
|
||||
one KV path, and an AppRole for the State Hub workload to authenticate with.
|
||||
Phase 4, after T01 approval only. Create the OpenBao policy granting read on
|
||||
exactly one KV path, the KV path *structure*, and the AppRole the `state-hub`
|
||||
workload authenticates with.
|
||||
|
||||
The token value never appears in Git, State Hub, a workplan, a log, or a chat
|
||||
message. This task records *where* the credential lives and *who* may read it —
|
||||
never what it is.
|
||||
**ops-mason does not touch the token value, even transiently.** The forge owner
|
||||
mints the read-only Forgejo token; it reaches the KV path through ops-warden's
|
||||
existing `paste_once_provision` desk. This task records *where* the credential
|
||||
lives and *who* may read it — never what it is. The value never appears in Git,
|
||||
State Hub, a workplan, a log, or a chat message.
|
||||
|
||||
Operator-executed. This workplan describes the lane; it does not perform the
|
||||
creation.
|
||||
|
||||
Acceptance: the KV path exists and holds a working read-only token; the policy
|
||||
grants read on that path alone; the AppRole authenticates and cannot read
|
||||
anything else.
|
||||
Acceptance: the policy grants read on that path alone; the AppRole
|
||||
authenticates and can read nothing else; the KV path exists and the desk has
|
||||
delivered a working token into it, confirmed by the consumer rather than by
|
||||
reading the value here. Recorded in `audit/build-log.jsonl` against the
|
||||
approved plan.
|
||||
|
||||
## Register the lane in the routing catalog
|
||||
|
||||
```task
|
||||
id: MASON-WP-0003-T03
|
||||
status: todo
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "e14f0eb3-a920-5623-b4c1-c20858dbb45d"
|
||||
```
|
||||
|
||||
Add an entry so `warden route find "state hub read private repository"` resolves.
|
||||
Its absence is why this had to be reasoned out from first principles rather than
|
||||
looked up.
|
||||
Propose a pointer-only entry (`warden_executes: false`, no authored `steps`,
|
||||
`status: draft`) so `warden route find "state hub read private repository"`
|
||||
resolves. Its absence is why this had to be reasoned out from first principles
|
||||
rather than looked up.
|
||||
|
||||
The entry should name the owner, the subsystem, the KV path, the rotation
|
||||
method, and state plainly that ops-warden routes but does not execute it.
|
||||
The entry names the owner, the subsystem, the KV path, the rotation method, and
|
||||
states plainly that ops-warden routes but does not execute it. Normal git
|
||||
contribution to `ops-warden`, not a live API call.
|
||||
|
||||
Acceptance: `warden route find` returns the lane; `warden route show` gives an
|
||||
operator enough to act without reading this workplan.
|
||||
|
||||
## Deliver the credential to the pod
|
||||
## Handoff
|
||||
|
||||
```task
|
||||
id: MASON-WP-0003-T04
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "1b802564-84aa-5934-8923-51b2ad9dd607"
|
||||
```
|
||||
|
||||
Make the token available to the `state-hub` deployment as a Kubernetes Secret,
|
||||
mounted or injected, and referenced by the chart the same way `state-hub-env`
|
||||
already is.
|
||||
|
||||
It must not be baked into the image, and it must not be committed to the chart.
|
||||
Rotation must not require a chart change.
|
||||
|
||||
Acceptance: the pod can read the credential; nothing in the repository contains
|
||||
it; rotating the token does not require a redeploy.
|
||||
|
||||
## Teach the derivation to use it
|
||||
|
||||
```task
|
||||
id: MASON-WP-0003-T05
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "4eeb9744-0d2f-57d8-a063-0e4ed371ac63"
|
||||
```
|
||||
|
||||
`derive_from_forge()` in `state-hub` clones anonymously. It should use the
|
||||
credential when present and continue working without it — a hub with no token
|
||||
must still derive public repositories rather than failing wholesale.
|
||||
|
||||
Credentials must never reach the clone URL in a way that lands in logs or process
|
||||
listings; use a credential helper or header, not an embedded userinfo URL.
|
||||
|
||||
**Distinguish "cannot read" from "does not exist".** A repository the hub is not
|
||||
permitted to read must be reported as unreadable, not as a repository whose
|
||||
records no longer derive. Today the reset treats a failed clone as an error,
|
||||
which is correct only by accident: if a clone ever returned empty instead of
|
||||
failing, every record in that repository would be proposed for retirement. That
|
||||
is the failure that nearly retired `vergabe-teilnahme`, and it must be impossible
|
||||
here by construction rather than by luck.
|
||||
|
||||
Acceptance: private repositories derive; a hub without the credential still
|
||||
derives public ones; an unreadable repository is reported as unreadable and never
|
||||
as retirement work.
|
||||
|
||||
## Confirm the nine
|
||||
|
||||
```task
|
||||
id: MASON-WP-0003-T06
|
||||
status: wait
|
||||
priority: medium
|
||||
state_hub_task_id: "cbc9c02c-e49a-54c3-91e5-51df238139a6"
|
||||
```
|
||||
|
||||
Re-run the fleet reset and confirm the nine repositories derive rather than
|
||||
error. Expect them to arrive with a real diff — quite possibly a large one, since
|
||||
none has ever been reconciled against its files.
|
||||
|
||||
Treat the first diff for each as review material, not as work to apply. Every
|
||||
repository examined this way for the first time has produced findings.
|
||||
|
||||
Acceptance: fleet errors fall from nine to zero; each newly readable repository
|
||||
has its first diff reviewed before anything is applied.
|
||||
Once T02 and T03 are done, notify `state-hub` so `STATE-WP-0084-T02` can
|
||||
unblock. `STATE-WP-0084-T01` does not wait on this workplan.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue