diff --git a/interfaces/admin/routes.py b/interfaces/admin/routes.py index ea52c0ce..441e7473 100644 --- a/interfaces/admin/routes.py +++ b/interfaces/admin/routes.py @@ -1401,8 +1401,18 @@ def build_admin_router() -> APIRouter: raise HTTPException(status_code=403, detail="workspace_paths_self_only") row = store.get_user_workspace_path_allowlist(tenant_id=tid, user_id=uid) if not row: - return {"ok": True, "from_db": False, "policy": {"extra_roots": "", "allow_any_path": False}} - return {"ok": True, "from_db": True, "policy": row} + return { + "ok": True, + "from_db": False, + "policy": {"extra_roots": "", "allow_any_path": False}, + "public_tools_allow_high": False, + } + return { + "ok": True, + "from_db": True, + "policy": row, + "public_tools_allow_high": bool(row.get("allow_high_risk_public_tools")), + } @router.post("/admin/api/users/workspace-path-policy") def api_users_workspace_path_policy_save( @@ -1429,11 +1439,13 @@ def build_admin_router() -> APIRouter: if err: return {"ok": False, "error": err} allow_any = bool(payload.get("allow_any_path", False)) + allow_high = bool(payload.get("public_tools_allow_high", False)) store.upsert_user_workspace_path_allowlist( tenant_id=tid, user_id=uid, extra_roots=norm, allow_any_path=allow_any, + allow_high_risk_public_tools=allow_high, ) store.add_admin_audit_log( actor_tenant_id=ctx["tenant_id"], @@ -1442,10 +1454,15 @@ def build_admin_router() -> APIRouter: target_type="user", target_id=uid, status="ok", - detail={"tenant_id": tid, "allow_any_path": allow_any, "extra_roots_preview": norm[:500]}, + detail={ + "tenant_id": tid, + "allow_any_path": allow_any, + "public_tools_allow_high": bool(allow_high), + "extra_roots_preview": norm[:500], + }, ) row = store.get_user_workspace_path_allowlist(tenant_id=tid, user_id=uid) - return {"ok": True, "policy": row or {}} + return {"ok": True, "policy": row or {}, "public_tools_allow_high": bool((row or {}).get("allow_high_risk_public_tools"))} @router.post("/admin/api/users/delete-unbound") def api_users_delete_unbound( diff --git a/interfaces/admin/static/app.js b/interfaces/admin/static/app.js index 7553dc04..ba9adcc9 100644 --- a/interfaces/admin/static/app.js +++ b/interfaces/admin/static/app.js @@ -208,6 +208,9 @@ const I18N = { "workspacePaths.allowAny": "允许任意路径(高风险)", "workspacePaths.allowAnyHint": "仅作用于内置工作区工具(read_file / glob 等)的路径校验;不会放开 MCP filesystem,也不会自动把整盘写进 MCP。需要 MCP 列目录的盘符/目录请填在「额外根路径」或环境变量 AIA_WORKSPACE_EXTRA_ROOTS。", + "workspacePaths.allowHighTools": "允许高风险 Public 工具(全局)", + "workspacePaths.allowHighToolsHint": + "开启后将放开 run_command / write_file / edit_file 等高风险 public 工具的模型可见性(等效 AIA_PUBLIC_TOOLS_ALLOW_HIGH=1)。", "workspacePaths.load": "加载", "workspacePaths.save": "保存", "workspacePaths.status": "状态", @@ -624,6 +627,9 @@ const I18N = { "workspacePaths.allowAny": "Allow any path (high risk)", "workspacePaths.allowAnyHint": "Applies only to built-in workspace tools (read_file / glob, etc.); it does not unlock MCP filesystem or auto-mount whole disks for MCP. List directories you need in “Extra roots” or AIA_WORKSPACE_EXTRA_ROOTS.", + "workspacePaths.allowHighTools": "Allow high-risk public tools (global)", + "workspacePaths.allowHighToolsHint": + "When enabled, high-risk public tools (run_command / write_file / edit_file, etc.) become model-visible (equivalent to AIA_PUBLIC_TOOLS_ALLOW_HIGH=1).", "workspacePaths.load": "Load", "workspacePaths.save": "Save", "workspacePaths.status": "Status", @@ -6856,8 +6862,13 @@ async function renderWorkspacePaths() { style: "min-height:88px;font-family:monospace;", }); const allowAnyCb = el("input", { type: "checkbox" }); + const allowHighToolsCb = el("input", { type: "checkbox" }); const status = el("div", { class: "muted", text: "" }); const canWrite = hasPermission("admin:user:write") || canWsWrite; + const canTogglePublicHigh = hasPermission("admin:user:write"); + if (!canTogglePublicHigh) { + allowHighToolsCb.disabled = true; + } const getEffectiveTid = () => (selfService ? sessionTid : String(tenantSel.value || "")); const getEffectiveUid = () => (selfService ? sessionUid : String(userSel.value || "")); @@ -6904,6 +6915,7 @@ async function renderWorkspacePaths() { const pol = r.policy || {}; extraInput.value = String(pol.extra_roots || ""); allowAnyCb.checked = !!pol.allow_any_path; + allowHighToolsCb.checked = !!r.public_tools_allow_high; status.textContent = (r.from_db ? t("workspacePaths.fromDb") + " · " : "") + JSON.stringify(pol); } catch (e) { status.textContent = String(e && e.message ? e.message : e); @@ -6937,12 +6949,16 @@ async function renderWorkspacePaths() { if (!tid || !uid) return; status.textContent = "…"; try { - const r = await apiPost("/admin/api/users/workspace-path-policy", { + const payload = { tenant_id: tid, user_id: uid, extra_roots: extraInput.value, allow_any_path: !!allowAnyCb.checked, - }); + }; + if (canTogglePublicHigh) { + payload.public_tools_allow_high = !!allowHighToolsCb.checked; + } + const r = await apiPost("/admin/api/users/workspace-path-policy", payload); if (!r.ok) { status.textContent = String(r.error || "error"); return; @@ -6979,6 +6995,11 @@ async function renderWorkspacePaths() { el("span", { text: t("workspacePaths.allowAny") }), ]), el("div", { class: "muted", style: "margin-top:6px;line-height:1.45;", text: t("workspacePaths.allowAnyHint") }), + el("label", { class: "row", style: "align-items:center;gap:8px;margin-top:10px;" }, [ + allowHighToolsCb, + el("span", { text: t("workspacePaths.allowHighTools") }), + ]), + el("div", { class: "muted", style: "margin-top:6px;line-height:1.45;", text: t("workspacePaths.allowHighToolsHint") }), el("div", { class: "row", style: "margin-top:10px;gap:8px;" }, [loadBtn, saveBtn]), el("div", { class: "muted", style: "margin-top:8px;" }, [el("span", { text: t("workspacePaths.status") + ": " }), status]), ); diff --git a/platform/persistence/sqlite_store.py b/platform/persistence/sqlite_store.py index e5c5f16b..20413394 100644 --- a/platform/persistence/sqlite_store.py +++ b/platform/persistence/sqlite_store.py @@ -544,6 +544,7 @@ class SqliteStore: user_id TEXT NOT NULL, extra_roots TEXT NOT NULL DEFAULT '', allow_any_path INTEGER NOT NULL DEFAULT 0, + allow_high_risk_public_tools INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL, PRIMARY KEY (tenant_id, user_id), FOREIGN KEY(tenant_id) REFERENCES tenant(id) ON DELETE CASCADE, @@ -551,6 +552,11 @@ class SqliteStore: ); """ ) + uw_cols = {row[1] for row in conn.execute("PRAGMA table_info(user_workspace_path_allowlist)").fetchall()} + if "allow_high_risk_public_tools" not in uw_cols: + conn.execute( + "ALTER TABLE user_workspace_path_allowlist ADD COLUMN allow_high_risk_public_tools INTEGER NOT NULL DEFAULT 0" + ) conn.execute( """ CREATE TABLE IF NOT EXISTS auth_session ( @@ -4553,7 +4559,7 @@ class SqliteStore: with self._connect() as conn: row = conn.execute( """ - SELECT tenant_id, user_id, extra_roots, allow_any_path, updated_at + SELECT tenant_id, user_id, extra_roots, allow_any_path, allow_high_risk_public_tools, updated_at FROM user_workspace_path_allowlist WHERE tenant_id = ? AND user_id = ? LIMIT 1 @@ -4567,6 +4573,7 @@ class SqliteStore: "user_id": str(row["user_id"] or ""), "extra_roots": str(row["extra_roots"] or ""), "allow_any_path": bool(int(row["allow_any_path"] or 0)), + "allow_high_risk_public_tools": bool(int(row["allow_high_risk_public_tools"] or 0)), "updated_at": str(row["updated_at"] or ""), } @@ -4597,6 +4604,7 @@ class SqliteStore: user_id: str, extra_roots: str, allow_any_path: bool, + allow_high_risk_public_tools: bool = False, ) -> None: tid = str(tenant_id or "").strip() uid = str(user_id or "").strip() @@ -4609,14 +4617,24 @@ class SqliteStore: with self._connect() as conn: conn.execute( """ - INSERT INTO user_workspace_path_allowlist (tenant_id, user_id, extra_roots, allow_any_path, updated_at) - VALUES (?, ?, ?, ?, ?) + INSERT INTO user_workspace_path_allowlist ( + tenant_id, user_id, extra_roots, allow_any_path, allow_high_risk_public_tools, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(tenant_id, user_id) DO UPDATE SET extra_roots = excluded.extra_roots, allow_any_path = excluded.allow_any_path, + allow_high_risk_public_tools = excluded.allow_high_risk_public_tools, updated_at = excluded.updated_at """, - (tid, uid, roots, 1 if allow_any_path else 0, ts), + ( + tid, + uid, + roots, + 1 if allow_any_path else 0, + 1 if allow_high_risk_public_tools else 0, + ts, + ), ) def create_auth_session( diff --git a/runtime/tools/catalog.py b/runtime/tools/catalog.py index 1f4cc36c..b5db8e55 100644 --- a/runtime/tools/catalog.py +++ b/runtime/tools/catalog.py @@ -273,7 +273,21 @@ def materialize_tool_specs( # Optional safety gate for public tools. # Default: only allow low risk public tools to be visible to all roles. # Override via env: AIA_PUBLIC_TOOLS_ALLOW_HIGH=1 to allow high risk public tools. - allow_high = _is_truthy(os.getenv("AIA_PUBLIC_TOOLS_ALLOW_HIGH", "0")) + # If env is unset, fallback to per-user workspace path policy switch. + raw_env = str(os.getenv("AIA_PUBLIC_TOOLS_ALLOW_HIGH") or "").strip() + if raw_env: + allow_high = _is_truthy(raw_env) + else: + allow_high = False + try: + if store is not None and path_policy_tenant_id and path_policy_user_id: + row = store.get_user_workspace_path_allowlist( + tenant_id=str(path_policy_tenant_id), + user_id=str(path_policy_user_id), + ) + allow_high = bool((row or {}).get("allow_high_risk_public_tools")) + except Exception: + allow_high = False if allow_high: return True return str(getattr(spec, "risk_level", "") or "low").strip().lower() != "high"