fix(channels): per-command trust tiers on approval cards (#5231) - #5248
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Confirmed: the base tier renders for any shell-titled card ( Design-Verdict: CONCERNS Server-side literal-grant binding is sound, but tier availability is inferred from parsed display text, so the "doomed option" harm partially survives. Watch
Suggestions
[DESIGN-REVIEWED] decedb2 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS Trust-menu consent labels under-specify their scope, and a card the server just declared stale re-arms live Approve/Reject buttons. Watch
Suggestions
[UX-REVIEWED] decedb2 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of I have everything I need. Verifying the key counts one more time before writing: the base-state test the diff removes ("offers only the plain trust action on a channel approval — no command-scoped tiers (#4421)") confirms the doomed tiers were already hidden at base, and First-Principles-Verdict: CONCERNS Framed as a fix, but #4421 already hid the doomed tiers at base — this ships a new scoped-trust capability, justified, yet with a second trust-matching language beside What this change shipsIntent: let a channel operator grant narrow per-command trust instead of only blanket channel-wide trust, and name the real command on the card. Framed as a FIX; the substance is an ADDITION plus one genuine fix.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] decedb2 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsNo blocking issues — one advisory display bug survives. FINDING — website/src/pages/ChannelPage.tsx (the [OPUS-REVIEWED] decedb2 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
4c60624 to
e5feded
Compare
|
Dispositions for the GPT 5.6 review of Stale cards can trust a newer command (
The request-body Exact-command grants retain glob semantics (
Exact grants are now |
|
Dispositions for the First Principles review of Blocker: dead
The premise was correct on that head: the parameter had zero consumers. The same round's GPT review found the stale-card TOCTOU (an old card's click binds to whatever is pending NOW), and the pattern is precisely the data that closes it — it is now REQUIRED and validated as the consent proof ( Watch: the chat endpoint still binds client-supplied patterns verbatim — accepted-and-deferred, filed as #5260 (together with the Design lane's shared-module suggestion, since deriving chat grants server-side naturally produces the shared engine). Subtraction: un-export |
|
Dispositions for the UX review of Silent fake success on non-shell tools — fixed with the review's own smallest-fix suggestion: per-command tiers are now gated out of non-shell channel cards (
Light-theme menu ghosting — fixed: it was a mid-animation capture artifact (Radix animates the menu in with opacity). The capture script now waits for the menu's animations to finish before writing a frame, and all three screenshots were recaptured and visually re-verified — the light-theme menu is fully opaque. New frames are SHA-pinned to |
|
Disposition for the Design review suggestion on
|
e5feded to
3eda4c0
Compare
|
Disposition for the GPT 5.6 review of
|
3eda4c0 to
6f65721
Compare
bolichen97
left a comment
There was a problem hiding this comment.
Approving per triage sweep: readiness passed, no blocking reviews, fix-type change. Auto-merge will be enabled; branch protection still gates.
0a825b2 to
e1e27fc
Compare
Audit note — part of this has already landed; the rest has notThis PR is not a duplicate and is not finished by anything on Already landed
Which parts
|
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
e1e27fc to
a461bf9
Compare
a461bf9 to
3428fd0
Compare
|
Disposition for the GPT 5.6 review of
|
|
Disposition for the GPT 5.6 review of
|
|
Disposition for the GPT 5.6 review of
|
|
Disposition for the GPT 5.6 review of
|
|
Disposition for the Design Review (CONCERNS) on Base tier still renders for compound/quoted/env-prefixed commands the server refuses — deferred to #5250 (server-declared tier availability in the card metadata), with the harm now bounded: PR #5233 has MERGED, so a refused tier click rolls the card back and surfaces the error instead of dying silently — the residual failure is one visible, recoverable error, not a dead end that teaches over-granting. The suggested client-side mirror of |
|
Disposition for the UX Review (CONCERNS) on Scope-silent tier labels (agent-scoped, runtime-only grants reuse chat's unqualified labels) — legitimate; folded into #5250's scope-accurate-labeling item (channel-specific keys mirroring Disposition for the First Principles Review (CONCERNS) on "Framed as a fix, but #4421 already hid the doomed tiers at base — this ships a new capability" — the framing is accepted and already declared: the PR body's Pattern harvest and description state that the fix "necessarily ships the missing capability". #4421's hiding was the stopgap for the 400s; the root harm it left standing (the only grant available is blanket channel trust, so scoped consent pushes users to over-grant) is what this PR fixes, and fixing it IS the capability. The inventory's items 1–8 each carry this review's own "justified/declared" verdict. |
Problem / Motivation
Channel approval cards render the shared
TrustDropdown(ApprovalCard defaultsshowTrust=true), which offers three trust tiers:trust,trust_command, andtrust_base. But two of the three could never succeed on a channel card:api_channel_approve_agentwhitelisted only("approved", "rejected", "trust")and returned 400invalid actionfortrust_command/trust_base.ChannelPage.tsxonApprovecall sites dropped thepatternargument, andapi.channelApproveAgenthad no pattern parameter.msg.fromRole) as the ApprovalCard title, so the dropdown offered to trust a "command" likedev— and on the ACP path the approval message carried no tool name at all (event.textis empty; onlytitleis populated), rendering⚠️ Approval needed: ****.Why it matters
The UI offers actions that always fail — the user picks a scoped, deliberately narrow grant ("trust this one command") and gets an
invalid actionerror, pushing them toward the much wider blankettrust(auto-approve everything for the channel). A scoped-consent control that fails teaches users to over-grant.What changed (motivation → approach → change)
Symptom → root cause: the trust tiers exist only in the chat approval path (
chat_handlers.pywrites session-scoped_trusted_patterns); channel agents had no per-command trust seam at all, and the channel card never carried the data a scoped grant needs.Approach: give
ChannelAgentthe same session-scoped seam, but derive grants server-side from the pending approval's canonical shell command, with the client-suppliedpatternserving as the consent proof: it must agree with the pending command, so a stale card (whose pattern describes an older command) or an LLM-influenced title that diverged from the real command fails closed with400 approval_supersededinstead of trusting a command the user never read. Display titles are LLM-influenced (an agent-authored title likeRunning: *must not be able to widen a grant to everything), so the pattern can only ever narrow or refuse, never widen. This is deliberately stricter than the chat path on several axes flagged by review:_stream_taskstashesagent._pending_approval_command(extracted from the provider event'stool_input) while the approval is pending; the handler derives the grant from that stash and requires the card'spatternto agree with it.rm secret.tmpfromrm *.tmp; per-segment grants liftingrm targetout of itscd /tmp/safe &&context; naive first-token bases turning a quoted"./my tool"into a"./myprefix grant; env prefixes and case-folded matching), so the restructure removes derivation entirely: the exact tier stores the whole command text matched by case-sensitive string equality, and the base tier stores one shlex-derived binary name — refused outright for compound, quoted, env-prefixed, or unparseable commands (400 pattern_underivable). A grant can never cover text the user did not read. Every refusal is SEL-audited (trust_pattern_denied). A command the provider REDACTED is never a grant target (two commands differing only in credentials redact to the same text).tool_inputonly when the provider classified the tool as shell (event.is_shell). A non-shell MCP tool whose arguments carry a nested"command"key (e.g.cron_add) can never inherit a shell grant, a non-shell pending tool gets a distinct400 pattern_underivable, and the card hides the per-command tiers for non-shell tools AND for redaction-marked inputs (both refused server-side), so the doomed options never render.Changes:
src/kiro_crew/channel.py—ChannelAgent._trusted_commands/_trusted_bases+_pending_approval_command(runtime-only, not persisted: grants are session-scoped like chat's); shell-gated literal-grant auto-approve in_stream_task(SEL-logged asauto_approved_trusted_pattern); the approval card names the CANONICAL command for shell tools (kiro's shelltitlecan be model-authored prose, which would make the tiers' consent proof mismatch the real command), withevent.titleas the non-shell fallback.src/kiro_crew/dashboard/handlers_channel.py— whitelist widened; per-tier grant semantics as above; grants SEL-logged (trust_pattern_grantedwith the granted patterns);ch.trusteduntouched for the per-command tiers (blankettrustunchanged).website/src/api/client.ts/website/src/pages/ChannelPage.tsx—patternplumbed through all threeonApprovecall sites (the server's consent proof);approvalToolTitle()extracts the embedded tool name so the TrustDropdown labels the real command instead of the agent role; per-command tiers render only for shell-titled cards (perCommandTiersprop onApprovalCard/TrustDropdown, default unchanged for other surfaces).docs/system-specs/modules/persistent-agent-channels.md— approval flow / security / API rows updated in the same commit.Known limitation (deliberate): per-command tiers are shell-only in this PR — non-shell channel cards no longer offer them (blanket trust remains), and the endpoint refuses with
pattern_underivablefor direct API callers. Structured pattern metadata on channel approval messages (mirroring chat'sperm_meta) is follow-up work — see the issue linked below.Tests
Backend (
test/test_handlers_channel_approve.py, new):trust_commandbinds the pending command as a literal, resolves the future asapproved, never setsch.trusted400 approval_superseded, no grant (mutation-verified: removing the consent check fails 2 tests)rm *.tmpnever matchesrm secret.tmp; a compound grant matches ONLY the identical pipeline, never a lifted segment (mutation-verified against substring matching)pattern: "*"can never scope a grant (mismatch → fail closed)trust_baserefuses compound, quoted-executable, env-prefixed, and substitution commands (mutation-verified against naive tokenization); the granted binary covers simple invocations only400codes, no grant, future untouched./Deploy.shgrant never matches./deploy.sh, mutation-verified against lowercase folding)trust_pattern_denied)approved/rejected/trust/ invalid action / no-pending behavior unchangedBackend (
test/test_channel_trusted_patterns.py, new):"command"key never matches (mutation-verified: removing theis_shellgate fails 2 tests)tool_input)_trusted_patterns/_pending_approval_commandare runtime-only (not serialized)Frontend (
website/src/test/ChannelPageCoverage.test.tsx):trust_command/trust_baseforward the pattern tochannelApproveAgent(mutation-verified: reverting the call-site plumbing fails 3 tests)Manual verification
Capture harness (
website/capture/channel-trust-tiers.*+website/scripts/capture-channel-trust-tiers.mjs) mounts the REAL ApprovalCard with the title resolved by the realapprovalToolTitlefrom a backend-shaped approval message; each scene asserts the exact-command tier offers the command (not the agent role) before writing a frame, so a before-state cannot produce these images.Note: CI's Frontend Tests shard 4 initially failed on the known
CliPanelCoveragehappy-dom WeakRef leak (this PR's added test files shifted the shard boundaries onto it); main's own fix (#5252, happy-dom ≥20.11.5) landed mid-review and this branch is rebased onto it — the full frontend suite now passes with zero failures.Local gates: isort / flake8 / mypy / black gate clean; full backend suite 61,814 passed (10 failures are pre-existing host-environment issues —
/tmpcwd artifact-source classification and xdist host-budget caps — reproduced identically on unmodified code);npx tsc -bclean; full vitest 22,942 passed, 0 failed (post-rebase, happy-dom 20.11.6).Screenshots / video
Channel approval card with the trust menu open — the tiers now name the real command:
More variants
Note: open PR #5233 (the #5204 rollback fix) edits the same three
onApprovecall sites; whichever lands second has a small mechanical conflict (combine pattern forwarding with the un-catched promise return).Related Issues
Closes #5231
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Pattern harvest
Rule candidate: never derive a trust/auto-approve grant from client-supplied or LLM-influenced text (display titles, glob patterns). Bind the grant server-side to the pending action's canonical text, and use the client-echoed pattern only as consent proof that must match — so client input can narrow or refuse a grant, never widen it. Every review round that found a scope leak here (raw globs, per-segment matching, first-token bases, env prefixes, case folding) was an instance of the same rule.