feat(settings): bỏ khoá mật khẩu ở Sandbox Security Layer
Bốn công tắc của nhóm này (xác nhận lệnh, chặn mạng, bật lớp bảo mật agent, AI kiểm tra lệnh) dựng ra ở trạng thái setEnabled(False) và chỉ mở khi nhập đúng mật khẩu. Theo yêu cầu, bỏ hẳn bước đó: form luôn bật/tắt được. Gỡ ô nhập, nút Mở khoá, nhãn trạng thái khoá, _sandbox_unlock() và _sandbox_password_matches(); dọn 10 khoá i18n thành chết và 2 field trong tools/check_dialogs.py. Giữ nguyên agent_security.sandbox_pw ở config.py — yêu cầu chỉ nói tới màn hình, không nói tới tầng cấu hình. Khoá này vốn không phải rào bảo mật: docstring của _sandbox_unlock() đã tự ghi "khoá phía giao diện để chặn bấm nhầm ... KHÔNG phải cơ chế bảo mật thật". Rào thật nằm ở sandbox lúc chạy lệnh. Vùng này thuộc diện SECURITY.md yêu cầu Cowork Team soát thêm. 10 bài test cũ (SEC-20260907-01) chốt các đường không được mở khoá nay mất đối tượng kiểm, thay bằng 12 bài chốt hành vi mới: bốn công tắc sửa được ngay, không còn widget mật khẩu, kèm guardrail quét mã nguồn chặn khoá lại. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+6
-83
@@ -13,20 +13,17 @@ chưa từng được gán nên gọi vào là AttributeError.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout,
|
||||
QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
||||
QMessageBox, QPushButton, QScrollArea, QSpinBox,
|
||||
QGroupBox, QHBoxLayout, QListWidget, QListWidgetItem,
|
||||
QScrollArea, QSpinBox,
|
||||
QTreeWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import dialog_buttons
|
||||
from .icons import IconLabel
|
||||
from .widgets import ToggleSwitch
|
||||
|
||||
|
||||
@@ -37,25 +34,6 @@ from ..presentation.settings.routing_settings_widget import RoutingSettingsWidge
|
||||
from ..presentation.settings.about_widget import AboutSettingsWidget
|
||||
|
||||
|
||||
def _sandbox_password_matches(entered: str, stored: str) -> bool:
|
||||
"""Whether ``entered`` unlocks the Sandbox Security group.
|
||||
|
||||
An empty ``stored`` must never match. ``DEFAULT_CONFIG`` ships
|
||||
``agent_security.sandbox_pw = ""`` and the config handed to this dialog is
|
||||
always deep-merged with those defaults, so a plain ``entered == stored``
|
||||
accepts an empty field on every install that never set a password. The MS365
|
||||
unlock guards the same way — see ``json_config_repository.unlock_ms365``.
|
||||
|
||||
Both sides are compared as UTF-8 bytes, not as ``str``:
|
||||
``compare_digest`` raises ``TypeError`` on ``str`` holding anything outside
|
||||
ASCII, and this app defaults to Vietnamese and ships to Japanese customers,
|
||||
so an accented password is ordinary input rather than an edge case.
|
||||
"""
|
||||
if not entered or not stored:
|
||||
return False
|
||||
return secrets.compare_digest(entered.encode("utf-8"), stored.encode("utf-8"))
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
"""Hộp thoại Cài đặt: cột mục lục bên trái, các trang bên phải
|
||||
(Nhà cung cấp · Connectors · Định tuyến · Tham số · Chung).
|
||||
@@ -108,29 +86,10 @@ class SettingsDialog(QDialog):
|
||||
self.sandbox_group = QGroupBox(tr("settings.group.sandbox"))
|
||||
sbl = QVBoxLayout(self.sandbox_group)
|
||||
|
||||
# --- Password protection for Sandbox Security (at top) ---
|
||||
self.sandbox_pw_label = IconLabel("lock", tr("settings.sandbox_pw_label"))
|
||||
sbl.addWidget(self.sandbox_pw_label)
|
||||
|
||||
pw_row = QHBoxLayout()
|
||||
self.sandbox_pw_edit = QLineEdit("")
|
||||
self.sandbox_pw_edit.setPlaceholderText(tr("settings.sandbox_pw_placeholder"))
|
||||
self.sandbox_pw_edit.setEchoMode(QLineEdit.Password)
|
||||
pw_row.addWidget(self.sandbox_pw_edit, 1)
|
||||
self.sandbox_unlock_btn = QPushButton(tr("settings.sandbox_unlock_btn"))
|
||||
self.sandbox_unlock_btn.clicked.connect(self._sandbox_unlock)
|
||||
pw_row.addWidget(self.sandbox_unlock_btn)
|
||||
self.sandbox_locked_status = IconLabel("lock", tr("settings.sandbox_locked"), color="#c00")
|
||||
self.sandbox_locked_status.text_label().setStyleSheet("color: #c00; font-weight: bold;")
|
||||
pw_row.addWidget(self.sandbox_locked_status)
|
||||
sbl.addLayout(pw_row)
|
||||
self._sandbox_unlocked = False # Start LOCKED — must enter password first
|
||||
self._sandbox_pw = sec.get("sandbox_pw", "")
|
||||
|
||||
# Separator line between pw section and sandbox settings
|
||||
pw_sep = QLabel("────────────────")
|
||||
sbl.addWidget(pw_sep)
|
||||
|
||||
# Nhóm này KHÔNG còn khoá bằng mật khẩu: bốn công tắc dưới đây bật/tắt
|
||||
# tự do. Khoá cũ chỉ là rào chống bấm nhầm ở phía giao diện, không phải
|
||||
# cơ chế bảo mật thật (rào thật nằm ở sandbox lúc chạy lệnh), nên bỏ đi
|
||||
# theo yêu cầu thay vì giữ một bước nhập mật khẩu không bảo vệ được gì.
|
||||
self.sandbox_confirm = ToggleSwitch(tr("settings.sandbox_confirm_commands"))
|
||||
self.sandbox_confirm.setChecked(bool(sec.get("cowork_confirm_commands", False)))
|
||||
self.sandbox_confirm.setToolTip(tr("settings.sandbox_confirm_commands_tooltip"))
|
||||
@@ -160,14 +119,6 @@ class SettingsDialog(QDialog):
|
||||
# Resource limits (CPU/Memory/Disk I/O) moved to the Parameter group
|
||||
# below — see _param_section("settings.group.sandbox_limits").
|
||||
|
||||
# Collect all sandbox-editable widgets and lock them until unlocked
|
||||
self._sandbox_widgets = [
|
||||
self.sandbox_confirm, self.sandbox_block_network,
|
||||
self.ai_check, self.sec_enabled,
|
||||
]
|
||||
for _w in self._sandbox_widgets:
|
||||
_w.setEnabled(False)
|
||||
|
||||
root.addWidget(self.sandbox_group)
|
||||
|
||||
# Connectors (MCP / REST API) are managed entirely in Monitoring → Tools
|
||||
@@ -299,34 +250,6 @@ class SettingsDialog(QDialog):
|
||||
|
||||
|
||||
|
||||
def _sandbox_unlock(self) -> None:
|
||||
"""Mở khoá nhóm cài đặt sandbox bằng mật khẩu.
|
||||
|
||||
Đây là khoá phía giao diện để chặn bấm nhầm vào một mục nhạy cảm, KHÔNG
|
||||
phải cơ chế bảo mật thật.
|
||||
"""
|
||||
pw = self.sandbox_pw_edit.text()
|
||||
if not self._sandbox_pw:
|
||||
# No password configured. Refusing with "wrong password" would be a
|
||||
# dead end — the user would keep retrying a password that cannot
|
||||
# exist — so name the actual state instead.
|
||||
QMessageBox.warning(self, tr("settings.sandbox_pw_unset_title"),
|
||||
tr("settings.sandbox_pw_unset_body"))
|
||||
return
|
||||
if _sandbox_password_matches(pw, self._sandbox_pw):
|
||||
self._sandbox_unlocked = True
|
||||
self.sandbox_locked_status.setText(tr("settings.sandbox_unlocked"))
|
||||
self.sandbox_locked_status.set_icon("unlock", "#090")
|
||||
self.sandbox_locked_status.text_label().setStyleSheet("color: #090; font-weight: bold;")
|
||||
# Enable all sandbox widgets
|
||||
for w in self._sandbox_widgets:
|
||||
w.setEnabled(True)
|
||||
QMessageBox.information(self, tr("settings.group.sandbox"),
|
||||
tr("settings.sandbox_unlocked_body"))
|
||||
else:
|
||||
QMessageBox.warning(self, tr("settings.sandbox_pw_wrong_title"),
|
||||
tr("settings.sandbox_pw_wrong_body"))
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Gom cấu hình từ mọi trang con rồi ghi xuống đĩa."""
|
||||
data = self.ctx.config.data
|
||||
|
||||
Reference in New Issue
Block a user