Skip to content

fix(exec-approval): distinguish non-persistable command from ask=always policy in approval prompt#98215

Closed
ianalloway wants to merge 330 commits into
openclaw:mainfrom
ianalloway:fix/approval-nonpersistable-message
Closed

fix(exec-approval): distinguish non-persistable command from ask=always policy in approval prompt#98215
ianalloway wants to merge 330 commits into
openclaw:mainfrom
ianalloway:fix/approval-nonpersistable-message

Conversation

@ianalloway

@ianalloway ianalloway commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

When `allowAlwaysPersistence.kind === "one-shot"` (for example, a command that uses shell redirection), `allow-always` is correctly excluded from the allowed decisions. The approval prompt still showed the same explanation used for an `ask=always` policy:

"The effective approval policy requires approval every time, so Allow Always is unavailable."

That is misleading because the command can be approved; it just cannot be saved as a reusable approval pattern.

Closes #97069

What Changed

  1. exec-approval-reply.ts — added optional nonPersistableCommand field to ExecApprovalPendingReplyParams. When true, buildExecApprovalPendingReplyPayload emits a distinct message explaining the command cannot be saved as a reusable pattern.

  2. bash-tools.exec-runtime.ts — same branch added to buildApprovalPendingMessage (foreground/CLI approval message path).

  3. bash-tools.exec-types.ts — added nonPersistableCommand?: boolean to the approval-pending variant of ExecToolDetails, so the flag survives the gateway→tool-result→embedded-handler hop.

  4. bash-tools.exec-host-shared.tsbuildExecApprovalPendingToolResult accepts and passes nonPersistableCommand through to both the message builder and the details output.

  5. bash-tools.exec-host-gateway.ts — derives and passes nonPersistableCommand: effectiveAllowAlwaysPersistence.kind === "one-shot" && hostAsk !== "always" to the tool-result builder.

  6. bash-tools.exec-host-node.ts — same derivation using allowAlwaysPersistence and approvalDecisionAsk.

  7. embedded-agent-subscribe.handlers.tools.tsreadExecApprovalPendingDetails return type extended with nonPersistableCommand?: boolean; value read from details and forwarded to buildExecApprovalPendingReplyPayload.

  8. exec-approval-forwarding.ts (Telegram) — derives nonPersistableCommand from allowedDecisions and request.request.ask and passes it through.

Evidence

188/188 tests pass across 5 test files after the change, including two tests that directly assert the distinct copy for non-persistable vs. ask=always:

$ pnpm exec vitest run \
    src/infra/exec-approval-reply.test.ts \
    src/agents/bash-tools.exec-host-shared.test.ts \
    src/agents/bash-tools.exec-host-gateway.test.ts

 Test Files  5 passed (5)
      Tests  188 passed (188)
   Duration  ~14s

Specific assertions in exec-approval-reply.test.ts (34/34):

  • shows non-persistable message when allow-always is excluded by command type — asserts output contains "cannot be saved as a reusable pattern", not "policy requires approval every time"
  • shows policy message when allow-always is excluded by ask=always policy — asserts output contains "policy requires approval every time", not "cannot be saved as a reusable pattern"

Gateway test confirms wiring at the buildExecApprovalPendingToolResult call site: src/agents/bash-tools.exec-host-gateway.test.ts 106/106 passing.

Production Paths Now Wired

Path Before After
Gateway foreground (buildApprovalPendingMessage) always policy copy ✅ correct copy
Gateway tool result details (ExecToolDetails) no flag ✅ flag stored
Embedded handler → reply payload no flag ✅ flag forwarded
Telegram forwarder → reply payload no flag ✅ flag derived + forwarded
Node foreground always policy copy ✅ correct copy

Copilot AI review requested due to automatic review settings June 30, 2026 16:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@openclaw-barnacle openclaw-barnacle Bot added size: XS triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jun 30, 2026
@clawsweeper

clawsweeper Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I did a careful shell check against current main, and this is already implemented.

Current main already solves the reported approval-copy problem through the broader merged implementation from #97740, so this competing branch is no longer necessary.

Root-cause cluster
Relationship: superseded
Canonical: #97069
Summary: The canonical bug report was fixed by the merged #97740; this PR and the remaining competing branches are superseded.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

So I’m closing this as already implemented rather than keeping a duplicate issue open.

Review details

Best possible solution:

Keep the merged reason-neutral wording across all approval surfaces; it fixes the false policy attribution without adding a parallel unavailable-reason field or retaining this obsolete branch.

Do we have a high-confidence way to reproduce the issue?

No on current main. The shared reply, generic forwarder, foreground agent, reaction, gateway validation, and Control UI paths now use truthful command-specific unavailable copy.

Is this the best way to solve the issue?

Yes. The merged neutral-copy solution fixes every known renderer with less state plumbing and compatibility surface than this PR’s optional flag.

Security review:

Security review cleared: No concrete security or supply-chain concern remains relevant because this branch should close rather than merge.

AGENTS.md: found and applied where relevant.

What I checked:

Likely related people:

  • saju01: Authored the merged fix that provides the current-main neutral approval copy across all affected surfaces. (role: introduced current behavior; confidence: high; commits: cbc84767bd7e; files: src/agents/bash-tools.exec-runtime.ts, src/gateway/server-methods/exec-approval.ts, src/infra/exec-approval-forwarder.ts)
  • vincentkoc: Co-authored the landing commit, closed the canonical issue, and has recent merged history in approval reaction and forwarding code. (role: merger and adjacent owner; confidence: high; commits: cbc84767bd7e, b3fbe5325e0b, b261e9e6ddaa; files: src/plugin-sdk/approval-reaction-runtime.ts, src/infra/exec-approval-forwarder.ts)
  • jesse-merhi: The one-shot persistence behavior exposing the misleading copy appears to date to the command-planner work in c9707ab635b9. (role: introduced related behavior; confidence: medium; commits: c9707ab635b9; files: src/gateway/server-methods/exec-approval.ts)

Codex review notes: model internal, reasoning high; reviewed against bc239a851618; fix evidence: commit cbc84767bd7e, main fix timestamp 2026-07-11T10:09:51Z.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9dd6623c7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/infra/exec-approval-reply.ts
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. labels Jun 30, 2026
@clawsweeper
clawsweeper Bot temporarily deployed to qa-live-shared June 30, 2026 16:36 Inactive
@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram agents Agent runtime and tooling size: S triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. and removed triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. size: XS labels Jul 7, 2026
@ianalloway

Copy link
Copy Markdown
Contributor Author

@jesse-merhi all CI is green — production wiring is in place for gateway, node, embedded, and Telegram paths. Ready for merge when you have a moment.

tirion-p and others added 7 commits July 11, 2026 12:23
* Update Claude CLI billing docs

* Sync Claude live testing billing note
…#97289)

* fix(shared): truncate assistant error text on UTF-16 boundary

Use truncateUtf16Safe from the existing utf16-slice utility instead of
String.prototype.slice(0, 600), which can split a surrogate pair and
produce malformed Unicode when the error message contains characters
outside the Basic Multilingual Plane (emoji, rare CJK, etc.).

* test(shared): add regression test for UTF-16 safe error truncation

Verify formatRawAssistantErrorForUi does not produce dangling surrogates
when truncating a fallback raw error message with a non-BMP character
(emoji) straddling the 600-code-unit boundary.

* fix(agents): truncate assistant error text fallback on UTF-16 boundary

Use truncateUtf16Safe instead of String.prototype.slice(0,600) in
formatAssistantErrorText and its companion comparison in
isRawAssistantErrorPassthrough, preventing dangling surrogates when
non-BMP characters straddle the 600-code-unit truncation boundary.
truncateReplyBody used a raw value.slice(0, MAX_REPLY_BODY_LENGTH - 3)
to shorten long reply bodies. When the cut index fell between the two
UTF-16 code units of a surrogate pair (e.g. an emoji), the slice left a
lone high surrogate before the ellipsis, which renders as a broken glyph
in the reply context shown to the agent.

Replace the raw slice with sliceUtf16Safe from the plugin SDK so the
truncation never cuts inside a surrogate pair. A normal (non-astral)
body is unaffected.

Co-authored-by: ly-wang19 <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
sanitizeForConsole filtered control characters code-point-aware but then
truncated with sanitized.slice(0, maxChars), which cuts on UTF-16 code units.
When the cap landed between the two code units of an astral character (emoji,
CJK extension, etc.) the output ended in a lone high surrogate before the
ellipsis. Cap on code points instead.

Co-authored-by: ly-wang19 <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
* fix(gateway): surface idempotency key in history metadata

* test(gateway): avoid unsafe optional chain

* fix(gateway): preserve oversized transcript idempotency keys

* fix(gateway): preserve oversized idempotency keys

* fix(gateway): reuse transcript json helpers

---------

Co-authored-by: 吴杨帆 <[email protected]>
* ci(docker): publish releases to Docker Hub

* ci(docker): clarify beta image tags
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Guard

This PR changes dependency-related files. Maintainers should confirm these changes are intentional.

Changed files:

  • extensions/qa-lab/package.json
  • package.json
  • pnpm-lock.yaml
  • pnpm-workspace.yaml

Maintainer follow-up:

  • Review whether the dependency changes are intentional.
  • Inspect resolved package deltas when lockfile, shrinkwrap, or workspace dependency policy changes are present.
  • Treat package-lock.json and npm-shrinkwrap.json diffs as security-review surfaces.
  • Run pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json locally for detailed release-style evidence.

@github-actions

Copy link
Copy Markdown
Contributor

Dependency graph changes are blocked

OpenClaw does not accept dependency graph changes through PRs unless a repository admin or security explicitly authorizes the current head SHA. Dependency updates are generated internally by maintainers so external PRs cannot change the resolved graph.

Detected dependency graph changes:

  • pnpm-lock.yaml changed.
  • extensions/qa-lab/package.json changed dependencies, devDependencies, peerDependencies, version.
  • package.json changed dependencies, devDependencies, version, engines.

Auto-scrub was not attempted because this PR changes package manifest dependency graph fields:

  • extensions/qa-lab/package.json changed dependencies, devDependencies, peerDependencies, version.
  • package.json changed dependencies, devDependencies, version, engines.

Dependency graph changes must be reviewed by security or handled by maintainers internally. Please remove lockfile changes manually if they are not needed.

To remove lockfile changes, restore them from the target branch:

git fetch origin
git checkout 'origin/main' -- 'pnpm-lock.yaml'
git commit -m 'chore: remove dependency lockfile change'
git push

If this PR intentionally needs a dependency graph change, ask a repository admin or member of @openclaw/openclaw-secops to comment:

/allow-dependencies-change

The action will approve the current head SHA (e09f6486f7008a4d45b791dfec804b878e291df5) when it reruns. A later push requires a fresh approval.

@steipete

Copy link
Copy Markdown
Contributor

Thanks for the careful analysis and focused tests here.

Current main already carries the same user-facing fix through #97740, merged as cbc8476. That implementation uses command-specific neutral copy across the shared approval renderers without adding parallel optional state through each execution path.

This branch also includes a large amount of unrelated history, including an iOS change already present on main, so rebasing or salvaging it would not improve the canonical fix. Closing as superseded. If the wording is still wrong on current main, a new focused reproduction against the landed path would change this decision.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: ios App: ios app: macos App: macos app: web-ui App: web-ui channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: qqbot channel: signal Channel integration: signal channel: slack Channel integration: slack channel: synology-chat channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo cli CLI command changes commands Command implementations dependencies-changed PR changes dependency-related files docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: amazon-bedrock extensions: anthropic extensions: chutes extensions: codex extensions: deepinfra extensions: device-pair extensions: diagnostics-otel Extension: diagnostics-otel extensions: duckduckgo extensions: fal extensions: firecrawl extensions: github-copilot extensions: google extensions: inworld Extension: inworld extensions: memory-core Extension: memory-core extensions: memory-wiki extensions: minimax extensions: ollama extensions: openai extensions: opencode extensions: openrouter extensions: qa-lab extensions: talk-voice extensions: together extensions: volcengine extensions: xai extensions: xiaomi extensions: zai gateway Gateway runtime mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P2 Normal backlog priority with limited blast radius. plugin: file-transfer plugin: google-meet plugin: pixverse rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. scripts Repository scripts size: XL status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Approval prompt wrongly says effective policy requires approval every time for non-persistable redirected command