Files
cowork-local/domain/security/tool_policy.py
T
anhtnm1andClaude Opus 5 e29a0ccdbd refactor: vá 4 hồi quy, tách 4 file chạm trần LOC, docstring lên 100%
Hồi quy đã vá
-------------
F-12  Kéo–thả hoặc dán tệp vào ô chat ném NameError. R08 tách `_Input` sang
      `chat_input_box.py` nhưng để `_paths_from_mime()` ở lại
      `composer_widget.py`, nên hai hàm sự kiện Qt gọi một cái tên không tồn
      tại. Bốn hàm dùng chung chuyển sang `composer_mime.py` — module thứ ba
      là chỗ duy nhất không lặp lại được lỗi này. Đo lại: cả thả lẫn dán đều
      gắn 1 tệp, khớp bản trước refactor.

F-01  Đổi provider thì bộ chọn model AI-Edit không làm gì. Hook cũ kiểm
      `folder.ai_model_combo`, thuộc tính R08-T12 đã dời sang
      `ai_panel.resolver`. Làm mới vô điều kiện, đúng như tab cũ: lần lấy đầu
      tiên hỏng thì đổi provider chính là lúc phải thử lại.

F-07  Hàng chọn kỳ của Dashboard bị đẩy xuống dưới các thẻ số liệu. Hàng này
      lọc CẢ BA thẻ con chứ không riêng biểu đồ, nên để nó nằm dưới là bắt
      người dùng đọc con số trước khi thấy con số đó tính cho kỳ nào. Kèm
      theo: `TokenUsageCardWidget` bị bỏ sót `setContentsMargins(0,0,0,0)`
      mà hai thẻ con còn lại đã có, đẩy cả hàng thẻ lệch 9px.
      `check_layout_geometry` nay khớp TỪNG BYTE với bản trước refactor.

F-11  Hai lớp khai trùng tên phương thức; Python giữ bản sau nên bản đầu là
      mã chết. `co4e_tab.py::showEvent` bản đầu gọi `_narrow_guard.attach()`
      và không bao giờ chạy.

Tách file (F-09)
----------------
Bốn file chạm trần 400 dòng, mỗi lần cắt ra một trách nhiệm thật:

    graph_renderer.py         -> graph_scene_builder.py + graph_export.py
    co4e_workflow_service.py  -> co4e_run_history.py
    json_config_repository.py -> config_sections.py
    agents_admin_tab.py       -> shared/agent_kind_visuals.py

File cuối còn xoá 3 bản sao của hàm đã có trong `shared/formatters.py`,
giống hệt đến từng dòng — nay định dạng thời gian và avatar không lệch nhau
giữa các bảng Giám sát nữa.

Docstring
---------
41,6% -> 100% (3.478/3.478 định nghĩa production), kể cả module dormant và
phương thức dunder. Toàn bộ phần bổ sung viết bằng tiếng Việt; comment tiếng
Anh có sẵn giữ nguyên — dịch ngược là một đợt riêng.

Seam chưa nối dây (F-05)
------------------------
9 seam mang nhãn `SEAM · dựng <ngày>` kèm hai câu: được nối khi nào, và để
dormant thì hỏng gì. Ngày lấy từ lịch sử git, không phải hạn tự đặt. Gate O
đọc nhãn đó và nhắc khi quá 30 ngày.

859 test xanh · 4/4 cổng CASAN · 19/24 checker khớp từng byte bản cũ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:41:45 +09:00

129 lines
5.3 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.
SEAM · dựng 2026-08-21 · chưa nối dây (F-05)
------------------------------------------------------------
Được nối khi: ``application/conversations/tool_policy_gateway.py`` trả về ``PolicyDecision`` thay cho ``bool``.
Để dormant thì sao: Hiện gateway chỉ trả đúng/sai nên lý do chặn bị mất —
đúng thứ kiểu dữ liệu này sinh ra để mang theo.
Cổng ``scripts/check_orphan_modules.py`` đếm tuổi seam từ ngày trên
và nhắc khi quá ``SEAM_MAX_AGE_DAYS``. Đổi nội dung dòng đó thì cổng
đọc theo — đừng sửa ngày để làm im lời nhắc.
"""
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):
"""Ép mọi quyết định DENY/ASK phải kèm lý do.
Người dùng thấy lý do trên hộp thoại, và nhật ký kiểm toán ghi lại nó — một
quyết định chặn không lý do là không truy được về sau.
"""
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:
"""Quyết định cho phép. Không cần lý do: đây là đường đi bình thường."""
return PolicyDecision(PolicyOutcome.ALLOW)
def deny(reason: str, layer: str = "policy") -> PolicyDecision:
"""Quyết định chặn hẳn, kèm lý do và tên lớp đã ra quyết định."""
return PolicyDecision(PolicyOutcome.DENY, reason, layer)
def ask(reason: str, layer: str = "policy") -> PolicyDecision:
"""Quyết định phải hỏi người dùng, kèm lý do và tên lớp đã ra quyết định."""
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.
"""
...