fix(context): redact full tool-call args before the summarizer cut

62ceddd342 cut raw args to HEAD+4096 before redaction to save time. The
PEM redaction pattern only matches a complete BEGIN...END block. A long key
whose END fell past the cut stayed unredacted, and once an earlier key was
redacted and the text shrank, its body landed in the 1200-char head that
goes into the persisted summary. Go back to the BASE order: redact the full
args, then apply the MAX/HEAD cut. This is a cold path (once per summarized
call per compaction), and _SUMMARY_INPUT_MAX_CHARS still bounds the prompt.
Extend the kept canonical-args test with a two-PEM input that leaks on
62ceddd342 and passes now.
This commit is contained in:
kshitijk4poor
2026-09-27 14:03:26 +05:30
committed by kshitij
parent be834681dc
commit fb86bc708d
2 changed files with 12 additions and 9 deletions

View File

@@ -3399,10 +3399,9 @@ class ContextCompressor(SummaryDispatchMixin, MicroCompactionMixin, ContextEngin
fn = getattr(tc, "function", None)
return f" {getattr(fn, 'name', '?') if fn else '?'}(...)"
fn = tc.get("function", {})
raw = fn.get("arguments", "") or ""
# Args are byte-exact now, so they can be huge: cut before the (costly) redaction pass, keeping
# slack past the head so a secret straddling the cut still matches and length checks stay honest.
args = _redact_compaction_text(raw[:self._TOOL_ARGS_HEAD + 4096] if len(raw) > self._TOOL_ARGS_HEAD + 4096 else raw)
# Redact the FULL args before cutting: delimited secrets (PEM BEGIN…END) only match whole, so a
# pre-redaction cut would leave a key body straddling the cut unredacted in the persisted summary.
args = _redact_compaction_text(fn.get("arguments", "") or "")
if len(args) > self._TOOL_ARGS_MAX:
args = args[:self._TOOL_ARGS_HEAD] + "..."
return f" {fn.get('name', '?')}({args})"
@@ -5484,14 +5483,11 @@ Write only the summary body. Do not include any preamble or prefix."""
telemetry, turns_to_summarize, compress_end - compress_start, feasibility_skip,
)
# Phase 4: Assemble compressed message list
compressed = self._assemble_compressed(
messages, compress_start, compress_end, scan, summary,
)
compressed = self._assemble_compressed(messages, compress_start, compress_end, scan, summary)
return self._finalize_compressed(compressed, canonical_messages, n_messages, spare_pending_images)
def _assemble_compressed(
self, messages: List[Dict[str, Any]], compress_start: int, compress_end: int,
scan: "_HandoffScan", summary: str,
self, messages: List[Dict[str, Any]], compress_start: int, compress_end: int, scan: "_HandoffScan", summary: str,
) -> List[Dict[str, Any]]:
"""Head + summary + tail from the pruned copy: its tool-result demotions are what let an oversized
head/tail compress at all (#61932); tool-call arguments are never rewritten by pruning."""