fix(computers): allocate listeners outside guest ephemeral ports

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 12:27:30 -05:00
1 parent 66028796f3
commit cfa2b81539
6 files changed
+112 -4

No files matched your search

@@ -4,6 +4,7 @@ import type {
Ledger,
Owner,
ProcessClaim,
RunnerPortAvailability,
} from "../domain/ledger.js";
export interface ComputerRepository {
create(record: ComputerRecord): Promise<void>;
@@ -31,6 +32,7 @@ export interface ComputerBackend {
ready(record: ComputerRecord): Promise<void>;
claim(record: ComputerRecord): Promise<void>;
advance(record: ComputerRecord, owner: Owner): Promise<void>;
runnerPorts(record: ComputerRecord): Promise<RunnerPortAvailability>;
runner(record: ComputerRecord, options?: { control?: boolean }): Promise<CommandManagedRuntimeRunner>;
launch(
record: ComputerRecord,
@@ -50,6 +50,7 @@ function fixture() {
}),
claim: vi.fn(async () => {}),
advance: vi.fn(async () => {}),
runnerPorts: vi.fn(async () => ({ ephemeralStart: 32768, ephemeralEnd: 60999, occupied: [] })),
runner: vi.fn(async () => ({ execute: vi.fn() })),
launch: vi.fn(async (_record, owner) => ({
...owner.process!,
@@ -114,6 +115,43 @@ function fixture() {
};
}
describe("computer ownership", () => {
it("allocates outside guest ephemeral and occupied ports while reserving live owners", async () => {
const f = fixture();
await f.attach();
vi.mocked(f.backend.runnerPorts).mockResolvedValue({
ephemeralStart: 16127, ephemeralEnd: 16128, occupied: [16129, 43130],
});
const first = await f.admit();
const second = await f.admit("other-agent", "other-session");
expect(first.listenerPort).toBe(16130);
expect(second.listenerPort).toBe(16131);
expect(f.backend.advance).toHaveBeenLastCalledWith(expect.anything(), expect.objectContaining({ port: 16131 }));
for (const port of [first.listenerPort, 43130]) {
await expect(f.service.preview({ ...f.scope, owner: first.owner, port })).rejects.toMatchObject({ code: "forbidden" });
}
});
it("fails closed when the guest reserves the entire fixed listener range", async () => {
const f = fixture();
await f.attach();
vi.mocked(f.backend.runnerPorts).mockResolvedValue({ ephemeralStart: 1024, ephemeralEnd: 65535, occupied: [] });
await expect(f.admit()).rejects.toMatchObject({ code: "conflict", message: "Computer has no available runner ports" });
expect(f.backend.advance).not.toHaveBeenCalled();
});
it("preserves historical warm listener identity without reallocating an occupied own port", async () => {
const f = fixture();
await f.attach();
const first = await f.admit();
await first.launch({ command: "runnerd" });
await f.repository.update(f.scope, record => { record.ledger.owners.find(owner => owner.id === first.owner.ownerId)!.port = 43130; });
await f.service.retainWarm({ ...f.scope, owner: first.owner, idleTimeoutMs: 60_000 });
vi.mocked(f.backend.runnerPorts).mockClear();
const next = await f.admit();
expect(next.listenerPort).toBe(43130);
expect(f.backend.runnerPorts).not.toHaveBeenCalled();
});
it("passes checkout credentials transiently without retaining them in computer state", async () => {
const f = fixture();
await f.attach();
@@ -7,6 +7,7 @@ import {
expired,
liveOwners,
nextPort,
isRunnerPort,
segment,
timeout,
type ComputerRecord,
@@ -455,6 +456,18 @@ finally:
});
await backend.ready(result.record);
await backend.claim(result.record);
if (result.owner.generation === 1 && !result.owner.process) {
const availability = await backend.runnerPorts(result.record);
const allocated = await repository.update(input, (record) => {
const owner = exactOwner(record, ref(result.record, result.owner));
if (owner.phase !== "starting")
throw new ComputerError("conflict", "Computer admission was retired");
owner.port = nextPort(record.ledger.owners.filter((other) => other.id !== owner.id), availability);
return { record: structuredClone(record), owner: structuredClone(owner) };
});
result.record = allocated.record;
result.owner = allocated.owner;
}
await backend.advance(result.record, result.owner);
await backend.renew(result.record);
if (input.probeId) {
@@ -661,7 +674,7 @@ finally:
expired(owner, now())
)
throw new ComputerError("conflict", "Preview owner is not live");
if (input.port >= 43127 && input.port < 44127)
if (isRunnerPort(input.port))
throw new ComputerError(
"forbidden",
"Runner ports are not application previews",