fix(exec): preserve turnSourceChannel as messageProvider in approval followup runs (#74666)

When an exec-approval followup run has no deliverable route and no
gateway-internal channel, buildAgentFollowupArgs was passing channel=undefined
to the spawned agent. This left defaults.messageProvider=undefined in the
followup run, causing tools.elevated.allowFrom.<provider> checks to always
fail with provider=null after the user approved an async elevated command.

Thread turnSourceChannel through buildAgentFollowupArgs and use it as a
fallback when sessionOnlyOriginChannel is absent. Fixes #74646.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hcl
2026-04-30 12:25:16 +08:00
committed by GitHub
parent 38aac70830
commit 2de6ad4544
3 changed files with 30 additions and 1 deletions

View File

@@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai
- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.
- Channels/Microsoft Teams: treat configured `19:...@thread.tacv2` and legacy `19:...@thread.skype` team/channel IDs as already resolved during startup, avoiding false `channels unresolved` warnings while preserving Graph name lookup for display-name entries. Fixes #74683. Thanks @dseravalli.
- CLI/browser: preserve parent flags while lazy-loading browser subcommands, so `openclaw browser --json open` and `openclaw browser --json tabs` keep machine-readable output after reparsing. Fixes #74574. Thanks @devintegeritsm.
- Exec/elevated: preserve `turnSourceChannel` as `messageProvider` on approval-followup runs so `tools.elevated.allowFrom.<provider>` checks no longer fail with `provider=null` after the user approves an async elevated command. Fixes #74646. Thanks @xhd2015.
- Plugins/runtime-deps: add `openclaw plugins deps` inspection and repair with script-free package-manager defaults shared across plugin installers, so operators can repair missing bundled runtime deps without corrupting JSON output or blocking unrelated conflict-free deps. Thanks @vincentkoc.
- Agents/output: strip internal `[tool calls omitted]` replay placeholders from user-facing replies while preserving visible reply whitespace. Fixes #74573. Thanks @blaspat.
- Providers/Google Vertex: route authorized_user ADC credentials through OpenClaw's REST transport so Docker installs using gcloud application-default credentials no longer crash in the Google SDK before requests are sent. Fixes #74628. Thanks @frankhal2001-design.

View File

@@ -252,6 +252,28 @@ describe("exec approval followup", () => {
expect(sendMessage).not.toHaveBeenCalled();
});
it("preserves turnSourceChannel as messageProvider on the followup run when no deliverable route exists", async () => {
// Regression: #74646 — tools.elevated.allowFrom.<provider> fails in approval followup
await sendExecApprovalFollowup({
approvalId: "req-elevated-74646",
sessionKey: "agent:main:telegram:-100123",
turnSourceChannel: "telegram",
resultText: "Exec completed: systemctl status gateway",
});
expect(callGatewayTool).toHaveBeenCalledWith(
"agent",
expect.any(Object),
expect.objectContaining({
sessionKey: "agent:main:telegram:-100123",
deliver: false,
channel: "telegram",
}),
{ expectFinal: true },
);
expect(sendMessage).not.toHaveBeenCalled();
});
it("throws when neither a session nor a deliverable route is available", async () => {
await expect(
sendExecApprovalFollowup({

View File

@@ -145,17 +145,22 @@ function buildAgentFollowupArgs(params: {
resultText: string;
deliveryTarget: ExternalBestEffortDeliveryTarget;
sessionOnlyOriginChannel?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
}) {
const { deliveryTarget, sessionOnlyOriginChannel } = params;
// When the followup run has no deliverable route and no gateway-internal channel,
// preserve the raw turnSourceChannel so the spawned agent inherits messageProvider.
// Without this, tools.elevated.allowFrom.<provider> checks fail with provider=null.
const fallbackChannel = sessionOnlyOriginChannel ?? params.turnSourceChannel;
return {
sessionKey: params.sessionKey,
message: buildExecApprovalFollowupPrompt(params.resultText),
deliver: deliveryTarget.deliver,
...(deliveryTarget.deliver ? { bestEffortDeliver: true as const } : {}),
channel: deliveryTarget.deliver ? deliveryTarget.channel : sessionOnlyOriginChannel,
channel: deliveryTarget.deliver ? deliveryTarget.channel : fallbackChannel,
to: deliveryTarget.deliver
? deliveryTarget.to
: sessionOnlyOriginChannel
@@ -241,6 +246,7 @@ export async function sendExecApprovalFollowup(
resultText,
deliveryTarget,
sessionOnlyOriginChannel,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,