Consume remote download snapshots without local path access

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 08:31:36 -05:00
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!));
}