194 lines
5.8 KiB
Python
194 lines
5.8 KiB
Python
"""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)
|