Files
curriculum-project-hub/hub/test/unit/runner.test.ts
T
hongjr03 ecbc2c8666 fix(hub): 对齐 ADR-0018 agent 执行面边界
runner.ts: 启用 SDK 内置沙箱(bubblewrap/seatbelt),写入限制在 workspace,
denyRead 敏感路径,failIfUnavailable 硬拒非沙箱运行。bypassPermissions 保留
(headless 无交互),沙箱是硬边界而非权限提示层。

install_service.sh: 默认 service user 从 root 改为 cph-hub;env 模板修正
OPENROUTER_API_KEY → ANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY
(与 server.ts 实际读取一致);补 __BASE_DIR__ sed 替换。

cph-hub.service: AssertPathExists=/usr/bin/bwrap 强制沙箱依赖;NoNewPrivileges。
不加 ProtectSystem/ProtectHome(会破坏 cph render-cache 与 bwrap user-namespace)。

trigger.ts: 权限闸门去掉 if (senderOpenId !== '') 短路——缺 open_id 一律
fail-closed 拒绝,不再静默跳过;role gate 同步去掉冗余守卫(已由上游保证)。
顺手把 JSON.parse(msg.content) 的 inline cast 换成 Zod schema parse
(FileMessageContentSchema / TextMessageContentSchema)。

ADR-0018: 状态改为 Accepted+implemented,机制段写定 SDK 沙箱,网络决定为开放,
Open Questions 更新。
2026-07-08 13:11:14 +08:00

94 lines
2.6 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
import { runAgent } from "../../src/agent/runner.js";
const queryMock = vi.hoisted(() => vi.fn());
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
query: queryMock,
}));
const stubPrisma = {
projectAgentLock: { update: async () => ({}) },
} as unknown as import("@prisma/client").PrismaClient;
function assistantMessage(text: string) {
return {
type: "assistant",
message: {
content: [{ type: "text", text }],
usage: { input_tokens: 3, output_tokens: 5 },
},
};
}
function resultMessage(sessionId: string) {
return {
type: "result",
subtype: "success",
session_id: sessionId,
};
}
function messages(...items: unknown[]) {
return (async function* () {
for (const item of items) yield item;
})();
}
describe("runAgent", () => {
beforeEach(() => {
queryMock.mockReset();
});
it("passes the previous Claude SDK session id through resume", async () => {
queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-new")));
const result = await runAgent({
prompt: "再发一次",
model: "anthropic/claude-sonnet-5",
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
systemPrompt: undefined,
resumeSessionId: "sdk-session-old",
runId: "run-1",
sessionId: "hub-session-1",
prisma: stubPrisma,
});
expect(queryMock).toHaveBeenCalledWith({
prompt: "再发一次",
options: expect.objectContaining({
cwd: "/tmp/ws",
model: "anthropic/claude-sonnet-5",
resume: "sdk-session-old",
// ADR-0018: agent execution surface bounded by workspace.
permissionMode: "bypassPermissions",
allowDangerouslySkipPermissions: true,
sandbox: expect.objectContaining({
enabled: true,
failIfUnavailable: true,
filesystem: expect.objectContaining({
allowWrite: ["/tmp/ws"],
}),
}),
}),
});
});
it("does not send resume for a fresh Hub session", async () => {
queryMock.mockReturnValue(messages(assistantMessage("fresh"), resultMessage("sdk-session-1")));
await runAgent({
prompt: "你好",
model: undefined,
project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" },
systemPrompt: undefined,
runId: "run-1",
sessionId: "hub-session-1",
prisma: stubPrisma,
});
const call = queryMock.mock.calls[0]?.[0] as { options?: Record<string, unknown> } | undefined;
expect(call?.options).not.toHaveProperty("resume");
});
});