From b48291e01eca26a5b04ea1d6219c13b4437c3ead Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 9 Mar 2026 16:14:08 -0700 Subject: [PATCH] Exec: mark child command env with OPENCLAW_CLI (#41411) --- src/agents/sandbox-create-args.test.ts | 10 +++++++++- src/agents/sandbox/docker.ts | 3 ++- src/entry.ts | 2 ++ src/infra/host-env-security.test.ts | 5 +++++ src/infra/host-env-security.ts | 5 +++-- src/infra/openclaw-exec-env.ts | 16 ++++++++++++++++ src/process/exec.test.ts | 2 ++ src/process/exec.ts | 3 ++- 8 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/infra/openclaw-exec-env.ts diff --git a/src/agents/sandbox-create-args.test.ts b/src/agents/sandbox-create-args.test.ts index 9bc00547143..0d9621ad9e1 100644 --- a/src/agents/sandbox-create-args.test.ts +++ b/src/agents/sandbox-create-args.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { OPENCLAW_CLI_ENV_VALUE } from "../infra/openclaw-exec-env.js"; import { buildSandboxCreateArgs } from "./sandbox/docker.js"; import type { SandboxDockerConfig } from "./sandbox/types.js"; @@ -113,7 +114,14 @@ describe("buildSandboxCreateArgs", () => { "1.5", ]), ); - expect(args).toEqual(expect.arrayContaining(["--env", "LANG=C.UTF-8"])); + expect(args).toEqual( + expect.arrayContaining([ + "--env", + "LANG=C.UTF-8", + "--env", + `OPENCLAW_CLI=${OPENCLAW_CLI_ENV_VALUE}`, + ]), + ); const ulimitValues: string[] = []; for (let i = 0; i < args.length; i += 1) { diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index 2bd9dad12b5..68c95e343ea 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -162,6 +162,7 @@ export function execDockerRaw( } import { formatCliCommand } from "../../cli/command-format.js"; +import { markOpenClawExecEnv } from "../../infra/openclaw-exec-env.js"; import { defaultRuntime } from "../../runtime.js"; import { computeSandboxConfigHash } from "./config-hash.js"; import { DEFAULT_SANDBOX_IMAGE } from "./constants.js"; @@ -365,7 +366,7 @@ export function buildSandboxCreateArgs(params: { if (params.cfg.user) { args.push("--user", params.cfg.user); } - const envSanitization = sanitizeEnvVars(params.cfg.env ?? {}); + const envSanitization = sanitizeEnvVars(markOpenClawExecEnv(params.cfg.env ?? {})); if (envSanitization.blocked.length > 0) { log.warn(`Blocked sensitive environment variables: ${envSanitization.blocked.join(", ")}`); } diff --git a/src/entry.ts b/src/entry.ts index 50b08029d05..14a839f38b9 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -9,6 +9,7 @@ import { shouldSkipRespawnForArgv } from "./cli/respawn-policy.js"; import { normalizeWindowsArgv } from "./cli/windows-argv.js"; import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js"; import { isMainModule } from "./infra/is-main.js"; +import { ensureOpenClawExecMarkerOnProcess } from "./infra/openclaw-exec-env.js"; import { installProcessWarningFilter } from "./infra/warning-filter.js"; import { attachChildProcessBridge } from "./process/child-process-bridge.js"; @@ -41,6 +42,7 @@ if ( // Imported as a dependency — skip all entry-point side effects. } else { process.title = "openclaw"; + ensureOpenClawExecMarkerOnProcess(); installProcessWarningFilter(); normalizeEnv(); if (!isTruthyEnvValue(process.env.NODE_DISABLE_COMPILE_CACHE)) { diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index 116006dbbcf..4e7bcdb9ed9 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -10,6 +10,7 @@ import { sanitizeHostExecEnv, sanitizeSystemRunEnvOverrides, } from "./host-env-security.js"; +import { OPENCLAW_CLI_ENV_VALUE } from "./openclaw-exec-env.js"; describe("isDangerousHostEnvVarName", () => { it("matches dangerous keys and prefixes case-insensitively", () => { @@ -40,6 +41,7 @@ describe("sanitizeHostExecEnv", () => { }); expect(env).toEqual({ + OPENCLAW_CLI: OPENCLAW_CLI_ENV_VALUE, PATH: "/usr/bin:/bin", OK: "1", }); @@ -68,6 +70,7 @@ describe("sanitizeHostExecEnv", () => { }); expect(env.PATH).toBe("/usr/bin:/bin"); + expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE); expect(env.BASH_ENV).toBeUndefined(); expect(env.GIT_SSH_COMMAND).toBeUndefined(); expect(env.EDITOR).toBeUndefined(); @@ -91,6 +94,7 @@ describe("sanitizeHostExecEnv", () => { }); expect(env.PATH).toBe("/usr/bin:/bin"); + expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE); expect(env.OK).toBe("1"); expect(env.SHELLOPTS).toBeUndefined(); expect(env.PS4).toBeUndefined(); @@ -109,6 +113,7 @@ describe("sanitizeHostExecEnv", () => { }); expect(env.GOOD_KEY).toBe("ok"); + expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE); expect(env[" BAD KEY"]).toBeUndefined(); expect(env["NOT-PORTABLE"]).toBeUndefined(); }); diff --git a/src/infra/host-env-security.ts b/src/infra/host-env-security.ts index 56b30bd0818..8c5d0989fdd 100644 --- a/src/infra/host-env-security.ts +++ b/src/infra/host-env-security.ts @@ -1,4 +1,5 @@ import HOST_ENV_SECURITY_POLICY_JSON from "./host-env-security-policy.json" with { type: "json" }; +import { markOpenClawExecEnv } from "./openclaw-exec-env.js"; const PORTABLE_ENV_VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/; @@ -101,7 +102,7 @@ export function sanitizeHostExecEnv(params?: { } if (!overrides) { - return merged; + return markOpenClawExecEnv(merged); } for (const [rawKey, value] of Object.entries(overrides)) { @@ -124,7 +125,7 @@ export function sanitizeHostExecEnv(params?: { merged[key] = value; } - return merged; + return markOpenClawExecEnv(merged); } export function sanitizeSystemRunEnvOverrides(params?: { diff --git a/src/infra/openclaw-exec-env.ts b/src/infra/openclaw-exec-env.ts new file mode 100644 index 00000000000..b4e8a876584 --- /dev/null +++ b/src/infra/openclaw-exec-env.ts @@ -0,0 +1,16 @@ +export const OPENCLAW_CLI_ENV_VAR = "OPENCLAW_CLI"; +export const OPENCLAW_CLI_ENV_VALUE = "1"; + +export function markOpenClawExecEnv>(env: T): T { + return { + ...env, + [OPENCLAW_CLI_ENV_VAR]: OPENCLAW_CLI_ENV_VALUE, + }; +} + +export function ensureOpenClawExecMarkerOnProcess( + env: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + env[OPENCLAW_CLI_ENV_VAR] = OPENCLAW_CLI_ENV_VALUE; + return env; +} diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index 19937d6cb32..88d9cfdd71e 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -3,6 +3,7 @@ import { EventEmitter } from "node:events"; import fs from "node:fs"; import process from "node:process"; import { describe, expect, it, vi } from "vitest"; +import { OPENCLAW_CLI_ENV_VALUE } from "../infra/openclaw-exec-env.js"; import { attachChildProcessBridge } from "./child-process-bridge.js"; import { resolveCommandEnv, runCommandWithTimeout, shouldSpawnWithShell } from "./exec.js"; @@ -31,6 +32,7 @@ describe("runCommandWithTimeout", () => { expect(resolved.OPENCLAW_BASE_ENV).toBe("base"); expect(resolved.OPENCLAW_TEST_ENV).toBe("ok"); expect(resolved.OPENCLAW_TO_REMOVE).toBeUndefined(); + expect(resolved.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE); }); it("suppresses npm fund prompts for npm argv", async () => { diff --git a/src/process/exec.ts b/src/process/exec.ts index ddc572092d8..3464a083894 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -4,6 +4,7 @@ import path from "node:path"; import process from "node:process"; import { promisify } from "node:util"; import { danger, shouldLogVerbose } from "../globals.js"; +import { markOpenClawExecEnv } from "../infra/openclaw-exec-env.js"; import { logDebug, logError } from "../logger.js"; import { resolveCommandStdio } from "./spawn-utils.js"; @@ -213,7 +214,7 @@ export function resolveCommandEnv(params: { resolvedEnv.npm_config_fund = "false"; } } - return resolvedEnv; + return markOpenClawExecEnv(resolvedEnv); } export async function runCommandWithTimeout(