diff --git a/AGENTS.history.json b/AGENTS.history.json index c83960e..f293c01 100644 --- a/AGENTS.history.json +++ b/AGENTS.history.json @@ -35,6 +35,14 @@ "reason": "Runtime copies must correspond to an intentional release state rather than an arbitrary task worktree.", "regression": "Do not install from task worktrees, ship from feature branches, or lose post-shipment restoration and reinstall steps." }, + { + "rules": [ + "SKILLS-BATCH-01" + ], + "decision": "A named release branch is the atomic batch for explicit promotion or shipping requests.", + "reason": "Per-commit confirmation contradicts the user's intended branch-level release workflow.", + "regression": "Do not ask whether to include committed changes already on the named release branch unless the user explicitly excluded them." + }, { "rules": [ "SKILLS-GOV-01", diff --git a/AGENTS.md b/AGENTS.md index 3dcd317..05e656e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,10 @@ Skills repo checkout and worktrees: `origin/main`, reinstall managed skills from `main`, and report retained worktrees or release branches. - self: list-heavy +- [SKILLS-BATCH-01] Treat an explicit request to promote or ship a named + `release/*` branch as authorization for every commit currently on that branch + unless the user explicitly excludes one; do not request per-commit inclusion + confirmation. Instruction and skill maintenance: diff --git a/skills/ceratops-gh-repo-lifecycle/scripts/github_contract_engine/format_report.py b/skills/ceratops-gh-repo-lifecycle/scripts/github_contract_engine/format_report.py index 670c68e..cf118f4 100644 --- a/skills/ceratops-gh-repo-lifecycle/scripts/github_contract_engine/format_report.py +++ b/skills/ceratops-gh-repo-lifecycle/scripts/github_contract_engine/format_report.py @@ -44,15 +44,46 @@ re.IGNORECASE, ) RAW_OUTPUT_KEYS = {"raw_stdout", "raw_stderr", "validator_stderr"} +AUTH_VALUE_RE = re.compile(r"\b(bearer|basic)\s+[^\s,;]+", re.IGNORECASE) +CREDENTIAL_ASSIGNMENT_RE = re.compile( + r"\b(api[_ -]?key|authorization|client[_ -]?secret|cookie|credentials?|" + r"password|private[_ -]?key|secret|token)\b" + r"(\s*[:=]\s*)(\"[^\"]*\"|'[^']*'|[^\s,;]+)", + re.IGNORECASE, +) +GITHUB_TOKEN_RE = re.compile( + r"\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})\b" +) +PRIVATE_KEY_RE = re.compile( + r"-----BEGIN [^-]*PRIVATE KEY-----.*?-----END [^-]*PRIVATE KEY-----", + re.DOTALL, +) +URL_CREDENTIAL_RE = re.compile(r"(https?://)[^/\s:@]+:[^/\s@]+@", re.IGNORECASE) def _sensitive_key(name: str) -> bool: return name in SENSITIVE_KEYS or name.endswith(SENSITIVE_SUFFIXES) +def _sanitize_text(value: str) -> str: + """Redact credential forms that may appear inside arbitrary error text.""" + + result = PRIVATE_KEY_RE.sub(REDACTED, value) + result = URL_CREDENTIAL_RE.sub(rf"\1{REDACTED}@", result) + result = AUTH_VALUE_RE.sub( + lambda match: f"{match.group(1)} {REDACTED}", result + ) + result = GITHUB_TOKEN_RE.sub(REDACTED, result) + return CREDENTIAL_ASSIGNMENT_RE.sub( + lambda match: f"{match.group(1)}{match.group(2)}{REDACTED}", result + ) + + def sanitize_for_output(value: Any, path: tuple[str, ...] = ()) -> Any: """Remove collected content and sensitive values at the stdout boundary.""" + if isinstance(value, str): + return _sanitize_text(value) if isinstance(value, list): return [sanitize_for_output(item, path) for item in value] if not isinstance(value, dict): @@ -86,9 +117,13 @@ def sanitize_for_output(value: Any, path: tuple[str, ...] = ()) -> Any: def write_json(payload: dict[str, Any]) -> None: - """Write one sanitized JSON document without treating data output as a log.""" + """Write one sanitized JSON document without exposing collected secrets.""" - sys.stdout.write(json.dumps(sanitize_for_output(payload), indent=2, sort_keys=True)) + sanitized_payload = sanitize_for_output(payload) + # CodeQL cannot infer the custom recursive sanitizer. The adjacent + # regression test verifies that sensitive inputs do not reach stdout. + # codeql[py/clear-text-logging-sensitive-data] + sys.stdout.write(json.dumps(sanitized_payload, indent=2, sort_keys=True)) sys.stdout.write("\n") diff --git a/skills/ceratops-governance-lifecycle/references/propose-rules-update.md b/skills/ceratops-governance-lifecycle/references/propose-rules-update.md index 6f97137..47251a5 100644 --- a/skills/ceratops-governance-lifecycle/references/propose-rules-update.md +++ b/skills/ceratops-governance-lifecycle/references/propose-rules-update.md @@ -45,8 +45,11 @@ Route approved skill-source mutations through `$ceratops-skill-lifecycle` 5. Draft under the rule-design contract. Resolve structural defects and every affected semantic review state inside the candidate, and identify every intentional behavior change. -6. Replay the current failure and relevant recorded history. Reject a candidate - that leaves the failed decision possible or regresses a recorded outcome. +6. Before accepting a replacement, split, merge, or compression, map every + operative part of the current text, including named commands and examples, + to preserved candidate behavior or an intentional change reported for + approval; then replay the current failure and relevant recorded history, + rejecting any unaccounted or regressed behavior. 7. Report the selected correction, material alternative, regression result, and uncertainty. diff --git a/tests/test_gh_validator_summary.py b/tests/test_gh_validator_summary.py index 1ed124e..8527dc4 100644 --- a/tests/test_gh_validator_summary.py +++ b/tests/test_gh_validator_summary.py @@ -558,7 +558,18 @@ def test_machine_output_removes_sensitive_and_raw_collected_content(self): "path": "/organization/billing_email", "actual": "private@example.com", "expected": "owner@example.com", - } + }, + { + "path": "/api/repository", + "source_error": { + "message": ( + "request failed: Authorization: Bearer gho_" + + "a" * 36 + + " password=hunter2 " + + "https://user:pass@example.com/private" + ) + }, + }, ], } safe = sanitize_for_output(report) @@ -582,6 +593,9 @@ def test_machine_output_removes_sensitive_and_raw_collected_content(self): output = stream.getvalue() self.assertNotIn("secret-value", output) self.assertNotIn("private@example.com", output) + self.assertNotIn("hunter2", output) + self.assertNotIn("user:pass", output) + self.assertNotIn("gho_", output) self.assertEqual(json.loads(output), safe) def test_contract_entrypoints_use_sanitized_json_writer(self):