mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(codex): allow kanban worker board writes
This commit is contained in:
parent
ee7cd10281
commit
4a7cd2e16d
3 changed files with 89 additions and 3 deletions
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue