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
64 changes: 62 additions & 2 deletions .github/workflows/firewall_gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ name: Tenant Firewall Gate
# — and drops the `organism-map blocks (generated)` leg with the same loud
# banner. Independent of the other two decisions; graded on push to main.
#
# The Codex adapters are the fourth (PyAutoMind#520): a PR that changes what
# an opted-in repo's `.codex/hooks.json` should say (a new hook definition, a
# repo opting in) cannot make the copies on the cloned mains match — they are
# written by session_hook_propagate.yml after merge. The decision renders every
# repo's adapter from the PR's base tree and from its head
# (`scripts/codex_hooks_render.py`) and sets `codex_pr` only when they differ,
# so a repos.yaml edit that leaves the adapters alone still grades them. It
# asserts what the PR controls — the generator's hermetic tests — and drops the
# `generated Codex hooks` leg with the same loud banner; graded on push to main.
#
# All three organs are checked out. PyAutoHands was omitted at first — its
# firewall clear waited behind the version-stamp task that claimed the repo
# (PyAutoHands#235) — and joined once that leg landed as PyAutoHands#237,
Expand All @@ -58,6 +68,7 @@ on:
branches: [main]
paths:
- "scripts/repos_sync.py"
- "scripts/codex_hooks_render.py"
- "policy/session_start_hook.sh"
- "policy/end_at_deliverable_hook.sh"
- "policy/where_to_file.md"
Expand All @@ -69,6 +80,7 @@ on:
pull_request:
paths:
- "scripts/repos_sync.py"
- "scripts/codex_hooks_render.py"
- "policy/session_start_hook.sh"
- "policy/end_at_deliverable_hook.sh"
- "policy/where_to_file.md"
Expand Down Expand Up @@ -168,6 +180,28 @@ jobs:
echo "map_pr=false" >> "$GITHUB_OUTPUT"
fi

# Independent of the three decisions above (PyAutoMind#520): does this PR
# change what any repo's .codex/hooks.json should say? Rendered from the
# base tree and the head tree, never guessed from which files changed —
# a repos.yaml edit that leaves every adapter alone still grades them.
- name: Codex-adapter PR?
id: codexpr
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
run: |
mkdir -p codex_base
git -C PyAutoMind archive "origin/${BASE_REF}" scripts repos.yaml | tar -x -C codex_base
python3 PyAutoMind/scripts/codex_hooks_render.py --mind codex_base > codex_base.json
python3 PyAutoMind/scripts/codex_hooks_render.py --mind PyAutoMind > codex_head.json
if cmp -s codex_base.json codex_head.json; then
echo "codex_pr=false" >> "$GITHUB_OUTPUT"
else
echo "Codex adapters this PR changes:"
diff codex_base.json codex_head.json | sed -n 's/^> *"\([^"]*\)": .*/ \1/p'
echo "codex_pr=true" >> "$GITHUB_OUTPUT"
fi

# The compensating control, and it runs FIRST so it is never shadowed by
# the leg below: PyAutoMind is the one repo the propagation workflow
# never writes to, so its own installed copies are exactly what this PR
Expand Down Expand Up @@ -198,6 +232,16 @@ jobs:
python3 -m pytest PyAutoMind/tests/test_repos_sync_organism_map.py -q \
-k "renders_from_the_manifest"

# The compensating control for the Codex adapters: the installed copies
# are written after merge, so what this PR controls is that the generator
# renders them — every opted-in repo, deterministically — and its tests pass.
- name: Assert the Codex adapters render (copies are written after merge)
if: steps.codexpr.outputs.codex_pr == 'true'
run: |
pip install pytest
python3 -m pytest PyAutoMind/tests/test_codex_hook_sync.py -q
! grep -q '"error: ' codex_head.json

# The shared-standards rollout (#474) is complete: all 46 registered
# repos carry the generated block on main (PyAutoArray#614 and
# euclid_strong_lens_modeling_pipeline#110 were the last). This step
Expand All @@ -212,11 +256,12 @@ jobs:
--only "shared-standards blocks (generated)" --repo PyAutoMind

- name: Drift check (canonical-hook, where-to-file or repos.yaml PR — unmeetable legs skipped)
if: steps.hookpr.outputs.hook_pr == 'true' || steps.hookpr.outputs.filing_pr == 'true' || steps.hookpr.outputs.map_pr == 'true'
if: steps.hookpr.outputs.hook_pr == 'true' || steps.hookpr.outputs.filing_pr == 'true' || steps.hookpr.outputs.map_pr == 'true' || steps.codexpr.outputs.codex_pr == 'true'
env:
HOOK_PR: ${{ steps.hookpr.outputs.hook_pr }}
FILING_PR: ${{ steps.hookpr.outputs.filing_pr }}
MAP_PR: ${{ steps.hookpr.outputs.map_pr }}
CODEX_PR: ${{ steps.codexpr.outputs.codex_pr }}
run: |
skip=()
if [ "$HOOK_PR" = "true" ]; then
Expand Down Expand Up @@ -267,11 +312,26 @@ jobs:
echo "=============================================================="
skip+=(--skip "organism-map blocks (generated)")
fi
if [ "$CODEX_PR" = "true" ]; then
echo "=============================================================="
echo "SKIPPED LEG: 'generated Codex hooks'"
echo "WHY: this PR changes what an opted-in repo's .codex/hooks.json"
echo " should say (base and head trees render differently). The"
echo " sibling organs are checked out at their mains, so their"
echo " adapters are stale BY CONSTRUCTION until"
echo " .github/workflows/session_hook_propagate.yml writes them on the"
echo " push to main after this PR merges."
echo "STILL ENFORCED: the generator renders every opted-in adapter"
echo " without error and its tests pass (asserted in the step above)."
echo "ON push TO main this leg is never skipped."
echo "=============================================================="
skip+=(--skip "generated Codex hooks")
fi
python3 PyAutoMind/scripts/repos_sync.py --check \
--root "$GITHUB_WORKSPACE" "${skip[@]}"

# Every leg, the shared-standards leg included, graded on the cloned mains.
- name: Drift check (all legs — absent repos are skipped)
if: steps.hookpr.outputs.hook_pr != 'true' && steps.hookpr.outputs.filing_pr != 'true' && steps.hookpr.outputs.map_pr != 'true'
if: steps.hookpr.outputs.hook_pr != 'true' && steps.hookpr.outputs.filing_pr != 'true' && steps.hookpr.outputs.map_pr != 'true' && steps.codexpr.outputs.codex_pr != 'true'
run: |
python3 PyAutoMind/scripts/repos_sync.py --check --root "$GITHUB_WORKSPACE"
2 changes: 1 addition & 1 deletion repos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ repos:
organ: Brain
role: "Reasoning/orchestration layer; how work is decomposed and routed; the specialist agents."
public_role: "Works out *how*: classifies, plans and routes work through specialist reasoning and coding agents."
codex_hooks: [mind-commit-guard, end-at-deliverable]
codex_hooks: [mind-commit-guard, scheduler-action-guard, end-at-deliverable]
PyAutoBroca:
board_owner: true
path: organs/PyAutoBroca
Expand Down
51 changes: 51 additions & 0 deletions scripts/codex_hooks_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Print every repo's rendered `.codex/hooks.json`, as one JSON object, for a Mind tree.

python3 scripts/codex_hooks_render.py --mind <PyAutoMind tree>

The tree's own `scripts/repos_sync.py` and `repos.yaml` do the rendering, so the
same helper renders a pull request's base tree and its head alike. The firewall
gate compares the two to decide whether a PR changes what the Codex adapters
should say: if it does, every opted-in repo's installed `.codex/hooks.json` is
stale by construction until `session_hook_propagate.yml` runs after merge.
A repo whose `codex_hooks` the tree cannot render prints as its error, so a
broken manifest still reads as a change rather than hiding one.
"""

from __future__ import annotations

import argparse
import importlib
import json
import sys
from pathlib import Path


def rendered(mind_root: Path) -> dict:
sys.path.insert(0, str(mind_root / "scripts"))
try:
repos_sync = importlib.import_module("repos_sync")
finally:
sys.path.pop(0)
_, repos = repos_sync.load_manifest(mind_root)
out = {}
for name, spec in sorted(repos.items()):
if not repos_sync.codex_hook_names(spec):
continue
try:
out[name] = repos_sync.render_codex_hooks(spec)
except ValueError as error:
out[name] = f"error: {error}"
return out


def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--mind", type=Path, required=True)
args = parser.parse_args(argv)
print(json.dumps(rendered(args.mind.resolve()), sort_keys=True, indent=2))
return 0


if __name__ == "__main__":
sys.exit(main())
7 changes: 7 additions & 0 deletions scripts/repos_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,13 @@ def all_checkouts(root):
),
"statusMessage": "Checking shared PyAutoMind commit safety",
},
"scheduler-action-guard": {
"matcher": "Bash",
"command": (
'python3 "$(git rev-parse --show-toplevel)/bin/scheduler_action_guard.py"'
),
"statusMessage": "Checking for raw scheduler actions",
},
"pyauto-api-gate": {
"matcher": "Bash",
"command": (
Expand Down
19 changes: 19 additions & 0 deletions tests/test_codex_hook_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,22 @@ def test_live_manifest_opts_in_exactly_the_phase_two_repos():
"PyAutoMind", "PyAutoBrain", "autofit_assistant",
"autogalaxy_assistant", "autolens_assistant", "autocti_assistant",
}


def test_render_helper_renders_the_opted_in_repos_of_a_tree():
"""`codex_hooks_render.py` is what the firewall gate diffs between a PR's base
and head; it renders through the tree's own generator and manifest."""
import subprocess

mind = Path(__file__).resolve().parents[1]
out = subprocess.run(
[sys.executable, str(mind / "scripts" / "codex_hooks_render.py"), "--mind", str(mind)],
capture_output=True, text=True, check=True,
).stdout
rendered = json.loads(out)
_, repos = repos_sync.load_manifest(mind)
opted_in = sorted(name for name, spec in repos.items() if repos_sync.codex_hook_names(spec))
assert sorted(rendered) == opted_in
assert all(not text.startswith("error: ") for text in rendered.values())
assert rendered["PyAutoBrain"] == repos_sync.render_codex_hooks(repos["PyAutoBrain"])
assert "scheduler_action_guard.py" in rendered["PyAutoBrain"]
42 changes: 39 additions & 3 deletions tests/test_session_hook_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,10 +630,12 @@ def gate_steps():
# Either decision enables the one skipping step; only neither runs every leg.
SKIP_IF = ("steps.hookpr.outputs.hook_pr == 'true' || "
"steps.hookpr.outputs.filing_pr == 'true' || "
"steps.hookpr.outputs.map_pr == 'true'")
"steps.hookpr.outputs.map_pr == 'true' || "
"steps.codexpr.outputs.codex_pr == 'true'")
FULL_IF = ("steps.hookpr.outputs.hook_pr != 'true' && "
"steps.hookpr.outputs.filing_pr != 'true' && "
"steps.hookpr.outputs.map_pr != 'true'")
"steps.hookpr.outputs.map_pr != 'true' && "
"steps.codexpr.outputs.codex_pr != 'true'")


def test_the_skip_is_reachable_only_from_a_pull_request():
Expand Down Expand Up @@ -805,7 +807,41 @@ def test_the_gate_skips_the_map_leg_by_its_printed_label():
run = _skip_step()["run"]
assert f'--skip "{repos_sync.MAP_BLOCKS}"' in run, run
assert "repos.yaml" in run
assert run.count("ON push TO main this leg is never skipped") == 3
assert run.count("ON push TO main this leg is never skipped") == 4


# --------------------------------------------------------------------------
# The gate's Codex-adapter PR path (PyAutoMind#520) — same shape, own step
# --------------------------------------------------------------------------

def test_the_gate_skips_the_codex_leg_by_its_printed_label():
"""A renamed leg must break here rather than silently stop being skipped."""
run = _skip_step()["run"]
assert f'--skip "{repos_sync.CODEX_HOOKS}"' in run, run
assert 'if [ "$CODEX_PR" = "true" ]' in run


def test_the_codex_decision_is_pr_only_and_renders_both_trees():
"""The decision compares rendered adapters, base against head — never a
guess from which files changed — and runs only on a pull request."""
step = next(step for step in gate_steps() if step.get("id") == "codexpr")
assert step["if"] == "github.event_name == 'pull_request'"
assert step["run"].count("scripts/codex_hooks_render.py") == 2
assert 'origin/${BASE_REF}' in step["run"]
assert "codex_pr=true" in step["run"] and "codex_pr=false" in step["run"]


def test_the_gate_still_asserts_the_codex_adapters_render():
"""The compensating control for the Codex skip runs the generator's tests
and refuses a render error, before the skipping drift check."""
step = next(step for step in gate_steps()
if step.get("if") == "steps.codexpr.outputs.codex_pr == 'true'"
and "pytest" in (step.get("run") or ""))
assert "tests/test_codex_hook_sync.py" in step["run"]
assert "codex_head.json" in step["run"]
steps = gate_steps()
decision = next(s for s in steps if s.get("id") == "codexpr")
assert steps.index(decision) < steps.index(step) < steps.index(_skip_step())


def test_the_gate_still_asserts_the_manifest_renders_the_map():
Expand Down
Loading