fix: make Codex hooks transactional
This commit is contained in:
1 parent
c3602da5f3
commit
cb6173e6ee
17 files changed
+2308
-870
No files matched your search
@@ -0,0 +1,193 @@
|
||||
"""Codex hook source and managed runtime I/O."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import stat
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from ._managed_files import (
|
||||
MAX_CONFIG_BYTES,
|
||||
MAX_SOURCE_BYTES,
|
||||
physical_directory_exists,
|
||||
physical_file_exists,
|
||||
read_physical_file,
|
||||
)
|
||||
from ._strict_json import JsonObject, parse_json_object
|
||||
from ._transaction import FileChange, source_change, write_changes
|
||||
from .codex_contract import (
|
||||
AGENT_KEY,
|
||||
CONFIG_FILE,
|
||||
CodexDocument,
|
||||
RenderedDocument,
|
||||
RuntimeContract,
|
||||
create_document,
|
||||
parse_document,
|
||||
)
|
||||
|
||||
|
||||
def _same_agent_id(value: Any, expected: str) -> bool:
|
||||
return isinstance(value, str) and os.path.normcase(value) == os.path.normcase(
|
||||
expected
|
||||
)
|
||||
|
||||
|
||||
def codex_home_path() -> str:
|
||||
configured = os.environ.get("CODEX_HOME")
|
||||
if configured is None or configured == "":
|
||||
return os.path.join(os.path.expanduser("~"), ".codex")
|
||||
try:
|
||||
metadata = os.stat(configured)
|
||||
except OSError as error:
|
||||
raise OSError(f"Resolve CODEX_HOME at {configured}: {error}") from error
|
||||
if not stat.S_ISDIR(metadata.st_mode):
|
||||
raise OSError(f"CODEX_HOME is not a directory: {configured}")
|
||||
try:
|
||||
canonical = os.path.realpath(configured, strict=True)
|
||||
except OSError as error:
|
||||
raise OSError(f"Canonicalize CODEX_HOME at {configured}: {error}") from error
|
||||
if not physical_directory_exists(canonical, "CODEX_HOME"):
|
||||
raise OSError(f"CODEX_HOME is missing: {canonical}")
|
||||
return canonical
|
||||
|
||||
|
||||
def config_path() -> str:
|
||||
return os.path.join(codex_home_path(), CONFIG_FILE)
|
||||
|
||||
|
||||
def read_document() -> CodexDocument:
|
||||
file_path = config_path()
|
||||
snapshot = read_physical_file(file_path, "Codex user hooks", MAX_SOURCE_BYTES)
|
||||
if snapshot is None:
|
||||
return create_document(file_path)
|
||||
return parse_document(file_path, snapshot.contents)
|
||||
|
||||
|
||||
def validate_hooks_directory(file_path: str) -> None:
|
||||
directory = os.path.dirname(file_path)
|
||||
physical_directory_exists(directory, "Codex hooks directory")
|
||||
|
||||
|
||||
def rendered_change(rendered: RenderedDocument) -> Optional[FileChange]:
|
||||
if not rendered.changed:
|
||||
return None
|
||||
return source_change(
|
||||
rendered.document.file_path,
|
||||
"Codex user hooks",
|
||||
rendered.document.raw,
|
||||
rendered.next_source,
|
||||
0o600,
|
||||
MAX_SOURCE_BYTES,
|
||||
)
|
||||
|
||||
|
||||
def runtime_change(
|
||||
file_path: str,
|
||||
label: str,
|
||||
next_source: str,
|
||||
mode: int,
|
||||
maximum_bytes: int = MAX_SOURCE_BYTES,
|
||||
) -> Optional[FileChange]:
|
||||
snapshot = read_physical_file(file_path, label, maximum_bytes)
|
||||
return source_change(
|
||||
file_path,
|
||||
label,
|
||||
None if snapshot is None else snapshot.contents,
|
||||
next_source,
|
||||
mode,
|
||||
maximum_bytes,
|
||||
)
|
||||
|
||||
|
||||
def _read_runtime_config(file_path: str) -> Optional[JsonObject]:
|
||||
snapshot = read_physical_file(
|
||||
file_path,
|
||||
"Elydora runtime config",
|
||||
MAX_CONFIG_BYTES,
|
||||
)
|
||||
if snapshot is None:
|
||||
return None
|
||||
return parse_json_object(
|
||||
snapshot.contents,
|
||||
f"Elydora runtime config at {file_path}",
|
||||
)
|
||||
|
||||
|
||||
def validate_runtime_identity(agent_directory: str, agent_id: str) -> bool:
|
||||
config_path_value = os.path.join(agent_directory, "config.json")
|
||||
config = _read_runtime_config(config_path_value)
|
||||
artifacts = [
|
||||
("private.key", "Elydora private key"),
|
||||
("guard.py", "Elydora guard runtime"),
|
||||
("hook.py", "Elydora audit runtime"),
|
||||
("chain-state.json", "Elydora chain state"),
|
||||
("status-cache.json", "Elydora status cache"),
|
||||
("error.log", "Elydora error log"),
|
||||
]
|
||||
artifact_states = [
|
||||
physical_file_exists(os.path.join(agent_directory, name), label)
|
||||
for name, label in artifacts
|
||||
]
|
||||
artifact_exists = any(artifact_states)
|
||||
if config is None:
|
||||
if artifact_exists:
|
||||
raise ValueError(
|
||||
"Elydora runtime identity cannot be verified without config.json: "
|
||||
f"{agent_directory}"
|
||||
)
|
||||
return False
|
||||
if config.get("agent_name") != AGENT_KEY or not _same_agent_id(
|
||||
config.get("agent_id"),
|
||||
agent_id,
|
||||
):
|
||||
raise ValueError(
|
||||
f"Elydora runtime config identity does not match Codex agent {agent_id}: "
|
||||
f"{config_path_value}"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def validate_runtime_tree(
|
||||
runtime_root: str, agent_directory: str, agent_id: str
|
||||
) -> None:
|
||||
if not physical_directory_exists(runtime_root, "Elydora runtime directory"):
|
||||
return
|
||||
if not physical_directory_exists(
|
||||
agent_directory,
|
||||
"Elydora agent runtime directory",
|
||||
):
|
||||
return
|
||||
validate_runtime_identity(agent_directory, agent_id)
|
||||
|
||||
|
||||
def runtime_files_exist(contracts: List[RuntimeContract]) -> bool:
|
||||
for contract in contracts:
|
||||
agent_directory = os.path.dirname(contract.guard_path)
|
||||
if not physical_directory_exists(
|
||||
agent_directory,
|
||||
"Elydora agent runtime directory",
|
||||
):
|
||||
continue
|
||||
config = _read_runtime_config(os.path.join(agent_directory, "config.json"))
|
||||
if (
|
||||
config is None
|
||||
or config.get("agent_name") != AGENT_KEY
|
||||
or not _same_agent_id(config.get("agent_id"), contract.agent_id)
|
||||
):
|
||||
continue
|
||||
if all(
|
||||
(
|
||||
physical_file_exists(contract.guard_path, "Elydora guard runtime"),
|
||||
physical_file_exists(contract.audit_path, "Elydora audit runtime"),
|
||||
physical_file_exists(
|
||||
os.path.join(agent_directory, "private.key"),
|
||||
"Elydora private key",
|
||||
),
|
||||
)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def write_codex_changes(changes: List[FileChange], label: str) -> None:
|
||||
write_changes(changes, label)
|
||||
Reference in new issue
Block a user