core/tools+config: name the access mode in the workspace escape error

This commit is contained in:
2026-10-16 19:29:11 +08:00
parent a4f852eec6
commit 1c84d63f37
4 changed files with 17 additions and 8 deletions
+3 -2
View File
@@ -40,8 +40,9 @@ DEFAULT_TOOL_DIRECTIVES = """\
### Workspace ### Workspace
- Explore with `tree`/`listdir`/`glob_paths`/`regex_files`/`read_file` when unsure. - Explore with `tree`/`listdir`/`glob_paths`/`regex_files`/`read_file` when unsure.
- Stay under the workspace (or a path from `new_temporary_workspace`). Respect path denylists. - Stay under the workspace (or a path from `new_temporary_workspace`). Respect path denylists.
- Need a path outside the workspace? Call `request_directory_access` \ - Need a path outside the workspace? Call `request_directory_access` with the \
(human approves; denylists apply) instead of retrying an escaped path. `mode` the escaped-path error names (human approves; denylists apply) instead of \
retrying that path.
### Files ### Files
- Prefer file tools over shell. `edit_replace` fails usually means a bad match — \ - Prefer file tools over shell. `edit_replace` fails usually means a bad match — \
+5 -5
View File
@@ -254,11 +254,11 @@ async def request_directory_access(path: str, reason: str = "", mode: str = "rea
"""Request human approval to use a directory (or exact file) outside the workspace. """Request human approval to use a directory (or exact file) outside the workspace.
Call this instead of retrying a path that failed with ``path escapes Call this instead of retrying a path that failed with ``path escapes
workspace root``. ``mode`` is ``read`` (inspect files), ``write`` (also workspace root``, passing the ``mode`` that error names. ``mode`` is ``read``
modify files) or ``exec`` (also use it as ``run_argv`` / PTY cwd); the human (inspect files), ``write`` (also modify files) or ``exec`` (also use it as
may approve a different level. Approval lasts for the session (or for this ``run_argv`` / PTY cwd); the human may approve a different level. Approval
process when ``--yes`` auto-approves). Paths matching the configured lasts for the session (or for this process when ``--yes`` auto-approves).
denylist can never be granted. Paths matching the configured denylist can never be granted.
""" """
requested = parse_access_mode(mode) requested = parse_access_mode(mode)
if requested is None: if requested is None:
+1 -1
View File
@@ -393,7 +393,7 @@ def resolve_path(path: str | Path, *, required: AccessMode = AccessMode.WRITE) -
if not _under_any_root(resolved, instance, policy) and not _path_grant_covers(resolved, required): if not _under_any_root(resolved, instance, policy) and not _path_grant_covers(resolved, required):
msg = ( msg = (
f"path escapes workspace root ({root}): {path}; " f"path escapes workspace root ({root}): {path}; "
"call request_directory_access to request access to a directory outside the workspace" f"call request_directory_access with mode={required.name.lower()!r} to request access outside the workspace"
) )
raise WorkspaceError(msg) raise WorkspaceError(msg)
# Normalize separators so denylist entries like ``/secrets/`` match on Windows. # Normalize separators so denylist entries like ``/secrets/`` match on Windows.
+8
View File
@@ -38,6 +38,14 @@ def test_escape_error_hints_request_tool(workspace: object) -> None:
_ = resolve_path("../outside") _ = resolve_path("../outside")
@pytest.mark.parametrize("required", [AccessMode.READ, AccessMode.WRITE, AccessMode.EXEC])
def test_escape_error_names_the_mode_to_request(workspace: object, required: AccessMode) -> None:
"""The refusal carries the lowest access mode that would cover the call."""
del workspace
with pytest.raises(WorkspaceError, match=f"mode='{required.name.lower()}'"):
_ = resolve_path("../outside", required=required)
def test_access_mode_parse_and_covers() -> None: def test_access_mode_parse_and_covers() -> None:
assert parse_access_mode(" Read ") is AccessMode.READ assert parse_access_mode(" Read ") is AccessMode.READ
assert parse_access_mode("EXEC") is AccessMode.EXEC assert parse_access_mode("EXEC") is AccessMode.EXEC