hermes(agent): reuse Codex CLI auth for auxiliaries
All checks were successful
Tests / Declarative: Post Actions passed: 233

This commit is contained in:
jenkins 2026-08-11 00:41:08 -03:00
parent 987c6e4cee
commit 5ecec6c60a
3 changed files with 131 additions and 1 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-cross-provider-fallback"
ai.bstein.dev/config-rev: "20260811-codex-cli-auxiliary"
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
@ -279,6 +279,8 @@ spec:
- /patched/chat_completion_helpers.py
- /opt/hermes/agent/conversation_loop.py
- /patched/conversation_loop.py
- /opt/hermes/agent/auxiliary_client.py
- /patched/auxiliary_client.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
@ -424,6 +426,7 @@ spec:
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/codex_runtime.py, subPath: codex_runtime.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/chat_completion_helpers.py, subPath: chat_completion_helpers.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/conversation_loop.py, subPath: conversation_loop.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.py}
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
@ -574,6 +577,7 @@ spec:
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/codex_runtime.py, subPath: codex_runtime.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/chat_completion_helpers.py, subPath: chat_completion_helpers.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/conversation_loop.py, subPath: conversation_loop.py}
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.py}
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}

View File

@ -223,6 +223,50 @@ FALLBACK_CONTEXT_AFTER = '''def build_cross_provider_codex_prompt(
def run_codex_app_server_turn(
'''
AUXILIARY_TOKEN_BEFORE = ''' except Exception as exc:
logger.debug("Could not read Codex auth for auxiliary client: %s", exc)
return None
'''
AUXILIARY_TOKEN_AFTER = ''' except Exception as exc:
logger.debug("Could not read Hermes Codex auth for auxiliary client: %s", exc)
# Agent Hermes deliberately runs Codex through the authenticated CLI
# app-server and does not duplicate those credentials into Hermes'
# provider auth store. Auxiliary tasks still use Hermes' native Codex
# Responses adapter, so read the current CLI access token without
# copying or refreshing it here. The Codex CLI remains the sole owner
# of refresh-token rotation.
try:
codex_home = os.environ.get("CODEX_HOME", "").strip()
if not codex_home:
codex_home = str(Path.home() / ".codex")
auth_path = Path(codex_home).expanduser() / "auth.json"
payload = json.loads(auth_path.read_text(encoding="utf-8"))
tokens = payload.get("tokens") or {}
access_token = tokens.get("access_token")
if not isinstance(access_token, str) or not access_token.strip():
return None
# Match the native expiry check above. An expired CLI token is not
# refreshed from this side channel because that would race the
# app-server's canonical refresh-token owner.
try:
import base64
jwt_payload = access_token.split(".")[1]
jwt_payload += "=" * (-len(jwt_payload) % 4)
claims = json.loads(base64.urlsafe_b64decode(jwt_payload))
expires_at = claims.get("exp", 0)
if expires_at and time.time() > expires_at:
logger.debug("Codex CLI access token is expired; skipping auxiliary route")
return None
except Exception:
pass
return access_token.strip()
except Exception as cli_exc:
logger.debug("Could not read Codex CLI auth for auxiliary client: %s", cli_exc)
return None
'''
def _replace_once(content: str, before: str, after: str, label: str) -> str:
"""Apply one exact replacement and fail closed when upstream drifts."""
@ -311,6 +355,21 @@ def patch_loop(source: Path, destination: Path) -> None:
)
def patch_auxiliary(source: Path, destination: Path) -> None:
"""Let auxiliary tasks reuse the current Codex CLI access token safely."""
content = source.read_text(encoding="utf-8")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(
_replace_once(
content,
AUXILIARY_TOKEN_BEFORE,
AUXILIARY_TOKEN_AFTER,
"Codex CLI auxiliary auth",
),
encoding="utf-8",
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("provider_source", type=Path)
@ -323,12 +382,15 @@ def main() -> int:
parser.add_argument("fallback_destination", type=Path)
parser.add_argument("loop_source", type=Path)
parser.add_argument("loop_destination", type=Path)
parser.add_argument("auxiliary_source", type=Path)
parser.add_argument("auxiliary_destination", type=Path)
args = parser.parse_args()
patch_provider(args.provider_source, args.provider_destination)
patch_session(args.session_source, args.session_destination)
patch_turn(args.turn_source, args.turn_destination)
patch_fallback(args.fallback_source, args.fallback_destination)
patch_loop(args.loop_source, args.loop_destination)
patch_auxiliary(args.auxiliary_source, args.auxiliary_destination)
return 0

View File

@ -957,6 +957,70 @@ def test_codex_runtime_patch_uses_cli_and_forwards_route(tmp_path: Path):
assert 'if agent.api_mode == "codex_app_server"' in loop_content
assert "build_cross_provider_codex_prompt" in loop_content
auxiliary = tmp_path / "auxiliary_client.py"
auxiliary.write_text(
codex_runtime_patch.AUXILIARY_TOKEN_BEFORE,
encoding="utf-8",
)
auxiliary_out = tmp_path / "patched/auxiliary_client.py"
codex_runtime_patch.patch_auxiliary(auxiliary, auxiliary_out)
auxiliary_content = auxiliary_out.read_text()
assert 'os.environ.get("CODEX_HOME"' in auxiliary_content
assert 'Path(codex_home).expanduser() / "auth.json"' in auxiliary_content
assert "sole owner" in auxiliary_content
def test_agent_mounts_codex_auxiliary_runtime_patch():
deployment = _agent_deployment()
pod = deployment["spec"]["template"]["spec"]
patch_init = next(
item for item in pod["initContainers"]
if item["name"] == "patch-codex-runtime"
)
assert patch_init["command"][-2:] == [
"/opt/hermes/agent/auxiliary_client.py",
"/patched/auxiliary_client.py",
]
expected_mount = {
"name": "codex-runtime-patch",
"mountPath": "/opt/hermes/agent/auxiliary_client.py",
"subPath": "auxiliary_client.py",
}
containers = {item["name"]: item for item in pod["containers"]}
for name in ("hermes", "terminal"):
assert expected_mount in containers[name]["volumeMounts"]
def test_codex_auxiliary_patch_reads_cli_token_without_copying_it(
tmp_path: Path,
monkeypatch,
):
codex_home = tmp_path / ".codex"
codex_home.mkdir()
(codex_home / "auth.json").write_text(
json.dumps({"tokens": {"access_token": "cli-access-token"}}),
encoding="utf-8",
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
source = tmp_path / "auxiliary_client.py"
source.write_text(
"import json, logging, os, time\n"
"from pathlib import Path\n"
"logger = logging.getLogger(__name__)\n"
"def read_token():\n"
" try:\n"
" raise RuntimeError('Hermes provider store intentionally empty')\n"
+ codex_runtime_patch.AUXILIARY_TOKEN_BEFORE,
encoding="utf-8",
)
destination = tmp_path / "patched/auxiliary_client.py"
codex_runtime_patch.patch_auxiliary(source, destination)
namespace: dict = {}
exec(compile(destination.read_text(), str(destination), "exec"), namespace)
assert namespace["read_token"]() == "cli-access-token"
def test_codex_runtime_patch_fails_closed_on_upstream_drift(tmp_path: Path):
source = tmp_path / "runtime_provider.py"