import { mkdtemp, mkdir, readFile, readdir, chmod, lstat, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { Db } from "@paperclipai/db"; import { PAPERCLIP_OPERATIONAL_SKILL_KEY, type PaperclipSkillEntry, } from "@paperclipai/adapter-utils/server-utils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const serviceMocks = vi.hoisted(() => ({ exportFiles: vi.fn(), readCommittedForRuntime: vi.fn(), getEffectiveProfilesForAgent: vi.fn(), githubBotConnectionIdsForRun: vi.fn(), })); vi.mock("../chat-github-tools.js", () => ({ githubBotConnectionIdsForRun: serviceMocks.githubBotConnectionIdsForRun, })); vi.mock("../agent-instructions.js", () => ({ agentInstructionsService: () => ({ exportFiles: serviceMocks.exportFiles }), agentInstructionsBundleMode: (agent: { adapterConfig?: { instructionsBundleMode?: string } }) => agent.adapterConfig?.instructionsBundleMode ?? null, })); vi.mock("../agent-instruction-revisions.js", () => ({ agentInstructionRevisionService: () => ({ readCommittedForRuntime: serviceMocks.readCommittedForRuntime }), })); vi.mock("../tool-access.js", () => ({ toolAccessService: () => ({ getEffectiveProfilesForAgent: serviceMocks.getEffectiveProfilesForAgent, }), })); import { createHash } from "node:crypto"; import { buildNativeRuntimeContext, resolveNativeRuntimeMcpSnapshot } from "./runtime-context.js"; const temporaryRoots: string[] = []; let previousPaperclipHome: string | undefined; let previousInstanceId: string | undefined; async function makeTreeWritable(target: string): Promise { const metadata = await lstat(target).catch(() => null); if (!metadata) return; if (metadata.isDirectory()) { await chmod(target, 0o700); for (const entry of await readdir(target)) { await makeTreeWritable(path.join(target, entry)); } } else { await chmod(target, 0o600); } } beforeEach(async () => { vi.clearAllMocks(); serviceMocks.readCommittedForRuntime.mockResolvedValue(null); serviceMocks.githubBotConnectionIdsForRun.mockResolvedValue(new Set()); previousPaperclipHome = process.env.PAPERCLIP_HOME; previousInstanceId = process.env.PAPERCLIP_INSTANCE_ID; const root = await mkdtemp(path.join(tmpdir(), "paperclip-native-context-")); temporaryRoots.push(root); process.env.PAPERCLIP_HOME = root; process.env.PAPERCLIP_INSTANCE_ID = "runtime_context_test"; serviceMocks.getEffectiveProfilesForAgent.mockResolvedValue({ agentId: "agent-1", profiles: [], entries: [{ effect: "include", connectionId: "connection-1" }], bindings: [], allowedTools: [{ id: "tool-1", connectionId: "connection-1" }], allowedToolNames: ["issues.read"], installedConnections: [{ id: "connection-1", transport: "mcp_remote", enabled: true, status: "active", healthStatus: "healthy", }], }); }); afterEach(async () => { if (previousPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = previousPaperclipHome; if (previousInstanceId === undefined) delete process.env.PAPERCLIP_INSTANCE_ID; else process.env.PAPERCLIP_INSTANCE_ID = previousInstanceId; // A rejected Promise.all does not cancel the other materializers. Let their // bounded local writes settle before removing the read-only asset tree. await new Promise((resolve) => setTimeout(resolve, 25)); while (temporaryRoots.length) { const root = temporaryRoots.pop()!; await makeTreeWritable(root); await rm(root, { recursive: true, force: true }); } }); describe("buildNativeRuntimeContext", () => { it("uses committed instructions when the disk projection is stale", async () => { serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "Old disk projection\n", "reference.md": "Sibling reference\n" }, }); serviceMocks.readCommittedForRuntime.mockResolvedValue({ revision: { entryFile: "AGENTS.md" }, content: "Saved canonical instructions\n", }); const context = await buildNativeRuntimeContext({ db: {} as Db, agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: { instructionsBundleMode: "managed" } }, runId: "run-1", runtimeConfig: {}, runtimeSkillEntries: [], }); expect(await readFile(path.join(context.instructions.bundle.rootPath, context.instructions.entryPath), "utf8")) .toBe("Saved canonical instructions\n"); expect(await readFile(path.join(context.instructions.bundle.rootPath, "reference.md"), "utf8")) .toBe("Sibling reference\n"); expect(serviceMocks.readCommittedForRuntime).toHaveBeenCalledWith({ companyId: "company-1", agentId: "agent-1" }); }); it("does not substitute canonical managed instructions into an external bundle", async () => { serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "External instructions\n" } }); const context = await buildNativeRuntimeContext({ db: {} as Db, agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterConfig: { instructionsBundleMode: "external" } }, runId: "run-1", runtimeConfig: {}, runtimeSkillEntries: [], }); expect(await readFile(path.join(context.instructions.bundle.rootPath, context.instructions.entryPath), "utf8")) .toBe("External instructions\n"); expect(serviceMocks.readCommittedForRuntime).not.toHaveBeenCalled(); }); it("keeps the pinned prompt immutable while exposing the registered editable instruction copy", async () => { serviceMocks.exportFiles.mockResolvedValue({ entryFile: "instructions/CHARter.md", files: { "instructions/CHARter.md": "Original instructions.\n" }, }); const workingRoot = path.join(temporaryRoots.at(-1)!, "private-run-copy"); await mkdir(path.join(workingRoot, "instructions"), { recursive: true }); await writeFile(path.join(workingRoot, "instructions/CHARter.md"), "Original instructions.\n"); const context = await buildNativeRuntimeContext({ db: {} as Db, agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: {} }, runId: "run-1", runtimeConfig: {}, runtimeSkillEntries: [], instructionWorkingCopy: { rootPath: workingRoot, entryPath: "instructions/CHARter.md" }, }); expect(context.instructions.workingCopy).toEqual({ rootPath: workingRoot, entryPath: "instructions/CHARter.md" }); await writeFile(path.join(workingRoot, "instructions/CHARter.md"), "Persist this next time.\n"); expect(await readFile(path.join(context.instructions.bundle.rootPath, context.instructions.entryPath), "utf8")) .toBe("Original instructions.\n"); }); it.each(["disabled", "degraded"] as const)( "omits an unavailable native MCP connection when it is %s without aborting runtime context creation", async (unavailableState) => { serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "Continue work without unavailable apps.\n" }, }); serviceMocks.getEffectiveProfilesForAgent.mockResolvedValue({ agentId: "agent-1", profiles: [], entries: [{ effect: "include", connectionId: "connection-1" }], bindings: [], allowedTools: [{ id: "tool-1", connectionId: "connection-1" }], allowedToolNames: ["issues.read"], installedConnections: [{ id: "connection-1", transport: "mcp_remote", enabled: unavailableState !== "disabled", status: unavailableState === "disabled" ? "disabled" : "active", healthStatus: unavailableState === "degraded" ? "degraded" : "healthy", }], }); const context = await buildNativeRuntimeContext({ db: {} as Db, agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: {}, }, runId: "run-1", runtimeConfig: {}, runtimeSkillEntries: [], }); expect(context.mcp.bindingId).toBeNull(); expect(context.mcp.assignmentSetId).toMatch(/^sha256:[a-f0-9]{64}$/); }, ); it("keeps healthy native MCP connections when another assigned connection is unavailable", async () => { serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "Continue work with the apps that are available.\n" }, }); serviceMocks.getEffectiveProfilesForAgent.mockResolvedValue({ agentId: "agent-1", profiles: [], entries: [ { effect: "include", connectionId: "connection-expired" }, { effect: "include", connectionId: "connection-healthy" }, ], bindings: [], allowedTools: [ { id: "tool-expired", connectionId: "connection-expired" }, { id: "tool-healthy", connectionId: "connection-healthy" }, ], allowedToolNames: ["expired.read", "healthy.read"], installedConnections: [ { id: "connection-expired", transport: "mcp_remote", enabled: true, status: "active", healthStatus: "degraded", }, { id: "connection-healthy", transport: "mcp_remote", enabled: true, status: "active", healthStatus: "healthy", }, ], }); const context = await buildNativeRuntimeContext({ db: {} as Db, agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: {}, }, runId: "run-1", runtimeConfig: {}, runtimeSkillEntries: [], }); expect(context.mcp.bindingId).toBe("native-mcp:run-1"); expect(context.mcp.assignmentSetId).toMatch(/^sha256:[a-f0-9]{64}$/); }); it("materializes every instruction and selected-skill file as immutable, content-addressed context", async () => { serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "Follow the agent instructions.\n", "references/policy.md": "Company policy sibling.\n", }, }); const sourceRoot = await mkdtemp(path.join(tmpdir(), "paperclip-native-skill-")); temporaryRoots.push(sourceRoot); const selectedRoot = path.join(sourceRoot, "reviewer"); const unselectedRoot = path.join(sourceRoot, "unused"); await mkdir(path.join(selectedRoot, "references"), { recursive: true }); await mkdir(unselectedRoot, { recursive: true }); await writeFile(path.join(selectedRoot, "SKILL.md"), "# Reviewer\nUse the checklist.\n"); await writeFile(path.join(selectedRoot, "references", "checklist.md"), "- Verify tests\n"); await writeFile(path.join(unselectedRoot, "SKILL.md"), "# Not selected\n"); const entries: PaperclipSkillEntry[] = [ { key: "company-1/reviewer", runtimeName: "reviewer", source: selectedRoot, versionId: "version-1" }, { key: "company-1/unused", runtimeName: "unused", source: unselectedRoot, versionId: "version-2" }, ]; const input = { db: {} as Db, agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: {}, }, runId: "run-1", runtimeConfig: { paperclipSkillSync: { desiredSkills: ["company-1/reviewer"] } }, runtimeSkillEntries: entries, }; const context = await buildNativeRuntimeContext(input); const repeated = await buildNativeRuntimeContext(input); expect(serviceMocks.exportFiles).toHaveBeenCalledWith(input.agent, { rejectSymlinks: true }); expect(serviceMocks.getEffectiveProfilesForAgent).toHaveBeenCalledWith("company-1", "agent-1"); expect(context.instructions.entryPath).toBe("AGENTS.md"); expect(await readFile(path.join(context.instructions.bundle.rootPath, "AGENTS.md"), "utf8")) .toBe("Follow the agent instructions.\n"); expect(await readFile(path.join(context.instructions.bundle.rootPath, "references", "policy.md"), "utf8")) .toBe("Company policy sibling.\n"); const unselected = await buildNativeRuntimeContext({ ...input, runtimeConfig: {} }); expect(unselected.skills).toEqual([]); expect(context.skills).toHaveLength(1); expect(context.skills[0]).toMatchObject({ key: "company-1/reviewer", runtimeName: "reviewer", versionId: "version-1", }); expect(await readFile(path.join(context.skills[0]!.bundle.rootPath, "SKILL.md"), "utf8")) .toContain("Use the checklist"); expect(await readFile(path.join(context.skills[0]!.bundle.rootPath, "references", "checklist.md"), "utf8")) .toBe("- Verify tests\n"); expect((await stat(context.instructions.bundle.rootPath)).mode & 0o222).toBe(0); expect((await stat(path.join(context.instructions.bundle.rootPath, "AGENTS.md"))).mode & 0o222).toBe(0); expect((await stat(context.skills[0]!.bundle.rootPath)).mode & 0o222).toBe(0); expect(context.mcp).toMatchObject({ bindingId: "native-mcp:run-1", assignmentSetId: expect.stringMatching(/^sha256:[a-f0-9]{64}$/), digest: expect.stringMatching(/^[a-f0-9]{64}$/), }); expect(repeated.aggregateDigest).toBe(context.aggregateDigest); expect(repeated.instructions.bundle.rootPath).toBe(context.instructions.bundle.rootPath); expect(repeated.skills[0]!.bundle.rootPath).toBe(context.skills[0]!.bundle.rootPath); }); it("fails closed for a missing assigned skill and omits a stale legacy operational skill", async () => { serviceMocks.exportFiles.mockResolvedValue({ entryFile: "AGENTS.md", files: { "AGENTS.md": "Test\n" } }); const base = { db: {} as Db, agent: { id: "agent-1", companyId: "company-1", name: "Reviewer", adapterType: "paperclip_runner", adapterConfig: {}, }, runId: "run-1", }; await expect(buildNativeRuntimeContext({ ...base, runtimeConfig: { paperclipSkillSync: { desiredSkills: ["company-1/missing"] } }, runtimeSkillEntries: [{ key: "company-1/missing", runtimeName: "missing", source: "/does/not/exist", sourceStatus: "missing", missingDetail: "assigned skill checkout is unavailable", }], })).rejects.toThrow("assigned skill checkout is unavailable"); const supportedRoot = await mkdtemp(path.join(tmpdir(), "paperclip-native-supported-skill-")); temporaryRoots.push(supportedRoot); await writeFile(path.join(supportedRoot, "SKILL.md"), "# Supported\n"); const context = await buildNativeRuntimeContext({ ...base, runtimeConfig: { paperclipSkillSync: { desiredSkills: [PAPERCLIP_OPERATIONAL_SKILL_KEY, "company-1/supported"], }, }, runtimeSkillEntries: [ { key: PAPERCLIP_OPERATIONAL_SKILL_KEY, runtimeName: "paperclip", source: "/unused", }, { key: "company-1/supported", runtimeName: "supported", source: supportedRoot, }, ], }); expect(context.skills.map((skill) => skill.key)).toEqual(["company-1/supported"]); }); }); it("pins both permitted GitHub tool catalogs even when another user’s health probe reports missing credentials", async () => { const connections = ["github-A", "github-B"].map((id) => ({ id, status: "active", enabled: true, healthStatus: "missing_secret", transport: "mcp_remote", config: { sourceTemplateKey: "github" }, transportConfig: {}, })); serviceMocks.getEffectiveProfilesForAgent.mockResolvedValue({ entries: connections.map((connection) => ({ effect: "include", connectionId: connection.id })), installedConnections: connections, allowedTools: connections.map((connection) => ({ id: `${connection.id}-get_me`, connectionId: connection.id })), }); const limit = vi.fn(async () => [{ responsibleUserId: "A", activeIdentityContextId: "context-A" }]); const db = { select: () => ({ from: () => ({ where: () => ({ limit }) }) }) } as unknown as Db; const snapshot = await resolveNativeRuntimeMcpSnapshot({ db, agent: { id: "agent-1", companyId: "company-1" }, runId: "run-1" }); const expected = createHash("sha256").update(JSON.stringify({ version: 1, agentId: "agent-1", connections: ["github-A", "github-B"], tools: ["github-A-get_me", "github-B-get_me"], })).digest("hex"); expect(snapshot.digest).toBe(expected); expect(snapshot.bindingId).toBe("native-mcp:run-1"); expect(limit).toHaveBeenCalledOnce(); }); it("pins channel tools only when the current task run is bound to that bot", async () => { serviceMocks.getEffectiveProfilesForAgent.mockResolvedValue({ entries: [{ effect: "include", connectionId: "bot-connection" }], installedConnections: [{ id: "bot-connection", status: "active", enabled: true, healthStatus: "healthy", transport: "chat_sdk", config: { sourceTemplateKey: "github-chat" }, transportConfig: {}, }], allowedTools: [{ id: "bot-read-pr", connectionId: "bot-connection" }], }); const input = { db: {} as Db, agent: { id: "agent-1", companyId: "company-1" }, runId: "run-1" }; expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBeNull(); serviceMocks.githubBotConnectionIdsForRun.mockResolvedValue(new Set(["bot-connection"])); expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBe("native-mcp:run-1"); expect(serviceMocks.githubBotConnectionIdsForRun).toHaveBeenLastCalledWith(input.db, "company-1", "agent-1", "run-1"); });