feat(guardrails): repeated-mutation halt + destructive-overwrite guard - #43930
feat(guardrails): repeated-mutation halt + destructive-overwrite guard#43930enzo-adami wants to merge 1 commit into
Conversation
…e guard Two data-protection guards for the per-turn tool guardrail controller, both born from real incidents on a long-running local agent: 1. Repeated-mutation halt: the existing no-progress guard only watches idempotent (read-only) tools, and the failure counters only advance on failed calls. A mutating tool that keeps *succeeding* on the exact same call is never caught — we observed a model issue the identical write_file 38 times in one turn. The guard counts identical successful mutating calls per signature (tool + args, content included, so legitimate iterative edits never match), warns at the existing no_progress warn threshold and halts at the block threshold. 2. Destructive-overwrite guard: blocks a write_file that would replace a non-empty file (>= safe_write_min_bytes, default 200) with content smaller than safe_write_shrink_ratio (default 0.5) of its current size. This is data-loss prevention, not loop detection: it is independent of hard_stop_enabled, does not halt the turn (the model can recover in place by writing the full intended content), and an identical re-issue confirms intent so genuine shrinks still go through. Motivated by a model accidentally blanking a status file and a full report with empty scaffold content. New config keys under tool_loop_guardrails (all with safe defaults): safe_write_enabled, safe_write_min_bytes, safe_write_shrink_ratio. Existing test suite passes unchanged (tests/run_agent/ test_tool_call_guardrail_runtime.py, 9/9). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Feature: Repeated-mutation halt + destructive-overwrite guard
- repeated_mutation_halt: When a mutating tool (e.g. write_file) is called identically 5+ times without change, the guard halts with a descriptive message. Different content = different signature, so iterative edits are never blocked. Counter resets each turn.
- destructive_overwrite_guard: Blocks write_file when the new content is <50% of the existing file size and the file is >=200 bytes. Independent of hard_stop_enabled — data-loss prevention. Re-issuing the identical call once acts as explicit confirmation.
- Both features are well-isolated and have comprehensive tests (139-line test file). The destructive overwrite guard is particularly valuable given the recent accidental-blanking incident (STATUS.md, research report).
Reviewed by Hermes Agent
|
Thanks for addressing a real guardrail gap: current main lets successful non-idempotent calls return without tracking progress ( Problems
Suggested changes
Automated hermes-sweeper review. |
|
Closing instead of rebasing this implementation. Current main now has result-aware idempotent no-progress guardrails, which supersede the generic repeated-mutation counter without false positives across terminal/browser/messaging operations. The destructive-overwrite half needs a separate design: backend-aware path resolution in |
What
Two opt-out guards for the per-turn tool guardrail controller
(
agent/tool_guardrails.py), both born from real incidents running along-lived local agent on a small model:
1. Repeated-mutation halt. The existing
idempotent_no_progressguardonly watches read-only tools, and the failure counters only advance when a
call fails. A mutating tool that keeps succeeding on the exact same
call is invisible to all current guards — we observed a model issue the
identical
write_file38 times in a single turn. The new check countsidentical successful mutating calls per signature (tool + full args, content
included — so legitimate iterative edits never match, since different
content means a different signature). It reuses the existing
no_progress_warn_after/no_progress_block_afterthresholds: warn, thenhalt (
repeated_mutation_halt). Gated behindhard_stop_enabledlike theother halts, and reset per turn.
2. Destructive-overwrite guard. Blocks a
write_filethat would replacea non-empty file (≥
safe_write_min_bytes, default 200) with contentsmaller than
safe_write_shrink_ratio(default 0.5) of its current size.Motivation: the model accidentally blanked a status file and a full research
report by overwriting them with empty scaffold content. Design points:
hard_stop_enabled, controlled by its ownsafe_write_enabledflag(default on).
writing the full intended content.
genuine shrink/replace, which then proceeds.
Config
New keys under
tool_loop_guardrails, all with safe defaults:safe_write_enabled(true),safe_write_min_bytes(200),safe_write_shrink_ratio(0.5, validated to (0, 1]).Tests
tests/run_agent/test_mutation_and_safe_write_guards.py(9 tests): warn →halt progression, no false positive on iterative edits with changing
content, per-turn reset,
hard_stop_enabledgating, shrink block + identicalre-issue override, small/new/growing files allowed, independence from
hard_stop_enabled, flag off. Existing guardrail suite passes unchanged(9/9).
Field experience
Both guards have been running patched into a production-like local install
since 2026-06-08/09: the mutation halt has caught real config-edit loops,
and the overwrite guard stopped further accidental file blanking with no
false positives on normal edit traffic at the 0.5 ratio (real accidental
blankings kept ~22% of the original size; a 0.2 ratio would have missed
them).
🤖 Generated with Claude Code