fix(approval): distinguish policy vs non-persistable reason for missing allow-always (fixes #97069) (AI-assisted)#97077
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 27, 2026, 11:09 AM ET / 15:09 UTC. Summary PR surface: Source +119, Tests +365. Total +484 across 58 files. Reproducibility: yes. Current main is source-reproducible because Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land one canonical, tested fix for #97069 that preserves missing/null Do we have a high-confidence way to reproduce the issue? Yes. Current main is source-reproducible because Is this the best way to solve the issue? Yes, if maintainers choose this branch as the canonical implementation. The ask-aware approach is a narrow renderer/data-plumbing fix; a typed unavailable-reason branch is broader and could cover the manual resolve-error path too, but it is more invasive for the reported prompt bug. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 4010b81a77f4. Label changesLabel justifications:
Evidence reviewedPR surface: Source +119, Tests +365. Total +484 across 58 files. View PR surface stats
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
|
Mantis Telegram Desktop ProofSummary: Mantis captured native Telegram Desktop before/after GIFs showing the approval prompt explanation change.
Motion-trimmed clips: |
69c4831 to
a525aed
Compare
a525aed to
5f6ee9c
Compare
2d9dd4b to
889bdff
Compare
…ng allow-always When allow-always is unavailable due to shell redirection or other non-persistable command elements, the approval prompt now correctly explains the real reason instead of misleadingly blaming the approval policy. Fixes openclaw#97069
Carry the parameter through Telegram's shared approval payload builders so the prompt correctly distinguishes ask=always policy explanations from non-persistable one-shot commands. - exec-approval-forwarding: pass request.request.ask - approval-handler.runtime: pass view.ask for exec approvals - Add regression tests for ask=always and ask=on-miss rendering
The shared buildExecApprovalPendingReplyPayload helper uses params.ask to distinguish policy-required (ask=always) from non-persistable (one-shot) Allow Always explanations. The embedded tool-result delivery path was calling this helper without ask, causing ask=always approvals to render the wrong non-persistable copy. - Read ask from approval-pending tool details - Pass ask through to buildExecApprovalPendingReplyPayload - Add regression tests for ask=always and ask=on-miss on embedded path
…de exec hosts Add ask?: ExecAsk to ExecToolDetails approval-pending variant and buildExecApprovalPendingToolResult params. Pass hostAsk from both gateway and node exec hosts so the embedded renderer reads real production data instead of relying on manually injected test fields.
…olicy fallback for omitted ask - Add ask param to buildApprovalPendingMessage so tool-result content distinguishes ask-always policy from non-persistable commands - Pass ask from buildExecApprovalPendingToolResult to message builder - Keep policy-required explanation as default when ask is omitted in exported reply helper, preserving backward compatibility for callers that pass filtered decisions without explicit ask
The forwarder condition was inverted: when ask is undefined (omitted), it showed the non-persistable message instead of the policy-required message. Align with exec-approval-reply.ts and buildApprovalPendingMessage patterns where omitted ask defaults to the policy-required explanation. Add regression tests for omitted ask in both forwarder and reply helpers.
…ng allow-always Carry ask context through all approval prompt renderers so that one-shot/non-persistable commands show the correct reason when Allow Always is unavailable, instead of always showing the policy-required explanation. - Add ask param to buildApprovalPendingMessage, buildExecApprovalPendingToolResult, and ExecToolDetails - Pass ask from gateway/node exec hosts and Telegram/forwarding surfaces - Keep omitted ask as policy-safe fallback in exported reply helper - Add regression tests for ask=always vs ask=on-miss across all surfaces Fixes openclaw#97069
…ge builders Add inline comments to both buildApprovalPendingMessage and buildExecApprovalPendingReplyPayload explaining that when is omitted (e.g. plugin SDK callers passing filtered decisions without the full exec context), the policy-required explanation is shown rather than the non-persistable message.
…enderers Update the remaining two prompt renderers that still showed policy-only Allow Always warning when ask context was available: - Control UI exec-approval.ts: check request.ask to select between non-persistable and policy-required i18n keys - plugin-sdk approval-reaction-runtime.ts: pass view.ask through to buildManualInstructionSection for ask-aware message selection - en.ts: add allowAlwaysUnavailableNonPersistable i18n key Add focused tests covering ask=always (policy), ask=on-miss (non-persistable), and omitted ask (policy fallback) scenarios.
…locales - Use the same approvalDecisionAsk that filtered Allow Always decisions instead of hostAsk for the exec approval pending tool result ask field - Run pnpm ui:i18n:sync to generate locale bundles for the new allowAlwaysUnavailableNonPersistable key
Pass view.ask only after discriminating approvalKind === 'exec' to satisfy TypeScript's union narrowing for PendingApprovalView. Plugin approval views do not carry ask, so the previous view.ask access was a TS2339 error on exact-head CI. Ref: Codex review finding on PR openclaw#97077
fd7e4f4 to
cee5dee
Compare
…checks Normalize null and undefined in the ask-aware unavailable-copy branches across reply, forwarder, and Control UI renderers so that explicit ask: null uses the policy-required fallback instead of the non-persistable message. Add focused null regression tests for reply and forwarder. Addresses Codex review finding on PR openclaw#97077.




What Problem This Solves
When a command includes shell redirection (e.g.,
openclaw --version 2>&1), the approval prompt incorrectly explains the unavailability of "Allow Always" by blaming the approval policy:This is misleading when the effective policy is
ask=on-miss— the real reason is that the command cannot be persisted due to shell redirection or dynamic content. Users may misdiagnose their approval policy asask=alwaysand waste time troubleshooting.Why This Change Was Made
The approval prompt text generation in
exec-approval-reply.tsandexec-approval-forwarder.tsunconditionally used the policy-related message whenallow-alwayswas excluded from allowed decisions. However,allow-alwayscan be excluded for two distinct reasons:ask=always): The effective policy requires approval every timeThe fix checks the
askparameter to distinguish these cases and shows an appropriate message:ask=always→ "The effective approval policy requires approval every time, so Allow Always is unavailable."User Impact
Users will now see accurate information about why "Allow Always" is unavailable, reducing confusion and troubleshooting time. The fix is purely informational — no approval behavior changes.
Evidence
Real behavior proof
buildExecApprovalPendingReplyPayloadandbuildExecApprovalRequestMessagewith bothask=alwaysandask=on-miss+allowedDecisions: ["allow-once", "deny"]to verify correct message textPending command:
openclaw --version 2>&1Other options:
Allow Always is unavailable because this command cannot be persisted (e.g., shell redirection or dynamic content).
Host: gateway
Full id:
test-oneshot