From 8242d8991645c86bca8d4cb666d8e69e123a7460 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 11:16:40 -0500 Subject: [PATCH 1/2] Allow bounded Boat connection probes before saving an agent Co-Authored-By: Paperclip --- .../computers/application/service.test.ts | 29 +++++++++++++++++++ .../modules/computers/application/service.ts | 29 ++++++++++++------- .../services/computer-environment-driver.ts | 6 ++-- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/server/src/modules/computers/application/service.test.ts b/server/src/modules/computers/application/service.test.ts index ac41da391d4..3a908f9fdcd 100644 --- a/server/src/modules/computers/application/service.test.ts +++ b/server/src/modules/computers/application/service.test.ts @@ -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(); diff --git a/server/src/modules/computers/application/service.ts b/server/src/modules/computers/application/service.ts index 6ef886c7f5b..1ea48de01a7 100644 --- a/server/src/modules/computers/application/service.ts +++ b/server/src/modules/computers/application/service.ts @@ -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; }, diff --git a/server/src/services/computer-environment-driver.ts b/server/src/services/computer-environment-driver.ts index 9a65627e29f..fd1e0ef8dcd 100644 --- a/server/src/services/computer-environment-driver.ts +++ b/server/src/services/computer-environment-driver.ts @@ -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. From db3a9aecf4eb7b31926dc4baf5f09a049b0f8ec7 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 11:15:40 -0500 Subject: [PATCH 2/2] fix(boat): resolve prior workspace authority to recorded lease Preserve historical PRP workspace coordinates and require the recorded lease run plus exact retired owner before rotating authority. Co-Authored-By: Paperclip --- .../native-session-executor.test.ts | 18 ++++++++++++++---- .../native-runtime/native-session-executor.ts | 18 +++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 3a7fd4f14dd..272976eb92d 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -13445,6 +13445,8 @@ describe("runnerd provider runtime wiring", () => { it.each([ { condition: "retired", expected: "verified" }, + { condition: "workspace-coordinate", expected: "verified" }, + { condition: "wrong-workspace-run", expected: "scope_mismatch" }, { condition: "uncertain", expected: "terminal_state_indeterminate" }, { condition: "stale-generation", expected: "terminal_state_indeterminate" }, { condition: "wrong-placement", expected: "scope_mismatch" }, @@ -13460,20 +13462,23 @@ describe("runnerd provider runtime wiring", () => { const descriptor = { kind: "remote-persistent", leaseId: "prior-lease", providerLeaseId: owner.ownerId, remoteCwd: "/remote/agent", computerId: owner.computerId, ownerGeneration: owner.generation, listenerPort: owner.listenerPort, placementId: "placement" }; - const db = { select: () => ({ from: (table: unknown) => ({ where: () => ({ limit: async () => + let leaseQuery: SQL | undefined; + const workspaceCoordinate = ["workspace-coordinate", "wrong-workspace-run"].includes(condition); + const db = { select: () => ({ from: (table: unknown) => ({ where: (query: SQL) => { if (table === environmentLeases) leaseQuery = query; return ({ limit: async () => table === environmentLeases ? condition === "wrong-lease" ? [] : [{ providerLeaseId: owner.ownerId, + heartbeatRunId: condition === "wrong-workspace-run" ? "unrelated-run" : prior.binding.runId, metadata: { agentId: prior.binding.agentId, computerOwner: { ...owner, generation: 1 } }, }] : [{ status: condition === "active-run" ? "running" : "succeeded", runnerProfileJson: { nativeExecutionInput: prior, nativeComputerWorkspace: descriptor } }], - }) }) }) } as unknown as Db; + }); } }) }) } as unknown as Db; computerRetirement.isRetired.mockReset().mockImplementation(async ({ owner: requested }) => { if (condition === "unavailable") throw new Error("ledger unavailable"); return condition !== "uncertain" && requested.generation === (condition === "stale-generation" ? 3 : 2); }); const input = { db, root, execution: current, identity: { runId: prior.binding.runId, normalizedSessionId: prior.session.normalizedSessionId!, runnerInstanceId: "prior-runner", - environmentLeaseId: condition === "wrong-lease" ? "other-lease" : "initial-physical-lease" }, + environmentLeaseId: workspaceCoordinate ? prior.binding.executionWorkspaceId : condition === "wrong-lease" ? "other-lease" : "initial-physical-lease" }, allowVerifiedBackup: false, remoteRunnerState: true, runnerExecutionTarget: { kind: "remote", transport: "computer", environmentId: "environment", remoteCwd: "/remote/agent", resourceAuthority: { ...owner, ownerId: "successor-owner", generation: 1 }, @@ -13493,12 +13498,17 @@ describe("runnerd provider runtime wiring", () => { expect(computerRetirement.isRetired).not.toHaveBeenCalled(); } expect(await verifyPriorRunnerdStateForSessionScope({ ...input, allowRetainedWarmRunner: false })).toBe(expected); - if (["retired", "uncertain", "stale-generation", "unavailable"].includes(condition)) { + if (["retired", "workspace-coordinate", "uncertain", "stale-generation", "unavailable"].includes(condition)) { expect(computerRetirement.isRetired).toHaveBeenCalledExactlyOnceWith({ companyId: current.binding.companyId, environmentId: "environment", agentId: current.binding.agentId, runId: prior.binding.runId, owner, }); } else expect(computerRetirement.isRetired).not.toHaveBeenCalled(); + if (workspaceCoordinate) { + const query = new PgDialect().sqlToQuery(leaseQuery!); + expect(query.params).toContain(descriptor.leaseId); + expect(query.params).not.toContain(prior.binding.executionWorkspaceId); + } } finally { await rm(root, { recursive: true, force: true }); } }); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 019da7ce561..2129dc862b8 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -4368,20 +4368,28 @@ export async function verifyPriorRunnerdStateForSessionScope(input: { // are authoritative even though no local runner-state.json exists. // This read cannot retire a live owner or accept a newer generation. try { - // Warm turns advance the attempt lease while the physical runner keeps - // its initial PRP lease identity. Verify that original lease belongs to - // this exact owner lineage instead of requiring the latest attempt id. + // PRP historically calls the immutable execution-workspace coordinate + // environmentLeaseId. Computer placement records the actual lease. Only + // this exact prior workspace coordinate may resolve through that record. + // Explicit physical lease identities still preserve their warm lineage. + const workspaceCoordinate = input.identity.environmentLeaseId === + priorExecution.binding.executionWorkspaceId; + const physicalLeaseId = workspaceCoordinate + ? priorWorkspace.leaseId + : input.identity.environmentLeaseId; const [physicalLease] = await input.db.select({ providerLeaseId: environmentLeases.providerLeaseId, + heartbeatRunId: environmentLeases.heartbeatRunId, metadata: environmentLeases.metadata, }).from(environmentLeases).where(and( - eq(environmentLeases.id, input.identity.environmentLeaseId), + eq(environmentLeases.id, physicalLeaseId), eq(environmentLeases.companyId, input.execution.binding.companyId), eq(environmentLeases.environmentId, target.environmentId), )).limit(1); const leaseMetadata = record(physicalLease?.metadata); const leaseOwner = record(leaseMetadata.computerOwner); - if (physicalLease?.providerLeaseId !== priorWorkspace.computerOwner.ownerId || + if ((workspaceCoordinate && physicalLease?.heartbeatRunId !== input.identity.runId) || + physicalLease?.providerLeaseId !== priorWorkspace.computerOwner.ownerId || leaseMetadata.agentId !== input.execution.binding.agentId || leaseOwner.computerId !== priorWorkspace.computerOwner.computerId || leaseOwner.ownerId !== priorWorkspace.computerOwner.ownerId ||