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/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/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/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/agent-inject: "true"
|
||||||
vault.hashicorp.com/role: hermes-agent
|
vault.hashicorp.com/role: hermes-agent
|
||||||
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
|
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 json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -100,19 +103,91 @@ def configure_claude_settings(path: Path) -> None:
|
|||||||
os.chmod(path, 0o600)
|
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."""
|
"""Expose Hermes tools to Codex and keep the owner lane unattended."""
|
||||||
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
||||||
if migrate_fn is None:
|
if migrate_fn is None:
|
||||||
from hermes_cli.codex_runtime_plugin_migration import migrate
|
from hermes_cli.codex_runtime_plugin_migration import migrate
|
||||||
|
|
||||||
migrate_fn = 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(
|
report = migrate_fn(
|
||||||
config,
|
config,
|
||||||
default_permission_profile=":danger-no-sandbox",
|
codex_home=codex_home,
|
||||||
|
default_permission_profile=None,
|
||||||
)
|
)
|
||||||
if report.errors:
|
if report.errors:
|
||||||
raise RuntimeError("; ".join(str(item) for item in report.errors))
|
raise RuntimeError("; ".join(str(item) for item in report.errors))
|
||||||
|
configure_codex_owner_permissions(codex_home / "config.toml")
|
||||||
print(report.summary())
|
print(report.summary())
|
||||||
return report
|
return report
|
||||||
|
|
||||||
@ -124,6 +199,7 @@ def main() -> None:
|
|||||||
configure_claude_settings(home / "settings.json")
|
configure_claude_settings(home / "settings.json")
|
||||||
hermes_home = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
hermes_home = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
||||||
configure_codex_runtime(hermes_home / "config.yaml")
|
configure_codex_runtime(hermes_home / "config.yaml")
|
||||||
|
validate_codex_runtime()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -55,6 +55,8 @@ SESSION_REQUEST_BEFORE = ''' ts = self._client.request(
|
|||||||
SESSION_REQUEST_AFTER = ''' turn_params: dict[str, Any] = {
|
SESSION_REQUEST_AFTER = ''' turn_params: dict[str, Any] = {
|
||||||
"threadId": self._thread_id,
|
"threadId": self._thread_id,
|
||||||
"input": [{"type": "text", "text": user_input_text}],
|
"input": [{"type": "text", "text": user_input_text}],
|
||||||
|
"approvalPolicy": "never",
|
||||||
|
"sandboxPolicy": {"type": "dangerFullAccess"},
|
||||||
}
|
}
|
||||||
if model:
|
if model:
|
||||||
turn_params["model"] = 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()
|
session_content = session_out.read_text()
|
||||||
assert 'turn_params["model"] = model' in session_content
|
assert 'turn_params["model"] = model' in session_content
|
||||||
assert 'turn_params["effort"] = effort' 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 = tmp_path / "codex_runtime.py"
|
||||||
turn.write_text(codex_runtime_patch.TURN_BEFORE, encoding="utf-8")
|
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")
|
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 = tmp_path / "config.yaml"
|
||||||
config.write_text("model: {}\n", encoding="utf-8")
|
config.write_text("model: {}\n", encoding="utf-8")
|
||||||
|
codex_home = tmp_path / ".codex"
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
class Report:
|
class Report:
|
||||||
@ -908,9 +911,32 @@ def test_codex_runtime_migration_uses_owner_unsafe_profile(tmp_path: Path):
|
|||||||
calls.append((value, kwargs))
|
calls.append((value, kwargs))
|
||||||
return Report()
|
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):
|
def test_tui_gateway_patch_extends_and_bounds_agent_startup(tmp_path: Path):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user