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:
tegwick 2026-08-26 21:25:43 +02:00
parent 463c47faf6
commit 1fdd8896f4
2 changed files with 245 additions and 107 deletions

View 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.

View file

@ -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 13: 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.