mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix(exec): prevent shell startup files from overriding daemon env
Carries forward the focused shell startup suppression fix from #40200 by NewdlDewdl. - launch bash, zsh, and fish exec shells with startup files suppressed - preserve fish/bash/sh PATH fallback, non-interactive shell fallback, and Windows PowerShell behavior - add regression coverage for the affected shell arg paths Fixes #40179. Carries forward #40200. Thanks @NewdlDewdl.
This commit is contained in:
committed by
GitHub
parent
d35e6f79e1
commit
ea9f17256a
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/model config: resolve per-model extra params through canonical model keys while preserving legacy double-prefixed fallback entries, so provider-prefixed model ids such as `openrouter/auto` keep their configured runtime params. (#44319) Thanks @HenryXiaoYang.
|
||||
- Gateway/shutdown: report structured shutdown warnings and HTTP close timeout warnings through `ShutdownResult` while preserving lifecycle hook hardening. Carries forward #41296. Thanks @edenfunf.
|
||||
- Control UI: keep Agents Overview and config-form select dropdowns on their configured value after options render while preserving inherited agent model placeholders. Fixes #40352; carries forward #52948. Thanks @xiaoquanidea.
|
||||
- Agents/exec: launch zsh, bash, and fish host exec shells with startup files suppressed while preserving existing PATH fallbacks, so daemon env is not overridden by shell startup files. Carries forward #40200; fixes #40179. Thanks @NewdlDewdl.
|
||||
- Plugins/QA: prebuild the private QA channel runtime before plugin gauntlet source runs so wrapper CPU/RSS measurements are not polluted by private QA dist rebuild work. Thanks @vincentkoc.
|
||||
- Gateway/reload: bound default restart deferral and SIGUSR1 restart drain to five minutes while preserving explicit `deferralTimeoutMs: 0` indefinite waits, so stale active work accounting cannot block config reloads forever. Thanks @vincentkoc.
|
||||
- Active Memory: register the prompt-build hook with the configured recall timeout plus setup grace instead of the 150s maximum budget, so default memory recall cannot delay turn startup for multiple minutes. Thanks @vincentkoc.
|
||||
|
||||
@@ -46,9 +46,10 @@ describe("getShellConfig", () => {
|
||||
|
||||
if (isWin) {
|
||||
it("uses PowerShell on Windows", () => {
|
||||
const { shell } = getShellConfig();
|
||||
const { shell, args } = getShellConfig();
|
||||
const normalized = shell.toLowerCase();
|
||||
expect(normalized.includes("powershell") || normalized.includes("pwsh")).toBe(true);
|
||||
expect(args).toEqual(["-NoProfile", "-NonInteractive", "-Command"]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -56,28 +57,48 @@ describe("getShellConfig", () => {
|
||||
it("prefers bash when fish is default and bash is on PATH", () => {
|
||||
const binDir = createTempCommandDir(tempDirs, [{ name: "bash" }]);
|
||||
process.env.PATH = binDir;
|
||||
const { shell } = getShellConfig();
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe(path.join(binDir, "bash"));
|
||||
expect(args).toEqual(["--noprofile", "--norc", "-c"]);
|
||||
});
|
||||
|
||||
it("falls back to sh when fish is default and bash is missing", () => {
|
||||
const binDir = createTempCommandDir(tempDirs, [{ name: "sh" }]);
|
||||
process.env.PATH = binDir;
|
||||
const { shell } = getShellConfig();
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe(path.join(binDir, "sh"));
|
||||
expect(args).toEqual(["-c"]);
|
||||
});
|
||||
|
||||
it("falls back to env shell when fish is default and no sh is available", () => {
|
||||
process.env.PATH = "";
|
||||
const { shell } = getShellConfig();
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe("/usr/bin/fish");
|
||||
expect(args).toEqual(["--no-config", "-c"]);
|
||||
});
|
||||
|
||||
it("uses startup-suppressed args for zsh env shells", () => {
|
||||
process.env.SHELL = "/bin/zsh";
|
||||
process.env.PATH = "";
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe("/bin/zsh");
|
||||
expect(args).toEqual(["-f", "-c"]);
|
||||
});
|
||||
|
||||
it("uses startup-suppressed args for bash env shells", () => {
|
||||
process.env.SHELL = "/bin/bash";
|
||||
process.env.PATH = "";
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe("/bin/bash");
|
||||
expect(args).toEqual(["--noprofile", "--norc", "-c"]);
|
||||
});
|
||||
|
||||
it("uses sh when SHELL is unset", () => {
|
||||
delete process.env.SHELL;
|
||||
process.env.PATH = "";
|
||||
const { shell } = getShellConfig();
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe("sh");
|
||||
expect(args).toEqual(["-c"]);
|
||||
});
|
||||
|
||||
it("falls back to sh on PATH when SHELL is /usr/bin/false", () => {
|
||||
@@ -93,15 +114,26 @@ describe("getShellConfig", () => {
|
||||
const binDir = createTempCommandDir(tempDirs, [{ name: "sh" }]);
|
||||
process.env.SHELL = "/sbin/nologin";
|
||||
process.env.PATH = binDir;
|
||||
const { shell } = getShellConfig();
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe(path.join(binDir, "sh"));
|
||||
expect(args).toEqual(["-c"]);
|
||||
});
|
||||
|
||||
it("falls back to startup-suppressed bash on PATH when SHELL is a placeholder", () => {
|
||||
const binDir = createTempCommandDir(tempDirs, [{ name: "bash" }]);
|
||||
process.env.SHELL = "/usr/bin/false";
|
||||
process.env.PATH = binDir;
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe(path.join(binDir, "bash"));
|
||||
expect(args).toEqual(["--noprofile", "--norc", "-c"]);
|
||||
});
|
||||
|
||||
it("falls back to bare sh when SHELL is a placeholder and no sh is on PATH", () => {
|
||||
process.env.SHELL = "/usr/bin/false";
|
||||
process.env.PATH = "";
|
||||
const { shell } = getShellConfig();
|
||||
const { shell, args } = getShellConfig();
|
||||
expect(shell).toBe("sh");
|
||||
expect(args).toEqual(["-c"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,19 @@ function isNonInteractiveShell(shellPath: string): boolean {
|
||||
return NON_INTERACTIVE_SHELLS.has(path.basename(shellPath));
|
||||
}
|
||||
|
||||
function getPosixShellArgs(shellPath: string): string[] {
|
||||
switch (path.basename(shellPath)) {
|
||||
case "bash":
|
||||
return ["--noprofile", "--norc", "-c"];
|
||||
case "zsh":
|
||||
return ["-f", "-c"];
|
||||
case "fish":
|
||||
return ["--no-config", "-c"];
|
||||
default:
|
||||
return ["-c"];
|
||||
}
|
||||
}
|
||||
|
||||
export function getShellConfig(): { shell: string; args: string[] } {
|
||||
if (process.platform === "win32") {
|
||||
// Use PowerShell instead of cmd.exe on Windows.
|
||||
@@ -71,20 +84,20 @@ export function getShellConfig(): { shell: string; args: string[] } {
|
||||
if (shellName === "fish") {
|
||||
const bash = resolveShellFromPath("bash");
|
||||
if (bash) {
|
||||
return { shell: bash, args: ["-c"] };
|
||||
return { shell: bash, args: getPosixShellArgs(bash) };
|
||||
}
|
||||
const sh = resolveShellFromPath("sh");
|
||||
if (sh) {
|
||||
return { shell: sh, args: ["-c"] };
|
||||
return { shell: sh, args: getPosixShellArgs(sh) };
|
||||
}
|
||||
}
|
||||
if (envShell) {
|
||||
return { shell: envShell, args: ["-c"] };
|
||||
return { shell: envShell, args: getPosixShellArgs(envShell) };
|
||||
}
|
||||
// Placeholder SHELL (or unset): prefer a resolved sh/bash on PATH so we do not
|
||||
// re-invoke the placeholder and get a spurious exitCode=1.
|
||||
const sh = resolveShellFromPath("sh") ?? resolveShellFromPath("bash");
|
||||
return { shell: sh ?? "sh", args: ["-c"] };
|
||||
const shell = resolveShellFromPath("sh") ?? resolveShellFromPath("bash") ?? "sh";
|
||||
return { shell, args: getPosixShellArgs(shell) };
|
||||
}
|
||||
|
||||
export function resolveShellFromPath(name: string): string | undefined {
|
||||
|
||||
Reference in New Issue
Block a user