mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
157 lines
5.8 KiB
TypeScript
157 lines
5.8 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const sharedClientMocks = vi.hoisted(() => ({
|
|
createIsolatedCodexAppServerClient: vi.fn(),
|
|
getSharedCodexAppServerClient: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./shared-client.js", () => ({
|
|
...sharedClientMocks,
|
|
getLeasedSharedCodexAppServerClient: sharedClientMocks.getSharedCodexAppServerClient,
|
|
releaseLeasedSharedCodexAppServerClient: vi.fn(),
|
|
}));
|
|
|
|
const { requestCodexAppServerJson } = await import("./request.js");
|
|
|
|
describe("requestCodexAppServerJson sandbox guard", () => {
|
|
beforeEach(() => {
|
|
sharedClientMocks.createIsolatedCodexAppServerClient.mockReset();
|
|
sharedClientMocks.getSharedCodexAppServerClient.mockReset();
|
|
});
|
|
|
|
it("fails closed before raw app-server bypass methods in sandboxed sessions", async () => {
|
|
await expect(
|
|
requestCodexAppServerJson({
|
|
method: "command/exec",
|
|
requestParams: { command: ["sh", "-lc", "id"] },
|
|
config: { agents: { defaults: { sandbox: { mode: "all" } } } },
|
|
sessionKey: "sandboxed-session",
|
|
}),
|
|
).rejects.toThrow(
|
|
"Codex-native app-server method `command/exec` is unavailable because OpenClaw sandboxing is active for this session.",
|
|
);
|
|
|
|
expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed before raw app-server bypass methods when exec host=node is active", async () => {
|
|
for (const method of ["command/exec", "process/spawn"]) {
|
|
await expect(
|
|
requestCodexAppServerJson({
|
|
method,
|
|
requestParams: { command: ["sh", "-lc", "id"] },
|
|
config: { tools: { exec: { host: "node", node: "worker-1" } } },
|
|
sessionKey: "node-session",
|
|
}),
|
|
).rejects.toThrow(
|
|
`Codex-native app-server method \`${method}\` is unavailable because OpenClaw exec host=node is active for this session.`,
|
|
);
|
|
}
|
|
|
|
expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("allows metadata methods in sandboxed sessions", async () => {
|
|
const request = vi.fn(async () => ({ ok: true }));
|
|
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({ request });
|
|
|
|
await expect(
|
|
requestCodexAppServerJson({
|
|
method: "thread/list",
|
|
requestParams: { limit: 10 },
|
|
config: { agents: { defaults: { sandbox: { mode: "all" } } } },
|
|
sessionKey: "sandboxed-session",
|
|
}),
|
|
).resolves.toEqual({ ok: true });
|
|
|
|
expect(request).toHaveBeenCalledWith("thread/list", { limit: 10 }, { timeoutMs: 60_000 });
|
|
});
|
|
|
|
it("fails closed for config-level exec host=node even without a session key", async () => {
|
|
await expect(
|
|
requestCodexAppServerJson({
|
|
method: "command/exec",
|
|
requestParams: { command: ["sh", "-lc", "id"] },
|
|
config: { tools: { exec: { host: "node", node: "worker-1" } } },
|
|
}),
|
|
).rejects.toThrow(
|
|
"Codex-native app-server method `command/exec` is unavailable because OpenClaw exec host=node is active for this session.",
|
|
);
|
|
|
|
expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed for MCP reload when config-level exec host=node is active", async () => {
|
|
await expect(
|
|
requestCodexAppServerJson({
|
|
method: "config/mcpServer/reload",
|
|
requestParams: {},
|
|
config: { tools: { exec: { host: "node", node: "worker-1" } } },
|
|
}),
|
|
).rejects.toThrow(
|
|
"Codex-native app-server method `config/mcpServer/reload` is unavailable because OpenClaw exec host=node is active for this session.",
|
|
);
|
|
|
|
expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("allows metadata methods when exec host=node is active", async () => {
|
|
const request = vi.fn(async () => ({ ok: true }));
|
|
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({ request });
|
|
|
|
await expect(
|
|
requestCodexAppServerJson({
|
|
method: "thread/list",
|
|
requestParams: { limit: 10 },
|
|
config: { tools: { exec: { host: "node", node: "worker-1" } } },
|
|
sessionKey: "node-session",
|
|
}),
|
|
).resolves.toEqual({ ok: true });
|
|
|
|
expect(request).toHaveBeenCalledWith("thread/list", { limit: 10 }, { timeoutMs: 60_000 });
|
|
});
|
|
|
|
it("allows sandbox-pinned thread starts in sandboxed sessions", async () => {
|
|
const request = vi.fn(async () => ({ thread: { id: "thread-1" }, model: "gpt-5.5" }));
|
|
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({ request });
|
|
const params = {
|
|
cwd: "/workspace",
|
|
environments: [{ environmentId: "openclaw-sandbox-abc123", cwd: "/workspace" }],
|
|
};
|
|
|
|
await expect(
|
|
requestCodexAppServerJson({
|
|
method: "thread/start",
|
|
requestParams: params,
|
|
config: { agents: { defaults: { sandbox: { mode: "all" } } } },
|
|
sessionKey: "sandboxed-session",
|
|
}),
|
|
).resolves.toEqual({ thread: { id: "thread-1" }, model: "gpt-5.5" });
|
|
|
|
expect(request).toHaveBeenCalledWith("thread/start", params, { timeoutMs: 60_000 });
|
|
});
|
|
|
|
it("blocks thread starts with sandbox environments when exec host=node is active", async () => {
|
|
const params = {
|
|
cwd: "/workspace",
|
|
environments: [{ environmentId: "openclaw-sandbox-abc123", cwd: "/workspace" }],
|
|
};
|
|
|
|
await expect(
|
|
requestCodexAppServerJson({
|
|
method: "thread/start",
|
|
requestParams: params,
|
|
config: {
|
|
agents: { defaults: { sandbox: { mode: "all" } } },
|
|
tools: { exec: { host: "node", node: "worker-1" } },
|
|
},
|
|
sessionKey: "node-session",
|
|
}),
|
|
).rejects.toThrow(
|
|
"Codex-native app-server method `thread/start` is unavailable because OpenClaw exec host=node is active for this session.",
|
|
);
|
|
|
|
expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled();
|
|
});
|
|
});
|