feat(sandbox): macOS host sandbox deny-list for sensitive paths
- Add default deny_read_paths/deny_read_regexes to host_sandbox_policy.json - Generate deny rules for ~/.ssh, ~/.aws, ~/.kube, keychains, .env, etc. - Readonly sandbox: global read + deny sensitive + write only /dev/null - Workspace-write sandbox: global read + deny sensitive + writable paths - Keep workspace files readable by re-allowing workspace subpath after deny - Backend API validates path authorization against deny list - Path auth dialog hint explains readable paths behavior
This commit is contained in:
parent
15106e7d00
commit
3db4e0beb4
@ -1,4 +1,28 @@
|
|||||||
{
|
{
|
||||||
"macos_writable_paths": [],
|
"macos_writable_paths": [],
|
||||||
"macos_readable_extra_paths": []
|
"macos_readable_extra_paths": [],
|
||||||
|
"macos_deny_read_paths": [
|
||||||
|
"~/.ssh",
|
||||||
|
"~/.aws",
|
||||||
|
"~/.azure",
|
||||||
|
"~/.gcp",
|
||||||
|
"~/.google",
|
||||||
|
"~/.kube",
|
||||||
|
"~/.docker",
|
||||||
|
"~/.gnupg",
|
||||||
|
"~/.npmrc",
|
||||||
|
"~/.netrc",
|
||||||
|
"~/.pypirc",
|
||||||
|
"~/.git-credentials",
|
||||||
|
"~/.bash_history",
|
||||||
|
"~/.zsh_history",
|
||||||
|
"~/.psql_history",
|
||||||
|
"~/.mysql_history",
|
||||||
|
"~/.pgpass",
|
||||||
|
"/Library/Keychains",
|
||||||
|
"~/Library/Keychains"
|
||||||
|
],
|
||||||
|
"macos_deny_read_regexes": [
|
||||||
|
"^/.*\\.env(\\.[^/]*)?$"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,17 +7,55 @@ from typing import Dict, List
|
|||||||
|
|
||||||
from config import HOST_SANDBOX_MACOS_WRITABLE_PATHS, deploy_config_path
|
from config import HOST_SANDBOX_MACOS_WRITABLE_PATHS, deploy_config_path
|
||||||
|
|
||||||
|
|
||||||
|
# macOS 默认拒绝读取的敏感路径(支持 ~/ 前缀)。
|
||||||
|
DEFAULT_MACOS_DENY_READ_PATHS = [
|
||||||
|
"~/.ssh",
|
||||||
|
"~/.aws",
|
||||||
|
"~/.azure",
|
||||||
|
"~/.gcp",
|
||||||
|
"~/.google",
|
||||||
|
"~/.kube",
|
||||||
|
"~/.docker",
|
||||||
|
"~/.gnupg",
|
||||||
|
"~/.npmrc",
|
||||||
|
"~/.netrc",
|
||||||
|
"~/.pypirc",
|
||||||
|
"~/.git-credentials",
|
||||||
|
"~/.bash_history",
|
||||||
|
"~/.zsh_history",
|
||||||
|
"~/.psql_history",
|
||||||
|
"~/.mysql_history",
|
||||||
|
"~/.pgpass",
|
||||||
|
"/Library/Keychains",
|
||||||
|
"~/Library/Keychains",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 默认按正则拒绝读取的敏感文件(可匹配文件系统中任意位置)。
|
||||||
|
DEFAULT_MACOS_DENY_READ_REGEXES = [
|
||||||
|
r"^/.*\.env(\.[^/]*)?$",
|
||||||
|
]
|
||||||
|
|
||||||
_LOCK = threading.Lock()
|
_LOCK = threading.Lock()
|
||||||
# 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config
|
# 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config
|
||||||
_POLICY_PATH = Path(deploy_config_path("host_sandbox_policy.json"))
|
_POLICY_PATH = Path(deploy_config_path("host_sandbox_policy.json"))
|
||||||
|
|
||||||
|
|
||||||
|
def _default_policy() -> Dict:
|
||||||
|
return {
|
||||||
|
"macos_writable_paths": [],
|
||||||
|
"macos_readable_extra_paths": [],
|
||||||
|
"macos_deny_read_paths": list(DEFAULT_MACOS_DENY_READ_PATHS),
|
||||||
|
"macos_deny_read_regexes": list(DEFAULT_MACOS_DENY_READ_REGEXES),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_file() -> None:
|
def _ensure_file() -> None:
|
||||||
if _POLICY_PATH.exists():
|
if _POLICY_PATH.exists():
|
||||||
return
|
return
|
||||||
_POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
_POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
_POLICY_PATH.write_text(
|
_POLICY_PATH.write_text(
|
||||||
json.dumps({"macos_writable_paths": [], "macos_readable_extra_paths": []}, ensure_ascii=False, indent=2),
|
json.dumps(_default_policy(), ensure_ascii=False, indent=2),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,30 +66,38 @@ def load_policy() -> Dict:
|
|||||||
try:
|
try:
|
||||||
data = json.loads(_POLICY_PATH.read_text(encoding="utf-8"))
|
data = json.loads(_POLICY_PATH.read_text(encoding="utf-8"))
|
||||||
except Exception:
|
except Exception:
|
||||||
data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
|
data = _default_policy()
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
|
data = _default_policy()
|
||||||
writable_items = data.get("macos_writable_paths")
|
|
||||||
if not isinstance(writable_items, list):
|
defaults = _default_policy()
|
||||||
writable_items = []
|
needs_save = False
|
||||||
readable_items = data.get("macos_readable_extra_paths")
|
for key in defaults:
|
||||||
if not isinstance(readable_items, list):
|
if key not in data or not isinstance(data.get(key), list):
|
||||||
readable_items = []
|
data[key] = list(defaults[key])
|
||||||
data["macos_writable_paths"] = [str(x).strip() for x in writable_items if str(x).strip()]
|
needs_save = True
|
||||||
data["macos_readable_extra_paths"] = [str(x).strip() for x in readable_items if str(x).strip()]
|
|
||||||
|
data["macos_writable_paths"] = [str(x).strip() for x in data["macos_writable_paths"] if str(x).strip()]
|
||||||
|
data["macos_readable_extra_paths"] = [str(x).strip() for x in data["macos_readable_extra_paths"] if str(x).strip()]
|
||||||
|
data["macos_deny_read_paths"] = [str(x).strip() for x in data["macos_deny_read_paths"] if str(x).strip()]
|
||||||
|
data["macos_deny_read_regexes"] = [str(x).strip() for x in data["macos_deny_read_regexes"] if str(x).strip()]
|
||||||
|
|
||||||
|
if needs_save:
|
||||||
|
try:
|
||||||
|
_POLICY_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def save_policy(policy: Dict) -> Dict:
|
def save_policy(policy: Dict) -> Dict:
|
||||||
payload = dict(policy or {})
|
payload = dict(policy or {})
|
||||||
writable_items = payload.get("macos_writable_paths")
|
defaults = _default_policy()
|
||||||
if not isinstance(writable_items, list):
|
for key in defaults:
|
||||||
writable_items = []
|
items = payload.get(key)
|
||||||
readable_items = payload.get("macos_readable_extra_paths")
|
if not isinstance(items, list):
|
||||||
if not isinstance(readable_items, list):
|
items = list(defaults[key])
|
||||||
readable_items = []
|
payload[key] = [str(x).strip() for x in items if str(x).strip()]
|
||||||
payload["macos_writable_paths"] = [str(x).strip() for x in writable_items if str(x).strip()]
|
|
||||||
payload["macos_readable_extra_paths"] = [str(x).strip() for x in readable_items if str(x).strip()]
|
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
_ensure_file()
|
_ensure_file()
|
||||||
_POLICY_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
_POLICY_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
@ -77,3 +123,25 @@ def get_macos_readable_paths() -> List[str]:
|
|||||||
if val and val not in merged:
|
if val and val not in merged:
|
||||||
merged.append(val)
|
merged.append(val)
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def get_macos_deny_read_paths() -> List[str]:
|
||||||
|
data = load_policy()
|
||||||
|
paths = data.get("macos_deny_read_paths", [])
|
||||||
|
merged: List[str] = []
|
||||||
|
for raw in list(DEFAULT_MACOS_DENY_READ_PATHS) + list(paths or []):
|
||||||
|
val = str(raw).strip()
|
||||||
|
if val and val not in merged:
|
||||||
|
merged.append(val)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def get_macos_deny_read_regexes() -> List[str]:
|
||||||
|
data = load_policy()
|
||||||
|
patterns = data.get("macos_deny_read_regexes", [])
|
||||||
|
merged: List[str] = []
|
||||||
|
for raw in list(DEFAULT_MACOS_DENY_READ_REGEXES) + list(patterns or []):
|
||||||
|
val = str(raw).strip()
|
||||||
|
if val and val not in merged:
|
||||||
|
merged.append(val)
|
||||||
|
return merged
|
||||||
|
|||||||
@ -7,7 +7,11 @@ import shlex
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from modules.host_sandbox_policy import get_macos_writable_paths
|
from modules.host_sandbox_policy import (
|
||||||
|
get_macos_writable_paths,
|
||||||
|
get_macos_deny_read_paths,
|
||||||
|
get_macos_deny_read_regexes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -22,6 +26,84 @@ class HostSandboxError(RuntimeError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# macOS 最小可读系统路径集合(Codex 风格 deny-default + allow-list)。
|
||||||
|
# 当前仅作为常量保留,供后续需要严格 allow-list 的只读沙箱模式使用。
|
||||||
|
# 当前只读沙箱采用“全局可读 + 敏感路径拒绝”模型,以保证工具兼容性。
|
||||||
|
MACOS_MINIMAL_READABLE_PATHS = [
|
||||||
|
"/bin",
|
||||||
|
"/sbin",
|
||||||
|
"/usr/bin",
|
||||||
|
"/usr/sbin",
|
||||||
|
"/usr/libexec",
|
||||||
|
"/usr/lib",
|
||||||
|
"/usr/share",
|
||||||
|
"/usr/local/lib",
|
||||||
|
"/opt/homebrew/lib",
|
||||||
|
"/lib",
|
||||||
|
"/etc",
|
||||||
|
"/private/etc",
|
||||||
|
"/tmp",
|
||||||
|
"/private/tmp",
|
||||||
|
"/var/tmp",
|
||||||
|
"/private/var/tmp",
|
||||||
|
"/var",
|
||||||
|
"/private/var",
|
||||||
|
"/dev",
|
||||||
|
"/System/Library/Frameworks",
|
||||||
|
"/System/Library/PrivateFrameworks",
|
||||||
|
"/System/Library/SubFrameworks",
|
||||||
|
"/System/Library/CoreServices",
|
||||||
|
"/System/Library/Extensions",
|
||||||
|
"/Library/Apple",
|
||||||
|
"/Library/Preferences",
|
||||||
|
"/Library/Filesystems/NetFSPlugins",
|
||||||
|
"/Applications",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_path(raw: str) -> Optional[str]:
|
||||||
|
"""展开路径中的 ~ 并返回绝对路径;无法展开时返回 None。"""
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
expanded = str(Path(raw).expanduser().resolve())
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return expanded
|
||||||
|
|
||||||
|
|
||||||
|
def _build_macos_read_rules(paths: List[str]) -> str:
|
||||||
|
"""把路径列表转成 (allow file-read* (subpath ...)) 规则段。"""
|
||||||
|
rules: List[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for raw in paths:
|
||||||
|
expanded = _expand_path(raw)
|
||||||
|
if expanded and expanded not in seen:
|
||||||
|
seen.add(expanded)
|
||||||
|
rules.append(f'(allow file-read* (subpath "{expanded}"))')
|
||||||
|
return "\n".join(rules)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_macos_deny_rules(paths: List[str]) -> str:
|
||||||
|
"""把路径列表转成 (deny file-read* (subpath ...)) 规则段。"""
|
||||||
|
rules: List[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for raw in paths:
|
||||||
|
expanded = _expand_path(raw)
|
||||||
|
if expanded and expanded not in seen:
|
||||||
|
seen.add(expanded)
|
||||||
|
rules.append(f'(deny file-read* (subpath "{expanded}"))')
|
||||||
|
return "\n".join(rules)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_macos_deny_regex_rules(patterns: List[str]) -> str:
|
||||||
|
"""把正则列表转成 (deny file-read* (regex #"...")) 规则段。"""
|
||||||
|
rules: List[str] = []
|
||||||
|
for pattern in patterns:
|
||||||
|
rules.append(f'(deny file-read* (regex #"{pattern}"))')
|
||||||
|
return "\n".join(rules)
|
||||||
|
|
||||||
|
|
||||||
# 宿主机网络权限档位
|
# 宿主机网络权限档位
|
||||||
NETWORK_PERMISSION_RESTRICTED = "restricted" # macOS: 仅本地回环;Linux/Windows: 暂不隔离
|
NETWORK_PERMISSION_RESTRICTED = "restricted" # macOS: 仅本地回环;Linux/Windows: 暂不隔离
|
||||||
NETWORK_PERMISSION_FULL = "full" # 完全开放
|
NETWORK_PERMISSION_FULL = "full" # 完全开放
|
||||||
@ -131,13 +213,24 @@ def _build_macos_readonly_plan(
|
|||||||
if not sandbox_exec:
|
if not sandbox_exec:
|
||||||
raise HostSandboxError("macOS 未找到 sandbox-exec,拒绝执行宿主机命令。")
|
raise HostSandboxError("macOS 未找到 sandbox-exec,拒绝执行宿主机命令。")
|
||||||
network_policy = _build_macos_network_policy(network_permission)
|
network_policy = _build_macos_network_policy(network_permission)
|
||||||
|
workspace = str(work_path.resolve())
|
||||||
|
|
||||||
|
# 只读沙箱:全局可读 + 敏感路径/文件拒绝,写权限仅 /dev/null。
|
||||||
|
# 工作区在 deny 规则之后再显式 allow,保证“工作区内不受沙箱影响”。
|
||||||
|
deny_rules = _build_macos_deny_rules(get_macos_deny_read_paths())
|
||||||
|
regex_rules = _build_macos_deny_regex_rules(get_macos_deny_read_regexes())
|
||||||
|
if regex_rules:
|
||||||
|
deny_rules += "\n" + regex_rules
|
||||||
|
|
||||||
profile = (
|
profile = (
|
||||||
'(version 1) '
|
'(version 1)\n'
|
||||||
'(deny default) '
|
'(deny default)\n'
|
||||||
'(allow sysctl-read) '
|
'(allow sysctl-read)\n'
|
||||||
'(allow process*) '
|
'(allow process*)\n'
|
||||||
f'{network_policy}'
|
f'{network_policy}'
|
||||||
'(allow file-read*) '
|
'(allow file-read*)\n'
|
||||||
|
f'{deny_rules}\n'
|
||||||
|
f'(allow file-read* (subpath "{workspace}"))\n'
|
||||||
'(allow file-write* (literal "/dev/null"))'
|
'(allow file-write* (literal "/dev/null"))'
|
||||||
)
|
)
|
||||||
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
|
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
|
||||||
@ -178,13 +271,20 @@ def _macos_profile_for_workspace(
|
|||||||
write_rules.append(f'(subpath "{entry}")')
|
write_rules.append(f'(subpath "{entry}")')
|
||||||
write_expr = " ".join(write_rules)
|
write_expr = " ".join(write_rules)
|
||||||
network_policy = _build_macos_network_policy(network_permission)
|
network_policy = _build_macos_network_policy(network_permission)
|
||||||
|
workspace = str(work_path.resolve())
|
||||||
|
deny_rules = _build_macos_deny_rules(get_macos_deny_read_paths())
|
||||||
|
regex_rules = _build_macos_deny_regex_rules(get_macos_deny_read_regexes())
|
||||||
|
if regex_rules:
|
||||||
|
deny_rules += "\n" + regex_rules
|
||||||
return (
|
return (
|
||||||
'(version 1) '
|
'(version 1)\n'
|
||||||
'(deny default) '
|
'(deny default)\n'
|
||||||
'(allow sysctl-read) '
|
'(allow sysctl-read)\n'
|
||||||
'(allow process*) '
|
'(allow process*)\n'
|
||||||
f'{network_policy}'
|
f'{network_policy}'
|
||||||
'(allow file-read*) '
|
'(allow file-read*)\n'
|
||||||
|
f'{deny_rules}\n'
|
||||||
|
f'(allow file-read* (subpath "{workspace}"))\n'
|
||||||
f'(allow file-write* {write_expr})'
|
f'(allow file-write* {write_expr})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,12 @@ from modules.skills_manager import (
|
|||||||
sync_workspace_skills,
|
sync_workspace_skills,
|
||||||
)
|
)
|
||||||
from modules.upload_security import UploadSecurityError
|
from modules.upload_security import UploadSecurityError
|
||||||
from modules.host_sandbox_policy import load_policy, save_policy
|
from modules.host_sandbox_policy import (
|
||||||
|
load_policy,
|
||||||
|
save_policy,
|
||||||
|
get_macos_deny_read_paths,
|
||||||
|
get_macos_deny_read_regexes,
|
||||||
|
)
|
||||||
from modules.user_manager import UserWorkspace
|
from modules.user_manager import UserWorkspace
|
||||||
from core.web_terminal import WebTerminal
|
from core.web_terminal import WebTerminal
|
||||||
from config.model_profiles import get_model_context_window
|
from config.model_profiles import get_model_context_window
|
||||||
@ -49,8 +54,30 @@ from server.state import tool_approval_manager, user_question_manager
|
|||||||
from server.extensions import socketio
|
from server.extensions import socketio
|
||||||
from server.monitor import get_cached_monitor_snapshot
|
from server.monitor import get_cached_monitor_snapshot
|
||||||
from server.files import sanitize_filename_preserve_unicode
|
from server.files import sanitize_filename_preserve_unicode
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
UPLOAD_FOLDER_NAME = ".agents/user_upload"
|
UPLOAD_FOLDER_NAME = ".agents/user_upload"
|
||||||
|
|
||||||
|
|
||||||
|
def _path_conflicts_with_deny_list(path: str) -> Optional[str]:
|
||||||
|
"""检查用户授权路径是否与内置 deny 列表冲突,返回错误信息或 None。"""
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
expanded = os.path.abspath(os.path.expanduser(path))
|
||||||
|
for deny_path in get_macos_deny_read_paths():
|
||||||
|
deny_expanded = os.path.abspath(os.path.expanduser(deny_path))
|
||||||
|
deny_lower = deny_expanded.lower().rstrip("/")
|
||||||
|
expanded_lower = expanded.lower().rstrip("/")
|
||||||
|
if expanded_lower == deny_lower or expanded_lower.startswith(deny_lower + "/"):
|
||||||
|
return f"禁止授权敏感路径: {path}"
|
||||||
|
for pattern in get_macos_deny_read_regexes():
|
||||||
|
try:
|
||||||
|
if re.search(pattern, expanded) or re.search(pattern, path):
|
||||||
|
return f"禁止授权敏感文件: {path}"
|
||||||
|
except re.error:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
@chat_bp.route('/api/permission-mode', methods=['GET'])
|
@chat_bp.route('/api/permission-mode', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@with_terminal
|
@with_terminal
|
||||||
@ -337,6 +364,8 @@ def get_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, user
|
|||||||
"enabled": can_manage,
|
"enabled": can_manage,
|
||||||
"writable_paths": data.get("macos_writable_paths", []),
|
"writable_paths": data.get("macos_writable_paths", []),
|
||||||
"readable_extra_paths": data.get("macos_readable_extra_paths", []),
|
"readable_extra_paths": data.get("macos_readable_extra_paths", []),
|
||||||
|
"deny_read_paths": data.get("macos_deny_read_paths", []),
|
||||||
|
"deny_read_regexes": data.get("macos_deny_read_regexes", []),
|
||||||
})
|
})
|
||||||
|
|
||||||
@chat_bp.route('/api/path-authorization', methods=['POST'])
|
@chat_bp.route('/api/path-authorization', methods=['POST'])
|
||||||
@ -357,11 +386,10 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
|
|||||||
readable_extra = [str(x).strip() for x in readable_items if str(x).strip()]
|
readable_extra = [str(x).strip() for x in readable_items if str(x).strip()]
|
||||||
if "/" in writable or "/" in readable_extra:
|
if "/" in writable or "/" in readable_extra:
|
||||||
return jsonify({"success": False, "error": "禁止授权根目录 /"}), 400
|
return jsonify({"success": False, "error": "禁止授权根目录 /"}), 400
|
||||||
blocked_prefix = ("~/.ssh", "~/.gnupg", "~/.aws")
|
|
||||||
for p in writable + readable_extra:
|
for p in writable + readable_extra:
|
||||||
lowered = p.lower()
|
conflict = _path_conflicts_with_deny_list(p)
|
||||||
if any(lowered.startswith(prefix) for prefix in blocked_prefix):
|
if conflict:
|
||||||
return jsonify({"success": False, "error": f"禁止授权敏感目录: {p}"}), 400
|
return jsonify({"success": False, "error": conflict}), 400
|
||||||
payload = save_policy({
|
payload = save_policy({
|
||||||
"macos_writable_paths": writable,
|
"macos_writable_paths": writable,
|
||||||
"macos_readable_extra_paths": readable_extra
|
"macos_readable_extra_paths": readable_extra
|
||||||
@ -370,4 +398,6 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
|
|||||||
"success": True,
|
"success": True,
|
||||||
"writable_paths": payload.get("macos_writable_paths", []),
|
"writable_paths": payload.get("macos_writable_paths", []),
|
||||||
"readable_extra_paths": payload.get("macos_readable_extra_paths", []),
|
"readable_extra_paths": payload.get("macos_readable_extra_paths", []),
|
||||||
|
"deny_read_paths": payload.get("macos_deny_read_paths", []),
|
||||||
|
"deny_read_regexes": payload.get("macos_deny_read_regexes", []),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -24,11 +24,22 @@
|
|||||||
仅可读
|
仅可读
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<p class="hint">
|
||||||
|
{{
|
||||||
|
mode === 'writable'
|
||||||
|
? '可读可写路径在 workspace-write 沙箱中可写入,只读沙箱中仅可读。'
|
||||||
|
: '仅可读路径在只读沙箱中会被加入允许读取列表;工作区可写沙箱默认已可读。'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
class="path-input"
|
class="path-input"
|
||||||
:value="value"
|
:value="value"
|
||||||
@input="$emit('update:value', ($event.target as HTMLTextAreaElement).value)"
|
@input="$emit('update:value', ($event.target as HTMLTextAreaElement).value)"
|
||||||
placeholder="每行一个路径,例如:~/Desktop/agents-export"
|
:placeholder="
|
||||||
|
mode === 'writable'
|
||||||
|
? '每行一个路径,例如:~/Desktop/agents-export'
|
||||||
|
: '每行一个路径,例如:~/Documents/reference'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn" @click="$emit('save')" :disabled="saving">保存</button>
|
<button type="button" class="btn" @click="$emit('save')" :disabled="saving">保存</button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user