Add blocked Stage 5 target admission record

This commit is contained in:
Svrnty 2026-05-31 23:48:56 -04:00
parent f8a6d6873d
commit 3f8a2eeeab
5 changed files with 132 additions and 1 deletions

View File

@ -55,7 +55,7 @@ Type: HITL
Status: blocked. 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. 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. 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 ## 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. 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.

View File

@ -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"
}

View File

@ -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-PRD.md
| |-- CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.md | |-- CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.md
| |-- CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.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-PRD.md
| |-- CTO-CASE-PROVIDER-ADMISSION-ISSUES.md | |-- CTO-CASE-PROVIDER-ADMISSION-ISSUES.md
| |-- CTO-CASE-PROVIDER-BUILD-PRD.md | |-- CTO-CASE-PROVIDER-BUILD-PRD.md

View File

@ -195,3 +195,8 @@ items:
status: validated status: validated
source: .sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.md source: .sot/03-PROTOCOLS/CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.md
owner: "" 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

View File

@ -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-PRD.md",
".sot/03-PROTOCOLS/CTO-CASE-STAGE5-OWNED-NONCRITICAL-REPO-ISSUES.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-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-PRD.md",
".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-ADMISSION-ISSUES.md", ".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-ADMISSION-ISSUES.md",
".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-BUILD-PRD.md", ".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-BUILD-PRD.md",
@ -334,6 +335,7 @@ REQUIRED_STAGE5_ISSUE_IDS = [
"CTO-WORK-037", "CTO-WORK-037",
"CTO-WORK-038", "CTO-WORK-038",
"CTO-WORK-039", "CTO-WORK-039",
"CTO-WORK-040",
] ]
REQUIRED_STAGE5_TARGET_ADMISSION_TEMPLATE_PHRASES = [ REQUIRED_STAGE5_TARGET_ADMISSION_TEMPLATE_PHRASES = [
@ -355,6 +357,31 @@ REQUIRED_STAGE5_TARGET_ADMISSION_TEMPLATE_PHRASES = [
"This template does not authorize owned repository mutation.", "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 = [ REQUIRED_PROVIDER_ADMISSION_PRD_PHRASES = [
"Local planning SOT only. Not a Core Protocol. Not active Core authority.", "Local planning SOT only. Not a Core Protocol. Not active Core authority.",
"https://github.com/workos/case.git", "https://github.com/workos/case.git",
@ -1027,6 +1054,40 @@ def main() -> int:
if phrase not in text: if phrase not in text:
errors.append(f"missing_stage5_target_admission_template_phrase:{phrase}") 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" provider_admission_prd = ROOT / ".sot/03-PROTOCOLS/CTO-CASE-PROVIDER-ADMISSION-PRD.md"
if provider_admission_prd.is_file(): if provider_admission_prd.is_file():
text = provider_admission_prd.read_text(encoding="utf-8") text = provider_admission_prd.read_text(encoding="utf-8")
@ -1286,6 +1347,7 @@ def main() -> int:
"CTO-WORK-037": "validated", "CTO-WORK-037": "validated",
"CTO-WORK-038": "blocked", "CTO-WORK-038": "blocked",
"CTO-WORK-039": "validated", "CTO-WORK-039": "validated",
"CTO-WORK-040": "blocked",
} }
for issue_id, expected in expected_statuses.items(): for issue_id, expected in expected_statuses.items():
checked.append(f"workboard_status:{issue_id}:{expected}") checked.append(f"workboard_status:{issue_id}:{expected}")
@ -1326,6 +1388,8 @@ def main() -> int:
errors.append("workboard_missing_stage5_issues_source") errors.append("workboard_missing_stage5_issues_source")
if "CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.md" not in text: if "CTO-CASE-STAGE5-TARGET-REPOSITORY-ADMISSION-TEMPLATE.md" not in text:
errors.append("workboard_missing_stage5_target_admission_template_source") 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: if "CTO-CASE-PROVIDER-ADMISSION-PRD.md" not in text:
errors.append("workboard_missing_provider_admission_prd_source") errors.append("workboard_missing_provider_admission_prd_source")
if "CTO-CASE-PROVIDER-ADMISSION-ISSUES.md" not in text: if "CTO-CASE-PROVIDER-ADMISSION-ISSUES.md" not in text: