hermes(agent): validate Codex owner runtime
All checks were successful
Tests / Declarative: Post Actions passed: 228

This commit is contained in:
jenkins 2026-08-10 22:53:59 -03:00
parent 64c58a1376
commit 137d6a54ef
4 changed files with 110 additions and 6 deletions

View File

@ -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

View File

@ -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__":

View File

@ -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

View File

@ -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):