fix: make Codex hooks transactional

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 02:41:42 -04:00
1 parent c3602da5f3
commit cb6173e6ee
17 files changed
+2308 -870

No files matched your search

+246 -334
View File
@@ -1,125 +1,31 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from pathlib import Path
import subprocess
from typing import Any
import pytest
from elydora.plugins import codex
from elydora.plugins.base import InstallConfig
from codex_support import (
AGENT_ID,
VALID_PRIVATE_KEY,
assert_native_handler,
legacy_handler,
managed_handler,
prepare_fixture,
symlink_or_skip,
write_json,
write_text,
)
from elydora.plugins.codex_contract import (
AUDIT_STATUS,
GUARD_STATUS,
build_handler,
)
from elydora.plugins.registry import SUPPORTED_AGENTS
AGENT_ID = "agent-1"
MISSING = object()
GUARD_STATUS = "Checking Elydora agent state"
AUDIT_STATUS = "Recording Elydora tool use"
@dataclass(frozen=True)
class CodexFixture:
plugin: codex.CodexPlugin
config: InstallConfig
home_dir: Path
agent_dir: Path
config_path: Path
guard_path: Path
hook_path: Path
runtime_config_path: Path
private_key_path: Path
def write_json_or_text(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
content = value if isinstance(value, str) else json.dumps(value, indent=2)
path.write_text(content, encoding="utf-8")
def prepare_fixture(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
*,
existing_settings: Any = MISSING,
create_guard: bool = True,
) -> CodexFixture:
home_dir = tmp_path / "home with spaces"
elydora_dir = home_dir / ".elydora"
agent_dir = elydora_dir / AGENT_ID
config_path = home_dir / ".codex" / "hooks.json"
agent_dir.mkdir(parents=True)
guard_path = agent_dir / "guard.py"
if create_guard:
guard_path.write_text(
"import sys\nsys.stderr.write('Agent is frozen by Elydora.')\n"
"raise SystemExit(2)\n",
encoding="utf-8",
)
if existing_settings is not MISSING:
write_json_or_text(config_path, existing_settings)
monkeypatch.setattr(codex, "ELYDORA_DIR", str(elydora_dir))
monkeypatch.setattr(codex, "CONFIG_PATH", str(config_path))
config: InstallConfig = {
"agent_id": AGENT_ID,
"agent_name": "codex",
"org_id": "org-1",
"private_key": "test-key",
"kid": "kid-1",
"base_url": "https://api.elydora.test",
"guard_script_path": str(guard_path),
}
return CodexFixture(
plugin=codex.CodexPlugin(),
config=config,
home_dir=home_dir,
agent_dir=agent_dir,
config_path=config_path,
guard_path=guard_path,
hook_path=agent_dir / "hook.py",
runtime_config_path=agent_dir / "config.json",
private_key_path=agent_dir / "private.key",
)
def find_handler(
settings: dict[str, Any],
event: str,
status_message: str,
) -> dict[str, Any]:
for group in settings["hooks"][event]:
for handler in group["hooks"]:
if handler.get("statusMessage") == status_message:
return handler
raise AssertionError(f"handler {status_message!r} not found")
def run_command(
command: str,
home_dir: Path,
payload: dict[str, Any],
) -> subprocess.CompletedProcess[str]:
env = {
**os.environ,
"HOME": str(home_dir),
"USERPROFILE": str(home_dir),
}
return subprocess.run(
command,
shell=True,
capture_output=True,
check=False,
env=env,
input=json.dumps(payload),
text=True,
)
def test_codex_registry_points_at_global_hooks_contract() -> None:
def test_codex_registry_points_at_native_user_hooks() -> None:
assert SUPPORTED_AGENTS["codex"] == {
"name": "OpenAI Codex",
"hook_event": "PreToolUse/PostToolUse",
@@ -127,285 +33,291 @@ def test_codex_registry_points_at_global_hooks_contract() -> None:
}
def test_install_preserves_existing_hooks_and_is_idempotent(
def test_codex_follows_and_canonicalizes_codex_home(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
target = tmp_path / "custom Codex home"
configured = tmp_path / "codex-home-link"
target.mkdir()
try:
configured.symlink_to(target, target_is_directory=True)
except OSError:
configured = target
monkeypatch.setenv("CODEX_HOME", str(configured))
fixture.install()
expected = Path(os.path.realpath(target)) / "hooks.json"
assert expected.is_file()
assert fixture.config_path.exists() is False
status = fixture.plugin.status()
assert status["installed"] is True
assert status["details"] == f"Config: {expected}"
@pytest.mark.parametrize("kind", ["missing", "file"])
def test_codex_rejects_invalid_codex_home_before_writes(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
kind: str,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
configured = tmp_path / f"{kind}-codex-home"
if kind == "file":
write_text(configured, "not a directory")
monkeypatch.setenv("CODEX_HOME", str(configured))
with pytest.raises(OSError, match="CODEX_HOME"):
fixture.install()
assert list(fixture.agent_dir.iterdir()) == []
assert fixture.config_path.exists() is False
def test_install_preserves_sources_migrates_legacy_and_is_idempotent(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
fixture = prepare_fixture(
monkeypatch,
tmp_path,
existing_settings={
"description": "Workspace hooks",
fixture = prepare_fixture(monkeypatch, tmp_path)
write_json(
fixture.config_path,
{
"description": "User hooks",
"metadata": {"owner": "user"},
"hooks": {
"SessionStart": [{
"hooks": [{"type": "command", "command": "existing-command"}],
}],
"SessionStart": [
{"matcher": "*", "hooks": [{"type": "command", "command": "keep"}]}
],
"PreToolUse": [
{
"matcher": "*",
"hooks": [legacy_handler(fixture.guard_path, GUARD_STATUS)],
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [legacy_handler(fixture.hook_path, AUDIT_STATUS)],
}
],
},
},
)
fixture.plugin.install(fixture.config)
fixture.plugin.install(fixture.config)
fixture.install()
first = fixture.config_path.read_text(encoding="utf-8")
fixture.install()
second = fixture.config_path.read_text(encoding="utf-8")
assert "run /hooks to review and trust" in capsys.readouterr().out
settings = json.loads(fixture.config_path.read_text(encoding="utf-8"))
assert settings["description"] == "Workspace hooks"
assert settings["hooks"]["SessionStart"][0]["hooks"][0]["command"] == "existing-command"
assert first == second
assert "approve both Elydora command hooks" in capsys.readouterr().out
settings = fixture.settings()
assert settings["description"] == "User hooks"
assert settings["metadata"] == {"owner": "user"}
assert settings["hooks"]["SessionStart"][0]["hooks"][0]["command"] == "keep"
assert len(settings["hooks"]["PreToolUse"]) == 1
assert len(settings["hooks"]["PostToolUse"]) == 1
assert settings["hooks"]["PreToolUse"][0]["matcher"] == "*"
guard = find_handler(settings, "PreToolUse", GUARD_STATUS)
assert guard["type"] == "command"
assert guard["timeout"] == 10
assert isinstance(guard["command"], str)
assert isinstance(guard["commandWindows"], str)
assert_native_handler(
managed_handler(settings, "PreToolUse", GUARD_STATUS),
GUARD_STATUS,
)
assert_native_handler(
managed_handler(settings, "PostToolUse", AUDIT_STATUS),
AUDIT_STATUS,
)
runtime = json.loads(fixture.runtime_config_path.read_text(encoding="utf-8"))
assert runtime["agent_name"] == "codex"
assert runtime["token"] == "token-1"
assert fixture.private_key_path.read_text(encoding="utf-8") == VALID_PRIVATE_KEY
assert fixture.guard_path.is_file()
assert fixture.hook_path.is_file()
def test_commands_block_frozen_agents_and_forward_official_payload(
@pytest.mark.parametrize(
("source", "error_pattern"),
[
('{"hooks":{},"hooks":{}}', "duplicate field"),
('{"hooks":{},}', "parse Codex user hooks"),
('{"hooks":{/*comment*/}}', "parse Codex user hooks"),
("[]", "must contain a JSON object"),
('{"hooks":null}', 'field "hooks" must be an object'),
('{"hooks":{"PreToolUse":null}}', 'hooks.PreToolUse" must be an array'),
(
'{"hooks":{"PreToolUse":[{"hooks":null}]}}',
"must contain a hooks array",
),
(
'{"hooks":{"PreToolUse":[{"hooks":[null]}]}}',
"must contain a hooks array",
),
],
)
def test_install_preserves_malformed_and_ambiguous_json(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
source: str,
error_pattern: str,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, existing_config=source)
with pytest.raises(ValueError, match=error_pattern):
fixture.install()
assert fixture.config_path.read_text(encoding="utf-8") == source
assert list(fixture.agent_dir.iterdir()) == []
def test_status_requires_exact_pair_identity_and_runtime_secrets(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
capture_path = tmp_path / "captured-event.json"
fixture.hook_path.write_text(
"import pathlib, sys\n"
f"pathlib.Path({str(capture_path)!r}).write_text(sys.stdin.read(), encoding='utf-8')\n",
encoding="utf-8",
)
settings = json.loads(fixture.config_path.read_text(encoding="utf-8"))
command_key = "commandWindows" if os.name == "nt" else "command"
payload = {
"hook_event_name": "PreToolUse",
"session_id": "session-1",
"turn_id": "turn-1",
"transcript_path": None,
"cwd": str(fixture.home_dir),
"model": "gpt-5",
"permission_mode": "default",
"tool_name": "Bash",
"tool_use_id": "call-1",
"tool_input": {"command": "echo test"},
}
fixture.install()
assert fixture.plugin.status()["installed"] is True
guard = find_handler(settings, "PreToolUse", GUARD_STATUS)
guard_result = run_command(guard[command_key], fixture.home_dir, payload)
assert guard_result.returncode == 2
assert "Agent is frozen by Elydora" in guard_result.stderr
fixture.private_key_path.unlink()
status = fixture.plugin.status()
assert status["installed"] is False
assert "runtime files missing" in status["details"]
payload["hook_event_name"] = "PostToolUse"
payload["tool_response"] = {"output": "test"}
audit = find_handler(settings, "PostToolUse", AUDIT_STATUS)
audit_result = run_command(audit[command_key], fixture.home_dir, payload)
assert audit_result.returncode == 0
assert json.loads(capture_path.read_text(encoding="utf-8")) == payload
fixture.install()
settings = fixture.settings()
guard = managed_handler(settings, "PreToolUse", GUARD_STATUS)
guard["extra"] = True
write_json(fixture.config_path, settings)
assert fixture.plugin.status()["installed"] is False
fixture.install()
runtime = json.loads(fixture.runtime_config_path.read_text(encoding="utf-8"))
runtime["agent_id"] = "another-agent"
write_json(fixture.runtime_config_path, runtime)
assert fixture.plugin.status()["installed"] is False
def test_status_requires_both_runtimes_and_uninstall_preserves_other_hooks(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
existing_handler = {"type": "command", "command": "existing-command"}
fixture = prepare_fixture(
monkeypatch,
tmp_path,
existing_settings={
"hooks": {"PreToolUse": [{"hooks": [existing_handler]}]},
},
)
fixture.plugin.install(fixture.config)
assert fixture.plugin.status() == {
"installed": True,
"agent": "codex",
"details": f"Config: {fixture.config_path}",
}
fixture.guard_path.unlink()
assert fixture.plugin.status() == {
"installed": False,
"agent": "codex",
"details": f"Configured at {fixture.config_path}; runtime scripts missing",
}
fixture.plugin.uninstall(AGENT_ID)
settings = json.loads(fixture.config_path.read_text(encoding="utf-8"))
assert settings["hooks"]["PreToolUse"] == [{"hooks": [existing_handler]}]
assert settings["hooks"]["PostToolUse"] == []
def test_install_preserves_user_handlers_with_matching_status_text(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
user_guard = {
"type": "command",
"command": "user-guard",
"statusMessage": GUARD_STATUS,
}
user_audit = {
"type": "command",
"command": "user-audit",
"statusMessage": AUDIT_STATUS,
}
fixture = prepare_fixture(
monkeypatch,
tmp_path,
existing_settings={
"hooks": {
"PreToolUse": [{"matcher": "Bash", "hooks": [user_guard]}],
"PostToolUse": [{"matcher": "Bash", "hooks": [user_audit]}],
},
},
)
fixture.plugin.install(fixture.config)
settings = json.loads(fixture.config_path.read_text(encoding="utf-8"))
assert settings["hooks"]["PreToolUse"][0]["hooks"][0] == user_guard
assert settings["hooks"]["PostToolUse"][0]["hooks"][0] == user_audit
assert len(settings["hooks"]["PreToolUse"]) == 2
assert len(settings["hooks"]["PostToolUse"]) == 2
def test_install_rejects_missing_guard_before_runtime_or_config_writes(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path, create_guard=False)
with pytest.raises(FileNotFoundError, match="guard runtime is missing"):
fixture.plugin.install(fixture.config)
assert fixture.config_path.exists() is False
assert fixture.hook_path.exists() is False
assert fixture.runtime_config_path.exists() is False
assert fixture.private_key_path.exists() is False
def test_status_surfaces_malformed_referenced_runtime_metadata(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
fixture.runtime_config_path.write_text("{ malformed", encoding="utf-8")
write_text(fixture.runtime_config_path, "{ malformed")
with pytest.raises(ValueError, match="parse Elydora runtime config"):
fixture.plugin.status()
def test_status_surfaces_malformed_matcher_groups(
def test_uninstall_removes_exact_ownership_and_preserves_lookalikes(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
settings = json.loads(fixture.config_path.read_text(encoding="utf-8"))
settings["hooks"]["PreToolUse"][0]["hooks"] = None
fixture.config_path.write_text(json.dumps(settings), encoding="utf-8")
fixture.install()
settings = fixture.settings()
agent_ten = fixture.agent_dir.parent / "agent-10"
other_guard = build_handler(str(agent_ten / "guard.py"), GUARD_STATUS)
other_audit = build_handler(str(agent_ten / "hook.py"), AUDIT_STATUS)
lookalike = {
"type": "command",
"command": f"inspect {fixture.guard_path}",
"commandWindows": f"inspect {fixture.guard_path}",
"timeout": 10,
"statusMessage": GUARD_STATUS,
}
managed_guard = dict(managed_handler(settings, "PreToolUse", GUARD_STATUS))
settings["hooks"]["SessionStart"] = [
{"matcher": "*", "hooks": [{"type": "command", "command": "keep"}]}
]
settings["hooks"]["PreToolUse"].extend(
[
{"matcher": "*", "hooks": [other_guard]},
{"matcher": "*", "hooks": [lookalike]},
{"matcher": "Bash", "hooks": [managed_guard]},
]
)
settings["hooks"]["PostToolUse"].append({"matcher": "*", "hooks": [other_audit]})
write_json(fixture.config_path, settings)
with pytest.raises(ValueError, match="matcher group must contain a hooks array"):
fixture.plugin.status()
fixture.plugin.uninstall(AGENT_ID)
remaining = fixture.settings()
assert remaining["hooks"]["SessionStart"][0]["hooks"][0]["command"] == "keep"
assert len(remaining["hooks"]["PreToolUse"]) == 3
assert len(remaining["hooks"]["PostToolUse"]) == 1
assert remaining["hooks"]["PreToolUse"][1]["hooks"][0] == lookalike
assert remaining["hooks"]["PreToolUse"][2] == {"matcher": "Bash", "hooks": []}
def test_uninstall_removes_config_owned_entirely_by_elydora(
def test_uninstall_removes_an_entirely_managed_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
fixture.install()
fixture.plugin.uninstall(AGENT_ID)
fixture.plugin.uninstall(AGENT_ID)
assert fixture.config_path.exists() is False
def test_uninstall_matches_the_exact_agent_runtime_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.plugin.install(fixture.config)
settings = json.loads(fixture.config_path.read_text(encoding="utf-8"))
other_agent_dir = fixture.agent_dir.parent / "agent-10"
for event, status in (
("PreToolUse", GUARD_STATUS),
("PostToolUse", AUDIT_STATUS),
):
handler = find_handler(settings, event, status)
other_handler = {
**handler,
"command": handler["command"].replace(
str(fixture.agent_dir), str(other_agent_dir)
),
"commandWindows": handler["commandWindows"].replace(
str(fixture.agent_dir), str(other_agent_dir)
),
}
settings["hooks"][event].append(
{"matcher": "*", "hooks": [other_handler]}
)
fixture.config_path.write_text(json.dumps(settings), encoding="utf-8")
fixture.plugin.uninstall(AGENT_ID)
remaining = json.loads(fixture.config_path.read_text(encoding="utf-8"))
assert len(remaining["hooks"]["PreToolUse"]) == 1
assert len(remaining["hooks"]["PostToolUse"]) == 1
assert str(other_agent_dir) in find_handler(
remaining, "PreToolUse", GUARD_STATUS
)["command"]
@pytest.mark.parametrize(
("existing_settings", "error_pattern"),
"overrides",
[
({"hooks": None}, 'field "hooks" must be an object'),
({"hooks": {"PreToolUse": None}}, 'field "hooks.PreToolUse" must be an array'),
(
{"hooks": {"PreToolUse": [{"hooks": None}]}},
"matcher group must contain a hooks array",
),
{"agent_name": "cursor"},
{"org_id": ""},
{"agent_id": "../escape"},
{"private_key": "invalid"},
{"token": ""},
{"base_url": "file:///tmp/elydora"},
{"base_url": "https://user:secret@api.elydora.com"},
{"base_url": "https://api.elydora.com?tenant=one"},
{"base_url": "https://api.elydora.com\\evil"},
{"base_url": "https://api.elydora.com/invalid path"},
{"base_url": "https://api.elydora.com:invalid"},
{"guard_script_path": "unmanaged.py"},
],
)
def test_install_preserves_invalid_shapes_before_runtime_writes(
def test_install_validates_identity_credentials_and_api_origin(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
existing_settings: dict[str, Any],
error_pattern: str,
overrides: dict[str, str],
) -> None:
fixture = prepare_fixture(
monkeypatch,
tmp_path,
existing_settings=existing_settings,
)
original = fixture.config_path.read_text(encoding="utf-8")
fixture = prepare_fixture(monkeypatch, tmp_path)
fixture.config.update(overrides)
with pytest.raises(ValueError, match=error_pattern):
fixture.plugin.install(fixture.config)
with pytest.raises(ValueError):
fixture.install()
assert fixture.config_path.read_text(encoding="utf-8") == original
assert fixture.hook_path.exists() is False
assert fixture.runtime_config_path.exists() is False
assert fixture.private_key_path.exists() is False
assert fixture.config_path.exists() is False
assert list(fixture.agent_dir.iterdir()) == []
def test_install_preserves_malformed_config_before_runtime_writes(
@pytest.mark.parametrize("kind", ["hooks", "config", "key", "guard", "audit"])
def test_linked_hook_and_runtime_files_are_rejected(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
kind: str,
) -> None:
fixture = prepare_fixture(
monkeypatch,
tmp_path,
existing_settings="{ malformed",
)
fixture = prepare_fixture(monkeypatch, tmp_path)
if kind == "hooks":
target = tmp_path / "hooks-target.json"
original = '{"hooks":{}}\n'
write_text(target, original)
fixture.config_path.parent.mkdir(parents=True)
symlink_or_skip(target, fixture.config_path)
with pytest.raises(OSError, match="physical file"):
fixture.install()
assert target.read_text(encoding="utf-8") == original
return
with pytest.raises(ValueError, match="parse Codex hooks config"):
fixture.plugin.install(fixture.config)
fixture.install()
file_path = {
"config": fixture.runtime_config_path,
"key": fixture.private_key_path,
"guard": fixture.guard_path,
"audit": fixture.hook_path,
}[kind]
target = tmp_path / f"{kind}-target"
target.write_bytes(file_path.read_bytes())
file_path.unlink()
symlink_or_skip(target, file_path)
assert fixture.config_path.read_text(encoding="utf-8") == "{ malformed"
assert fixture.hook_path.exists() is False
assert fixture.runtime_config_path.exists() is False
assert fixture.private_key_path.exists() is False
with pytest.raises(OSError, match="physical file"):
fixture.plugin.status()
assert target.read_bytes()