Merge branch 'codex/boat-environment-core' into codex/boat-environment

* codex/boat-environment-core:
  test: keep dev-server guidance acceptance in stacked PR
  fix: validate persisted warm origin run identity
  fix(computers): allocate listeners outside guest ephemeral ports
  fix: explain detached dev servers on persistent computers
This commit is contained in:
Dotta committed 2026-10-10 12:28:24 -05:00
commit e398e3543d
9 files changed
+124 -8

No files matched your search

@@ -3443,6 +3443,11 @@ export function trustedRuntimeReadOnlyRoots(
return [...roots];
}
function withComputerProcessInstructions(instructions: string, persistentAgentHome?: string): string {
if (!persistentAgentHome) return instructions;
return `${instructions}\n\nComputer runtime: files in AGENT_HOME persist across turns. A dev server needed between warm turns must survive the shell tool's process-group cleanup: use a detached session, for example \`nohup setsid <command> </dev/null >dev-server.log 2>&1 &\`, and verify its listener from a separate command after the launching command returns. Detached processes still belong to this runner and stop when its warm timeout expires. Do not create services or change runner ownership to keep them alive.`;
}
export function createRunnerdCodexAppServerArgs(input: {
environment: NodeJS.ProcessEnv | undefined;
codexHome: string;
@@ -4826,9 +4831,9 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
const includeCodexCollaborationInstructions =
provider === "codex" &&
record(params.config).include_collaboration_mode_instructions !== false;
const unboundBaseInstructions = String(
const unboundBaseInstructions = withComputerProcessInstructions(String(
params.developerInstructions ?? params.baseInstructions ?? "You are a Paperclip agent.",
);
), this.options.persistentAgentHome);
const baseInstructions =
sourceRuntimeContext && runtimeContext
? unboundBaseInstructions.replaceAll(
@@ -5445,7 +5450,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
runtimeContext,
this.options.baseInstructions === undefined
? undefined
: { text: this.options.baseInstructions, context: sourceRuntimeContext },
: { text: withComputerProcessInstructions(this.options.baseInstructions, this.options.persistentAgentHome), context: sourceRuntimeContext },
);
if (provider === "codex") {
// These controller-owned, token-free paths belong to the new run.
@@ -16,7 +16,7 @@ import { join } from "node:path";
import { spawn, spawnSync } from "node:child_process";
import { remoteProgram } from "./remote-program.js";
import { afterEach, describe, it, expect, vi } from "vitest";
import { boatBackend, createBoatTransportAdmission, desktopReadinessProgram, processProgram } from "./boat.js";
import { boatBackend, createBoatTransportAdmission, desktopReadinessProgram, processProgram, runnerPortInventoryProgram } from "./boat.js";
import { buildGitAuthInvocation } from "../../../services/git-credentials.js";
import type { ComputerRecord } from "../domain/ledger.js";
const sshFactory = vi.hoisted(() => vi.fn());
@@ -44,6 +44,22 @@ const json = (value: unknown, status = 200, headers?: Record<string, string>) =>
headers: { "content-type": "application/json", ...headers },
});
describe("Boat transport validation", () => {
it("reads outgoing and listening TCP ports across IPv4 and IPv6", () => {
const root = mkdtempSync(join(tmpdir(), "boat-port-inventory-"));
try {
writeFileSync(join(root, "range"), "32768 60999\n");
writeFileSync(join(root, "tcp"), "header\n0: 00000000:3F00 00000000:0000 0A\n");
writeFileSync(join(root, "tcp6"), "header\n0: 00000000000000000000000000000000:A87A 00000000:01BB 08\n");
const program = runnerPortInventoryProgram
.replace("/proc/sys/net/ipv4/ip_local_port_range", join(root, "range"))
.replace("/proc/net/tcp6", join(root, "tcp6"))
.replace("/proc/net/tcp", join(root, "tcp"));
const result = spawnSync("python3", ["-c", program], { encoding: "utf8" });
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({ ephemeralStart: 32768, ephemeralEnd: 60999, occupied: [16128, 43130] });
} finally { rmSync(root, { recursive: true, force: true }); }
});
it("recognizes idle as an already running computer", async () => {
const fetcher = vi.fn(async () =>
json({
@@ -475,6 +475,15 @@ print('{}')
});
}
},
async runnerPorts(record) {
const value = await execute(record, runnerPortInventoryProgram, {});
if (!Number.isInteger(value.ephemeralStart) || !Number.isInteger(value.ephemeralEnd)
|| value.ephemeralStart < 1 || value.ephemeralEnd > 65535
|| value.ephemeralStart > value.ephemeralEnd || !Array.isArray(value.occupied)
|| value.occupied.some((port: unknown) => !Number.isInteger(port) || Number(port) < 1 || Number(port) > 65535))
throw new ComputerError("provider_error", "Computer returned invalid listener port availability");
return value;
},
async advance(record, owner) {
await execute(record, processProgram, { action: "advance", owner });
},
@@ -564,3 +573,17 @@ print('{}')
},
};
}
/** Read-only Linux socket inventory; all states matter, not only LISTEN. */
export const runnerPortInventoryProgram = String.raw`
import json
with open('/proc/sys/net/ipv4/ip_local_port_range') as source:
start,end=map(int,source.read().split())
occupied=set()
for path in ['/proc/net/tcp','/proc/net/tcp6']:
with open(path) as source:
for line in source.readlines()[1:]:
port=int(line.split()[1].rsplit(':',1)[1],16)
if port:occupied.add(port)
print(json.dumps({'ephemeralStart':start,'ephemeralEnd':end,'occupied':sorted(occupied)}))
`;
@@ -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",
+18 -2
View File
@@ -87,12 +87,28 @@ export function exactOwner(record: ComputerRecord, ref: OwnerRef): Owner {
throw new ComputerError("conflict", "Computer owner has been superseded");
return owner;
}
export function nextPort(owners: Owner[]): number {
export interface RunnerPortAvailability {
ephemeralStart: number;
ephemeralEnd: number;
occupied: number[];
}
// Keep fixed listeners below the usual Linux ephemeral range, and still check
// the actual guest policy: custom images can configure a different range.
export const RUNNER_PORT_START = 16127;
export const RUNNER_PORT_END = 17127;
export function isRunnerPort(port: number): boolean {
return (port >= RUNNER_PORT_START && port < RUNNER_PORT_END)
|| (port >= 43127 && port < 44127); // Existing warm owners retain their port.
}
export function nextPort(owners: Owner[], availability?: RunnerPortAvailability): number {
const occupied = new Set(
owners.filter((o) => o.phase !== "retired").map((o) => o.port),
);
for (let port = 43127; port < 44127; port++)
for (const port of availability?.occupied ?? []) occupied.add(port);
for (let port = RUNNER_PORT_START; port < RUNNER_PORT_END; port++) {
if (availability && port >= availability.ephemeralStart && port <= availability.ephemeralEnd) continue;
if (!occupied.has(port)) return port;
}
throw new ComputerError("conflict", "Computer has no available runner ports");
}
export function liveOwners(ledger: Ledger): Owner[] {
@@ -13459,6 +13459,7 @@ describe("runnerd provider runtime wiring", () => {
{ condition: "workspace-coordinate", expected: "verified" },
{ condition: "warm-origin", expected: "verified" },
{ condition: "origin-other-session", expected: "scope_mismatch" },
{ condition: "origin-wrong-profile-run", expected: "scope_mismatch" },
{ condition: "origin-other-owner", expected: "scope_mismatch" },
{ condition: "origin-newer-generation", expected: "scope_mismatch" },
{ condition: "origin-active", expected: "scope_mismatch" },
@@ -13487,7 +13488,7 @@ describe("runnerd provider runtime wiring", () => {
const originDescriptor = { ...descriptor, leaseId: "origin-lease",
providerLeaseId: condition === "origin-other-owner" ? "other-owner" : owner.ownerId,
ownerGeneration: condition === "origin-newer-generation" ? 3 : 1 };
const originRow = { status: condition === "origin-active" ? "running" : "succeeded",
const originRow = { id: condition === "origin-wrong-profile-run" ? "other-run" : originExecution.binding.runId, status: condition === "origin-active" ? "running" : "succeeded",
runnerProfileJson: { nativeExecutionInput: originExecution, nativeComputerWorkspace: originDescriptor } };
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 (limit: number) =>
@@ -4397,6 +4397,7 @@ export async function verifyPriorRunnerdStateForSessionScope(input: {
// workspace coordinate. Resolve that recorded origin, never an arbitrary
// run ID, before checking the physical lease and latest retirement.
const origins = await input.db.select({
id: heartbeatRuns.id,
status: heartbeatRuns.status,
runnerProfileJson: heartbeatRuns.runnerProfileJson,
}).from(heartbeatRuns).where(and(
@@ -4412,6 +4413,7 @@ export async function verifyPriorRunnerdStateForSessionScope(input: {
const originExecution = parseNativeExecutionInput(originProfile.nativeExecutionInput);
const originWorkspace = readNativeComputerWorkspaceReference(originProfile.nativeComputerWorkspace);
if (!TERMINAL_HEARTBEAT_RUN_STATUSES.has(origin.status) ||
originExecution.binding.runId !== origin.id ||
originExecution.binding.executionWorkspaceId !== input.identity.environmentLeaseId ||
nativeSessionScopeKey(originExecution) !== nativeSessionScopeKey(priorExecution) ||
!originWorkspace || originWorkspace.remoteCwd !== priorWorkspace.remoteCwd ||