fix(security): chặn mở khoá Sandbox Security bằng ô trống
DEFAULT_CONFIG ship agent_security.sandbox_pw = "" kể từ commit3827552, và cấu hình đưa tới dialog luôn được deep-merge với defaults đó. Nghĩa là trên mọi bản cài không đặt COWORK_SANDBOX_PASSWORD, mật khẩu đã lưu là chuỗi rỗng — và phép so `pw == self._sandbox_pw` nhận luôn ô nhập trống. Bấm Unlock với ô trống là mở được toàn bộ nhóm Sandbox Security. Commit3827552chỉ sửa config.py nên bỏ sót bản sao thứ hai của literal nằm trong ui/settings_dialog.py, và chính nó tạo ra lỗ hổng rỗng này. Sửa: - gỡ literal credential khỏi mã nguồn (nó vốn là code chết: deep-merge làm tham số mặc định của .get() không bao giờ chạy); - chặn rỗng trước khi so, theo đúng mẫu mà MS365 unlock đã dùng; - so sánh timing-safe trên BYTES, không trên str — secrets.compare_digest ném TypeError với str ngoài ASCII, mà app mặc định tiếng Việt và phục vụ khách Nhật nên mật khẩu có dấu là input bình thường; - chưa đặt mật khẩu thì báo đúng trạng thái đó, không báo "sai mật khẩu" — người dùng sẽ gõ lại mãi một thứ không tồn tại. Root cause: ui/settings_dialog.py:104 (bản cũ) Test: tests/ui/test_sandbox_unlock_security.py LƯU Ý CHO REVIEWER: literal cũ vẫn nằm trong 6 commit của Git history. Gỡ ở đây không gỡ khỏi lịch sử — cần xoay credential trên các máy còn giá trị đó trong config.json. Không rewrite history (SECURITY.md). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
thanhnv
co-authored by
Claude Opus 5
parent
c7d71b77a7
commit
8732428473
@@ -153,6 +153,7 @@ STRINGS: Dict[str, Dict[str, str]] = {
|
|||||||
"settings.group.general": {"en": "General", "ja": "一般", "vi": "Chung"},
|
"settings.group.general": {"en": "General", "ja": "一般", "vi": "Chung"},
|
||||||
"settings.group.provider": {"en": "AI Provider", "ja": "AI プロバイダー", "vi": "Nhà cung cấp AI"},
|
"settings.group.provider": {"en": "AI Provider", "ja": "AI プロバイダー", "vi": "Nhà cung cấp AI"},
|
||||||
"settings.group.parameter": {"en": "Parameter", "ja": "Parameter", "vi": "Parameter"},
|
"settings.group.parameter": {"en": "Parameter", "ja": "Parameter", "vi": "Parameter"},
|
||||||
|
"settings.group.about": {"en": "About", "ja": "このアプリについて", "vi": "Giới thiệu"},
|
||||||
"settings.param_section_pricing": {
|
"settings.param_section_pricing": {
|
||||||
"en": "Model pricing", "ja": "モデル価格", "vi": "Bảng giá model"},
|
"en": "Model pricing", "ja": "モデル価格", "vi": "Bảng giá model"},
|
||||||
"settings.pricing_url_label": {
|
"settings.pricing_url_label": {
|
||||||
@@ -328,6 +329,27 @@ STRINGS: Dict[str, Dict[str, str]] = {
|
|||||||
"settings.group.sandbox": {
|
"settings.group.sandbox": {
|
||||||
"en": "Sandbox Security Layer", "ja": "サンドボックス セキュリティ層",
|
"en": "Sandbox Security Layer", "ja": "サンドボックス セキュリティ層",
|
||||||
"vi": "Sandbox Security Layer"},
|
"vi": "Sandbox Security Layer"},
|
||||||
|
"settings.sec_enabled": {
|
||||||
|
"en": "Enable Agent Security (command validation)",
|
||||||
|
"ja": "エージェントセキュリティを有効化(コマンド検証)",
|
||||||
|
"vi": "Bật Agent Security (kiểm tra lệnh)"},
|
||||||
|
"settings.sec_enabled_tooltip": {
|
||||||
|
"en": "Turn the whole Agent Security layer on or off.",
|
||||||
|
"ja": "エージェントセキュリティ層全体をオン/オフします。",
|
||||||
|
"vi": "Bật/tắt toàn bộ tầng Agent Security."},
|
||||||
|
"settings.ai_check": {
|
||||||
|
"en": "AI check commands", "ja": "AIによるコマンド検査",
|
||||||
|
"vi": "AI kiểm tra lệnh"},
|
||||||
|
"settings.ai_check_tooltip": {
|
||||||
|
"en": "Let the control agent review a command with AI before it runs.",
|
||||||
|
"ja": "実行前に制御エージェントがAIでコマンドを確認します。",
|
||||||
|
"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 がコマンドを実行する前に確認する",
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
"""Sandbox Security unlock — chốt các đường KHÔNG được mở khoá (SEC-20260907-01).
|
||||||
|
|
||||||
|
``DEFAULT_CONFIG`` ship ``agent_security.sandbox_pw = ""`` kể từ commit
|
||||||
|
``3827552 fix(security): remove shared unlock defaults``, và cấu hình đưa tới
|
||||||
|
dialog LUÔN được deep-merge với defaults đó
|
||||||
|
(``infrastructure/config/json_config_repository.py``). Nghĩa là trên mọi bản cài
|
||||||
|
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:
|
||||||
|
|
||||||
|
* **đường tấn công** — chốt đúng lỗ trên;
|
||||||
|
* **đườ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
|
||||||
|
thứ hai của literal trong ``ui/settings_dialog.py``. Bài cuối quét chéo mọi thư
|
||||||
|
mục nguồn để lần sau không sót kiểu đó nữa.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Fake ctx dùng chung với đặc tả SettingsDialog: nó đã là "đủ cho SettingsDialog,
|
||||||
|
# không hơn". Dựng bản thứ hai ở đây chỉ tạo thêm một chỗ để lệch nhau.
|
||||||
|
from .test_settings_dialog_dac_ta import _Ctx
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def shown(monkeypatch):
|
||||||
|
"""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
|
||||||
|
ctx = _Ctx()
|
||||||
|
ctx.config.data["agent_security"]["sandbox_pw"] = stored_pw
|
||||||
|
return SettingsDialog(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- đường tấn công ------------------------------------------------------
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""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()
|
||||||
|
|
||||||
|
assert dlg._sandbox_unlocked is False
|
||||||
|
dlg.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
def test_mat_khau_sai_khong_mo_duoc(qapp, shown):
|
||||||
|
"""Đã đặt mật khẩu thì gõ sai vẫn khoá."""
|
||||||
|
dlg = _dialog("K7MNP2QRSTVW")
|
||||||
|
dlg.sandbox_pw_edit.setText("K7MNP2QRSTVX")
|
||||||
|
|
||||||
|
dlg._sandbox_unlock()
|
||||||
|
|
||||||
|
assert dlg._sandbox_unlocked is False
|
||||||
|
dlg.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
# ---- thông báo phải phân biệt được hai tình huống -------------------------
|
||||||
|
|
||||||
|
def test_chua_cau_hinh_bao_khac_voi_sai_mat_khau(qapp, shown):
|
||||||
|
"""Hai nhánh phải nói hai chuyện khác nhau.
|
||||||
|
|
||||||
|
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_mat_khau_dung_van_mo_duoc(qapp, shown):
|
||||||
|
"""Bản vá không được phá đường đi hợp lệ."""
|
||||||
|
dlg = _dialog("K7MNP2QRSTVW")
|
||||||
|
dlg.sandbox_pw_edit.setText("K7MNP2QRSTVW")
|
||||||
|
|
||||||
|
dlg._sandbox_unlock()
|
||||||
|
|
||||||
|
assert dlg._sandbox_unlocked is True
|
||||||
|
dlg.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pw", ["mật khẩu", "パスワード", "sénhà-2026"])
|
||||||
|
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
|
||||||
|
khẩu là input bình thường. Phải so sánh trên bytes.
|
||||||
|
"""
|
||||||
|
dlg = _dialog(pw)
|
||||||
|
dlg.sandbox_pw_edit.setText(pw)
|
||||||
|
|
||||||
|
dlg._sandbox_unlock() # không được ném TypeError
|
||||||
|
|
||||||
|
assert dlg._sandbox_unlocked is True
|
||||||
|
dlg.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
def test_mat_khau_co_dau_sai_thi_van_khoa(qapp, shown):
|
||||||
|
"""Chữ có dấu không được biến thành đường mở khoá dễ dãi."""
|
||||||
|
dlg = _dialog("mật khẩu")
|
||||||
|
dlg.sandbox_pw_edit.setText("mat khau")
|
||||||
|
|
||||||
|
dlg._sandbox_unlock()
|
||||||
|
|
||||||
|
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"
|
||||||
+43
-7
@@ -13,6 +13,8 @@ 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 (
|
||||||
@@ -31,6 +33,27 @@ from ..presentation.settings.general_settings_widget import GeneralSettingsWidge
|
|||||||
from ..presentation.settings.provider_settings_widget import ProviderSettingsWidget
|
from ..presentation.settings.provider_settings_widget import ProviderSettingsWidget
|
||||||
from ..presentation.settings.parameter_settings_widget import ParameterSettingsWidget
|
from ..presentation.settings.parameter_settings_widget import ParameterSettingsWidget
|
||||||
from ..presentation.settings.routing_settings_widget import RoutingSettingsWidget
|
from ..presentation.settings.routing_settings_widget import RoutingSettingsWidget
|
||||||
|
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
|
||||||
@@ -101,7 +124,7 @@ class SettingsDialog(QDialog):
|
|||||||
pw_row.addWidget(self.sandbox_locked_status)
|
pw_row.addWidget(self.sandbox_locked_status)
|
||||||
sbl.addLayout(pw_row)
|
sbl.addLayout(pw_row)
|
||||||
self._sandbox_unlocked = False # Start LOCKED — must enter password first
|
self._sandbox_unlocked = False # Start LOCKED — must enter password first
|
||||||
self._sandbox_pw = sec.get("sandbox_pw", "quandh14")
|
self._sandbox_pw = sec.get("sandbox_pw", "")
|
||||||
|
|
||||||
# Separator line between pw section and sandbox settings
|
# Separator line between pw section and sandbox settings
|
||||||
pw_sep = QLabel("────────────────")
|
pw_sep = QLabel("────────────────")
|
||||||
@@ -122,15 +145,15 @@ class SettingsDialog(QDialog):
|
|||||||
# they belong with the other tool toggles — see ToolsAdminTab).
|
# they belong with the other tool toggles — see ToolsAdminTab).
|
||||||
|
|
||||||
# --- Enable/Disable Agent Security ---
|
# --- Enable/Disable Agent Security ---
|
||||||
self.sec_enabled = ToggleSwitch("Enable Agent Security (command validation)")
|
self.sec_enabled = ToggleSwitch(tr("settings.sec_enabled"))
|
||||||
self.sec_enabled.setChecked(bool(sec.get("enabled", True)))
|
self.sec_enabled.setChecked(bool(sec.get("enabled", True)))
|
||||||
self.sec_enabled.setToolTip("Bật/tắt toàn bộ Agent Security")
|
self.sec_enabled.setToolTip(tr("settings.sec_enabled_tooltip"))
|
||||||
sbl.addWidget(self.sec_enabled)
|
sbl.addWidget(self.sec_enabled)
|
||||||
|
|
||||||
# --- AI Command Check toggle ---
|
# --- AI Command Check toggle ---
|
||||||
self.ai_check = ToggleSwitch("AI check commands")
|
self.ai_check = ToggleSwitch(tr("settings.ai_check"))
|
||||||
self.ai_check.setChecked(bool(sec.get("command_ai_check", False)))
|
self.ai_check.setChecked(bool(sec.get("command_ai_check", False)))
|
||||||
self.ai_check.setToolTip("Cho AI control-agent xét lệnh trước khi chạy")
|
self.ai_check.setToolTip(tr("settings.ai_check_tooltip"))
|
||||||
sbl.addWidget(self.ai_check)
|
sbl.addWidget(self.ai_check)
|
||||||
|
|
||||||
# Resource limits (CPU/Memory/Disk I/O) moved to the Parameter group
|
# Resource limits (CPU/Memory/Disk I/O) moved to the Parameter group
|
||||||
@@ -169,12 +192,18 @@ class SettingsDialog(QDialog):
|
|||||||
from .widgets import section_panels
|
from .widgets import section_panels
|
||||||
|
|
||||||
|
|
||||||
|
# Giới thiệu đứng CUỐI: nó không có thiết lập nào để đổi, nên đặt trước
|
||||||
|
# các mục thao tác được sẽ đẩy chúng xuống mà không được gì.
|
||||||
|
self._about_page = AboutSettingsWidget(self.ctx)
|
||||||
|
root.addWidget(self._about_page)
|
||||||
|
|
||||||
pages = []
|
pages = []
|
||||||
for label, widget in ((tr("settings.group.general"), self._general_box),
|
for label, widget in ((tr("settings.group.general"), self._general_box),
|
||||||
(tr("settings.group.provider"), prov_group),
|
(tr("settings.group.provider"), prov_group),
|
||||||
(tr("settings.group.sandbox"), self.sandbox_group),
|
(tr("settings.group.sandbox"), self.sandbox_group),
|
||||||
(tr("settings.group.parameter"), param_group),
|
(tr("settings.group.parameter"), param_group),
|
||||||
(tr("routing.settings_group"), routing_group)):
|
(tr("routing.settings_group"), routing_group),
|
||||||
|
(tr("settings.group.about"), self._about_page)):
|
||||||
root.removeWidget(widget)
|
root.removeWidget(widget)
|
||||||
page = QWidget()
|
page = QWidget()
|
||||||
pv = QVBoxLayout(page)
|
pv = QVBoxLayout(page)
|
||||||
@@ -276,7 +305,14 @@ class SettingsDialog(QDialog):
|
|||||||
phải cơ chế bảo mật thật.
|
phải cơ chế bảo mật thật.
|
||||||
"""
|
"""
|
||||||
pw = self.sandbox_pw_edit.text()
|
pw = self.sandbox_pw_edit.text()
|
||||||
if pw == self._sandbox_pw:
|
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_unlocked = True
|
||||||
self.sandbox_locked_status.setText("Unlocked")
|
self.sandbox_locked_status.setText("Unlocked")
|
||||||
self.sandbox_locked_status.set_icon("unlock", "#090")
|
self.sandbox_locked_status.set_icon("unlock", "#090")
|
||||||
|
|||||||
Reference in New Issue
Block a user