diff --git a/docs/credential-change-approval.md b/docs/credential-change-approval.md index 6d10430..95413b3 100644 --- a/docs/credential-change-approval.md +++ b/docs/credential-change-approval.md @@ -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//[/]` or `platform/suppliers//`; +`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 --policy --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/- to pass, then +# fast-forward main to it. Never merge a red or unchecked ccr/* branch. +git switch main && git merge --ff-only ccr/- && git push origin main + +# 3. Open the founder decision and commit the decision id. +python3 scripts/credential-change.py request-decision +git commit -m ": request founder decision" -- credential-change-requests/-.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 `. 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. diff --git a/scripts/ccr_intake.py b/scripts/ccr_intake.py index a9e9082..0fa7c10 100644 --- a/scripts/ccr_intake.py +++ b/scripts/ccr_intake.py @@ -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.", ]) diff --git a/tests/test_ccr_intake.py b/tests/test_ccr_intake.py index c827b0b..80af7c5 100644 --- a/tests/test_ccr_intake.py +++ b/tests/test_ccr_intake.py @@ -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") diff --git a/workplans/RPF-WP-0061-self-service-ccr-intake.md b/workplans/RPF-WP-0061-self-service-ccr-intake.md index c31e9a1..99d61c4 100644 --- a/workplans/RPF-WP-0061-self-service-ccr-intake.md +++ b/workplans/RPF-WP-0061-self-service-ccr-intake.md @@ -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.