Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Gauthier Guinet <43207538+Gguinet@users.noreply.github.com> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Quentin <torroba.q@gmail.com> Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com> Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
120 lines
3.5 KiB
Python
120 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
import fnmatch
|
|
from pathlib import Path, PurePath
|
|
|
|
from vibe.core.tools.base import ToolPermission
|
|
from vibe.core.tools.permissions import (
|
|
PermissionContext,
|
|
PermissionScope,
|
|
RequiredPermission,
|
|
)
|
|
|
|
|
|
def wildcard_match(text: str, pattern: str) -> bool:
|
|
"""Match text against a wildcard pattern using fnmatch.
|
|
|
|
If pattern ends with " *", trailing part is optional (matches with or without args).
|
|
"""
|
|
if fnmatch.fnmatch(text, pattern):
|
|
return True
|
|
if pattern.endswith(" *") and fnmatch.fnmatch(text, pattern[:-2]):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _make_absolute(path_str: str) -> Path:
|
|
path = Path(path_str).expanduser()
|
|
if not path.is_absolute():
|
|
path = Path.cwd() / path
|
|
return path
|
|
|
|
|
|
def resolve_path_permission(
|
|
path_str: str, *, allowlist: list[str], denylist: list[str]
|
|
) -> PermissionContext | None:
|
|
"""Resolve permission for a file path against glob patterns.
|
|
|
|
Returns NEVER on denylist match, ALWAYS on allowlist match, None otherwise.
|
|
"""
|
|
file_str = str(_make_absolute(path_str).resolve())
|
|
|
|
for pattern in denylist:
|
|
if fnmatch.fnmatch(file_str, pattern):
|
|
return PermissionContext(permission=ToolPermission.NEVER)
|
|
|
|
for pattern in allowlist:
|
|
if fnmatch.fnmatch(file_str, pattern):
|
|
return PermissionContext(permission=ToolPermission.ALWAYS)
|
|
|
|
return None
|
|
|
|
|
|
def is_path_within_workdir(path_str: str) -> bool:
|
|
"""Return True if the resolved path is inside cwd."""
|
|
try:
|
|
_make_absolute(path_str).resolve().relative_to(Path.cwd().resolve())
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def resolve_file_tool_permission(
|
|
path_str: str,
|
|
*,
|
|
tool_name: str,
|
|
allowlist: list[str],
|
|
denylist: list[str],
|
|
config_permission: ToolPermission,
|
|
sensitive_patterns: list[str],
|
|
) -> PermissionContext | None:
|
|
"""Resolve permission for a file-based tool invocation.
|
|
|
|
Checks allowlist/denylist, then sensitive patterns, then workdir boundary.
|
|
Returns PermissionContext with granular required_permissions when applicable.
|
|
"""
|
|
if (
|
|
result := resolve_path_permission(
|
|
path_str, allowlist=allowlist, denylist=denylist
|
|
)
|
|
) is not None:
|
|
return result
|
|
|
|
required: list[RequiredPermission] = []
|
|
|
|
file_path = _make_absolute(path_str)
|
|
file_str = str(file_path.resolve())
|
|
|
|
for pattern in sensitive_patterns:
|
|
if PurePath(file_str).match(pattern):
|
|
required.append(
|
|
RequiredPermission(
|
|
scope=PermissionScope.FILE_PATTERN,
|
|
invocation_pattern=file_path.name,
|
|
session_pattern="*",
|
|
label=f"accessing sensitive files ({tool_name})",
|
|
)
|
|
)
|
|
break
|
|
|
|
if not is_path_within_workdir(path_str):
|
|
if config_permission == ToolPermission.NEVER:
|
|
return PermissionContext(permission=ToolPermission.NEVER)
|
|
resolved = file_path.resolve()
|
|
parent_dir = str(resolved.parent)
|
|
glob = str(Path(parent_dir) / "*")
|
|
required.append(
|
|
RequiredPermission(
|
|
scope=PermissionScope.OUTSIDE_DIRECTORY,
|
|
invocation_pattern=glob,
|
|
session_pattern=glob,
|
|
label=f"outside workdir ({glob})",
|
|
)
|
|
)
|
|
|
|
if required:
|
|
return PermissionContext(
|
|
permission=ToolPermission.ASK, required_permissions=required
|
|
)
|
|
|
|
return None
|