mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-19 10:36:26 +02:00
fix(computers): delete large agent files without downloading
Hash remote files through confined descriptors in bounded chunks before editor mutations. Preserve the remote compare-and-swap removal check and read-size cap; cover a real 17 MiB file, stale deletion, and path confinement. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
03542b0845
commit
ec80163f2f
6 files changed
+73
-4
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
writeFileSync,
|
||||
mkdirSync,
|
||||
} from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
@@ -170,6 +171,25 @@ describe("confined computer files", () => {
|
||||
error: "invalid",
|
||||
});
|
||||
});
|
||||
it("hashes and deletes files over the read limit while fencing changed bytes", () => {
|
||||
const f = fixture();
|
||||
f.call({ action: "seed", files: {} });
|
||||
const bytes = Buffer.alloc(17 * 1024 * 1024, 7);
|
||||
writeFileSync(join(f.root, "large.bin"), bytes);
|
||||
const expected = createHash("sha256").update(bytes).digest("hex");
|
||||
expect(f.call({ action: "read", path: "large.bin" })).toEqual({ error: "invalid" });
|
||||
expect(f.call({ action: "hash", path: "large.bin" })).toEqual({ sha256: expected, size: bytes.length });
|
||||
bytes[0] = 8;
|
||||
writeFileSync(join(f.root, "large.bin"), bytes);
|
||||
expect(f.call({ action: "remove", path: "large.bin", expectedSha256: expected })).toEqual({ error: "conflict" });
|
||||
const current = f.call({ action: "hash", path: "large.bin" });
|
||||
expect(current.sha256).not.toBe(expected);
|
||||
expect(f.call({ action: "remove", path: "large.bin", expectedSha256: current.sha256 })).toEqual({});
|
||||
expect(f.call({ action: "hash", path: "large.bin" })).toEqual({ error: "not_found" });
|
||||
symlinkSync(join(f.temp, "outside"), join(f.root, "escape"));
|
||||
expect(f.call({ action: "hash", path: "escape" })).toEqual({ error: "invalid" });
|
||||
expect(f.call({ action: "hash", path: "../outside" })).toEqual({ error: "invalid" });
|
||||
});
|
||||
it("rejects stale edits and uses no-clobber move semantics", () => {
|
||||
const f = fixture();
|
||||
f.call({ action: "seed", files: {} });
|
||||
|
||||
@@ -143,6 +143,18 @@ try:
|
||||
finally:os.close(parentfd)
|
||||
if not stat.S_ISDIR(info.st_mode) and not stat.S_ISREG(info.st_mode):fail('invalid')
|
||||
print(json.dumps({'name':name,'kind':'directory' if stat.S_ISDIR(info.st_mode) else 'file','size':info.st_size,'mtimeMs':info.st_mtime*1000}))
|
||||
elif act=='hash':
|
||||
parentfd,name=parent(path)
|
||||
try:fd=os.open(name,os.O_RDONLY|os.O_NOFOLLOW,dir_fd=parentfd)
|
||||
finally:os.close(parentfd)
|
||||
with os.fdopen(fd,'rb') as f:
|
||||
if not stat.S_ISREG(os.fstat(f.fileno()).st_mode):fail('invalid')
|
||||
h=hashlib.sha256();size=0
|
||||
while True:
|
||||
block=f.read(65536)
|
||||
if not block:break
|
||||
h.update(block);size+=len(block)
|
||||
print(json.dumps({'sha256':h.hexdigest(),'size':size}))
|
||||
elif act=='read':
|
||||
parentfd,name=parent(path)
|
||||
try:fd=os.open(name,os.O_RDONLY|os.O_NOFOLLOW,dir_fd=parentfd)
|
||||
|
||||
@@ -702,6 +702,9 @@ finally:
|
||||
async stat(path: string): Promise<{ name: string; kind: "file" | "directory"; size: number; mtimeMs: number }> {
|
||||
return remote({ action: "stat", path });
|
||||
},
|
||||
async hash(path: string): Promise<{ sha256: string; size: number }> {
|
||||
return remote({ action: "hash", path });
|
||||
},
|
||||
async remove(path: string, expectedSha256: string) {
|
||||
await remote({ action: "remove", path, expectedSha256 });
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user