diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index b213473ef..0f1b15efb 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -97,24 +97,17 @@ class AstrBotConfig(dict): and "computer_use_local_permissions" in default_provider_settings and "computer_use_local_permissions" not in provider_settings ): - # Preserve the legacy Local meaning while replacing its use of the - # shared admin switch with explicit per-role permissions. - member_execution = not provider_settings.get( - "computer_use_require_admin", - True, + # Preserve legacy POSIX access; Windows uses its supported defaults. + permissions = copy.deepcopy( + default_provider_settings["computer_use_local_permissions"] ) - provider_settings["computer_use_local_permissions"] = { - "member": { - "allow_execution": member_execution, - "allow_network": False, - "filesystem_scope": "workspace", - }, - "admin": { - "allow_execution": True, - "allow_network": True, - "filesystem_scope": "host", - }, - } + permissions["member"]["allow_execution"] = permissions["member"][ + "filesystem_scope" + ] != "none" and not provider_settings.get( + "computer_use_require_admin", True + ) + permissions["admin"]["filesystem_scope"] = "host" + provider_settings["computer_use_local_permissions"] = permissions config_migrated = True # 检查配置完整性,并插入 has_new = self.check_config_integrity(default_config, conf, schema=schema) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index fdca9f20d..a23f65606 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1,6 +1,7 @@ """如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。""" import os +import platform from astrbot import __version__ from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG @@ -8,6 +9,32 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path from .agent_runner import get_agent_runner_config_default + +def get_local_permission_defaults(system: str | None = None) -> dict: + """Return fresh Local permission defaults for the operating system. + + Args: + system: Operating system name, or None to use the current system. + + Returns: + Per-role policies. Windows disables member access and gives admins + unrestricted access because workspace isolation is unavailable. + """ + windows = (system or platform.system()).lower() == "windows" + return { + "member": { + "allow_execution": False, + "allow_network": False, + "filesystem_scope": "none" if windows else "workspace", + }, + "admin": { + "allow_execution": True, + "allow_network": True, + "filesystem_scope": "host" if windows else "workspace", + }, + } + + VERSION = __version__ DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") @@ -152,18 +179,7 @@ DEFAULT_CONFIG = { "add_cron_tools": True, }, "computer_use_runtime": "none", - "computer_use_local_permissions": { - "member": { - "allow_execution": False, - "allow_network": False, - "filesystem_scope": "workspace", - }, - "admin": { - "allow_execution": True, - "allow_network": True, - "filesystem_scope": "workspace", - }, - }, + "computer_use_local_permissions": get_local_permission_defaults(), "computer_use_require_admin": True, "sandbox": { "booter": "shipyard_neo", diff --git a/astrbot/core/tools/computer_tools/util.py b/astrbot/core/tools/computer_tools/util.py index 3fa492537..0fdfc2009 100644 --- a/astrbot/core/tools/computer_tools/util.py +++ b/astrbot/core/tools/computer_tools/util.py @@ -5,6 +5,7 @@ from typing import Literal from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.computer.process_sandbox import create_process_sandbox +from astrbot.core.config.default import get_local_permission_defaults from astrbot.core.db import BaseDatabase from astrbot.core.utils.astrbot_path import get_astrbot_workspaces_path from astrbot.core.workspace import ( @@ -93,29 +94,28 @@ def get_local_permission_policy( ) provider_settings = cfg.get("provider_settings", {}) role = "admin" if context.context.event.role == "admin" else "member" - default_execution = role == "admin" - default_network = role == "admin" - default_filesystem = "workspace" + defaults = get_local_permission_defaults()[role] permissions = provider_settings.get("computer_use_local_permissions") role_policy = permissions.get(role) if isinstance(permissions, dict) else None if not isinstance(role_policy, dict): role_policy = {} if role == "member" and not isinstance(permissions, dict): - default_execution = not provider_settings.get( + defaults["allow_execution"] = not provider_settings.get( "computer_use_require_admin", True, ) - filesystem_scope = role_policy.get("filesystem_scope", default_filesystem) + filesystem_scope = role_policy.get("filesystem_scope", defaults["filesystem_scope"]) if filesystem_scope not in {"none", "workspace", "host"}: - filesystem_scope = default_filesystem + filesystem_scope = defaults["filesystem_scope"] allow_execution = ( filesystem_scope != "none" - and role_policy.get("allow_execution", default_execution) is True + and role_policy.get("allow_execution", defaults["allow_execution"]) is True ) allow_network = ( - allow_execution and role_policy.get("allow_network", default_network) is True + allow_execution + and role_policy.get("allow_network", defaults["allow_network"]) is True ) return LocalPermissionPolicy( allow_execution=allow_execution, diff --git a/astrbot/dashboard/services/config_service.py b/astrbot/dashboard/services/config_service.py index ebad00529..c664a7f09 100644 --- a/astrbot/dashboard/services/config_service.py +++ b/astrbot/dashboard/services/config_service.py @@ -19,6 +19,7 @@ from astrbot.core.config.default import ( CONFIG_METADATA_3_SYSTEM, DEFAULT_CONFIG, DEFAULT_VALUE_MAP, + get_local_permission_defaults, ) from astrbot.core.config.i18n_utils import ConfigMetadataI18n from astrbot.core.core_lifecycle import AstrBotCoreLifecycle @@ -323,6 +324,7 @@ def validate_config( } validate(data, meta_all) provider_settings = data.get("provider_settings", {}) + defaults = get_local_permission_defaults(runtime.get("os") if runtime else None) permissions = ( provider_settings.get("computer_use_local_permissions", {}) if isinstance(provider_settings, dict) @@ -346,7 +348,9 @@ def validate_config( errors.append( f"Local permission {role}.{key} must be a boolean." ) - scope = policy.get("filesystem_scope", "workspace") + scope = policy.get( + "filesystem_scope", defaults[role]["filesystem_scope"] + ) if scope not in ("none", "workspace", "host"): errors.append( f"Invalid local filesystem scope for {role}: {scope}." @@ -354,7 +358,10 @@ def validate_config( if scope == "none": policy["allow_execution"] = False policy["allow_network"] = False - elif policy.get("allow_execution", role == "admin") is False: + elif ( + policy.get("allow_execution", defaults[role]["allow_execution"]) + is False + ): policy["allow_network"] = False if ( @@ -367,9 +374,6 @@ def validate_config( old_settings = (current_config or {}).get("provider_settings", {}) old_permissions = old_settings.get("computer_use_local_permissions", {}) was_local = old_settings.get("computer_use_runtime") == "local" - defaults = DEFAULT_CONFIG["provider_settings"][ - "computer_use_local_permissions" - ] for role in ("member", "admin"): # Keep unchanged legacy policies, but check both roles when # activating Local access or creating a profile. diff --git a/dashboard/src/components/shared/ConfigItemRenderer.vue b/dashboard/src/components/shared/ConfigItemRenderer.vue index ceaec5db6..6e453f444 100644 --- a/dashboard/src/components/shared/ConfigItemRenderer.vue +++ b/dashboard/src/components/shared/ConfigItemRenderer.vue @@ -257,7 +257,8 @@ import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue' import PluginSetSelector from './PluginSetSelector.vue' import T2ITemplateEditor from './T2ITemplateEditor.vue' import DashboardTotpManager from './DashboardTotpManager.vue' -import LocalPermissionMatrix from './LocalPermissionMatrix.vue' +import LocalPermissionMatrix, { windowsPermissionDefaults } from './LocalPermissionMatrix.vue' +import { statsApi } from '@/api/v1' import { computed, ref } from 'vue' import { useI18n, useModuleI18n } from '@/i18n/composables' import { usePluginI18n } from '@/utils/pluginI18n' @@ -306,7 +307,9 @@ const { t } = useI18n() const { getRaw } = useModuleI18n('features/config-metadata') const { configText } = usePluginI18n() -function emitUpdate(val) { +async function emitUpdate(val) { + const enablingLocal = props.configKey === 'provider_settings.computer_use_runtime' + && (props.modelValue === 'none' || props.modelValue == null) && val === 'local' if ( props.itemMeta?._special === 'agent_runner_type' && props.configRoot?.agent_runner @@ -317,6 +320,22 @@ function emitUpdate(val) { ) } emit('update:modelValue', val) + if (enablingLocal && props.configRoot?.provider_settings) { + const settings = props.configRoot.provider_settings + try { + const response = await statsApi.version() + if (response.data?.data?.runtime?.os === 'windows' && settings.computer_use_runtime === 'local') { + const permissions = { ...settings.computer_use_local_permissions } + for (const [role, defaults] of Object.entries(windowsPermissionDefaults)) { + const policy = permissions[role] + if (!policy || policy.filesystem_scope === 'workspace') permissions[role] = { ...defaults } + } + settings.computer_use_local_permissions = permissions + } + } catch (error) { + console.warn('Failed to initialize local permissions:', error) + } + } } const listSelectItems = computed(() => diff --git a/dashboard/src/components/shared/LocalPermissionMatrix.vue b/dashboard/src/components/shared/LocalPermissionMatrix.vue index 048efcac4..c5aebea19 100644 --- a/dashboard/src/components/shared/LocalPermissionMatrix.vue +++ b/dashboard/src/components/shared/LocalPermissionMatrix.vue @@ -133,6 +133,13 @@ + +