import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ExecApprovalsResolved } from "../infra/exec-approvals.js"; import { captureEnv } from "../test-utils/env.js"; import { sanitizeBinaryOutput } from "./shell-utils.js"; const isWin = process.platform === "win32"; vi.mock("../infra/shell-env.js", async (importOriginal) => { const mod = await importOriginal(); return { ...mod, getShellPathFromLoginShell: vi.fn(() => "/custom/bin:/opt/bin"), resolveShellEnvFallbackTimeoutMs: vi.fn(() => 1234), }; }); vi.mock("../infra/exec-approvals.js", async (importOriginal) => { const mod = await importOriginal(); const approvals: ExecApprovalsResolved = { path: "/tmp/exec-approvals.json", socketPath: "/tmp/exec-approvals.sock", token: "token", defaults: { security: "full", ask: "off", askFallback: "full", autoAllowSkills: false, }, agent: { security: "full", ask: "off", askFallback: "full", autoAllowSkills: false, }, allowlist: [], file: { version: 1, socket: { path: "/tmp/exec-approvals.sock", token: "token" }, defaults: { security: "full", ask: "off", askFallback: "full", autoAllowSkills: false, }, agents: {}, }, }; return { ...mod, resolveExecApprovals: () => approvals }; }); const normalizeText = (value?: string) => sanitizeBinaryOutput(value ?? "") .replace(/\r\n/g, "\n") .replace(/\r/g, "\n") .trim(); const normalizePathEntries = (value?: string) => normalizeText(value) .split(/[:\s]+/) .map((entry) => entry.trim()) .filter(Boolean); describe("exec PATH login shell merge", () => { let envSnapshot: ReturnType; beforeEach(() => { envSnapshot = captureEnv(["PATH"]); }); afterEach(() => { envSnapshot.restore(); }); it("merges login-shell PATH for host=gateway", async () => { if (isWin) { return; } process.env.PATH = "/usr/bin"; const { createExecTool } = await import("./bash-tools.exec.js"); const { getShellPathFromLoginShell } = await import("../infra/shell-env.js"); const shellPathMock = vi.mocked(getShellPathFromLoginShell); shellPathMock.mockClear(); shellPathMock.mockReturnValue("/custom/bin:/opt/bin"); const tool = createExecTool({ host: "gateway", security: "full", ask: "off" }); const result = await tool.execute("call1", { command: "echo $PATH" }); const entries = normalizePathEntries(result.content.find((c) => c.type === "text")?.text); expect(entries).toEqual(["/custom/bin", "/opt/bin", "/usr/bin"]); expect(shellPathMock).toHaveBeenCalledTimes(1); }); it("throws security violation when env.PATH is provided", async () => { if (isWin) { return; } process.env.PATH = "/usr/bin"; const { createExecTool } = await import("./bash-tools.exec.js"); const { getShellPathFromLoginShell } = await import("../infra/shell-env.js"); const shellPathMock = vi.mocked(getShellPathFromLoginShell); shellPathMock.mockClear(); const tool = createExecTool({ host: "gateway", security: "full", ask: "off" }); await expect( tool.execute("call1", { command: "echo $PATH", env: { PATH: "/explicit/bin" }, }), ).rejects.toThrow(/Security Violation: Custom 'PATH' variable is forbidden/); expect(shellPathMock).not.toHaveBeenCalled(); }); }); describe("exec host env validation", () => { it("blocks LD_/DYLD_ env vars on host execution", async () => { const { createExecTool } = await import("./bash-tools.exec.js"); const tool = createExecTool({ host: "gateway", security: "full", ask: "off" }); await expect( tool.execute("call1", { command: "echo ok", env: { LD_DEBUG: "1" }, }), ).rejects.toThrow(/Security Violation: Environment variable 'LD_DEBUG' is forbidden/); }); it("defaults to gateway when sandbox runtime is unavailable", async () => { const { createExecTool } = await import("./bash-tools.exec.js"); const tool = createExecTool({ security: "full", ask: "off" }); const err = await tool .execute("call1", { command: "echo ok", host: "sandbox", }) .then(() => null) .catch((error: unknown) => (error instanceof Error ? error : new Error(String(error)))); expect(err).toBeTruthy(); expect(err?.message).toMatch(/exec host not allowed/); expect(err?.message).toMatch(/tools\.exec\.host=gateway/); }); it("fails closed when sandbox host is explicitly configured without sandbox runtime", async () => { const { createExecTool } = await import("./bash-tools.exec.js"); const tool = createExecTool({ host: "sandbox", security: "full", ask: "off" }); await expect( tool.execute("call1", { command: "echo ok", }), ).rejects.toThrow(/sandbox runtime is unavailable/); }); });