RPF-WP-0061-T04: request-decision opens the founder hub decision; CCR-2026-0051 decision e633607e
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:
parent
3d971178d8
commit
8545bf8e4c
4 changed files with 110 additions and 1 deletions
|
|
@ -99,3 +99,6 @@ lifecycle:
|
|||
metadata version only.
|
||||
compromised: Revoke the SMTP credential at Scaleway, disable the role, rotate the
|
||||
KV value, then re-enable after review.
|
||||
state_hub:
|
||||
decision_id: e633607e-ff3e-46ec-bb6f-143e8b44f1af
|
||||
decision_api_url: http://127.0.0.1:8000/decisions/e633607e-ff3e-46ec-bb6f-143e8b44f1af
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ CCR_FILE_RE = re.compile(r"^(CCR-\d{4}-\d{4})(-[a-z0-9-]+)?\.ya?ml$")
|
|||
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
||||
TTL_UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
||||
PLATFORM_OWNER = "railiance-platform-owner"
|
||||
PLATFORM_TOPIC_ID = "ca369340-a64e-442e-98f1-a4fa7dc74a38"
|
||||
|
||||
|
||||
def load_profile(path: Path = DEFAULT_PROFILE) -> tuple[dict[str, Any], str]:
|
||||
|
|
@ -278,6 +279,63 @@ def command_check_branch(args: argparse.Namespace, cc: Any) -> int:
|
|||
return 1 if errors else 0
|
||||
|
||||
|
||||
def decision_payload(ccr: dict[str, Any], cc: Any) -> dict[str, Any]:
|
||||
openbao = ccr["openbao"]
|
||||
auth = openbao["auth"]
|
||||
review = ccr["review"]["comments"][-1]
|
||||
description = "\n".join([
|
||||
f"Founder decision for {ccr['id']}: {ccr.get('title')}.",
|
||||
"",
|
||||
"Non-secret request summary:",
|
||||
f"- Requester: {(ccr.get('requester') or {}).get('agent')}",
|
||||
f"- OpenBao path: {openbao.get('kv_path')} fields {openbao.get('fields')}",
|
||||
f"- Policy: {openbao.get('policy_name')} ({openbao.get('policy_file')})",
|
||||
f"- Auth: {auth.get('method')} role {auth.get('role')}, SA {bound_list(auth, 'service_account_names')}"
|
||||
f" in {bound_list(auth, 'service_account_namespaces')}, ttl {auth.get('ttl')}",
|
||||
f"- Risk: {(ccr.get('risk') or {}).get('classification')}",
|
||||
f"- Platform review: {review.get('reviewer')}: {review.get('comment')}",
|
||||
"",
|
||||
cc.render_decision_templates(ccr),
|
||||
"",
|
||||
"Approval permits apply after confirm-binding; it exposes and stores no value.",
|
||||
])
|
||||
return {
|
||||
"topic_id": PLATFORM_TOPIC_ID,
|
||||
"title": f"Review {ccr['id']} {ccr.get('title')}",
|
||||
"description": description,
|
||||
"escalation_note": "Founder decision required before OpenBao credential lane apply.",
|
||||
}
|
||||
|
||||
|
||||
def request_decision(path: Path, cc: Any, base_url: str) -> dict[str, Any]:
|
||||
"""Open the founder's State Hub decision for a platform-reviewed CCR (RPF-WP-0061-T04)."""
|
||||
ccr, _warnings = cc.validate_or_exit(path)
|
||||
if ccr.get("status") != "proposed":
|
||||
cc.fail(f"request-decision requires status proposed, got {ccr.get('status')}")
|
||||
if (ccr.get("state_hub") or {}).get("decision_id"):
|
||||
cc.fail(f"{ccr['id']} already has State Hub decision {ccr['state_hub']['decision_id']}")
|
||||
comments = (ccr.get("review") or {}).get("comments") or []
|
||||
last = comments[-1] if comments else {}
|
||||
if last.get("decision") != "approved" or not str(last.get("reviewer", "")).startswith(PLATFORM_OWNER):
|
||||
cc.fail("request-decision requires a platform-owner approval as the latest review comment")
|
||||
decision = cc.state_hub_post_json(base_url, "/decisions/", decision_payload(ccr, cc))
|
||||
if not decision.get("id") or decision.get("status") not in (None, "open"):
|
||||
cc.fail(f"State Hub returned an unexpected decision: {decision.get('id')} {decision.get('status')}")
|
||||
state_hub = ccr.setdefault("state_hub", {})
|
||||
state_hub["decision_id"] = decision["id"]
|
||||
state_hub["decision_api_url"] = f"{base_url.rstrip('/')}/decisions/{decision['id']}"
|
||||
ccr["updated"] = datetime.now(timezone.utc).date().isoformat()
|
||||
cc.dump_yaml(path, ccr)
|
||||
return decision
|
||||
|
||||
|
||||
def command_request_decision(args: argparse.Namespace, cc: Any) -> int:
|
||||
path = cc.resolve_ccr(args.ref)
|
||||
decision = request_decision(path, cc, args.state_hub_url)
|
||||
print(f"[OK] {path.name}: State Hub decision {decision['id']} opened; commit the CCR file")
|
||||
return 0
|
||||
|
||||
|
||||
def git(*argv: str) -> None:
|
||||
subprocess.run(["git", "-C", str(REPO_DIR), *argv], check=True)
|
||||
|
||||
|
|
@ -359,6 +417,13 @@ def add_parsers(sub: Any, cc: Any, state_hub_url: str) -> None:
|
|||
check_branch.add_argument("--main-root", required=True, help="tree of main to compare against")
|
||||
check_branch.set_defaults(func=lambda args: command_check_branch(args, cc))
|
||||
|
||||
decision_p = sub.add_parser(
|
||||
"request-decision", help="Open the founder State Hub decision for a platform-reviewed proposed CCR"
|
||||
)
|
||||
decision_p.add_argument("ref")
|
||||
decision_p.add_argument("--state-hub-url", default=state_hub_url)
|
||||
decision_p.set_defaults(func=lambda args: command_request_decision(args, cc))
|
||||
|
||||
submit_p = sub.add_parser(
|
||||
"submit",
|
||||
help="File a client CCR draft under the next CCR id and run the standard review",
|
||||
|
|
|
|||
|
|
@ -247,6 +247,32 @@ class CcrIntakeTests(unittest.TestCase):
|
|||
errors = ccr_intake.branch_gate(main_root, Path(STANDARD).stem, cc)
|
||||
self.assertTrue(any("already exists on main" in e for e in errors), errors)
|
||||
|
||||
def test_request_decision_records_id_after_platform_review(self) -> None:
|
||||
ccr_intake.review_standard(self.path, cc)
|
||||
with mock.patch.object(cc, "state_hub_post_json", return_value={"id": "dec-1", "status": "open"}) as post:
|
||||
ccr_intake.request_decision(self.path, cc, "http://hub")
|
||||
payload = post.call_args.args[2]
|
||||
self.assertIn("APPROVE: CCR-2026-0051", payload["description"])
|
||||
self.assertNotIn("status", payload)
|
||||
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")
|
||||
with self.assertRaises(SystemExit):
|
||||
ccr_intake.request_decision(self.path, cc, "http://hub")
|
||||
|
||||
def test_request_decision_requires_platform_approval(self) -> None:
|
||||
self.edit(lambda d: d["risk"].update(classification="high"))
|
||||
ccr_intake.review_standard(self.path, cc)
|
||||
with mock.patch.object(cc, "state_hub_post_json") as post, self.assertRaises(SystemExit):
|
||||
ccr_intake.request_decision(self.path, cc, "http://hub")
|
||||
post.assert_not_called()
|
||||
|
||||
def test_request_decision_refuses_pre_resolved_decision(self) -> None:
|
||||
ccr_intake.review_standard(self.path, cc)
|
||||
resolved = {"id": "dec-2", "status": "resolved"}
|
||||
with mock.patch.object(cc, "state_hub_post_json", return_value=resolved), self.assertRaises(SystemExit):
|
||||
ccr_intake.request_decision(self.path, cc, "http://hub")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -143,11 +143,26 @@ fact.
|
|||
|
||||
```task
|
||||
id: RPF-WP-0061-T04
|
||||
status: todo
|
||||
status: progress
|
||||
priority: medium
|
||||
state_hub_task_id: "15ac9eae-9890-5140-b1ca-cd0ad3ca77ac"
|
||||
```
|
||||
|
||||
2026-10-18: `credential-change.py request-decision <ccr>` opens the founder
|
||||
decision for a `proposed` CCR whose latest review is a platform-owner
|
||||
approval, refuses a second decision or a decision returned already resolved,
|
||||
and writes `state_hub.decision_id` into the CCR. The existing `sync-decision`
|
||||
moves the file. First use: CCR-2026-0051, decision e633607e. The CI gate
|
||||
passed on main at 3d97117 after adding the jsonschema pin.
|
||||
|
||||
Open, needs a founder choice: the hub does not authenticate decisions.
|
||||
`decided_by` on resolve is free text, and `POST /decisions/` accepts an
|
||||
already-resolved decision. With self-service, the requester could resolve its
|
||||
own CCR decision and run `sync-decision`. This gap existed before (the
|
||||
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.
|
||||
|
||||
## T05 — Client documentation and first self-service CCR
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue