diff --git a/CHANGELOG.md b/CHANGELOG.md index 68fd8dcf57f..3034788d190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -312,6 +312,7 @@ Docs: https://docs.openclaw.ai - Agents/strict OpenAI turn ordering: apply assistant-first transcript bootstrap sanitization to strict OpenAI-compatible providers (for example vLLM/Gemma via `openai-completions`) without adding Google-specific session markers, preventing assistant-first history rejections. (#39252) Thanks @scoootscooob. - Discord/exec approvals gateway auth: pass resolved shared gateway credentials into the Discord exec-approvals gateway client so token-auth installs stop failing approvals with `gateway token mismatch`. Related to #38179. Thanks @0riginal-claw for the adjacent PR #35147 investigation. - Subagents/workspace inheritance: propagate parent workspace directory to spawned subagent runs so child sessions reliably inherit workspace-scoped instructions (`AGENTS.md`, `SOUL.md`, etc.) without exposing workspace override through tool-call arguments. (#39247) Thanks @jasonQin6. +- Exec approvals/gateway-node policy: honor explicit `ask=off` from `exec-approvals.json` even when runtime defaults are stricter, so trusted full/off setups stop re-prompting on gateway and node exec paths. Landed from contributor PR #26789 by @pandego. Thanks @pandego. ## 2026.3.2 diff --git a/src/agents/bash-tools.exec-host-shared.ts b/src/agents/bash-tools.exec-host-shared.ts index eef3575fed3..c24e0a2f1fa 100644 --- a/src/agents/bash-tools.exec-host-shared.ts +++ b/src/agents/bash-tools.exec-host-shared.ts @@ -133,7 +133,9 @@ export function resolveExecHostApprovalContext(params: { ask: params.ask, }); const hostSecurity = minSecurity(params.security, approvals.agent.security); - const hostAsk = maxAsk(params.ask, approvals.agent.ask); + // An explicit ask=off policy in exec-approvals.json must be able to suppress + // prompts even when tool/runtime defaults are stricter (for example on-miss). + const hostAsk = approvals.agent.ask === "off" ? "off" : maxAsk(params.ask, approvals.agent.ask); const askFallback = approvals.agent.askFallback; if (hostSecurity === "deny") { throw new Error(`exec denied: host=${params.host} security=deny`); diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts index 3e0b9d6292e..a1314ec7981 100644 --- a/src/agents/bash-tools.exec.approval-id.test.ts +++ b/src/agents/bash-tools.exec.approval-id.test.ts @@ -187,6 +187,43 @@ describe("exec approvals", () => { expect(calls).not.toContain("exec.approval.request"); }); + it("uses exec-approvals ask=off to suppress gateway prompts", async () => { + const approvalsPath = path.join(process.env.HOME ?? "", ".openclaw", "exec-approvals.json"); + await fs.mkdir(path.dirname(approvalsPath), { recursive: true }); + await fs.writeFile( + approvalsPath, + JSON.stringify( + { + version: 1, + defaults: { security: "full", ask: "off", askFallback: "full" }, + agents: { + main: { security: "full", ask: "off", askFallback: "full" }, + }, + }, + null, + 2, + ), + ); + + const calls: string[] = []; + vi.mocked(callGatewayTool).mockImplementation(async (method) => { + calls.push(method); + return { ok: true }; + }); + + const tool = createExecTool({ + host: "gateway", + ask: "on-miss", + security: "full", + approvalRunningNoticeMs: 0, + }); + + const result = await tool.execute("call3b", { command: "echo ok" }); + expect(result.details.status).toBe("completed"); + expect(calls).not.toContain("exec.approval.request"); + expect(calls).not.toContain("exec.approval.waitDecision"); + }); + it("requires approval for elevated ask when allowlist misses", async () => { const calls: string[] = []; let resolveApproval: (() => void) | undefined;