fix: filter launchd handoff environment

This commit is contained in:
jesse-merhi
2026-04-29 13:09:20 +10:00
parent 7ddd815e46
commit 706eb8833f
3 changed files with 67 additions and 4 deletions

View File

@@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai
- Configure/Ollama: show the configured Ollama model allowlist after Cloud only or Cloud + Local setup and skip slow per-model cloud metadata fetches. (#73995) Thanks @obviyus.
- Channels/WhatsApp: detect explicit group `@mentions` again when the bot's own E.164 is in `allowFrom`, so shared-number setups no longer skip group pings that directly mention the bot. Fixes #49317. (#73453) Thanks @juan-flores077.
- WhatsApp/reliability: publish real transport-liveness into WhatsApp channel status and force earlier reconnects on silent transport stalls, so quiet healthy sessions stay connected while wedged sockets recover before the later remote 408 path. (#72656) Thanks @Sathvik-1007.
- Core/channels: tighten selected runtime, media, and plugin edge-case handling while preserving existing behavior. Thanks @jesse-merhi.
## 2026.4.27

View File

@@ -72,6 +72,35 @@ describe("scheduleDetachedLaunchdRestartHandoff", () => {
expect(args[1]).not.toContain('basename "$service_target"');
});
it("sanitizes restart helper environment overrides before spawning", () => {
spawnMock.mockReturnValue({ pid: 4242, unref: unrefMock });
scheduleDetachedLaunchdRestartHandoff({
env: {
HOME: "/Users/test",
OPENCLAW_PROFILE: "default",
PATH: "/tmp/evil-bin",
DYLD_INSERT_LIBRARIES: "/tmp/evil.dylib",
NPM_CONFIG_GLOBALCONFIG: "/tmp/evil-npmrc",
},
mode: "kickstart",
});
const [, args, options] = spawnMock.mock.calls[0] as [
string,
string[],
{ env: Record<string, string | undefined> },
];
expect(args[1]).toContain("exec >>'/Users/test/.openclaw/logs/gateway-restart.log' 2>&1");
expect(args[1]).not.toContain("/tmp/evil-bin");
expect(args[1]).not.toContain("/tmp/evil.dylib");
expect(args[1]).not.toContain("/tmp/evil-npmrc");
expect(options.env.OPENCLAW_PROFILE).toBe("default");
expect(options.env.PATH).not.toBe("/tmp/evil-bin");
expect(options.env.DYLD_INSERT_LIBRARIES).toBeUndefined();
expect(options.env.NPM_CONFIG_GLOBALCONFIG).toBeUndefined();
});
it("rejects invalid launchd labels before spawning the helper", () => {
expect(() => {
scheduleDetachedLaunchdRestartHandoff({

View File

@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
import os from "node:os";
import path from "node:path";
import { formatErrorMessage } from "../infra/errors.js";
import { sanitizeHostExecEnv } from "../infra/host-env-security.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { sanitizeForLog } from "../terminal/ansi.js";
import { resolveGatewayLaunchAgentLabel } from "./constants.js";
@@ -25,6 +26,13 @@ export type LaunchdRestartTarget = {
const START_AFTER_EXIT_PRINT_RETRY_COUNT = 15;
const START_AFTER_EXIT_PRINT_RETRY_DELAY_SECONDS = 0.2;
type LaunchdRestartLogEnv = {
HOME?: string;
USERPROFILE?: string;
OPENCLAW_STATE_DIR?: string;
OPENCLAW_PROFILE?: string;
};
function assertValidLaunchAgentLabel(label: string): string {
const trimmed = label.trim();
if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) {
@@ -40,6 +48,27 @@ function resolveGuiDomain(): string {
return `gui/${process.getuid()}`;
}
function collectStringEnvOverrides(
env?: Record<string, string | undefined>,
): Record<string, string> | undefined {
const overrides = Object.fromEntries(
Object.entries(env ?? {}).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
return Object.keys(overrides).length > 0 ? overrides : undefined;
}
function collectRestartLogEnv(env?: Record<string, string | undefined>): LaunchdRestartLogEnv {
const source = { ...process.env, ...env };
return {
HOME: source.HOME,
USERPROFILE: source.USERPROFILE,
OPENCLAW_STATE_DIR: source.OPENCLAW_STATE_DIR,
OPENCLAW_PROFILE: source.OPENCLAW_PROFILE,
};
}
function resolveLaunchAgentLabel(env?: Record<string, string | undefined>): string {
const envLabel = normalizeOptionalString(env?.OPENCLAW_LAUNCHD_LABEL);
if (envLabel) {
@@ -80,11 +109,11 @@ export function isCurrentProcessLaunchdServiceLabel(
function buildLaunchdRestartScript(
mode: LaunchdRestartHandoffMode,
env: Record<string, string | undefined>,
restartLogEnv: LaunchdRestartLogEnv,
): string {
const waitForCallerPid = `wait_pid="$4"
label="$5"
${renderPosixRestartLogSetup(env)}
${renderPosixRestartLogSetup(restartLogEnv)}
printf '[%s] openclaw restart attempt source=launchd-handoff mode=${mode} target=%s waitPid=%s\\n' "$(date -u +%FT%TZ)" "$service_target" "$wait_pid" >&2
if [ -n "$wait_pid" ] && [ "$wait_pid" -gt 1 ] 2>/dev/null; then
while kill -0 "$wait_pid" >/dev/null 2>&1; do
@@ -169,13 +198,17 @@ export function scheduleDetachedLaunchdRestartHandoff(params: {
typeof params.waitForPid === "number" && Number.isFinite(params.waitForPid)
? Math.floor(params.waitForPid)
: 0;
const restartEnv = { ...process.env, ...params.env };
const restartLogEnv = collectRestartLogEnv(params.env);
const restartEnv = sanitizeHostExecEnv({
baseEnv: process.env,
overrides: collectStringEnvOverrides(params.env),
});
try {
const child = spawn(
"/bin/sh",
[
"-c",
buildLaunchdRestartScript(params.mode, restartEnv),
buildLaunchdRestartScript(params.mode, restartLogEnv),
"openclaw-launchd-restart-handoff",
target.serviceTarget,
target.domain,