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
37 changes: 36 additions & 1 deletion src/autoskillit/hook_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class HookDef:
"works-as-is"
)
mechanism: Literal["deny", "additionalContext", "output-rewrite"] = "deny"
enforcement_strength: dict[str, str] = field(default_factory=dict)

def __post_init__(self) -> None:
if self.event_type != "SessionStart" and not self.matcher:
Expand Down Expand Up @@ -96,17 +97,20 @@ def __post_init__(self) -> None:
"guards/pipeline_step_guard.py",
],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher="mcp__.*autoskillit.*__remove_clone",
scripts=["guards/remove_clone_guard.py"],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"mcp__.*autoskillit.*__open_kitchen.*",
scripts=["guards/open_kitchen_guard.py"],
timeout_seconds=5,
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher="AskUserQuestion",
Expand All @@ -115,21 +119,25 @@ def __post_init__(self) -> None:
session_scope="headless_only",
codex_status="not-applicable",
mechanism="deny",
enforcement_strength={"claude_code": "hard", "codex": "not-applicable"},
),
HookDef(
matcher=r"mcp__.*autoskillit.*__merge_worktree",
scripts=["guards/branch_protection_guard.py"],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"mcp__.*autoskillit.*__push_to_remote",
scripts=["guards/branch_protection_guard.py"],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/unsafe_install_guard.py"],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
Expand All @@ -149,23 +157,27 @@ def __post_init__(self) -> None:
# stdlib-only boundary on hook scripts prevents a shared import.
exempt_session_types=frozenset({"orchestrator"}),
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/compose_pr_body_guard.py"],
session_scope="headless_only",
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/planner_gh_discovery_guard.py"],
session_scope="headless_only",
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/artifact_download_guard.py"],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
Expand All @@ -175,6 +187,7 @@ def __post_init__(self) -> None:
# stdlib-only boundary on hook scripts prevents a shared import.
exempt_session_types=frozenset({"orchestrator"}),
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__run_cmd",
Expand All @@ -184,35 +197,41 @@ def __post_init__(self) -> None:
# stdlib-only boundary on hook scripts prevents a shared import.
exempt_skills=frozenset({"implement-experiment"}),
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Write|Edit",
scripts=["guards/generated_file_write_guard.py"],
mechanism="deny",
enforcement_strength={"claude_code": "hard", "codex": "works-as-is"},
),
HookDef(
matcher=r"Write|Edit|Bash|mcp__.*autoskillit.*__run_cmd",
scripts=["guards/write_guard.py"],
session_scope="headless_only",
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Write|Edit",
scripts=["guards/planner_result_naming_guard.py"],
session_scope="headless_only",
mechanism="deny",
enforcement_strength={"claude_code": "hard", "codex": "works-as-is"},
),
HookDef(
matcher=r"Write|Edit",
scripts=["guards/recipe_write_advisor.py"],
session_scope="interactive_only",
mechanism="deny",
enforcement_strength={"claude_code": "hard", "codex": "works-as-is"},
),
HookDef(
matcher=r"Grep",
scripts=["guards/grep_pattern_lint_guard.py"],
codex_status="not-applicable",
mechanism="deny",
enforcement_strength={"claude_code": "hard", "codex": "not-applicable"},
),
HookDef(
matcher=r"Bash|Write|Edit|Read|Glob|Grep",
Expand All @@ -221,24 +240,28 @@ def __post_init__(self) -> None:
session_scope="interactive_only",
codex_status="degraded",
mechanism="deny",
enforcement_strength={"claude_code": "hard", "codex": "degraded"},
),
HookDef(
matcher=r"mcp__.*autoskillit.*__(run_skill|run_cmd|run_python).*",
scripts=["guards/skill_orchestration_guard.py"],
session_scope="headless_only",
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|mcp__.*autoskillit.*__(run_cmd|run_python)",
scripts=["guards/recipe_read_guard.py"],
session_scope="headless_only",
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"Bash|Agent",
scripts=["guards/background_exec_guard.py"],
session_scope="headless_only",
mechanism="deny",
enforcement_strength={"claude_code": "hard", "codex": "works-as-is"},
),
HookDef(
matcher=r"(mcp__.*autoskillit.*__)?dispatch_food_truck",
Expand All @@ -249,12 +272,14 @@ def __post_init__(self) -> None:
"guards/fleet_claim_guard.py",
],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
event_type="PostToolUse",
matcher="mcp__.*autoskillit.*",
scripts=["formatters/pretty_output_hook.py"],
mechanism="output-rewrite",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
event_type="PostToolUse",
Expand All @@ -266,18 +291,21 @@ def __post_init__(self) -> None:
"recipe_confirmed_post_hook.py",
],
mechanism="output-rewrite",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
event_type="PostToolUse",
matcher=r"mcp__.*autoskillit.*__(run_skill|run_python).*",
scripts=["review_gate_post_hook.py"],
mechanism="output-rewrite",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
event_type="PostToolUse",
matcher=r"(mcp__.*autoskillit.*__)?dispatch_food_truck",
scripts=["resume_gate_post_hook.py"],
mechanism="output-rewrite",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
event_type="PostToolUse",
Expand All @@ -286,36 +314,42 @@ def __post_init__(self) -> None:
session_scope="headless_only",
codex_status="degraded",
mechanism="output-rewrite",
enforcement_strength={"claude_code": "hard", "codex": "degraded"},
),
HookDef(
event_type="PostToolUse",
matcher="Skill",
scripts=["skill_load_post_hook.py"],
codex_status="not-applicable",
mechanism="additionalContext",
enforcement_strength={"claude_code": "hard", "codex": "not-applicable"},
),
HookDef(
matcher=r"Read|Write|Edit|Bash|Grep|Glob",
scripts=["guards/skill_load_guard.py"],
session_scope="headless_only",
codex_status="works-as-is",
mechanism="deny",
enforcement_strength={"claude_code": "hard", "codex": "works-as-is"},
),
HookDef(
matcher=r"mcp__.*autoskillit.*__(wait_for_ci|enqueue_pr)",
scripts=["guards/review_loop_gate.py"],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
matcher=r"(mcp__.*autoskillit.*__)?reset_dispatch",
scripts=["guards/reset_resume_gate.py"],
mechanism="deny",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
HookDef(
event_type="SessionStart",
scripts=["session_start_hook.py"],
session_scope="interactive_only",
mechanism="additionalContext",
enforcement_strength={"claude_code": "soft", "codex": "works-as-is"},
),
]

Expand Down Expand Up @@ -418,6 +452,7 @@ def _canonical_registry_payload(
[
{
"codex_status": h.codex_status,
"enforcement_strength": dict(sorted(h.enforcement_strength.items())),
"event_type": h.event_type,
"exempt_session_types": sorted(h.exempt_session_types),
"exempt_skills": sorted(h.exempt_skills),
Expand All @@ -432,7 +467,7 @@ def _canonical_registry_payload(
key=lambda row: (row["event_type"], row["matcher"], tuple(row["scripts"])), # type: ignore[arg-type]
)
return json.dumps(
{"format_version": 2, "registry": registry_rows, "retired": sorted(retired)},
{"format_version": 3, "registry": registry_rows, "retired": sorted(retired)},
sort_keys=True,
separators=(",", ":"),
)
Expand Down
2 changes: 1 addition & 1 deletion src/autoskillit/hooks/registry.sha256
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e2026897c0e2c46ff1b6beb448cade141294f1c7b296dad2b4cf46e61d8c646e
cf12c32c7990708aedb676d6fcf239fb2b7c1a90d61444d5cb367daf0cfa7795
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"_registry_hash": "e2026897c0e2c46ff1b6beb448cade141294f1c7b296dad2b4cf46e61d8c646e",
"_staleness_note": "Regenerate in the same commit as P5-A6 (B4-hookdef-extension) when mechanism/enforcement_strength fields are added to HookDef.",
"_registry_hash": "cf12c32c7990708aedb676d6fcf239fb2b7c1a90d61444d5cb367daf0cfa7795",
"hooks": {
"PostToolUse": [
{
Expand Down
72 changes: 71 additions & 1 deletion tests/hooks/test_hook_registry_codex_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
import pytest

from autoskillit.execution.backends._codex_hooks import generate_codex_hooks_config
from autoskillit.hook_registry import HookDef
from autoskillit.hook_registry import (
HOOK_REGISTRY_HASH,
RETIRED_SCRIPT_BASENAMES,
HookDef,
_canonical_registry_payload,
compute_registry_hash,
)
from autoskillit.hooks import HOOK_REGISTRY

pytestmark = [pytest.mark.layer("hooks"), pytest.mark.medium]
Expand Down Expand Up @@ -81,3 +87,67 @@ def test_posttooluse_pretty_output_is_output_rewrite(self):
f"Hook {hook_def.scripts} has mechanism={hook_def.mechanism!r}, "
"expected 'output-rewrite' for pretty_output_hook"
)

def test_hookdef_has_enforcement_strength_field(self):
field_names = {f.name for f in dataclasses.fields(HookDef)}
assert "enforcement_strength" in field_names

def test_enforcement_strength_default_is_empty_dict(self):
hd = HookDef(matcher="test", scripts=["s.py"])
assert hd.enforcement_strength == {}

def test_all_registry_entries_have_enforcement_strength_keys(self):
for hd in HOOK_REGISTRY:
assert "claude_code" in hd.enforcement_strength, (
f"Hook {hd.scripts} missing 'claude_code' key"
)
assert "codex" in hd.enforcement_strength, f"Hook {hd.scripts} missing 'codex' key"

def test_enforcement_strength_claude_code_values_are_valid(self):
for hd in HOOK_REGISTRY:
assert hd.enforcement_strength["claude_code"] in ("hard", "soft"), (
f"Hook {hd.scripts} has invalid claude_code: "
f"{hd.enforcement_strength['claude_code']!r}"
)

def test_enforcement_strength_codex_matches_codex_status(self):
for hd in HOOK_REGISTRY:
assert hd.enforcement_strength["codex"] == hd.codex_status, (
f"Hook {hd.scripts}: enforcement_strength codex="
f"{hd.enforcement_strength['codex']!r} != "
f"codex_status={hd.codex_status!r}"
)

def test_compute_registry_hash_changes_on_enforcement_strength(self):
original = HOOK_REGISTRY[0]
mutated_entry = dataclasses.replace(
original,
enforcement_strength={"claude_code": "hard", "codex": "test-sentinel"},
)
mutated_registry = [mutated_entry, *HOOK_REGISTRY[1:]]
new_hash = compute_registry_hash(mutated_registry, RETIRED_SCRIPT_BASENAMES)
assert new_hash != HOOK_REGISTRY_HASH

def test_format_version_is_3(self):
import json

payload = json.loads(_canonical_registry_payload(HOOK_REGISTRY, RETIRED_SCRIPT_BASENAMES))
assert payload["format_version"] == 3

def test_enforcement_strength_round_trip(self):
es = {"claude_code": "hard", "codex": "works-as-is"}
hd = HookDef(matcher="test", scripts=["s.py"], enforcement_strength=es)
assert hd.enforcement_strength == es
assert hd.enforcement_strength["claude_code"] == "hard"
assert hd.enforcement_strength["codex"] == "works-as-is"

def test_canonical_registry_payload_includes_enforcement_strength(self):
import json

payload_str = _canonical_registry_payload(HOOK_REGISTRY, RETIRED_SCRIPT_BASENAMES)
payload = json.loads(payload_str)
for row in payload["registry"]:
assert "enforcement_strength" in row, (
f"Registry row for matcher={row.get('matcher')!r} is missing"
" 'enforcement_strength'. Add it to _canonical_registry_payload."
)
Loading