fix: scope the deny scan to command fields and keep redacted paths distinct - #9636
Conversation
|
Intent: Stop two egress-side false positives without weakening the deny path: a non-shell tool with client-established provenance is no longer refused for a document body that merely quotes a command shape or a credential path, and project-tree redaction keeps distinct paths distinct so the tree never silently drops a file. |
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS Both fixes are root-cause corrections with fail-closed provenance, floor-preserving redaction, and the frontend join semantics actually verified — the right shape, spec updated in-commit. The scan scoping keys on client-derived provenance plus a positive, producer-named tool list rather than the agent-influenced Suggestions
[DESIGN-REVIEWED] bbf90df |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsThe candidate list contains no candidates — the discovery pass reported "No candidates." I've independently examined the changed code for grounded defects (Step 2): the field-scoped scan's provenance gating ( No findings. [OPUS-REVIEWED] bbf90df |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of All checks are done. I have what I need for the verdict: the defects both have checkable provenance in the base tree (issue #8053 is cited in base First-Principles-Verdict: CONCERNS Two of the ten Not justified as shipped
What this change shipsInventory (9 items) — 6 justifiedIntent: stop the deny scan refusing benign document bodies and stop the dashboard silently dropping redaction-colliding files — a FIX (both defects checkable in base: issue #8053 at
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] bbf90df |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
3b720b1 to
c3b6568
Compare
|
self-added: yes
|
c3b6568 to
b18d250
Compare
|
self-added: yes
|
b18d250 to
6afc405
Compare
|
self-added: yes
|
6afc405 to
94a7223
Compare
4de14e6 to
2e6d3f7
Compare
|
Rebased onto current |
|
self-added: yes
|
2e6d3f7 to
6c42082
Compare
|
6c42082 to
1b795da
Compare
|
1b795da to
ccb53b0
Compare
…stinct The always-enforced tool_input scan in the permission funnel handed every string in the payload to the shell-command predicates for every tool, so a document write whose body merely quoted a denied command or named a credential path was refused, and a benign body over the command size ceiling was refused for its length. Only a shell tool executes its command field; a body is prose or source. For a non-shell tool whose provenance the client established from the tool_call frame (resolved non-shell classification, params from the cache, resolved tool identity), the scan now skips strings under the document-body keys and keeps every other string: a command word, every path spelling, a URL. A shell tool keeps the full scan over every string, and a frame missing any provenance flag is an unknown tool and keeps the full scan, fail closed. A walk that hits its work cap is denied as unverifiable. The security floor and the deny list are untouched. The project tree and git-status listings redact every path with the whole-string redactor, which collapses each matched token to one fixed tag. Two genuinely different paths whose only differing segment is credential-shaped therefore redacted to the same string, and the de-duplication that guards the dashboard tree against its "Duplicate path" crash then silently dropped one of them. Add a path-aware redactor that works segment by segment and suffixes a redacted segment with a short stable discriminator derived from the original segment, so distinct inputs stay distinct while no byte of the token survives. It never emits less redaction than the whole-string redactor it wraps: the segment-wise result is returned only when that redactor finds nothing left in it, otherwise the whole-string result is returned unchanged. Both listings use it through the context-aware redact shim and keep their de-duplication for the fallback case. Visible behavior change: cdk.out is now pruned from the tree's fallback directory walk beside node_modules, .venv, dist and build. Git listings already honor .gitignore and are unaffected.
|
self-added: yes
|
ccb53b0 to
bbf90df
Compare
Problem / Motivation
Two security-boundary defects on the permission funnel and the dashboard egress path, batched because both are the same shape: a check that is right about the mechanism but wrong about the field it reads.
rm -rf /,git push origin main) or named a credential path was refused, and any benign body over the command size ceiling was refused for its length.Why it matters
The first defect blocks legitimate agent work (writing docs, tests or scripts that mention a command) with a security refusal, which trains users to distrust the deny list. The second makes the dashboard tree and git panel lie by omission: a file exists in the repository and is missing from the listing, with no indicator that anything was dropped.
What changed (motivation → approach → change)
Behavior changes
diff_signals.pyflagged no structural!signals.fs_write/writenames): strings directly undercontent,fileText/file_text,text,newStr/oldStr,new_str/old_str,newText/oldTextare no longer run through the shell-command deny predicates. The claude-agent-acp backend's tools are not on the list because their frames carry no_meta.kiro.toolName, so they keep the full scan. Every other string still is. Shell tools, MCP tools, other built-ins and unclassified frames are unchanged.Blocked: tool arguments too large to security-scan (deny-by-default)instead of being scanned partially.~plus 12-character label (for example[REDACTED_AWS_KEY]~3f9a1c0b7d2e); the label is stable within one gateway process and changes on restart.docs/system-specs/modules/security.md) documents both mechanisms.Tests
New:
test/test_llm_helpers_non_shell_gate.py(24 tests): body keys skipped only when all three provenance flags hold; shell tools and every partial-provenance combination keep the full scan; a command word, a path and a URL in a non-body field are still denied; a mapping under a body key is still walked; a truncated walk is denied; the security floor is untouched.test/test_redact_path_segments.py: distinct credential-shaped segments stay distinct; identical originals map to identical outputs; no byte of the token and no unkeyed hash prefix of it survives; the label is stable across calls and listings, depends on the process key, and is independent of input order; the whole-string fallback and fixed-point properties hold.Updated:
test/test_project_tree.py(+2): redaction-collision paths stay distinct; a true collision is still de-duplicated.test/test_project_git_status_log.py(+2): redaction-collision files stay distinct; a true collision is still de-duplicated.Commands run locally (exit codes checked, output redirected to files):
.venv/bin/python -m pytest -q test/test_llm_helpers_non_shell_gate.py test/test_redact_path_segments.py test/test_project_tree.py test/test_project_git_status_log.py test/test_llm_helpers_edit_gate.py: 109 passed.prepare-pr/profiles/kirocrew.json: see the gate table in the PR's first comment thread / PREP notes; every backend and meta gate green, frontend build andtsc -bgreen, remaining frontend gates skipped because the diff does not touchwebsite/.Manual verification
N/A: unit coverage exercises both handlers through their aiohttp test clients and the permission funnel through its event fixtures; there is no UI change to inspect.
Related Issues
no linked issue: closeout batch from an internal deployment, no GitHub issue tracks it.
Pattern harvest
Rule candidate: review-prompt
Pattern: "a security check applied to the wrong field shape: a shell-command predicate over a document body, or a whole-string redactor over a structured path whose consumer de-duplicates". Both fixes keep the mechanism and change only which strings reach it; a reviewer prompt asking "what does this field actually carry, and what consumes the output" would have caught both at authoring time.
Not in scope
assert_security_floor, the deny catalog, orMAX_SCANNABLE_COMMAND_CHARS._edit_target_denial) beyond the spec sentence that now describes non-edit kinds elsewhere.redactkeep the whole-string form./only, because the listings it serves emit POSIX-relative paths.Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)