diff --git a/.sot/03-PROTOCOLS/CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.md b/.sot/03-PROTOCOLS/CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.md index 93e2fab..bc51982 100644 --- a/.sot/03-PROTOCOLS/CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.md +++ b/.sot/03-PROTOCOLS/CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.md @@ -55,7 +55,7 @@ Type: HITL Status: blocked. -Blocked by: CTO-WORK-037, CTO-WORK-039, and explicit JP selection or approval of an owned low-risk noncritical Target Repository. +Blocked by: CTO-WORK-037, CTO-WORK-039, CTO-WORK-040, and explicit JP selection or approval of an owned low-risk noncritical Target Repository. User stories covered: CTO Case Candidate Backend PRD stories 4, 5, 7, 8, 9, 10, 11, 13. @@ -112,6 +112,41 @@ Validator: `python3 tools/validate_cto_child.py` Done evidence: template artifact, issue reference, validator JSON, clean worktree, commit. + +### CTO-WORK-040 - Stage 5 Target Repository Admission Record + +Type: HITL + +Status: blocked. + +Blocked by: CTO-WORK-039 and explicit JP selection or approval of an owned low-risk noncritical Target Repository. + +User stories covered: CTO Case Candidate Backend PRD stories 4, 5, 7, 8, 9, 10, 11, 13. + +What to build: Maintain the concrete Stage 5 Target Repository admission record. The current record is intentionally `not_admitted` and blocks Stage 5 execution until JP supplies the repository path, ownership proof, noncritical rationale, allowed paths, forbidden paths, and approval metadata. + +Acceptance criteria: + +- [x] Admission record exists as JSON. +- [x] Admission record status is `not_admitted` by default. +- [x] Admission record contains no repository path until JP approves one. +- [x] Admission record contains no secrets or credentials. +- [x] Admission record includes all required forbidden actions. +- [x] Admission record requires operator outcome. +- [x] Local CTO validator checks the safe blocked record state. +- [ ] JP supplies an owned low-risk noncritical repository path. +- [ ] JP supplies ownership evidence. +- [ ] JP supplies noncritical rationale. +- [ ] JP supplies allowed paths and forbidden paths. +- [ ] JP supplies approval source and approval timestamp. +- [ ] Admission record is updated to `admitted` only after all required fields are present. + +Allowed files: CTO child workspace planning docs and local validator only until a concrete Target Repository is approved. + +Validator: `python3 tools/validate_cto_child.py` + +Done evidence for current blocked state: admission JSON, issue reference, validator JSON, clean worktree, commit. + ## Granularity Check This is intentionally two slices: one planning route and one executable harness route. Stage 5 is not over-granular because it is the first proof involving an admitted owned repository and must separate repository ownership, approval, allowed paths, verification, and operator outcome before default candidacy. diff --git a/.sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION.json b/.sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION.json new file mode 100644 index 0000000..a346c74 --- /dev/null +++ b/.sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION.json @@ -0,0 +1,26 @@ +{ + "admission_status": "not_admitted", + "target_repository_path": "", + "repository_owner": "", + "ownership_evidence": "", + "risk_classification": "", + "noncritical_rationale": "", + "allowed_paths": [], + "forbidden_paths": [], + "forbidden_actions": [ + "push", + "merge", + "deploy", + "close", + "pr_open", + "issue_close", + "public_publication", + "credential_change", + "vendor_source_mutation", + "cortex_core_mutation" + ], + "approval_source": "", + "approval_timestamp": "", + "operator_outcome_required": true, + "review_trigger": "before Stage 5 execution, before target repository path change, before allowed path change, before forbidden action change, before risk classification change" +} diff --git a/README.md b/README.md index 5fe61fa..e89e4f1 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ This workspace is registered as a child-local planning workspace. Registration d | |-- CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-PRD.md | |-- CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.md | |-- CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.md +| |-- CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION.json | |-- CTO-CASE-PROVIDER-ADMISSION-PRD.md | |-- CTO-CASE-PROVIDER-ADMISSION-ISSUES.md | |-- CTO-CASE-PROVIDER-BUILD-PRD.md diff --git a/WORKBOARD.yaml b/WORKBOARD.yaml index 92f8740..639ca75 100644 --- a/WORKBOARD.yaml +++ b/WORKBOARD.yaml @@ -195,3 +195,8 @@ items: status: validated source: .sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.md owner: "" + - id: CTO-WORK-040 + title: Stage 5 Target Repository Admission Record + status: blocked + source: .sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION.json + owner: jp diff --git a/tools/validate_cto_child.py b/tools/validate_cto_child.py index d1416b7..848a8d7 100644 --- a/tools/validate_cto_child.py +++ b/tools/validate_cto_child.py @@ -35,6 +35,7 @@ REQUIRED_FILES = [ ".sot/03-PROTOCOLS/CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-PRD.md", ".sot/03-PROTOCOLS/CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.md", ".sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.md", + ".sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION.json", ".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-ADMISSION-PRD.md", ".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-ADMISSION-ISSUES.md", ".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-BUILD-PRD.md", @@ -334,6 +335,7 @@ REQUIRED_STAGE5_ISSUE_IDS = [ "CTO-WORK-037", "CTO-WORK-038", "CTO-WORK-039", + "CTO-WORK-040", ] REQUIRED_STAGE5_TARGET_ADMISSION_TEMPLATE_PHRASES = [ @@ -355,6 +357,31 @@ REQUIRED_STAGE5_TARGET_ADMISSION_TEMPLATE_PHRASES = [ "This template does not authorize owned repository mutation.", ] +REQUIRED_STAGE5_TARGET_ADMISSION_JSON = { + "admission_status": "not_admitted", + "target_repository_path": "", + "repository_owner": "", + "ownership_evidence": "", + "risk_classification": "", + "noncritical_rationale": "", + "approval_source": "", + "approval_timestamp": "", + "operator_outcome_required": True, +} + +REQUIRED_STAGE5_TARGET_FORBIDDEN_ACTIONS = [ + "push", + "merge", + "deploy", + "close", + "pr_open", + "issue_close", + "public_publication", + "credential_change", + "vendor_source_mutation", + "cortex_core_mutation", +] + REQUIRED_PROVIDER_ADMISSION_PRD_PHRASES = [ "Local planning SOT only. Not a Core Protocol. Not active Core authority.", "https://github.com/workos/case.git", @@ -1027,6 +1054,40 @@ def main() -> int: if phrase not in text: errors.append(f"missing_stage5_target_admission_template_phrase:{phrase}") + stage5_target_admission = ROOT / ".sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION.json" + if stage5_target_admission.is_file(): + checked.append("stage5_target_admission_json:parse") + try: + payload = json.loads(stage5_target_admission.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + errors.append(f"stage5_target_admission_invalid_json:{exc}") + payload = {} + if not isinstance(payload, dict): + errors.append("stage5_target_admission_must_be_object") + payload = {} + for key, expected in REQUIRED_STAGE5_TARGET_ADMISSION_JSON.items(): + checked.append(f"stage5_target_admission_json:{key}") + if payload.get(key) != expected: + errors.append(f"stage5_target_admission_mismatch:{key}:expected_{expected}:actual_{payload.get(key)}") + for key in ["allowed_paths", "forbidden_paths", "forbidden_actions"]: + checked.append(f"stage5_target_admission_json_list:{key}") + if not isinstance(payload.get(key), list): + errors.append(f"stage5_target_admission_missing_list:{key}") + forbidden_actions = payload.get("forbidden_actions", []) + if isinstance(forbidden_actions, list): + for action in REQUIRED_STAGE5_TARGET_FORBIDDEN_ACTIONS: + checked.append(f"stage5_target_admission_forbidden_action:{action}") + if action not in forbidden_actions: + errors.append(f"stage5_target_admission_missing_forbidden_action:{action}") + if payload.get("allowed_paths") != []: + errors.append("stage5_target_admission_allowed_paths_must_be_empty_while_not_admitted") + if not isinstance(payload.get("review_trigger"), str) or not payload.get("review_trigger"): + errors.append("stage5_target_admission_missing_review_trigger") + for key in payload: + checked.append(f"stage5_target_admission_json_secret_key:{key}") + if key.lower() in {"api_key", "apikey", "access_token", "token", "secret", "password", "credential_value"}: + errors.append(f"stage5_target_admission_forbidden_secret_key:{key}") + provider_admission_prd = ROOT / ".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-ADMISSION-PRD.md" if provider_admission_prd.is_file(): text = provider_admission_prd.read_text(encoding="utf-8") @@ -1286,6 +1347,7 @@ def main() -> int: "CTO-WORK-037": "validated", "CTO-WORK-038": "blocked", "CTO-WORK-039": "validated", + "CTO-WORK-040": "blocked", } for issue_id, expected in expected_statuses.items(): checked.append(f"workboard_status:{issue_id}:{expected}") @@ -1326,6 +1388,8 @@ def main() -> int: errors.append("workboard_missing_stage5_issues_source") if "CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.md" not in text: errors.append("workboard_missing_stage5_target_admission_template_source") + if "CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION.json" not in text: + errors.append("workboard_missing_stage5_target_admission_json_source") if "CTO-CASE-PROVIDER-ADMISSION-PRD.md" not in text: errors.append("workboard_missing_provider_admission_prd_source") if "CTO-CASE-PROVIDER-ADMISSION-ISSUES.md" not in text: