fix: confine Agent and MCP data access

This commit is contained in:
2026-07-10 16:38:08 +08:00
parent 73fa28a178
commit f4968d6657
25 changed files with 1521 additions and 221 deletions
+59 -17
View File
@@ -1,4 +1,7 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runAgent } from "../../src/agent/runner.js";
const queryMock = vi.hoisted(() => vi.fn());
@@ -49,8 +52,30 @@ function abortableMessages(ac: AbortController, ...items: unknown[]) {
}
describe("runAgent", () => {
beforeEach(() => {
let root: string;
let workspaceRoot: string;
let workspace: string;
let previousSecrets: Record<string, string | undefined>;
beforeEach(async () => {
queryMock.mockReset();
root = await mkdtemp(join(tmpdir(), "hub-runner-"));
workspaceRoot = join(root, "workspaces");
workspace = join(workspaceRoot, "org", "project");
await mkdir(workspace, { recursive: true });
workspaceRoot = await realpath(workspaceRoot);
workspace = await realpath(workspace);
previousSecrets = Object.fromEntries(
["DATABASE_URL", "FEISHU_APP_SECRET", "HUB_SESSION_SECRET"].map((name) => [name, process.env[name]]),
);
});
afterEach(async () => {
for (const [name, value] of Object.entries(previousSecrets)) {
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
await rm(root, { recursive: true, force: true });
});
it("passes the previous Claude SDK session id through resume", async () => {
@@ -59,7 +84,7 @@ describe("runAgent", () => {
const result = await runAgent({
prompt: "再发一次",
model: "anthropic/claude-sonnet-5",
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
systemPrompt: undefined,
resumeSessionId: "sdk-session-old",
runId: "run-1",
@@ -70,17 +95,22 @@ describe("runAgent", () => {
expect(queryMock).toHaveBeenCalledWith({
prompt: "再发一次",
options: expect.objectContaining({
cwd: "/tmp/ws",
cwd: workspace,
model: "anthropic/claude-sonnet-5",
resume: "sdk-session-old",
// ADR-0018: agent execution surface bounded by workspace.
permissionMode: "bypassPermissions",
allowDangerouslySkipPermissions: true,
settingSources: [],
strictMcpConfig: true,
sandbox: expect.objectContaining({
enabled: true,
failIfUnavailable: true,
allowUnsandboxedCommands: false,
filesystem: expect.objectContaining({
allowWrite: ["/tmp/ws"],
allowWrite: [workspace],
denyRead: ["/"],
allowRead: expect.arrayContaining([workspace]),
}),
}),
}),
@@ -93,7 +123,7 @@ describe("runAgent", () => {
await runAgent({
prompt: "你好",
model: undefined,
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
systemPrompt: undefined,
runId: "run-1",
sessionId: "hub-session-1",
@@ -110,7 +140,7 @@ describe("runAgent", () => {
await runAgent({
prompt: "审校一下",
model: undefined,
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
systemPrompt: undefined,
tools: ["read_file", "cph_check", "send_file"],
runId: "run-1",
@@ -132,7 +162,7 @@ describe("runAgent", () => {
await runAgent({
prompt: "只回答",
model: undefined,
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
systemPrompt: undefined,
tools: [],
runId: "run-1",
@@ -154,7 +184,7 @@ describe("runAgent", () => {
const result = await runAgent({
prompt: "算一下成本",
model: undefined,
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
systemPrompt: undefined,
runId: "run-1",
sessionId: "hub-session-1",
@@ -164,20 +194,21 @@ describe("runAgent", () => {
expect(result.costUsd).toBe(0.0042);
});
it("passes provider env per run without mutating process env", async () => {
delete process.env["CPH_TEST_PROVIDER_ENV_MUTATION"];
it("passes only provider and safe runtime env without mutating process env", async () => {
process.env["DATABASE_URL"] = "postgresql://platform-secret";
process.env["FEISHU_APP_SECRET"] = "feishu-secret";
process.env["HUB_SESSION_SECRET"] = "session-secret";
queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-1")));
await runAgent({
prompt: "你好",
model: undefined,
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
systemPrompt: undefined,
providerEnv: {
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
ANTHROPIC_AUTH_TOKEN: "test-token",
ANTHROPIC_API_KEY: "",
CPH_TEST_PROVIDER_ENV_MUTATION: "per-run",
},
runId: "run-1",
sessionId: "hub-session-1",
@@ -191,11 +222,22 @@ describe("runAgent", () => {
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
ANTHROPIC_AUTH_TOKEN: "test-token",
ANTHROPIC_API_KEY: "",
CPH_TEST_PROVIDER_ENV_MUTATION: "per-run",
},
sandbox: {
credentials: {
envVars: expect.arrayContaining([
{ name: "ANTHROPIC_AUTH_TOKEN", mode: "deny" },
{ name: "ANTHROPIC_API_KEY", mode: "deny" },
]),
},
},
},
});
expect(process.env["CPH_TEST_PROVIDER_ENV_MUTATION"]).toBeUndefined();
const env = (call as { options: { env: Record<string, string | undefined> } }).options.env;
expect(env).not.toHaveProperty("DATABASE_URL");
expect(env).not.toHaveProperty("FEISHU_APP_SECRET");
expect(env).not.toHaveProperty("HUB_SESSION_SECRET");
expect(process.env["DATABASE_URL"]).toBe("postgresql://platform-secret");
});
it("persists structured user and assistant messages best-effort", async () => {
@@ -209,7 +251,7 @@ describe("runAgent", () => {
await runAgent({
prompt: "你好",
model: undefined,
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
systemPrompt: undefined,
runId: "run-1",
sessionId: "hub-session-1",
@@ -241,7 +283,7 @@ describe("runAgent", () => {
const runPromise = runAgent({
prompt: "长任务",
model: undefined,
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
systemPrompt: undefined,
runId: "run-abort",
sessionId: "hub-session-abort",