fix(approvals): distinguish expired, already-resolved, and unknown approval ids#92376
fix(approvals): distinguish expired, already-resolved, and unknown approval ids#92376Hidetsugu55 wants to merge 1 commit into
Conversation
…proval ids Resolving an exec/plugin approval that was not pending previously collapsed expired, already-resolved, and never-seen ids into a single "unknown or expired approval id" error, so operators could not tell whether to re-run the command or whether their decision had already landed. - ExecApprovalManager keeps a bounded FIFO archive of terminated records and exposes classifyApprovalId(), which respects caller visibility so it never leaks the existence of approvals the caller could not see. - handleApprovalResolve now reports an elapsed window as "approval expired" (reason APPROVAL_EXPIRED, with remediation) both inside the grace window and after the record is archived, and reports a post-grace decided approval as "approval already resolved". Genuinely unknown ids still return the existing message. Same-decision idempotent retries are unchanged. - Add isApprovalExpiredError / isApprovalAlreadyResolvedError detectors. Tests: exec-approval-manager (classify pending/resolved/expired/unknown, visibility, post-grace archive, FIFO eviction), approval-shared (in-grace and post-grace expired vs already-resolved), approval-errors detectors.
|
Codex review: needs changes before merge. Reviewed June 30, 2026, 7:29 PM ET / 23:29 UTC. Summary PR surface: Source +154, Tests +272. Total +426 across 6 files. Reproducibility: yes. Source inspection of current main shows stale cleanup consumers only recognize Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Mantis proof suggestion Risk before merge
Maintainer options:
Copy recommended automerge instructionNext step before merge
Security Review findings
Review detailsBest possible solution: Keep the finer gateway taxonomy, but merge only after terminal approval errors are classified through one cleanup-compatible API and the terminal archive stores only minimal state/visibility metadata. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection of current main shows stale cleanup consumers only recognize Is this the best way to solve the issue? No, not as submitted. Distinct gateway reasons are the right direction, but the best fix must also migrate stale-approval consumers and avoid retaining full approval payloads in the terminal archive. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against a841c2788202. Label changesLabel justifications:
Evidence reviewedPR surface: Source +154, Tests +272. Total +426 across 6 files. View PR surface stats
Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (1 earlier review cycle)
|
|
Maintainer sequencing note after #66685 landed as d68ba5e: This PR splits that contract into |
|
This pull request has been automatically marked as stale due to inactivity. |
Resolving an exec/plugin approval that was not currently pending previously collapsed three different situations — the approval expired, it was already resolved, or the id was never seen — into a single
unknown or expired approval iderror. Operators could not tell whether to re-run the command (expired) or whether their decision had already landed (resolved), which is one of the concrete failure modes behind "Failed to submit approval: unknown or expired approval id".What changed
ExecApprovalManagerkeeps a bounded FIFO archive of terminated records (512 entries) and exposesclassifyApprovalId(), which respects caller visibility so it never leaks the existence of approvals the caller could not otherwise see.handleApprovalResolvenow reports an elapsed window asapproval expired(reason: APPROVAL_EXPIRED, with remediation) both inside the resolved-grace window and after the record is archived, and reports a post-grace decided approval asapproval already resolved. Genuinely unknown ids keep the existingunknown or expired approval idmessage. Same-decision idempotent retries during the grace window are unchanged.isApprovalExpiredError/isApprovalAlreadyResolvedErrordetectors alongside the existingisApprovalNotFoundError.Tests
pnpm exec vitest run --config test/vitest/vitest.gateway.config.ts src/gateway/exec-approval-manager.test.ts src/gateway/server-methods/approval-shared.test.ts— classify pending/resolved/expired/unknown, visibility filtering, post-grace archive, FIFO eviction; in-grace and post-grace expired vs already-resolved resolution.pnpm exec vitest run --config vitest.config.ts src/infra/approval-errors.test.ts— new detectors../node_modules/.bin/oxfmt --checkon all changed files.Real behavior proof
Behavior or issue addressed: Submitting an approval that is no longer pending returned the same
unknown or expired approval idwhether it had expired, had already been resolved, or never existed, so the operator had no idea whether re-running the command would help. This change makes an expired approval reportapproval expired(with anAPPROVAL_EXPIREDreason and remediation) distinctly from a genuinely unknown id, including after the in-memory grace window has elapsed and the record has moved to the bounded archive.Real environment tested: A real OpenClaw gateway runtime started from this branch's worktree on macOS (Darwin, Apple Silicon), exercising the genuine gateway server,
ExecApprovalManager, and theexec.approval.request/exec.approval.resolveserver methods over a websocket operator connection. No model backend is involved — approvals are created and resolved directly through the real gateway methods.Exact steps or command run after this patch: A standalone operator client connects to the in-process gateway and calls
exec.approval.request(with a short or longtimeoutMs) to register a real approval, lets the short one elapse, waits past the 15s resolved-grace window so the record is archived, then callsexec.approval.resolvefor: an expired-but-in-grace id, an expired-and-archived id, and a never-issued id, capturing each error.Evidence after fix: Live error responses returned by the running gateway's
exec.approval.resolve:Observed result after fix: An expired approval returns
approval expired/APPROVAL_EXPIREDboth within the grace window and after the record is archived, while a never-issued id still returnsunknown or expired approval id/APPROVAL_NOT_FOUND. Before this patch all of these returned the sameunknown or expired approval id. The remediation text now tells the operator to re-run the command to request a fresh approval.What was not tested: The already-resolved-after-grace path (
approval already resolved) was exercised through the unit suite rather than this live run, because a gateway approval with no interactive delivery route is expired by the gateway before it can be decided in this headless harness; the resolved-vs-expired classification itself is covered byexec-approval-manager.test.tsandapproval-shared.test.ts. No third-party model or channel was used.