RPF-WP-0061: option A recorded, client guide (T05), fixture independent of live CCR state
All checks were successful
CCR intake gate / gate (push) Successful in 1m11s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 306568@bnt-lap001
Assistant-Session: 8bdf8150-ea56-41e7-9132-13f04f6b0b91
This commit is contained in:
codex 2026-10-18 12:48:35 +02:00
parent 45c8d9370e
commit 04f18c8c73
4 changed files with 86 additions and 6 deletions

View file

@ -303,3 +303,61 @@ Use the whynot-design npm token lane as the pilot:
Once that path feels good, reuse it for the sibling workload-KV lanes and the
credential broker's OpenBao token-role gates.
## Self-Service Intake (RPF-WP-0061)
Clients file standard CCRs without a railiance-platform worker session. The
platform's judgement is in [`registry/ccr-standard-profile.yaml`](../registry/ccr-standard-profile.yaml);
`review-standard` applies it, and `.forgejo/workflows/ccr-intake.yaml` re-checks
it in CI. Allowed submitters: **ops-mason** only (profile `submitters`).
**In profile** means: a `workload-kv-read` lane on mount `platform`; one exact path
`platform/workloads/<a>/<b>[/<c>]` or `platform/suppliers/<supplier>/<account>`;
`metadata_read: false`; a policy file equal to the generated HCL; Kubernetes auth
with role `external-secrets-*`, audience `openbao`, all ttls 15m or less,
`token_no_default_policy: true`; one service account in one non-platform
namespace; risk `low` or `medium`; and no other live CCR reading the path.
Everything else (OIDC roles, AppRole, token roles, second readers, high risk)
still goes to the `railiance-platform` inbox.
### Client steps
Run these in a railiance-platform checkout that is up to date with `main`:
```bash
# 1. File the draft. Allocates the CCR id, copies the policy, runs the review.
python3 scripts/credential-change.py submit \
--ccr <draft.yaml> --policy <policy.hcl> --slug <slug> \
--requester ops-mason --push --notify
# [NEEDS-PLATFORM] lines mean out of profile: stop. --notify messaged
# railiance-platform. Do not edit the review comment to get past it.
# 2. Wait for the "CCR intake gate" run on ccr/<id>-<slug> to pass, then
# fast-forward main to it. Never merge a red or unchecked ccr/* branch.
git switch main && git merge --ff-only ccr/<id>-<slug> && git push origin main
# 3. Open the founder decision and commit the decision id.
python3 scripts/credential-change.py request-decision <id>
git commit -m "<id>: request founder decision" -- credential-change-requests/<id>-<slug>.yaml
git push origin main
```
After step 3, the client waits. The **founder** resolves the State Hub decision.
Then a **platform session or the founder** runs `sync-decision <id>`. Clients do
not run `sync-decision` on their own CCRs. State Hub does not authenticate who
resolved a decision (`decided_by` is free text). Until founder approval can be
verified (RPF-WP-0061 follow-up B), the sync must come from someone other than
the requester. After the CCR is `approved`, the usual steps apply:
`confirm-binding` against native state, apply (ops-mason under
ops-mason-construct for in-profile lanes, otherwise the applier), then
`record-evidence`.
### Known limits
- All workstation agents push to Forgejo as the same account, so `ccr/*`
cannot yet be restricted to ops-mason. The submitter list and the
merge-only-green rule are enforced by tooling and convention, not by Forgejo.
The main-branch `check-ids` job and `check-branch` detect violations after the
fact (follow-up C).
- The automated review is a source review only. It never sets `approved` and
never touches OpenBao.

View file

@ -295,7 +295,9 @@ def decision_payload(ccr: dict[str, Any], cc: Any) -> dict[str, Any]:
f"- Risk: {(ccr.get('risk') or {}).get('classification')}",
f"- Platform review: {review.get('reviewer')}: {review.get('comment')}",
"",
cc.render_decision_templates(ccr),
# The link line would be a placeholder: the decision does not exist yet.
*[line for line in cc.render_decision_templates(ccr).splitlines()
if not line.startswith("Decision link:")],
"",
"Approval permits apply after confirm-binding; it exposes and stores no value.",
])

View file

@ -47,6 +47,9 @@ class CcrIntakeTests(unittest.TestCase):
self.addCleanup(env.stop)
self.profile, _digest = ccr_intake.load_profile()
self.path = self.ccrs / STANDARD
# The live CCR moves through approval; the fixture must stay pre-approval.
self.edit(lambda d: (d.update(status="proposed"), d["review"].update(comments=[]),
d.pop("state_hub", None)))
def edit(self, change) -> None:
data = cc.load_yaml(self.path)
@ -254,6 +257,7 @@ class CcrIntakeTests(unittest.TestCase):
payload = post.call_args.args[2]
self.assertIn("APPROVE: CCR-2026-0051", payload["description"])
self.assertNotIn("status", payload)
self.assertNotIn("Decision link", payload["description"])
state_hub = cc.load_yaml(self.path)["state_hub"]
self.assertEqual(state_hub["decision_id"], "dec-1")
self.assertEqual(cc.load_yaml(self.path)["status"], "proposed")

View file

@ -114,7 +114,7 @@ gaps, core-hub database policy assertion) predate this work.
```task
id: RPF-WP-0061-T03
status: progress
status: done
priority: medium
state_hub_task_id: "0fd97924-1bc7-5aa1-a520-c6c5737bf09b"
```
@ -143,7 +143,7 @@ fact.
```task
id: RPF-WP-0061-T04
status: progress
status: done
priority: medium
state_hub_task_id: "15ac9eae-9890-5140-b1ca-cd0ad3ca77ac"
```
@ -163,15 +163,31 @@ platform worker never verified the resolver) but self-service makes it
cheaper to exploit. Until it is closed, `sync-decision` on a self-service CCR
stays a platform/founder step.
Founder decision 2026-10-18: option A. Clients submit, pass CI, get the
automated review and open the decision themselves; the founder resolves it;
`sync-decision` stays with a platform session or the founder. CCR-2026-0051 went
through this path: founder resolved e633607e at 10:46:17Z; synced at 45c8d93.
Follow-ups, not part of this plan: (B) founder approval provable through an
MFA-backed OIDC login so clients can sync themselves; (C) separate Forgejo and
OS identities per agent so `ccr/*` can be restricted. Both stem from the
finding that every workstation agent pushes as Forgejo user `tegwick` with the
key `workstation-automation`.
## T05 — Client documentation and first self-service CCR
```task
id: RPF-WP-0061-T05
status: todo
status: wait
priority: medium
state_hub_task_id: "27797fa5-3d47-5037-a98c-2d269198d1c4"
```
Document the flow in `docs/credential-change-approval.md` and notify
ops-mason and ops-warden. Acceptance: one CCR filed, reviewed, approved and
applied by a client with no railiance-platform worker session involved.
ops-mason and ops-warden. Acceptance (narrowed by option A): ops-mason submits
one CCR, passes the CI gate, merges and opens the decision with no
railiance-platform worker session; only the post-approval sync is platform/founder.
2026-10-18: guide added as "Self-Service Intake" in
`docs/credential-change-approval.md`; ops-mason and ops-warden notified. Waiting
for ops-mason's first self-service submission, which also gives the first live
`ccr/*` CI run.