Files
codex-app-server/cloud/tests/test_host_sandbox_routing.py
T
host 4a78ca1e95 Fix codex-host profile: capability allow + netlink (bwrap needs both)
Two bwrap failures found by live host tasks, both in the
AppArmor profile (not in codex or the worker):
1. deny capability sys_admin (etc.) breaks bwrap at userns
   creation: codex-host with the deny list failed, the same
   profile with capability, allowed it. codexsvc holds no host
   capabilities, so the allow only covers fake-root caps inside
   bwrap's own user namespace (scoped, harmless to the host);
   network/ptrace/file denies still hold independently.
2. bwrap brings up loopback in its new net namespace via a
   NETLINK_ROUTE socket: without network netlink raw, codex
   workspace-write fails with 'Failed to bind NETLINK_ROUTE
   socket' and no tool runs. And a bare 'deny network raw'
   overrides the netlink allow (deny wins over allow), so the
   raw deny must name inet/inet6 explicitly (verified by an
   isolated netlink bind test under each profile variant).
README correction in the same change: codex workspace-write
does NOT isolate /tmp -- an agent /tmp write lands on host
/tmp (verified). The verified boundary is: own worktree
writable, everything else read-only, /tmp shared.
2026-10-26 08:39:57 +08:00

116 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""宿主级沙箱路由单测:严格 executor 认领 + 配置/profile 文件。
只用 SQLite 测试库与假 runner,不跑真实 codex/Docker。
覆盖:claim_executor_only 开启时 _ready_tasks 只返回
executor == 'local' 的任务(NULL 与 docker 都排除),且
_claim 对 NULL 任务返回 False、对 local 任务返回 True;
host-config-host.json 的 sandbox 确为 workspace-write;
codex-host profile 含 userns 与 mount,且不含 deny mount。
"""
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
try:
from cloud.server import init_db
from cloud.worker import Worker
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.server import init_db
from cloud.worker import Worker
import sqlite3
def _seed(db_path):
init_db(db_path)
conn = sqlite3.connect(db_path)
conn.execute(
"INSERT INTO projects (id, name) VALUES ('p1', 'p1')")
conn.execute(
"INSERT INTO tasks (id, project_id, prompt, status, executor)"
" VALUES ('null-task', 'p1', 'hi', 'queued', NULL)")
conn.execute(
"INSERT INTO tasks (id, project_id, prompt, status, executor)"
" VALUES ('docker-task', 'p1', 'hi', 'queued', 'docker')")
conn.execute(
"INSERT INTO tasks (id, project_id, prompt, status, executor)"
" VALUES ('local-task', 'p1', 'hi', 'queued', 'local')")
conn.commit()
conn.close()
class StrictClaimTestCase(unittest.TestCase):
def test_strict_worker_only_takes_local(self):
tmp = tempfile.TemporaryDirectory()
db_path = os.path.join(tmp.name, "control.db")
_seed(db_path)
worker = Worker(db_path, "/fake/host.py",
env={"XCM_API_KEY": "test-placeholder"},
claim_executor_only=True)
ready = worker._ready_tasks()
self.assertEqual([t["id"] for t in ready], ["local-task"])
# CAS 层同样严格:NULL 任务抢不到,local 任务抢得到。
self.assertFalse(worker._claim("null-task", executor="local"))
self.assertTrue(worker._claim("local-task", executor="local"))
tmp.cleanup()
def test_default_worker_still_takes_null(self):
tmp = tempfile.TemporaryDirectory()
db_path = os.path.join(tmp.name, "control.db")
_seed(db_path)
worker = Worker(db_path, "/fake/host.py",
env={"XCM_API_KEY": "test-placeholder"})
ready = worker._ready_tasks()
ids = [t["id"] for t in ready]
self.assertIn("null-task", ids)
self.assertIn("local-task", ids)
self.assertNotIn("docker-task", ids)
tmp.cleanup()
class HostConfigProfileTestCase(unittest.TestCase):
def test_host_config_uses_workspace_write(self):
repo_root = Path(__file__).resolve().parents[2]
cfg = json.loads((repo_root / "host-config-host.json").read_text())
self.assertEqual(cfg["sandbox"], "workspace-write")
self.assertEqual(cfg["state"], "work/host-state-host.json")
self.assertEqual(cfg["codex_home"],
"/var/lib/codex-app-server/codex-home")
def test_codex_host_profile_allows_bwrap(self):
repo_root = Path(__file__).resolve().parents[2]
text = (repo_root / "deploy" / "apparmor" / "codex-host"
).read_text()
self.assertIn("userns,", text)
# bwrap 需要 mount 与 pivot_root:profile 必须允许,
# 不能像 docker-codex 那样 deny。
self.assertIn("mount,", text)
self.assertIn("pivot_root,", text)
self.assertNotIn("deny mount,", text)
self.assertNotIn("deny umount,", text)
# bwrap 还需要 userns 内的 capability(实测:deny
# capability sys_admin 会让 bwrap 在 userns 创建处失败,
# 而 capability, 允许则成功;codexsvc 本身无宿主
# capability,所以这里允许的只是 userns 内的假 root
# capability,不授予宿主特权)。
self.assertIn("capability,", text)
self.assertNotIn("deny capability sys_admin,", text)
# bwrap 还需要 netlink(在新网络命名空间里拉起 loopback;
# 缺了会报 Failed to bind NETLINK_ROUTE socket,且任何
# 工具都跑不起来)。注意 AppArmor 里 deny 优先于 allow,
# 所以 raw 的 deny 必须写明 inet/inet6,不能写裸的
# deny network raw(那样会连 netlink raw 一起挡掉,
# 已用隔离 netlink bind 实测验证)。
self.assertIn("network netlink raw,", text)
self.assertIn("deny network inet raw,", text)
self.assertNotIn("deny network raw,", text)
if __name__ == "__main__":
unittest.main()