diff --git a/server/src/modules/computers/application/service.test.ts b/server/src/modules/computers/application/service.test.ts index 9d8a55d4fa8..356766d035c 100644 --- a/server/src/modules/computers/application/service.test.ts +++ b/server/src/modules/computers/application/service.test.ts @@ -582,3 +582,29 @@ with open(output,'wb') as out,open(error,'wb') as err: expect(f.backend.retire).toHaveBeenCalledOnce(); }); }); + + +describe("computer retirement proof", () => { + it("proves only the exact retired run without provider actions", async () => { + const f = fixture(); + await f.attach(); + const binding = await f.admit(); + const input = { ...f.scope, owner: binding.owner, agentId: "agent", runId: "run" }; + expect(await f.service.isRetired(input)).toBe(false); + expect(f.backend.retire).not.toHaveBeenCalled(); + await f.service.retire(input); + vi.mocked(f.backend.retire).mockClear(); + vi.mocked(f.backend.inspect).mockClear(); + expect(await f.service.isRetired(input)).toBe(true); + for (const change of [ + { agentId: "other-agent" }, + { runId: "other-run" }, + { owner: { ...binding.owner, computerId: "other-computer" } }, + { owner: { ...binding.owner, ownerId: "other-owner" } }, + { owner: { ...binding.owner, generation: binding.owner.generation + 1 } }, + ]) expect(await f.service.isRetired({ ...input, ...change })).toBe(false); + await expect(f.service.isRetired({ ...input, companyId: "other-company" })).rejects.toMatchObject({ code: "not_found" }); + expect(f.backend.retire).not.toHaveBeenCalled(); + expect(f.backend.inspect).not.toHaveBeenCalled(); + }); +}); diff --git a/server/src/modules/computers/application/service.ts b/server/src/modules/computers/application/service.ts index 0fdf0203557..39945961d4f 100644 --- a/server/src/modules/computers/application/service.ts +++ b/server/src/modules/computers/application/service.ts @@ -484,6 +484,18 @@ finally: ).toISOString(); }); } + async function isRetired(input: Scope & { owner: OwnerRef; agentId: string; runId: string }) { + const record = await repository.get(input); + if (record.id !== input.owner.computerId) return false; + return record.ledger.owners.some((owner) => + owner.id === input.owner.ownerId && + owner.generation === input.owner.generation && + owner.kind === "runner" && + owner.phase === "retired" && + owner.agentId === input.agentId && + owner.runId === input.runId, + ); + } async function retire(input: Scope & { owner: OwnerRef }) { const state = await repository.update(input, (record) => { const owner = record.ledger.owners.find( @@ -956,6 +968,7 @@ finally: }, recover, retainWarm, + isRetired, retire, connect, renewViewer,