diff --git a/chv-guest/chv-cell.py b/chv-guest/chv-cell.py index a0ef7c2..1e222c8 100644 --- a/chv-guest/chv-cell.py +++ b/chv-guest/chv-cell.py @@ -11,7 +11,9 @@ systemd),单元即沙箱——单元内 codex 用 danger-full-access,不 --private-users=1000:65536 —— 单元内 uid 0 → 客机侧 chv(uid 1000)。 效果:workspace / ~/.codex(chv 所有)单元内仍可写;host-root 0600 文件(如 /etc/chv/guest-env)在单元内显示为 overflow uid、不可读; - 再加 --inaccessible 遮蔽作纵深防御。 + 再加 --inaccessible 遮蔽作纵深防御(只遮 /usr 下路径:--volatile=yes + 下容器根是基本空的 tmpfs,/etc/chv 等在容器根里根本不存在,按构造 + 即不可达,列出反而让 nspawn 解析失败而起不来,见 INACCESSIBLE_BASE)。 网络:--network-veth。宿主侧 ve-chv-cell(10.200.0.1/24),单元侧 host0(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站 @@ -79,11 +81,15 @@ DROP_CAPABILITIES = [ SYSCALL_FILTER = ("@system-service ~io_uring_setup " "~io_uring_enter ~io_uring_register") -# 单元内遮掉的路径:凭证与浏览器(单元内跑不起自己的浏览器, -# 只能走 broker 的 chv-browser)。 +# 单元内遮掉的路径:浏览器(单元内跑不起自己的浏览器, +# 只能走 broker 的 chv-browser)。只列 /usr 下的路径—— +# round-11 复测纠正(第二轮):--volatile=yes 下容器根是 +# 基本空的 tmpfs(只挂进只读 /usr),nspawn 按容器根解析 +# --inaccessible 路径,不存在的路径即启动失败;/etc/chv 与 +# /home/chv/.git-credentials 在容器根里根本不存在(无 /home、 +# 无 /etc/chv),凭证文件按构造就不可达,不再列出遮蔽。 +# 旧的两项若列回,cell 会报 Failed to resolve 而起不来。 INACCESSIBLE_BASE = [ - "/etc/chv", - "/home/chv/.git-credentials", "/usr/local/share/ms-playwright", ] diff --git a/cloud/tests/test_chv_muse_p2.py b/cloud/tests/test_chv_muse_p2.py index 5c2cd60..55e1199 100644 --- a/cloud/tests/test_chv_muse_p2.py +++ b/cloud/tests/test_chv_muse_p2.py @@ -78,10 +78,17 @@ class CellArgvTest(unittest.TestCase): playwright_package_dir="/usr/local/lib/python3.12/dist-packages/playwright") hidden = [a.split("=", 1)[1] for a in argv if a.startswith("--inaccessible=")] - for path in ("/etc/chv", "/home/chv/.git-credentials", - "/usr/local/share/ms-playwright", + # /usr 下的浏览器路径仍遮蔽(/usr 只读挂进单元,遮蔽有效)。 + for path in ("/usr/local/share/ms-playwright", "/usr/local/lib/python3.12/dist-packages/playwright"): self.assertIn(path, hidden) + # round-11 复测纠正(第二轮):/etc/chv 与 + # /home/chv/.git-credentials 不再列出——--volatile=yes 下 + # 容器根是基本空的 tmpfs,这两处在容器根里根本不存在, + # 凭证文件按构造即不可达;列出反而让 nspawn 解析失败 + #(Failed to resolve)而起不来。 + for path in ("/etc/chv", "/home/chv/.git-credentials"): + self.assertNotIn(path, hidden) def test_writable_binds_and_tmpfs(self): argv = CELL.build_nspawn_argv(["true"]) @@ -348,26 +355,27 @@ class BrowserTest(unittest.TestCase): ok, _ = BROWSER.validate_url(bad) self.assertFalse(ok, bad) - def test_truncate_a11y_small_tree_untouched(self): - tree = {"role": "WebArea", "name": "Example", - "children": [{"role": "heading", "name": "Hi"}]} - out, truncated = BROWSER.truncate_a11y(tree) + def test_truncate_aria_small_snapshot_untouched(self): + text = ("- generic [ref=e1]:\n" + " - heading \"Example Domain\" [level=1] [ref=e2]") + out, truncated = BROWSER.truncate_aria(text) self.assertFalse(truncated) - self.assertEqual(out, tree) + self.assertEqual(out, text) - def test_truncate_a11y_node_cap(self): - tree = {"role": "WebArea", - "children": [{"role": "link", "name": "l%d" % i} - for i in range(300)]} - out, truncated = BROWSER.truncate_a11y(tree) + def test_truncate_aria_node_cap(self): + text = "\n".join("- link \"l%d\" [ref=e%d]" % (i, i) + for i in range(300)) + out, truncated = BROWSER.truncate_aria(text) self.assertTrue(truncated) - self.assertLessEqual(len(out["children"]), 200) + nodes = [ln for ln in out.splitlines() + if ln.lstrip().startswith("- ")] + self.assertLessEqual(len(nodes), 200) - def test_truncate_a11y_char_cap(self): - tree = {"role": "WebArea", "name": "x" * 70000, "children": []} - out, truncated = BROWSER.truncate_a11y(tree) + def test_truncate_aria_char_cap(self): + text = "- paragraph \"" + "x" * 70000 + "\" [ref=e1]" + out, truncated = BROWSER.truncate_aria(text) self.assertTrue(truncated) - self.assertTrue(out.get("truncated_chars")) + self.assertLessEqual(len(out), BROWSER.MAX_A11Y_CHARS) def test_forbidden_ops_never_execute(self): agent = mock.Mock() @@ -405,7 +413,11 @@ class BrowserTest(unittest.TestCase): self.assertTrue(resp["ok"]) self.assertEqual(resp["title"], "Example Domain") self.assertEqual(resp["url"], "https://example.com/") - self.assertEqual(resp["a11y"]["role"], "WebArea") + # round-11 复测纠正(第二轮):a11y 是 ARIA 快照字符串 + #(Playwright Page.aria_snapshot()),不是 dict 树。 + self.assertIsInstance(resp["a11y"], str) + self.assertIn("Example Domain", resp["a11y"]) + self.assertFalse(resp["truncated"]) snap = agent.snapshot() self.assertTrue(snap["ok"]) shot = agent.screenshot() @@ -488,13 +500,13 @@ class FakePage: def title(self): return "Example Domain" - @property - def accessibility(self): - return self - - def snapshot(self): - return {"role": "WebArea", "name": "Example Domain", - "children": [{"role": "heading", "name": "Example"}]} + # 真实 Playwright 1.64.1 的快照入口是 Page.aria_snapshot() + #(返回字符串);旧 fake 的 accessibility.snapshot() 是 + # Puppeteer 的形状,真实库里不存在,round-11 第二轮复测 + # 已证实会报 'Page' object has no attribute 'accessibility'。 + def aria_snapshot(self): + return ("- generic [ref=e1]:\n" + " - heading \"Example Domain\" [level=1] [ref=e2]") def screenshot(self, path=None): with open(path, "wb") as fh: