diff --git a/server/src/modules/computers/application/service.test.ts b/server/src/modules/computers/application/service.test.ts index c438ae50643..b61bb3ea461 100644 --- a/server/src/modules/computers/application/service.test.ts +++ b/server/src/modules/computers/application/service.test.ts @@ -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(); diff --git a/server/src/modules/computers/application/service.ts b/server/src/modules/computers/application/service.ts index 70a59657f0b..65aaf69dbc9 100644 --- a/server/src/modules/computers/application/service.ts +++ b/server/src/modules/computers/application/service.ts @@ -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")