Repository navigation
fix: approve shows what the gate is actually waiting on - #366
Merged
Merged
Conversation
#364 made `codeoid approve` work by attaching to the session and scanning its scrollback for tool calls in `waiting_confirmation`. That reads the ANNOUNCED tool message, which is not always what is parked: - A backend that ties a gate to its announcement by tool name alone (qwen) can show one call while another is parked, and approve now presents what it shows as exactly what will run. - A leftover `waiting_confirmation` row from a past run (after a daemon restart) looked pending; approving it printed "Approved." while nothing happened. - The scan gave up after 3s on a long scrollback, and attaching just to read had side effects. The daemon now answers `session.approvals {sessionId}` with only the calls its approval gate is really waiting on, each with the input the gate received (after hook rewrites). It needs session:watch or session:attach, like the scrollback it replaces. `session.approve` reports whether the answer took effect (`data.outcome`: settled / early / stale), still as response.ok so the web ApprovalBar's dismissal of leftovers is unchanged. The CLI: - uses that list; adds --id (stable, unlike --pick, for scripts) - exits 1 whenever it does not answer, and reports a stale answer - refuses to approve on a partial view when the input is longer than it shows, instead of silently cutting it - shows C1 controls, bidi and other invisible characters as visible escapes on the lines a decision rests on, dialogs included - treats Ctrl-D at the prompt as "no" instead of hanging Closes #349 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…limits Security review of the session.approvals change: - An empty `--id` (an unset $ID in a script) fell back to the oldest waiting call. It is refused now. - Escape every Unicode default-ignorable and format character on the lines a decision rests on (variation selectors, Hangul fillers, ...), not a hand-picked list. - Single-line fields (tool name, id, dialog title) escape newlines, so a crafted tool name can't print a fake input block above the real one. - The protocol said the listed input is exactly what runs. Not on every backend: codex and the Gemini CLI ignore a hook rewrite, and codex ties an MCP elicitation to its call by server name. Say so; those are backend fixes, tracked separately. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Unicode-property rewrite dropped the line and paragraph separators (Zl/Zp, not Cf or default-ignorable), which most terminals draw as nothing. Add them back, with a test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 11, 2026
Collaborator
Author
|
@highflame-oracle review |
rsharath
approved these changes
Oct 11, 2026
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
codeoid approvenow shows, and answers, the tool call the session is really waiting on.#364 got the command working again by attaching to the session and scanning its history for calls marked "waiting for approval". That history records what the agent announced, which is not always what is waiting:
approvecould show the harmless one while the dangerous one is the call it approves. Sinceapprovepresents what it shows as exactly what will run, that is the worst place for a mismatch.Now the daemon itself lists only the calls it is actually parked on, each with the exact input that runs if approved.
Other fixes to
codeoid approve--id <approvalId>. Picks a call by its id, which is stable.--pick Ncounts from the oldest, so N can point at a different call between two runs.--pickstill works.--yes.Behaviour change: yes/no questions (#348) now get the same confirm prompt as tool calls, and need
--yeswhen run without a terminal. Before, only tool calls asked first.Verification
Ran on a real daemon (
codeoid start --local, real Claude turns, the actual CLI):--yes: refused, exit 1.y: approved, and the agent'srm a.txtran.--deny: denied, file kept.Tests:
src/tests/cli-approve.test.ts. They drive the real CLI over a real WebSocket into the daemon's request handling, against a session genuinely waiting on approval.main's CLI swapped back in, the CLI tests fail (12 of the first 17).Not run: no end-to-end regression tier applies; codeoid has none, and this change is confined to codeoid.
Review: this code went through four review rounds on an earlier branch, written before #364 landed. Each round's security findings were fixed and retested. This PR re-applies those fixes on top of #364's command, which already had the full-input display,
--pickand--yes. Review on this branch:--idfell back to the oldest call; some invisible characters weren't escaped; a newline in a tool name could fake lines.Deferred to follow-up issues #367 and #368: two older backend bugs the review found. They also affect the web approval card:
This PR documents both limits rather than claiming "exactly what runs" on those backends.
Closes #349
For agents: implementation detail
packages/protocol/src/approvals.ts:SessionApprovalsMsg/SessionApprovalsResultMsg(approvals: [{approvalId, toolName, input}]), andSessionApproveOutcome = "settled" | "early" | "stale".sessionApprovalsSchemais in the client union.response.oktosession.approvenow carriesdata: { outcome }. It staysresponse.okfor stale ids, because the web ApprovalBar sends those to dismiss leftovers.Session#approvalInputs:approvalId → {toolName, input}. It is set in thecanUseToolclosure immediately before#waitForApproval, witheffectiveInputafter hook rewrite, and cleared infinally.Session.pendingToolApprovals()iterates#pendingApprovalsand reads only that record. It has no fallback to the announced message.Session.approve()returns the outcome.SessionManager#sessionApprovalsrequiressession:watchorsession:attachplus a tenant check, the same assession.turnsand attach.TerminalClient.approveSessionusessession.approvals; the attach-and-scan#pendingToolApprovalsis deleted.#confirmApprovalhandles both the dialog and tool paths; EOF counts as "no".plainTextadditionally escapes U+0080–009F, U+00AD, U+061C, U+180E, U+200B–200F, U+2028–2029, U+202A–202E, U+2060–2069, U+3164, U+FEFF and U+E0000–E007F as\u{..}.APPROVE_SHOW_MAX(8000) are refused unless--yes.response.ok.dataon approve, and the Rust TUI's serde ignores unknown keys, so no consumer change is needed.rawInputis absent.pendingDialogtitle/message insession.listis visible tosession:list-only callers.🤖 Generated with Claude Code