vibe/vibe/core/tools/utils.py
Mathias Gesbert eb580209d4
v2.6.0 (#524)
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>
2026-03-23 18:45:21 +01:00

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