cli: leave a directory-access request to the human under YOLO

The access hook short-circuited on YOLO mode and answered its own question
with a process-only grant, so `--yes` (and `/yolo on`) reached any path outside
the workspace without asking. That grant is a filesystem capability, not one
risky call: the command denylist and a skill write are asked either way, and so
is this one now.

`yolo_allow` stays as the process-scoped store a host fills when it approves
without persisting; nothing auto-approves it any more. A non-interactive run
denies, like the other policy prompts.
This commit is contained in:
2026-10-29 23:19:38 +08:00
parent b75ee88764
commit 7b3f55ea65
3 changed files with 16 additions and 9 deletions
+8 -5
View File
@@ -297,12 +297,15 @@ class ReplState:
reason: str,
timeout_seconds: float,
) -> AccessDecision | str | None:
"""Directory-access confirm: YOLO/``--yes`` auto-approves, else ask."""
from plyngent.cli.limits import prompt_directory_access_confirm
from plyngent.tools.access import AccessDecision as Decision
"""Directory-access confirm: always ask, whatever YOLO mode says.
An out-of-workspace path is a grant on the filesystem rather than one
risky call, so ``--yes`` / ``/yolo`` do not answer it — the command
denylist and a skill write are not skipped either. A non-interactive run
denies.
"""
from plyngent.cli.limits import prompt_directory_access_confirm
if self.effective_yolo() != "off":
return Decision(mode, persist=False)
return prompt_directory_access_confirm(path, mode, reason, timeout_seconds)
def _install_config_access(self) -> None:
+6 -3
View File
@@ -4,7 +4,8 @@ A grant is one ``path → mode`` pair; its *source* is structural:
- :attr:`WorkspacePolicy.config_allow` — static TOML pre-allow (never prompts),
- :attr:`SessionState.access_grants` — human-approved for this session,
- :attr:`WorkspacePolicy.yolo_allow` — ``--yes`` / YOLO auto-approval, process only.
- :attr:`WorkspacePolicy.yolo_allow` — process only: a host that approved
without keeping the grant (``--yes`` does **not** answer this prompt).
``resolve_path`` consults all three. Grants are a policy gate for path-resolving
tools, **not** a sandbox: an ``exec`` grant (or any command) can reach the rest
@@ -257,8 +258,10 @@ async def request_directory_access(path: str, reason: str = "", mode: str = "rea
workspace root``, passing the ``mode`` that error names. ``mode`` is ``read``
(inspect files), ``write`` (also modify files) or ``exec`` (also use it as
``run_argv`` / PTY cwd); the human may approve a different level. Approval
lasts for the session (or for this process when ``--yes`` auto-approves).
Paths matching the configured denylist can never be granted.
lasts for the session (or for this process when the host answers without
persisting); ``--yes`` / YOLO mode does not answer this prompt, and a
non-interactive run denies. Paths matching the configured denylist can never
be granted.
"""
requested = parse_access_mode(mode)
if requested is None:
+2 -1
View File
@@ -114,7 +114,8 @@ class WorkspacePolicy:
policy_confirm_timeout_seconds: float = DEFAULT_POLICY_CONFIRM_TIMEOUT_SECONDS
# Static TOML pre-allow (resolved path → mode); never prompts.
config_allow: dict[Path, AccessMode] = field(default_factory=dict)
# Process-scoped YOLO / --yes grants (resolved path → mode); never persisted.
# Process-scoped grants (resolved path → mode): a host answer kept only for
# this process. YOLO mode never fills it; nothing is auto-approved here.
yolo_allow: dict[Path, AccessMode] = field(default_factory=dict)
# Host-contributed read-only roots (skill directories): every path tool may
# read them, nothing may write them. Not a grant, so they stay out of