hermes(agent): validate Codex owner runtime
All checks were successful
Tests / Declarative: Post Actions passed: 228
All checks were successful
Tests / Declarative: Post Actions passed: 228
This commit is contained in:
parent
64c58a1376
commit
137d6a54ef
@ -24,7 +24,7 @@ spec:
|
||||
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
|
||||
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
|
||||
ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available
|
||||
ai.bstein.dev/config-rev: "20260811-codex-app-server-routing"
|
||||
ai.bstein.dev/config-rev: "20260811-codex-app-server-permissions"
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
vault.hashicorp.com/role: hermes-agent
|
||||
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
|
||||
|
||||
@ -5,6 +5,9 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@ -100,19 +103,91 @@ def configure_claude_settings(path: Path) -> None:
|
||||
os.chmod(path, 0o600)
|
||||
|
||||
|
||||
def configure_codex_runtime(config_path: Path, migrate_fn=None) -> Any:
|
||||
OWNER_PERMISSIONS_BEGIN = "# begin hermes-agent owner permissions"
|
||||
OWNER_PERMISSIONS_END = "# end hermes-agent owner permissions"
|
||||
|
||||
|
||||
def configure_codex_owner_permissions(path: Path) -> None:
|
||||
"""Use the stable Codex 0.147 approval and sandbox configuration keys."""
|
||||
content = path.read_text(encoding="utf-8") if path.is_file() else ""
|
||||
content = re.sub(
|
||||
rf"(?ms)^{re.escape(OWNER_PERMISSIONS_BEGIN)}\n.*?^{re.escape(OWNER_PERMISSIONS_END)}\n?",
|
||||
"",
|
||||
content,
|
||||
)
|
||||
|
||||
# Remove stale root-level values from the pre-0.147 Hermes migration.
|
||||
# Stop at the first TOML table so identically named nested keys remain intact.
|
||||
lines = content.splitlines()
|
||||
first_table = next(
|
||||
(index for index, line in enumerate(lines) if line.lstrip().startswith("[")),
|
||||
len(lines),
|
||||
)
|
||||
root = [
|
||||
line
|
||||
for line in lines[:first_table]
|
||||
if not re.match(
|
||||
r"^\s*(default_permissions|approval_policy|sandbox_mode)\s*=",
|
||||
line,
|
||||
)
|
||||
]
|
||||
remainder = lines[first_table:]
|
||||
owner_block = [
|
||||
OWNER_PERMISSIONS_BEGIN,
|
||||
'approval_policy = "never"',
|
||||
'sandbox_mode = "danger-full-access"',
|
||||
OWNER_PERMISSIONS_END,
|
||||
"",
|
||||
]
|
||||
rendered = "\n".join([*owner_block, *root, *remainder]).rstrip() + "\n"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(rendered, encoding="utf-8")
|
||||
os.chmod(path, 0o600)
|
||||
|
||||
|
||||
def validate_codex_runtime(codex_bin: str = "codex") -> None:
|
||||
"""Fail pod initialization when Codex cannot load its config or auth."""
|
||||
resolved = shutil.which(codex_bin)
|
||||
if resolved is None:
|
||||
raise RuntimeError(f"Codex executable not found: {codex_bin}")
|
||||
result = subprocess.run(
|
||||
[resolved, "login", "status"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=20,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
detail = (result.stderr or result.stdout or "unknown error").strip()
|
||||
raise RuntimeError(f"Codex runtime validation failed: {detail}")
|
||||
|
||||
|
||||
def configure_codex_runtime(
|
||||
config_path: Path,
|
||||
migrate_fn=None,
|
||||
codex_home: Path | None = None,
|
||||
) -> Any:
|
||||
"""Expose Hermes tools to Codex and keep the owner lane unattended."""
|
||||
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
||||
if migrate_fn is None:
|
||||
from hermes_cli.codex_runtime_plugin_migration import migrate
|
||||
|
||||
migrate_fn = migrate
|
||||
if codex_home is None:
|
||||
codex_home = Path(
|
||||
os.environ.get(
|
||||
"CODEX_HOME",
|
||||
str(Path(os.environ.get("HOME", "/opt/data/home")) / ".codex"),
|
||||
)
|
||||
)
|
||||
report = migrate_fn(
|
||||
config,
|
||||
default_permission_profile=":danger-no-sandbox",
|
||||
codex_home=codex_home,
|
||||
default_permission_profile=None,
|
||||
)
|
||||
if report.errors:
|
||||
raise RuntimeError("; ".join(str(item) for item in report.errors))
|
||||
configure_codex_owner_permissions(codex_home / "config.toml")
|
||||
print(report.summary())
|
||||
return report
|
||||
|
||||
@ -124,6 +199,7 @@ def main() -> None:
|
||||
configure_claude_settings(home / "settings.json")
|
||||
hermes_home = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
||||
configure_codex_runtime(hermes_home / "config.yaml")
|
||||
validate_codex_runtime()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -55,6 +55,8 @@ SESSION_REQUEST_BEFORE = ''' ts = self._client.request(
|
||||
SESSION_REQUEST_AFTER = ''' turn_params: dict[str, Any] = {
|
||||
"threadId": self._thread_id,
|
||||
"input": [{"type": "text", "text": user_input_text}],
|
||||
"approvalPolicy": "never",
|
||||
"sandboxPolicy": {"type": "dangerFullAccess"},
|
||||
}
|
||||
if model:
|
||||
turn_params["model"] = model
|
||||
|
||||
@ -877,6 +877,8 @@ def test_codex_runtime_patch_uses_cli_and_forwards_route(tmp_path: Path):
|
||||
session_content = session_out.read_text()
|
||||
assert 'turn_params["model"] = model' in session_content
|
||||
assert 'turn_params["effort"] = effort' in session_content
|
||||
assert '"approvalPolicy": "never"' in session_content
|
||||
assert '"sandboxPolicy": {"type": "dangerFullAccess"}' in session_content
|
||||
|
||||
turn = tmp_path / "codex_runtime.py"
|
||||
turn.write_text(codex_runtime_patch.TURN_BEFORE, encoding="utf-8")
|
||||
@ -892,9 +894,10 @@ def test_codex_runtime_patch_fails_closed_on_upstream_drift(tmp_path: Path):
|
||||
codex_runtime_patch.patch_provider(source, tmp_path / "patched.py")
|
||||
|
||||
|
||||
def test_codex_runtime_migration_uses_owner_unsafe_profile(tmp_path: Path):
|
||||
def test_codex_runtime_migration_uses_owner_unsafe_mode(tmp_path: Path):
|
||||
config = tmp_path / "config.yaml"
|
||||
config.write_text("model: {}\n", encoding="utf-8")
|
||||
codex_home = tmp_path / ".codex"
|
||||
calls = []
|
||||
|
||||
class Report:
|
||||
@ -908,9 +911,32 @@ def test_codex_runtime_migration_uses_owner_unsafe_profile(tmp_path: Path):
|
||||
calls.append((value, kwargs))
|
||||
return Report()
|
||||
|
||||
client_config.configure_codex_runtime(config, migrate)
|
||||
client_config.configure_codex_runtime(config, migrate, codex_home)
|
||||
|
||||
assert calls[0][1]["default_permission_profile"] == ":danger-no-sandbox"
|
||||
assert calls[0][1]["default_permission_profile"] is None
|
||||
assert calls[0][1]["codex_home"] == codex_home
|
||||
content = (codex_home / "config.toml").read_text(encoding="utf-8")
|
||||
assert 'approval_policy = "never"' in content
|
||||
assert 'sandbox_mode = "danger-full-access"' in content
|
||||
assert "default_permissions" not in content
|
||||
|
||||
|
||||
def test_codex_owner_permissions_replace_stale_profile(tmp_path: Path):
|
||||
config = tmp_path / "config.toml"
|
||||
config.write_text(
|
||||
'default_permissions = ":danger-no-sandbox"\n\n[features]\nhooks = true\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
client_config.configure_codex_owner_permissions(config)
|
||||
client_config.configure_codex_owner_permissions(config)
|
||||
|
||||
content = config.read_text(encoding="utf-8")
|
||||
assert content.count(client_config.OWNER_PERMISSIONS_BEGIN) == 1
|
||||
assert content.count('approval_policy = "never"') == 1
|
||||
assert content.count('sandbox_mode = "danger-full-access"') == 1
|
||||
assert "default_permissions" not in content
|
||||
assert "[features]\nhooks = true" in content
|
||||
|
||||
|
||||
def test_tui_gateway_patch_extends_and_bounds_agent_startup(tmp_path: Path):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user