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 @@
+
+