mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-18 15:39:47 +02:00
Bound initial computer home uploads and exclude runtime state
Stream initial personal files through bounded chunks into private staging and atomically publish without replacing an existing home. Keep the home-level runtime directory out of instruction enumeration and seeding while preserving nested user content. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
421f7f0600
commit
cebfa8b09f
8 files changed
+204
-32
No files matched your search
@@ -545,6 +545,32 @@ with open(output,'wb') as out,open(error,'wb') as err:
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("bounds initial seed requests and aborts a failed upload without publishing", async () => {
|
||||
const f = fixture();
|
||||
await f.attach();
|
||||
const files = await f.service.files({ ...f.scope, agentId: "agent" });
|
||||
vi.mocked(f.backend.remote).mockImplementation(async (_record, input) => {
|
||||
if (input.action === "seed-begin") return { started: true };
|
||||
if (input.action === "seed-commit") return { seeded: true };
|
||||
return {};
|
||||
});
|
||||
await expect(files.seedBytes({ "large.bin": Buffer.alloc(2 * 1024 * 1024 + 1) })).resolves.toEqual({ seeded: true });
|
||||
const chunks = vi.mocked(f.backend.remote).mock.calls.map(([, input]) => input).filter(input => input.action === "seed-chunk");
|
||||
expect(chunks).toHaveLength(3);
|
||||
expect(chunks.map(input => input.offset)).toEqual([0, 1024 * 1024, 2 * 1024 * 1024]);
|
||||
expect(chunks.every(input => Buffer.from(input.base64 as string, "base64").length <= 1024 * 1024)).toBe(true);
|
||||
vi.mocked(f.backend.remote).mockClear();
|
||||
await expect(files.seedFiles((async function* () {
|
||||
yield { path: "first", offset: 0, bytes: Buffer.from("first") };
|
||||
throw new Error("source disappeared");
|
||||
})())).rejects.toThrow("source disappeared");
|
||||
const actions = vi.mocked(f.backend.remote).mock.calls.map(([, input]) => input.action);
|
||||
expect(actions).toEqual(["seed-begin", "seed-chunk", "seed-abort"]);
|
||||
vi.mocked(f.backend.remote).mockResolvedValue({ started: false });
|
||||
let read = false;
|
||||
await expect(files.seedFiles((async function* () { read = true; yield { path: "unused", offset: 0, bytes: Buffer.alloc(0) }; })())).resolves.toEqual({ seeded: false });
|
||||
expect(read).toBe(false);
|
||||
});
|
||||
it("realizes probe cwd before readiness without consuming the initial personal-home seed", async () => {
|
||||
const f = fixture();
|
||||
const temp = realpathSync(mkdtempSync(join(tmpdir(), "computer-probe-")));
|
||||
|
||||
@@ -687,9 +687,33 @@ finally:
|
||||
base64: bytes.toString("base64"),
|
||||
expectedSha256,
|
||||
});
|
||||
const seedFiles = async (chunks: AsyncIterable<{ path: string; offset: number; bytes: Buffer }>): Promise<{ seeded: boolean }> => {
|
||||
const seedId = randomUUID();
|
||||
const started = await remote({ action: "seed-begin", seedId });
|
||||
if (!started.started) return { seeded: false };
|
||||
try {
|
||||
for await (const chunk of chunks) {
|
||||
if (chunk.bytes.length > 1024 * 1024) throw new ComputerError("invalid", "Initial home chunk exceeds its size limit");
|
||||
await remote({ action: "seed-chunk", seedId, path: chunk.path, offset: chunk.offset, base64: chunk.bytes.toString("base64") });
|
||||
}
|
||||
return await remote({ action: "seed-commit", seedId });
|
||||
} catch (error) {
|
||||
await remote({ action: "seed-abort", seedId }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const seedBytes = async (files: Record<string, Buffer>): Promise<{ seeded: boolean }> => seedFiles((async function* () {
|
||||
for (const [path, bytes] of Object.entries(files)) {
|
||||
for (let offset = 0; offset < Math.max(1, bytes.length); offset += 1024 * 1024) {
|
||||
yield { path, offset, bytes: bytes.subarray(offset, offset + 1024 * 1024) };
|
||||
}
|
||||
}
|
||||
})());
|
||||
return {
|
||||
root,
|
||||
readBytes,
|
||||
seedFiles,
|
||||
seedBytes,
|
||||
writeBytes,
|
||||
async read(path: string) {
|
||||
const value = await readBytes(path);
|
||||
@@ -729,29 +753,8 @@ finally:
|
||||
): Promise<{ sha256: string }> {
|
||||
return remote({ action: "move", path: from, to, expectedSha256 });
|
||||
},
|
||||
async seedBytes(
|
||||
files: Record<string, Buffer>,
|
||||
): Promise<{ seeded: boolean }> {
|
||||
return remote({
|
||||
action: "seed",
|
||||
files: Object.fromEntries(
|
||||
Object.entries(files).map(([key, value]) => [
|
||||
key,
|
||||
value.toString("base64"),
|
||||
]),
|
||||
),
|
||||
});
|
||||
},
|
||||
async seed(files: Record<string, string>): Promise<{ seeded: boolean }> {
|
||||
return remote({
|
||||
action: "seed",
|
||||
files: Object.fromEntries(
|
||||
Object.entries(files).map(([key, value]) => [
|
||||
key,
|
||||
Buffer.from(value).toString("base64"),
|
||||
]),
|
||||
),
|
||||
});
|
||||
return seedBytes(Object.fromEntries(Object.entries(files).map(([path, content]) => [path, Buffer.from(content)])));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user