Skip to content

fix(responses): fail closed for direct Chat routes when mandatory hosted tool_choice is unrepresentable - #810

Closed
luvs01 wants to merge 2 commits into
devfrom
codex/fix-vulnerability-in-computer_use-selection
Closed

luvs01 wants to merge 2 commits into
devfrom
codex/fix-vulnerability-in-computer_use-selection

Conversation

@luvs01

@luvs01 luvs01 commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Transferred upstream

The completed fix and regression coverage continue in lidge-jun/opencodex#8146, targeting dev, from this same branch at fdda6036e62c7ef2508ffad3a713b02997374dea. The upstream PR is ready for review and contains the current validation results and resolved source-review findings. This fork-local PR is closed as transferred; the branch remains the upstream PR's source.


Motivation

  • Prevent a tool-choice narrowing bypass where tool_choice:{"type":"computer_use"} accepted by validation could be translated to auto on a direct Responses→Chat route and thereby leak other declared client-executed tools.

Description

  • Add a post-normalization guard in src/server/responses/request-prepare.ts that calls chatCanPreserveMandatoryToolChoice and returns a 400 error when a translated OpenAI Chat destination cannot preserve a mandatory hosted selector.
  • Import the helper from ./combo-required-tool-contract and run the check after applyFinalRouteRequestNormalization.
  • Add a focused regression test tests/responses/responses-direct-chat-tool-choice.test.ts that exercises a computer_use selector alongside a client function tool and asserts the request is rejected and no upstream send occurs.
  • Register the new test in the repository test-layout maps (scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json).

Testing

  • Ran node_modules/.bin/bun test tests/responses/responses-direct-chat-tool-choice.test.ts tests/server/server-combo-required-tool-contract.test.ts and observed all tests passed (121 passed, 0 failed).
  • Ran node_modules/.bin/bun run typecheck with no type errors.
  • Ran node_modules/.bin/bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts and observed those tooling tests passed.
  • Ran node_modules/.bin/bun run structure:check and structure SSOT checks passed.
  • Note: node_modules/.bin/bun run test:changed was not runnable in this checkout because no upstream dev comparison ref was available.

Codex Task

Summary by CodeRabbit

  • Bug Fixes
    • Requests routed through the direct Chat interface now receive a clear 400 error when a mandatory tool choice cannot be preserved. Such requests are stopped before being sent to the upstream service, helping prevent them from proceeding with altered tool-choice behavior.

@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
Walkthrough

Request preparation now rejects mandatory tool choices that the selected provider and model cannot preserve on normalized openai-chat routes. A regression test checks the 400 response and verifies that no upstream request is sent.

Changes: Direct Chat tool-choice validation

Layer / File(s) Summary
Validate mandatory tool choice during request preparation
src/server/responses/request-prepare.ts:71, 1442–1446, tests/responses/responses-direct-chat-tool-choice.test.ts:1–38, scripts/test-layout/layout.json:134, tests/fixtures/test-layout-expected.json:303–304
After route normalization, preparation returns a 400 invalid_request_error when an openai-chat route cannot preserve the mandatory tool choice. The regression test checks the error message and confirms that the upstream fetch is not called. The test-layout configuration and fixture assign the test to the responses group.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: lidge-jun

Merge Risk: Moderate · previous review 8f909

This change rejects direct Chat requests whose mandatory hosted tool choice cannot be preserved. Two gaps remain. A named MCP selector can still resolve to a same-named client function. In routing profiles, the new refusal can stop fallback to a compatible target. Both should be addressed before merge.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title accurately and specifically describes the primary change: rejecting direct Responses-to-Chat routes when a mandatory hosted tool choice cannot be preserved.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 1 functions across 2 files. (2 skipped: 2 unsupported.)

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 007dbfd and 8f909f4.

Files selected for processing: 4
  • scripts/test-layout/layout.json
  • src/server/responses/request-prepare.ts
  • tests/fixtures/test-layout-expected.json
  • tests/responses/responses-direct-chat-tool-choice.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Nov 1, 2026
@luvs01

luvs01 commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner Author

No description provided.

coderabbitai[bot]

This comment was marked as resolved.

…allback

Integrate current dev, preserve MCP selector ownership, carry local refusal identity through lifetime wrappers, and add direct-route and policy fallback regressions.

Prepared from the verified tree of local commit 886f63996dd96b1f90bcf77c0a246a2e17c300df; the tree is identical.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

Devin Review

assertPolicyNormalizedRoute(policyScope, admittedPolicyRoute, route);
if (route.provider.adapter === "openai-chat"
&& !chatCanPreserveMandatoryToolChoice(parsed._rawBody, route.provider, route.modelId)) {
if (policyScope.eligibility) throw new PolicyCandidateUnavailableError();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Single-candidate policy returns an internal 409 for an invalid tool choice

When a policy has only an incompatible Chat candidate, this branch returns a marked 409. With no fallback candidate, the client receives policy_candidate_unavailable instead of a tool-choice 400.

Learn more

A policy candidate refusal is an in-process marker used to authorize the next policy attempt. handleResponsesWithPolicyFallback returns the original response when no further eligible candidate exists. Thus a policy with one Chat candidate exposes the internal 409, while a direct route reports a client-facing 400 for the same mandatory choice.

Example: A profile containing only chat/model receives tool_choice: {"type":"computer_use"}. No request is sent, but the client receives 409 policy_candidate_unavailable rather than 400 invalid_request_error explaining the incompatible choice.

Recommended fix: Preserve a client-facing tool-choice refusal for exhaustion in handleResponsesWithPolicyFallback, while retaining the local marker to authorize hops. Distinguish this new refusal from other existing policy candidate failures so their behavior does not change.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@luvs01 luvs01 closed this Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aardvark bug Something isn't working codex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant