import type { PrpStructuredRunResult } from "../../vendor/paperclip-runner/index.js"; import { nativeCompletionFeedback } from "./native-completion-feedback.js"; import { createHash, randomUUID } from "node:crypto"; import { spawn } from "node:child_process"; import { once } from "node:events"; import { mkdtemp, link, mkdir, readFile, readdir, rm, symlink, unlink, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { eq, sql } from "drizzle-orm"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { activityLog, agents, assets, companies, createDb, heartbeatRuns, issueAttachments, issueComments, issues, issueWorkProducts, } from "@paperclipai/db"; import { startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js"; import { createLocalDiskStorageProvider } from "../../storage/local-disk-provider.js"; import { createStorageService } from "../../storage/service.js"; import type { StorageService } from "../../storage/types.js"; import { issueService } from "../issues.js"; import { findHeartbeatRunCompletionComment, resolveHeartbeatRunResponse } from "../heartbeat-run-summary.js"; import { renderNativeRunnerStagedAttachmentPrompt, stageNativeRunnerAttachmentBytes, stageNativeRunnerWakeAttachments, } from "./native-runner-file-handoff.js"; import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js"; describe("native runner file handoff", () => { let temporary: Awaited< ReturnType > | null = null; let db: ReturnType; let temporaryRoot: string; let workspaceRoot: string; let storageRoot: string; const companyId = "00000000-0000-4000-8000-000000009101"; const agentId = "00000000-0000-4000-8000-000000009102"; const issueId = "00000000-0000-4000-8000-000000009103"; const runId = "00000000-0000-4000-8000-000000009104"; beforeAll(async () => { temporary = await startEmbeddedPostgresTestDatabase( "native-runner-file-handoff-", ); db = createDb(temporary.connectionString); temporaryRoot = await mkdtemp( path.join(tmpdir(), "paperclip-native-file-handoff-"), ); workspaceRoot = path.join(temporaryRoot, "workspace"); storageRoot = path.join(temporaryRoot, "storage"); await mkdir(workspaceRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true }); await db.insert(companies).values({ id: companyId, name: "Native file handoff", issuePrefix: "NFH", issueCounter: 1, }); await db.insert(agents).values({ id: agentId, companyId, name: "Native file agent", adapterType: "paperclip_runner", adapterConfig: { provider: "codex" }, runtimeConfig: {}, status: "active", }); await db.insert(issues).values({ id: issueId, companyId, issueNumber: 1, identifier: "NFH-1", title: "Prepare a requested file", status: "in_progress", workMode: "standard", assigneeAgentId: agentId, }); await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, status: "running", runtimeMode: "native", nativeIssueId: issueId, invocationSource: "assignment", triggerDetail: "system", contextSnapshot: { issueId }, }); await db .update(issues) .set({ executionRunId: runId }) .where(eq(issues.id, issueId)); }); afterAll(async () => { await temporary?.cleanup(); }); function authority( overrides: Partial<{ companyId: string; executionTargetKind: "local" | "remote"; readRemoteTaskFile: (input: { contentRef: string; byteSize: number; sha256: string }) => Promise; readRemoteWorkspaceFile: (input: { contentRef: string; byteSize: number; sha256: string }) => Promise; }> = {}, ) { return new PaperclipRunnerToolAuthority(db, { companyId: overrides.companyId ?? companyId, agentId, issueId, runId, workspaceRoot, executionTargetKind: overrides.executionTargetKind ?? "local", readRemoteWorkspaceFile: overrides.readRemoteWorkspaceFile, readRemoteTaskFile: overrides.readRemoteTaskFile, storage: createStorageService( createLocalDiskStorageProvider(storageRoot), ), }); } function callFor(relativePath: string, body: Buffer, idempotencyKey: string) { return { tool: "register_deliverable", callId: `call-${idempotencyKey}`, arguments: { idempotencyKey, filename: "answer.txt", contentType: "text/plain", byteSize: body.length, sha256: createHash("sha256").update(body).digest("hex"), contentRef: relativePath, title: "Requested answer", }, }; } async function createInboundAttachmentFixture(input: { storage: StorageService; filename: string; body: Buffer; commentBody: string; }) { const stored = await input.storage.putFile({ companyId, namespace: `issues/${issueId}`, originalFilename: input.filename, contentType: "text/plain", body: input.body, }); const comment = await issueService(db).addComment( issueId, input.commentBody, { userId: "inbound-user" }, ); const attachment = await issueService(db).createAttachment({ issueId, issueCommentId: comment.id, provider: stored.provider, objectKey: stored.objectKey, contentType: stored.contentType, byteSize: stored.byteSize, sha256: stored.sha256, originalFilename: stored.originalFilename, createdByUserId: "inbound-user", }); return { attachment, comment, stored }; } function doneReport(refs: string[]): PrpStructuredRunResult { return { schema: "paperclip.run_result.v1", reportedWorkDisposition: "done", summary: "Created the requested checklist.", completionClaim: { contractRevision: "test", objectiveSatisfied: true, criteria: [], remainingWork: [] }, evidence: refs.map((ref) => ({ ref })), verification: [], attentionRequests: [], artifacts: [], }; } async function stageEmptyWake(root: string, boundCompanyId = companyId) { await db.update(heartbeatRuns) .set({ contextSnapshot: { issueId } }) .where(eq(heartbeatRuns.id, runId)); return stageNativeRunnerWakeAttachments({ db, binding: { companyId: boundCompanyId, issueId, runId, agentId, workspaceRoot: root, executionTargetKind: "local", }, }); } it("leaves an empty workspace unchanged for an empty wake and permits later explicit staging", async () => { const root = await mkdtemp(path.join(temporaryRoot, "empty-wake-")); try { const wake = await stageEmptyWake(root); expect(wake.attachments).toEqual([]); await wake.cleanup(); await expect(readdir(root)).resolves.toEqual([]); const explicit = await stageNativeRunnerAttachmentBytes({ workspaceRoot: root, body: Buffer.from("later authorized bytes") }); await expect(readFile(path.join(root, explicit.workspaceRelativePath), "utf8")) .resolves.toBe("later authorized bytes"); await explicit.cleanup(); await expect(readFile(path.join(root, explicit.workspaceRelativePath))) .resolves.toEqual(Buffer.alloc(0)); } finally { await rm(root, { recursive: true, force: true }); } }); it("scrubs inactive staging bytes on an empty wake without creating another directory", async () => { const root = await mkdtemp(path.join(temporaryRoot, "empty-wake-residue-")); try { const previous = await stageNativeRunnerAttachmentBytes({ workspaceRoot: root, body: Buffer.from("previous attachment") }); await previous.cleanup(); const slot = path.join(root, previous.workspaceRelativePath); await writeFile(slot, "stale bytes after an interrupted owner"); const directories = await readdir(path.join(root, ".paperclip-inbound")); const wake = await stageEmptyWake(root); expect(wake.attachments).toEqual([]); await wake.cleanup(); await expect(readFile(slot)).resolves.toEqual(Buffer.alloc(0)); await expect(readdir(path.join(root, ".paperclip-inbound"))).resolves.toEqual(directories); } finally { await rm(root, { recursive: true, force: true }); } }); it.each(["root-file", "root-symlink", "entry-symlink"] as const)( "rejects unsafe %s staging on an empty wake without changing outside bytes", async (kind) => { const root = await mkdtemp(path.join(temporaryRoot, "empty-wake-unsafe-")); const outside = await mkdtemp(path.join(temporaryRoot, "empty-wake-outside-")); try { await writeFile(path.join(outside, "keep.txt"), "outside bytes"); const inbound = path.join(root, ".paperclip-inbound"); if (kind === "root-file") await writeFile(inbound, "not a directory"); else if (kind === "root-symlink") await symlink(outside, inbound); else { await mkdir(inbound); await symlink(outside, path.join(inbound, "untrusted-owner")); } await expect(stageEmptyWake(root)).rejects.toThrow(/staging_(?:path|residue)_denied/); await expect(readFile(path.join(outside, "keep.txt"), "utf8")).resolves.toBe("outside bytes"); } finally { await rm(root, { recursive: true, force: true }); await rm(outside, { recursive: true, force: true }); } }, ); it("still authorizes an empty wake before touching its workspace", async () => { const root = await mkdtemp(path.join(temporaryRoot, "empty-wake-unauthorized-")); try { await expect(stageEmptyWake(root, "00000000-0000-4000-8000-000000009199")) .rejects.toThrow("paperclip_runner_attachment_staging_not_authorized"); await expect(readdir(root)).resolves.toEqual([]); } finally { await rm(root, { recursive: true, force: true }); } }); it("rejects a workspace-only file completion and invented delivery receipts without asking the user to approve completion", async () => { for (const ref of ["launch-checklist.md", "./out/report.pdf", "/workspace/answer.txt", "file:out/report.csv", "deliverable:00000000-0000-4000-8000-000000000001"]) { await expect(nativeCompletionFeedback(db, runId, doneReport([ref]))) .rejects.toThrow(/register_deliverable|registered attachment/); } await expect(nativeCompletionFeedback(db, runId, doneReport([]))) .rejects.toThrow(/requested file.*accessible|register_deliverable/); await expect(nativeCompletionFeedback(db, runId, doneReport(["verification:passed"]))) .rejects.toThrow(/requested file.*accessible|register_deliverable/); await expect(nativeCompletionFeedback(db, runId, doneReport(["https://example.com/report.pdf"]))) .rejects.toThrow(/requested file.*accessible|register_deliverable/); await db.update(issues).set({ title: "Explain how a newsletter works" }).where(eq(issues.id, issueId)); try { await expect(nativeCompletionFeedback(db, runId, doneReport([]))) .resolves.toContain("Completion report accepted"); await expect(nativeCompletionFeedback(db, runId, doneReport(["README.md"]))) .resolves.toContain("Completion report accepted"); await expect(nativeCompletionFeedback(db, runId, { ...doneReport([]), artifacts: [{ kind: "file", ref: "unpublished.pdf" }] })) .rejects.toThrow("workspace-only file"); } finally { await db.update(issues).set({ title: "Prepare a requested file" }).where(eq(issues.id, issueId)); } }); it("accepts a cited registered work product and respects the current request", async () => { const [product] = await db.insert(issueWorkProducts).values({ companyId, issueId, type: "artifact", provider: "external", title: "Requested report", status: "ready_for_review", url: "https://example.com/report.pdf", createdByRunId: runId }).returning(); try { for (const ref of [product.url!, `work_product:${product.id}`]) { await expect(nativeCompletionFeedback(db, runId, doneReport([ref]))) .resolves.toContain("Completion report accepted"); } // Metadata can name a nonexistent or cleaned-up workspace file; it is not a download. await db.update(issueWorkProducts).set({ url: null, metadata: { resourceRef: { kind: "workspace_file", path: "missing-report.pdf" } } }).where(eq(issueWorkProducts.id, product.id)); await expect(nativeCompletionFeedback(db, runId, doneReport([`work_product:${product.id}`]))) .rejects.toThrow("requested file has no accessible delivery evidence"); await db.update(issueWorkProducts).set({ url: product.url }).where(eq(issueWorkProducts.id, product.id)); await db.update(issueWorkProducts).set({ status: "failed" }).where(eq(issueWorkProducts.id, product.id)); await expect(nativeCompletionFeedback(db, runId, doneReport([`work_product:${product.id}`]))) .rejects.toThrow("requested file has no accessible delivery evidence"); for (const objective of [ "Do not create a file. Explain the result inline.", "Run hostname, id, and pwd. Report the output in a completion comment. Do not modify repository files or create attachments.", ]) { await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionContinuation: { objective } } }).where(eq(heartbeatRuns.id, runId)); await expect(nativeCompletionFeedback(db, runId, doneReport([]))) .resolves.toContain("Completion report accepted"); } } finally { await db.delete(issueWorkProducts).where(eq(issueWorkProducts.id, product.id)); await db.update(heartbeatRuns).set({ contextSnapshot: { issueId } }).where(eq(heartbeatRuns.id, runId)); } }); it.each([ "Use native write/read file tools. Write memory/pi-native.txt inside AGENT_HOME. This is personal memory, not a task deliverable.", "Use native write to copy bytes into pi-agent-memory-proof.txt. This is an internal assertion file, not a deliverable.", "Attempt native write once to /outside/pi-unassigned.txt. This intentionally unassigned root must be denied.", "Write internal-proof.txt; then check it exists. This is an internal verification file, not a deliverable.", "Attempt native write to /outside/probe.txt and create no files. This negative test must be denied.", "Write internal-proof.txt and return it in chat. This is an internal verification file, not a deliverable.", "Write internal-proof.txt and send it as a code block in your response. This is an internal verification file, not a deliverable.", ])("accepts an internal file outcome without treating it as published output: %s", async objective => { await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionContinuation: { objective } } }) .where(eq(heartbeatRuns.id, runId)); try { await expect(nativeCompletionFeedback(db, runId, doneReport([]))) .resolves.toContain("Completion report accepted"); } finally { await db.update(heartbeatRuns).set({ contextSnapshot: { issueId } }).where(eq(heartbeatRuns.id, runId)); } }); it.each([ "Write report.pdf; write internal-proof.txt. This is an internal verification file, not a deliverable.", "Write report.pdf and attempt native write to /outside/probe.txt; this negative test must be denied.", "Write report.txt and attach it. This is an internal verification file, not a deliverable.", "Write report.txt and send it to me. This is an internal verification file, not a deliverable.", "Write report.txt. This is an internal verification file, not a deliverable. Send it as a download link in your response.", ])("still requires publication when the task also asks for internal or denied writes: %s", async objective => { await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionContinuation: { objective } } }) .where(eq(heartbeatRuns.id, runId)); try { await expect(nativeCompletionFeedback(db, runId, doneReport([]))) .rejects.toThrow("requested file has no accessible delivery evidence"); } finally { await db.update(heartbeatRuns).set({ contextSnapshot: { issueId } }).where(eq(heartbeatRuns.id, runId)); } }); it("prepares one verified same-run attachment and replays without duplicates", async () => { const body = Buffer.from("native runner file handoff\n", "utf8"); await mkdir(path.join(workspaceRoot, "out"), { recursive: true }); await writeFile(path.join(workspaceRoot, "out", "answer.txt"), body); const runner = authority(); const definition = runner .definitions() .find((entry) => entry.name === "register_deliverable"); expect(definition).toMatchObject({ description: expect.stringContaining( "does not confirm provider delivery", ), inputSchema: { properties: { filename: { description: expect.stringContaining("Directory components"), }, byteSize: { minimum: 1, maximum: 10 * 1024 * 1024 }, contentRef: { description: expect.stringContaining("Workspace-relative"), }, }, }, }); const call = callFor("out/answer.txt", body, "requested-answer-v1"); const first = (await runner.execute(call)) as { entityRefs: string[]; } & Record; const replay = await runner.execute({ ...call, callId: "replay-same-key" }); const replayWithNewKey = await runner.execute({ ...call, callId: "replay-new-key", arguments: { ...call.arguments, idempotencyKey: "requested-answer-v2" }, }); expect(first).toMatchObject({ commandId: expect.stringMatching(/^deliverable-prepared:/u), disposition: "applied", stateRevision: 0, entityRefs: [expect.any(String), expect.any(String), expect.any(String)], scheduledWakeIds: [], attachmentId: first.entityRefs[0], contentPath: `/api/attachments/${first.entityRefs[0]}/content`, downloadPath: `/api/attachments/${first.entityRefs[0]}/content?download=1`, }); expect(replay).toEqual(first); expect(replayWithNewKey).toMatchObject({ disposition: "duplicate", entityRefs: first.entityRefs, attachmentId: first.attachmentId, contentPath: first.contentPath, downloadPath: first.downloadPath, }); // Exercise real persisted comments with the actual applied/duplicate receipts. // Preparing the attachment must not consume the agent's final reply slot. const preparedComments = await db.select().from(issueComments).where(eq(issueComments.issueId, issueId)); for (const receipt of [first, replayWithNewKey]) { const resultJson = { semanticToolReceipts: { file: { operationId: "register_deliverable", result: receipt } } }; const existingComment = findHeartbeatRunCompletionComment(preparedComments, resultJson); expect(existingComment).toBeNull(); expect(resolveHeartbeatRunResponse({ resultJson, existingComment, finalAgentMessage: { text: "Here is the requested answer.", channel: "final", sourceEventId: "final-1" }, })).toMatchObject({ text: "Here is the requested answer.", decision: { commentAction: "create" } }); } await expect(nativeCompletionFeedback(db, runId, doneReport([`deliverable:${first.entityRefs[0]}`]))) .resolves.toContain("Completion report accepted"); await db.update(issues).set({ title: "Create a Markdown document on this task." }).where(eq(issues.id, issueId)); try { await expect(nativeCompletionFeedback(db, runId, doneReport([`deliverable:${first.entityRefs[0]}`]))) .resolves.toContain("Completion report accepted"); await expect(nativeCompletionFeedback(db, runId, doneReport(["out/answer.txt"]))) .rejects.toThrow("write_document"); } finally { await db.update(issues).set({ title: "Prepare a requested file" }).where(eq(issues.id, issueId)); } const otherIssueId = "00000000-0000-4000-8000-000000009111"; await db.insert(issues).values({ id: otherIssueId, companyId, title: "Unrelated file", status: "in_progress" }); await db.update(issueAttachments).set({ issueId: otherIssueId }).where(eq(issueAttachments.id, first.entityRefs[0])); try { await expect(nativeCompletionFeedback(db, runId, doneReport([`deliverable:${first.entityRefs[0]}`]))) .rejects.toThrow("registered attachment on this task"); } finally { await db.update(issueAttachments).set({ issueId }).where(eq(issueAttachments.id, first.entityRefs[0])); await db.delete(issues).where(eq(issues.id, otherIssueId)); } const attachmentRows = await db .select() .from(issueAttachments) .where(eq(issueAttachments.issueId, issueId)); expect(attachmentRows).toHaveLength(1); expect(attachmentRows[0]).toMatchObject({ originatingRunId: runId, issueCommentId: expect.any(String), }); await expect( db.select().from(assets).where(eq(assets.companyId, companyId)), ).resolves.toEqual([ expect.objectContaining({ createdByAgentId: agentId, originalFilename: "answer.txt", contentType: "text/plain", byteSize: body.length, sha256: call.arguments.sha256, }), ]); await expect( db .select() .from(issueWorkProducts) .where(eq(issueWorkProducts.issueId, issueId)), ).resolves.toEqual([ expect.objectContaining({ type: "artifact", provider: "paperclip", title: "Requested answer", createdByRunId: runId, }), ]); await expect( db.select().from(issueComments).where(eq(issueComments.issueId, issueId)), ).resolves.toEqual([ expect.objectContaining({ authorAgentId: agentId, createdByRunId: runId, body: "Prepared Requested answer for this response.", metadata: expect.objectContaining({ authorizationReason: "paperclip_runner_protocol", }), }), ]); await expect( db.select().from(activityLog).where(eq(activityLog.entityId, issueId)), ).resolves.toEqual([ expect.objectContaining({ action: "issue.attachment_added", agentId, runId, details: expect.objectContaining({ attachmentId: first.entityRefs[0], workProductId: first.entityRefs[1], commentId: first.entityRefs[2], source: "paperclip_runner_protocol", }), }), ]); }); it("fails closed for remote targets, symlinks, traversal, hash drift, and foreign bindings", async () => { const body = Buffer.from("untrusted path checks\n", "utf8"); await writeFile(path.join(workspaceRoot, "checked.txt"), body); await symlink( path.join(workspaceRoot, "checked.txt"), path.join(workspaceRoot, "linked.txt"), ); await mkdir(path.join(workspaceRoot, "real-directory"), { recursive: true, }); await writeFile( path.join(workspaceRoot, "real-directory", "nested.txt"), body, ); await symlink( path.join(workspaceRoot, "real-directory"), path.join(workspaceRoot, "linked-directory"), ); await writeFile(path.join(workspaceRoot, "hardlink-origin.txt"), body); await link( path.join(workspaceRoot, "hardlink-origin.txt"), path.join(workspaceRoot, "hardlinked.txt"), ); await expect( authority({ executionTargetKind: "remote" }).execute( callFor("checked.txt", body, "remote-denied"), ), ).rejects.toThrow("paperclip_runner_file_handoff_remote_unsupported"); await expect( authority().execute(callFor("linked.txt", body, "symlink-denied")), ).rejects.toThrow("paperclip_runner_file_handoff_symlink_denied"); await expect( authority().execute( callFor( "linked-directory/nested.txt", body, "intermediate-symlink-denied", ), ), ).rejects.toThrow("paperclip_runner_file_handoff_symlink_denied"); await expect( authority().execute(callFor("hardlinked.txt", body, "hardlink-denied")), ).rejects.toThrow("paperclip_runner_file_handoff_file_changed"); await expect( authority().execute(callFor("../checked.txt", body, "traversal-denied")), ).rejects.toThrow("paperclip_runner_file_handoff_path_denied"); await expect( authority().execute({ ...callFor("checked.txt", body, "hash-denied"), arguments: { ...callFor("checked.txt", body, "hash-denied").arguments, sha256: "0".repeat(64), }, }), ).rejects.toThrow("paperclip_runner_file_handoff_hash_mismatch"); await expect( authority({ companyId: "00000000-0000-4000-8000-000000009999" }).execute( callFor("checked.txt", body, "foreign-denied"), ), ).rejects.toThrow("paperclip_runner_tool_binding_not_authorized"); }); it("registers a verified remote output without reading a controller path", async () => { const body = Buffer.from("Remote requested file\n"); const reader = vi.fn(async () => body); const remote = authority({ executionTargetKind: "remote", readRemoteWorkspaceFile: reader }); expect(remote.definitions()).toContainEqual(expect.objectContaining({ name: "register_deliverable" })); const call = callFor("remote-only/result.txt", body, "remote-output"); const result = await remote.execute(call) as { entityRefs: string[] }; expect(reader).toHaveBeenCalledWith({ contentRef: call.arguments.contentRef, byteSize: body.length, sha256: call.arguments.sha256 }); await expect(nativeCompletionFeedback(db, runId, doneReport([`deliverable:${result.entityRefs[0]}`]))) .resolves.toContain("Completion report accepted"); const badReader = vi.fn(async () => Buffer.from("wrong bytes")); await expect(authority({ executionTargetKind: "remote", readRemoteWorkspaceFile: badReader }) .execute(callFor("remote-only/drift.txt", body, "remote-drift"))) .rejects.toThrow(/size|hash/); }); it("reads provider files before mutation locks and replays without reading again", async () => { const body = Buffer.from("Provider callback output\n"); const reader = vi.fn(async () => { await db.transaction(async tx => { await tx.execute(sql`set local lock_timeout = '500ms'`); await tx.select({ id: agents.id }).from(agents).where(eq(agents.id, agentId)).for("share"); }); return body; }); const remote = authority({ executionTargetKind: "remote", readRemoteTaskFile: reader }); const call = callFor("proof.txt", body, "provider-lock-callback"); const result = await remote.execute(call); reader.mockRejectedValueOnce(new Error("The original file is no longer available")); await expect(remote.execute(call)).resolves.toEqual(result); expect(reader).toHaveBeenCalledTimes(1); }); it("rejects publication if agent authority changes during a provider read", async () => { const body = Buffer.from("Revoked output\n"); const before = await db.select({ id: issueAttachments.id }).from(issueAttachments); const reader = vi.fn(async () => { await db.update(agents).set({ status: "paused" }).where(eq(agents.id, agentId)); return body; }); try { await expect(authority({ executionTargetKind: "remote", readRemoteTaskFile: reader }) .execute(callFor("proof.txt", body, "revoked-during-read"))) .rejects.toThrow("paperclip_runner_tool_binding_not_authorized"); expect(await db.select({ id: issueAttachments.id }).from(issueAttachments)).toEqual(before); } finally { await db.update(agents).set({ status: "active" }).where(eq(agents.id, agentId)); } }); it("registers an absolute file only through a task-location-authorizing reader", async () => { const body=Buffer.from("Home output\n");const reader=vi.fn(async()=>body); const call=callFor("/home/agent/proof.txt",body,"home-output"); await expect(authority({executionTargetKind:"remote",readRemoteWorkspaceFile:reader}).execute(call)).rejects.toThrow("path_denied"); expect(reader).not.toHaveBeenCalled(); await expect(authority({executionTargetKind:"remote",readRemoteTaskFile:reader}).execute(call)).resolves.toBeDefined(); expect(reader).toHaveBeenCalledWith(expect.objectContaining({contentRef:"/home/agent/proof.txt"})); }); it("stages only exact wake-bound inbound bytes without exposing an API credential", async () => { const storage = createStorageService( createLocalDiskStorageProvider(storageRoot), ); const body = Buffer.from( "inspect these authenticated inbound bytes\n", "utf8", ); const stored = await storage.putFile({ companyId, namespace: `issues/${issueId}`, originalFilename: "inbound.txt", contentType: "text/plain", body, }); const comment = await issueService(db).addComment( issueId, "Please inspect the attached file.", { userId: "inbound-user" }, ); const attachment = await issueService(db).createAttachment({ issueId, issueCommentId: comment.id, provider: stored.provider, objectKey: stored.objectKey, contentType: stored.contentType, byteSize: stored.byteSize, sha256: stored.sha256, originalFilename: stored.originalFilename, createdByUserId: "inbound-user", }); await db .update(heartbeatRuns) .set({ contextSnapshot: { issueId, paperclipWake: { comments: [ { id: comment.id, body: "Please inspect the attached file.", attachments: [ { id: attachment.id, filename: "inbound.txt", contentType: "text/plain", byteSize: body.length, contentPath: `/api/attachments/${attachment.id}/content`, }, ], }, ], }, }, }) .where(eq(heartbeatRuns.id, runId)); const stage = await stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage, }); expect(stage.attachments).toEqual([ { id: attachment.id, filename: "inbound.txt", contentType: "text/plain", byteSize: body.length, workspaceRelativePath: expect.stringMatching( /^\.paperclip-inbound\/.+\/[0-9a-f-]{36}$/u, ), unavailableReason: null, }, ]); const relativePath = stage.attachments[0]?.workspaceRelativePath; expect(relativePath).toBeTruthy(); await expect( readFile(path.join(workspaceRoot, relativePath!)), ).resolves.toEqual(body); const concurrentStage = await stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage, }); expect(concurrentStage.attachments[0]?.workspaceRelativePath).not.toBe( relativePath, ); await expect( readFile(path.join(workspaceRoot, relativePath!)), ).resolves.toEqual(body); await concurrentStage.cleanup(); const liveOwner = spawn( process.execPath, ["-e", "setInterval(() => undefined, 1_000)"], { stdio: "ignore" }, ); await once(liveOwner, "spawn"); if (!liveOwner.pid) throw new Error("live staging owner did not start"); try { const liveForeignDirectory = path.join( workspaceRoot, ".paperclip-inbound", `process-${liveOwner.pid}-unknown-00000000-0000-4000-8000-000000009298`, ); const liveForeignFile = path.join(liveForeignDirectory, "active-slot"); await mkdir(liveForeignDirectory, { recursive: true }); await writeFile(liveForeignFile, "bytes owned by another live process"); const crossProcessStage = await stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage, }); await expect(readFile(liveForeignFile, "utf8")).resolves.toBe( "bytes owned by another live process", ); await crossProcessStage.cleanup(); const recycledOwnerDirectory = path.join( workspaceRoot, ".paperclip-inbound", `process-${liveOwner.pid}-0-00000000-0000-4000-8000-000000009297`, ); const recycledOwnerFile = path.join(recycledOwnerDirectory, "stale-slot"); await mkdir(recycledOwnerDirectory, { recursive: true }); await writeFile(recycledOwnerFile, "bytes from a recycled pid owner"); const recycledOwnerStage = await stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage, }); await expect(readFile(recycledOwnerFile)).resolves.toEqual( Buffer.alloc(0), ); await recycledOwnerStage.cleanup(); } finally { liveOwner.kill("SIGTERM"); await once(liveOwner, "exit"); } const prompt = renderNativeRunnerStagedAttachmentPrompt(stage.attachments); expect(prompt).toContain( "Never substitute an older generated workspace file", ); expect(prompt).toContain(relativePath!); expect(prompt).not.toContain("/api/attachments/"); expect(prompt).not.toContain("PAPERCLIP_API_KEY"); await stage.cleanup(); await expect( readFile(path.join(workspaceRoot, relativePath!)), ).resolves.toEqual(Buffer.alloc(0)); const crashDirectory = path.join( workspaceRoot, ".paperclip-inbound", "process-2147483647-0-00000000-0000-4000-8000-000000009299", ); await mkdir(crashDirectory, { recursive: true }); const outsideResidue = path.join(temporaryRoot, "outside-residue.txt"); const linkedResidue = path.join(crashDirectory, "linked-residue"); await writeFile(outsideResidue, "outside bytes must remain intact"); await symlink(outsideResidue, linkedResidue); await expect( stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage, }), ).rejects.toThrow("paperclip_runner_attachment_staging_residue_denied"); await expect(readFile(outsideResidue, "utf8")).resolves.toBe( "outside bytes must remain intact", ); await unlink(linkedResidue); const crashedResidue = path.join(crashDirectory, "opaque-residue"); await writeFile(crashedResidue, "bytes retained by an abrupt prior crash"); const recoveredStage = await stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage, }); await expect(readFile(crashedResidue)).resolves.toEqual(Buffer.alloc(0)); await recoveredStage.cleanup(); await expect( stageNativeRunnerWakeAttachments({ db, binding: { companyId: "00000000-0000-4000-8000-000000009999", issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage, }), ).rejects.toThrow("paperclip_runner_attachment_staging_not_authorized"); const remoteStage = await stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot: "/remote/workspace", executionTargetKind: "remote", }, storage, }); expect(remoteStage.attachments).toEqual([ expect.objectContaining({ id: attachment.id, workspaceRelativePath: null, unavailableReason: "remote_workspace_staging_unsupported", }), ]); await remoteStage.cleanup(); }); it("excludes an older same-issue attachment when the current wake selects a newer file", async () => { const storage = createStorageService( createLocalDiskStorageProvider(storageRoot), ); const historicalBody = Buffer.from( "historical decoy marker: amber-larch-17\n", "utf8", ); const currentBody = Buffer.from( "current wake marker: cobalt-sparrow-42\n", "utf8", ); const historical = await createInboundAttachmentFixture({ storage, filename: "historical.txt", body: historicalBody, commentBody: "An older attachment from this task.", }); const current = await createInboundAttachmentFixture({ storage, filename: "current.txt", body: currentBody, commentBody: "Inspect only the file attached to this turn.", }); await db .update(heartbeatRuns) .set({ contextSnapshot: { issueId, paperclipWake: { comments: [ { id: current.comment.id, body: "Inspect only the file attached to this turn.", attachments: [ { id: current.attachment.id, filename: "current.txt", contentType: "text/plain", byteSize: currentBody.length, contentPath: `/api/attachments/${current.attachment.id}/content`, }, ], }, ], }, }, }) .where(eq(heartbeatRuns.id, runId)); const objectReads: string[] = []; const observingStorage: StorageService = { ...storage, getObject: async (readCompanyId, objectKey, options) => { objectReads.push(objectKey); return storage.getObject(readCompanyId, objectKey, options); }, }; const stage = await stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage: observingStorage, }); expect(stage.attachments).toEqual([ expect.objectContaining({ id: current.attachment.id, filename: "current.txt", unavailableReason: null, }), ]); expect(objectReads).toEqual([current.stored.objectKey]); expect(objectReads).not.toContain(historical.stored.objectKey); const relativePath = stage.attachments[0]?.workspaceRelativePath; expect(relativePath).toBeTruthy(); await expect( readFile(path.join(workspaceRoot, relativePath!)), ).resolves.toEqual(currentBody); await stage.cleanup(); }); it("stages no historical attachment when the current wake contains only an omission", async () => { const storage = createStorageService( createLocalDiskStorageProvider(storageRoot), ); const historical = await createInboundAttachmentFixture({ storage, filename: "omission-decoy.txt", body: Buffer.from( "never substitute this historical attachment\n", "utf8", ), commentBody: "Historical file that is not part of the current wake.", }); const currentComment = await issueService(db).addComment( issueId, "Inspect the current attachment if Paperclip imported it.", { userId: "inbound-user" }, ); await db .update(heartbeatRuns) .set({ contextSnapshot: { issueId, paperclipWake: { comments: [ { id: currentComment.id, body: "Inspect the current attachment if Paperclip imported it.", attachments: [], }, ], attachmentOmissions: [ { commentId: currentComment.id, reasons: { unsupported_type: 1 }, }, ], }, }, }) .where(eq(heartbeatRuns.id, runId)); const objectReads: string[] = []; const observingStorage: StorageService = { ...storage, getObject: async (readCompanyId, objectKey, options) => { objectReads.push(objectKey); return storage.getObject(readCompanyId, objectKey, options); }, }; const stage = await stageNativeRunnerWakeAttachments({ db, binding: { companyId, issueId, runId, agentId, workspaceRoot, executionTargetKind: "local", }, storage: observingStorage, }); expect(stage.attachments).toEqual([]); expect(objectReads).toEqual([]); expect(objectReads).not.toContain(historical.stored.objectKey); expect(renderNativeRunnerStagedAttachmentPrompt(stage.attachments)).toBe( "", ); await stage.cleanup(); }); it("fails closed on a reminted work product and removes definite pre-commit storage failures", async () => { const [existingAttachment] = await db .select({ id: issueAttachments.id }) .from(issueAttachments) .where(eq(issueAttachments.originatingRunId, runId)) .limit(1); expect(existingAttachment).toBeDefined(); await db .delete(issueWorkProducts) .where(eq(issueWorkProducts.issueId, issueId)); const foreignRunId = "00000000-0000-4000-8000-000000009105"; await db.insert(heartbeatRuns).values({ id: foreignRunId, companyId, agentId, status: "succeeded", runtimeMode: "native", nativeIssueId: issueId, invocationSource: "automation", triggerDetail: "system", contextSnapshot: { issueId }, }); await db.insert(issueWorkProducts).values({ companyId, issueId, type: "artifact", provider: "paperclip", externalId: existingAttachment!.id, title: "Reminted by another run", status: "active", reviewState: "none", isPrimary: false, healthStatus: "unknown", createdByRunId: foreignRunId, }); const originalBody = Buffer.from("native runner file handoff\n", "utf8"); await expect( authority().execute( callFor("out/answer.txt", originalBody, "reminted-work-product"), ), ).rejects.toThrow("paperclip_runner_file_handoff_work_product_missing"); const failureBody = Buffer.from("storage mismatch cleanup\n", "utf8"); await writeFile(path.join(workspaceRoot, "mismatch.txt"), failureBody); const realStorage = createStorageService( createLocalDiskStorageProvider(storageRoot), ); let deletedObjectKey: string | null = null; const mismatchingStorage: StorageService = { ...realStorage, putFile: async (input) => ({ ...(await realStorage.putFile(input)), sha256: "0".repeat(64), }), deleteObject: async (deleteCompanyId, objectKey) => { deletedObjectKey = objectKey; await realStorage.deleteObject(deleteCompanyId, objectKey); }, }; const mismatchingAuthority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId, workspaceRoot, executionTargetKind: "local", storage: mismatchingStorage, }); await expect( mismatchingAuthority.execute( callFor("mismatch.txt", failureBody, "storage-mismatch"), ), ).rejects.toThrow("paperclip_runner_file_handoff_storage_mismatch"); expect(deletedObjectKey).toEqual(expect.any(String)); await expect( realStorage.headObject(companyId, deletedObjectKey!), ).resolves.toMatchObject({ exists: false }); const receiptFailureBody = Buffer.from( "receipt persistence cleanup\n", "utf8", ); await writeFile( path.join(workspaceRoot, "receipt-failure.txt"), receiptFailureBody, ); let receiptFailureObjectKey: string | null = null; const receiptFailureStorage: StorageService = { ...realStorage, putFile: async (input) => { const stored = await realStorage.putFile(input); receiptFailureObjectKey = stored.objectKey; return stored; }, deleteObject: async (deleteCompanyId, objectKey) => { await realStorage.deleteObject(deleteCompanyId, objectKey); }, }; await db.execute(sql` CREATE OR REPLACE FUNCTION paperclip_test_fail_native_receipt() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.result_json IS DISTINCT FROM OLD.result_json THEN RAISE EXCEPTION 'forced_native_receipt_failure'; END IF; RETURN NEW; END; $$ `); await db.execute(sql` CREATE TRIGGER paperclip_test_fail_native_receipt BEFORE UPDATE ON heartbeat_runs FOR EACH ROW EXECUTE FUNCTION paperclip_test_fail_native_receipt() `); try { const receiptFailureAuthority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId, workspaceRoot, executionTargetKind: "local", storage: receiptFailureStorage, }); let receiptFailure: unknown; try { await receiptFailureAuthority.execute( callFor( "receipt-failure.txt", receiptFailureBody, "receipt-persistence-failure", ), ); } catch (error) { receiptFailure = error; } expect(receiptFailure).toMatchObject({ message: expect.stringContaining( 'Failed query: update "heartbeat_runs"', ), cause: { message: expect.stringContaining("forced_native_receipt_failure"), }, }); } finally { await db.execute( sql`DROP TRIGGER IF EXISTS paperclip_test_fail_native_receipt ON heartbeat_runs`, ); await db.execute( sql`DROP FUNCTION IF EXISTS paperclip_test_fail_native_receipt()`, ); } expect(receiptFailureObjectKey).toEqual(expect.any(String)); await expect( realStorage.headObject(companyId, receiptFailureObjectKey!), ).resolves.toMatchObject({ exists: false }); }); it("returns authenticated file-delivery modes and upgrades receipt descriptions without repeating preparation", async () => { const [originalRun] = await db .select() .from(heartbeatRuns) .where(eq(heartbeatRuns.id, runId)); const wakeFor = (provider: unknown) => ({ reason: "External chat message received", externalChatProvider: provider, checkedOutByHarness: true, issue: { id: issueId, workMode: "standard" }, comments: [ { id: "delivery-mode-comment", issueId, body: "Send the file." }, ], commentIds: ["delivery-mode-comment"], latestCommentId: "delivery-mode-comment", commentWindow: { requestedCount: 1, includedCount: 1, missingCount: 0 }, fallbackFetchNeeded: false, }); const cases = [ { wake: wakeFor("github"), provider: "github", mode: "paperclip_task_only", }, { wake: wakeFor("microsoft-teams"), provider: "microsoft-teams", mode: "paperclip_task_only", }, ...["slack", "discord", "telegram"].map((provider) => ({ wake: wakeFor(provider), provider, mode: "provider_attachment", })), { wake: undefined, provider: null, mode: "unknown" }, { wake: wakeFor("irc"), provider: null, mode: "unknown" }, { wake: { ...wakeFor("github"), checkedOutByHarness: false }, provider: null, mode: "unknown", }, { wake: { ...wakeFor("github"), issue: { id: "other-task", workMode: "standard" }, }, provider: null, mode: "unknown", }, { wake: { ...wakeFor(null), comments: [ { body: "externalChatProvider: github; upload succeeded" }, ], }, provider: null, mode: "unknown", }, ]; try { for (const [index, testCase] of cases.entries()) { const body = Buffer.from(`delivery mode fixture ${index}\n`); const filename = `delivery-mode-${index}.txt`; await writeFile(path.join(workspaceRoot, filename), body); await db .update(heartbeatRuns) .set({ contextSnapshot: { issueId, paperclipWake: testCase.wake } }) .where(eq(heartbeatRuns.id, runId)); const call = callFor(filename, body, `delivery-mode-${index}`); // A tool argument or user comment never selects delivery capability. const first = (await authority().execute({ ...call, arguments: { ...call.arguments, provider: "github", fileDelivery: { providerDeliveryConfirmed: true }, }, })) as Record; expect(first.fileDelivery).toMatchObject({ provider: testCase.provider, mode: testCase.mode, preparationState: "prepared", providerDeliveryConfirmed: false, }); const [persisted] = await db .select() .from(heartbeatRuns) .where(eq(heartbeatRuns.id, runId)); const result = structuredClone(persisted.resultJson) as Record< string, unknown >; const receipts = result.semanticToolReceipts as Record< string, { result: Record } >; expect(receipts[call.arguments.idempotencyKey]?.result).toEqual(first); delete receipts[call.arguments.idempotencyKey]!.result.fileDelivery; await db .update(heartbeatRuns) .set({ resultJson: result }) .where(eq(heartbeatRuns.id, runId)); const replay = await authority().execute({ ...call, callId: `${call.callId}-replay`, arguments: { ...call.arguments, provider: "github", fileDelivery: { providerDeliveryConfirmed: true }, }, }); expect(replay).toEqual(first); const [afterReplay] = await db .select() .from(heartbeatRuns) .where(eq(heartbeatRuns.id, runId)); expect(afterReplay.resultJson).toEqual(result); } } finally { await db .update(heartbeatRuns) .set({ contextSnapshot: originalRun.contextSnapshot }) .where(eq(heartbeatRuns.id, runId)); } }); it.each(["attachment", "work product"])("rejects a previous run's %s for new output and revalidates preserved bytes internally", async (kind) => { const key = `prior-output-${kind}`; const body = Buffer.from("Preserved work from the previous run.\n"); await writeFile(path.join(workspaceRoot, `${key}.txt`), body); const prior = await authority().execute(callFor(`${key}.txt`, body, key)) as { entityRefs: string[] }; const [product] = await db.insert(issueWorkProducts).values({ companyId, issueId, type: "artifact", provider: "external", title: "Previous report", status: "ready_for_review", url: "https://example.com/previous.pdf", createdByRunId: runId }).returning(); const nextRunId = randomUUID(); await db.insert(heartbeatRuns).values({ id: nextRunId, companyId, agentId, status: "running", runtimeMode: "native", nativeIssueId: issueId, invocationSource: "assignment", triggerDetail: "system", contextSnapshot: { issueId } }); await db.update(issues).set({ executionRunId: nextRunId }).where(eq(issues.id, issueId)); const ref = kind === "attachment" ? `deliverable:${prior.entityRefs[0]}` : `work_product:${product.id}`; try { await expect(nativeCompletionFeedback(db, nextRunId, doneReport([ref]))) .rejects.toThrow(/current run|this run|requested file.*accessible/); // A reference in a text-only follow-up is still useful evidence, not a new output claim. await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionContinuation: { objective: "Do not create a file. Explain the result inline." } } }).where(eq(heartbeatRuns.id, nextRunId)); await expect(nativeCompletionFeedback(db, nextRunId, doneReport([ref]))) .resolves.toContain("Completion report accepted"); await db.update(heartbeatRuns).set({ contextSnapshot: { issueId } }).where(eq(heartbeatRuns.id, nextRunId)); // The replacement can verify and publish the existing bytes itself. No user action is needed. const replacement = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId: nextRunId, workspaceRoot, executionTargetKind: "local", storage: createStorageService(createLocalDiskStorageProvider(storageRoot)) }); const current = await replacement.execute(callFor(`${key}.txt`, body, `${key}-verified`)) as { entityRefs: string[] }; expect(current.entityRefs[0]).not.toBe(prior.entityRefs[0]); expect(await readFile(path.join(workspaceRoot, `${key}.txt`))).toEqual(body); // Feedback reloads the durable receipt, so a controller restart of the same run keeps this proof. await expect(nativeCompletionFeedback(db, nextRunId, doneReport([`deliverable:${current.entityRefs[0]}`]))) .resolves.toContain("Completion report accepted"); await expect(nativeCompletionFeedback(db, nextRunId, doneReport([ref, `deliverable:${current.entityRefs[0]}`]))) .resolves.toContain("Completion report accepted"); const [currentProduct] = await db.insert(issueWorkProducts).values({ companyId, issueId, type: "artifact", provider: "external", title: "Current report", status: "ready_for_review", url: "https://example.com/current.pdf", createdByRunId: nextRunId }).returning(); await expect(nativeCompletionFeedback(db, nextRunId, doneReport([ref, `work_product:${currentProduct.id}`]))) .resolves.toContain("Completion report accepted"); } finally { await db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, issueId)); await db.delete(issueWorkProducts).where(eq(issueWorkProducts.id, product.id)); } }); it.each(["filename", "size", "hash", "origin", "missing receipt", "wrong operation"])("requires matching current publication proof after %s changes", async (mutation) => { const key = `receipt-match-${mutation}`; const body = Buffer.from(`verified publication ${mutation}\n`); await writeFile(path.join(workspaceRoot, `${key}.txt`), body); const result = await authority().execute(callFor(`${key}.txt`, body, key)) as { entityRefs: string[] }; const [attachment] = await db.select().from(issueAttachments).where(eq(issueAttachments.id, result.entityRefs[0])); const [asset] = await db.select().from(assets).where(eq(assets.id, attachment.assetId)); const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)); if (mutation === "filename") await db.update(assets).set({ originalFilename: "different.txt" }).where(eq(assets.id, asset.id)); if (mutation === "size") await db.update(assets).set({ byteSize: asset.byteSize + 1 }).where(eq(assets.id, asset.id)); if (mutation === "hash") await db.update(assets).set({ sha256: "0".repeat(64) }).where(eq(assets.id, asset.id)); if (mutation === "origin") await db.update(issueAttachments).set({ originatingRunId: null }).where(eq(issueAttachments.id, attachment.id)); if (mutation === "missing receipt" || mutation === "wrong operation") { const changed = structuredClone(run.resultJson!); const receipts = changed.semanticToolReceipts as Record; if (mutation === "missing receipt") delete receipts[key]; else receipts[key]!.operationId = "report_progress"; await db.update(heartbeatRuns).set({ resultJson: changed }).where(eq(heartbeatRuns.id, runId)); } try { await expect(nativeCompletionFeedback(db, runId, doneReport([`deliverable:${attachment.id}`]))) .rejects.toThrow(/current run|this run/); } finally { await db.update(assets).set({ originalFilename: asset.originalFilename, byteSize: asset.byteSize, sha256: asset.sha256 }).where(eq(assets.id, asset.id)); await db.update(issueAttachments).set({ originatingRunId: attachment.originatingRunId }).where(eq(issueAttachments.id, attachment.id)); await db.update(heartbeatRuns).set({ resultJson: run.resultJson }).where(eq(heartbeatRuns.id, runId)); } }); });