fix(codex): allow kanban worker board writes

This commit is contained in:
Hoang V. Pham 2026-05-15 15:01:27 +07:00 committed by Teknium
parent ee7cd10281
commit 4a7cd2e16d
3 changed files with 89 additions and 3 deletions

View file

@ -74,12 +74,43 @@ class CodexAppServerClient:
env: Optional[dict[str, str]] = None,
) -> None:
self._codex_bin = codex_bin
cmd = [codex_bin, "app-server"] + list(extra_args or [])
spawn_env = os.environ.copy()
if env:
spawn_env.update(env)
if 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.
spawn_env.setdefault("RUST_LOG", "warn")

View file

@ -241,3 +241,58 @@ class TestSpawnEnvIsolation:
assert captured["env"].get("CODEX_HOME") == "/tmp/profile/codex"
# And HOME still passes through unchanged
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)

View file

@ -91,11 +91,11 @@ What works inside a codex-runtime worker:
- The Hermes tool callback for browser_*, vision, image_gen, skills, TTS
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_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