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
+2 -2
View File
@@ -184,9 +184,9 @@ def test_soft_confirm_reason_matrix() -> None:
def test_classify_danger_fetch() -> None:
assert classify_danger("fetch_local", {"url": "https://example.com/", "method": "GET"}) is None
reason = classify_danger("fetch_local", {"url": "http://example.com/", "method": "GET"})
assert reason is not None and "cleartext" in reason
assert isinstance(reason, str) and "cleartext" in reason
reason2 = classify_danger("fetch_local", {"url": "https://example.com/api", "method": "DELETE"})
assert reason2 is not None and "DELETE" in reason2
assert isinstance(reason2, str) and "DELETE" in reason2
async def test_fetch_get_post_put_delete(workspace: Path, http_server: str) -> None: