Read a sandboxed tool's refusal under the id its renderer is given - #402
Merged
Merged
Conversation
kevin9327
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
September 6, 2026 13:28
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
# Conflicts: # CHANGELOG.md
davidmckayv
approved these changes
Sep 6, 2026
davidmckayv
left a comment
Contributor
There was a problem hiding this comment.
Deep-reviewed against live code (correctness, governance, no vendor/secret/scale issues). Composed build+tests green. CHANGELOG/format rebase on CI-validated substance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
A sandboxed component that the server refused is drawn anyway. The refusal card cannot appear,
because the key it is looked up under is always
undefined.The handler records the refusal under the id it is given:
and the renderer reads it back under a different one:
A renderer's props are not a handler's context. The id arrives flat, as
toolCallId. Both siblingsin this folder already read it that way —
gallery-tools.tsx:93types its props{ toolCallId?: string; … }, andcomputer-tools.tsx:296destructures({ result, status, toolCallId })— and so does the library's own renderer:computer-tools.tsx:274even converts between the two shapes deliberately, sending{ toolCallId: toolCall.id }from a handler. This is the one place the two were confused.props.toolCallis not a field that exists, so the expression isundefined,refusals.getisnever called, and
renderfalls through to theisHeldbranch and draws the component.What it costs
The decision request is what makes "revoke it and watch it go" true rather than nearly true — the
file says so itself:
That question is still asked and the refusal is still audited on the server. Only the answer on
screen was lost. Two reachable cases:
The tool list is refreshed by a five-second poll, so until it catches up the component keeps being
drawn instead of showing why it should not be.
sees the component render as though nothing happened.
Fix
Read
props.toolCallId, and type the props as the sibling does. The type is the point as much asthe access: with the props typed
{ toolCallId?: string; … }, the old line is a compile errorrather than a silent
undefined.That is this change with only the property access put back, and it is the regression guard: the
mistake cannot be made again without
bun run --filter app typechecksaying so. I did not add aruntime test — reaching this renderer means standing up the CopilotKit provider and a live tool
registration around a hook, and the assertion it would make is the one the compiler now makes for
free.
Where it runs
refusalsis per-component-instance React stateand stays that way; this fixes which key it is read under.
renders comes from
POST /components/:name/decision, which is answered from Postgres, soevery replica gives the same verdict and this now shows it.
untouched; this makes the call-time refusal visible in the window before it fires.
Boundary and audit
decideComponentis unchanged and stillruns before anything is drawn.
component.refusedfor this path. No row was missing — the row was written and the person was shown the
component anyway, which is the gap this closes.
under its real name.
Changelog
CHANGELOG.mdunderUnreleased.How I tested
Windows 11, Bun 1.3.14.
bun run --filter app typecheckclean, and failing with TS2551 when the oldaccess is put back.
bun test app/testsis 319 passed, 1 failed — the failure isskill-creator-slug.test.ts, which is a pre-existing Windows path issue onmainand the subject of#394.
bunx biome checkclean.