CI / test (push) Canceled after 0s
## Summary Nhánh `feature/perf-ui-logic`: tối ưu hiệu năng/UI, sửa lỗi workspace và điều hướng, và làm cho công tắc **"Block network for agent-run commands"** chặn thật mọi đường ra mạng của app, **trừ nhà cung cấp AI**. **Chặn mạng (b78d483,8c497cf,10b8379)** - Bộ kiểm tra chung `application/network/network_guard.py`, nối vào cấu hình đang chạy ở Composition Root: đổi công tắc trong Settings là có hiệu lực ngay. - Lệnh shell của agent và task script chạy trong **Windows AppContainer không có quyền mạng**: kernel chặn socket, ping, DNS, Invoke-WebRequest… Không cần quyền admin. Không cô lập được thì lệnh bị từ chối, không chạy khi mạng còn mở. macOS dùng `sandbox-exec`, Linux dùng `unshare --net`. - Bật chặn thì: dừng MCP đang chạy, không khởi động server mới, từ chối lời gọi connector; Microsoft 365 (đăng nhập, Graph, đồng bộ cloud, rules, mail), Teams, nút Test REST/Jira/MCP, link đính kèm task, pip tự cài và tài nguyên web trong xem trước HTML đều bị từ chối. - Vẫn dùng được: chat, tải danh sách model, thử model; tool OneDrive đã đồng bộ trên máy. - Công tắc **mặc định tắt** khi mở app lần đầu; nhãn giữ nguyên như cũ. - Xem trước HTML trong tab Folder giờ hiện được ảnh/CSS/JS từ web khi mạng mở (trước đây trang `file://` không tải được). - Sửa lỗi app văng khi chuyển tab Graph → Folder: profile WebEngine của trang xem trước bị huỷ trước trang (`0xc0000409` trong Qt6Core.dll); giờ dùng một profile chung thuộc QApplication. - Không cấp quyền AppContainer kế thừa lên thư mục chứa PySide6 (nếu có, Chromium không nạp được `Qt6WebEngineCore.dll` và tab Graph trắng). - Cột mục lục trong Settings tính độ rộng theo kiểu chữ của mục đang chọn, "Sandbox Security Layer" không còn bị cắt. **Các commit khác trong nhánh** - `b7a41b3` mỗi thư mục làm việc chỉ thuộc về một project · `bbdf146` bật nút Sửa project khi đã có project đang mở - `35f24e0`, `cc8d5c8`, `2e3e719`, `c699beb` canh hàng / khoảng cách thanh điều hướng - `2759ed9` không refresh workspace khi chuyển tab Cowork · `7607f44` checkpoint hiệu năng và UI - `8548c1e` chặn tool mạng của agent · `caf3b74` renderer GraphRAG native trên macOS · `c00b83c` khoảng cách metadata hàng project · `a04f8a9` ẩn picker workspace cloud ## Change Type - [x] Cowork feature - [x] Bug fix - [ ] Core AI contribution - [x] Test / hardening - [x] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? - Mọi đường ra mạng do app tự mở, trừ nhà cung cấp AI (xem Summary). - Test: `tests/test_network_guard_lanes.py` (có bài chạy AppContainer thật trên Windows), `tests/ui/test_html_preview_remote_images.py`. What is intentionally NOT included? - Chặn cả nhà cung cấp AI / chạy model trên máy (Phương án 2). - Terminal người dùng tự gõ trong tab Folder, sinh ảnh, cơ chế tự tin chứng chỉ lạ (`tls_trust`). - Huy hiệu trạng thái "đang chặn" trên thanh trên cùng. ## Validation - [x] Unit tests - [x] Integration tests - [x] Manual verification - [x] Regression check Commands / evidence: - `python -m pytest tests/test_network_guard_lanes.py tests/test_sandbox_block_network.py tests/ui -q` → chỉ còn 1 bài fail, fail cả trên `b7a41b3` (nhãn `ProjectRow` 'Project' chưa dịch, `tests/ui/test_i18n_khong_con_chu_cu.py`). - `python -m pytest tests -q --ignore=tests/ui` → 4 bài fail, cả 4 cũng fail trên `b7a41b3` (`test_canonical_audit_logger`, 2 bài `test_mcp_audit_security`, `test_monitoring_tab_container`). - Chạy cả `tests` trong một lượt thì treo ở các test dựng MainWindow trong `tests/ui`; `b7a41b3` cũng treo đúng chỗ đó. - `check_imports.py` và `check_orphan_modules.py` PASS. `check_loc.py` báo 9 file quá dài, giống hệt trước khi sửa (không file nào do nhánh này làm dài thêm). - Kiểm tra tay trên Windows 11: trong AppContainer, Python báo `WinError 10013`, ping/nslookup/PowerShell/curl đều không ra được mạng; cmd, git, python chạy bình thường. - Kiểm tra tay trên Windows 11: xem trước HTML tải được 4/4 tài nguyên web khi mạng mở, 0/4 khi bật chặn; tab Graph hoạt động; tạo/huỷ trang xem trước nhiều lần không còn cảnh báo profile của Qt. ## Security Impact Permission / credential / network / customer data impact: - Network: khi bật công tắc, chỉ nhà cung cấp AI còn ra mạng; nội dung chat vẫn gửi tới nhà cung cấp AI. - Permission: lần đầu chạy lệnh trong sandbox, app **thêm quyền (ACE) cho SID AppContainer** trên thư mục làm việc (ghi), thư mục cài Python gốc (đọc), gốc venv và `Scripts` (đọc). Không xoá quyền nào. Thư mục chứa PySide6 không bao giờ nhận quyền kế thừa; một quyền kế thừa sai trên venv (từ bản dev trước) được tự gỡ. - Credential: không đổi. Khi chặn, trạng thái đăng nhập M365 được đọc thẳng từ kho token trên máy, không dựng MSAL. ## Compatibility - [x] No breaking change - [ ] Breaking change documented Ghi chú: `block_network` mặc định đổi từ bật sang tắt cho cấu hình mới; máy đã lưu `true` thì giữ nguyên. Khi đang chặn, lệnh dùng công cụ cài trong thư mục người dùng (ngoài Program Files) có thể báo Access denied; thư viện trong venv của app không dùng được trong sandbox. ## Reviewer Notes - `infrastructure/sandbox/appcontainer_process.py` gọi Win32 bằng ctypes (CreateAppContainerProfile, CreateProcessW với SECURITY_CAPABILITIES) và dùng `icacls` để cấp quyền: nên xem kỹ phần cấp quyền. - `tests/conftest.py` thêm fixture autouse gỡ `network_guard` sau mỗi test, vì `build_context()` gắn cổng này ở mức process. - `core/task_executors.py` đang đúng bằng trần LOC nên `_run_script` được tách sang `core/task_script.py`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: minhanhpkpro <minhanhpkpro@gmail.com> Co-authored-by: Duy Le Huu <duylh19@fpt.com> Co-authored-by: thanhnv <thanhnv.ip@gmail.com> Reviewed-on: #13
390 lines
15 KiB
Python
390 lines
15 KiB
Python
"""Công tắc "Chặn mạng" phải chặn mọi đường ra mạng của app, TRỪ nhà cung cấp AI.
|
|
|
|
Bản đồ ``infura/network-map.html`` liệt kê các làn trước đây "không kiểm soát"
|
|
hoặc chỉ chặn một phần: M365, Teams và nút Test, connector/MCP đang chạy,
|
|
task script và link đính kèm task, tự cài thư viện, xem trước HTML, lệnh shell
|
|
chỉ bị proxy giả. Mỗi làn có ít nhất một bài ở đây:
|
|
|
|
* **bật** — làn từ chối TRƯỚC khi chạm mạng (mọi lời gọi HTTP thật đều nổ);
|
|
* **tắt** — đường cũ giữ nguyên, vì chặn một chiều là hỏng tính năng.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import socket
|
|
import sys
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from cowork_local.application.network import network_guard
|
|
|
|
|
|
def _no_network(*args, **kwargs):
|
|
raise AssertionError("đã chạm mạng dù 'Chặn mạng' đang bật")
|
|
|
|
|
|
@pytest.fixture
|
|
def chan_mang(monkeypatch):
|
|
"""Bật công tắc qua cổng chung và làm nổ mọi lời gọi HTTP thật."""
|
|
import requests
|
|
|
|
from cowork_local.core import tls_trust
|
|
|
|
for name in ("request", "get", "post", "put", "patch", "delete"):
|
|
monkeypatch.setattr(requests, name, _no_network)
|
|
monkeypatch.setattr(tls_trust, "request", _no_network)
|
|
monkeypatch.setattr(tls_trust, "request_any_method", _no_network)
|
|
network_guard.bind(lambda: True)
|
|
yield
|
|
network_guard.bind(None)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _go_cong_sau_moi_bai():
|
|
yield
|
|
network_guard.bind(None)
|
|
|
|
|
|
# ---- cổng chung ------------------------------------------------------------
|
|
|
|
def test_chua_noi_day_thi_khong_chan():
|
|
network_guard.bind(None)
|
|
assert network_guard.is_blocked() is False
|
|
network_guard.ensure_allowed("x")
|
|
|
|
|
|
def test_doc_cong_tac_loi_thi_coi_nhu_dang_chan():
|
|
"""Không đọc được công tắc thì đóng cửa, không mở toang."""
|
|
def hong():
|
|
raise KeyError("agent_security")
|
|
|
|
network_guard.bind(hong)
|
|
with pytest.raises(network_guard.NetworkBlockedError):
|
|
network_guard.ensure_allowed("Teams")
|
|
|
|
|
|
def test_cong_doc_cau_hinh_song_khong_chup_lai(tmp_path):
|
|
"""Đổi công tắc trong Settings có hiệu lực ngay, không cần mở lại app."""
|
|
from cowork_local.presentation.shell.bootstrap import build_context
|
|
|
|
ctx = build_context(tmp_path / "config.json")
|
|
ctx.config.agent_security["block_network"] = True
|
|
assert network_guard.is_blocked() is True
|
|
ctx.config.agent_security["block_network"] = False
|
|
assert network_guard.is_blocked() is False
|
|
|
|
|
|
# ---- Microsoft 365 -----------------------------------------------------------
|
|
|
|
def test_graph_tu_choi_bang_loi_ma_noi_goi_da_bat(chan_mang):
|
|
from cowork_local.core import ms365_graph
|
|
|
|
with pytest.raises(ms365_graph.Ms365GraphError, match="Sandbox Security Layer"):
|
|
ms365_graph.list_onedrive_files("token")
|
|
|
|
|
|
def test_dang_nhap_va_lay_token_tu_choi(chan_mang, monkeypatch):
|
|
from cowork_local.core import ms365_auth
|
|
|
|
monkeypatch.setattr(ms365_auth, "_app", _no_network)
|
|
with pytest.raises(ms365_auth.Ms365AuthError, match="blocked"):
|
|
ms365_auth.get_access_token("", "")
|
|
with pytest.raises(ms365_auth.Ms365AuthError, match="blocked"):
|
|
ms365_auth.sign_in_device_code("", "", lambda flow: None)
|
|
|
|
|
|
def test_kiem_tra_da_dang_nhap_khong_dung_msal_khi_chan(chan_mang, monkeypatch):
|
|
"""Dựng app MSAL là tải cấu hình OpenID của tenant — phải đọc kho token trực tiếp."""
|
|
from cowork_local.core import ms365_auth
|
|
|
|
monkeypatch.setattr(ms365_auth, "_app", _no_network)
|
|
monkeypatch.setattr(ms365_auth, "_cached_account_offline",
|
|
lambda: {"username": "a@b.c"})
|
|
assert ms365_auth.signed_in_account("", "") == {"username": "a@b.c"}
|
|
|
|
|
|
def test_mail_canh_bao_khong_gui_khi_chan(chan_mang, monkeypatch):
|
|
from types import SimpleNamespace
|
|
|
|
from cowork_local.core import agent_security_alert, ms365_auth
|
|
from cowork_local.core.agent_security_types import SecurityVerdict
|
|
|
|
monkeypatch.setattr(ms365_auth, "_app", _no_network)
|
|
config = SimpleNamespace(data={"agent_security": {"admin_email": "admin@x.y"}}, ms365={})
|
|
verdict = SecurityVerdict(allowed=False, layer="command", reason="test")
|
|
sent, note = agent_security_alert.notify_admin(config, verdict)
|
|
assert sent is False
|
|
assert "blocked" in note
|
|
|
|
|
|
# ---- Teams, connector REST, Jira, nút Test ---------------------------------
|
|
|
|
def test_teams_tu_choi(chan_mang):
|
|
from cowork_local.core.teams import TeamsNotifier
|
|
|
|
ok, detail = TeamsNotifier("https://x.webhook.office.com/hook").send("t", "b")
|
|
assert ok is False
|
|
assert "blocked" in detail
|
|
|
|
|
|
def test_connector_rest_va_nut_test_tu_choi(chan_mang):
|
|
from cowork_local.core.ext_connectors import RestApiConnector
|
|
|
|
rc = RestApiConnector({"id": "erp", "name": "ERP", "base_url": "https://erp.example"})
|
|
assert rc.call({"method": "GET", "path": "items"})["ok"] is False
|
|
ok, message = rc.test_connection()
|
|
assert ok is False and "blocked" in message
|
|
|
|
|
|
def test_nut_test_jira_tu_choi(chan_mang):
|
|
from cowork_local.core import jira_tool
|
|
|
|
cfg = {"base_url": "https://jira.example", "email": "a@b.c", "api_token": "t"}
|
|
out = jira_tool.search(cfg, "order by created DESC", 1)
|
|
assert out.startswith("Jira search failed") and "blocked" in out
|
|
|
|
|
|
# ---- MCP -------------------------------------------------------------------
|
|
|
|
def test_mcp_khong_khoi_dong_va_khong_goi_khi_chan(chan_mang):
|
|
from cowork_local.core.mcp_client import McpServerConnection, McpServerError
|
|
|
|
conn = McpServerConnection("srv", "definitely-not-run")
|
|
with pytest.raises(McpServerError, match="blocked"):
|
|
conn.start(timeout=1)
|
|
assert conn.call_tool("srv__search", {})["ok"] is False
|
|
|
|
|
|
def _context(tmp_path, block: bool):
|
|
from cowork_local.config import AppConfig
|
|
from cowork_local.state import AppContext
|
|
|
|
ctx = AppContext(AppConfig.load(tmp_path / "config.json"))
|
|
ctx.config.agent_security["block_network"] = block
|
|
return ctx
|
|
|
|
|
|
def test_chuan_bi_connector_dung_mcp_dang_chay_va_giu_onedrive_cuc_bo(tmp_path, monkeypatch):
|
|
from cowork_local.core import ms365_local
|
|
|
|
ctx = _context(tmp_path, block=True)
|
|
stopped = []
|
|
monkeypatch.setattr(ctx, "stop_mcp_connections", lambda: stopped.append(True))
|
|
local = (["ms365_local__onedrive_list"], object())
|
|
monkeypatch.setattr(ms365_local, "build_ms365_local_tools", lambda config: local)
|
|
monkeypatch.setattr(ctx._mcp_manager, "ensure", _no_network)
|
|
|
|
assert ctx.build_mcp_tools() == local
|
|
assert stopped == [True]
|
|
|
|
|
|
# ---- task lập lịch -----------------------------------------------------------
|
|
|
|
def test_link_dinh_kem_task_khong_tai(chan_mang):
|
|
from cowork_local.core.tasks import resolve_input_text
|
|
|
|
text = resolve_input_text({"input": {"mode": "empty", "links": ["https://example.com/a"]}})
|
|
assert "not fetched" in text
|
|
|
|
|
|
def test_task_script_chay_trong_tien_trinh_khong_mang(chan_mang, monkeypatch, tmp_path):
|
|
from cowork_local.core import deps, task_script
|
|
|
|
calls = []
|
|
|
|
def fake_run(command, **kwargs):
|
|
calls.append(kwargs)
|
|
return 0, "done", False, False, False
|
|
|
|
monkeypatch.setattr(deps, "run_cancellable", fake_run)
|
|
assert task_script.run_script("echo hi", tmp_path, 30) == "done"
|
|
assert calls and calls[0]["isolate_network"] is True
|
|
|
|
|
|
def test_task_script_khong_co_lap_duoc_thi_khong_chay(chan_mang, monkeypatch, tmp_path):
|
|
from cowork_local.core import deps, task_script
|
|
|
|
monkeypatch.setattr(deps, "run_cancellable",
|
|
lambda command, **kw: (None, "no AppContainer", False, False, False))
|
|
with pytest.raises(RuntimeError, match="could not be isolated"):
|
|
task_script.run_script("echo hi", tmp_path, 30)
|
|
|
|
|
|
def test_tat_chan_mang_thi_task_script_chay_nhu_cu(tmp_path):
|
|
from cowork_local.core import task_script
|
|
|
|
assert "hi" in task_script.run_script("echo hi", tmp_path, 30)
|
|
|
|
|
|
# ---- tự cài thư viện ---------------------------------------------------------
|
|
|
|
def test_khong_tu_cai_thu_vien_khi_chan(chan_mang, monkeypatch):
|
|
from cowork_local.core import deps
|
|
|
|
monkeypatch.setattr(deps, "run_cancellable", _no_network)
|
|
assert deps.ensure_module("khong_ton_tai_xyz_123") is None
|
|
assert "khong_ton_tai_xyz_123" not in deps._FAILED # thử lại khi mở mạng
|
|
ok, detail = deps.pip_install("requests")
|
|
assert ok is False and "blocked" in detail
|
|
|
|
|
|
def test_pptx_khong_bi_nho_la_thieu_khi_chi_do_chan_mang(chan_mang, monkeypatch):
|
|
from cowork_local.application.workspaces import file_preview_helpers as fph
|
|
from cowork_local.core import deps
|
|
|
|
monkeypatch.setattr(fph, "_PPTX_READY", None)
|
|
monkeypatch.setattr(deps, "ensure_module", lambda *a, **k: None)
|
|
assert fph.pptx_available() is False
|
|
assert fph._PPTX_READY is None
|
|
|
|
|
|
# ---- lệnh shell của agent ----------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("backend", ["direct", "integrity_job_wfp", "appcontainer", "windows_sandbox"])
|
|
def test_moi_backend_deu_chay_co_lap_mang(monkeypatch, tmp_path, backend):
|
|
"""Các backend cũ chỉ đặt biến proxy — khi chặn mạng, tất cả đi qua lớp cô lập của OS."""
|
|
from cowork_local.core import deps
|
|
from cowork_local.core.sandbox_manager import SandboxManager
|
|
|
|
calls = []
|
|
|
|
def fake_run(command, **kwargs):
|
|
calls.append(kwargs)
|
|
return 0, "ok", False, False, False
|
|
|
|
monkeypatch.setattr(deps, "run_cancellable", fake_run)
|
|
result = SandboxManager()._execute_with_backend(backend, "echo hi", str(tmp_path), True, 30)
|
|
assert calls and calls[0]["isolate_network"] is True
|
|
assert result["ok"] is True and result["sandbox"] == "network_isolated"
|
|
|
|
|
|
def test_lenh_khong_co_lap_duoc_thi_bi_tu_choi(monkeypatch, tmp_path):
|
|
from cowork_local.core import deps
|
|
from cowork_local.core.sandbox_manager import SandboxManager
|
|
|
|
monkeypatch.setattr(deps, "run_cancellable",
|
|
lambda command, **kw: (None, "no isolation", False, False, False))
|
|
result = SandboxManager()._execute_with_backend("direct", "echo hi", str(tmp_path), True, 30)
|
|
assert result["ok"] is False and result["sandbox"] == "blocked"
|
|
|
|
|
|
def test_he_dieu_hanh_khong_ho_tro_thi_bao_loi_khong_chay(monkeypatch):
|
|
from cowork_local.infrastructure.sandbox import network_isolation
|
|
|
|
monkeypatch.setattr(network_isolation.sys, "platform", "sunos5")
|
|
with pytest.raises(network_isolation.NetworkIsolationUnavailable):
|
|
network_isolation.spawn_without_network("echo hi", None, None)
|
|
|
|
|
|
def _listening_socket():
|
|
server = socket.socket()
|
|
server.bind(("127.0.0.1", 0))
|
|
server.listen(4)
|
|
def serve():
|
|
try:
|
|
for _ in range(4):
|
|
server.accept()
|
|
except OSError:
|
|
pass # socket closed at the end of the test
|
|
|
|
threading.Thread(target=serve, daemon=True).start()
|
|
return server
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "win32", reason="AppContainer chỉ có trên Windows")
|
|
def test_appcontainer_that_su_cat_mang_nhung_van_ghi_duoc_thu_muc(tmp_path):
|
|
"""Bài thật, không giả lập: cùng một lệnh Python nối tới một cổng đang nghe,
|
|
ngoài sandbox thì được, trong sandbox thì bị kernel từ chối."""
|
|
from cowork_local.core.deps import run_cancellable
|
|
|
|
server = _listening_socket()
|
|
port = server.getsockname()[1]
|
|
probe = (f'"{sys.executable}" -c "import socket;'
|
|
"print('PYTHON'+'_RAN');"
|
|
f"socket.create_connection(('127.0.0.1',{port}),5);print('CONN'+'ECTED')\"")
|
|
try:
|
|
rc, out, *_ = run_cancellable(probe, cwd=str(tmp_path), timeout=60, shell=True)
|
|
assert rc == 0 and "CONNECTED" in out
|
|
|
|
rc, out, *_ = run_cancellable(probe + " & echo written> marker.txt",
|
|
cwd=str(tmp_path), timeout=60, isolate_network=True)
|
|
assert "PYTHON_RAN" in out, out # Python itself starts in the sandbox
|
|
assert "CONNECTED" not in out, out
|
|
assert (Path(tmp_path) / "marker.txt").read_text().strip() == "written"
|
|
finally:
|
|
server.close()
|
|
|
|
|
|
# ---- xem trước HTML ------------------------------------------------------------
|
|
|
|
class _FakeUrl:
|
|
def __init__(self, scheme):
|
|
self._scheme = scheme
|
|
|
|
def scheme(self):
|
|
return self._scheme
|
|
|
|
|
|
class _FakeRequest:
|
|
def __init__(self, scheme):
|
|
self._url = _FakeUrl(scheme)
|
|
self.blocked = False
|
|
|
|
def requestUrl(self): # noqa: N802 - Qt name
|
|
return self._url
|
|
|
|
def block(self, flag):
|
|
self.blocked = flag
|
|
|
|
|
|
@pytest.mark.parametrize("scheme,expected", [
|
|
("https", True), ("http", True), ("wss", True), ("file", False), ("data", False),
|
|
])
|
|
def test_xem_truoc_html_chan_tai_nguyen_web(chan_mang, scheme, expected):
|
|
pytest.importorskip("PySide6.QtWebEngineCore")
|
|
from cowork_local.presentation.folder.offline_web_page import RemoteRequestBlocker
|
|
|
|
req = _FakeRequest(scheme)
|
|
RemoteRequestBlocker().interceptRequest(req)
|
|
assert req.blocked is expected
|
|
|
|
|
|
def test_xem_truoc_html_tai_binh_thuong_khi_tat():
|
|
pytest.importorskip("PySide6.QtWebEngineCore")
|
|
from cowork_local.presentation.folder.offline_web_page import RemoteRequestBlocker
|
|
|
|
req = _FakeRequest("https")
|
|
RemoteRequestBlocker().interceptRequest(req)
|
|
assert req.blocked is False
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "win32", reason="AppContainer chỉ có trên Windows")
|
|
def test_khong_cap_quyen_ke_thua_len_thu_muc_chua_qt(tmp_path, monkeypatch):
|
|
"""Quyền AppContainer kế thừa xuống Qt6WebEngineCore.dll làm tiến trình render
|
|
của WebEngine không nạp được DLL — tab Graph và xem trước HTML trắng trơn."""
|
|
from cowork_local.infrastructure.sandbox import appcontainer_process as ac
|
|
|
|
qt_dir = tmp_path / "venv" / "Lib" / "site-packages" / "PySide6"
|
|
qt_dir.mkdir(parents=True)
|
|
monkeypatch.setattr(ac, "_qt_package_dir", lambda: str(qt_dir))
|
|
monkeypatch.setattr(ac, "_icacls", _no_network)
|
|
|
|
with pytest.raises(ac.NetworkIsolationUnavailable, match="Qt runtime"):
|
|
ac.grant_access(str(tmp_path / "venv"), "S-1-15-2-1", "read")
|
|
with pytest.raises(ac.NetworkIsolationUnavailable, match="Qt runtime"):
|
|
ac.grant_access(str(tmp_path), "S-1-15-2-1", "write")
|
|
|
|
|
|
def test_quyen_cho_python_khong_bao_gio_phu_len_thu_vien_qt(monkeypatch, tmp_path):
|
|
from cowork_local.infrastructure.sandbox import appcontainer_process as ac
|
|
|
|
venv, base = tmp_path / "venv", tmp_path / "base"
|
|
monkeypatch.setattr(ac.sys, "prefix", str(venv))
|
|
monkeypatch.setattr(ac.sys, "base_prefix", str(base))
|
|
for qt_dir in (venv / "Lib" / "site-packages" / "PySide6",
|
|
base / "Lib" / "site-packages" / "PySide6"):
|
|
monkeypatch.setattr(ac, "_qt_package_dir", lambda d=qt_dir: str(d))
|
|
for folder, mode in ac._interpreter_grants():
|
|
assert mode == "read_here" or not ac._covers(folder, str(qt_dir)), (folder, mode)
|