mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 06:42:06 +01:00
Realize isolated computer probe workspace before readiness
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
f9bbb866ea
commit
8b3b221cc6
2 files changed
+74
-1
No files matched your search
@@ -1,4 +1,15 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { remoteProgram } from "../adapters/remote-program.js";
|
||||
import { createComputerService } from "./service.js";
|
||||
import type { ComputerBackend, ComputerRepository } from "./ports.js";
|
||||
import { ComputerError, type ComputerRecord } from "../domain/ledger.js";
|
||||
@@ -329,6 +340,56 @@ describe("computer ownership", () => {
|
||||
await f.service.reconcile();
|
||||
expect(f.backend.stop).toHaveBeenCalledOnce();
|
||||
});
|
||||
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-")));
|
||||
const local = (value: string) =>
|
||||
value.replace("/home/user/paperclip/", `${temp}/`);
|
||||
vi.mocked(f.backend.remote).mockImplementation(async (_record, input) => {
|
||||
const result = spawnSync(
|
||||
"python3",
|
||||
["-c", remoteProgram.replaceAll("/home/user/paperclip/", `${temp}/`)],
|
||||
{
|
||||
input: JSON.stringify({
|
||||
...input,
|
||||
root: local(input.root as string),
|
||||
}),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
if (result.status !== 0) throw new Error(result.stderr);
|
||||
return JSON.parse(result.stdout);
|
||||
});
|
||||
try {
|
||||
await f.attach();
|
||||
const probe = await f.service.admitProbe({
|
||||
...f.scope,
|
||||
agentId: "agent",
|
||||
probeId: "probe",
|
||||
idleTimeoutMs: 60_000,
|
||||
});
|
||||
expect(probe.remoteCwd).not.toBe(probe.agentHome);
|
||||
const ready = spawnSync("sh", ["-c", "pwd"], {
|
||||
cwd: local(probe.remoteCwd),
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(ready.status).toBe(0);
|
||||
expect(ready.stdout.trim()).toBe(local(probe.remoteCwd));
|
||||
expect(existsSync(local(probe.agentHome))).toBe(false);
|
||||
const run = await f.admit();
|
||||
expect(run.remoteCwd).toBe(probe.agentHome);
|
||||
expect(existsSync(local(run.agentHome))).toBe(false);
|
||||
const files = await f.service.files({ ...f.scope, agentId: "agent" });
|
||||
await expect(
|
||||
files.seed({ "AGENTS.md": "initial managed instructions" }),
|
||||
).resolves.toEqual({ seeded: true });
|
||||
expect(
|
||||
readFileSync(join(local(run.agentHome), "AGENTS.md"), "utf8"),
|
||||
).toBe("initial managed instructions");
|
||||
} finally {
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("gives harness probes a finite owner without fabricating a heartbeat run", async () => {
|
||||
const f = fixture();
|
||||
await f.attach();
|
||||
|
||||
@@ -372,7 +372,9 @@ sys.stdout.write(output);sys.stderr.write(error);sys.exit(child.returncode)
|
||||
record.ledger.placements[input.sessionKey] ??= {
|
||||
id: randomUUID(),
|
||||
root: `${base(record)}/agents/${input.agentId}`,
|
||||
cwd: `${base(record)}/agents/${input.agentId}`,
|
||||
cwd: input.probeId
|
||||
? `${base(record)}/probes/${input.probeId}`
|
||||
: `${base(record)}/agents/${input.agentId}`,
|
||||
};
|
||||
return { record: structuredClone(record), owner: structuredClone(owner) };
|
||||
});
|
||||
@@ -380,6 +382,16 @@ sys.stdout.write(output);sys.stderr.write(error);sys.exit(child.returncode)
|
||||
await backend.claim(result.record);
|
||||
await backend.advance(result.record, result.owner);
|
||||
await backend.renew(result.record);
|
||||
if (input.probeId) {
|
||||
// Readiness commands run before managed-home preparation. Give them an
|
||||
// existing workspace without creating the personal home: an absent home
|
||||
// must remain eligible for the first atomic managed-file seed.
|
||||
await backend.remote(result.record, {
|
||||
action: "seed",
|
||||
root: result.record.ledger.placements[input.sessionKey]!.cwd,
|
||||
files: {},
|
||||
});
|
||||
}
|
||||
const active = await repository.update(input, (record) => {
|
||||
const owner = exactOwner(record, ref(result.record, result.owner));
|
||||
if (owner.phase !== "starting")
|
||||
|
||||
Reference in new issue
Block a user