hermes(agent): reuse Codex CLI auth for auxiliaries
All checks were successful
Tests / Declarative: Post Actions passed: 233
All checks were successful
Tests / Declarative: Post Actions passed: 233
This commit is contained in:
parent
987c6e4cee
commit
5ecec6c60a
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user