From 94fa30b457771d4100f264b3c55c278332937046 Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Tue, 28 Jul 2026 13:27:17 +0300 Subject: [PATCH 1/2] fix: enforce strict Codex output schemas --- .../agent-completion-assessment.schema.json | 34 +++++- schemas/maintenance-plan.schema.json | 110 +++++++++++++++--- scripts/test.sh | 1 + scripts/validate-structured-output-schemas | 75 ++++++++++++ 4 files changed, 201 insertions(+), 19 deletions(-) create mode 100755 scripts/validate-structured-output-schemas diff --git a/schemas/agent-completion-assessment.schema.json b/schemas/agent-completion-assessment.schema.json index e0695db..1163cc2 100644 --- a/schemas/agent-completion-assessment.schema.json +++ b/schemas/agent-completion-assessment.schema.json @@ -1,14 +1,36 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", + "additionalProperties": false, "required": ["contractVersion", "instructionDigests", "phaseStatus", "criteria", "goNoGo", "unresolved", "summary"], "properties": { - "contractVersion": {"const": 1}, - "instructionDigests": {"type": "object"}, - "phaseStatus": {"enum": ["complete", "blocked", "needs_human"]}, - "criteria": {"type": "array"}, - "goNoGo": {"enum": ["go", "no_go"]}, - "unresolved": {"type": "array"}, + "contractVersion": {"type": "integer", "const": 1}, + "instructionDigests": { + "type": "object", + "additionalProperties": false, + "required": ["shared", "phaseTemplate", "eventContract"], + "properties": { + "shared": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "phaseTemplate": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "eventContract": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} + } + }, + "phaseStatus": {"type": "string", "enum": ["complete", "blocked", "needs_human"]}, + "criteria": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "status", "evidence"], + "properties": { + "id": {"type": "string"}, + "status": {"type": "string", "enum": ["passed", "failed", "unresolved"]}, + "evidence": {"type": "array", "items": {"type": "string"}} + } + } + }, + "goNoGo": {"type": "string", "enum": ["go", "no_go"]}, + "unresolved": {"type": "array", "items": {"type": "string"}}, "summary": {"type": "string"} } } diff --git a/schemas/maintenance-plan.schema.json b/schemas/maintenance-plan.schema.json index bd2c6bd..7ba8a73 100644 --- a/schemas/maintenance-plan.schema.json +++ b/schemas/maintenance-plan.schema.json @@ -1,41 +1,82 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", + "additionalProperties": false, "required": ["schemaVersion", "actionKey", "action", "agentContract", "evidence", "repositories", "preconditions", "editsRequired", "allowedPaths", "requiredChecks", "agentOperations", "budgets", "notification", "risk", "completionAssessment", "summary"], "properties": { - "schemaVersion": {"const": 1}, + "schemaVersion": {"type": "integer", "const": 1}, "actionKey": {"type": "string"}, - "action": {"enum": ["no_change", "new_patch", "new_branch", "branch_eol", "repair", "reconcile_partial", "blocked", "needs_human"]}, - "agentContract": {"type": "object"}, + "action": {"type": "string", "enum": ["no_change", "new_patch", "new_branch", "branch_eol", "repair", "reconcile_partial", "blocked", "needs_human"]}, + "agentContract": { + "type": "object", + "additionalProperties": false, + "required": ["contractVersion", "instructionDigests"], + "properties": { + "contractVersion": {"type": "integer", "const": 1}, + "instructionDigests": { + "type": "object", + "additionalProperties": false, + "required": ["shared", "phaseTemplate", "eventContract"], + "properties": { + "shared": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "phaseTemplate": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "eventContract": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} + } + } + } + }, "evidence": { "type": "array", "minItems": 4, "maxItems": 4, "items": { "type": "object", - "required": ["captureId", "digest", "locator"], + "additionalProperties": false, + "required": ["captureId", "digest", "claim", "locator"], "properties": { - "captureId": {"enum": ["php_bin_policy_selector", "php_bin_state", "support_policy", "policy_invariants"]}, + "captureId": {"type": "string", "enum": ["php_bin_policy_selector", "php_bin_state", "support_policy", "policy_invariants"]}, "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "claim": {"type": "string"}, "locator": { "type": "object", + "additionalProperties": false, "required": ["kind", "value"], "properties": { - "kind": {"const": "json_pointer"}, + "kind": {"type": "string", "const": "json_pointer"}, "value": {"type": "string", "pattern": "^/"} } } } } }, - "repositories": {"type": "array"}, - "preconditions": {"type": "object"}, + "repositories": {"type": "array", "items": {"type": "string", "enum": ["php-bin", "mise-php"]}, "uniqueItems": true}, + "preconditions": { + "type": "object", + "additionalProperties": false, + "required": ["misePhpHead", "phpBinPolicyCommit", "supportPolicyDigest", "policyInvariantsDigest", "phpBinOperatorCommit", "operatorState"], + "properties": { + "misePhpHead": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "phpBinPolicyCommit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "supportPolicyDigest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "policyInvariantsDigest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "phpBinOperatorCommit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "operatorState": {"type": "string", "const": "enabled"} + } + }, "editsRequired": {"type": "boolean"}, - "allowedPaths": {"type": "object"}, - "requiredChecks": {"const": ["Plugin contract"]}, + "allowedPaths": { + "type": "object", + "additionalProperties": false, + "required": ["mise-php"], + "properties": { + "mise-php": {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + } + }, + "requiredChecks": {"type": "array", "items": {"type": "string"}, "const": ["Plugin contract"]}, "agentOperations": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "budgets": { "type": "object", + "additionalProperties": false, "required": ["maxModelCalls", "maxRetries", "timeoutMinutes"], "properties": { "maxModelCalls": {"type": "integer", "minimum": 1, "maximum": 5}, @@ -43,9 +84,52 @@ "timeoutMinutes": {"type": "integer", "minimum": 1, "maximum": 60} } }, - "notification": {"type": "object"}, - "risk": {"type": "string"}, - "completionAssessment": {"type": "object"}, + "notification": { + "type": "object", + "additionalProperties": false, + "required": ["suggestedSeverity", "summary", "humanActionRequired"], + "properties": { + "suggestedSeverity": {"type": "string", "enum": ["info", "warning", "critical"]}, + "summary": {"type": "string"}, + "humanActionRequired": {"type": "boolean"} + } + }, + "risk": {"type": "string", "enum": ["routine", "compatibility", "lifecycle", "recovery", "policy-sensitive"]}, + "completionAssessment": { + "type": "object", + "additionalProperties": false, + "required": ["contractVersion", "instructionDigests", "phaseStatus", "criteria", "goNoGo", "unresolved", "summary"], + "properties": { + "contractVersion": {"type": "integer", "const": 1}, + "instructionDigests": { + "type": "object", + "additionalProperties": false, + "required": ["shared", "phaseTemplate", "eventContract"], + "properties": { + "shared": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "phaseTemplate": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "eventContract": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} + } + }, + "phaseStatus": {"type": "string", "enum": ["complete", "blocked", "needs_human"]}, + "criteria": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "status", "evidence"], + "properties": { + "id": {"type": "string"}, + "status": {"type": "string", "enum": ["passed", "failed", "unresolved"]}, + "evidence": {"type": "array", "items": {"type": "string"}} + } + } + }, + "goNoGo": {"type": "string", "enum": ["go", "no_go"]}, + "unresolved": {"type": "array", "items": {"type": "string"}}, + "summary": {"type": "string"} + } + }, "summary": {"type": "string"} } } diff --git a/scripts/test.sh b/scripts/test.sh index bc794ac..d2a62af 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,6 +7,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" "$SCRIPT_DIR/check-public-language.sh" "$SCRIPT_DIR/validate-codex-action-inputs" +"$SCRIPT_DIR/validate-structured-output-schemas" if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then echo "Plugin installation tests require macOS arm64." >&2 diff --git a/scripts/validate-structured-output-schemas b/scripts/validate-structured-output-schemas new file mode 100755 index 0000000..8ec14ec --- /dev/null +++ b/scripts/validate-structured-output-schemas @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Validate every static Codex Structured Outputs schema used by workflows.""" + +from __future__ import annotations + +import json +import pathlib +import re +import sys +from typing import Any + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +OUTPUT_SCHEMA_RE = re.compile(r'--output-schema","([^"]+\.json)"') + + +def fail(message: str) -> None: + print(f"Structured output schema error: {message}", file=sys.stderr) + raise SystemExit(1) + + +def validate_node(node: Any, location: str) -> None: + if isinstance(node, list): + for index, item in enumerate(node): + validate_node(item, f"{location}[{index}]") + return + if not isinstance(node, dict): + return + + declared_type = node.get("type") + types = {declared_type} if isinstance(declared_type, str) else set(declared_type or []) + if "object" in types: + if node.get("additionalProperties") is not False: + fail(f"{location} must set additionalProperties to false") + properties = node.get("properties") + if not isinstance(properties, dict): + fail(f"{location} must declare object properties") + required = node.get("required") + if not isinstance(required, list) or set(required) != set(properties): + fail(f"{location} must require every declared property exactly once") + if len(required) != len(set(required)): + fail(f"{location} contains duplicate required properties") + + if ("const" in node or "enum" in node) and "type" not in node: + fail(f"{location} uses const or enum without an explicit type") + + for key, value in node.items(): + validate_node(value, f"{location}.{key}") + + +def main() -> int: + schema_paths: set[pathlib.Path] = set() + for workflow in sorted((ROOT / ".github/workflows").glob("*.yml")): + for relative in OUTPUT_SCHEMA_RE.findall(workflow.read_text()): + schema_paths.add(ROOT / relative) + + if not schema_paths: + fail("no static Codex output schemas were discovered") + + for path in sorted(schema_paths): + if not path.is_file() or not path.resolve().is_relative_to(ROOT): + fail(f"unsafe or missing schema path: {path}") + try: + document = json.loads(path.read_text()) + except json.JSONDecodeError as error: + fail(f"{path.relative_to(ROOT)} is invalid JSON: {error}") + validate_node(document, str(path.relative_to(ROOT))) + + print(f"Validated {len(schema_paths)} Codex Structured Outputs schemas.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + From 59dbba0b2a17564945bbac0c3db0e42eccf65fea Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Tue, 28 Jul 2026 13:30:44 +0300 Subject: [PATCH 2/2] security: protect schema validator --- .github/CODEOWNERS | 1 + maintenance/protected-paths.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b1bf045..e590378 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,4 +8,5 @@ /scripts/admit-maintenance-plan @loadinglucian /scripts/seal-maintenance-patch @loadinglucian /scripts/validate-codex-action-inputs @loadinglucian +/scripts/validate-structured-output-schemas @loadinglucian /scripts/verify-merge-admission @loadinglucian diff --git a/maintenance/protected-paths.json b/maintenance/protected-paths.json index 5a489db..321c7a2 100644 --- a/maintenance/protected-paths.json +++ b/maintenance/protected-paths.json @@ -10,6 +10,7 @@ "scripts/admit-maintenance-plan", "scripts/seal-maintenance-patch", "scripts/validate-codex-action-inputs", + "scripts/validate-structured-output-schemas", "scripts/verify-merge-admission", "maintenance-events/*", "readiness/*",