Skip to content

fix(agent): enforce approval gates on final-response tool execution - #28

Open
skundu42 wants to merge 1 commit into
OpenRouterTeam:mainfrom
skundu42:main
Open

fix(agent): enforce approval gates on final-response tool execution#28
skundu42 wants to merge 1 commit into
OpenRouterTeam:mainfrom
skundu42:main

Conversation

@skundu42

@skundu42 skundu42 commented Aug 27, 2026

Copy link
Copy Markdown

Fixes approval-gate bypasses in SDK runtime behavior by:

  • Enforcing approval checks before tools execute on the allow_final_response path.
  • Passing schema-normalized arguments to approval predicates.
  • Failing closed when tool input validation fails.
  • Applying PreToolUse mutations before approval checks.
  • Preventing PreToolUse from running twice after approval resume.
  • Persisting ungated tool results when another tool pauses for approval.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ca60c2737

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +27 to 29
# Prevent PreToolUse from running twice after an approval resume.
pre_tool_use_applied: Optional[bool] = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Version the newly persisted call marker

When a state contains a pending call, dataclasses.asdict now serializes pre_tool_use_applied even when it is null, while the wire-format version remains 1. An older v1 process in a rolling deployment or rollback passes that object to its three-field ParsedToolCall(**item) constructor and raises TypeError, rather than rejecting a newer state version cleanly. Either keep v1 output backward-compatible or introduce a new serialization version that readers can distinguish.

Useful? React with 👍 / 👎.

Comment on lines +646 to +648
elif outcome["result"] is None:
# HITL tools cannot produce an unsent result until resumed.
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep paused HITL calls in the pending state

When a PermissionRequest hook allows an approval-gated HITL call while another call still requires user approval, this helper executes the allowed HITL call before saving the gate. If on_tool_called returns None to request a pause, this branch produces neither a result nor a pending call, and the caller subsequently persists only the still-unapproved calls; the HITL call is therefore lost and cannot be resumed. Return paused calls to the caller and retain them in state.

Useful? React with 👍 / 👎.

Comment on lines +652 to +655
effective_call = outcome["call"]
results.append(
create_unsent_result(effective_call.id, effective_call.name, outcome["result"].get("result"))
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply output converters before persisting ungated results

When an ungated tool executes alongside a call that pauses for approval, this success path stores the raw result in UnsentToolResult. On resume, unsent_results_to_api_format JSON-serializes that raw value and never invokes the tool's to_model_output converter, unlike the normal execution path through _tool_result_to_output; tools using content conversion therefore send a different or invalid payload to the model after the approval pause.

Useful? React with 👍 / 👎.

Comment on lines +905 to +907
hook_resolved_unsent.extend(
await self._execute_calls_to_unsent_results(executable_calls, tools, turn_context)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Carry next-turn overrides across an approval pause

In a mixed turn containing an executable call and a still-pending approval, this helper executes the former and then the caller immediately saves state and returns. Because the executed calls are not retained and execute_next_turn_params_functions is never run for them, options such as model, temperature, or instructions declared in that tool's next_turn_params are missing from the resumed request, even though the resume is the next model turn after that execution.

Useful? React with 👍 / 👎.

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.

1 participant