Files
PaperClipAI/server/src/services/native-runtime/native-runner-file-handoff.test.ts
T
DottaandPaperclip 57e977be72 feat: integrate Pi 1.0 into the experimental Runner (#14921)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - The experimental Runner owns provider processes and durable
sessions.
> - Pi needs working task execution and human controls.
> - The five-PR stack must preserve changes already on master.
> - Each layer now carries the complete integrated source for a safe
sequential fallback.
> - This PR belongs to native GitHub stack #15602, ending at #14956.

## Linked Issues or Issue Description

Refs #14436, #14631, #14743 and #14956.

Ship Pi 1.0 through the experimental Paperclip Runner. The five PRs are
#14921, #14922, #14923, #14924 and #14956. The user authorized the
complete merge after checks pass. Existing `pi_local` execution is
unchanged. Accounting and wider provider/platform qualification remain
deferred.

## What Changed

- Recover missing final replies after workspace finalization changes
owners, using accepted-turn evidence without rerunning work or granting
external-chat publication.
- Preserve the admitted Pi instruction root across warm runs, while
retaining changed-root rejection.
- Give Pi a bounded 15-second default shutdown grace so stop, drain
acknowledgement and durable suspension can complete. Explicit deadlines
and other providers retain their existing behavior.
- Integrate the Pi 1.0 runtime and master contracts.
- Use Pi profile 22. Preserve explicit caller-selected models and exact
native thinking levels. Keep Pi's wrapper, helper, extension and
question/control behavior unchanged from the qualified profile-19
runtime.
- Preserve master's Dot lifecycle and consent fields, configured task
environment, status guards and current Codex/Claude dependency versions.
Cursor stays qualified. Copilot stays pending; profile 17 binds the
changed shared protocol validation sources.
- Exclude general AWS IAM credentials from Pi static/custom provider
bindings and selected task projections; preserve the provider-scoped
Bedrock bearer key. Profile 21 is retained as historical provenance.
Rust and cloud install probes use the current declaration.
- Patch bundled brace-expansion 5.0.9 to the exact official 5.0.12
payload. Pin the patch and complete runtime closures. Include the patch
in normal installed setup tooling. Keep the upstream Pi shrinkwrap as
provenance and permit only this exact security correction.
- Include current attestation files in the Docker build context. Keep
the repository lockfile unchanged from master. CI and private image
builds resolve manifest changes before their frozen installation.

## Verification

- Full local `pnpm -r typecheck` passes, including Runner Rust, server
and UI. Focused integration checks pass: 194 Runner
admission/environment tests, 63 profile/credential tests with one
expected skip, 152 Dot/UI configuration tests, and Pi transcript/notice
tests.
- Full local `pnpm build` passes on the final source.
- Fresh final-source checks pass: all 698 Rust workspace tests (32
binaries), 156 credential/profile/controller tests with one expected
skip, Runner TypeScript typecheck, and 20 package/setup/sandbox tests.
- The profile-21 Pi materializer passes on the native host with the
official pinned Node 24.21.0 and its npm. It verifies all 150 locked
packages, the patched dependency and the exact closure. Setup/package
bundle tests and UI token gates pass.
- The old hashes were reproduced for all three supported targets before
calculating the patched graph. New closure hashes are darwin-arm64
`282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf`,
darwin-x64
`64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e`, and
linux-x64
`713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410`.
Independent native platform execution is separate from graph identity
reproduction.
- Historical cloud qualification remains unchanged: all seven core cases
pass on shipping source `10dc43c9ec65d88c2f782d62afb296d09494f215`,
harness `1a4408a48cfb5a1f094a311141c257c92cd7a893`, image
`sha256:5b3a775b383591bda1b0c1889e509acc70ce7f37c53f09733c81d59037f02280`,
and accepted Sonnet 4.6/low fixture. All 215 canonical files and all
seven cleanup checks pass independent verification. These are profile-19
results and are not relabeled as fresh profile-22 runs.
- Current Pi digest:
`sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9`.
[The readiness
plan](https://github.com/paperclipai/paperclip/blob/codex/pi-production-readiness/doc/plans/2026-10-02-pi-production-readiness.md)
preserves campaign and failed-attempt provenance.
- Merge only after every PR's current-head CI and fresh review pass.
Linux CI covers the full suites, build and browser tests. The local
embedded Postgres API-authority suite cannot start on this macOS/Node 26
host, so Linux CI must confirm that suite.

### Fresh profile-22 core qualification — 2026-10-08

All seven accepted core cases pass canonically on Pi profile 22, with
`openrouter/anthropic/claude-sonnet-4.6` and native-confirmed low
thinking. This model is a fixture; production accepts the caller's
explicit Pi provider/model.

Runtime/install source: `3241a992f2a7703e59e97ed0fd3e5d6405de4401`.
Frozen accepted harness: `1a4408a48cfb5a1f094a311141c257c92cd7a893`.
Immutable cloud image:
`ghcr.io/paperclipai/paperclip-daytona-runner@sha256:506f22db7edd78f37c0c40bec1cc084af1850455026dbf467194bfbb8fcef141`.
Pi digest:
`sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9`.

[Hosted Linux image and clean-install
verification](https://github.com/paperclipai/paperclip/actions/runs/37868328023)
passes, including all 20 source-bound archives, normal CLI/Pi setup,
companion import and the production pack reader. This exact installation
source includes the latest master integration and the corrected Pi warm
instruction-root fence. Full local typecheck/build and current-head
hosted CI verify the final stack. All 13 focused real-root regressions
pass. The full local executor suite passed 662 tests; 15 database tests
could not start the Mac embedded PostgreSQL service. Hosted Linux CI
passes the full required verification and E2E checks. These fresh
results keep their own source identity; profile-19 results remain
historical.

| Core path | Canonical campaign | Retained archive SHA-256 |
| --- | --- | --- |
| File edit, validation, download and Done |
`pi-core22-replyfix-0-1791511228` | 23 files;
`a473e8603a3dd4737863291f8d3d1e392391f0b16d433c3e0e0e9d8baf7a97b0` |
| Pending question and controller restart |
`pi-core22-replyfix-1-1791511376` | 33 files;
`6b829c4eb74e1f32a89c692a4ae7130dbfc1c6d3cf13915effe2103d9e242c8e` |
| Three-turn session/process/workspace continuity |
`pi-core22-replyfix-2-1791511587` | 23 files;
`7a87021f8f9a3fdd3c58bb4467f8d82c635e3ea4795d6e75f144d9aa14818df8` |
| Four typed questions and browser reconnects |
`pi-core22-replyfix-3-1791511881` | 42 files;
`9e31755252be1f4f9cb0626c984c142d4d1ae5f5bee3a7af08444db8d12c280a` |
| Plan approval and completion | `pi-core22-replyfix-4-1791512031` | 22
files;
`a0383ce1aab38e7b5a25ce0e9dd3bebea5c037ebd96ae6b29dae19015da2ae2c` |
| Same-turn steering and permission denial |
`pi-core22-replyfix-5-1791512261` | 39 files;
`c929b8c7070f0b66aedc17e65ca46e6beab1e363926ac9f7e2a75fb250f05949` |
| Stop during pending permission | `pi-core22-replyfix-6-1791512390` |
33 files;
`7f0a58ae0f4d5bfc76149435f4e322537089c5bd16e7ffe9b5ad71f10a621a07` |

All 215 canonical files (28714587 bytes) are independently
hash-verified. All seven cleanup grades pass, with no owned runtime
process or temporary root after each case. Automatic retries are zero.
The owned cloud host stopped normally after retention. The prior
profile-22 warm attempt remains failed and separately retained: archive
SHA-256
`1e54eba5ec72b50cee1534b23d1d1d4f21a090006b8a64501ba70db972abfde5`. Its
original canonical classification is preserved. Diagnosis reproduced a
product bug comparing an agent-files root against an unset
checkpoint-only field. The fix stores the admitted physical root
separately from the adopted per-run collection capability. The real-root
regression fails before the fix and passes afterward, including
rejection of a changed physical root. Fixture, grader, model and all
seven accepted case IDs are unchanged; this fresh campaign tests
final-reply publication after file registration first. The intermediate
restart attempt also remains failed and retained: archive SHA-256
`5dcaefdf1d17cf4cd54fd4cf810f45e736667392339b8ce7caf08bb4e225277f`. Its
original canonical classification is preserved. Pi resumed, wrote the
verified answer and completed its task; exact runner suspension was
proven, but idle stop consumed about 5.2s and left under 3s for the
drain acknowledgement. The Pi-only default shutdown grace is now 15s,
preserving a full 5s drain round trip and a finite suspension reserve.
Explicit caller deadlines, other provider defaults, literal drain
receipts and exact suspension identity checks remain unchanged. The
timing regression fails before this correction and passes afterward; all
18 focused settlement tests and Runner typecheck pass. The final-source
file attempt is also preserved as failed (`candidate_failure`), archive
SHA-256
`db6767b6773ea618997927ac77bdb005a5ac81492c7b9c0ffbc900449f829bc9`.
Native edit, validation, exact downloadable artifact and Done/succeeded
all passed, and the exact final reply was durably recorded. A workspace
recovery owner completed before the live heartbeat reached presentation,
leaving that reply absent from task chat. Recovery now materializes only
a completed final reply from the accepted turn of an ordinary internal
Done task, preserving issue/run/contract binding, suppression,
external-chat authorization and same-run deduplication. The database
regression covers the generated file-preparation receipt, suppression,
unapproved external continuation and replay. Server typecheck and all 49
response-selection tests pass; hosted Linux verifies the database
regression because embedded PostgreSQL cannot start on this Mac.

The delayed-final-answer database regression passes on [the final
root-source Linux server
shard](https://github.com/paperclipai/paperclip/actions/runs/37868262553/job/113628594152),
alongside 1,108 passing tests. The first root Runner shard had one
unchanged durable-resume test exceed its 5-second timeout; the identical
top-source shard and the isolated exact test passed. One rerun of that
failed job and its required aggregate passed without source or test
changes. The original failed job log and the single-rerun receipt remain
retained.

### October 9 merge verification

Current merge head: `5a8fe63512a7166aaef5cf50065a25008aa8b44b`. All
current-head checks pass, including `ci / verify` and `ci / e2e`;
exact-head Greptile review is 5/5 with no unresolved threads. Current
master conflicts are resolved. The user authorized the maintainer
override of the code-owner review gate after these checks. The seven
retained live core cases remain bound to source
`3241a992f2a7703e59e97ed0fd3e5d6405de4401` and its recorded cloud image.

## Risks

- The security correction changes the dependency closure and profile
identity. Old sessions must reopen on the new profile. Exact identities
and credential bindings fail closed.
- The runner remains experimental and requires explicit selection.
Legacy Pi Local is unchanged. Caller model IDs pass through; the E2E
model is a fixture.
- Accounting and the broad platform/provider matrix remain deferred.
This merge does not publish a release or deploy a service.

## Model Used

OpenAI GPT-6 through Codex assisted with reasoning, repository
inspection, editing and tool use. The exact serving ID and context
window are not exposed in this session. Final live qualification uses Pi
1.0.0 with `openrouter/anthropic/claude-sonnet-4.6` and native-confirmed
low thinking.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` OR (b) described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-09 08:56:41 -05:00

1382 lines
53 KiB
TypeScript

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<typeof startEmbeddedPostgresTestDatabase>
> | null = null;
let db: ReturnType<typeof createDb>;
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";
readRemoteWorkspaceFile: (input: { contentRef: string; byteSize: number; sha256: string }) => Promise<Buffer>;
}> = {},
) {
return new PaperclipRunnerToolAuthority(db, {
companyId: overrides.companyId ?? companyId,
agentId,
issueId,
runId,
workspaceRoot,
executionTargetKind: overrides.executionTargetKind ?? "local",
readRemoteWorkspaceFile: overrides.readRemoteWorkspaceFile,
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");
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionContinuation: { objective: "Do not create a file. Explain the result inline." } } }).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<string, unknown>;
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("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<string, unknown>;
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<string, unknown> }
>;
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<string, { operationId: string }>;
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));
}
});
});