1212 access depends on the local runtime implementation and host OS permissions.
1313 Upload and download tools are defined here, but `LocalBooter` does not
1414 implement them and the main agent does not expose them in local mode.
15- - Member + local: read/write/edit/grep are restricted to `data/skills`,
16- `data/workspaces/{normalized_umo}`, and `/tmp/.astrbot`. Upload/download are
17- denied by `check_admin_permission` if invoked.
15+ - Member + local: read/grep are restricted to `data/skills`,
16+ plugin-provided `data/plugins/*/skills`,
17+ `data/workspaces/{normalized_umo}`, and `/tmp/.astrbot`; write/edit are
18+ restricted to the same local roots except plugin-provided Skills, which are
19+ read-only. Upload/download are denied by `check_admin_permission` if invoked.
1820- Admin + sandbox: read/write/edit/grep are not path-restricted by this
1921 module;
2022 sandbox filesystem boundaries are enforced by the sandbox runtime. Upload and
4547from astrbot .core .computer .file_read_utils import read_file_tool_result
4648from astrbot .core .message .components import File , Image
4749from astrbot .core .utils .astrbot_path import (
50+ get_astrbot_plugin_path ,
4851 get_astrbot_skills_path ,
4952 get_astrbot_system_tmp_path ,
5053 get_astrbot_temp_path ,
6770_IMAGE_FILE_SUFFIXES = {".bmp" , ".gif" , ".jpeg" , ".jpg" , ".png" , ".webp" }
6871
6972
70- def _restricted_env_path_labels (umo : str ) -> list [str ]:
73+ def _restricted_env_path_labels (umo : str , * , include_plugin_skills : bool ) -> list [str ]:
7174 """Labels for the allowed directories in a local(not sandbox) and restricted(not admin) environment"""
7275 normalized_umo = normalize_umo_for_workspace (umo )
73- return [
76+ labels = [
7477 "data/skills" ,
75- f"data/workspaces/{ normalized_umo } " ,
76- get_astrbot_system_tmp_path (),
77- get_astrbot_temp_path (),
7878 ]
79+ if include_plugin_skills :
80+ labels .append ("data/plugins/*/skills" )
81+ labels .extend (
82+ [
83+ f"data/workspaces/{ normalized_umo } " ,
84+ get_astrbot_system_tmp_path (),
85+ get_astrbot_temp_path (),
86+ ]
87+ )
88+ return labels
7989
8090
8191def get_astrbot_workspaces_path () -> str :
@@ -89,8 +99,30 @@ def _workspace_root(umo: str) -> Path:
8999 return (Path (get_astrbot_workspaces_path ()) / normalized_umo ).resolve (strict = False )
90100
91101
102+ def _plugin_skill_roots () -> tuple [Path , ...]:
103+ plugins_root = Path (get_astrbot_plugin_path ())
104+ if not plugins_root .exists ():
105+ return ()
106+ return tuple (
107+ (plugin_dir / "skills" ).resolve (strict = False )
108+ for plugin_dir in plugins_root .iterdir ()
109+ if plugin_dir .is_dir () and (plugin_dir / "skills" ).is_dir ()
110+ )
111+
112+
92113def _read_allowed_roots (umo : str ) -> tuple [Path , ...]:
93114 """Non-admin users can only read files within these directories (and their subdirectories)"""
115+ return (
116+ Path (get_astrbot_skills_path ()).resolve (strict = False ),
117+ * _plugin_skill_roots (),
118+ _workspace_root (umo ),
119+ Path (get_astrbot_system_tmp_path ()).resolve (strict = False ),
120+ Path (get_astrbot_temp_path ()).resolve (strict = False ),
121+ )
122+
123+
124+ def _write_allowed_roots (umo : str ) -> tuple [Path , ...]:
125+ """Non-admin users cannot modify plugin-provided Skills."""
94126 return (
95127 Path (get_astrbot_skills_path ()).resolve (strict = False ),
96128 _workspace_root (umo ),
@@ -131,11 +163,16 @@ def _resolve_user_path(path: str, *, local_env: bool, umo: str) -> Path:
131163 return (Path .cwd () / candidate ).resolve (strict = False )
132164
133165
134- def _is_path_within_allowed_roots (path : str , umo : str ) -> bool :
166+ def _is_path_within_allowed_roots (
167+ path : str ,
168+ * ,
169+ umo : str ,
170+ allowed_roots : tuple [Path , ...],
171+ ) -> bool :
135172 resolved = _resolve_user_path (path , local_env = True , umo = umo )
136173 return any (
137174 resolved == allowed_root or resolved .is_relative_to (allowed_root )
138- for allowed_root in _read_allowed_roots ( umo )
175+ for allowed_root in allowed_roots
139176 )
140177
141178
@@ -145,14 +182,24 @@ def _normalize_rw_path(
145182 restricted : bool ,
146183 local_env : bool ,
147184 umo : str ,
185+ write : bool = False ,
148186) -> str :
149187 normalized_path = _resolve_tool_path (path , local_env = local_env , umo = umo )
150188 if not normalized_path :
151189 raise ValueError ("`path` must be a non-empty string." )
152- if restricted and not _is_path_within_allowed_roots (normalized_path , umo ):
153- allowed = ", " .join (_restricted_env_path_labels (umo ))
190+ if restricted :
191+ allowed_roots = _write_allowed_roots (umo ) if write else _read_allowed_roots (umo )
192+ if restricted and not _is_path_within_allowed_roots (
193+ normalized_path ,
194+ umo = umo ,
195+ allowed_roots = allowed_roots ,
196+ ):
197+ allowed = ", " .join (
198+ _restricted_env_path_labels (umo , include_plugin_skills = not write )
199+ )
200+ access = "Write" if write else "Read"
154201 raise PermissionError (
155- "Read access is restricted for this user. "
202+ f" { access } access is restricted for this user. "
156203 f"Allowed directories: { allowed } . Blocked path: { normalized_path } ."
157204 )
158205 return normalized_path
@@ -290,6 +337,7 @@ async def call(
290337 restricted = restricted ,
291338 local_env = local_env ,
292339 umo = context .context .event .unified_msg_origin ,
340+ write = True ,
293341 )
294342 if local_env
295343 else path .strip ()
@@ -368,6 +416,7 @@ async def call(
368416 restricted = restricted ,
369417 local_env = local_env ,
370418 umo = umo ,
419+ write = True ,
371420 )
372421 if local_env
373422 else path .strip ()
@@ -532,10 +581,16 @@ def _normalize_search_paths(
532581 disallowed = [
533582 path
534583 for path in normalized
535- if not _is_path_within_allowed_roots (path , umo )
584+ if not _is_path_within_allowed_roots (
585+ path ,
586+ umo = umo ,
587+ allowed_roots = _read_allowed_roots (umo ),
588+ )
536589 ]
537590 if disallowed :
538- allowed = ", " .join (_restricted_env_path_labels (umo ))
591+ allowed = ", " .join (
592+ _restricted_env_path_labels (umo , include_plugin_skills = True )
593+ )
539594 blocked = ", " .join (disallowed )
540595 raise PermissionError (
541596 "Read access is restricted for this user. "
0 commit comments