From 7b3f55ea65a622fe3b0f488b5c13193cdda0c007 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Thu, 29 Oct 2026 23:19:38 +0800 Subject: [PATCH] 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. --- src/plyngent/cli/state.py | 13 ++++++++----- src/plyngent/tools/access.py | 9 ++++++--- src/plyngent/tools/workspace.py | 3 ++- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/plyngent/cli/state.py b/src/plyngent/cli/state.py index 64acecd..6690aae 100644 --- a/src/plyngent/cli/state.py +++ b/src/plyngent/cli/state.py @@ -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: diff --git a/src/plyngent/tools/access.py b/src/plyngent/tools/access.py index 7ea3269..18b2281 100644 --- a/src/plyngent/tools/access.py +++ b/src/plyngent/tools/access.py @@ -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: diff --git a/src/plyngent/tools/workspace.py b/src/plyngent/tools/workspace.py index c93518c..222758e 100644 --- a/src/plyngent/tools/workspace.py +++ b/src/plyngent/tools/workspace.py @@ -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