-
Notifications
You must be signed in to change notification settings - Fork 0
fix: enforce strict Codex output schemas #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"} | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,51 +1,135 @@ | ||
| { | ||
| "$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}, | ||
| "maxRetries": {"type": "integer", "minimum": 1, "maximum": 3}, | ||
| "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"} | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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)"') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Make workflow schema discovery format-complete. The Also applies to: 53-55 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Detect object-shaped schemas that omit At Line 32, validation runs only for explicitly typed objects. JSON Schema allows Proposed fix+OBJECT_KEYWORDS = frozenset(
+ {"properties", "required", "additionalProperties", "patternProperties",
+ "propertyNames", "dependentRequired", "dependentSchemas",
+ "unevaluatedProperties", "minProperties", "maxProperties"}
+)
+
def validate_node(node: Any, location: str) -> None:
...
declared_type = node.get("type")
types = {declared_type} if isinstance(declared_type, str) else set(declared_type or [])
+ if OBJECT_KEYWORDS.intersection(node) and "object" not in types:
+ fail(f"{location} uses object keywords without type: object")
if "object" in types:📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Bigpixelrocket/mise-php
Length of output: 6722
🏁 Script executed:
Repository: Bigpixelrocket/mise-php
Length of output: 687
Prevent duplicate evidence
captureIds in the schema.evidenceis bounded to 4 items from a 4-valuecaptureIdenum, but withoutuniqueItemsplus capture-level uniqueness it still accepts entries that omit a required evidence type. AdduniqueItems: trueand another check that forces distinctcaptureIdvalues, for example via a separaterequiresCaptureIdskeyword, enum constraint on each item, or anunevaluatedPropertiesrule.🤖 Prompt for AI Agents