close
Skip to content

fix: redact model-authored tool titles on all deny surfaces (#2274) - #2389

Merged
pepmach merged 1 commit into
mainfrom
fix/redact-deny-titles-2274
Aug 9, 2026
Merged

fix: redact model-authored tool titles on all deny surfaces (#2274)#2389
pepmach merged 1 commit into
mainfrom
fix/redact-deny-titles-2274

Conversation

@bolichen97

Copy link
Copy Markdown
Collaborator

Summary

event.title is model-authored display text (_select_tool_title prefers the backend-supplied description, including for Bash tools). The PreToolUse hook-block deny path already redacts it (_redacted_hook_block / _reject_hook_blocked), but every other deny path published the raw title — verified on main @ 8b502bc, 23 leaking surfaces in src/kiro_crew/dashboard/chat_runner.py:

  • 9 blocked transcript rows interpolated raw event.title into 🚫 … (invalid: …) / 🚫 … (hook error) rows that are broadcast to the dashboard AND persisted to the ConversationLog (auto-approve, context-builder continuation, trust-reads, trust/YOLO, interactive-approval paths).
  • 14 SEL audit calls passed tool_name=event.title unredacted — 8 under the rows above, 6 on paths whose row was already redacted (normal invocation, host-gate deny, trusted-pattern, trust-reads, batch rejection, interactive approve).

A credential or exfiltration URL the model plants in a tool title reached both surfaces verbatim.

Fix

Redact at the chokepoint, mirroring the existing hook-block pattern — one redaction site per deny shape, not nine inline copies:

  • New _redact_display_text() helper carries the redaction idiom (redact_exfiltration_urls + redact_credentials, idempotent, byte-identical for clean titles); _redacted_hook_block() is rewritten on top of it (behavior-identical).
  • New shared async helpers _reject_invalid_tool() and _reject_hook_error() render each deny shape in exactly one place (reject → redacted row → redacted audit), so a permission path added later cannot reintroduce the leak by omission. All 9 inline sites replaced; per-site control flow (continue/break) and per-site metadata={"reason": "interactive"} preserved.
  • The trust-reads site never audited the invalid-name shape; it passes audit=False to keep this change redaction-only.
  • The 6 audit-only sites reuse the redacted variable already in scope (_deny_title, _safe, _title, _approved_title) or call _redact_display_text(event.title).
  • The hook-error audit error field is also redacted: hooks are fired with the tool name and parsed input, so an exception that wraps its inputs can carry model-authored text.
  • The interpolated validation {e} needs no redaction: _validate_tool_name raises only fixed messages that never echo the offending name.
  • docs/system-specs/modules/security.md documents the chokepoint contract.

Tests

test/test_dashboard_approval.py::TestDenyRowTitleRedaction — 10 new tests, red-checked (all 10 fail on unfixed code with the raw credential leaking, pass with the fix):

  • 9 behavioral per-path tests driving each deny path with a credential-shaped title (assembled at runtime), asserting the secret reaches no transcript row and no audit tool_name, and [REDACTED: credential] appears in the blocked row. Both invalid-name and hook-error triggers covered where the path has both; the trust-reads test also pins the no-audit behavior.
  • 1 structural guard: 🚫 {event.title} and tool_name=event.title count zero in the module source, and each deny shape is rendered exactly once (its helper).

Verified locally: isort/flake8/mypy clean; test_dashboard_approval.py 60/60; full suite 39,496 passed (74 pre-existing host-environment failures — sandbox backend unavailable — identical on unmodified main); docs-lint and brand gate clean.

Out of scope (deliberate)

logger.warning(...) call sites (e.g. AUTO-REJECTED tool=%r, Slack-delivery failure, approval timeout) still write the raw title to the server log. Server-log redaction is a separate surface class from the issue's two (ConversationLog rows + SEL audit) and is intentionally not touched here; flagged by pre-push review and worth a follow-up issue.

Risk notes

Audit consumers that key on exact tool_name values will see the redacted form only for titles containing a credential or suspicious URL — identical to the change already made on the hook-block path. No behavior change for clean titles (both redactors return clean input unchanged).

Closes #2274

@bolichen97
bolichen97 requested a review from a team as a code owner August 9, 2026 19:59
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed e8d4d463e1b032964f7add279e5b664178e9c3a1 — this comment is updated in place on each push.

Review details

This is a security-hardening refactor: it consolidates title redaction into _redact_display_text() and two shared deny helpers (_reject_invalid_tool, _reject_hook_error), and routes all deny surfaces through them.

Let me verify the refactor preserves behavior and doesn't drop any audit/reject step.

Checking the key concerns:

  • Redaction preserved: Each converted inline site (event.title_redact_display_text(event.title)) matches the two-redactor sequence it replaces (redact_exfiltration_urls then redact_credentials), byte-identical for clean titles.
  • Reject/append/audit order: Helpers do reject_toolslot.appendsel().log_tool_invocation, matching the original inline order at every call site.
  • Control flow: Each converted site retains its trailing continue/break.
  • Outcomes/metadata preserved: invalid→denied, hook error→hook_error, interactive/trust_reads metadata threaded through.
  • Trust-reads path previously did reject+append with no SEL audit; routing it through _reject_invalid_tool now adds an audit record — additive, compliant with the SEL-audit invariant, and covered by the new test.
  • _reject_hook_error now also redacts the error field before auditing (additive safety); _reject_invalid_tool leaves the validation error unredacted, which is sound since _validate_tool_name raises fixed messages that never echo the title.

No guard removed without replacement; no new reachable leak on a changed line; no AUTOSDE rule violation (the redaction hardening strengthens the backend-security-controls "never trust LLM output" rule rather than weakening it).

No findings.

[OPUS-REVIEWED] e8d4d46

Verdict parsed from the review's SHA-scoped output markers for commit e8d4d463e1b032964f7add279e5b664178e9c3a1.

False positive or not applicable? A repository writer can comment:
/ai-review override fable e8d4d463e1b032964f7add279e5b664178e9c3a1: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of e8d4d463e1b032964f7add279e5b664178e9c3a1 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] e8d4d46

False positive or not applicable? A repository writer can comment:
/ai-review override gpt e8d4d463e1b032964f7add279e5b664178e9c3a1: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Advisory design-level review of e8d4d463e1b032964f7add279e5b664178e9c3a1 — updated in place on each push; does not block merge.

Design-Verdict: CONCERNS

Sound chokepoint design, but the description and the shipped code disagree on the trust-reads path — a claimed audit=False doesn't exist.

Watch

  • Phantom description / smuggled behavior change: the description says "The trust-reads site never audited the invalid-name shape; it passes audit=False to keep this change redaction-only," but _reject_invalid_tool has no audit parameter and always calls sel().log_tool_invocation — the trust-reads hunk passes metadata={"reason": "trust_reads"} and now emits a denied audit record that never existed on main. Cause: description written against an earlier revision → mechanism: reviewers and audit consumers trust the "redaction-only, no behavior change" risk note → consequence: a new SEL record class (denied/trust_reads) lands unreviewed, and the test (test_trust_reads_invalid_name_redacts) pins the opposite of what the description claims. Decide which is intended — if the new audit is deliberate (it is arguably the better behavior, matching every sibling path), say so in the description; if not, add the audit=False escape hatch the description promises.

[DESIGN-REVIEWED] e8d4d46

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 9, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator Author

Addressed the GPT 5.6 blocking finding in d58378e: removed the audit=False escape hatch from _reject_invalid_tool — the trust-reads invalid-name deny now emits a SEL denied record like every other path, tagged metadata={"reason": "trust_reads"}. The trust-reads test now pins the audited record (redacted tool_name + metadata) instead of the old no-audit behavior.

Also applied the Design review suggestion: the four remaining inline two-call redaction sites (_deny_title, _safe, _title, _approved_title) now call _redact_display_text(), making the helper the single redaction idiom on these surfaces.

@bolichen97
bolichen97 force-pushed the fix/redact-deny-titles-2274 branch from 57a9dbc to d58378e Compare August 9, 2026 20:10
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 9, 2026
event.title prefers the model's own description field, so it is
LLM-controlled display text. The PreToolUse hook-block deny path already
redacts it, but nine other deny rows interpolated the raw title into
blocked transcript rows (broadcast to the dashboard and persisted to the
ConversationLog) and fourteen SEL audit calls passed tool_name=event.title
unredacted, so a credential or exfiltration URL the model planted in the
title reached both surfaces verbatim.

Redact at the chokepoint instead of per site: a new _redact_display_text
helper carries the redaction idiom, and two shared deny helpers
(_reject_invalid_tool, _reject_hook_error) render each deny shape in
exactly one place so a permission path added later cannot reintroduce the
leak by omission. The six audit-only sites reuse the redacted variable
already in scope or call the helper directly. Both redactors return clean
input unchanged, so titles without credential/exfil content are
byte-identical in rows and audit records.

Closes #2274
@bolichen97
bolichen97 force-pushed the fix/redact-deny-titles-2274 branch from d58378e to e8d4d46 Compare August 9, 2026 20:17
@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 9, 2026
@pepmach
pepmach merged commit edba4af into main Aug 9, 2026
51 checks passed
@pepmach
pepmach deleted the fix/redact-deny-titles-2274 branch August 9, 2026 22:54
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 9, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
…ev#2274) (kirodotdev#2389)

event.title prefers the model's own description field, so it is
LLM-controlled display text. The PreToolUse hook-block deny path already
redacts it, but nine other deny rows interpolated the raw title into
blocked transcript rows (broadcast to the dashboard and persisted to the
ConversationLog) and fourteen SEL audit calls passed tool_name=event.title
unredacted, so a credential or exfiltration URL the model planted in the
title reached both surfaces verbatim.

Redact at the chokepoint instead of per site: a new _redact_display_text
helper carries the redaction idiom, and two shared deny helpers
(_reject_invalid_tool, _reject_hook_error) render each deny shape in
exactly one place so a permission path added later cannot reintroduce the
leak by omission. The six audit-only sites reuse the redacted variable
already in scope or call the helper directly. Both redactors return clean
input unchanged, so titles without credential/exfil content are
byte-identical in rows and audit records.

Closes kirodotdev#2274
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unredacted LLM-authored tool titles on deny paths

2 participants