core/agent: let YOLO mode skip YOLO-tagged soft confirms

`--yes` / `/yolo on|off|once` clears the prompt of a `YOLO`-tagged tool: interpreter runs (`run_argv`, `run_argv_batch`, `open_pty`), the file mutations (overwrite / copy / move / delete) and `fetch_local`'s cleartext and mutating requests. A tool without the tag still asks, and hard path/command denylists stay untouched.

`classify_danger` now answers with a `DangerReason` — the reason text plus `yolo_skip` — so one call can refuse that skip: a command that runs through `sudo` / `su` / `doas` / `pkexec` (as the program or a wrapper) keeps its confirm whatever YOLO mode says. It never adds a reason of its own, so the program the denylist gate already asks about is not asked twice.

`prepare_batch_confirm` leaves the covered calls out, so a skipped call neither prompts on its own nor takes a slot in the round's aggregated prompt, and one escalating step keeps the whole batch in front of the human.
This commit is contained in:
2026-10-21 19:10:46 +08:00
parent 029ac47350
commit 8ec63aa6a2
10 changed files with 301 additions and 94 deletions
+10 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from plyngent.tools.command_scan import basename, is_interpreter, unwrap_command
from plyngent.tools.command_scan import basename, escalators_in_chain, is_interpreter, unwrap_command
def test_basename() -> None:
@@ -9,6 +9,15 @@ def test_basename() -> None:
assert basename("pdm") == "pdm"
def test_escalators_in_chain() -> None:
assert escalators_in_chain(["sudo", "-u", "root", "python", "-c", "print(1)"]) == ("sudo",)
assert escalators_in_chain(["env", "FOO=1", "doas", "id"]) == ("doas",)
assert escalators_in_chain(["pdm", "run", "sudo", "id"]) == ("sudo",)
# Only the command line counts: the code an interpreter carries is not read.
assert escalators_in_chain(["bash", "-c", "sudo id"]) == ()
assert escalators_in_chain(["grep", "-c", "sudo", "notes.txt"]) == ()
def test_unwrap_plain_command() -> None:
scan = unwrap_command(["grep", "-c", "needle", "f.txt"])
assert scan.wrappers == ()