Skip to content

fix(codex): bridge MCP tool approval elicitations#68807

Closed
kesslerio wants to merge 5 commits into
openclaw:mainfrom
kesslerio:fix/codex-mcp-approval-elicitation
Closed

fix(codex): bridge MCP tool approval elicitations#68807
kesslerio wants to merge 5 commits into
openclaw:mainfrom
kesslerio:fix/codex-mcp-approval-elicitation

Conversation

@kesslerio

Copy link
Copy Markdown
Contributor

Summary

  • Problem: native Codex app-server sessions let MCP tool approval elicitations fall through to the fail-closed client fallback, which surfaced Codex Apps GitHub writes as immediate user rejected MCP tool call failures.
  • Why it matters: Codex Apps connector writes could not reach the existing OpenClaw approval UX even though plugin approvals, Control UI handling, and chat /approve flows already existed.
  • What changed: targeted mcpServer/elicitation/request handling now bridges _meta.codex_approval_kind = "mcp_tool_call" through plugin.approval.request / plugin.approval.waitDecision, clamps approval payload lengths to gateway schema limits, and adds focused regression coverage.
  • What did NOT change (scope boundary): generic item/tool/requestUserInput, non-approval elicitations, connector auth, and GitHub permission behavior all remain unchanged and fail closed outside this targeted MCP approval path.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: extensions/codex/src/app-server/run-attempt.ts only bridged native approval request families and item/tool/call, so MCP tool approvals arriving on mcpServer/elicitation/request fell through to client.ts's default { action: "decline" } fallback.
  • Missing detection / guardrail: there was no targeted bridge for _meta.codex_approval_kind = "mcp_tool_call", and no regression test proving the native Codex bridge had to intercept that request family before fallback.
  • Contributing context (if known): the Codex app-server protocol carries MCP tool approvals on the elicitation path rather than on the older approval request methods.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • extensions/codex/src/app-server/elicitation-bridge.test.ts
    • extensions/codex/src/app-server/run-attempt.test.ts
    • extensions/codex/src/app-server/client.test.ts
  • Scenario the test should lock in: an MCP tool approval elicitation tagged with _meta.codex_approval_kind = "mcp_tool_call" routes through plugin approval request/wait, unsupported generic prompts still fail closed, and long approval payloads are clamped before gateway validation.
  • Why this is the smallest reliable guardrail: the bug lives at the native Codex bridge boundary, so the tightest durable coverage is one bridge-level test plus one run-attempt routing test.
  • Existing test that already covers this (if any): none before this PR.
  • If no new test is added, why not: N/A.

User-visible / Behavior Changes

  • Native Codex app-server sessions can now surface MCP tool approval prompts for Codex Apps writes through the existing OpenClaw approval flow instead of immediately declining them as fake user rejections.
  • Unsupported generic item/tool/requestUserInput and non-approval elicitation flows still fail closed exactly as before.

Diagram (if applicable)

Before:
Codex Apps MCP write -> mcpServer/elicitation/request -> client fallback decline -> "user rejected MCP tool call"

After:
Codex Apps MCP write -> mcpServer/elicitation/request -> native elicitation bridge
-> plugin.approval.request / waitDecision -> existing approval UX -> accept/decline response

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? Yes
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: this changes how an existing MCP tool approval request is routed, not what the tool can access. The bridge is gated to _meta.codex_approval_kind = "mcp_tool_call", leaves unsupported flows fail-closed, and clamps payloads to existing gateway validation limits.

Repro + Verification

Environment

  • OS: NixOS
  • Runtime/container: local OpenClaw repo checkout, native Codex app-server path
  • Model/provider: codex/gpt-5.4
  • Integration/channel (if any): Codex Apps GitHub connector via native app-server
  • Relevant config (redacted): native Codex app-server enabled; existing plugin approval flow available

Steps

  1. Start a native Codex app-server session with Codex Apps available.
  2. Trigger a GitHub connector mutation that asks for MCP tool approval.
  3. Observe the request path returned by the native bridge.

Expected

  • The approval prompt routes into the existing plugin approval flow and returns an accept/decline response instead of auto-declining.

Actual

  • Before this patch, the request fell through to the client fallback and returned an immediate decline, which surfaced as user rejected MCP tool call.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • focused Codex app-server tests for approval bridge, elicitation bridge, run-attempt routing, and client fallback behavior
    • payload-length clamp so long elicitation prompts still pass gateway validation
  • Edge cases checked:
    • unavailable approval route declines cleanly
    • non-approval elicitation requests stay unhandled
    • generic item/tool/requestUserInput still fails closed
  • What you did not verify:
    • a live end-to-end GitHub connector mutation through the Control UI or chat approval surface

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: the approval-content synthesis may miss a future schema shape for MCP tool approvals.
    • Mitigation: the bridge is narrowly gated to _meta.codex_approval_kind = "mcp_tool_call", only accepts form-mode object schemas, and fails closed when required fields cannot be satisfied.
  • Risk: rich approval prompts could exceed gateway schema limits and get rejected before reaching the approval UX.
    • Mitigation: the shared plugin approval helper now clamps title and description lengths before calling plugin.approval.request, with regression coverage.

AI Assistance

  • Investigation, implementation, and test authoring were done with Codex.
  • Human verification was limited to focused local test execution and code inspection in this checkout.

Tests

  • pnpm exec vitest run extensions/codex/src/app-server/approval-bridge.test.ts extensions/codex/src/app-server/elicitation-bridge.test.ts extensions/codex/src/app-server/run-attempt.test.ts extensions/codex/src/app-server/client.test.ts

@greptile-apps

greptile-apps Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a targeted bridge for mcpServer/elicitation/request in the native Codex app-server path, routing MCP tool approval elicitations (identified by _meta.codex_approval_kind = "mcp_tool_call") through the existing plugin.approval.request / waitDecision flow instead of falling through to the { action: "decline" } client fallback. It also adds gateway payload-length clamping to plugin-approval-roundtrip.ts and focused regression coverage across the bridge, run-attempt routing, and client fallback layers. All remaining findings are P2 style issues.

Confidence Score: 5/5

Safe to merge — the bridge is narrowly gated, fails closed on all unrecognized shapes, and does not change existing approval or non-approval code paths.

All findings are P2 or lower (a duplicate test helper call and a documented heuristic risk). The fix is logically sound, the routing order in run-attempt.ts is correct, payload clamping is verified by tests, and the non-approval fallback remains unchanged.

No files require special attention. The heuristic approval-field detection in elicitation-bridge.ts is a known acknowledged risk, not a current defect.

Comments Outside Diff (1)

  1. extensions/codex/src/app-server/client.test.ts, line 50-52 (link)

    P2 Duplicate vi.useRealTimers() call in afterEach

    vi.useRealTimers() is called twice in the same afterEach block (lines 51 and 53). The second call is redundant — vi.restoreAllMocks() between them does not re-enable fake timers, so the second useRealTimers() is a no-op.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/codex/src/app-server/client.test.ts
    Line: 50-52
    
    Comment:
    **Duplicate `vi.useRealTimers()` call in `afterEach`**
    
    `vi.useRealTimers()` is called twice in the same `afterEach` block (lines 51 and 53). The second call is redundant — `vi.restoreAllMocks()` between them does not re-enable fake timers, so the second `useRealTimers()` is a no-op.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/codex/src/app-server/client.test.ts
Line: 50-52

Comment:
**Duplicate `vi.useRealTimers()` call in `afterEach`**

`vi.useRealTimers()` is called twice in the same `afterEach` block (lines 51 and 53). The second call is redundant — `vi.restoreAllMocks()` between them does not re-enable fake timers, so the second `useRealTimers()` is a no-op.

```suggestion
  afterEach(() => {
    resetSharedCodexAppServerClientForTests();
    vi.restoreAllMocks();
    vi.useRealTimers();
    for (const client of clients) {
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/codex/src/app-server/elicitation-bridge.ts
Line: 176-206

Comment:
**Approved outcome silently converts to `decline` when no schema property matches the heuristic**

`buildAcceptedContent` returns `undefined` if `sawApprovalField` never becomes `true`, which happens when none of the schema properties' names/titles/descriptions match `/\b(approve|approval|allow|accept|decision)\b/`. The caller in `buildElicitationResponse` then falls through to `{ action: "decline" }` even though the user explicitly approved.

In practice this only fires for elicitations tagged `codex_approval_kind = "mcp_tool_call"`, so current Codex Apps schemas (which use `approve` / `allow` style names) are safe. However, a future or third-party MCP server that uses a property name like `confirm`, `grant`, `ok`, or `permitted` would receive a silent decline on an explicitly-approved request. The PR description acknowledges this risk, but it's worth noting that the fail-closed behavior here is non-obvious: the user sees an approval prompt, clicks "Allow", and the tool still gets declined with no error surfaced.

Consider logging a warning (even at debug level) when `outcome` is approved but `buildAcceptedContent` returns `undefined`, to aid future debugging.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(codex): clamp plugin approval payloa..." | Re-trigger Greptile

Comment thread extensions/codex/src/app-server/elicitation-bridge.ts

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

Copy link
Copy Markdown
Contributor

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: ffb19e4db4

ℹ️ 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 thread extensions/codex/src/app-server/elicitation-bridge.ts Outdated

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

Copy link
Copy Markdown
Contributor

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: 701ecb9e44

ℹ️ 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 +243 to +244
if (!isPersistField(property) || outcome !== "approved-session") {
return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Map allow-once onto required persist fields

readPersistFieldValue only returns a value for approved-session, and readFallbackFieldValue explicitly suppresses defaults for persist-like fields on approved-once. When an MCP approval schema makes persist required (for example with options like never/session/always), an allow-once decision leaves persist unset, so buildAcceptedContent returns undefined and the bridge declines the request even though the user approved it once.

Useful? React with 👍 / 👎.

@steipete

Copy link
Copy Markdown
Contributor

Thanks @kesslerio — the useful narrowed version of this is now on main in 1cbd5a9470.

What landed:

  • mcpServer/elicitation/request handling for Codex MCP tool approvals
  • routing through OpenClaw plugin approvals
  • accept/decline/cancel mapping back to Codex's typed elicitation response
  • bounded approval title/description payloads
  • warning + fail-closed behavior when an approved schema has no mappable approval field
  • focused tests for the bridge, run-attempt routing, and fallback behavior

One local adjustment from the PR: the bridge now requires the request threadId to match the active Codex thread, while tolerating turnId: null/missing per the current ../codex generated protocol.

Main CI is green on dcc243c889: https://github.com/openclaw/openclaw/actions/runs/24811581054

Closing this PR as superseded by the landed main commit.

@steipete steipete closed this Apr 23, 2026
medikoo pushed a commit to medikoo/openclaw that referenced this pull request Apr 24, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: native Codex app-server does not surface app approval/input prompts, and Codex Apps GitHub writes fail as "user rejected MCP tool call"

2 participants