{"repo":"NousResearch/hermes-agent","public_id":"26203","display_label":"Sandbox writable-path fixes for Kanban and NixOS","summary":"These issues and pull requests address cases where sandboxed environments deny write access to necessary paths, specifically Kanban workspace and board DB roots in the Codex app-server and working directories in the NixOS systemd service module.","subsystem":"agent/transports","counts":{"members":7,"open_issues":1,"open_issues_distinct":1,"open_prs":1,"closed_issues":1,"merged_prs":2,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":28435,"merged_at":"2026-05-19T04:03:26+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-19T04:03:26+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.25523809523809526},"fix_edges":[{"issue_number":26203,"pr_number":27557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-17T18:50:43+00:00"},{"issue_number":27941,"pr_number":28435,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-19T04:03:26+00:00"}],"members":[{"kind":"issue","number":26203,"title":"Codex app-server Kanban workers cannot complete tasks under sandbox without board DB writable root","url":null,"state":"closed"},{"kind":"issue","number":27941,"title":"[Bug]: codex_app_server Kanban workers cannot write artifacts to HERMES_KANBAN_WORKSPACE when it differs from cwd","url":null,"state":"open"},{"kind":"pr","number":26212,"title":"fix(codex): allow sandboxed Kanban worker handoffs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4a7cd2e16","number":0},{"kind":"pr","value":"#27557","number":27557}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":27557,"title":"fix(codex): scope kanban worker writable root in app-server sandbox (salvage #26212)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/transports/codex_app_server.py","scripts/release.py","tests/agent/transports/test_codex_app_server_runtime.py","website/docs/user-guide/features/codex-app-server-runtime.md"],"fix_hunks":[{"file":"agent/transports/codex_app_server.py","hunks":["@@ -74,12 +74,43 @@ def __init__(\n         env: Optional[dict[str, str]] = None,\n     ) -> None:\n         self._codex_bin = codex_bin\n-        cmd = [codex_bin, \"app-server\"] + list(extra_args or [])\n         spawn_env = os.environ.copy()\n         if env:\n             spawn_env.update(env)\n         if codex_home:\n             spawn_env[\"CODEX_HOME\"] = codex_home\n+\n+        app_server_args = list(extra_args or [])\n+        # Kanban workers must be able to write their handoff/status back to\n+        # the board DB, which lives outside the per-task workspace. Keep the\n+        # Codex sandbox on, but add the Kanban root as the only extra writable\n+        # root. Without this, codex-runtime workers finish their actual work\n+        # but crash/block when kanban_complete/kanban_block writes SQLite.\n+        if spawn_env.get(\"HERMES_KANBAN_TASK\"):\n+            kanban_db = spawn_env.get(\"HERMES_KANBAN_DB\")\n+            kanban_root = (\n+                os.path.dirname(kanban_db)\n+                if kanban_db\n+                else spawn_env.get(\n+                    \"HERMES_KANBAN_ROOT\",\n+                    os.path.join(\n+                        spawn_env.get(\"HERMES_HOME\", os.path.expanduser(\"~/.hermes\")),\n+                        \"kanban\",\n+                    ),\n+                )\n+            )\n+            app_server_args.extend(\n+                [\n+                    \"-c\",\n+                    'sandbox_mode=\"workspace-write\"',\n+                    \"-c\",\n+                    f'sandbox_workspace_write.writable_roots=[\"{kanban_root}\"]',\n+                    \"-c\",\n+                    \"sandbox_workspace_write.network_access=false\",\n+                ]\n+            )\n+\n+        cmd = [codex_bin, \"app-server\"] + app_server_args\n         # Codex emits tracing to stderr; default WARN keeps it quiet for users.\n         spawn_env.setdefault(\"RUST_LOG\", \"warn\")\n "]},{"file":"scripts/release.py","hunks":["@@ -1159,6 +1159,8 @@\n     \"6666242+bird@users.noreply.github.com\": \"bird\",  # PR #25219 (gateway docker exit-75 restart)\n     \"[redacted:email]\": \"davidcampbelldc\",  # PR #26834 (web_server proxy_headers=False)\n     \"165905879+davidcampbelldc@users.noreply.github.com\": \"davidcampbelldc\",\n+    \"[redacted:email]\": \"hehehe0803\",  # PR #26212 salvage (codex kanban writable root)\n+    \"26063003+hehehe0803@users.noreply.github.com\": \"hehehe0803\",\n }\n \n "]},{"file":"tests/agent/transports/test_codex_app_server_runtime.py","hunks":["@@ -241,3 +241,58 @@ def kill(self):\n         assert captured[\"env\"].get(\"CODEX_HOME\") == \"/tmp/profile/codex\"\n         # And HOME still passes through unchanged\n         assert captured[\"env\"].get(\"HOME\") == \"/users/alice\"\n+\n+    def test_kanban_worker_adds_only_kanban_writable_root(self, monkeypatch):\n+        \"\"\"Codex-runtime Kanban workers need to write board state outside\n+        their scratch/worktree workspace, but should not fall back to\n+        danger-full-access. Hermes passes a narrow app-server config override\n+        for the Kanban root only.\n+        \"\"\"\n+        import subprocess\n+        from agent.transports import codex_app_server as cas\n+\n+        captured = {}\n+\n+        class FakePopen:\n+            def __init__(self, cmd, *args, **kwargs):\n+                captured[\"cmd\"] = list(cmd)\n+                captured[\"env\"] = kwargs.get(\"env\", {}).copy()\n+                self.stdin = None\n+                self.stdout = None\n+                self.stderr = None\n+                self.pid = 1\n+                self.returncode = None\n+\n+            def poll(self):\n+                return None\n+\n+            def terminate(self):\n+                pass\n+\n+            def wait(self, timeout=None):\n+                return 0\n+\n+            def kill(self):\n+                pass\n+\n+        monkeypatch.setattr(subprocess, \"Popen\", FakePopen)\n+        monkeypatch.setenv(\"HOME\", \"/users/alice\")\n+        monkeypatch.setenv(\"HERMES_HOME\", \"/users/alice/.hermes/profiles/backend-worker\")\n+        monkeypatch.setenv(\"HERMES_KANBAN_TASK\", \"t_smoke\")\n+        monkeypatch.setenv(\n+            \"HERMES_KANBAN_DB\",\n+            \"/users/alice/.hermes/kanban/boards/smoke/kanban.db\",\n+        )\n+\n+        client = cas.CodexAppServerClient(codex_bin=\"codex\")\n+        client._closed = True\n+\n+        cmd = captured[\"cmd\"]\n+        assert cmd[:2] == [\"codex\", \"app-server\"]\n+        assert 'sandbox_mode=\"workspace-write\"' in cmd\n+        assert (\n+            'sandbox_workspace_write.writable_roots=[\"/users/alice/.hermes/kanban/boards/smoke\"]'\n+            in cmd\n+        )\n+        assert \"sandbox_workspace_write.network_access=false\" in cmd\n+        assert all(\"danger\" not in part for part in cmd)"]},{"file":"website/docs/user-guide/features/codex-app-server-runtime.md","hunks":["@@ -91,11 +91,11 @@ What works inside a codex-runtime worker:\n - The Hermes tool callback for browser_*, vision, image_gen, skills, TTS\n \n What also works because the MCP callback exposes them:\n-- **`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.\n+- **`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.\n - **`kanban_show` / `kanban_list`** — read-only board queries for the worker to check its own context.\n - **`kanban_create` / `kanban_unblock` / `kanban_link`** — orchestrator-only operations. Available for orchestrator agents running on the codex runtime that need to dispatch new tasks.\n \n-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.\n+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.\n \n ### Cron jobs\n \n"]}]},{"kind":"pr","number":28435,"title":"fix(codex_app_server): include every Kanban-pinned path in writable_roots (#27941) (#27950)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["website/docs/user-guide/features/codex-app-server-runtime.md"]},{"kind":"pr","number":34637,"title":"fix(agent): grant Kanban workspace to Codex sandbox","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":41905,"title":"fix(agent): resume codex app-server thread across turns to preserve session context","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"b5fcf635dc22","number":0},{"kind":"commit","value":"85cb1dc74b30","number":0},{"kind":"commit","value":"35cea9b87e48","number":0},{"kind":"pr","value":"#116343","number":116343},{"kind":"pr","value":"#115759","number":115759},{"kind":"pr","value":"#116586","number":116586}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}