mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
Read deliverable files before locking mutation rows (#17309)
This commit is contained in:
1 parent
412914cfa7
commit
583ec2443a
3 files changed
+71
-34
No files matched your search
@@ -602,6 +602,40 @@ describe("native runner file handoff", () => {
|
||||
.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");
|
||||
|
||||
@@ -298,7 +298,7 @@ function sameFileIdentity(left: Stats, right: Stats): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
async function readVerifiedWorkspaceFile(
|
||||
export async function readVerifiedWorkspaceFile(
|
||||
binding: NativeRunnerFileHandoffBinding,
|
||||
input: NativeRunnerFileHandoffInput,
|
||||
): Promise<VerifiedWorkspaceFile> {
|
||||
@@ -1141,14 +1141,11 @@ async function assertCurrentBinding(
|
||||
export async function prepareNativeRunnerFileHandoff(input: {
|
||||
readonly db: Db;
|
||||
readonly binding: NativeRunnerFileHandoffBinding;
|
||||
readonly deliverable: NativeRunnerFileHandoffInput;
|
||||
readonly verifiedFile: VerifiedWorkspaceFile;
|
||||
readonly storage?: StorageService;
|
||||
}): Promise<PreparedNativeRunnerFileHandoff> {
|
||||
const { statusVersion } = await assertCurrentBinding(input.db, input.binding);
|
||||
const verified = await readVerifiedWorkspaceFile(
|
||||
input.binding,
|
||||
input.deliverable,
|
||||
);
|
||||
const verified = input.verifiedFile;
|
||||
|
||||
const [existing] = await input.db
|
||||
.select({
|
||||
|
||||
@@ -94,7 +94,7 @@ import { getNativeReviewAssignment, readNativeReviewAssignmentContext, type Nati
|
||||
import { childReviewOutcomes } from "./child-review-outcomes.js";
|
||||
import { logActivity, persistActivity, publishActivity } from "../activity-log.js";
|
||||
import { captureRunIdentity } from "../run-identity.js";
|
||||
import { prepareNativeRunnerFileHandoff, type RemoteWorkspaceFileReader } from "./native-runner-file-handoff.js";
|
||||
import { prepareNativeRunnerFileHandoff, readVerifiedWorkspaceFile, type RemoteWorkspaceFileReader } from "./native-runner-file-handoff.js";
|
||||
import { MAX_ATTACHMENT_BYTES } from "../../attachment-types.js";
|
||||
import {
|
||||
READ_CURRENT_WAKE_COMMENTS_TOOL_DEFINITION,
|
||||
@@ -1927,6 +1927,35 @@ export class PaperclipRunnerToolAuthority {
|
||||
if (!workspaceRoot) {
|
||||
throw new Error("paperclip_runner_file_handoff_workspace_unavailable");
|
||||
}
|
||||
const current = await this.#boundContext();
|
||||
const authorizeWorkspace = async (actor: typeof agents.$inferSelect) => {
|
||||
if (this.binding.dotRuntime) {
|
||||
if (!this.binding.workspaceBridge || actor.adapterConfig?.dotWorkspaceAccess !== true)
|
||||
throw forbidden("Dot workspace access is required to publish workspace files.");
|
||||
assertRunnerWorkspacePathAllowed(typeof input.contentRef === "string" ? input.contentRef : "");
|
||||
await this.binding.assertBridgeAuthority?.();
|
||||
}
|
||||
};
|
||||
await authorizeWorkspace(current.actor);
|
||||
const fileBinding = {
|
||||
companyId: this.binding.companyId,
|
||||
issueId: this.binding.issueId,
|
||||
runId: this.binding.runId,
|
||||
agentId: this.binding.agentId,
|
||||
workspaceRoot,
|
||||
executionTargetKind: this.binding.executionTargetKind ?? "local",
|
||||
readRemoteWorkspaceFile: this.binding.readRemoteWorkspaceFile,
|
||||
readRemoteTaskFile: this.binding.readRemoteTaskFile,
|
||||
} as const;
|
||||
const prior = record(record(current.run.resultJson).semanticToolReceipts)[idempotencyKey];
|
||||
const verifiedFile = prior === undefined ? await readVerifiedWorkspaceFile(fileBinding, {
|
||||
filename: typeof input.filename === "string" ? input.filename : "",
|
||||
contentType: typeof input.contentType === "string" ? input.contentType : "",
|
||||
byteSize: typeof input.byteSize === "number" ? input.byteSize : Number.NaN,
|
||||
sha256: typeof input.sha256 === "string" ? input.sha256 : "",
|
||||
contentRef: typeof input.contentRef === "string" ? input.contentRef : "",
|
||||
title: typeof input.title === "string" ? input.title : "",
|
||||
}) : null;
|
||||
let publication:
|
||||
Awaited<ReturnType<typeof persistActivity>>["publication"] | null = null;
|
||||
let rollbackDefinitePreCommitFailure: (() => Promise<void>) | null = null;
|
||||
@@ -1935,35 +1964,12 @@ export class PaperclipRunnerToolAuthority {
|
||||
idempotencyKey,
|
||||
input,
|
||||
async (tx, context) => {
|
||||
if (this.binding.dotRuntime) {
|
||||
if (!this.binding.workspaceBridge || context.actor.adapterConfig?.dotWorkspaceAccess !== true)
|
||||
throw forbidden("Dot workspace access is required to publish workspace files.");
|
||||
assertRunnerWorkspacePathAllowed(typeof input.contentRef === "string" ? input.contentRef : "");
|
||||
await this.binding.assertBridgeAuthority?.();
|
||||
}
|
||||
await authorizeWorkspace(context.actor);
|
||||
if (!verifiedFile) throw new Error("paperclip_runner_file_handoff_receipt_changed");
|
||||
const prepared = await prepareNativeRunnerFileHandoff({
|
||||
db: tx,
|
||||
binding: {
|
||||
companyId: this.binding.companyId,
|
||||
issueId: this.binding.issueId,
|
||||
runId: this.binding.runId,
|
||||
agentId: this.binding.agentId,
|
||||
workspaceRoot,
|
||||
executionTargetKind: this.binding.executionTargetKind ?? "local",
|
||||
readRemoteWorkspaceFile: this.binding.readRemoteWorkspaceFile,
|
||||
readRemoteTaskFile: this.binding.readRemoteTaskFile,
|
||||
},
|
||||
deliverable: {
|
||||
filename: typeof input.filename === "string" ? input.filename : "",
|
||||
contentType:
|
||||
typeof input.contentType === "string" ? input.contentType : "",
|
||||
byteSize:
|
||||
typeof input.byteSize === "number" ? input.byteSize : Number.NaN,
|
||||
sha256: typeof input.sha256 === "string" ? input.sha256 : "",
|
||||
contentRef:
|
||||
typeof input.contentRef === "string" ? input.contentRef : "",
|
||||
title: typeof input.title === "string" ? input.title : "",
|
||||
},
|
||||
binding: fileBinding,
|
||||
verifiedFile,
|
||||
storage: this.binding.storage,
|
||||
});
|
||||
rollbackDefinitePreCommitFailure =
|
||||
|
||||
Reference in new issue
Block a user