Repository navigation
fix: codex runs what you approve, and shows it first - #373
Merged
Merged
Conversation
codeoid answered codex's item/* approval requests with the legacy
ReviewDecision words ("approved" / "denied"). codex's app-server
protocol for item/commandExecution and item/fileChange approvals takes
"accept" / "decline", so it rejected every reply as "approval request
failed": an approved command or edit never ran. item/permissions
expects a permissions GRANT, not a decision, and got the same wrong
shape.
- Reply per method: accept / decline for commands and file changes; the
requested profile (scope: turn) or an empty grant for permissions.
- A codex item that ends `declined` was reported as a SUCCESS (only
`failed` counted as failure). Only `completed` succeeds now; when
codeoid's gate declined the item its card is already cancelled and
codex's own completion no longer overwrites it.
- The fake codex validates replies against the decision enums in codex
0.154's published schema and behaves like the real one on an invalid
reply, so the old words can't pass the tests again. It also covers
file-change and permissions approvals.
Verified against a real codex-cli 0.154.0: an approved `rm` now runs; a
denied one does not, and its card stays cancelled.
Closes #369
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Security review of the #369 change, which made codex approvals take effect for the first time: - codex 0.154's file-change approval carries no diff; it only rides the item's item/started. The approval showed an empty change, and approving applied a patch nobody saw. Command and file-change items are now recorded from item/started and the approval shows their real diff / command. A file change with no diff to show, or one that also asks for session-wide write access (grantRoot), is declined without asking, with the reason on the card. - A command approval for a stdin write to a running terminal (its input isn't in the request) or with no command at all is declined without asking; a managed-network approval shows its host. - Several approvals can share one item (zsh-exec-bridge subcommands): codex's per-callback approvalId is now part of codeoid's id, so two pending approvals no longer collide. - A call a backend settles itself left the session on "waiting approval" for the rest of the turn; completing it with nothing parked moves the session on. - The fake codex now follows 0.154's message order (item/started first, no diff in the request). Verified against a real codex-cli 0.154.0: each file-change approval showed the exact change (delete, then add) and approving applied it. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Second security review of the #369 change: - With codex's apply_patch_streaming_events, item/started can carry none or part of a patch and the rest arrives as item/fileChange/patchUpdated. The approval now shows the latest diff. - A sub-command approval (approvalId set) with no command fell back to the PARENT's command. It is declined without asking instead. - codex completes only the parent item, so approved sub-command cards never closed (stuck "executing", watchdog paused). The parent's completion now closes them. - Only an explicit `command` kind is approvable; an unknown future kind is declined. Network access with no command is `codex_network_access` so Bash hooks and Bash auto-approval don't cover it. - A call the backend settled itself drops its approval mapping (a late click is "stale", never a buffered early decision) and is audited as session.backend_settled. The waiting-approval fallback also respects a parked provider dialog. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Third security review of the #369 change: - A patch that changes while its approval is being decided is declined even on a yes: the yes covered the diff that was shown. - A sibling approval answered after its parent command finished is declined (and one arriving after it, without asking). - A composite id is built only when codex sends both an item id and a callback id. - Any network grant, alone or with a command, is codex_network_access and must name its host. Capability roles now cover codex's tools: codex_file_change is a write; codex_network_access needs network:true (read-only does not cover open egress). - A call a backend settled itself is audited when it was asking for approval or didn't run (with success=...), and its early-click and form leftovers are cleared. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…lines Fourth security review of the #369 change: - Naming a command-plus-network approval codex_network_access hid the command from Bash-scoped tool_call hooks. With a command it is Bash again (host shown alongside); only a bare network grant is codex_network_access. - Approvals in flight on one item are counted, not flagged, and the "patch changed" mark is cleared in finally, so concurrent or failed approvals can't consume or leave each other's state. - A sibling approval arriving after its parent finished gets a card with the reason instead of a silent decline. - Providers mark a call they refused themselves (declinedByBackend); that, or a card that was asking for approval, is what gets audited as session.backend_settled — not any tool that merely failed on its own. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Collaborator
Author
|
@highflame-oracle review |
rsharath
approved these changes
Oct 11, 2026
Collaborator
Author
|
@highflame-oracle review |
* fix: tie a codex MCP approval to the call it is actually about codex asks to approve an MCP tool call with an elicitation that names only the SERVER. codeoid kept one pending call per server and gated whatever was stored under that name, so with two calls to one server in flight (a delete_repo and a get_issue on github) the approval could show the other call's tool and arguments. - Pending MCP calls are a per-server list, removed by item id on completion. - When the elicitation names the tool (message) and carries the arguments (_meta.tool_params), that is what's shown — it is what codex will run — and the started item is only used to pair the result. - Otherwise the one matching call is used (several identical ones are the same call); several DIFFERENT candidates, or none with nothing to show, are declined without asking, with the reason on the card. Closes #368 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix: give unpaired MCP approvals unique ids; don't treat server questions as tool calls Security review of the #368 change: - An MCP approval that couldn't be paired to a started call used the turn id as its id — shared by every request in the turn, so two such approvals collided and one answer could settle the other. Each gets a unique id now. - The elicitation method also carries an MCP server's OWN questions (a form, an OAuth url). They were treated as tool-call approvals — in autonomous mode one could be accepted with its url never shown. Only codex's `mcp_tool_call` approval (form mode, codex_approval_kind) is a tool approval now; anything else is shown with its message and url and declined. - The tool name is read from codex's whole message template, anchored at the end, so a quote in a tool name doesn't cut it short. - The pending-call list is cleared each turn and on teardown. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix: pin that an unmarked MCP form elicitation is declined Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Brings in #371 (refuse hook-rewritten calls on backends that run their own input). One conflict, in MockSessionProvider: both sides added a test field (appliesToolInputRewrite, ungatedToolIds) — kept both. The session.approvals note no longer lists #368 as a known limit; it is fixed on this branch (#374). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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 fixes
On codex, anything you approved never actually ran. codeoid answered codex's approval requests with outdated decision words. Current codex rejected every reply as a failed approval, so approved commands and file edits silently didn't happen, and codeoid still marked them completed. Approvals now take effect: approve runs it, deny doesn't, and the record shows what really happened.
Making approvals real exposed what the approver was being shown. This PR also makes sure every codex approval shows what codex will actually do:
codex_network_access, so Bash rules don't auto-approve it. A command that needs the network staysBash, so Bash hooks still see it, with the host shown alongside.Verification
Ran against a real codex-cli 0.154.0 (
codeoid start --local, the actual CLI):rm a.txtdeleted the file. A deniedrm b.txtkept it, and its card stayed cancelled.note.txtproduced two approvals. One showed the delete with the old contents, the other the add with the new; approving both leftgoodbye.main, an approvedrmleft the file in place. codex's own log recordedRejected("approval request failed").Tests:
Not run: no end-to-end regression tier; codeoid has none.
Review: four rounds of security review, the cap. Every round's findings were fixed with tests:
Interaction with #371: #371 makes codex refuse calls a hook rewrote, since codex runs the input it proposed. That matters more once approvals take effect, which is what this PR does. The two touch the same file in separate places; whichever merges second needs a trivial rebase.
Follow-ups filed: #368 (codex MCP calls matched by server name), #372 (Telegram approvals don't show the input).
Closes #369
Closes #368
For agents: implementation detail
src/daemon/providers/codex/index.ts.approvalReply(method, params, allow):{decision:"accept"|"decline"}foritem/commandExecution|fileChange/requestApproval. Foritem/permissions/requestApproval, allow returns{permissions: params.permissions ?? {}, scope:"turn"}and deny returns{permissions:{}}.#startedItemsstores command and file-change items fromitem/started, updated (as a fresh object) fromitem/fileChange/patchUpdated. It's cleared per turn and onitem/completed.approvalToTool(method, params, started)builds the shown input and returnsunshowablefor each of these:grantRoot;kindother thancommand;tool_start+tool_complete{success:false, declinedByBackend:true}.${itemId}:${approvalId}, used only when both are present.#openSubApprovalscloses approved siblings on the parent's completion.#completedParentsdeclines late siblings.#approvalsInFlightis a refcount, and#patchChangedInFlightturns a yes into a decline when the patch changed during the decision.successis true only forstatus === "completed".#declinedItemssuppresses codex'sdeclinedcompletion for calls codeoid declined, because the card is already cancelled.src/daemon/session.ts.tool_completemoveswaiting_approvaltothinkingwhen no approvals or UI requests are pending.session.backend_settledwhen its card was waiting or the provider setdeclinedByBackend.ProviderEvent.tool_complete.declinedByBackend?is a new optional field.tool-safety.ts.codex_file_changeis now inWRITE_TOOLS. The newOPEN_NETWORK_TOOLS = {codex_network_access}is denied unlessnetwork: true.fake-codex-validate.tshasapprovalReplyError(0.154 enums).fake-codex.tsfollows the 0.154 message order and has new scenarios.MockSessionProvider.ungatedToolIdslets a test announce a call without gating it.🤖 Generated with Claude Code