Skip to content

fix: enforce strict Codex output schemas - #14

Merged
loadinglucian merged 2 commits into
mainfrom
fix/structured-output-schemas
Jul 28, 2026
Merged

fix: enforce strict Codex output schemas#14
loadinglucian merged 2 commits into
mainfrom
fix/structured-output-schemas

Conversation

@loadinglucian

@loadinglucian loadinglucian commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Make every production Codex output object closed and require every declared property.
  • Add explicit types for constants and enums.
  • Define exact evidence, precondition, path, notification, and completion shapes.
  • Validate every static workflow output schema offline and protect that validator.

Verification

  • scripts/test.sh
  • Exact-SHA A00-A20 verifier: sha256:6e54b3f5ce69e396e4ba6d3bf2490fd79a16d2e4d71610ba6091de305656321a
  • Paired php-bin commit: 4725bd3a4bc3a30b9ac006ea83ca48a91cf3e47d
  • Quiet-path consumer run 30350370770 passed
  • Merged-main actionable consumer passes when the next policy event occurs

Security and licensing

  • No credential, permission, dependency, or license changes
  • Schema changes preserve deterministic admission and protected-path controls
  • Validator is covered by CODEOWNERS and the runtime protected-path manifest

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request tightens two structured-output JSON schemas, adds recursive schema validation for workflow-referenced schemas, and invokes that validator from the test script.

Changes

Structured output schema validation

Layer / File(s) Summary
Completion assessment contract
schemas/agent-completion-assessment.schema.json
Requires fixed fields, typed version and digest values, closed objects, constrained statuses, and structured criteria evidence.
Maintenance plan contract
schemas/maintenance-plan.schema.json
Adds strict validation for plan inputs, evidence, repositories, preconditions, budgets, notifications, and completion assessments.
Automated schema checking
scripts/validate-structured-output-schemas, scripts/test.sh
Discovers workflow-referenced schemas, validates their structure recursively, and runs the validator during tests.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The template sections exist, but several required verification and security checklist items are missing or replaced with unrelated checks. Add the missing template checkboxes for unsupported targets, release/checksum behavior, and php-bin compatibility, and explicitly confirm SHA-256 and no committed fixtures/install dirs.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: stricter Codex output schema validation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/structured-output-schemas

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
schemas/maintenance-plan.schema.json (1)

10-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate instructionDigests schema block — extract to $defs.

The identical instructionDigests object (shared/phaseTemplate/eventContract with sha256 pattern) is spelled out twice: once under agentContract (Lines 16-25) and once under completionAssessment (Lines 104-112). Any future change to the digest pattern or required fields must be applied in both places or the two contract checkpoints silently drift apart.

♻️ Proposed refactor using a shared definition
+  "$defs": {
+    "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}$"}
+      }
+    }
+  },
   "properties": {
     ...
     "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}$"}
-          }
-        }
+        "instructionDigests": {"$ref": "`#/`$defs/instructionDigests"}
       }
     },
     ...
     "completionAssessment": {
       ...
-        "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}$"}
-          }
-        },
+        "instructionDigests": {"$ref": "`#/`$defs/instructionDigests"},
     }

Note: scripts/validate-structured-output-schemas (context snippet 3) recurses into every dict node regardless of $ref indirection, so a $defs entry following the same closed-object pattern would still pass its checks. Please confirm the exact Codex structured-output subset in use (codex-version: 0.145.0, per .github/workflows/maintenance-consumer.yml) actually supports $ref/$defs before adopting, since unsupported keywords would break the live consumer run.

Also applies to: 98-113

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@schemas/maintenance-plan.schema.json` around lines 10 - 27, Extract the
duplicated instructionDigests object into a shared $defs entry and replace both
agentContract and completionAssessment copies with references to it, preserving
the required fields, closed-object constraint, and SHA-256 pattern. Before
applying the refactor, verify that Codex structured-output version 0.145.0
supports $ref/$defs; if unsupported, retain the duplicated schema and avoid
introducing unsupported keywords.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@schemas/maintenance-plan.schema.json`:
- Around line 28-51: Update the evidence schema to reject duplicate captureId
values, not merely duplicate full evidence objects. Add uniqueItems: true and a
capture-level uniqueness constraint using the project’s supported schema
mechanism, while preserving the existing four-item bound and allowed captureId
enum.

In `@scripts/validate-structured-output-schemas`:
- Line 14: Make workflow schema discovery format-complete by updating
OUTPUT_SCHEMA_RE to accept normal whitespace around the --output-schema argument
and the static argument forms used by workflows, not only the exact comma-quote
sequence. Update the workflow globbing logic at the related discovery sites to
scan both .yml and .yaml files, while preserving schema validation across all
discovered files.
- Around line 30-43: Update the schema validation logic around the
declared_type/types handling so any schema containing properties, required, or
additionalProperties is treated as object-shaped and must explicitly include
"object" in types. Reject such schemas when "object" is absent, while preserving
the existing strict additionalProperties, properties, and required validations
for explicitly typed objects.

---

Nitpick comments:
In `@schemas/maintenance-plan.schema.json`:
- Around line 10-27: Extract the duplicated instructionDigests object into a
shared $defs entry and replace both agentContract and completionAssessment
copies with references to it, preserving the required fields, closed-object
constraint, and SHA-256 pattern. Before applying the refactor, verify that Codex
structured-output version 0.145.0 supports $ref/$defs; if unsupported, retain
the duplicated schema and avoid introducing unsupported keywords.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b9b946bd-c07a-4d77-b82e-52e171602ee2

📥 Commits

Reviewing files that changed from the base of the PR and between fc5f72f and 94fa30b.

📒 Files selected for processing (4)
  • schemas/agent-completion-assessment.schema.json
  • schemas/maintenance-plan.schema.json
  • scripts/test.sh
  • scripts/validate-structured-output-schemas

Comment on lines 28 to 51
"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": "^/"}
}
}
}
}
},

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)schemas/maintenance-plan\.schema\.json$|maintenance-plan' || true

echo "== schema excerpt =="
if [ -f schemas/maintenance-plan.schema.json ]; then
  cat -n schemas/maintenance-plan.schema.json | sed -n '1,120p'
fi

echo "== uniqueItems occurrences =="
if [ -f schemas/maintenance-plan.schema.json ]; then
  python3 - <<'PY'
import json
p='schemas/maintenance-plan.schema.json'
data=json.load(open(p))
for path, val in json_paths({'$': data}, '$'):
    if isinstance(val, dict) and val.get('uniqueItems') is True:
        print(path)
PY
fi

echo "== captureId occurrences in repo =="
rg -n '"captureId"|captureId' -S . || true

echo "== validator behavior probe with python jsonschema if available =="
python3 - <<'PY'
import json, sys
try:
    from jsonschema import validate, ValidationError
except Exception as e:
    print("jsonschema unavailable:", type(e).__name__, str(e))
    sys.exit(0)

schema = json.load(open('schemas/maintenance-plan.schema.json'))
duplicate = {
  "evidence": [
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","claim":"first","locator":{"kind":"json_pointer","value":"/a"}},
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","claim":"second","locator":{"kind":"json_pointer","value":"/a"}},
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","claim":"third","locator":{"kind":"json_pointer","value":"/a"}},
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000003","claim":"fourth","locator":{"kind":"json_pointer","value":"/a"}},
  ],
}
try:
    validate(duplicate, schema)
    print("duplicate-first captureId VALID without uniqueItems")
except ValidationError as err:
    print("duplicate-first captureId INVALID:", str(err))

with_unique = json.loads(json.dumps(schema))
with_unique['properties']['evidence']['uniqueItems'] = True
try:
    validate(duplicate, with_unique)
    print("duplicate-first captureId VALID with uniqueItems")
except ValidationError as err:
    print("duplicate-first captureId INVALID with uniqueItems:", str(err))

unique = duplicate.copy()
unique["evidence"] = [
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","claim":"first","locator":{"kind":"json_pointer","value":"/a"}},
      {"captureId":"php_bin_state","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","claim":"second","locator":{"kind":"json_pointer","value":"/b"}},
      {"captureId":"support_policy","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","claim":"third","locator":{"kind":"json_pointer","value":"/c"}},
      {"captureId":"policy_invariants","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000003","claim":"fourth","locator":{"kind":"json_pointer","value":"/d"}},
]
try:
    validate(unique, with_unique)
    print("all-four distinct captureIds VALID with uniqueItems")
except ValidationError as err:
    print("all-four distinct captureIds INVALID with uniqueItems:", str(err))
PY

Repository: Bigpixelrocket/mise-php

Length of output: 6722


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json

schema = json.load(open('schemas/maintenance-plan.schema.json'))

def scan(obj, path='$'):
    if isinstance(obj, dict):
        yield path, obj
        for k, v in obj.items():
            yield from scan(v, f'{path}.{k}')
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            yield from scan(v, f'{path}[{i}]')

for path, val in scan(schema):
    if isinstance(val, dict) and val.get('uniqueItems') is True:
        parent_path = path.rsplit('.', 1)[0] if '.' in path else '/properties/' + path
        print(path, 'in', parent_path)

required_capture = schema['properties']['evidence']['items']['properties']['captureId'].get('enum')
print(
    "evidence length:", schema['properties']['evidence'].get('minItems'), schema['properties']['evidence'].get('maxItems'),
    "captureIds:", required_capture,
    "evidence uniqueItems:", schema['properties']['evidence'].get('uniqueItems')
)
items_props = schema['properties']['evidence']['items']['properties']
print("evidence item required:", schema['properties']['evidence']['items']['required'])
PY

echo
if python3 - <<'PY'
import importlib.util
print(importlib.util.find_spec("jsonschema"))
PY
then
  python3 - <<'PY'
from jsonschema import validate, ValidationError
import json

schema = json.load(open('schemas/maintenance-plan.schema.json'))

duplicate = {
  "evidence": [
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","claim":"first","locator":{"kind":"json_pointer","value":"/a"}},
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","claim":"second","locator":{"kind":"json_pointer","value":"/a"}},
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","claim":"third","locator":{"kind":"json_pointer","value":"/a"}},
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000003","claim":"fourth","locator":{"kind":"json_pointer","value":"/a"}},
  ],
}
try:
    validate(duplicate, schema)
    print("duplicate-first captureId VALID without uniqueItems")
except ValidationError as err:
    print("duplicate-first captureId INVALID:", str(err))

with_unique = json.loads(json.dumps(schema))
with_unique['properties']['evidence']['uniqueItems'] = True
try:
    validate(duplicate, with_unique)
    print("duplicate-first captureId VALID with uniqueItems")
except ValidationError as err:
    print("duplicate-first captureId INVALID with uniqueItems:", str(err))

unique = duplicate.copy()
unique["evidence"] = [
      {"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","claim":"first","locator":{"kind":"json_pointer","value":"/a"}},
      {"captureId":"php_bin_state","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","claim":"second","locator":{"kind":"json_pointer","value":"/b"}},
      {"captureId":"support_policy","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","claim":"third","locator":{"kind":"json_pointer","value":"/c"}},
      {"captureId":"policy_invariants","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000003","claim":"fourth","locator":{"kind":"json_pointer","value":"/d"}},
]
try:
    validate(unique, with_unique)
    print("all-four distinct captureIds VALID with uniqueItems")
except ValidationError as err:
    print("all-four distinct captureIds INVALID with uniqueItems:", str(err))
  PY
else
  echo "jsonschema library not available"
  echo "Behavioral summary remains deterministic: JSON Schema uniqueItems compares array items by JSON equality; two evidence objects differ because their non-captureId fields (digest/claim/locator) differ, so uniqueItems alone does not enforce distinct captureIds. It only prevents exact duplicate evidence entries."
fi

Repository: Bigpixelrocket/mise-php

Length of output: 687


Prevent duplicate evidence captureIds in the schema.

evidence is bounded to 4 items from a 4-value captureId enum, but without uniqueItems plus capture-level uniqueness it still accepts entries that omit a required evidence type. Add uniqueItems: true and another check that forces distinct captureId values, for example via a separate requiresCaptureIds keyword, enum constraint on each item, or an unevaluatedProperties rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@schemas/maintenance-plan.schema.json` around lines 28 - 51, Update the
evidence schema to reject duplicate captureId values, not merely duplicate full
evidence objects. Add uniqueItems: true and a capture-level uniqueness
constraint using the project’s supported schema mechanism, while preserving the
existing four-item bound and allowed captureId enum.



ROOT = pathlib.Path(__file__).resolve().parents[1]
OUTPUT_SCHEMA_RE = re.compile(r'--output-schema","([^"]+\.json)"')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make workflow schema discovery format-complete.

The .yml-only glob and exact --output-schema","…" regex can miss .yaml workflows or normally spaced command arguments; validation can still pass if another schema is discovered. Scan both extensions and support the static argument forms used by workflows.

Also applies to: 53-55

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/validate-structured-output-schemas` at line 14, Make workflow schema
discovery format-complete by updating OUTPUT_SCHEMA_RE to accept normal
whitespace around the --output-schema argument and the static argument forms
used by workflows, not only the exact comma-quote sequence. Update the workflow
globbing logic at the related discovery sites to scan both .yml and .yaml files,
while preserving schema validation across all discovered files.

Comment on lines +30 to +43
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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Detect object-shaped schemas that omit type: object.

At Line 32, validation runs only for explicitly typed objects. JSON Schema allows properties, required, or additionalProperties without type, so an open object can bypass this strictness check. Reject object keywords unless type includes object.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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")
OBJECT_KEYWORDS = frozenset(
{
"properties",
"required",
"additionalProperties",
"patternProperties",
"propertyNames",
"dependentRequired",
"dependentSchemas",
"unevaluatedProperties",
"minProperties",
"maxProperties",
}
)
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:
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")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/validate-structured-output-schemas` around lines 30 - 43, Update the
schema validation logic around the declared_type/types handling so any schema
containing properties, required, or additionalProperties is treated as
object-shaped and must explicitly include "object" in types. Reject such schemas
when "object" is absent, while preserving the existing strict
additionalProperties, properties, and required validations for explicitly typed
objects.

@loadinglucian
loadinglucian merged commit 433d469 into main Jul 28, 2026
2 of 3 checks passed
@loadinglucian
loadinglucian deleted the fix/structured-output-schemas branch July 28, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant