Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/.github/codex/ @loadinglucian
/.github/codex-action-contract.json @loadinglucian
/.github/workflows/ @loadinglucian
/.github/CODEOWNERS @loadinglucian
/.codex/ @loadinglucian
/maintenance/ @loadinglucian
/schemas/ @loadinglucian
/scripts/admit-maintenance-plan @loadinglucian
/scripts/seal-maintenance-patch @loadinglucian
/scripts/validate-codex-action-inputs @loadinglucian
/scripts/verify-merge-admission @loadinglucian
42 changes: 42 additions & 0 deletions .github/codex-action-contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"schemaVersion": 1,
"action": "openai/codex-action",
"commit": "52fe01ec70a42f454c9d2ebd47598f9fd6893d56",
"metadataDigest": "sha256:d1f3b1807fd9e1ff5ea1038730ec620dfca85337f2fda5605f777071e07204ae",
"allowedInputs": [
"prompt",
"prompt-file",
"output-file",
"openai-api-key",
"responses-api-endpoint",
"working-directory",
"sandbox",
"permission-profile",
"codex-version",
"codex-args",
"output-schema",
"output-schema-file",
"model",
"effort",
"codex-home",
"safety-strategy",
"codex-user",
"allow-users",
"allow-bots",
"allow-bot-users"
],
"securityInputs": {
"allow-bots": {
"kind": "boolean",
"requiredValue": false
},
"allow-bot-users": {
"kind": "bot-user-list",
"requiredValues": [
"github-actions[bot]"
]
}
},
"requiredCodexVersion": "0.145.0",
"expectedInvocations": 3
}
24 changes: 15 additions & 9 deletions .github/workflows/maintenance-consumer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,22 @@ jobs:
'{shared:$shared,phaseTemplate:$phaseTemplate,eventContract:$eventContract}'
} > maintenance-run/investigation-prompt.md
mkdir -p "$RUNNER_TEMP/codex-home"
cp .codex/investigation.config.toml "$RUNNER_TEMP/codex-home/"
cp .codex/investigation.config.toml "$RUNNER_TEMP/codex-home/config.toml"
- name: Run repository-scoped offline Codex investigation
if: steps.compare.outputs.trigger != 'quiet' && steps.operator.outputs.state == 'enabled'
uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 # v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
codex-version: 0.145.0
prompt-file: maintenance-run/investigation-prompt.md
output-file: maintenance-run/maintenance-plan.json
codex-home: ${{ runner.temp }}/codex-home
sandbox: read-only
safety-strategy: drop-sudo
allow-users: loadinglucian
allow-bots: github-actions[bot]
codex-args: '["--strict-config","--ephemeral","--profile","investigation","--output-schema","schemas/maintenance-plan.schema.json"]'
allow-bots: false
allow-bot-users: github-actions[bot]
codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/maintenance-plan.schema.json"]'
- name: Admit plan against exact policy and checkout
if: steps.compare.outputs.trigger != 'quiet' && steps.operator.outputs.state == 'enabled'
run: |
Expand Down Expand Up @@ -191,19 +193,21 @@ jobs:
'.agentContract.instructionDigests={shared:$shared,phaseTemplate:$phaseTemplate,eventContract:$eventContract}' \
maintenance-run/maintenance-plan.json > maintenance-run/implementation-plan.json
mkdir -p "$RUNNER_TEMP/codex-home"
cp .codex/implementation.config.toml "$RUNNER_TEMP/codex-home/"
cp .codex/implementation.config.toml "$RUNNER_TEMP/codex-home/config.toml"
- name: Run offline Codex implementation
uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 # v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
codex-version: 0.145.0
prompt-file: maintenance-run/implementation-prompt.md
output-file: maintenance-run/implementation-result.json
codex-home: ${{ runner.temp }}/codex-home
sandbox: workspace-write
safety-strategy: drop-sudo
allow-users: loadinglucian
allow-bots: github-actions[bot]
codex-args: '["--strict-config","--ephemeral","--profile","implementation","--output-schema","schemas/agent-completion-assessment.schema.json"]'
allow-bots: false
allow-bot-users: github-actions[bot]
codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/agent-completion-assessment.schema.json"]'
- name: Seal admitted diff
run: |
./scripts/seal-maintenance-patch \
Expand Down Expand Up @@ -341,19 +345,21 @@ jobs:
printf '\n```\n'
} > maintenance-run/repair-prompt.md
mkdir -p "$RUNNER_TEMP/codex-home"
cp .codex/repair.config.toml "$RUNNER_TEMP/codex-home/"
cp .codex/repair.config.toml "$RUNNER_TEMP/codex-home/config.toml"
- name: Run one offline Codex repair
uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 # v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
codex-version: 0.145.0
prompt-file: maintenance-run/repair-prompt.md
output-file: maintenance-run/repair-result.json
codex-home: ${{ runner.temp }}/codex-home
sandbox: workspace-write
safety-strategy: drop-sudo
allow-users: loadinglucian
allow-bots: github-actions[bot]
codex-args: '["--strict-config","--ephemeral","--profile","repair","--output-schema","schemas/agent-completion-assessment.schema.json"]'
allow-bots: false
allow-bot-users: github-actions[bot]
codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/agent-completion-assessment.schema.json"]'
- name: Seal combined repaired diff
run: |
mv maintenance-run/repair-plan.json maintenance-run/implementation-plan.json
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ From a checkout containing both repositories:
--output ./verification-results
```

Each repository's `scripts/test.sh` also validates every pinned Codex Action
invocation, exact CLI version, and canonical `config.toml` loading against the
reviewed offline contract in `.github/codex-action-contract.json` before
exercising maintenance behavior.

Inspect `support-snapshot.json`, `maintenance-events/`, `readiness/`, retained
workflow artifacts, and the event's GitHub issue. Recovery corrects the cause
and reruns the normal admitted path; it never disables checksum, policy,
Expand Down
2 changes: 2 additions & 0 deletions maintenance/protected-paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
"schemaVersion": 1,
"patterns": [
".github/codex/maintenance/*",
".github/codex-action-contract.json",
".github/workflows/*",
".codex/*",
"schemas/*",
"maintenance/*",
"scripts/admit-maintenance-plan",
"scripts/seal-maintenance-patch",
"scripts/validate-codex-action-inputs",
"scripts/verify-merge-admission",
"maintenance-events/*",
"readiness/*",
Expand Down
1 change: 1 addition & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

"$SCRIPT_DIR/check-public-language.sh"
"$SCRIPT_DIR/validate-codex-action-inputs"

if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then
echo "Plugin installation tests require macOS arm64." >&2
Expand Down
144 changes: 144 additions & 0 deletions scripts/validate-codex-action-inputs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Validate every Codex Action invocation against the reviewed offline contract."""

from __future__ import annotations

import argparse
import json
import pathlib
import re
import subprocess
import sys
from typing import Any


ROOT = pathlib.Path(__file__).resolve().parents[1]
CONTRACT_PATH = ROOT / ".github/codex-action-contract.json"
BOT_USER_RE = re.compile(r"^[A-Za-z0-9-]+(?:\[bot\])?$")


def load_workflow(path: pathlib.Path) -> dict[str, Any]:
"""Load one GitHub Actions workflow through Ruby's YAML parser."""
script = (
"document = YAML.safe_load(File.read(ARGV.fetch(0)), aliases: true); "
"STDOUT.write(JSON.generate(document))"
)
result = subprocess.run(
["ruby", "-ryaml", "-rjson", "-e", script, str(path)],
cwd=ROOT,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
document = json.loads(result.stdout)
if not isinstance(document, dict):
raise ValueError(f"workflow is not an object: {path}")
return document


def validate() -> dict[str, Any]:
"""Validate every repository Codex Action call and return audit evidence."""
contract = json.loads(CONTRACT_PATH.read_text())
action = contract["action"]
commit = contract["commit"]
if not re.fullmatch(r"[0-9a-f]{40}", commit):
raise ValueError("Codex Action contract commit is not an exact SHA")
if not re.fullmatch(r"sha256:[0-9a-f]{64}", contract["metadataDigest"]):
raise ValueError("Codex Action metadata digest is invalid")

allowed_inputs = set(contract["allowedInputs"])
required_codex_version = contract["requiredCodexVersion"]
if not re.fullmatch(r"\d+\.\d+\.\d+", required_codex_version):
raise ValueError("required Codex CLI version is not exact")
required_bot_users = set(contract["securityInputs"]["allow-bot-users"]["requiredValues"])
expected_ref = f"{action}@{commit}"
invocations = []
canonical_config_copies = 0
for path in sorted((ROOT / ".github/workflows").glob("*.yml")):
workflow_body = path.read_text()
canonical_config_copies += workflow_body.count('"$RUNNER_TEMP/codex-home/config.toml"')
if re.search(r'cp\s+\.codex/\S+\.config\.toml\s+"\$RUNNER_TEMP/codex-home/"', workflow_body):
raise ValueError(f"{path.relative_to(ROOT)} copies a phase config under an unloaded filename")
document = load_workflow(path)
jobs = document.get("jobs", {})
if not isinstance(jobs, dict):
raise ValueError(f"workflow jobs are not an object: {path}")
for job_name, job in jobs.items():
if not isinstance(job, dict):
continue
for index, step in enumerate(job.get("steps", [])):
if not isinstance(step, dict):
continue
uses = step.get("uses")
if not isinstance(uses, str) or not uses.startswith(f"{action}@"):
continue
location = f"{path.relative_to(ROOT)}:{job_name}:step-{index + 1}"
if uses != expected_ref:
raise ValueError(f"{location} does not use reviewed Codex Action SHA")
inputs = step.get("with", {})
if not isinstance(inputs, dict):
raise ValueError(f"{location} inputs are not an object")
unknown = sorted(set(inputs) - allowed_inputs)
if unknown:
raise ValueError(f"{location} has unknown Codex Action inputs: {unknown}")
if inputs.get("codex-version") != required_codex_version:
raise ValueError(f"{location} does not pin the reviewed Codex CLI version")
codex_args = inputs.get("codex-args")
try:
parsed_args = json.loads(codex_args)
except (TypeError, json.JSONDecodeError) as error:
raise ValueError(f"{location} codex-args must be a JSON array") from error
if not isinstance(parsed_args, list) or not all(isinstance(item, str) for item in parsed_args):
raise ValueError(f"{location} codex-args must contain only strings")
if "--strict-config" not in parsed_args or "--ephemeral" not in parsed_args:
raise ValueError(f"{location} omits required strict or ephemeral execution")
if any(item == "--profile" or item.startswith("--profile=") for item in parsed_args):
raise ValueError(f"{location} selects an undefined named profile")
allow_bots = inputs.get("allow-bots")
if type(allow_bots) is not bool or allow_bots is not False:
raise ValueError(f"{location} allow-bots must be the boolean false")
allow_bot_users = inputs.get("allow-bot-users")
if not isinstance(allow_bot_users, str):
raise ValueError(f"{location} allow-bot-users must be a bot username list")
bot_users = {item.strip() for item in allow_bot_users.split(",") if item.strip()}
if bot_users != required_bot_users or not all(BOT_USER_RE.fullmatch(item) for item in bot_users):
raise ValueError(f"{location} allow-bot-users does not match the reviewed bot allowlist")
invocations.append({"workflow": str(path.relative_to(ROOT)), "job": job_name, "step": index + 1})

expected_count = contract["expectedInvocations"]
if len(invocations) != expected_count:
raise ValueError(f"expected {expected_count} Codex Action invocations, found {len(invocations)}")
if canonical_config_copies != expected_count:
raise ValueError(
f"expected {expected_count} canonical Codex config copies, found {canonical_config_copies}"
)
return {
"action": action,
"commit": commit,
"codexVersion": required_codex_version,
"metadataDigest": contract["metadataDigest"],
"invocations": invocations,
"result": "passed",
}


def main() -> int:
"""Run validation and emit human-readable or machine-readable output."""
parser = argparse.ArgumentParser()
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
try:
result = validate()
if args.json:
print(json.dumps(result, sort_keys=True))
else:
print(f"Validated {len(result['invocations'])} pinned Codex Action invocations.")
return 0
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as error:
print(f"Codex Action input validation failed: {error}", file=sys.stderr)
return 1


if __name__ == "__main__":
raise SystemExit(main())
2 changes: 2 additions & 0 deletions test/test_maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ def test_readiness_requires_exact_commits_and_digests(self):
readiness("new_branch:8.6", "main", "bad", "bad", "main", [])

def test_protected_controls_are_not_admissible(self):
self.assertTrue(protected(".github/codex-action-contract.json"))
self.assertTrue(protected(".github/workflows/maintenance.yml"))
self.assertTrue(protected("maintenance/admission.py"))
self.assertTrue(protected("scripts/validate-codex-action-inputs"))
self.assertTrue(protected("maintenance-events/new-patch.json"))
self.assertTrue(protected("readiness/new-branch.json"))
self.assertFalse(protected("lib/releases.lua"))
Expand Down
Loading