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:
DottaandPaperclip committed 2026-10-10 11:00:36 -05:00
1 parent 421f7f0600
commit cebfa8b09f
8 files changed
+204 -32

No files matched your search

@@ -78,11 +78,15 @@ describe("persistent agent directories", () => {
it("seeds a persistent execution home before a run or instruction-copy receipt exists", async () => {
const remoteRoot = "/home/user/paperclip/test/agents/target";
let exists = false;
const seedBytes = vi.fn(async () => { exists = true; });
const received: Record<string, Buffer> = {};
const seedFiles = vi.fn(async (chunks: AsyncIterable<{ path: string; bytes: Buffer }>) => {
for await (const chunk of chunks) received[chunk.path] = Buffer.concat([received[chunk.path] ?? Buffer.alloc(0), chunk.bytes]);
exists = true;
});
const remote = { root: remoteRoot, listPage: async () => {
if (!exists) throw { code: "not_found" };
return { entries: [], truncated: false };
}, seedBytes };
}, seedFiles };
const lookup = vi.spyOn(persistentFiles, "persistentAgentFiles").mockResolvedValue(remote as never);
const environmentId = randomUUID();
const executionTarget = { kind: "remote", transport: "computer", environmentId,
@@ -91,14 +95,14 @@ describe("persistent agent directories", () => {
await fs.writeFile(path.join(root, "memory.bin"), Buffer.from([0, 255, 7]));
expect(await preparePersistentAgentExecutionHome(db, { companyId, agentId, target: executionTarget })).toBe(remoteRoot);
expect(lookup).toHaveBeenCalledWith(db, companyId, agentId, environmentId);
expect(seedBytes).toHaveBeenCalledOnce();
expect(seedBytes).toHaveBeenCalledWith(expect.objectContaining({
expect(seedFiles).toHaveBeenCalledOnce();
expect(received).toEqual(expect.objectContaining({
[entryFile]: Buffer.from(initial), "memory.bin": Buffer.from([0, 255, 7]),
}));
// Subsequent setup adopts the remote tree, regardless of controller edits.
await fs.writeFile(path.join(root, "memory.bin"), "stale controller bytes");
await preparePersistentAgentExecutionHome(db, { companyId, agentId, target: executionTarget });
expect(seedBytes).toHaveBeenCalledOnce();
expect(seedFiles).toHaveBeenCalledOnce();
expect(await db.select().from(agentInstructionWorkingCopies).where(eq(agentInstructionWorkingCopies.agentId, agentId))).toEqual([]);
} finally { lookup.mockRestore(); }
});
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as fileStore from "../services/agent-file-store.js";
import * as persistentFiles from "../services/persistent-agent-files.js";
import { agentInstructionsService } from "../services/agent-instructions.js";
@@ -82,6 +83,26 @@ describe("agent instructions service", () => {
expect(db.transaction).not.toHaveBeenCalled();
});
it("excludes only home-level runtime files from remote listing and export", async () => {
const agent = makeAgent({ instructionsBundleMode: "managed" });
const list = vi.fn(async (relative = "") => {
if (relative === ".paperclip-runtime") throw new Error("Must not enumerate provider packs or session state");
if (!relative) return [{ name: "AGENTS.md", kind: "file", size: 12 }, { name: ".paperclip-runtime", kind: "directory", size: 0 }, { name: "notes", kind: "directory", size: 0 }];
if (relative === "notes") return [{ name: ".paperclip-runtime", kind: "directory", size: 0 }];
return [{ name: "user.md", kind: "file", size: 12 }];
});
const readBytes = vi.fn(async () => ({ bytes: Buffer.from("instructions"), sha256: "hash" }));
vi.spyOn(persistentFiles, "persistentAgentFiles").mockResolvedValue({ root: "/remote/home", list, readBytes } as never);
vi.spyOn(persistentFiles, "seedPersistentAgentHome").mockResolvedValue(undefined);
vi.spyOn(fileStore, "adoptAgentFiles").mockResolvedValue("/controller/home");
const db = { transaction: async (fn: (tx: unknown) => Promise<unknown>) => fn({ select: () => ({ from: () => ({ where: async () => [agent] }) }) }) };
const svc = agentInstructionsService(db as never);
expect((await svc.getBundle(agent)).files.map(file => file.path)).toEqual(["AGENTS.md", "notes/.paperclip-runtime/user.md"]);
expect((await svc.exportFiles(agent)).files).toEqual({ "AGENTS.md": "instructions", "notes/.paperclip-runtime/user.md": "instructions" });
expect(list).not.toHaveBeenCalledWith(".paperclip-runtime");
expect(readBytes).not.toHaveBeenCalledWith(expect.stringMatching(/^\.paperclip-runtime\//));
});
it("previews an explicit external-to-managed migration before adopting remote personal files", async () => {
const root = await makeTempDir("boat-instructions-migration-"); cleanupDirs.add(root);
await fs.writeFile(path.join(root, "AGENTS.md"), "external migration source");