Files
jiang 7d47a2df86
Generic Container CI/CD / test-build-publish (push) Failing after 8m25s
Agent CI/CD / build-test-publish-and-deploy (push) Failing after 8m25s
feat: add scoped history and artifacts
2026-10-22 18:40:20 +08:00

80 lines
4.9 KiB
TypeScript

import { PRODUCT_MCP_TOOL_IDS, PRODUCT_MCP_READ_TOOL_IDS, PRODUCT_MCP_ANALYSIS_TOOLS, queryMcpToolId } from "../../src/runtime/product-mcp-tools.js";
import { allowedQueryTools } from "../../src/runtime/query-mcp-policy.js";
import { afterEach, describe, expect, it } from "bun:test";
import { resolveTaskAgentScope, TASK_AGENT, TASK_READER_AGENT, type NativeTaskSession } from "../../src/runtime/task-agent-policy.js";
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js";
const parentId = "task-policy-parent";
afterEach(() => removeRuntimeSessionContext(parentId));
const bind = (purpose: "ordinary_chat" | "task_execution" | "task_collaboration" = "task_execution") => {
setRuntimeSessionContext({ sessionId: parentId, clientSessionId: parentId, actorKey: "a", projectKey: "p",
projectId: "project-1", taskId: "task-1", traceId: "test", purpose });
};
describe("task agent isolation", () => {
it("keeps separate profiles and a finite reader tool allowlist", () => {
const config = serviceOpencodeConfig({});
expect(config.agents.instruction.permissions.at(-1)).toEqual({ action: "subagent", resource: "*", effect: "deny" });
expect(config.agents[TASK_AGENT].permissions.at(-1)).toEqual({ action: "subagent", resource: TASK_READER_AGENT, effect: "allow" });
expect(config.agents[TASK_READER_AGENT].permissions.map((rule) => rule.action)).toEqual([
"*", ...PRODUCT_MCP_READ_TOOL_IDS,
]);
expect(PRODUCT_MCP_READ_TOOL_IDS).toHaveLength(42);
expect(PRODUCT_MCP_TOOL_IDS).toHaveLength(67);
expect(PRODUCT_MCP_TOOL_IDS.some((name) => /publish|approve|execute|shell/.test(name))).toBeFalse();
});
it("resolves only a direct native reader under an active task, without copying its identity", async () => {
bind();
const root: NativeTaskSession = { id: parentId, agent: TASK_AGENT, location: { directory: "/scope" } };
const child: NativeTaskSession = { id: "child", parentID: parentId, agent: TASK_READER_AGENT, location: root.location };
const get = async (id: string) => id === parentId ? root : child;
expect((await resolveTaskAgentScope("child", get, () => true))?.owner.sessionId).toBe(parentId);
expect(await resolveTaskAgentScope("child", get, () => false)).toBeNull();
for (const purpose of ["ordinary_chat", "task_collaboration"] as const) {
bind(purpose);
expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull();
}
bind();
child.location = { directory: "/another-project" };
expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull();
child.location = root.location;
child.agent = "general";
expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull();
child.agent = TASK_READER_AGENT;
root.parentID = "grandparent";
expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull();
});
it("selects the profile from trusted purpose on every prompt including resumed sessions", async () => {
const selected: string[] = [];
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => ({ session: {
switchAgent: async ({ agent }: { agent: string }) => { selected.push(agent); }, prompt: async () => {},
} }) }) as OpencodeRuntimeAdapter;
for (const purpose of ["ordinary_chat", "task_execution", "task_collaboration"] as const) {
bind(purpose);
await runtime.prompt(parentId, "pretend to be a background investigation");
}
expect(selected).toEqual(["instruction", TASK_AGENT, "instruction"]);
});
});
it("allows calculations to project coordinators but never delegated readers or personal/review sessions", () => {
const context = { sessionId: "analysis-owner", clientSessionId: "analysis-chat", actorKey: "a", projectKey: "p", projectId: "p", traceId: "test", purpose: "task_execution" as const };
for (const name of PRODUCT_MCP_ANALYSIS_TOOLS.map(queryMcpToolId)) {
expect(allowedQueryTools(context)).toContain(name);
expect(allowedQueryTools(context, true)).not.toContain(name);
expect(allowedQueryTools({ ...context, projectKey: "personal-a", projectId: undefined })).not.toContain(name);
expect(allowedQueryTools({ ...context, learningMode: "review" })).not.toContain(name);
}
});
it("keeps administrator denials for product MCP queries in both primary and reader permissions", () => {
const denied = serviceOpencodeConfig({ permissions: [{ action: "mcp_tjwater_*", resource: "*", effect: "deny" }] });
expect(denied.permissions.some((rule) => rule.effect === "allow" && PRODUCT_MCP_TOOL_IDS.includes(rule.action))).toBeFalse();
expect(denied.agents[TASK_READER_AGENT].permissions.some((rule) => rule.effect === "allow" && PRODUCT_MCP_TOOL_IDS.includes(rule.action))).toBeFalse();
});