hermes(agent): route native Codex through CLI runtime
All checks were successful
Tests / Declarative: Post Actions passed: 227
All checks were successful
Tests / Declarative: Post Actions passed: 227
This commit is contained in:
parent
2f5780ebf9
commit
4ec4976c46
@ -12,6 +12,9 @@ data:
|
|||||||
provider: openai-codex
|
provider: openai-codex
|
||||||
default: gpt-5.6-terra
|
default: gpt-5.6-terra
|
||||||
model: gpt-5.6-terra
|
model: gpt-5.6-terra
|
||||||
|
# Reuse the owner's authenticated Codex CLI instead of maintaining a
|
||||||
|
# second rotating OAuth token in Hermes' provider store.
|
||||||
|
openai_runtime: codex_app_server
|
||||||
|
|
||||||
fallback_providers:
|
fallback_providers:
|
||||||
- provider: anthropic
|
- provider: anthropic
|
||||||
|
|||||||
@ -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-stock-supervisor-probe"
|
ai.bstein.dev/config-rev: "20260811-codex-app-server-routing"
|
||||||
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
|
||||||
@ -241,6 +241,33 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 100m, memory: 128Mi}
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
|
- name: patch-codex-runtime
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /opt/hermes/.venv/bin/python
|
||||||
|
- /opt/coordinator/patch_codex_runtime.py
|
||||||
|
- /opt/hermes/hermes_cli/runtime_provider.py
|
||||||
|
- /patched/runtime_provider.py
|
||||||
|
- /opt/hermes/agent/transports/codex_app_server_session.py
|
||||||
|
- /patched/codex_app_server_session.py
|
||||||
|
- /opt/hermes/agent/codex_runtime.py
|
||||||
|
- /patched/codex_runtime.py
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsUser: 10000
|
||||||
|
runAsGroup: 10000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- name: coordinator
|
||||||
|
mountPath: /opt/coordinator
|
||||||
|
readOnly: true
|
||||||
|
- name: codex-runtime-patch
|
||||||
|
mountPath: /patched
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
- name: bootstrap-coordinator
|
- name: bootstrap-coordinator
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
@ -274,6 +301,9 @@ spec:
|
|||||||
- sh
|
- sh
|
||||||
- -ec
|
- -ec
|
||||||
- |
|
- |
|
||||||
|
set -a
|
||||||
|
. /opt/data/.env
|
||||||
|
set +a
|
||||||
/opt/hermes/.venv/bin/python /opt/coordinator/configure_agent_clients.py
|
/opt/hermes/.venv/bin/python /opt/coordinator/configure_agent_clients.py
|
||||||
/opt/hermes/.venv/bin/python /opt/coordinator/migrate_herdr_state.py
|
/opt/hermes/.venv/bin/python /opt/coordinator/migrate_herdr_state.py
|
||||||
env:
|
env:
|
||||||
@ -352,6 +382,9 @@ spec:
|
|||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: provider-auth, mountPath: /shared-auth}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/hermes_cli/runtime_provider.py, subPath: runtime_provider.py}
|
||||||
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/transports/codex_app_server_session.py, subPath: codex_app_server_session.py}
|
||||||
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/codex_runtime.py, subPath: codex_runtime.py}
|
||||||
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.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: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
|
||||||
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
||||||
@ -497,6 +530,9 @@ spec:
|
|||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: provider-auth, mountPath: /shared-auth}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/hermes_cli/runtime_provider.py, subPath: runtime_provider.py}
|
||||||
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/transports/codex_app_server_session.py, subPath: codex_app_server_session.py}
|
||||||
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/codex_runtime.py, subPath: codex_runtime.py}
|
||||||
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.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: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
|
||||||
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
||||||
@ -615,6 +651,8 @@ spec:
|
|||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: tui-gateway-patch
|
- name: tui-gateway-patch
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: codex-runtime-patch
|
||||||
|
emptyDir: {}
|
||||||
- name: auto-router-plugin
|
- name: auto-router-plugin
|
||||||
configMap:
|
configMap:
|
||||||
name: hermes-auto-router-plugin
|
name: hermes-auto-router-plugin
|
||||||
|
|||||||
@ -58,6 +58,7 @@ configMapGenerator:
|
|||||||
- install_agent_tools.sh=scripts/install_agent_tools.sh
|
- install_agent_tools.sh=scripts/install_agent_tools.sh
|
||||||
- migrate_herdr_state.py=scripts/migrate_herdr_state.py
|
- migrate_herdr_state.py=scripts/migrate_herdr_state.py
|
||||||
- patch_hermes_auth.py=scripts/patch_hermes_auth.py
|
- patch_hermes_auth.py=scripts/patch_hermes_auth.py
|
||||||
|
- patch_codex_runtime.py=scripts/patch_codex_runtime.py
|
||||||
- patch_tui_gateway.py=scripts/patch_tui_gateway.py
|
- patch_tui_gateway.py=scripts/patch_tui_gateway.py
|
||||||
- patch_ttyd_index.py=scripts/patch_ttyd_index.py
|
- patch_ttyd_index.py=scripts/patch_ttyd_index.py
|
||||||
options:
|
options:
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
def configure_claude_state(path: Path) -> None:
|
def configure_claude_state(path: Path) -> None:
|
||||||
"""Disable Claude's long-session resume chooser without losing state."""
|
"""Disable Claude's long-session resume chooser without losing state."""
|
||||||
@ -98,11 +100,30 @@ 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:
|
||||||
|
"""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
|
||||||
|
report = migrate_fn(
|
||||||
|
config,
|
||||||
|
default_permission_profile=":danger-no-sandbox",
|
||||||
|
)
|
||||||
|
if report.errors:
|
||||||
|
raise RuntimeError("; ".join(str(item) for item in report.errors))
|
||||||
|
print(report.summary())
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Configure provider clients under the persistent Hermes home."""
|
"""Configure provider clients under the persistent Hermes home."""
|
||||||
home = Path(os.environ.get("CLAUDE_CONFIG_DIR", "/opt/data/home/.claude"))
|
home = Path(os.environ.get("CLAUDE_CONFIG_DIR", "/opt/data/home/.claude"))
|
||||||
configure_claude_state(home / ".claude.json")
|
configure_claude_state(home / ".claude.json")
|
||||||
configure_claude_settings(home / "settings.json")
|
configure_claude_settings(home / "settings.json")
|
||||||
|
hermes_home = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
||||||
|
configure_codex_runtime(hermes_home / "config.yaml")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -328,6 +328,7 @@ def _profile_config(
|
|||||||
"provider": primary_provider,
|
"provider": primary_provider,
|
||||||
"default": primary_model,
|
"default": primary_model,
|
||||||
"model": primary_model,
|
"model": primary_model,
|
||||||
|
"openai_runtime": "codex_app_server",
|
||||||
}
|
}
|
||||||
config["fallback_providers"] = [
|
config["fallback_providers"] = [
|
||||||
fallback,
|
fallback,
|
||||||
@ -398,6 +399,7 @@ def configure_routes(root: Path, codex: Catalog, claude: Catalog) -> dict[str, A
|
|||||||
"provider": "openai-codex",
|
"provider": "openai-codex",
|
||||||
"default": codex_coordinator,
|
"default": codex_coordinator,
|
||||||
"model": codex_coordinator,
|
"model": codex_coordinator,
|
||||||
|
"openai_runtime": "codex_app_server",
|
||||||
}
|
}
|
||||||
base["fallback_providers"] = [
|
base["fallback_providers"] = [
|
||||||
{"provider": "anthropic", "model": claude_coordinator},
|
{"provider": "anthropic", "model": claude_coordinator},
|
||||||
@ -441,10 +443,7 @@ def configure_routes(root: Path, codex: Catalog, claude: Catalog) -> dict[str, A
|
|||||||
),
|
),
|
||||||
env_values,
|
env_values,
|
||||||
)
|
)
|
||||||
local = [
|
local = ["custom/gpt-oss:20b"]
|
||||||
"custom/qwen2.5:14b-instruct-q4_0",
|
|
||||||
"custom/gpt-oss:20b",
|
|
||||||
]
|
|
||||||
routes[codex_name] = [
|
routes[codex_name] = [
|
||||||
f"openai-codex/{codex_model}",
|
f"openai-codex/{codex_model}",
|
||||||
f"anthropic/{claude_model}",
|
f"anthropic/{claude_model}",
|
||||||
@ -473,7 +472,6 @@ def configure_routes(root: Path, codex: Catalog, claude: Catalog) -> dict[str, A
|
|||||||
routes["synthesis-xhigh"] = [
|
routes["synthesis-xhigh"] = [
|
||||||
f"anthropic/{claude_models['xhigh']}",
|
f"anthropic/{claude_models['xhigh']}",
|
||||||
f"openai-codex/{codex_models['xhigh']}",
|
f"openai-codex/{codex_models['xhigh']}",
|
||||||
"custom/qwen2.5:14b-instruct-q4_0",
|
|
||||||
"custom/gpt-oss:20b",
|
"custom/gpt-oss:20b",
|
||||||
]
|
]
|
||||||
_write_yaml(
|
_write_yaml(
|
||||||
@ -486,7 +484,6 @@ def configure_routes(root: Path, codex: Catalog, claude: Catalog) -> dict[str, A
|
|||||||
routes["coordinator"] = [
|
routes["coordinator"] = [
|
||||||
f"openai-codex/{codex_coordinator}",
|
f"openai-codex/{codex_coordinator}",
|
||||||
f"anthropic/{claude_coordinator}",
|
f"anthropic/{claude_coordinator}",
|
||||||
"custom/qwen2.5:14b-instruct-q4_0",
|
|
||||||
"custom/gpt-oss:20b",
|
"custom/gpt-oss:20b",
|
||||||
]
|
]
|
||||||
return routes
|
return routes
|
||||||
|
|||||||
155
services/hermes/scripts/patch_codex_runtime.py
Normal file
155
services/hermes/scripts/patch_codex_runtime.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Let Hermes use the authenticated Codex app-server with routed settings."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PROVIDER_BEFORE = ''' if provider == "openai-codex":
|
||||||
|
try:
|
||||||
|
creds = resolve_codex_runtime_credentials()
|
||||||
|
'''
|
||||||
|
PROVIDER_AFTER = ''' if provider == "openai-codex":
|
||||||
|
if (
|
||||||
|
str(model_cfg.get("openai_runtime") or "").strip().lower()
|
||||||
|
== "codex_app_server"
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"provider": "openai-codex",
|
||||||
|
"api_mode": "codex_app_server",
|
||||||
|
"base_url": DEFAULT_CODEX_BASE_URL,
|
||||||
|
"api_key": "codex-cli-runtime",
|
||||||
|
"source": "codex-cli",
|
||||||
|
"requested_provider": requested_provider,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
creds = resolve_codex_runtime_credentials()
|
||||||
|
'''
|
||||||
|
|
||||||
|
SESSION_SIGNATURE_BEFORE = ''' def run_turn(
|
||||||
|
self,
|
||||||
|
user_input: Any,
|
||||||
|
*,
|
||||||
|
turn_timeout: float = 600.0,
|
||||||
|
'''
|
||||||
|
SESSION_SIGNATURE_AFTER = ''' def run_turn(
|
||||||
|
self,
|
||||||
|
user_input: Any,
|
||||||
|
*,
|
||||||
|
model: Optional[str] = None,
|
||||||
|
effort: Optional[str] = None,
|
||||||
|
turn_timeout: float = 600.0,
|
||||||
|
'''
|
||||||
|
|
||||||
|
SESSION_REQUEST_BEFORE = ''' ts = self._client.request(
|
||||||
|
"turn/start",
|
||||||
|
{
|
||||||
|
"threadId": self._thread_id,
|
||||||
|
"input": [{"type": "text", "text": user_input_text}],
|
||||||
|
},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
SESSION_REQUEST_AFTER = ''' turn_params: dict[str, Any] = {
|
||||||
|
"threadId": self._thread_id,
|
||||||
|
"input": [{"type": "text", "text": user_input_text}],
|
||||||
|
}
|
||||||
|
if model:
|
||||||
|
turn_params["model"] = model
|
||||||
|
if effort:
|
||||||
|
turn_params["effort"] = effort
|
||||||
|
ts = self._client.request(
|
||||||
|
"turn/start",
|
||||||
|
turn_params,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
|
TURN_BEFORE = ''' try:
|
||||||
|
turn = agent._codex_session.run_turn(user_input=user_message)
|
||||||
|
'''
|
||||||
|
TURN_AFTER = ''' reasoning = getattr(agent, "reasoning_config", None)
|
||||||
|
effort = (
|
||||||
|
str(reasoning.get("effort") or "").strip()
|
||||||
|
if isinstance(reasoning, dict)
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
turn = agent._codex_session.run_turn(
|
||||||
|
user_input=user_message,
|
||||||
|
model=str(getattr(agent, "model", "") or "").strip() or None,
|
||||||
|
effort=effort or None,
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_once(content: str, before: str, after: str, label: str) -> str:
|
||||||
|
"""Apply one exact replacement and fail closed when upstream drifts."""
|
||||||
|
if content.count(before) != 1:
|
||||||
|
raise RuntimeError(f"Hermes {label} patch context changed")
|
||||||
|
return content.replace(before, after, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_provider(source: Path, destination: Path) -> None:
|
||||||
|
"""Allow the explicit Codex app-server runtime without duplicate OAuth."""
|
||||||
|
content = source.read_text(encoding="utf-8")
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination.write_text(
|
||||||
|
_replace_once(
|
||||||
|
content,
|
||||||
|
PROVIDER_BEFORE,
|
||||||
|
PROVIDER_AFTER,
|
||||||
|
"runtime provider",
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_session(source: Path, destination: Path) -> None:
|
||||||
|
"""Pass the router's selected model and effort to each Codex turn."""
|
||||||
|
content = source.read_text(encoding="utf-8")
|
||||||
|
content = _replace_once(
|
||||||
|
content,
|
||||||
|
SESSION_SIGNATURE_BEFORE,
|
||||||
|
SESSION_SIGNATURE_AFTER,
|
||||||
|
"app-server session signature",
|
||||||
|
)
|
||||||
|
content = _replace_once(
|
||||||
|
content,
|
||||||
|
SESSION_REQUEST_BEFORE,
|
||||||
|
SESSION_REQUEST_AFTER,
|
||||||
|
"app-server turn request",
|
||||||
|
)
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_turn(source: Path, destination: Path) -> None:
|
||||||
|
"""Forward live Hermes route metadata into the Codex session adapter."""
|
||||||
|
content = source.read_text(encoding="utf-8")
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination.write_text(
|
||||||
|
_replace_once(content, TURN_BEFORE, TURN_AFTER, "Codex turn"),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("provider_source", type=Path)
|
||||||
|
parser.add_argument("provider_destination", type=Path)
|
||||||
|
parser.add_argument("session_source", type=Path)
|
||||||
|
parser.add_argument("session_destination", type=Path)
|
||||||
|
parser.add_argument("turn_source", type=Path)
|
||||||
|
parser.add_argument("turn_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)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@ -37,6 +37,7 @@ policy = _load("claude_command_policy")
|
|||||||
migration = _load("migrate_herdr_state")
|
migration = _load("migrate_herdr_state")
|
||||||
auth_patch = _load("patch_hermes_auth")
|
auth_patch = _load("patch_hermes_auth")
|
||||||
tui_gateway_patch = _load("patch_tui_gateway")
|
tui_gateway_patch = _load("patch_tui_gateway")
|
||||||
|
codex_runtime_patch = _load("patch_codex_runtime")
|
||||||
ttyd_patch = _load("patch_ttyd_index")
|
ttyd_patch = _load("patch_ttyd_index")
|
||||||
client_config = _load("configure_agent_clients")
|
client_config = _load("configure_agent_clients")
|
||||||
|
|
||||||
@ -858,6 +859,60 @@ def test_auth_patch_fails_closed_on_upstream_drift(tmp_path: Path):
|
|||||||
auth_patch.patch(source, tmp_path / "patched.py")
|
auth_patch.patch(source, tmp_path / "patched.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_codex_runtime_patch_uses_cli_and_forwards_route(tmp_path: Path):
|
||||||
|
provider = tmp_path / "runtime_provider.py"
|
||||||
|
provider.write_text(codex_runtime_patch.PROVIDER_BEFORE, encoding="utf-8")
|
||||||
|
provider_out = tmp_path / "patched/runtime_provider.py"
|
||||||
|
codex_runtime_patch.patch_provider(provider, provider_out)
|
||||||
|
assert '"api_mode": "codex_app_server"' in provider_out.read_text()
|
||||||
|
|
||||||
|
session = tmp_path / "codex_app_server_session.py"
|
||||||
|
session.write_text(
|
||||||
|
codex_runtime_patch.SESSION_SIGNATURE_BEFORE
|
||||||
|
+ codex_runtime_patch.SESSION_REQUEST_BEFORE,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
session_out = tmp_path / "patched/codex_app_server_session.py"
|
||||||
|
codex_runtime_patch.patch_session(session, session_out)
|
||||||
|
session_content = session_out.read_text()
|
||||||
|
assert 'turn_params["model"] = model' in session_content
|
||||||
|
assert 'turn_params["effort"] = effort' in session_content
|
||||||
|
|
||||||
|
turn = tmp_path / "codex_runtime.py"
|
||||||
|
turn.write_text(codex_runtime_patch.TURN_BEFORE, encoding="utf-8")
|
||||||
|
turn_out = tmp_path / "patched/codex_runtime.py"
|
||||||
|
codex_runtime_patch.patch_turn(turn, turn_out)
|
||||||
|
assert "model=str(getattr(agent" in turn_out.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_codex_runtime_patch_fails_closed_on_upstream_drift(tmp_path: Path):
|
||||||
|
source = tmp_path / "runtime_provider.py"
|
||||||
|
source.write_text("def changed():\n pass\n", encoding="utf-8")
|
||||||
|
with pytest.raises(RuntimeError, match="context changed"):
|
||||||
|
codex_runtime_patch.patch_provider(source, tmp_path / "patched.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_codex_runtime_migration_uses_owner_unsafe_profile(tmp_path: Path):
|
||||||
|
config = tmp_path / "config.yaml"
|
||||||
|
config.write_text("model: {}\n", encoding="utf-8")
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class Report:
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def summary():
|
||||||
|
return "configured"
|
||||||
|
|
||||||
|
def migrate(value, **kwargs):
|
||||||
|
calls.append((value, kwargs))
|
||||||
|
return Report()
|
||||||
|
|
||||||
|
client_config.configure_codex_runtime(config, migrate)
|
||||||
|
|
||||||
|
assert calls[0][1]["default_permission_profile"] == ":danger-no-sandbox"
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
source = tmp_path / "server.py"
|
source = tmp_path / "server.py"
|
||||||
destination = tmp_path / "patched/server.py"
|
destination = tmp_path / "patched/server.py"
|
||||||
|
|||||||
@ -92,7 +92,9 @@ def test_configure_routes_builds_cross_provider_fallback_profiles(tmp_path: Path
|
|||||||
(tmp_path / "profiles/claude-high/config.yaml").read_text(encoding="utf-8")
|
(tmp_path / "profiles/claude-high/config.yaml").read_text(encoding="utf-8")
|
||||||
)
|
)
|
||||||
assert root["model"]["model"] == "gpt-5.6-terra"
|
assert root["model"]["model"] == "gpt-5.6-terra"
|
||||||
|
assert root["model"]["openai_runtime"] == "codex_app_server"
|
||||||
assert codex_profile["model"]["model"] == "gpt-5.6-sol"
|
assert codex_profile["model"]["model"] == "gpt-5.6-sol"
|
||||||
|
assert codex_profile["model"]["openai_runtime"] == "codex_app_server"
|
||||||
assert codex_profile["fallback_providers"][0] == {
|
assert codex_profile["fallback_providers"][0] == {
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
"model": "claude-opus-5",
|
"model": "claude-opus-5",
|
||||||
@ -104,6 +106,7 @@ def test_configure_routes_builds_cross_provider_fallback_profiles(tmp_path: Path
|
|||||||
assert codex_profile["fallback_providers"][1] == routing.ATLAS_FALLBACK
|
assert codex_profile["fallback_providers"][1] == routing.ATLAS_FALLBACK
|
||||||
assert len(codex_profile["fallback_providers"]) == 2
|
assert len(codex_profile["fallback_providers"]) == 2
|
||||||
assert routes["coordinator"][0] == "openai-codex/gpt-5.6-terra"
|
assert routes["coordinator"][0] == "openai-codex/gpt-5.6-terra"
|
||||||
|
assert "custom/qwen2.5:14b-instruct-q4_0" not in routes["coordinator"]
|
||||||
assert "max" not in json.dumps(routes)
|
assert "max" not in json.dumps(routes)
|
||||||
|
|
||||||
profile_env = (tmp_path / "profiles/codex-high/.env").read_text(encoding="utf-8")
|
profile_env = (tmp_path / "profiles/codex-high/.env").read_text(encoding="utf-8")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user