Exec/ACP: inject OPENCLAW_SHELL into child shell env (#31271)

* exec: mark runtime shell context in exec env

* tests(exec): cover OPENCLAW_SHELL in gateway exec

* tests(exec): cover OPENCLAW_SHELL in pty mode

* acpx: mark runtime shell context for spawned process

* tests(acpx): log OPENCLAW_SHELL in runtime fixture

* tests(acpx): assert OPENCLAW_SHELL in runtime prompt

* docs(env): document OPENCLAW_SHELL runtime markers

* docs(exec): describe OPENCLAW_SHELL exec marker

* docs(acp): document OPENCLAW_SHELL acp marker

* docs(gateway): note OPENCLAW_SHELL for background exec

* tui: tag local shell runs with OPENCLAW_SHELL

* tests(tui): assert OPENCLAW_SHELL in local shell runner

* acp client: tag spawned bridge env with OPENCLAW_SHELL

* tests(acp): cover acp client OPENCLAW_SHELL env helper

* docs(env): include acp-client and tui-local shell markers

* docs(acp): document acp-client OPENCLAW_SHELL marker

* docs(tui): document tui-local OPENCLAW_SHELL marker

* exec: keep shell runtime env string-only for docker args

* changelog: note OPENCLAW_SHELL runtime markers
This commit is contained in:
Vincent Koc
2026-03-01 20:31:06 -08:00
committed by GitHub
parent aeb817353f
commit b7615e0ce3
16 changed files with 145 additions and 7 deletions

View File

@@ -291,6 +291,10 @@ export async function runExecProcess(opts: {
const sessionId = createSessionSlug();
const execCommand = opts.execCommand ?? opts.command;
const supervisor = getProcessSupervisor();
const shellRuntimeEnv: Record<string, string> = {
...opts.env,
OPENCLAW_SHELL: "exec",
};
const session: ProcessSession = {
id: sessionId,
@@ -385,7 +389,7 @@ export async function runExecProcess(opts: {
containerName: opts.sandbox.containerName,
command: execCommand,
workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
env: opts.env,
env: shellRuntimeEnv,
tty: opts.usePty,
}),
],
@@ -400,14 +404,14 @@ export async function runExecProcess(opts: {
mode: "pty" as const,
ptyCommand: execCommand,
childFallbackArgv: childArgv,
env: opts.env,
env: shellRuntimeEnv,
stdinMode: "pipe-open" as const,
};
}
return {
mode: "child" as const,
argv: childArgv,
env: opts.env,
env: shellRuntimeEnv,
stdinMode: "pipe-closed" as const,
};
})();

View File

@@ -95,6 +95,20 @@ describe("exec PATH login shell merge", () => {
expect(shellPathMock).toHaveBeenCalledTimes(1);
});
it("sets OPENCLAW_SHELL for host=gateway commands", async () => {
if (isWin) {
return;
}
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
const result = await tool.execute("call-openclaw-shell", {
command: 'printf "%s" "${OPENCLAW_SHELL:-}"',
});
const value = normalizeText(result.content.find((c) => c.type === "text")?.text);
expect(value).toBe("exec");
});
it("throws security violation when env.PATH is provided", async () => {
if (isWin) {
return;

View File

@@ -17,3 +17,15 @@ test("exec supports pty output", async () => {
const text = result.content?.find((item) => item.type === "text")?.text ?? "";
expect(text).toContain("ok");
});
test("exec sets OPENCLAW_SHELL in pty mode", async () => {
const tool = createExecTool({ allowBackground: false, security: "full", ask: "off" });
const result = await tool.execute("toolcall-openclaw-shell", {
command: "node -e \"process.stdout.write(process.env.OPENCLAW_SHELL || '')\"",
pty: true,
});
expect(result.details.status).toBe("completed");
const text = result.content?.find((item) => item.type === "text")?.text ?? "";
expect(text).toContain("exec");
});