RPF-WP-0061: render-policy and plan_ref for ops-mason submissions; approvals only via sync-decision
All checks were successful
CCR intake gate / gate (push) Successful in 3m12s
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:53:19 +02:00
parent 04f18c8c73
commit 7e7ca8be51
3 changed files with 56 additions and 1 deletions

View file

@ -325,6 +325,11 @@ still goes to the `railiance-platform` inbox.
Run these in a railiance-platform checkout that is up to date with `main`: Run these in a railiance-platform checkout that is up to date with `main`:
```bash ```bash
# 0. Generate the policy instead of writing it, so it is byte-equal by construction.
python3 scripts/credential-change.py render-policy --kv-path <platform/...> > <policy.hcl>
# Optional: plan_ref: {repo, path, commit} in the draft links the client's
# construction plan; submit keeps it and review-standard only checks its shape.
# 1. File the draft. Allocates the CCR id, copies the policy, runs the review. # 1. File the draft. Allocates the CCR id, copies the policy, runs the review.
python3 scripts/credential-change.py submit \ python3 scripts/credential-change.py submit \
--ccr <draft.yaml> --policy <policy.hcl> --slug <slug> \ --ccr <draft.yaml> --policy <policy.hcl> --slug <slug> \
@ -342,7 +347,11 @@ git commit -m "<id>: request founder decision" -- credential-change-requests/<id
git push origin main git push origin main
``` ```
After step 3, the client waits. The **founder** resolves the State Hub decision. After step 3, the client waits and does not edit `status` or `review.comments`
itself. Approvals reach the file only through `sync-decision`, even when the
founder approved the client's plan elsewhere. The hub decision is the single
approval record for both the client plan and the CCR; clients copy their plan
approval *from* it after the sync. The **founder** resolves the State Hub decision.
Then a **platform session or the founder** runs `sync-decision <id>`. Clients do 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 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 resolved a decision (`decided_by` is free text). Until founder approval can be

View file

@ -14,6 +14,7 @@ import json
import re import re
import shutil import shutil
import subprocess import subprocess
import sys
import urllib.request import urllib.request
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timezone from datetime import datetime, timezone
@ -126,6 +127,13 @@ def standard_violations(
risk = (ccr.get("risk") or {}).get("classification") risk = (ccr.get("risk") or {}).get("classification")
if risk not in profile["allowed_risk_classifications"]: if risk not in profile["allowed_risk_classifications"]:
v.append(f"risk classification {risk!r} needs a platform review") v.append(f"risk classification {risk!r} needs a platform review")
plan_ref = ccr.get("plan_ref")
if plan_ref is not None and not (
isinstance(plan_ref, dict)
and all(isinstance(plan_ref.get(k), str) and plan_ref.get(k) for k in ("repo", "path", "commit"))
and re.match(r"^[0-9a-f]{7,40}$", plan_ref["commit"])
):
v.append("plan_ref must have non-empty repo, path and a hex commit")
conflicts = reader_conflicts(ccr, directory, profile, cc) conflicts = reader_conflicts(ccr, directory, profile, cc)
if conflicts: if conflicts:
v.append(f"kv_path already has a reader: {', '.join(conflicts)}") v.append(f"kv_path already has a reader: {', '.join(conflicts)}")
@ -338,6 +346,23 @@ def command_request_decision(args: argparse.Namespace, cc: Any) -> int:
return 0 return 0
def standard_policy_hcl(kv_path: str, cc: Any) -> str:
"""The exact HCL review-standard compares against, for a standard-lane path."""
return cc.generated_policy_hcl({
"request_type": "workload-kv-read",
"openbao": {"mount": kv_path.split("/", 1)[0], "kv_path": kv_path,
"metadata_read": False, "token_self_lifecycle": True},
})
def command_render_policy(args: argparse.Namespace, cc: Any) -> int:
profile, _digest = load_profile(Path(args.profile))
if not any(re.match(p, args.kv_path) for p in profile["kv_path_patterns"]):
cc.fail(f"kv_path {args.kv_path!r} is outside the standard workload/supplier prefixes")
sys.stdout.write(standard_policy_hcl(args.kv_path, cc))
return 0
def git(*argv: str) -> None: def git(*argv: str) -> None:
subprocess.run(["git", "-C", str(REPO_DIR), *argv], check=True) subprocess.run(["git", "-C", str(REPO_DIR), *argv], check=True)
@ -426,6 +451,13 @@ def add_parsers(sub: Any, cc: Any, state_hub_url: str) -> None:
decision_p.add_argument("--state-hub-url", default=state_hub_url) decision_p.add_argument("--state-hub-url", default=state_hub_url)
decision_p.set_defaults(func=lambda args: command_request_decision(args, cc)) decision_p.set_defaults(func=lambda args: command_request_decision(args, cc))
render_policy = sub.add_parser(
"render-policy", help="Print the standard-lane policy HCL for a KV path (what review-standard expects)"
)
render_policy.add_argument("--kv-path", required=True)
render_policy.add_argument("--profile", default=str(DEFAULT_PROFILE))
render_policy.set_defaults(func=lambda args: command_render_policy(args, cc))
submit_p = sub.add_parser( submit_p = sub.add_parser(
"submit", "submit",
help="File a client CCR draft under the next CCR id and run the standard review", help="File a client CCR draft under the next CCR id and run the standard review",

View file

@ -277,6 +277,20 @@ class CcrIntakeTests(unittest.TestCase):
with mock.patch.object(cc, "state_hub_post_json", return_value=resolved), self.assertRaises(SystemExit): with mock.patch.object(cc, "state_hub_post_json", return_value=resolved), self.assertRaises(SystemExit):
ccr_intake.request_decision(self.path, cc, "http://hub") ccr_intake.request_decision(self.path, cc, "http://hub")
def test_render_policy_matches_reviewed_policy_file(self) -> None:
rendered = ccr_intake.standard_policy_hcl("platform/suppliers/scaleway/mas-tem-relay", cc)
self.assertEqual(rendered, (REPO_DIR / POLICY).read_text())
def test_plan_ref_passes_through_submit_and_is_shape_checked(self) -> None:
args = self.submit_args()
draft = cc.load_yaml(Path(args.ccr))
draft["plan_ref"] = {"repo": "ops-mason", "path": "plans/x.md", "commit": "d19e6b1"}
cc.dump_yaml(Path(args.ccr), draft)
destination, violations = ccr_intake.submit(args, cc)
self.assertEqual(violations, [])
self.assertEqual(cc.load_yaml(destination)["plan_ref"]["commit"], "d19e6b1")
self.assert_violation(lambda d: d.update(plan_ref={"repo": "ops-mason"}), "plan_ref")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()