Refresh verified provider launch leases after cold model selection

This commit is contained in:
Dotta 2026-09-11 14:44:56 -05:00
parent 6cd4126108
commit 6dc0efd6db
5 changed files with 211 additions and 0 deletions

View File

@ -13,6 +13,7 @@ import { describe, expect, it, vi } from "vitest";
import type { VerifiedAcpxCommandLease } from "./installation-integrity.js";
import { openCodexAcpxRuntime } from "./codex-runtime-adapter.js";
import { createAcpxCommandLeaseOwner } from "./command-lease-owner.js";
import { resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
import type { AcpxRuntimePortOpenOptions } from "./runtime-host.js";
@ -1367,6 +1368,60 @@ describe("Codex ACPX runtime adapter", () => {
await port.close({ reason: "test complete" });
});
it("uses a fresh single-use command after a cold model control consumes its launch", async () => {
const runtime = fakeRuntime();
const freshCommand = () => {
const command = fakeCommand();
vi.mocked(command.spawn).mockReturnValueOnce(fakeChild()).mockImplementation(() => {
throw new Error("Verified ACPX command lease is closed");
});
return command;
};
const first = freshCommand();
const second = freshCommand();
const openCommand = vi.fn(async () => second);
const owner = createAcpxCommandLeaseOwner(first, openCommand);
let runtimeOptions: AcpRuntimeOptions;
vi.mocked(runtime.setConfigOption!).mockImplementation(async () => {
runtimeOptions.spawnAgent!({ command: "ignored", args: [], options: {} });
});
vi.mocked(runtime.startTurn).mockImplementation(() => {
runtimeOptions.spawnAgent!({ command: "ignored", args: [], options: {} });
return {
requestId: "cold-turn",
promptStarted: Promise.resolve(),
events: { async *[Symbol.asyncIterator]() {} },
result: Promise.resolve({ status: "completed" }),
cancel: vi.fn(),
closeStream: vi.fn(),
};
});
const port = await openCodexAcpxRuntime(
{
...openOptions(owner.command),
refreshConsumedCommand: owner.refreshConsumedCommand,
},
{
createRegistry: () => registry(),
createStore: () => store(),
awaitProviderOwnership: providerOwnershipEstablished,
awaitProviderExit: providerOwnershipEstablished,
createRuntime: (options) => {
runtimeOptions = options;
return runtime;
},
},
);
await port.setModel!("gpt-5.6-sol");
expect(openCommand).toHaveBeenCalledOnce();
const turn = port.startTurn({ text: "Resume", requestId: "cold-turn" });
await expect(turn.result).resolves.toMatchObject({ status: "completed" });
expect(first.spawn).toHaveBeenCalledOnce();
expect(second.spawn).toHaveBeenCalledOnce();
await port.close({ reason: "test complete" });
await owner.command.close();
});
it("admits a verified provider that starts with the first recovered turn", async () => {
const runtime = fakeRuntime();
const child = fakeChild();

View File

@ -265,6 +265,7 @@ export async function openQualifiedAcpxRuntime(
update.goal === null ? null : structuredClone(update.goal),
);
};
const commandLaunches = { count: 0, refreshConsumedCommand: options.refreshConsumedCommand };
const runtimeOptions: GoalAwareAcpRuntimeOptions = {
cwd: options.cwd,
sessionStore,
@ -327,6 +328,7 @@ export async function openQualifiedAcpxRuntime(
// handshake cannot create a provider process after authority is gone.
options.signal?.throwIfAborted();
options.assertWorkspaceHeld?.();
commandLaunches.count += 1;
return children.add(
options.command.spawn(input.args, input.options, {
credentialFenceFds,
@ -431,6 +433,7 @@ export async function openQualifiedAcpxRuntime(
children,
runtimeCloseTimeoutMs,
goalState,
commandLaunches,
);
} catch (error) {
const cleanupReason = "ACPX runtime identity validation failed";
@ -857,6 +860,7 @@ function runtimePort(
children: SpawnedChildSet,
runtimeCloseTimeoutMs: number,
goalState: AcpxRuntimeGoalState,
commandLaunches: { count: number; refreshConsumedCommand?: () => Promise<void> },
): AcpxRuntimePort {
type RuntimeCloseAttempt = {
readonly outcome: Promise<unknown | null>;
@ -1161,6 +1165,7 @@ function runtimePort(
// only for this control call, and verify ownership before return.
const finishOwnershipAdmission =
children.beginLifetimeOwnershipAdmission();
const spawnsBeforeControl = commandLaunches.count;
try {
await runtime.setConfigOption?.({
handle,
@ -1170,6 +1175,11 @@ function runtimePort(
} finally {
await finishOwnershipAdmission();
}
// Cold ACP config calls open and close a temporary connection.
// A later prompt needs a newly verified single-use launch snapshot.
if (commandLaunches.count > spawnsBeforeControl) {
await commandLaunches.refreshConsumedCommand?.();
}
},
}
: {}),

View File

@ -0,0 +1,79 @@
import type { ChildProcess } from "node:child_process";
import { describe, expect, it, vi } from "vitest";
import { createAcpxCommandLeaseOwner } from "./command-lease-owner.js";
import type { VerifiedAcpxCommandLease } from "./installation-integrity.js";
function lease() {
let consumed = false;
return {
spawn: vi.fn(() => {
if (consumed) throw new Error("single-use command already consumed");
consumed = true;
return {} as ChildProcess;
}),
close: vi.fn(async () => {
consumed = true;
}),
} satisfies VerifiedAcpxCommandLease;
}
describe("ACPX verified command lease owner", () => {
it("refreshes only consumed snapshots and preserves single-use spawn enforcement", async () => {
const first = lease();
const second = lease();
const open = vi.fn(async () => second);
const owner = createAcpxCommandLeaseOwner(first, open);
await owner.refreshConsumedCommand();
expect(open).not.toHaveBeenCalled();
owner.command.spawn();
expect(() => owner.command.spawn()).toThrow("already consumed");
await Promise.all([owner.refreshConsumedCommand(), owner.refreshConsumedCommand()]);
expect(open).toHaveBeenCalledOnce();
owner.command.spawn();
expect(second.spawn).toHaveBeenCalledOnce();
expect(() => owner.command.spawn()).toThrow("already consumed");
await owner.command.close();
expect(first.close).toHaveBeenCalledOnce();
expect(second.close).toHaveBeenCalledOnce();
expect(() => owner.command.spawn()).toThrow("closing");
await expect(owner.refreshConsumedCommand()).rejects.toThrow("closing");
});
it("retains a replacement acquired during shutdown and retries its failed cleanup", async () => {
const first = lease();
const replacement = lease();
replacement.close.mockRejectedValueOnce(new Error("close failed"));
let acquired!: (value: VerifiedAcpxCommandLease) => void;
const owner = createAcpxCommandLeaseOwner(
first,
() => new Promise((resolve) => {
acquired = resolve;
}),
);
owner.command.spawn();
const refresh = owner.refreshConsumedCommand();
const rejectedRefresh = expect(refresh).rejects.toThrow("closed during refresh");
await Promise.resolve();
const close = owner.command.close();
const rejectedClose = expect(close).rejects.toThrow("leases did not close");
acquired(replacement);
await rejectedRefresh;
await rejectedClose;
expect(replacement.spawn).not.toHaveBeenCalled();
await owner.command.close();
expect(replacement.close).toHaveBeenCalledTimes(2);
expect(first.close).toHaveBeenCalledOnce();
});
it("fails closed when fresh command verification fails", async () => {
const initial = lease();
const owner = createAcpxCommandLeaseOwner(initial, async () => {
throw new Error("installation changed");
});
owner.command.spawn();
await expect(owner.refreshConsumedCommand()).rejects.toThrow("installation changed");
expect(() => owner.command.spawn()).toThrow("already consumed");
await owner.command.close();
expect(initial.close).toHaveBeenCalledOnce();
});
});

View File

@ -0,0 +1,58 @@
import type { VerifiedAcpxCommandLease } from "./installation-integrity.js";
/** Keep each launch single-use while owning replacements for transient ACP controls. */
export function createAcpxCommandLeaseOwner(
initial: VerifiedAcpxCommandLease,
openCommand: () => Promise<VerifiedAcpxCommandLease>,
) {
const leases = new Set([initial]);
let current = initial;
let consumed = false;
let closing = false;
let refresh: Promise<void> | null = null;
const command: VerifiedAcpxCommandLease = {
spawn(...args) {
if (closing) throw new Error("Verified ACPX command owner is closing");
consumed = true;
return current.spawn(...args);
},
async close() {
closing = true;
// Late acquisitions remain owned. Retry every lease whose close fails.
await refresh?.catch(() => undefined);
const failures: unknown[] = [];
for (const lease of leases) {
try {
await lease.close();
leases.delete(lease);
} catch (error) {
failures.push(error);
}
}
if (failures.length) throw new AggregateError(failures, "ACPX command leases did not close");
},
};
return {
command,
async refreshConsumedCommand(): Promise<void> {
if (closing) throw new Error("Verified ACPX command owner is closing");
if (!consumed) return;
if (!refresh) {
refresh = Promise.resolve()
.then(openCommand)
.then((replacement) => {
leases.add(replacement);
if (closing) throw new Error("Verified ACPX command owner closed during refresh");
current = replacement;
consumed = false;
});
}
const pending = refresh;
try {
await pending;
} finally {
if (refresh === pending) refresh = null;
}
},
};
}

View File

@ -20,6 +20,7 @@ import {
type VerifiedAcpxCommandLease,
type VerifiedAcpxInstallation,
} from "./installation-integrity.js";
import { createAcpxCommandLeaseOwner } from "./command-lease-owner.js";
import {
requireVerifiedAcpxModel,
type AcpxModelStatus,
@ -117,6 +118,8 @@ export interface AcpxRuntimePort {
export interface AcpxRuntimePortOpenOptions {
command: VerifiedAcpxCommandLease;
/** Replace a consumed launch snapshot after an ephemeral control session. */
refreshConsumedCommand?: () => Promise<void>;
profile: QualifiedAcpxProfile;
cwd: string;
stateDirectory: string;
@ -401,6 +404,11 @@ export class AcpxRuntimeHost {
reportFailure: (failure) =>
dependencies.reportRetainedCleanupFailure(failure),
});
const commandOwner = createAcpxCommandLeaseOwner(
command,
() => installation.openCommand(),
);
command = commandOwner.command;
toolBridge = options.semanticTools
? await acquireAbortableAdmissionResource({
signal: options.signal,
@ -421,6 +429,7 @@ export class AcpxRuntimeHost {
options.assertWorkspaceHeld?.();
return dependencies.openRuntime({
command: command!,
refreshConsumedCommand: commandOwner.refreshConsumedCommand,
profile,
cwd: binding.workspacePath,
stateDirectory: sandbox.stateDirectory,