fix(sandbox): chặn mạng thật sự cho mọi tool NETWORK của agent
CI / test (pull_request) Canceled after 0s

block_network trước đây chỉ được run_command đọc tới, nên fetch_url,
jira_search, jira_get_issue và install_package vẫn ra internet bình thường
trong khi Monitoring báo Mạng - Bị chặn. Thêm cổng chặn ngay đầu bốn tool
đó, trước mọi lời gọi mạng, kèm test hồi quy và guardrail theo
BUILT_IN_CAPABILITIES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 08:33:26 +09:00
co-authored by Claude Opus 5
parent d0df96c726
commit 57af508971
5 changed files with 175 additions and 9 deletions
+8 -4
View File
@@ -100,11 +100,15 @@ DEFAULT_CONFIG: Dict[str, Any] = {
"resource_limit_cpu_percent": 80, # 0 = unlimited; caps a run_command/install_package process TREE's total CPU%
"resource_limit_memory_mb": 2048, # 0 = unlimited; caps total RSS memory (MB)
"resource_limit_disk_mb": 512, # 0 = unlimited; caps total disk read+write (MB)
"block_network": True, # strip proxy env / point at a black-hole address for agent-run commands
# Cut the agent off the network: proxy env pointed at a black hole for
# agent-run shell commands, PLUS a flat refusal from every tool tagged
# ToolCapability.NETWORK (fetch_url, jira_*, install_package) — those
# reach the net in-process, where the proxy trick has nothing to act on.
"block_network": True,
# Allow the agent's fetch_url tool to read web pages / online documents /
# SharePoint-OneDrive share links. SEPARATE from block_network (that only
# sandboxes agent-run shell commands) — reading a URL for info is safe and
# useful, so this defaults ON. Toggle in Settings → Security.
# SharePoint-OneDrive share links. Its own toggle — reading a URL for info
# is safe and useful, so this defaults ON — but block_network outranks it:
# with the network blocked the tool is refused either way.
"allow_url_fetch": True,
"sandbox_pw": "", # set through COWORK_SANDBOX_PASSWORD
"rulebase_path": "", # custom RULEBASE.md — attached to every agent execution
@@ -128,6 +128,15 @@ def install_package(ctx: ToolContext, args: Dict[str, Any],
package = str(args.get("package", "")).strip()
if not package:
return {"ok": False, "output": "No package specified."}
# ``pip install`` bắt buộc phải ra internet, mà ``deps.pip_install`` chạy
# subprocess với ``os.environ`` nguyên vẹn — biến proxy hố đen của
# ``network_blocked_env`` không chạm tới nó. Từ chối thẳng ở đây (giống cách
# run_command chặn theo tên các công cụ không đi qua proxy) thay vì để pip
# thử 600 giây rồi báo một lỗi proxy khó hiểu.
if ctx.block_network:
return {"ok": False, "output": (
"install_package: network access is blocked by the Sandbox Security Layer "
"(\"Block network for agent-run commands\" is on in Settings).")}
python = _sandbox_python(ctx, cancel, on_output)
ok, detail = pip_install(package, cancel=cancel, on_output=on_output, python=python)
head = f"Installed {package}." if ok else f"Could not install {package}."
+25 -1
View File
@@ -6,11 +6,26 @@ tag added in R05-T01/domain/tools/tool_registry.py describes.
"""
from __future__ import annotations
from typing import Any, Dict
from typing import Any, Dict, Optional
from .tool_context import ToolContext
def _network_refusal(ctx: ToolContext, tool: str) -> Optional[Dict[str, Any]]:
"""Lời từ chối khi Sandbox Security Layer đang chặn mạng; None nếu được đi.
``block_network`` trước đây chỉ được đọc ở ``command_tools.py`` (lệnh shell),
nên ba tool mang ``ToolCapability.NETWORK`` ở file này vẫn ra internet bình
thường trong khi Monitoring báo "Mạng: Bị chặn". Kiểm ở đây, TRƯỚC mọi lời
gọi mạng, để công tắc chặn đúng thứ nó nói là chặn.
"""
if not ctx.block_network:
return None
return {"ok": False, "output": (
f"{tool}: network access is blocked by the Sandbox Security Layer "
"(\"Block network for agent-run commands\" is on in Settings).")}
def fetch_url(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
"""Fetch a URL's text content (web page / online document / SharePoint-
OneDrive share link) via link_fetch — the same parser task-link attachments
@@ -20,6 +35,9 @@ def fetch_url(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
return {"ok": False, "output": "fetch_url: 'url' is required."}
if not url.lower().startswith(("http://", "https://")):
return {"ok": False, "output": f"fetch_url: not an http(s) URL: {url}"}
blocked = _network_refusal(ctx, "fetch_url")
if blocked is not None:
return blocked
if not ctx.allow_url_fetch:
return {"ok": False,
"output": ("fetch_url: URL fetching is turned off in Settings → Security "
@@ -37,6 +55,9 @@ def fetch_url(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
def jira_search(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
"""Tìm issue trên Jira bằng JQL."""
blocked = _network_refusal(ctx, "jira_search")
if blocked is not None:
return blocked
from cowork_local.core import jira_tool
out = jira_tool.search(ctx.jira, str(args.get("jql", "")),
@@ -47,6 +68,9 @@ def jira_search(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
def jira_get_issue(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
"""Lấy chi tiết một issue Jira theo mã."""
blocked = _network_refusal(ctx, "jira_get_issue")
if blocked is not None:
return blocked
from cowork_local.core import jira_tool
out = jira_tool.get_issue(ctx.jira, str(args.get("key", "")))
+8 -4
View File
@@ -37,12 +37,16 @@ class ToolContext:
# None (default) = no limits, matching pre-existing behavior.
resource_limits: Optional[Dict[str, float]] = None
# Sandbox Security Layer — Settings' "Block network for agent commands"
# (policy-level, see deps.py::network_blocked_env). False (default) =
# — the proxy-env block for shell commands (deps.py::network_blocked_env)
# AND a flat refusal from every NETWORK-capability tool, which reaches the
# net in-process where proxy env vars mean nothing. False (default) =
# unrestricted, matching pre-existing behavior.
block_network: bool = False
# Whether the fetch_url tool may read URLs — SEPARATE from block_network
# (reading a web page/share link for info is safe; running networked shell
# commands is the risk). Defaults True; set from agent_security.allow_url_fetch.
# Whether the fetch_url tool may read URLs. Its own toggle, but NOT a way
# around block_network: with the network blocked every NETWORK-capability
# tool is refused first (fetch_tools.py::_network_refusal), so this flag only
# decides anything while the network is open. Defaults True; set from
# agent_security.allow_url_fetch.
allow_url_fetch: bool = True
# Jira read connector config (base_url/email/api_token) — None disables the
# jira_* tools' ability to connect. Populated from config.data["jira"].
+125
View File
@@ -0,0 +1,125 @@
"""Công tắc "Chặn mạng cho lệnh do agent chạy" phải chặn MỌI đường ra mạng của
agent, không riêng ``run_command``.
Trước đây ``block_network`` chỉ được đọc ở đúng một chỗ —
``infrastructure/filesystem/command_tools.py`` trong ``run_command`` — nên bốn
tool mang ``ToolCapability.NETWORK`` (``fetch_url``, ``jira_search``,
``jira_get_issue``, ``install_package``) vẫn ra internet bình thường trong khi
màn Monitoring báo "Mạng: Bị chặn" và docstring của ``fetch_tools`` tự nhận là
*"Honors the Sandbox Security Layer's Block network policy"*. Người dùng bật
công tắc rồi thấy agent vẫn search web được — đúng triệu chứng được báo.
Hai nhóm bài:
* **hành vi** — bật thì mọi tool NETWORK từ chối TRƯỚC khi chạm mạng, tắt thì
đường cũ giữ nguyên (chặn một chiều là hỏng tính năng);
* **guardrail** — thêm tool mạng mới mà quên chặn thì bài ở đây đỏ ngay.
"""
from __future__ import annotations
from typing import Dict
import pytest
from cowork_local.core.tools import ToolContext
from cowork_local.domain.tools import BUILT_IN_CAPABILITIES, ToolCapability
from cowork_local.infrastructure.filesystem import command_tools, fetch_tools
# tên tool -> (handler, args hợp lệ tối thiểu). Args phải hợp lệ, nếu không bài
# test sẽ đỏ vì lỗi thiếu tham số chứ không vì cổng chặn mạng.
_TOOL_MANG: Dict[str, tuple] = {
"fetch_url": (fetch_tools.fetch_url, {"url": "https://example.com/"}),
"jira_search": (fetch_tools.jira_search, {"jql": "project = ABC"}),
"jira_get_issue": (fetch_tools.jira_get_issue, {"key": "ABC-1"}),
"install_package": (command_tools.install_package, {"package": "requests"}),
}
@pytest.fixture
def cam_ra_mang(monkeypatch):
"""Mọi đường ra mạng thật đều nổ.
Vừa giữ cho bộ test không chạm internet, vừa làm lộ tool nào lọt qua cổng
chặn: nó sẽ đỏ ngay tại lời gọi mạng thay vì im lặng đi ra ngoài.
"""
def no_ra_mang(*args, **kwargs):
raise AssertionError("tool đã chạm mạng dù 'Chặn mạng' đang bật")
from cowork_local.core import deps, jira_tool, link_fetch
monkeypatch.setattr(link_fetch, "fetch_link_preview", no_ra_mang)
monkeypatch.setattr(jira_tool, "search", no_ra_mang)
monkeypatch.setattr(jira_tool, "get_issue", no_ra_mang)
monkeypatch.setattr(jira_tool, "get_issue_by_url", no_ra_mang)
monkeypatch.setattr(deps, "pip_install", no_ra_mang)
# ---- hành vi: bật công tắc thì mọi tool mạng đều bị chặn -----------------
@pytest.mark.parametrize("ten", sorted(_TOOL_MANG))
def test_bat_chan_mang_thi_tool_tu_choi_truoc_khi_cham_mang(tmp_path, cam_ra_mang, ten):
"""Đây là chính triệu chứng người dùng báo: bật rồi mà vẫn ra được web."""
handler, args = _TOOL_MANG[ten]
ctx = ToolContext(tmp_path, block_network=True)
ket_qua = handler(ctx, args)
assert ket_qua["ok"] is False, f"{ten} vẫn chạy khi đang chặn mạng"
assert "Sandbox Security Layer" in ket_qua["output"], ket_qua["output"]
def test_allow_url_fetch_khong_lach_duoc_chan_mang(tmp_path, cam_ra_mang):
"""Hai công tắc vẫn độc lập, nhưng "Chặn mạng" là cái mạnh hơn: bật nó thì
"Cho phép agent lấy dữ liệu từ URL" không mở lại đường được."""
ctx = ToolContext(tmp_path, block_network=True, allow_url_fetch=True)
ket_qua = fetch_tools.fetch_url(ctx, {"url": "https://example.com/"})
assert ket_qua["ok"] is False
# ---- hành vi: tắt công tắc thì đường cũ giữ nguyên -----------------------
def test_tat_chan_mang_thi_fetch_url_van_doc_duoc(tmp_path, monkeypatch):
"""Chặn một chiều là hỏng tính năng — cổng phải mở lại được."""
from cowork_local.core import link_fetch
monkeypatch.setattr(link_fetch, "fetch_link_preview",
lambda url: f"nội dung của {url}")
ctx = ToolContext(tmp_path, block_network=False)
ket_qua = fetch_tools.fetch_url(ctx, {"url": "https://example.com/"})
assert ket_qua["ok"] is True
assert "example.com" in ket_qua["output"]
def test_tat_chan_mang_thi_install_package_van_chay(tmp_path, monkeypatch):
from cowork_local.core import deps
da_goi = []
def gia_lap_pip(package, **kwargs):
da_goi.append(package)
return True, "ok"
monkeypatch.setattr(deps, "pip_install", gia_lap_pip)
ctx = ToolContext(tmp_path, block_network=False)
ket_qua = command_tools.install_package(ctx, {"package": "requests"})
assert da_goi == ["requests"]
assert ket_qua["ok"] is True
# ---- guardrail: danh sách tool mạng không được lệch ----------------------
def test_moi_tool_mang_deu_co_bai_o_day():
"""``BUILT_IN_CAPABILITIES`` là nơi duy nhất khai báo tool nào chạm mạng.
Thêm một tool NETWORK mới mà quên chặn thì bài này đỏ ngay."""
tag_mang = {ten for ten, cap in BUILT_IN_CAPABILITIES.items()
if cap & ToolCapability.NETWORK}
assert tag_mang == set(_TOOL_MANG), (
"danh sách tool mạng đã đổi — chặn tool mới ở cổng block_network "
"rồi bổ sung vào _TOOL_MANG")