feat: add pending-interactions tracker and standalone approval endpoints#8407
Conversation
…nts (#8405) Co-authored-by: Claude <noreply@anthropic.com>
| const interaction = pendingInteractions.resolve(requestId); | ||
| if (!interaction) { | ||
| return Response.json( | ||
| { error: 'No pending interaction found for this requestId' }, | ||
| { status: 404 }, | ||
| ); | ||
| } | ||
|
|
||
| interaction.session.handleConfirmationResponse(requestId, decision); | ||
| return Response.json({ accepted: true }); |
There was a problem hiding this comment.
🔴 Missing kind validation allows /v1/confirm to consume secret interactions (and vice versa), orphaning the real pending request
The handleConfirm endpoint calls pendingInteractions.resolve(requestId) on line 36 without checking that the interaction's kind is 'confirmation'. Similarly, handleSecret on line 71 doesn't check that kind is 'secret'.
Root Cause and Impact
If a secret_request interaction is registered under a given requestId, and a client mistakenly (or maliciously) sends a POST to /v1/confirm with that same requestId:
pendingInteractions.resolve(requestId)atapproval-routes.ts:36removes the interaction from the map.interaction.session.handleConfirmationResponse(requestId, decision)is called atapproval-routes.ts:44.- Inside
session.ts:326,this.prompter.resolveConfirmation(...)looks up the requestId in the confirmation prompter's pending map — but it's not there (the request lives in the secret prompter). It logs a warning and returns silently. - The HTTP response is
{ accepted: true }— the caller thinks it succeeded. - The secret request is now orphaned: removed from
pendingInteractionsbut never resolved in the secret prompter. The agent loop hangs forever waiting for a secret response that will never come.
The same issue exists in reverse: /v1/secret can consume a confirmation interaction, orphaning the confirmation prompt.
Impact: Cross-kind resolution silently orphans the real pending prompt, causing the agent loop to hang indefinitely (until the permission timeout fires).
| const interaction = pendingInteractions.resolve(requestId); | |
| if (!interaction) { | |
| return Response.json( | |
| { error: 'No pending interaction found for this requestId' }, | |
| { status: 404 }, | |
| ); | |
| } | |
| interaction.session.handleConfirmationResponse(requestId, decision); | |
| return Response.json({ accepted: true }); | |
| const interaction = pendingInteractions.resolve(requestId); | |
| if (!interaction || interaction.kind !== 'confirmation') { | |
| // If we consumed a non-confirmation interaction, re-register it | |
| if (interaction) pendingInteractions.register(requestId, interaction); | |
| return Response.json( | |
| { error: 'No pending confirmation found for this requestId' }, | |
| { status: 404 }, | |
| ); | |
| } | |
| interaction.session.handleConfirmationResponse(requestId, decision); | |
| return Response.json({ accepted: true }); |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 940cbcecc9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const interaction = pendingInteractions.get(requestId); | ||
| if (!interaction) { | ||
| return Response.json( | ||
| { error: 'No pending interaction found for this requestId' }, | ||
| { status: 404 }, |
There was a problem hiding this comment.
Require active confirmation before adding trust rules
handleTrustRule only checks whether the requestId exists in the in-memory tracker, but it never verifies that the confirmation is still pending in the session. Since confirmation prompts can auto-timeout/resolve independently, stale tracker entries can remain and still pass this check, allowing POST /v1/trust-rules to persist a rule after the approval flow is no longer active. That violates the endpoint’s pending-confirmation contract and can apply persistent allow/deny behavior outside the intended decision window.
Useful? React with 👍 / 👎.
| const interaction = pendingInteractions.resolve(requestId); | ||
| if (!interaction) { |
There was a problem hiding this comment.
Validate interaction kind before consuming request IDs
/v1/confirm and /v1/secret immediately call pendingInteractions.resolve(requestId) without checking the stored interaction kind first. If a client accidentally sends a secret requestId to /v1/confirm (or vice versa), the entry is deleted before the mismatch is detected, so the correct endpoint can no longer resolve that prompt and the request is effectively forced to timeout/deny.
Useful? React with 👍 / 👎.
Summary
PR 2/6 in the remove-runs-centralize-messages plan (cherry-picked to main).
Plan: .private/plans/REMOVE_RUNS_CENTRALIZE_MESSAGES.md