fix(chat): offer only resolvable decisions on collapsed tool rows (#5434) - #5485
Conversation
) CollapsibleToolGroup offered a Trust decision on its inline approval row and its decision labels could report "Trusted", while the resolve path behind both ChatPage mounts — toApiDecision into the one-shot api.resolveApproval — has no trust verb: 'trust' was silently downgraded to a one-shot approve, claiming a standing grant the backend never records. This is the same overstating-the-grant wiring #5400 / PR #5433 fixed on the spawn-approval card, latent here behind hasPermission={false}. Unlike the spawn card, this component has one mount whose resolve path DOES honor trust: app-sdk ChatEmbed routes decisions verbatim through the slot approve endpoint (a deliberate, test-pinned fix). A blanket removal would regress that surface, so the mirror of #5433's principle — offer only verbs the resolve path honors — is applied per mount: - CollapsibleToolGroup gains a FAIL-CLOSED `canTrust` prop; the Trust button renders only when the mount declares it. - ChatMessageList threads `canTrust` (default withheld) and ChatEmbed — whose approve endpoint records standing trust — declares it. - ChatPage's mounts stay one-shot: toApiDecision no longer maps a stray 'trust' to 'approve' (fail-safe to reject) and now carries a comment naming the constraint so the next surface author does not re-earn #5400. - Tests pin the honest decision set on both sides of the seam, the fail-closed default, and (as a source contract, since the mounts are unreachable by construction) that neither ChatPage mount declares canTrust — all mutation-verified. No reachable rendered UI changes: both ChatPage mounts pass hasPermission={false}, and ChatEmbed keeps its full Approve/Trust/Reject row. Closes #5434
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: |
Design Review (Fable 5) — ✅ PASSDesign-level review of The description's claims check out: ChatEmbed routes decisions verbatim to the slot approve endpoint (which honors trust), both ChatPage mounts resolve through the one-shot Design-Verdict: PASS Fail-closed per-mount capability declaration correctly preserves the one surface whose resolve path honors trust; claims verified against the code. Suggestions
[DESIGN-REVIEWED] 51e6937 |
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS The Trust button now appears only where trusting actually persists — labels finally match the grant, and no reachable surface changes. The diff removes a latent lie: [UX-REVIEWED] 51e6937 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence gathered. Composing the review now. First-Principles-Verdict: CONCERNS The per-mount What this change shipsIntent: stop the collapsed tool row from offering a Trust that the main chat's resolve path silently records as a one-shot approve — a FIX.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 51e6937 |
Disposition — First Principles (CONCERNS)Watch: ChatInput's own Subtraction: drop the Item 6 "rides along, undeclared": fair on the PR body; the |
ChatInput offered Trust verbs whenever the source was attended, but its resolve path only honors them when a slot backs the composer: without `activeSlot`, a trust decision fell through to the one-shot `api.resolveApproval`, whose own `toApiDecision` mapped `trust` / `trust_reads` to a plain `approve` while `finish()` dispatched the trust verb — the composer reported a standing grant that was never created. Same class as #5400 (ChatPage) and #5434 (CollapsibleToolGroup): a surface must only offer trust verbs its resolve path honors. - Withhold the Trust dropdown and Trust reads button unless the slot-backed resolve path is available (`approvalCanTrust`). - Narrow `toApiDecision` to `approved -> approve, else reject` and carry the constraint comment from PR #5485 forward to this copy. - Route ALL trust verbs through the slot branch; the defence-in-depth downgrade (unattended source, or no slot) resolves one-shot AND now reports `approved` — the grant actually made — instead of the requested trust verb. - Rewrite the test that pinned the defect and add regression tests for the withheld controls and the granted-decision reporting. Closes #5486
#5486) ChatInput's own toApiDecision mapped trust/trust_reads to a one-shot approve for the api.resolveApproval fallback, so a Trust click landing with no activeSlot ran the tool once while finish() dispatched decision: 'trust' -- the composer reported a standing grant the backend never recorded. Mirrors the fix already merged for the same defect class at ChatPage (#5400, PR #5433) and the collapsed tool row (#5434, PR #5485): offer only trust verbs the resolve path honors, and keep the mapping fail-closed behind it. Refs #5486
#5486) (#8194) ChatInput's own toApiDecision mapped trust/trust_reads to a one-shot approve for the api.resolveApproval fallback, so a Trust click landing with no activeSlot ran the tool once while finish() dispatched decision: 'trust' -- the composer reported a standing grant the backend never recorded. Mirrors the fix already merged for the same defect class at ChatPage (#5400, PR #5433) and the collapsed tool row (#5434, PR #5485): offer only trust verbs the resolve path honors, and keep the mapping fail-closed behind it. Refs #5486 Co-authored-by: gh-autofix#2887 <chenmingwei23@users.noreply.github.com>
Summary
CollapsibleToolGroupoffered a Trust decision on its inline approval row (and could label a decision "Trusted"), while the resolve path behind bothChatPagemounts —toApiDecisioninto the one-shotapi.resolveApproval— has no trust verb: a trust click was silently downgraded to a one-shot approve, claiming a standing grant the backend never records. Same class as #5400 / PR #5433 (spawn-approval card), third surface after #5202/#5203. Latent today (hasPermission={false}on both mounts), fixed while still unreachable.Why not a blanket removal (divergence from #5433's shape, same principle): this component has one mount whose resolve path DOES honor trust — app-sdk
ChatEmbedroutes decisions verbatim throughPOST /api/chat/slots/{slot}/approve, a deliberate, test-pinned behavior ("sends Trust as trust, not as a plain approve"). Removing Trust outright would regress that live surface, and itsChatMessageListmock would keep vitest green while the real button vanished. So #5433's principle — offer only verbs the resolve path honors — is applied per mount:CollapsibleToolGroupgains a fail-closedcanTrustprop; the Trust button renders only when the mount declares it.ChatMessageListthreadscanTrust(default withheld; added to therenderItemmemo deps);ChatEmbeddeclares it.ChatPage's mounts stay one-shot:toApiDecisionno longer maps a stray'trust'to'approve'(fails safe to reject) and carries a comment naming the constraint, as CollapsibleToolGroup wires the #5400 trust/one-shot mismatch, latent behind hasPermission #5434 suggests.Testing
CollapsibleToolGroupCov80.test.tsx: default mount offers exactly Approve/Reject (mutation-verified: re-adding an ungated Trust button fails it);canTrustmount offers Approve/Trust/Reject and reportstrustverbatim.ChatMessageList.test.tsx:canTrustthreads through and is withheld by default (mutation-verified: dropping the thread fails it).ChatEmbed.test.tsx: the embed declarescanTrust(mutation-verified: dropping the declaration fails it) — pins the anti-regression for the one legitimate Trust surface.ChatPage.collapsedGroupTrust.test.tsx(new): source contract that neither ChatPage mount declarescanTrust(mutation-verified). A render test is impossible by construction:groupDisplayItemsskipspermissionrows entirely (the pinned ApprovalBar owns them), so no transcript can mount these groups — the same latency that made CollapsibleToolGroup wires the #5400 trust/one-shot mismatch, latent behind hasPermission #5434 an inspection finding.npx tsc -bclean; fullnpx vitest run23284 passed / 0 failed.Screenshots
Why no screenshot: no reachable rendered state changes. Both ChatPage mounts pass
hasPermission={false}(the approval row never renders there — verified unreachable by construction, see the new contract test), and ChatEmbed — the only production surface that renders the row — keeps its exact Approve/Trust/Reject set viacanTrust.Closes #5434