Files
cowork-local/domain/security/tool_policy.py
T
Nam Pham Dinh ThanhandClaude Opus 5 3138856741 feat(domain): DTO ToolPolicyGateway — bản đề xuất, gỡ chốt cho N3
N3 (Co4E) cần gọi tool nhưng Team Hoa chưa bắt đầu. Ba đường: N3 ngồi đợi
(trái nguyên tắc không team nào chặn team nào), N3 tự phỏng đoán (không ai
soi, chắc chắn phải sửa), hoặc viết một bản đề xuất để Hoa duyệt. Chọn cái
thứ ba.

Ranh giới giữ đúng sơ đồ phân hệ trong plan.md: domain/security/ là của
Gamma, application/conversations/tool_policy_gateway.py là của Hoa. Nên Gamma
định nghĩa hình dạng, Hoa cài đặt. Không đụng file nào của họ.

Hình dạng bám vào code đang chạy: SecurityVerdict (allowed/reason/layer) và
hộp thoại xin phép ở chat_panel.py:1312. Khác biệt duy nhất là gộp thành một
câu trả lời ba trạng thái ALLOW/DENY/ASK, thay vì bắt chỗ gọi tự nhớ hỏi hai
nơi.

Hai ràng buộc đưa vào có chủ đích, mỗi cái một test:
  - DENY và ASK bắt buộc có reason, ném lỗi ngay lúc dựng. Người dùng cần
    biết vì sao bị chặn và audit_log cần ghi lại.
  - ASK không phải allowed. Đây là bẫy dễ mắc nhất: coi ASK như ALLOW thì
    tool chạy trước khi có ai đồng ý.

Kèm FakeToolPolicyGateway lập trình được theo tên tool hoặc theo hàm, có ghi
lại đã hỏi những gì — test khẳng định được "có hỏi cổng không", không chỉ
"kết quả đúng không".

docs/refactor/GammaTeam_decisions.md thêm quyết định 3, kèm nguyên văn tin
nhắn cần gửi Hoa và ô đánh dấu đã gửi / đã xác nhận.

102 test xanh (96 + 6 mới). CASAN Check 1 sạch. domain/ và application/ có 0
import PySide6 — kiểm bằng AST, vì grep đếm ra 4 mà cả 4 là chữ "PySide6"
nằm trong chính docstring cảnh báo. Check 3 của Team Duy nên phân tích cú
pháp chứ đừng grep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 22:07:00 +09:00

111 lines
4.2 KiB
Python

"""Cổng chính sách cho lời gọi tool — hình dạng dữ liệu, chưa phải cài đặt.
BẢN ĐỀ XUẤT, chờ Team Hoa xác nhận
==================================
Sơ đồ phân hệ trong ``plan.md`` giao ``domain/security/`` cho Team Gamma và
``application/conversations/tool_policy_gateway.py`` cho Team Hoa. Nên Gamma
định nghĩa *hình dạng*, Hoa *cài đặt*.
Viết trước vì N3 (Co4E) cần gọi tool và Team Hoa chưa bắt đầu. Không có nó thì
N3 phải tự phỏng đoán rồi sửa lại sau — mà phỏng đoán của một người thì tệ hơn
một đề xuất viết ra để cả hai bên soi.
Nếu Hoa thấy khác, sửa file này chứ đừng đẻ kiểu thứ hai. Đổi sớm rẻ hơn đổi
muộn: hiện chỉ N3 dùng.
Mô hình bám theo code đang chạy, không bịa:
* ``core/agent_security.py::SecurityVerdict`` — allowed / reason / layer
* ``ui/permission_dialog.py`` — hộp thoại hỏi người dùng khi
``ctx.project_confirm_commands()`` bật (``ui/chat_panel.py:1312``)
Điểm khác biệt duy nhất so với hôm nay: gộp hai thứ đó thành **một câu trả lời
ba trạng thái**, thay vì code gọi phải tự nhớ hỏi cả hai nơi.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, Protocol, runtime_checkable
class PolicyOutcome(str, Enum):
"""Ba trạng thái. ``ASK`` là thứ hệ thống hiện tại đã có (hộp thoại xin
phép) nhưng chưa được coi là một kết quả chính thức."""
ALLOW = "allow"
DENY = "deny"
ASK = "ask"
@dataclass(frozen=True)
class ToolCallRequest:
"""Một lời gọi tool đang chờ được duyệt.
``surface`` cho biết chỗ phát sinh — ``"cowork"``, ``"code"``, ``"co4e"``,
``"task"``. Chính sách khác nhau theo màn: Co4E chạy nền nên không thể bật
hộp thoại hỏi giữa chừng như Cowork.
"""
name: str
arguments: Dict[str, Any] = field(default_factory=dict)
surface: str = "cowork"
project_id: str = ""
#: True nếu tool đến từ MCP server ngoài, False nếu là tool dựng sẵn.
external: bool = False
@dataclass(frozen=True)
class PolicyDecision:
"""Câu trả lời của cổng.
``reason`` bắt buộc có khi DENY hoặc ASK — người dùng phải biết vì sao bị
chặn, và ``core/audit_log.py`` cần nó để ghi lại.
``layer`` giữ đúng từ vựng của ``SecurityVerdict``: ``"prompt"`` |
``"attachment"`` | ``"command"``, cộng thêm ``"policy"`` cho quyết định của
chính cổng này.
"""
outcome: PolicyOutcome
reason: str = ""
layer: str = "policy"
@property
def allowed(self) -> bool:
"""Tương thích với chỗ đang đọc ``SecurityVerdict.allowed``.
Chú ý: ``ASK`` KHÔNG phải allowed — còn phải hỏi người dùng đã.
"""
return self.outcome is PolicyOutcome.ALLOW
def __post_init__(self):
if self.outcome is not PolicyOutcome.ALLOW and not self.reason:
raise ValueError("DENY và ASK bắt buộc có reason — người dùng và "
"audit log đều cần biết vì sao")
def allow() -> PolicyDecision:
return PolicyDecision(PolicyOutcome.ALLOW)
def deny(reason: str, layer: str = "policy") -> PolicyDecision:
return PolicyDecision(PolicyOutcome.DENY, reason, layer)
def ask(reason: str, layer: str = "policy") -> PolicyDecision:
return PolicyDecision(PolicyOutcome.ASK, reason, layer)
@runtime_checkable
class ToolPolicyGateway(Protocol):
"""Hỏi trước khi chạy tool. Cài đặt thật: Team Hoa (R07, hạn 29/08)."""
def check(self, request: ToolCallRequest) -> PolicyDecision:
"""Được chạy tool này không.
KHÔNG được tự bật hộp thoại bên trong — cổng chỉ *trả lời*, còn hỏi ai
và hỏi thế nào là việc của tầng giao diện. Có vậy thì Co4E chạy nền mới
dùng chung cổng được với Cowork chạy tương tác.
"""
...