fix(codex): allow kanban worker board writes
This commit is contained in:
@@ -74,12 +74,43 @@ class CodexAppServerClient:
|
|||||||
env: Optional[dict[str, str]] = None,
|
env: Optional[dict[str, str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._codex_bin = codex_bin
|
self._codex_bin = codex_bin
|
||||||
cmd = [codex_bin, "app-server"] + list(extra_args or [])
|
|
||||||
spawn_env = os.environ.copy()
|
spawn_env = os.environ.copy()
|
||||||
if env:
|
if env:
|
||||||
spawn_env.update(env)
|
spawn_env.update(env)
|
||||||
if codex_home:
|
if codex_home:
|
||||||
spawn_env["CODEX_HOME"] = codex_home
|
spawn_env["CODEX_HOME"] = codex_home
|
||||||
|
|
||||||
|
app_server_args = list(extra_args or [])
|
||||||
|
# Kanban workers must be able to write their handoff/status back to
|
||||||
|
# the board DB, which lives outside the per-task workspace. Keep the
|
||||||
|
# Codex sandbox on, but add the Kanban root as the only extra writable
|
||||||
|
# root. Without this, codex-runtime workers finish their actual work
|
||||||
|
# but crash/block when kanban_complete/kanban_block writes SQLite.
|
||||||
|
if spawn_env.get("HERMES_KANBAN_TASK"):
|
||||||
|
kanban_db = spawn_env.get("HERMES_KANBAN_DB")
|
||||||
|
kanban_root = (
|
||||||
|
os.path.dirname(kanban_db)
|
||||||
|
if kanban_db
|
||||||
|
else spawn_env.get(
|
||||||
|
"HERMES_KANBAN_ROOT",
|
||||||
|
os.path.join(
|
||||||
|
spawn_env.get("HERMES_HOME", os.path.expanduser("~/.hermes")),
|
||||||
|
"kanban",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app_server_args.extend(
|
||||||
|
[
|
||||||
|
"-c",
|
||||||
|
'sandbox_mode="workspace-write"',
|
||||||
|
"-c",
|
||||||
|
f'sandbox_workspace_write.writable_roots=["{kanban_root}"]',
|
||||||
|
"-c",
|
||||||
|
"sandbox_workspace_write.network_access=false",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = [codex_bin, "app-server"] + app_server_args
|
||||||
# Codex emits tracing to stderr; default WARN keeps it quiet for users.
|
# Codex emits tracing to stderr; default WARN keeps it quiet for users.
|
||||||
spawn_env.setdefault("RUST_LOG", "warn")
|
spawn_env.setdefault("RUST_LOG", "warn")
|
||||||
|
|
||||||
|
|||||||
@@ -241,3 +241,58 @@ class TestSpawnEnvIsolation:
|
|||||||
assert captured["env"].get("CODEX_HOME") == "/tmp/profile/codex"
|
assert captured["env"].get("CODEX_HOME") == "/tmp/profile/codex"
|
||||||
# And HOME still passes through unchanged
|
# And HOME still passes through unchanged
|
||||||
assert captured["env"].get("HOME") == "/users/alice"
|
assert captured["env"].get("HOME") == "/users/alice"
|
||||||
|
|
||||||
|
def test_kanban_worker_adds_only_kanban_writable_root(self, monkeypatch):
|
||||||
|
"""Codex-runtime Kanban workers need to write board state outside
|
||||||
|
their scratch/worktree workspace, but should not fall back to
|
||||||
|
danger-full-access. Hermes passes a narrow app-server config override
|
||||||
|
for the Kanban root only.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
from agent.transports import codex_app_server as cas
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
class FakePopen:
|
||||||
|
def __init__(self, cmd, *args, **kwargs):
|
||||||
|
captured["cmd"] = list(cmd)
|
||||||
|
captured["env"] = kwargs.get("env", {}).copy()
|
||||||
|
self.stdin = None
|
||||||
|
self.stdout = None
|
||||||
|
self.stderr = None
|
||||||
|
self.pid = 1
|
||||||
|
self.returncode = None
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "Popen", FakePopen)
|
||||||
|
monkeypatch.setenv("HOME", "/users/alice")
|
||||||
|
monkeypatch.setenv("HERMES_HOME", "/users/alice/.hermes/profiles/backend-worker")
|
||||||
|
monkeypatch.setenv("HERMES_KANBAN_TASK", "t_smoke")
|
||||||
|
monkeypatch.setenv(
|
||||||
|
"HERMES_KANBAN_DB",
|
||||||
|
"/users/alice/.hermes/kanban/boards/smoke/kanban.db",
|
||||||
|
)
|
||||||
|
|
||||||
|
client = cas.CodexAppServerClient(codex_bin="codex")
|
||||||
|
client._closed = True
|
||||||
|
|
||||||
|
cmd = captured["cmd"]
|
||||||
|
assert cmd[:2] == ["codex", "app-server"]
|
||||||
|
assert 'sandbox_mode="workspace-write"' in cmd
|
||||||
|
assert (
|
||||||
|
'sandbox_workspace_write.writable_roots=["/users/alice/.hermes/kanban/boards/smoke"]'
|
||||||
|
in cmd
|
||||||
|
)
|
||||||
|
assert "sandbox_workspace_write.network_access=false" in cmd
|
||||||
|
assert all("danger" not in part for part in cmd)
|
||||||
|
|||||||
@@ -91,11 +91,11 @@ What works inside a codex-runtime worker:
|
|||||||
- The Hermes tool callback for browser_*, vision, image_gen, skills, TTS
|
- The Hermes tool callback for browser_*, vision, image_gen, skills, TTS
|
||||||
|
|
||||||
What also works because the MCP callback exposes them:
|
What also works because the MCP callback exposes them:
|
||||||
- **`kanban_complete` / `kanban_block` / `kanban_comment` / `kanban_heartbeat`** — the worker handoff tools. These read `HERMES_KANBAN_TASK` from env (set by the dispatcher), gate access correctly, and write to `~/.hermes/kanban.db`. Without these in the callback, a worker on this runtime could do its task but couldn't report back, hanging until the dispatcher's timeout.
|
- **`kanban_complete` / `kanban_block` / `kanban_comment` / `kanban_heartbeat`** — the worker handoff tools. These read `HERMES_KANBAN_TASK` from env (set by the dispatcher), gate access correctly, and write to the per-board SQLite DB pinned by `HERMES_KANBAN_DB`. Without these in the callback, a worker on this runtime could do its task but couldn't report back, hanging until the dispatcher's timeout.
|
||||||
- **`kanban_show` / `kanban_list`** — read-only board queries for the worker to check its own context.
|
- **`kanban_show` / `kanban_list`** — read-only board queries for the worker to check its own context.
|
||||||
- **`kanban_create` / `kanban_unblock` / `kanban_link`** — orchestrator-only operations. Available for orchestrator agents running on the codex runtime that need to dispatch new tasks.
|
- **`kanban_create` / `kanban_unblock` / `kanban_link`** — orchestrator-only operations. Available for orchestrator agents running on the codex runtime that need to dispatch new tasks.
|
||||||
|
|
||||||
The kanban tools are gated by `HERMES_KANBAN_TASK` env var the dispatcher sets — that var is propagated to the codex subprocess (codex inherits env) and from there to the spawned `hermes-tools` MCP server subprocess. So the tools see the right task id and gate correctly.
|
The kanban tools are gated by `HERMES_KANBAN_TASK` env var the dispatcher sets — that var is propagated to the codex subprocess (codex inherits env) and from there to the spawned `hermes-tools` MCP server subprocess. So the tools see the right task id and gate correctly. For Codex app-server workers, Hermes also passes narrow app-server sandbox overrides when `HERMES_KANBAN_TASK` is present: keep `workspace-write` sandboxing, add only the current board directory (derived from `HERMES_KANBAN_DB`) as an extra writable root, and keep network disabled by default. This avoids the brittle `:danger-no-sandbox` workaround while letting `kanban_complete` / `kanban_block` update the board DB.
|
||||||
|
|
||||||
### Cron jobs
|
### Cron jobs
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user