Skip to content

fix(chat): stop offering trust tiers a spawn approval cannot grant (#5400) - #5433

Merged
iamwhatever merged 1 commit into
mainfrom
fix/spawn-approval-trust-label-5400
Aug 24, 2026
Merged

fix(chat): stop offering trust tiers a spawn approval cannot grant (#5400)#5433
iamwhatever merged 1 commit into
mainfrom
fix/spawn-approval-trust-label-5400

Conversation

@bolichen97

Copy link
Copy Markdown
Collaborator

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-shot api.resolveApproval endpoint, which has no trust verb, so every tier collapsed to a plain approve. 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 (the TrustDropdown is gone), and decisionLabel can only ever read Approved / Rejected. The approval_type === 'chat' branch in ApprovalEntry.onAction (and its slot prop / pattern param) is deleted: ApprovalEntry is module-local with a single mount behind the isSpawnApproval filter, 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 (ChatInputapproveChatSlot) is untouched.
  • i18n catalogs — the three now-unreferenced keys (pages.chat.activityViewer.trusted / trusted_base / trusted_command) are removed from all 13 catalogs (the deadKeys gate is a ratchet); en-XA regenerated via npm 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 dropdown vi.mock is removed — verified nothing else in the file renders a Radix dropdown (40/40 pass without it).
  • Capture harnesswebsite/capture/spawn-approval-trust.{html,tsx} mounts the REAL ActivityViewer with a pending spawn approval through its own toolLog seam; website/scripts/capture-spawn-approval-trust.mjs asserts 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)

Before After
Dark before-dark after-dark
Light before-light after-light

Tests

  • npx tsc -b clean; npx vitest run targeted: ActivityViewer suites (75), full src/i18n (629), TrustDropdown — all green; full suite runs in CI.
  • i18n gates: check-i18n-keys (12674 refs resolve), lint:i18n at baseline, deadKeys ratchet intact (keys and references removed together).
  • eslint: 0 errors, no warnings in changed files; jscpd clean.
  • Pre-push review: Opus (claude-opus-5) — no blocking findings, 2 fixable advisories addressed in this commit (dead-branch deletion, exact-match capture wait); a third (latent same-class wiring in 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 against codex-review.yml's WHAT-BLOCKS list — the server-side GPT review lane on this PR remains the authoritative gate.

…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
@github-actions

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of a8aa9b23b0352a8ba1fd113b00490f08d296659c — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

@github-actions

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of a8aa9b23b0352a8ba1fd113b00490f08d296659c — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

The diff checks out against the description: isSpawnApproval at ActivityViewer.tsx:244 excludes approval_type === 'chat', confirming the deleted chat branch was unreachable; TrustDropdown remains in use by the real chat trust path (ChatInput.tsx, ApprovalCard.tsx), which is untouched. The capture harness and screenshot directories follow long-standing repo convention. Removing the false affordance (rather than building trust semantics into the one-shot resolveApproval endpoint) is the narrower, reversible option and matches the direction of the cited precedents.

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

@github-actions

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of a8aa9b23b0352a8ba1fd113b00490f08d296659c and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] a8aa9b2

False positive or not applicable? A repository writer can comment:
/ai-review override gpt a8aa9b23b0352a8ba1fd113b00490f08d296659c: <one-sentence reason>

@github-actions

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed a8aa9b23b0352a8ba1fd113b00490f08d296659c — this comment is updated in place on each push.

Review details

The diff is a clean, well-scoped removal. The isSpawnApproval filter (line 244) excludes approval_type === 'chat', so the removed chat branch in onAction was dead for this card; the resolveApproval path is preserved. Grep confirms no surviving references to the removed activityViewer.trusted* keys. decisionLabel now carries only approved/rejected, the only values onAction can set. Capture files are test-only tooling, not bundled.

No findings.

[OPUS-REVIEWED] a8aa9b2

Verdict parsed from the review's SHA-scoped output markers for commit a8aa9b23b0352a8ba1fd113b00490f08d296659c.

False positive or not applicable? A repository writer can comment:
/ai-review override fable a8aa9b23b0352a8ba1fd113b00490f08d296659c: <one-sentence reason>

@github-actions

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of a8aa9b23b0352a8ba1fd113b00490f08d296659c — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Verification is complete. The fix checks out: the spawn card resolves through api.resolveApproval (website/src/api/client.ts:2459, approve|reject only), the deleted chat branch was unreachable (isSpawnApproval filters approval_type !== 'chat', single mount), the removed i18n keys have zero remaining references, and the capture/screenshot files follow the documented temp-screenshots/README.md convention with 225 sibling capture-*.mjs scripts. I counted one latent same-class sibling (CollapsibleToolGroup's trust→approve mapping in ChatPage), which is unreachable today and declared as a follow-up.

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 ships

Intent: stop the spawn-approval card from reporting "Trusted" when no trust is ever granted (#5400) — a FIX.

  1. Spawn-approval card offers only Approve / Reject; Trust dropdown gone — justified (the fix)
  2. Resolved spawn card can only read Approved / Rejected, never "Trusted" — justified (the fix)
  3. Dead chat-approval branch and slot prop deleted from the card — rides along, declared, pure deletion
  4. Three "Trusted…" strings removed from all 13 language catalogs — justified (deadKeys ratchet requires keys and references leave together)
  5. Trust-dropdown test replaced by one pinning the Approve/Reject-only row — declared
  6. New capture page + assert-before-shoot screenshot script — rides along, declared; matches the repo's 225 existing capture-*.mjs scripts
  7. Four before/after PNGs under temp-screenshots/ — declared, sanctioned by temp-screenshots/README.md and its cleanup workflow

Watch

One counted unfixed sibling of the root cause (grepped toApiDecision|trust): ChatPage.tsx:4464 maps 'trust' → plain 'approve' behind CollapsibleToolGroup's Trust button — unreachable today (both mounts pass hasPermission={false}, ChatPage.tsx:6947, 6995) and declared as a filed follow-up. Note the general fix is not deleting the button: the third mount (app-sdk/ChatEmbed.tsx:183) routes trust verbatim through the slot endpoint and is honest.

Subtractions

Drop 'trust' from toApiDecision in ChatPage.tsx:4463-4464 — the only control that could send it is unreachable (both mounts pass hasPermission={false}), so the branch is dead wiring of exactly the class this PR removes.

[FIRST-PRINCIPLES-REVIEWED] a8aa9b2

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 24, 2026
@iamwhatever
iamwhatever merged commit 7c64354 into main Aug 24, 2026
63 checks passed
@iamwhatever
iamwhatever deleted the fix/spawn-approval-trust-label-5400 branch August 24, 2026 03:25
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 24, 2026
bolichen97 pushed a commit that referenced this pull request Aug 24, 2026
) (#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>
chenmingwei23 added a commit that referenced this pull request Sep 3, 2026
#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
bolichen97 pushed a commit that referenced this pull request Sep 3, 2026
#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spawn-approval trust tiers grant one-shot approval but the card reports "Trusted"

2 participants