feat(tui): explain tool auto approval - #12728
Conversation
Auto-approval provenance was only recorded on the metadata of allowed tool calls (state.metadata.approval), so denied calls had no structured explanation of which rule/config/agent denied them. Since 'kilo export' serializes state.metadata verbatim into the JSON session log, denials showed up with no provenance at all. Add PermissionProvenance.classifyDenial, which reads the deciding deny rule off a DeniedError's tagged ruleset and classifies it the same way approvals are classified. Wire it into SessionTools' ctx.ask via Effect.tapErrorTag so denials are recorded before the tool call fails, reusing the existing carryApproval/failToolCall preservation so the metadata survives onto the final error state.
Ports the auto-approval provenance explanation already shown in kilo-ui/vscode to the TUI. Adds a Kilo-owned tool-approval.tsx with a plain-text description helper (describeApproval) and a shared ApprovalNote row component, then wires a single call into InlineTool/ InlineToolRow (Shell, Read, Grep, Glob, WebFetch, etc.) and BlockTool (Write, Edit, ApplyPatch, Task), showing a muted line under completed/ failed tool calls. Todo writes are excluded via a hideApproval prop, mirroring the kilo-ui behavior that treats them as orchestration rather than an auditable action.
Code Review SummaryStatus: 4 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Resolved since the previous review
Files Reviewed (4 files, incremental)
Notes: incremental review of Fix these issues in Kilo Cloud Previous Review Summaries (2 snapshots, latest commit a07661b)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit a07661b)Status: 4 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Resolved since the previous review
Files Reviewed (7 files)
Notes: the Fix these issues in Kilo Cloud Previous review (commit 6b27a26)Status: 4 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (6 files)
Notes: the shared-file hooks in Reviewed by claude-opus-5 · Input: 52 · Output: 17.5K · Cached: 1.7M Review guidance: REVIEW.md from base branch |
|
Why do we show this in the TUI and in vscode on the top? The bottom feels consistent, but right now in the TUI it looks like the annotation is part of the output. |
|
@marius-kilocode Good catch on the text discrepancy; it makes sense to make things as consistent as possible. I played around with the placement of the approval text, and came up with this. WDYT?
|
The note was appended after the tool's own output (or, in an interim
revert, on its own line above it), which either looked like part of
the output or was visually noisier than desired. Render it inline on
the header/title line instead, matching the existing RoutedModelMeta
badge convention (' · note'), so it reads unambiguously as metadata
about the call rather than output.
…ed them
DeniedError.ruleset only carried the deny-permission subset, so
PermissionProvenance.classifyDenial had to guess the deciding rule via
findLast(action === "deny"). With two deny rules for different
patterns under the same permission (e.g. bash: { "git push *": deny,
"rm -rf *": deny }), this could attribute a denial to whichever rule
sorted last instead of the one that actually matched the request.
Permission.ask now embeds the exact rule resolve()/evaluate() matched
against the request's pattern directly on the error (ruleset: { rule,
matches }), so classifyDenial reads it instead of re-deriving it.
Some denials carry no rule at all (e.g. the headless-subagent policy
denial), where classify({ rule: undefined }) reports the same
{ source: "default" } shape as the *approval* fallback -- silently
rendering a refusal as an auto-approval in the TUI and kilo export.
classifyDenial now synthesizes an explicit deny rule for the request's
permission/pattern in that case, so rule.action always reflects the
real outcome.
Adds test/kilocode/permission/deny-provenance.test.ts covering both
regressions against the real Permission.Service, and updates the
existing session-tools.test.ts denial fixture to the new ruleset
shape.
…ssion/index.ts
Applies the kilocode-merge-minimizer skill to the prior fix. The
hard-veto and headless-subagent DeniedError sites are reverted to
their exact pre-fix shape -- neither carries a specific rule anyway,
so wrapping their ruleset in a { rule, matches } object added shared
upstream diff for no benefit. Only the main deny path (which already
had the deciding rule in scope) still changes, and now passes the
bare rule instead of a wrapper object, shrinking that hunk from a
multi-line block to a single-line swap.
PermissionProvenance.classifyDenial now duck-types ruleset as a
possible bare Permission.Rule (checking action === "deny" and a
string pattern) instead of expecting a { rule } wrapper, so it still
reads the main deny path's rule directly while falling back to a
synthesized deny rule for the other paths, exactly as before.
Net shared-file diff across permission/index.ts, session/tools.ts, and
the TUI's routes/session/index.tsx for this whole feature is now 9
insertions / 12 deletions, down from ~50+ lines.
…o-approval-tui feat(tui): explain tool auto approval

Issue
Fixes https://docs.google.com/document/d/1eXKc_A9f_mVuc3LiniP3gbLWm7Ic_cPe58z1Tq_ZlzY/edit?pli=1&tab=t.0
Context
#12494 and #12556 added a "why was this tool call approved" line to the kilo-ui/VS Code chat, sourced from
state.metadata.approvalon the tool part, but two gaps remained: denied calls never got that metadata (sokilo export's JSON session log had no record of why a call was refused), and the TUI never rendered the line at all, even though the metadata was already available to it.Implementation
PermissionProvenance.classifyDenialreads the decidingdenyrule off aDeniedError's tagged ruleset and classifies it the same way approvals are classified. Wired intoSessionTools'ctx.askviaEffect.tapErrorTag, so the denial reason is recorded on the tool call's metadata before the call fails, reusing the existingcarryApproval/failToolCallpreservation so it survives onto the final error state and shows up inkilo export.packages/tui/src/kilocode/tool-approval.tsx(plain-textdescribeApproval+ a sharedApprovalNoterow), wired with a single call at each tool-rendering site (InlineTool/InlineToolRowfor Shell, Read, Grep, Glob, WebFetch, etc.;BlockToolfor Write, Edit, ApplyPatch, Task). Todo writes are excluded via ahideApprovalprop, matching the kilo-ui behavior from fix(ui): refine auto-approval line for subagent and todo tools #12556 that treats them as orchestration rather than an auditable action.session/tools.ts,tui/routes/session/index.tsx) down to minimal hooks; all classification/formatting logic lives in Kilo-ownedkilocode/modules.Screenshots / Video
How to Test
Manual/local verification
bun run typecheckclean across the monorepo (bun turbo typecheck, 23/23 tasks).bun testinpackages/opencode(targeted:test/kilocode/sandbox/session-tools.test.ts,test/kilocode/permission/*) andpackages/tui— all passing, including a new regression test asserting a denied tool call records{ source, rule }on its metadata.bun run script/check-opencode-annotations.ts --worktreepasses.Reviewer test steps
bun dev -- /path/to/scratch/projectauto-approved by the code agent..kilo/kilo.jsonwith{ "permission": { "bash": { "echo *": "deny" } } }, restart, ask it to runecho hi— the call fails and the row showsdenied by the project config (matched bash \echo *`)`.kilo export <sessionID>after step 3 and confirmmessages[].parts[].state.metadata.approvalon the tool part has the same{ source: "project", rule: {...} }shape for the denial.