mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
Consume remote download snapshots without local path access
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
52aee03920
commit
80377989b0
4 files changed
+24
-2
No files matched your search
@@ -235,6 +235,10 @@ describeEmbeddedPostgres("workspace file resources", () => {
|
||||
await expect(service.readContent(graph.issueId, { path: ".env", workspace: "execution" })).rejects.toMatchObject({ status: 403 });
|
||||
await expect(service.readContent(graph.issueId, { path: "../other-company/secret", workspace: "execution" })).rejects.toMatchObject({ status: 403 });
|
||||
expect(readBytes).not.toHaveBeenCalled();
|
||||
workspaceFiles.mockClear();
|
||||
const readOnly = workspaceFileResourceService(db, undefined, { allowComputerAccess: false });
|
||||
await expect(readOnly.prepareDownload(graph.issueId, { path: "README.md", workspace: "execution" })).rejects.toMatchObject({ status: 422, details: { code: "remote_workspace" } });
|
||||
expect(workspaceFiles).not.toHaveBeenCalled();
|
||||
} finally { factory.mockRestore(); await fs.rm(workspace.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ export async function readCustomerSuccessResource(
|
||||
if (q.operation.startsWith("files.")) {
|
||||
try {
|
||||
const issue = await ownRow(tx, schema.issues, q.companyId!, q.resourceId!);
|
||||
const files = workspaceFileResourceService(tx);
|
||||
const files = workspaceFileResourceService(tx, undefined, { allowComputerAccess: false });
|
||||
const input = { path: q.path ?? "", ...q.context, limit: q.limit, offset: q.offset };
|
||||
const opts = {
|
||||
issue: issue as unknown as NonNullable<Parameters<typeof files.list>[2]>["issue"],
|
||||
@@ -338,6 +338,17 @@ export async function readCustomerSuccessResource(
|
||||
if (q.operation === "files.read")
|
||||
return await files.readContent(q.resourceId!, input, opts);
|
||||
const resolved = await files.prepareDownload(q.resourceId!, input, opts);
|
||||
if ("bytes" in resolved) {
|
||||
const bytes = resolved.bytes;
|
||||
if (bytes.length === 0 && !q.range) return binary(bytes, 0);
|
||||
const start = q.range?.start ?? 0;
|
||||
const end = q.range?.end ?? bytes.length - 1;
|
||||
if (start >= bytes.length || end >= bytes.length || end - start + 1 > MAX_INSPECTION_BYTES)
|
||||
throw unprocessable("Request a bounded byte range");
|
||||
// The remote backend already captured a bounded, confined snapshot;
|
||||
// do not reinterpret its path on the controller filesystem.
|
||||
return binary(bytes.subarray(start, end + 1), bytes.length, start);
|
||||
}
|
||||
const file = await fs.open(resolved.realPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
||||
try {
|
||||
const stat = await file.stat();
|
||||
|
||||
@@ -919,6 +919,10 @@ export class PaperclipRunnerToolAuthority {
|
||||
return { bytes: Buffer.concat(chunks), filename: asset.originalFilename ?? "file", contentType: asset.contentType };
|
||||
}
|
||||
const resolved = await workspaceFileResourceService(this.db).prepareDownload(this.binding.issueId, { path: file.path!, workspace: "auto" });
|
||||
if ("bytes" in resolved) {
|
||||
if (resolved.bytes.length > RUNNER_API_MAX_BYTES) throw badRequest("Workspace file exceeds API transfer limit");
|
||||
return { bytes: resolved.bytes, filename: resolved.resource.title, contentType: resolved.resource.contentType ?? "application/octet-stream" };
|
||||
}
|
||||
const handle = await openRunnerApiWorkspaceFile(resolved.realPath);
|
||||
try {
|
||||
const stat = await handle.stat();
|
||||
|
||||
@@ -1049,8 +1049,11 @@ async function listChangedWorkspaceFiles(input: {
|
||||
};
|
||||
}
|
||||
|
||||
export function workspaceFileResourceService(db: Db, actor?: AuthorizationActor) {
|
||||
export function workspaceFileResourceService(db: Db, actor?: AuthorizationActor, options: { allowComputerAccess?: boolean } = {}) {
|
||||
async function placementFiles(candidate: WorkspaceCandidate) {
|
||||
// Computer reads acquire a bounded machine owner and may resume it. Pure
|
||||
// inspection transactions must remain read-only, including provider effects.
|
||||
if (options.allowComputerAccess === false) throw unprocessable("Computer access is unavailable in read-only inspection", { code: "remote_workspace" });
|
||||
if (!candidate.placement) throw unprocessable("Workspace placement is unavailable", { code: "remote_workspace" });
|
||||
return computerFileAccess(() => computerService(db).workspaceFiles(candidate.placement!));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user