diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e38cc1703a..6f369f55d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,7 @@ Docs: https://docs.openclaw.ai - Agents/compaction: rerun transcript repair after `session.compact()` so orphaned `tool_result` blocks cannot survive compaction and break later Anthropic requests. (#16095) thanks @claw-sylphx. - Agents/compaction: trigger overflow recovery from the tool-result guard once post-compaction context still exceeds the safe threshold, so long tool loops compact before the next model call hard-fails. (#29371) thanks @keshav55. - macOS/exec approvals: harden exec-host request HMAC verification to use a timing-safe compare and keep malformed or truncated signatures fail-closed in focused IPC auth coverage. +- Gateway/exec approvals: surface requested env override keys in gateway-host approval prompts so operators can review surviving env context without inheriting noisy base host env. ## 2026.3.13 diff --git a/src/agents/bash-tools.exec-host-gateway.ts b/src/agents/bash-tools.exec-host-gateway.ts index 149a4785dd5..4a0223af7a4 100644 --- a/src/agents/bash-tools.exec-host-gateway.ts +++ b/src/agents/bash-tools.exec-host-gateway.ts @@ -40,6 +40,7 @@ export type ProcessGatewayAllowlistParams = { command: string; workdir: string; env: Record; + requestedEnv?: Record; pty: boolean; timeoutSec?: number; defaultTimeoutSec: number; @@ -152,6 +153,7 @@ export async function processGatewayAllowlist( await registerExecApprovalRequestForHostOrThrow({ approvalId, command: params.command, + env: params.requestedEnv, workdir: params.workdir, host: "gateway", security: hostSecurity, diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index 8a0bd30907a..5fe0f7deac4 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -429,6 +429,7 @@ export function createExecTool( command: params.command, workdir, env, + requestedEnv: params.env, pty: params.pty === true && !sandbox, timeoutSec: params.timeout, defaultTimeoutSec, diff --git a/src/gateway/server-methods/exec-approval.ts b/src/gateway/server-methods/exec-approval.ts index 81d479cbbd6..383e8498a28 100644 --- a/src/gateway/server-methods/exec-approval.ts +++ b/src/gateway/server-methods/exec-approval.ts @@ -4,7 +4,10 @@ import { DEFAULT_EXEC_APPROVAL_TIMEOUT_MS, type ExecApprovalDecision, } from "../../infra/exec-approvals.js"; -import { buildSystemRunApprovalBinding } from "../../infra/system-run-approval-binding.js"; +import { + buildSystemRunApprovalBinding, + buildSystemRunApprovalEnvBinding, +} from "../../infra/system-run-approval-binding.js"; import { resolveSystemRunApprovalRequestContext } from "../../infra/system-run-approval-context.js"; import type { ExecApprovalManager } from "../exec-approval-manager.js"; import { @@ -107,6 +110,7 @@ export function createExecApprovalHandlers( ); return; } + const envBinding = buildSystemRunApprovalEnvBinding(p.env); const systemRunBinding = host === "node" ? buildSystemRunApprovalBinding({ @@ -132,7 +136,7 @@ export function createExecApprovalHandlers( ? undefined : sanitizeExecApprovalDisplayText(approvalContext.commandPreview), commandArgv: host === "node" ? undefined : effectiveCommandArgv, - envKeys: systemRunBinding?.envKeys?.length ? systemRunBinding.envKeys : undefined, + envKeys: envBinding.envKeys.length > 0 ? envBinding.envKeys : undefined, systemRunBinding: systemRunBinding?.binding ?? null, systemRunPlan: approvalContext.plan, cwd: effectiveCwd ?? null, diff --git a/src/gateway/server-methods/server-methods.test.ts b/src/gateway/server-methods/server-methods.test.ts index bd42485f4f8..a7afcb60f5f 100644 --- a/src/gateway/server-methods/server-methods.test.ts +++ b/src/gateway/server-methods/server-methods.test.ts @@ -6,7 +6,10 @@ import { fileURLToPath } from "node:url"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { emitAgentEvent } from "../../infra/agent-events.js"; import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js"; -import { buildSystemRunApprovalBinding } from "../../infra/system-run-approval-binding.js"; +import { + buildSystemRunApprovalBinding, + buildSystemRunApprovalEnvBinding, +} from "../../infra/system-run-approval-binding.js"; import { resetLogger, setLoggerOverride } from "../../logging.js"; import { ExecApprovalManager } from "../exec-approval-manager.js"; import { validateExecApprovalRequestParams } from "../protocol/index.js"; @@ -583,6 +586,31 @@ describe("exec approval handlers", () => { ); }); + it("stores sorted env keys for gateway approvals without node-only binding", async () => { + const { handlers, broadcasts, respond, context } = createExecApprovalFixture(); + await requestExecApproval({ + handlers, + respond, + context, + params: { + host: "gateway", + nodeId: undefined, + systemRunPlan: undefined, + env: { + Z_VAR: "z", + A_VAR: "a", + }, + }, + }); + const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested"); + expect(requested).toBeTruthy(); + const request = (requested?.payload as { request?: Record })?.request ?? {}; + expect(request["envKeys"]).toEqual( + buildSystemRunApprovalEnvBinding({ A_VAR: "a", Z_VAR: "z" }).envKeys, + ); + expect(request["systemRunBinding"]).toBeNull(); + }); + it("prefers systemRunPlan canonical command/cwd when present", async () => { const { handlers, broadcasts, respond, context } = createExecApprovalFixture(); await requestExecApproval({