fix(chat): stop offering trust tiers a spawn approval cannot grant (#5400) - #5433
Conversation
…5400) Spawn approvals in the activity viewer resolve through the one-shot api.resolveApproval endpoint, which has no trust verb: every tier the card's TrustDropdown offered (trust / trust_command / trust_base) collapsed to a plain approve, while the decision label still reported "Trusted". The UI overstated the grant — the next identical call prompts again and the user cannot tell whether trust failed to stick. Narrow the surface to the honest set: the spawn-approval card renders only Approve / Reject, and its decision label can only ever read Approved / Rejected. The chat-approval path (which does honor trust, via approveChatSlot) is untouched — ApprovalEntry mounts solely behind the isSpawnApproval filter. The three now-unreferenced catalog keys are removed from every locale (deadKeys is a ratchet) and the pseudolocale regenerated. Screenshot evidence under temp-screenshots/spawn-approval-trust/ is shot by an asserting harness: the after-frame assertion fails on the pre-fix code. Closes #5400
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Removing the trust tiers that never granted trust makes the card honest — Approve/Reject now says exactly what happens, in both themes. [UX-REVIEWED] a8aa9b2 |
Design Review (Fable 5) — ✅ PASSDesign-level review of The diff checks out against the description: Design-Verdict: PASS Removing an affordance the backend never honored fixes the consent-integrity mismatch at its root; the narrower surface beats retrofitting trust verbs onto a one-shot endpoint. [DESIGN-REVIEWED] a8aa9b2 |
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: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe diff is a clean, well-scoped removal. The No findings. [OPUS-REVIEWED] a8aa9b2 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) — ✅ PASSPremise-level review of Verification is complete. The fix checks out: the spawn card resolves through First-Principles-Verdict: PASS The card claimed a standing trust grant the one-shot endpoint cannot make; deleting the claim is the subtractive fix at cause level. What this change shipsIntent: stop the spawn-approval card from reporting "Trusted" when no trust is ever granted (#5400) — a FIX.
WatchOne counted unfixed sibling of the root cause (grepped SubtractionsDrop [FIRST-PRINCIPLES-REVIEWED] a8aa9b2 |
) (#5485) 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 Co-authored-by: Zezhen Xu <zezhexu@dev-dsk-zezhexu-2b-15d11a49.us-west-2.amazon.com>
#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
Closes #5400.
On a spawn approval in the chat activity viewer, the trust dropdown offered "Trust this command" / "Trust all
<base>commands" / "Trust all tools", and picking any made the card report "Trusted" — but no standing trust was ever granted. Spawn approvals resolve through the one-shotapi.resolveApprovalendpoint, which has no trust verb, so every tier collapsed to a plainapprove. The UI overstated the grant: the next identical call prompts again, and the user cannot tell whether trust "did not stick" or was silently downgraded. This is #5203's consent-integrity defect in the opposite direction, and the fix takes the same narrow-the-surface direction #5202 took for the channel surface.What changed
website/src/pages/chat/ActivityViewer.tsx— the spawn-approval card renders only Approve / Reject (theTrustDropdownis gone), anddecisionLabelcan only ever read Approved / Rejected. Theapproval_type === 'chat'branch inApprovalEntry.onAction(and itsslotprop /patternparam) is deleted:ApprovalEntryis module-local with a single mount behind theisSpawnApprovalfilter, so the branch was unreachable — and had it ever become reachable it would have produced the inverse mismatch (silently withholding tiers the chat surface genuinely grants). The real chat-approval trust path (ChatInput→approveChatSlot) is untouched.pages.chat.activityViewer.trusted/trusted_base/trusted_command) are removed from all 13 catalogs (thedeadKeysgate is a ratchet);en-XAregenerated vianpm run i18n:pseudo.website/src/test/ActivityViewerCoverage.test.tsx— replaces the trust-dropdown test with one pinning the honest set: the action row is exactly Approve/Reject (asserted by length and order) and the resolved card reads "Approved", never "Trusted". Mutation-verified: the new test fails (and is the only failure) with the fix reverted. The now-unneeded Radix dropdownvi.mockis removed — verified nothing else in the file renders a Radix dropdown (40/40 pass without it).website/capture/spawn-approval-trust.{html,tsx}mounts the REALActivityViewerwith a pending spawn approval through its owntoolLogseam;website/scripts/capture-spawn-approval-trust.mjsasserts each frame's action row before writing (the after-assertion fails on pre-fix code, so an after-frame cannot be shot from the old card by mistake).Screenshots (SHA-pinned)
Tests
npx tsc -bclean;npx vitest runtargeted: ActivityViewer suites (75), fullsrc/i18n(629), TrustDropdown — all green; full suite runs in CI.check-i18n-keys(12674 refs resolve),lint:i18nat baseline,deadKeysratchet intact (keys and references removed together).CollapsibleToolGroup, unreachable today) is being filed as a follow-up issue. GPT lane degraded (gpt-5.6-sol and gpt-5.6-terra both stalled at 30 min); backstopped by a contract-driven self-review againstcodex-review.yml's WHAT-BLOCKS list — the server-side GPT review lane on this PR remains the authoritative gate.