Delta team/fix comment ui v2 #11

Merged
gitea-admin merged 3 commits from delta-team/fix-comment-UI-v2 into main 2026-09-14 13:15:41 +00:00
5 changed files with 69 additions and 309 deletions
Showing only changes of commit 10799dc67b - Show all commits
-6
View File
@@ -344,12 +344,6 @@ STRINGS: Dict[str, Dict[str, str]] = {
"en": "Let the control agent review a command with AI before it runs.", "en": "Let the control agent review a command with AI before it runs.",
"ja": "実行前に制御エージェントがAIでコマンドを確認します。", "ja": "実行前に制御エージェントがAIでコマンドを確認します。",
"vi": "Cho control-agent dùng AI xét lệnh trước khi chạy."}, "vi": "Cho control-agent dùng AI xét lệnh trước khi chạy."},
"settings.sandbox_pw_unset_title": {
"en": "Sandbox Security", "ja": "サンドボックスセキュリティ", "vi": "Bảo mật Sandbox"},
"settings.sandbox_pw_unset_body": {
"en": "No sandbox password is set yet, so these settings stay locked. Set COWORK_SANDBOX_PASSWORD, or ask your administrator.",
"ja": "サンドボックスのパスワードが未設定のため、この設定はロックされたままです。COWORK_SANDBOX_PASSWORD を設定するか、管理者にお問い合わせください。",
"vi": "Chưa đặt mật khẩu sandbox nên nhóm thiết lập này vẫn khóa. Hãy đặt COWORK_SANDBOX_PASSWORD, hoặc liên hệ quản trị viên."},
"settings.sandbox_confirm_commands": { "settings.sandbox_confirm_commands": {
"en": "Confirm before Cowork runs a command", "en": "Confirm before Cowork runs a command",
"ja": "Cowork がコマンドを実行する前に確認する", "ja": "Cowork がコマンドを実行する前に確認する",
-22
View File
@@ -45,28 +45,6 @@ STRINGS: Dict[str, Dict[str, str]] = {
"ファイアウォールではありません。上のコマンドホワイトリストと併用してください。", "ファイアウォールではありません。上のコマンドホワイトリストと併用してください。",
"vi": "Kiểm soát ở tầng chính sách (trỏ biến môi trường proxy vào hố đen) — không phải " "vi": "Kiểm soát ở tầng chính sách (trỏ biến môi trường proxy vào hố đen) — không phải "
"firewall tầng kernel. Kết hợp với whitelist lệnh ở trên để phòng thủ nhiều lớp."}, "firewall tầng kernel. Kết hợp với whitelist lệnh ở trên để phòng thủ nhiều lớp."},
"settings.sandbox_pw_label": {
"en": "Sandbox Security Password", "ja": "サンドボックスセキュリティのパスワード",
"vi": "Mật khẩu Bảo mật Sandbox"},
"settings.sandbox_pw_placeholder": {
"en": "Enter password to edit sandbox settings",
"ja": "サンドボックス設定を変更するにはパスワードを入力してください",
"vi": "Nhập mật khẩu để sửa thiết lập sandbox"},
"settings.sandbox_unlock_btn": {"en": "Unlock", "ja": "ロック解除", "vi": "Mở khoá"},
"settings.sandbox_locked": {
"en": "Locked (changes disabled)", "ja": "ロック中(変更できません)",
"vi": "Đang khoá (không sửa được)"},
"settings.sandbox_unlocked": {
"en": "Unlocked", "ja": "ロック解除済み", "vi": "Đã mở khoá"},
"settings.sandbox_unlocked_body": {
"en": "Sandbox settings unlocked.", "ja": "サンドボックス設定のロックを解除しました。",
"vi": "Đã mở khoá thiết lập sandbox."},
"settings.sandbox_pw_wrong_title": {
"en": "Wrong Password", "ja": "パスワードが違います", "vi": "Sai mật khẩu"},
"settings.sandbox_pw_wrong_body": {
"en": "Password incorrect. Sandbox settings remain locked.",
"ja": "パスワードが正しくありません。サンドボックス設定はロックされたままです。",
"vi": "Mật khẩu không đúng. Thiết lập sandbox vẫn bị khoá."},
"settings.sandbox_unlimited": {"en": "Unlimited", "ja": "無制限", "vi": "Không giới hạn"}, "settings.sandbox_unlimited": {"en": "Unlimited", "ja": "無制限", "vi": "Không giới hạn"},
"settings.sandbox_cpu_label": {"en": "CPU limit", "ja": "CPU 制限", "vi": "Giới hạn CPU"}, "settings.sandbox_cpu_label": {"en": "CPU limit", "ja": "CPU 制限", "vi": "Giới hạn CPU"},
"settings.sandbox_memory_label": {"en": "Memory limit", "ja": "メモリ制限", "vi": "Giới hạn bộ nhớ"}, "settings.sandbox_memory_label": {"en": "Memory limit", "ja": "メモリ制限", "vi": "Giới hạn bộ nhớ"},
+62 -197
View File
@@ -1,23 +1,21 @@
"""Sandbox Security unlock — chốt các đường KHÔNG được mở khoá (SEC-20260907-01). """Sandbox Security Layer: bốn công tắc luôn sửa được, không còn khoá mật khẩu.
``DEFAULT_CONFIG`` ship ``agent_security.sandbox_pw = ""`` kể từ commit Trước đây nhóm này bị khoá: bốn công tắc dựng ra ở trạng thái ``setEnabled(False)``
``3827552 fix(security): remove shared unlock defaults``, và cấu hình đưa tới và chỉ mở khi nhập đúng mật khẩu qua ``_sandbox_unlock()``. Bộ bài cũ ở file này
dialog LUÔN được deep-merge với defaults đó (SEC-20260907-01) chốt các đường KHÔNG được mở khoá — chúng mất đối tượng kiểm khi
(``infrastructure/config/json_config_repository.py``). Nghĩa là trên mọi bản cài tính năng khoá bị bỏ theo yêu cầu, nên được thay bằng các bài dưới đây.
không đặt ``COWORK_SANDBOX_PASSWORD``, mật khẩu đã lưu là chuỗi rỗng — và phép so
sánh ``pw == self._sandbox_pw`` nhận luôn ô nhập trống.
Ba nhóm bài ở đây: Docstring của ``_sandbox_unlock()`` cũ đã tự nói rõ nó là gì: *"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"*. Rào thật
nằm ở tầng sandbox lúc chạy lệnh, không ở hộp thoại Cài đặt.
* **đường tấn công** — chốt đúng lỗ trên; Hai nhóm bài:
* **đường đi đúng** — bản vá không được phá, kể cả với mật khẩu có dấu;
* **chặn cả lớp lỗi** — commit ``3827552`` sửa ``config.py`` nhưng bỏ sót bản sao * **hành vi mới** — mở hộp thoại là bật/tắt được ngay, không qua bước nào;
thứ hai của literal trong ``ui/settings_dialog.py``. Bài cuối quét chéo mọi thư * **guardrail** — quét mã nguồn để lần sau không ai lặng lẽ khoá lại.
mục nguồn để lần sau không sót kiểu đó nữa.
""" """
from __future__ import annotations from __future__ import annotations
import re
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -27,209 +25,76 @@ import pytest
from .test_settings_dialog_dac_ta import _Ctx from .test_settings_dialog_dac_ta import _Ctx
@pytest.fixture def _dialog():
def shown(monkeypatch): """SettingsDialog dựng đúng như bản cài thật."""
"""Ghi lại mọi QMessageBox thay vì bật modal thật (modal sẽ treo test).
Trả về list các ``(loại, tiêu_đề, nội_dung)`` — cần thiết để phân biệt
"chưa cấu hình mật khẩu" với "sai mật khẩu"; nếu chỉ nuốt hộp thoại đi thì
hai nhánh gộp lại làm một mà test vẫn xanh.
"""
from PySide6.QtWidgets import QMessageBox
calls: list[tuple[str, str, str]] = []
def _record(kind):
def _fn(_parent, title, text, *a, **k):
calls.append((kind, title, text))
return staticmethod(_fn)
monkeypatch.setattr(QMessageBox, "warning", _record("warning"))
monkeypatch.setattr(QMessageBox, "information", _record("information"))
return calls
def _dialog(stored_pw: str):
"""SettingsDialog với ``sandbox_pw`` đúng như bản cài thật: key CÓ mặt."""
from cowork_local.ui.settings_dialog import SettingsDialog from cowork_local.ui.settings_dialog import SettingsDialog
ctx = _Ctx() return SettingsDialog(_Ctx())
ctx.config.data["agent_security"]["sandbox_pw"] = stored_pw
return SettingsDialog(ctx)
# ---- đường tấn công ------------------------------------------------------ _CONG_TAC = ("sandbox_confirm", "sandbox_block_network", "sec_enabled", "ai_check")
def test_o_trong_khong_mo_duoc_khoa(qapp, shown):
"""Chưa đặt mật khẩu (sandbox_pw == "") thì ô nhập trống KHÔNG được mở khoá."""
dlg = _dialog("")
dlg.sandbox_pw_edit.setText("")
dlg._sandbox_unlock()
assert dlg._sandbox_unlocked is False
dlg.deleteLater()
def test_go_bua_khi_chua_dat_mat_khau_cung_khong_mo_duoc(qapp, shown): # ---- hành vi mới: sửa được ngay, không cần mật khẩu ----------------------
"""Mật khẩu lưu rỗng thì KHÔNG chuỗi nào mở được, kể cả chuỗi khác rỗng."""
dlg = _dialog("")
dlg.sandbox_pw_edit.setText("bat ky")
dlg._sandbox_unlock() @pytest.mark.parametrize("ten", _CONG_TAC)
def test_cong_tac_sua_duoc_ngay_khi_mo_hop_thoai(qapp, ten):
"""Đây là chính yêu cầu: không còn bước nhập mật khẩu nào chắn ở giữa."""
dlg = _dialog()
assert dlg._sandbox_unlocked is False assert getattr(dlg, ten).isEnabled() is True, f"{ten} vẫn bị khoá"
dlg.deleteLater()
def test_mat_khau_sai_khong_mo_duoc(qapp, shown): @pytest.mark.parametrize("ten", _CONG_TAC)
"""Đã đặt mật khẩu thì gõ sai vẫn khoá.""" def test_bat_tat_duoc_va_luu_dung_gia_tri(qapp, ten):
dlg = _dialog("K7MNP2QRSTVW") """Bật/tắt phải ăn vào widget — khoá cũ chặn đúng ở bước này."""
dlg.sandbox_pw_edit.setText("K7MNP2QRSTVX") dlg = _dialog()
w = getattr(dlg, ten)
dlg._sandbox_unlock() truoc = w.isChecked()
w.setChecked(not truoc)
assert dlg._sandbox_unlocked is False assert w.isChecked() is (not truoc)
dlg.deleteLater() w.setChecked(truoc)
assert w.isChecked() is truoc
# ---- thông báo phải phân biệt được hai tình huống ------------------------- def test_khong_con_widget_mat_khau_nao(qapp):
"""Ô nhập, nút Mở khoá và nhãn "Đang khoá" phải biến mất khỏi hộp thoại."""
dlg = _dialog()
def test_chua_cau_hinh_bao_khac_voi_sai_mat_khau(qapp, shown): for ten in ("sandbox_pw_edit", "sandbox_unlock_btn", "sandbox_locked_status",
"""Hai nhánh phải nói hai chuyện khác nhau. "sandbox_pw_label"):
assert not hasattr(dlg, ten), f"{ten} vẫn còn trên hộp thoại"
Người chưa từng đặt mật khẩu mà nhận "Password incorrect" sẽ gõ lại mãi một
thứ không tồn tại. Không có bài này thì gộp hai nhánh về một thông báo chung
vẫn xanh hết.
"""
from cowork_local.i18n import tr
dlg = _dialog("")
dlg.sandbox_pw_edit.setText("")
dlg._sandbox_unlock()
chua_cau_hinh = list(shown)
dlg.deleteLater()
shown.clear()
dlg2 = _dialog("K7MNP2QRSTVW")
dlg2.sandbox_pw_edit.setText("sai roi")
dlg2._sandbox_unlock()
sai_mat_khau = list(shown)
dlg2.deleteLater()
assert len(chua_cau_hinh) == 1, "phải hiện đúng một thông báo"
assert len(sai_mat_khau) == 1
assert chua_cau_hinh[0][2] == tr("settings.sandbox_pw_unset_body")
assert chua_cau_hinh[0][2] != sai_mat_khau[0][2], (
"chưa cấu hình mật khẩu và sai mật khẩu phải là hai thông báo khác nhau")
# ---- đường đi đúng vẫn phải chạy ---------------------------------------- def test_khong_con_duong_mo_khoa_trong_ma(qapp):
"""Hàm mở khoá và cờ trạng thái khoá không còn tồn tại."""
import cowork_local.ui.settings_dialog as mod
def test_mat_khau_dung_van_mo_duoc(qapp, shown): dlg = _dialog()
"""Bản vá không được phá đường đi hợp lệ.""" assert not hasattr(dlg, "_sandbox_unlock")
dlg = _dialog("K7MNP2QRSTVW") assert not hasattr(dlg, "_sandbox_unlocked")
dlg.sandbox_pw_edit.setText("K7MNP2QRSTVW") assert not hasattr(dlg, "_sandbox_widgets")
assert not hasattr(mod, "_sandbox_password_matches")
dlg._sandbox_unlock()
assert dlg._sandbox_unlocked is True
dlg.deleteLater()
@pytest.mark.parametrize("pw", ["mật khẩu", "パスワード", "sénhà-2026"]) # ---- guardrail: không ai khoá lại mà không sửa bài test này --------------
def test_mat_khau_co_dau_khong_lam_crash(qapp, shown, pw):
"""``secrets.compare_digest`` ném TypeError nếu str có ký tự ngoài ASCII.
App mặc định tiếng Việt và phục vụ khách Nhật, nên chữ có dấu trong ô mật def test_ma_nguon_khong_con_khoa_nhom_sandbox():
khẩu là input bình thường. Phải so sánh trên bytes. """Chặn cả lớp lỗi: lần sau ai thêm lại ``setEnabled(False)`` cho nhóm này
""" thì bài này đỏ ngay, không đợi có người mở app mới thấy."""
dlg = _dialog(pw) src = (Path(__file__).resolve().parents[2]
dlg.sandbox_pw_edit.setText(pw) / "ui" / "settings_dialog.py").read_text(encoding="utf-8")
code = "\n".join(l for l in src.splitlines() if not l.strip().startswith("#"))
dlg._sandbox_unlock() # không được ném TypeError for dau_hieu in ("_sandbox_unlock", "_sandbox_widgets", "_sandbox_unlocked",
"sandbox_pw"):
assert dlg._sandbox_unlocked is True assert dau_hieu not in code, f"khoá sandbox đã quay lại: {dau_hieu}"
dlg.deleteLater()
def test_mat_khau_co_dau_sai_thi_van_khoa(qapp, shown): def test_phep_quet_thuc_su_doc_duoc_file():
"""Chữ có dấu không được biến thành đường mở khoá dễ dãi.""" """Lưới an toàn: đổi tên file làm bài trên quét rỗng mà vẫn xanh."""
dlg = _dialog("mật khẩu") src = (Path(__file__).resolve().parents[2]
dlg.sandbox_pw_edit.setText("mat khau") / "ui" / "settings_dialog.py").read_text(encoding="utf-8")
dlg._sandbox_unlock() assert "class SettingsDialog" in src
assert len(src) > 2000, f"chỉ đọc được {len(src)} ký tự — đường dẫn đã hỏng"
assert dlg._sandbox_unlocked is False
dlg.deleteLater()
# ---- hàm so khớp, gọi thẳng ----------------------------------------------
@pytest.mark.parametrize("entered,stored,expected", [
("", "", False), # cả hai rỗng
("", "K7MNP2QRSTVW", False), # ô nhập rỗng
("K7MNP2QRSTVW", "", False), # chưa đặt mật khẩu — nhánh phòng thủ
("K7MNP2QRSTVW", "K7MNP2QRSTVW", True),
("mật khẩu", "mật khẩu", True), # ngoài ASCII
("mật khẩu", "mat khau", False),
])
def test_ham_so_khop(entered, stored, expected):
"""Gọi thẳng ``_sandbox_password_matches`` — phủ cả nhánh mà call site đã
chặn trước bằng return sớm."""
from cowork_local.ui.settings_dialog import _sandbox_password_matches
assert _sandbox_password_matches(entered, stored) is expected
# ---- chặn cả lớp lỗi -----------------------------------------------------
#: ``.get("<khoá kiểu credential>", "<literal khác rỗng>")`` — mặc định trông có
#: vẻ an toàn nhưng thực ra là credential nằm trong mã nguồn. Nó cũng là code
#: chết: cấu hình đã deep-merge với DEFAULT_CONFIG nên key luôn tồn tại.
#:
#: Cố ý KHÔNG bắt ``key`` và ``code`` trần: ``it.get("key", "?")`` của Jira
#: (``core/jira_tool.py``) là mã issue, không phải credential. Danh sách dưới đây
#: chỉ gồm tên đã mang nghĩa bí mật.
_CREDENTIAL_FALLBACK = re.compile(
r'\.get\(\s*["\'][a-z_]*'
r'(?:pw|passwd|password|secret|token|api_key|unlock_code|access_code)'
r'[a-z_]*["\']\s*,\s*["\'][^"\']+["\']'
)
#: Quét CHÉO mọi thư mục nguồn, không chỉ tầng giao diện. Sai sót gốc của commit
#: ``3827552`` là sửa ``config.py`` mà quên bản sao trong ``ui/`` — tức là lỗi đi
#: xuyên thư mục, nên phép quét cũng phải đi xuyên thư mục.
_SCANNED = (
"ui", "presentation", "core", "infrastructure", "application", "domain",
"mcp_servers", "providers", "security", "theme", "config.py", "state.py",
)
def test_khong_con_fallback_credential_trong_ma_nguon():
"""Không file nguồn nào được đặt credential làm giá trị mặc định của ``.get()``."""
root = Path(__file__).resolve().parents[2]
offenders = []
for name in _SCANNED:
target = root / name
if target.is_file():
files = [target]
elif target.is_dir():
files = [p for p in target.rglob("*.py") if "__pycache__" not in p.parts]
else: # thư mục bị đổi tên/xoá
continue
for path in files:
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if _CREDENTIAL_FALLBACK.search(line):
offenders.append(
f"{path.relative_to(root).as_posix()}:{lineno}: {line.strip()}")
assert not offenders, "credential nằm trong mã nguồn:\n " + "\n ".join(offenders)
def test_phep_quet_thuc_su_nhin_thay_file():
"""Lưới an toàn cho bài trên: đổi tên thư mục làm nó quét rỗng mà vẫn xanh."""
root = Path(__file__).resolve().parents[2]
seen = sum(
1 for name in _SCANNED
for _ in ([root / name] if (root / name).is_file()
else (root / name).rglob("*.py") if (root / name).is_dir() else [])
)
assert seen > 200, f"chỉ quét được {seen} file — phạm vi quét đã hỏng"
+1 -1
View File
@@ -26,7 +26,7 @@ from capture_screens import _apply_theme, _isolate_home, _load_fonts # noqa: E4
SETTINGS_FIELDS = [ SETTINGS_FIELDS = [
"language_combo", "theme_combo", "tray_chk", "notify_chk", "language_combo", "theme_combo", "tray_chk", "notify_chk",
"provider_combo", "prov_base", "prov_key", "prov_model", "provider_combo", "prov_base", "prov_key", "prov_model",
"sandbox_pw_edit", "sandbox_unlock_btn", "sandbox_confirm", "sandbox_confirm",
"sandbox_block_network", "sec_enabled", "ai_check", "sandbox_block_network", "sec_enabled", "ai_check",
] ]
TASK_FIELDS = [ TASK_FIELDS = [
+6 -83
View File
@@ -13,20 +13,17 @@ chưa từng được gán nên gọi vào là AttributeError.
""" """
from __future__ import annotations from __future__ import annotations
import secrets
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtGui import QGuiApplication from PySide6.QtGui import QGuiApplication
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout,
QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QGroupBox, QHBoxLayout, QListWidget, QListWidgetItem,
QMessageBox, QPushButton, QScrollArea, QSpinBox, QScrollArea, QSpinBox,
QTreeWidgetItem, QVBoxLayout, QWidget, QTreeWidgetItem, QVBoxLayout, QWidget,
) )
from ..i18n import tr from ..i18n import tr
from .dialog_buttons import dialog_buttons from .dialog_buttons import dialog_buttons
from .icons import IconLabel
from .widgets import ToggleSwitch from .widgets import ToggleSwitch
@@ -37,25 +34,6 @@ from ..presentation.settings.routing_settings_widget import RoutingSettingsWidge
from ..presentation.settings.about_widget import AboutSettingsWidget 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): 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 """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). (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")) self.sandbox_group = QGroupBox(tr("settings.group.sandbox"))
sbl = QVBoxLayout(self.sandbox_group) sbl = QVBoxLayout(self.sandbox_group)
# --- Password protection for Sandbox Security (at top) --- # 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
self.sandbox_pw_label = IconLabel("lock", tr("settings.sandbox_pw_label")) # tự do. Khoá cũ chỉ là rào chống bấm nhầm ở phía giao diện, không phải
sbl.addWidget(self.sandbox_pw_label) # 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ì.
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)
self.sandbox_confirm = ToggleSwitch(tr("settings.sandbox_confirm_commands")) self.sandbox_confirm = ToggleSwitch(tr("settings.sandbox_confirm_commands"))
self.sandbox_confirm.setChecked(bool(sec.get("cowork_confirm_commands", False))) self.sandbox_confirm.setChecked(bool(sec.get("cowork_confirm_commands", False)))
self.sandbox_confirm.setToolTip(tr("settings.sandbox_confirm_commands_tooltip")) 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 # Resource limits (CPU/Memory/Disk I/O) moved to the Parameter group
# below — see _param_section("settings.group.sandbox_limits"). # 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) root.addWidget(self.sandbox_group)
# Connectors (MCP / REST API) are managed entirely in Monitoring → Tools # 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: def _save(self) -> None:
"""Gom cấu hình từ mọi trang con rồi ghi xuống đĩa.""" """Gom cấu hình từ mọi trang con rồi ghi xuống đĩa."""
data = self.ctx.config.data data = self.ctx.config.data