Repository navigation
Description
Parent #1162. Blocked on #1170 (interactive Codex runner → codex app-server for turn/steer). Related #170 (sandbox mapping), #846 (MCP under approval_policy=never), #509 (edit-before-approve), #813 (Codex PreToolUse guardrail). Do not implement #1170 steer in this issue.
Verdict
Not a small mapping. Do not flip Codex to on-request / untrusted until a live JSON-RPC reply path exists. Do not fake ask by killing the child. Keep approvalPolicy: "never" on the #1170 runner so these ServerRequests never stall a turn.
Claude asks because --permission-prompt-tool stdio keeps stdin open. Codex exec --json has no equivalent (see electron/codex-guardrail.js: "codex exec has no control_request"). App-server 0.153.4 does have an ask channel, but it is a different protocol than Claude control_request.
What Solenta has today
| Layer | Claude | Codex now |
|---|---|---|
| Spawn | claude -p --input-format stream-json --permission-prompt-tool stdio --permission-mode <mode> |
codex exec --json --sandbox <…> + approval_policy=never (exec pin) |
| Incoming ask | JSONL control_request {request_id, request:{subtype:"can_use_tool", tool_name, input}} |
none |
| Pending store | active[threadId].kind === "claude" + pendingPermissions[] |
kind: "codex", no list |
| Reply | handle.respond → JSONL control_response {behavior:"allow"|"deny", updatedInput?, updatedPermissions?} |
{kill} only; stdin ignored |
| UI | PermissionPrompt: Accept / Accept all / Deny; editable command (#509); AskUserQuestion / ExitPlanMode specials |
never shown for Codex |
| IPC | threads.respondPermission (allow | allowAlways | deny, optional updatedCommand / answers) |
same IPC exists; runner falls through to persisted plan (#707) |
| Guardrail | classifyTool can deny before the card |
PreToolUse hook; Ask is deny (Codex continues the tool after parsing ask) |
getPendingPermission / respondPermission hard-gate on kind === "claude" (electron/runner.js). The renderer card is provider-agnostic if the runner fills ThreadDetail.pendingPermission. That is the only reuse.
Permission modes advertised for Codex are the full Claude set, but codexSandboxFor only changes --sandbox:
plan→read-onlydefault/acceptEdits→workspace-writebypassPermissions→danger-full-access
No interactive ask. First-party MCP survives because #846 sets default_tools_approval_mode="approve" under the never policy.
What app-server 0.153.4 actually sends
Generated from live codex app-server generate-json-schema (CLI 0.153.4). turn/start uses the NEW item/* methods. execCommandApproval / applyPatchApproval are DEPRECATED (legacy SendUserTurn / SendUserMessage) — do not implement them on a turn/start runner.
AskForApproval on thread/start and turn/start: untrusted | on-request | never | {granular:{mcp_elicitations, rules, sandbox_approval, request_permissions?, skill_approval?}}.
approvalsReviewer: user (default) | auto_review | guardian_subagent. Solenta should keep user until a reply path exists.
1. item/commandExecution/requestApproval (stable, closest to Claude Bash)
Params (required: threadId, turnId, itemId, startedAtMs):
command: string or null (not Claude's{command, cmd, script}object)cwd,reason,commandActions[],kind:command|writeStdinapprovalId: extra id when several callbacks share oneitemId(zsh-exec-bridge / stdin)availableDecisions[],proposedExecpolicyAmendment[],additionalPermissions,networkApprovalContext
Reply: { decision } where decision is:
accept↔ SolentaallowacceptForSession↔allowAlways(session cache, not ClaudeupdatedPermissionsaddRules){acceptWithExecpolicyAmendment:{execpolicy_amendment:[]}}— no Solenta UI{applyNetworkPolicyAmendment:{network_policy_amendment:{host,action}}}— no Solenta UIdecline— deny, turn continuescancel— deny and interrupt the turn (Claude Deny does not do this)
#509 hole: response cannot rewrite the command. Accept runs the proposed string. Do not send updatedCommand into this RPC.
writeStdin is not a new tool — it is input to an existing terminal. Do not render it as a Bash card.
2. item/fileChange/requestApproval (stable)
Params: threadId, turnId, itemId, startedAtMs, optional reason / grantRoot (UNSTABLE). No file contents, no diffs. The client must correlate itemId with item/started (fileChange item) to show the patch. Reply: accept | acceptForSession | decline | cancel.
PermissionPrompt today is command-textarea or JSON preview. File-change needs a diff surface (or a read-only JSON dump of the correlated item — worse).
Deprecated twin: applyPatchApproval (callId, conversationId, fileChanges map). Ignore on turn/start.
3. item/permissions/requestApproval (stable)
Not allow/deny. Params: cwd, permissions (fileSystem entries + network.enabled), threadId, turnId, itemId, startedAtMs.
Reply: { permissions: GrantedPermissionProfile, scope: "turn"\|"session", strictAutoReview? }. This is a sandbox overlay grant (extra write roots / network). New UI, or auto-decline until a card exists.
4. mcpServer/elicitation/request (stable)
MCP elicitation/create passthrough: serverName, threadId, optional turnId, plus one of mode: form | openai/form | openaiForm | url with requestedSchema or url. Reply: { action: accept|decline|cancel, content? }.
This is a form/url, not PermissionPrompt. First-party Solenta MCP currently uses default_tools_approval_mode="approve" (#846) so it should not hit this if we keep that config; user-registered MCP might.
5. item/tool/requestUserInput (in the stable bundle; description says EXPERIMENTAL)
{threadId, turnId, itemId, isBlocking, questions[{id, header, question, options, isOther, isSecret}]}. Reply is structured answers. Closer to Claude AskUserQuestion / persisted pendingQuestion (#647) than to Allow/Deny. Blocking vs non-blocking must be honored: blocking without a reply stalls the turn.
Not permission prompts (do not map)
item/tool/call— client executes a dynamic toolaccount/chatgptAuthTokens/refresh,attestation/generatecurrentTime/read— experimental-schema only
Why a naive map would break
- No transport.
startCodexRun→runCodexwith stdin ignored.providerUsage.runStdioJsonRpconly matches client-initiatedids (handshakeId/matchId) and kills the child; an inbound ServerRequest is dropped and the turn waits forever. - Reply shape. JSON-RPC
{id, result:{decision}}vs Claude stdincontrol_response. Needhandle.respondJsonRpc(id, result)on a long-lived app-server process (Switch Codex interactive runner to app-server for real turn/steer #1170). - Kind gate.
respondPermissionbails to persisted-plan for any non-claude active run. Codex must growpendingPermissions(or a sibling list keyed by JSON-RPC id +approvalId). - Decision vocab. Three Solenta buttons vs six+ Codex decisions, plus
declinevscancel. Accept-all is session cache, not Claude addRules. Execpolicy / network-policy amendments have no control. - Cannot edit the command. Shipping
updatedCommandinto Codex would silently ignore the edit and run the original. - Multiple outstanding asks.
approvalIdcan fan out under oneitemId. Claude queuespendingPermissionsand shows the oldest. Same queue is fine if the reply id is the JSON-RPC request id (andapprovalIdis stored). - Stop / settle. Claude idle
control_requestgetsrespondError("No active turn…"). Codex needs a JSON-RPC error ordecline/cancelon kill, or app-server hangs the turn. - Guardrails. Live
classifyTooldeny must JSON-RPC-decline before the card. Do not rely on PreToolUse ask (it is deny-then-continue). - Mode matrix. Solenta's four Claude modes ≠
AskForApproval × SandboxPolicy.acceptEditswould need granular auto-accept of fileChange while still asking for commands. Until that exists, keep advertising sandbox-only behavior; do not pretend default asks. - Explicit Grok worker forks are impossible when approval policy is
never#846 MCP. Flipping toon-requestwithout keeping first-partydefault_tools_approval_mode="approve"re-breaksthread_send/ memory tools.
What UI / IPC is needed (after #1170)
Reuse threads.respondPermission + PermissionPrompt only for item/commandExecution/requestApproval with kind=command:
requestId= JSON-RPCid(stringified)toolName="command"(or a stable label)command=params.command(show the textarea read-only, or hide Accept until we accept that edits are dropped)summary/inputfromcommand+cwd+reason- Map
allow→accept,allowAlways→acceptForSession,deny→decline(notcancel; Stop already interrupts) - Ignore
updatedCommand - Prefer
availableDecisionswhen present; hide Accept all ifacceptForSessionis absent
New work, not a map:
- Bidirectional JSON-RPC in the Switch Codex interactive runner to app-server for real turn/steer #1170 runner (handle ServerRequest while
turnisinProgress; tests against a fake app-server, same gate as Switch Codex interactive runner to app-server for real turn/steer #1170 steer) kind !== "claude"pending-permission path- fileChange: correlate
item/started→ diff card - permissions overlay grant card
- MCP elicitation form / URL
requestUserInput→ AskUserQuestion-shaped picker (isBlocking)writeStdincard- Stop/settle must answer or cancel outstanding ServerRequests
- Tests: fake JSON-RPC server emits each method; assert reply shape; assert
on-requestis not set until the command path replies
Suggested mode mapping (only once command reply works)
| Solenta mode | sandboxPolicy | approvalPolicy |
|---|---|---|
plan |
readOnly |
never or on-request (read-only still escapes for network) |
default |
workspace-write equivalent | on-request |
acceptEdits |
workspace-write | not Claude acceptEdits until granular fileChange auto-accept exists; snap to default or document the lie |
bypassPermissions |
dangerFullAccess |
never |
Until #1170 lands, do not change codexSandboxFor or exec argv. Workflow / ask / commitmsg stay on exec with never.
Out of scope
- Implementing the app-server interactive runner or
turn/steer(Switch Codex interactive runner to app-server for real turn/steer #1170) - Flipping
supportsSteer thread/queue/add- Guardian /
auto_reviewreviewer item/tool/calldynamic client tools
PR plan
- After Switch Codex interactive runner to app-server for real turn/steer #1170 exists: bidirectional JSON-RPC + fail-closed: unknown ServerRequest → JSON-RPC error; stop →
cancel/declineoutstanding ids. KeepapprovalPolicy: "never". Fake-server tests. - Command ask: map
item/commandExecution/requestApprovalontopendingPermission/respondPermissionas above. Enableon-requestonly fordefault(and maybeplan). Guardrail deny →declinewithout a card. - fileChange diff card via
item/startedcorrelation. - permissions/requestApproval overlay grant (or auto-decline with an event).
- elicitation + requestUserInput (separate if the command path is enough for parity).
Gate each PR on a fake app-server. Do not spend tokens on a live in-progress model to prove the RPC.
Activity
currentbits commentedon Sep 10, 2026
Reply path landed without flipping on-request or switching exec to app-server.
electron/codexJsonRpc.js— long-lived bidirectional JSON-RPC. Unknown ServerRequests → -32601. Stop →canceloutstanding ids (no second reply).electron/codexApprovals.js+runner.handleCodexServerRequest—item/commandExecution/requestApprovalkind=command →pendingPermission. allow→accept, allowAlways→acceptForSession, deny→decline.updatedCommandignored (textarea read-only). Guardrail deny declines without a card.approval_policy=neverstays. Fake app-server tests inelectron/test/codex-jsonrpc.test.jsandcodex-approvals-runner.test.js.#1170 must attach
createCodexJsonRpcSessionas the Codex handle and callhandleCodexServerRequestfromonServerRequestbefore enabling on-request.Follow-ups: #1189 fileChange diff card, #1190 permissions overlay + elicitation + requestUserInput.