mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 21:07:40 +01:00
Allow bounded Boat connection probes before saving an agent
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
26d404cbfe
commit
8242d89916
3 files changed
+50
-14
No files matched your search
@@ -621,6 +621,35 @@ with open(output,'wb') as out,open(error,'wb') as err:
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("admits an unsaved-agent probe in a bounded company-scoped temporary home", async () => {
|
||||
const f = fixture();
|
||||
await f.attach();
|
||||
const probe = await f.service.admitProbe({ ...f.scope, probeId: "unsaved", idleTimeoutMs: 60_000 });
|
||||
const record = await f.repository.get(f.scope);
|
||||
const owner = record.ledger.owners.find((entry) => entry.id === probe.owner.ownerId)!;
|
||||
expect(owner.agentId).toBeUndefined();
|
||||
expect(owner.runId).toBeUndefined();
|
||||
expect(owner.probeId).toBe("unsaved");
|
||||
expect(probe.remoteCwd).toBe(`/home/user/paperclip/${f.scope.companyId}/probes/unsaved`);
|
||||
expect(probe.agentHome).toBe(probe.remoteCwd);
|
||||
expect(Object.keys(record.ledger.placements)).toEqual(["probe-unsaved"]);
|
||||
await expect(f.service.realizeWorkspace({ ...f.scope, owner: probe.owner, mode: "shared" }))
|
||||
.resolves.toMatchObject({ remoteCwd: probe.remoteCwd, agentHome: probe.agentHome });
|
||||
await expect(f.service.realizeWorkspace({ ...f.scope, owner: probe.owner, mode: "shared", projectId: "project" }))
|
||||
.rejects.toMatchObject({ code: "conflict" });
|
||||
await expect(f.service.admitProbe({ ...f.scope, companyId: "foreign", probeId: "other", idleTimeoutMs: 60_000 }))
|
||||
.rejects.toMatchObject({ code: "not_found" });
|
||||
f.advance(60_001);
|
||||
await f.service.reconcile();
|
||||
expect(f.repository.runState).not.toHaveBeenCalled();
|
||||
expect(f.backend.retire).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("still requires an agent for a real heartbeat admission", async () => {
|
||||
const f = fixture();
|
||||
await f.attach();
|
||||
await expect(f.service.admit({ ...f.scope, runId: "run", sessionKey: "session", idleTimeoutMs: 60_000 }))
|
||||
.rejects.toMatchObject({ code: "invalid" });
|
||||
});
|
||||
it("gives harness probes a finite owner without fabricating a heartbeat run", async () => {
|
||||
const f = fixture();
|
||||
await f.attach();
|
||||
|
||||
@@ -358,14 +358,16 @@ finally:
|
||||
}
|
||||
async function admit(
|
||||
input: Scope & {
|
||||
agentId: string;
|
||||
agentId?: string;
|
||||
runId?: string;
|
||||
probeId?: string;
|
||||
sessionKey: string;
|
||||
idleTimeoutMs: number;
|
||||
},
|
||||
) {
|
||||
segment(input.agentId);
|
||||
if (input.agentId) segment(input.agentId);
|
||||
if (!input.agentId && !input.probeId)
|
||||
throw new ComputerError("invalid", "A computer run requires an agent");
|
||||
if ((!input.runId && !input.probeId) || (input.runId && input.probeId))
|
||||
throw new ComputerError(
|
||||
"invalid",
|
||||
@@ -414,17 +416,22 @@ finally:
|
||||
};
|
||||
record.ledger.owners.push(owner);
|
||||
}
|
||||
record.ledger.placements[input.agentId] ??= {
|
||||
id: randomUUID(),
|
||||
root: `${base(record)}/agents/${input.agentId}`,
|
||||
cwd: `${base(record)}/agents/${input.agentId}`,
|
||||
};
|
||||
const agentHome = input.agentId
|
||||
? `${base(record)}/agents/${input.agentId}`
|
||||
: `${base(record)}/probes/${input.probeId}`;
|
||||
if (input.agentId) {
|
||||
record.ledger.placements[input.agentId] ??= {
|
||||
id: randomUUID(),
|
||||
root: agentHome,
|
||||
cwd: agentHome,
|
||||
};
|
||||
}
|
||||
record.ledger.placements[input.sessionKey] ??= {
|
||||
id: randomUUID(),
|
||||
root: `${base(record)}/agents/${input.agentId}`,
|
||||
root: agentHome,
|
||||
cwd: input.probeId
|
||||
? `${base(record)}/probes/${input.probeId}`
|
||||
: `${base(record)}/agents/${input.agentId}`,
|
||||
: agentHome,
|
||||
};
|
||||
return { record: structuredClone(record), owner: structuredClone(owner) };
|
||||
});
|
||||
@@ -795,7 +802,7 @@ finally:
|
||||
},
|
||||
) {
|
||||
const { record, owner } = await scoped(input);
|
||||
if (!owner.agentId || owner.phase !== "active")
|
||||
if (owner.phase !== "active" || (!owner.agentId && input.projectId))
|
||||
throw new ComputerError("conflict", "Workspace owner is not active");
|
||||
const placement = record.ledger.placements[owner.sessionKey!]!;
|
||||
if (!input.projectId)
|
||||
@@ -976,7 +983,7 @@ finally:
|
||||
admit,
|
||||
admitProbe(
|
||||
input: Scope & {
|
||||
agentId: string;
|
||||
agentId?: string;
|
||||
probeId: string;
|
||||
idleTimeoutMs: number;
|
||||
},
|
||||
|
||||
@@ -45,13 +45,13 @@ export function createComputerEnvironmentDriver(db: Db): EnvironmentRuntimeDrive
|
||||
driver: "computer",
|
||||
async acquireRunLease(input) {
|
||||
if (!(await instanceSettingsService(db).getExperimental()).enableBoatEnvironments) throw new Error("boat_environments_disabled");
|
||||
if (!input.agentId) throw new Error("computer_agent_required");
|
||||
if (input.heartbeatRunId && !input.agentId) throw new Error("computer_agent_required");
|
||||
const idleTimeoutMs = resolvePaperclipRunnerIdleTimeoutMs(input.environment.config.runnerIdleTimeoutMs);
|
||||
const binding = input.heartbeatRunId ? await computers.admit({ companyId: input.companyId, environmentId: input.environment.id,
|
||||
agentId: input.agentId, runId: input.heartbeatRunId, idleTimeoutMs,
|
||||
agentId: input.agentId ?? undefined, runId: input.heartbeatRunId, idleTimeoutMs,
|
||||
sessionKey: createHash("sha256").update(JSON.stringify([input.agentId, input.executionWorkspaceId ?? input.issueId, input.adapterType, input.executionConfigurationKey])).digest("hex") })
|
||||
: await computers.admitProbe({ companyId: input.companyId, environmentId: input.environment.id,
|
||||
agentId: input.agentId, probeId: randomUUID(), idleTimeoutMs });
|
||||
agentId: input.agentId ?? undefined, probeId: randomUUID(), idleTimeoutMs });
|
||||
try {
|
||||
// The durable owner advanced generation; prior retained bookkeeping no
|
||||
// longer owns the process and must not be reaped as an allocation.
|
||||
|
||||
Reference in new issue
Block a user