feat(runner): compose ACPX runtime admission (#12399)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The ACPX pieces already verify profiles, installations, recovery
identity, permissions, runtime files, credentials, and models
independently.
> - A production host must compose those checks in one fail-closed order
and clean every acquired resource on partial startup.
> - Directly importing a third-party ACP runtime here would mix
dependency adoption with the security lifecycle.
> - This pull request defines a narrow injected runtime port and admits
it only after all package-local boundaries pass.
> - The benefit is a testable host lifecycle without adding `acpx`,
changing the lockfile, or making the adapter selectable.

## Linked Issues or Issue Description

**Agent or provider**

The qualified Pi, Claude, and Codex ACPX profiles; Codex additionally
uses the managed credential lease.

**Why this adapter is useful**

The runner needs one owner for startup ordering, immutable identity
checks, exact model verification, and cleanup. Otherwise a failure after
credential staging or command admission can leave secret files or
executable leases alive, and a resumed provider can attach to a
different profile, workspace, model, or permission mode.

**How the agent is invoked**

A later dependency-adapter pull request will implement the injected
runtime port with the pinned ACPX library. This host passes that adapter
an opaque verified command lease, canonical workspace, private state
directory, profile-bound session key, qualified permission policy,
launch-only environment, and bounded instructions. It does not expose
the runtime directly or add a user-selectable adapter.

**Additional context**

This pull request is stacked on #12398. Installation verification has a
production default; only the third-party runtime opener is injected.
Tests use a fake port so this boundary remains package-local and
dependency-free.

## What Changed

- Add a minimal ACP runtime port for identity, status, model selection,
and bounded shutdown.
- Derive the qualified profile and canonical recovery binding before any
provider startup.
- Reject expected-identity drift and irrelevant managed-Codex inputs
before opening the provider.
- Verify that even an injected installation result matches the closed
profile digest.
- Prepare the private sandbox and stage Codex credentials only for the
Codex profile.
- Acquire an opaque verified command lease and pass only the composed
launch boundary to the runtime port.
- Apply the canonical permission policy and collision-resistant provider
session key.
- Select and verify the exact effective model before returning an
admitted host.
- Create a strict versioned identity record and compare resumed provider
identifiers with the expected record.
- Keep the runtime private and expose only cloned identity, binding,
runtime-root, and persistence-safe environment views.
- On startup or shutdown failure, attempt runtime close, credential
cleanup, and command-lease cleanup in order and aggregate every error.
- Add tests for Codex secret isolation, Claude selector verification,
recovery drift, injected digest drift, partial-start cleanup, and
cleanup retry.

## Verification

- Runner TypeScript typecheck — passed.
- Runner protocol and TypeScript tests — passed: 12 protocol tests and
426 Vitest tests, including 6 runtime-host tests.
- `pnpm -r typecheck` — passed for all applicable workspaces.
- `pnpm build` — passed, including runner binary, server, UI, and
workspace packages.
- Prettier and `git diff --check` — passed.
- The diff contains 2 files and does not change `pnpm-lock.yaml`, a
workflow, a dependency, a public package export, server selection, or UI
behavior.

## Risks

The main risk is leaking a partially admitted resource when a later
admission step fails. Resource acquisition is linear and all failure
paths use the same ordered cleanup routine. The runtime port is
deliberately minimal and privately owned by the host; it cannot bypass
profile, model, recovery, sandbox, credential, or command admission. The
actual ACPX implementation and its process-supervision behavior remain a
separate review unit.

## Model Used

OpenAI Codex with GPT-5 and repository tool use.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either linked an existing public item or described the
issue in this PR
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal task
identifier
- [x] I have run the affected tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have documented the admission and cleanup boundary
- [ ] All applicable GitHub Actions are green
- [ ] Greptile is 5/5 with every actionable comment resolved
- [x] I will address all review findings before requesting merge
This commit is contained in:
Dotta 2026-08-30 17:53:31 -05:00 committed by GitHub
parent dba1a2d4f5
commit d0718c226c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 932 additions and 0 deletions

View File

@ -0,0 +1,489 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type {
VerifiedAcpxCommandLease,
VerifiedAcpxInstallation,
} from "./installation-integrity.js";
import { resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
import {
AcpxRuntimeHost,
type AcpxRuntimeHostDependencies,
type AcpxRuntimePort,
} from "./runtime-host.js";
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { force: true, recursive: true })),
);
});
describe("ACPX runtime host", () => {
it("rejects a pre-aborted admission before acquiring provider resources", async () => {
const fixture = await hostFixture();
const controller = new AbortController();
const cancellation = new Error("admission cancelled before start");
controller.abort(cancellation);
const openRuntime = vi.fn(async () => runtimePort());
const verifyInstallation = vi.fn(
fixture.dependencies({ openRuntime }).verifyInstallation!,
);
await expect(
AcpxRuntimeHost.open(
{
...fixture.options,
agent: "claude",
model: "claude-sonnet-5",
permissionMode: "deny-all",
signal: controller.signal,
},
{
...fixture.dependencies({ openRuntime }),
verifyInstallation,
},
),
).rejects.toBe(cancellation);
expect(verifyInstallation).not.toHaveBeenCalled();
expect(openRuntime).not.toHaveBeenCalled();
expect(fixture.commandClose).not.toHaveBeenCalled();
});
it("composes admission, isolation, model verification, and cleanup", async () => {
const fixture = await hostFixture();
let capturedEnvironment: Readonly<NodeJS.ProcessEnv> = {};
const runtime = runtimePort({
onClose: vi.fn(async () => undefined),
});
const dependencies = fixture.dependencies({
openRuntime: async (options) => {
capturedEnvironment = options.launchEnvironment;
await writeFile(
join(options.launchEnvironment.CODEX_HOME!, "auth.json"),
'{"provider_generated":true}',
);
return runtime;
},
});
const host = await AcpxRuntimeHost.open(
{
...fixture.options,
agent: "codex",
model: "gpt-5.6-sol",
permissionMode: "approve-reads",
environment: {
PATH: process.env.PATH,
OPENAI_API_KEY: "launch-secret",
HTTPS_PROXY: "https://proxy-user:proxy-secret@example.test",
},
systemInstructions: "Use the supplied runtime context.",
},
dependencies,
);
expect(host.identity()).toMatchObject({
schema: "paperclip.runner.acpx-identity.v1",
acpxRecordId: "record-1",
requestedModel: "gpt-5.6-sol",
permissionMode: "approve-reads",
});
expect(capturedEnvironment.OPENAI_API_KEY).toBe("launch-secret");
expect(host.persistedEnvironment().OPENAI_API_KEY).toBeUndefined();
expect(host.persistedEnvironment().HTTPS_PROXY).toBeUndefined();
const authPath = join(host.runtimeRoot(), "codex-home", "auth.json");
await expect(readFile(authPath, "utf8")).resolves.toContain(
"provider_generated",
);
await host.close({ reason: "test complete" });
await expect(readFile(authPath)).rejects.toMatchObject({ code: "ENOENT" });
expect(runtime.close).toHaveBeenCalledOnce();
expect(fixture.commandClose).toHaveBeenCalledOnce();
});
it("selects and verifies Claude's qualified reported model", async () => {
const fixture = await hostFixture();
let selected = false;
const setModel = vi.fn(async (model: string) => {
expect(model).toBe("claude-sonnet-5");
selected = true;
});
const runtime = runtimePort({
getStatus: async () => ({
models: {
currentModelId: selected ? "sonnet" : "default",
availableModelIds: ["default", "sonnet"],
},
}),
setModel,
});
const host = await AcpxRuntimeHost.open(
{
...fixture.options,
agent: "claude",
model: "claude-sonnet-5",
permissionMode: "deny-all",
},
fixture.dependencies({ openRuntime: async () => runtime }),
);
expect(setModel).toHaveBeenCalledOnce();
expect(host.identity().effectiveModel).toBe("claude-sonnet-5");
await host.close({ reason: "verified" });
});
it("rejects recovery drift before opening the provider", async () => {
const fixture = await hostFixture();
const openRuntime = vi.fn(async () => runtimePort());
await expect(
AcpxRuntimeHost.open(
{
...fixture.options,
agent: "claude",
model: "claude-sonnet-5",
permissionMode: "approve-reads",
expectedIdentity: {
kind: "acpx",
normalizedSessionId: fixture.options.normalizedSessionId,
acpxRecordId: "record-1",
backendSessionId: "backend-1",
agentSessionId: "agent-1",
profileDigest: resolveQualifiedAcpxProfile(
"claude",
"claude-sonnet-5",
).commandDigest,
workspaceDigest: `sha256:${"0".repeat(64)}`,
requestedModel: "claude-sonnet-5",
effectiveModel: "claude-sonnet-5",
permissionMode: "approve-reads",
},
},
fixture.dependencies({ openRuntime }),
),
).rejects.toThrow(/immutable session configuration/);
expect(openRuntime).not.toHaveBeenCalled();
});
it("rejects an injected installation that does not match the profile", async () => {
const fixture = await hostFixture();
const openRuntime = vi.fn(async () => runtimePort());
const dependencies = fixture.dependencies({ openRuntime });
dependencies.verifyInstallation = async () => ({
commandDigest: `sha256:${"f".repeat(64)}`,
agentServerPackageJsonPath: join(fixture.root, "package.json"),
agentRuntimePackageJsonPath: null,
openCommand: async () => {
throw new Error("mismatched installation must not open");
},
});
await expect(
AcpxRuntimeHost.open(
{
...fixture.options,
agent: "claude",
model: "claude-sonnet-5",
permissionMode: "approve-all",
},
dependencies,
),
).rejects.toThrow(/does not match its profile/);
expect(openRuntime).not.toHaveBeenCalled();
});
it("cleans credentials and command leases when provider open fails", async () => {
const fixture = await hostFixture();
let authPath = "";
await expect(
AcpxRuntimeHost.open(
{
...fixture.options,
agent: "codex",
model: "gpt-5.6-sol",
permissionMode: "approve-all",
environment: {
PAPERCLIP_ACPX_CODEX_AUTH_JSON_SECRET:
'{"tokens":{"access_token":"canary"}}',
},
},
fixture.dependencies({
openRuntime: async (options) => {
authPath = join(options.launchEnvironment.CODEX_HOME!, "auth.json");
throw new Error("provider failed");
},
}),
),
).rejects.toThrow("provider failed");
await expect(readFile(authPath)).rejects.toMatchObject({ code: "ENOENT" });
expect(fixture.commandClose).toHaveBeenCalledOnce();
});
it("attempts every cleanup when runtime shutdown fails", async () => {
const fixture = await hostFixture();
let failClose = true;
const runtime = runtimePort({
onClose: vi.fn(async () => {
if (failClose) throw new Error("runtime close failed");
}),
});
const host = await AcpxRuntimeHost.open(
{
...fixture.options,
agent: "codex",
model: "gpt-5.6-sol",
permissionMode: "approve-all",
environment: {
PAPERCLIP_ACPX_CODEX_AUTH_JSON_SECRET: "{}",
},
},
fixture.dependencies({ openRuntime: async () => runtime }),
);
const authPath = join(host.runtimeRoot(), "codex-home", "auth.json");
await expect(host.close({ reason: "first close" })).rejects.toThrow(
/cleanup failed/,
);
await expect(readFile(authPath)).rejects.toMatchObject({ code: "ENOENT" });
expect(fixture.commandClose).toHaveBeenCalledOnce();
failClose = false;
await expect(
host.close({ reason: "retry close" }),
).resolves.toBeUndefined();
});
it("closes a command lease that resolves after admission is aborted", async () => {
const fixture = await hostFixture();
const commandAdmission = deferred<VerifiedAcpxCommandLease>();
const lateCommandClose = vi.fn(async () => undefined);
const openCommand = vi.fn(() => commandAdmission.promise);
const openRuntime = vi.fn(async () => runtimePort());
const controller = new AbortController();
const cancellation = new Error("command admission cancelled");
const profile = resolveQualifiedAcpxProfile("claude", "claude-sonnet-5");
const opening = AcpxRuntimeHost.open(
{
...fixture.options,
agent: "claude",
model: "claude-sonnet-5",
permissionMode: "deny-all",
signal: controller.signal,
},
{
verifyInstallation: async () => ({
commandDigest: profile.commandDigest,
agentServerPackageJsonPath: join(fixture.root, "package.json"),
agentRuntimePackageJsonPath: null,
openCommand,
}),
openRuntime,
reportRetainedCleanupFailure: vi.fn(),
},
);
await vi.waitFor(() => expect(openCommand).toHaveBeenCalledOnce());
controller.abort(cancellation);
await expect(opening).rejects.toBe(cancellation);
commandAdmission.resolve({
spawn: () => {
throw new Error("late command must not spawn");
},
close: lateCommandClose,
});
await vi.waitFor(() => expect(lateCommandClose).toHaveBeenCalledOnce());
expect(openRuntime).not.toHaveBeenCalled();
});
it("closes a credential lease that resolves after admission is aborted", async () => {
const fixture = await hostFixture();
const lateCredentialPath = join(fixture.root, "late-auth.json");
await writeFile(lateCredentialPath, '{"access_token":"canary"}');
const credentialAdmission = deferred<{
path: string;
mode: "inline_json";
close(): Promise<void>;
}>();
const cleanupFailure = new Error("transient credential cleanup failure");
let cleanupAttempts = 0;
const lateCredentialClose = vi.fn(async () => {
cleanupAttempts += 1;
if (cleanupAttempts === 1) throw cleanupFailure;
await rm(lateCredentialPath);
});
const reportRetainedCleanupFailure = vi.fn();
const stageCredential = vi.fn(() => credentialAdmission.promise);
const openRuntime = vi.fn(async () => runtimePort());
const controller = new AbortController();
const cancellation = new Error("credential admission cancelled");
const opening = AcpxRuntimeHost.open(
{
...fixture.options,
agent: "codex",
model: "gpt-5.6-sol",
permissionMode: "deny-all",
signal: controller.signal,
},
{
...fixture.dependencies({
openRuntime,
reportRetainedCleanupFailure,
}),
stageCredential,
},
);
await vi.waitFor(() => expect(stageCredential).toHaveBeenCalledOnce());
controller.abort(cancellation);
await expect(opening).rejects.toBe(cancellation);
credentialAdmission.resolve({
path: lateCredentialPath,
mode: "inline_json",
close: lateCredentialClose,
});
await vi.waitFor(() =>
expect(lateCredentialClose).toHaveBeenCalledTimes(2),
);
await vi.waitFor(async () =>
expect(readFile(lateCredentialPath)).rejects.toMatchObject({
code: "ENOENT",
}),
);
expect(reportRetainedCleanupFailure).toHaveBeenCalledOnce();
expect(reportRetainedCleanupFailure).toHaveBeenCalledWith({
resource: "credential",
attempt: 1,
error: cleanupFailure,
});
expect(openRuntime).not.toHaveBeenCalled();
expect(fixture.commandClose).not.toHaveBeenCalled();
});
it("forwards cancellation and closes a runtime that resolves after abort", async () => {
const fixture = await hostFixture();
const runtimeAdmission = deferred<AcpxRuntimePort>();
const lateRuntime = runtimePort();
let receivedSignal: AbortSignal | undefined;
const openRuntime = vi.fn((options) => {
receivedSignal = options.signal;
return runtimeAdmission.promise;
});
const controller = new AbortController();
const cancellation = new Error("runtime admission cancelled");
const opening = AcpxRuntimeHost.open(
{
...fixture.options,
agent: "codex",
model: "gpt-5.6-sol",
permissionMode: "deny-all",
environment: {
PAPERCLIP_ACPX_CODEX_AUTH_JSON_SECRET: "{}",
},
signal: controller.signal,
},
fixture.dependencies({ openRuntime }),
);
await vi.waitFor(() => expect(openRuntime).toHaveBeenCalledOnce());
expect(receivedSignal).toBe(controller.signal);
controller.abort(cancellation);
await expect(opening).rejects.toBe(cancellation);
expect(fixture.commandClose).toHaveBeenCalledOnce();
runtimeAdmission.resolve(lateRuntime);
await vi.waitFor(() =>
expect(lateRuntime.close).toHaveBeenCalledWith({
reason: "ACPX runtime admission aborted",
}),
);
});
});
function runtimePort(
input: {
getStatus?: AcpxRuntimePort["getStatus"];
setModel?: NonNullable<AcpxRuntimePort["setModel"]>;
onClose?: AcpxRuntimePort["close"];
} = {},
): AcpxRuntimePort & { close: ReturnType<typeof vi.fn> } {
return {
identity: async () => ({
acpxRecordId: "record-1",
backendSessionId: "backend-1",
agentSessionId: "agent-1",
}),
getStatus:
input.getStatus ??
(async () => ({
models: {
currentModelId: "gpt-5.6-sol",
availableModelIds: ["gpt-5.6-sol"],
},
})),
...(input.setModel ? { setModel: input.setModel } : {}),
close: vi.fn(input.onClose ?? (async () => undefined)),
};
}
async function hostFixture() {
const root = await mkdtemp(join(tmpdir(), "paperclip-acpx-host-"));
temporaryDirectories.push(root);
const runtimeDirectory = join(root, "runtime");
const workingDirectory = join(root, "workspace");
await Promise.all([mkdir(runtimeDirectory), mkdir(workingDirectory)]);
const commandClose = vi.fn(async () => undefined);
const command: VerifiedAcpxCommandLease = {
spawn: () => {
throw new Error("test command is not spawnable");
},
close: commandClose,
};
return {
root,
commandClose,
options: {
runtimeDirectory,
normalizedSessionId: "normalized-session-1",
workingDirectory,
},
dependencies(
input: Pick<AcpxRuntimeHostDependencies, "openRuntime"> &
Partial<
Pick<AcpxRuntimeHostDependencies, "reportRetainedCleanupFailure">
>,
): AcpxRuntimeHostDependencies {
return {
verifyInstallation: async (profile) =>
({
commandDigest: profile.commandDigest,
agentServerPackageJsonPath: join(root, "package.json"),
agentRuntimePackageJsonPath: null,
openCommand: async () => command,
}) satisfies VerifiedAcpxInstallation,
openRuntime: input.openRuntime,
reportRetainedCleanupFailure:
input.reportRetainedCleanupFailure ?? vi.fn(),
};
},
};
}
function deferred<T>(): {
promise: Promise<T>;
resolve(value: T): void;
} {
let resolve!: (value: T) => void;
const promise = new Promise<T>((settle) => {
resolve = settle;
});
return { promise, resolve };
}

View File

@ -0,0 +1,443 @@
import type { NativeAcpxPermissionMode } from "../../contracts/native-execution.js";
import {
stageManagedCodexCredential,
type ManagedCodexCredentialLease,
} from "./codex-credentials.js";
import {
verifyQualifiedAcpxInstallation,
type VerifiedAcpxCommandLease,
type VerifiedAcpxInstallation,
} from "./installation-integrity.js";
import {
requireVerifiedAcpxModel,
type AcpxModelStatus,
} from "./model-verification.js";
import { acpxRuntimePermissionPolicy } from "./permission-policy.js";
import {
resolveQualifiedAcpxProfile,
type QualifiedAcpxAgent,
type QualifiedAcpxProfile,
} from "./qualified-profiles.js";
import {
createAcpxIdentityRecord,
createAcpxRecoveryBinding,
verifyExpectedAcpxIdentity,
type AcpxIdentityRecord,
type AcpxRecoveryBinding,
} from "./recovery-identity.js";
import {
prepareAcpxRuntimeSandbox,
type AcpxRuntimeSandbox,
} from "./runtime-sandbox.js";
import type { AcpxExpectedSessionIdentity } from "./sidecar-protocol.js";
export interface AcpxRuntimePortIdentity {
acpxRecordId: string;
backendSessionId: string;
agentSessionId: string;
}
/** Minimal third-party ACP runtime surface admitted by the host boundary. */
export interface AcpxRuntimePort {
identity(): Promise<AcpxRuntimePortIdentity>;
getStatus(): Promise<AcpxModelStatus>;
setModel?(model: string): Promise<void>;
close(input: { reason: string }): Promise<void>;
}
export interface AcpxRuntimePortOpenOptions {
command: VerifiedAcpxCommandLease;
profile: QualifiedAcpxProfile;
cwd: string;
stateDirectory: string;
providerSessionKey: string;
permissionMode: NativeAcpxPermissionMode;
permissionPolicy: ReturnType<typeof acpxRuntimePermissionPolicy>;
launchEnvironment: Readonly<NodeJS.ProcessEnv>;
systemInstructions: string;
/** Abort provider admission and clean any runtime that resolves too late. */
signal?: AbortSignal;
}
export interface AcpxRetainedCleanupFailure {
resource: "credential" | "command" | "runtime";
attempt: number;
error: unknown;
}
export interface AcpxRuntimeHostDependencies {
verifyInstallation?: (
profile: QualifiedAcpxProfile,
) => Promise<VerifiedAcpxInstallation>;
/** Internal test seam for aborting credential acquisition. */
stageCredential?: typeof stageManagedCodexCredential;
openRuntime(options: AcpxRuntimePortOpenOptions): Promise<AcpxRuntimePort>;
/**
* Required observability channel for resources acquired after admission was
* aborted. Implementations must not throw from this callback.
*/
reportRetainedCleanupFailure(failure: AcpxRetainedCleanupFailure): void;
}
export interface OpenAcpxRuntimeHostOptions {
runtimeDirectory: string;
normalizedSessionId: string;
workingDirectory: string;
agent: QualifiedAcpxAgent;
model: string;
permissionMode: NativeAcpxPermissionMode;
systemInstructions?: string;
environment?: NodeJS.ProcessEnv;
managedCodexCredentialSourcePath?: string;
expectedIdentity?: AcpxExpectedSessionIdentity;
/** Abort admission without admitting resources that resolve afterward. */
signal?: AbortSignal;
}
const activeRuntimeHostCleanupOwners = new Set<Promise<unknown>>();
const RETAINED_CLEANUP_RETRY_INITIAL_DELAY_MS = 10;
const RETAINED_CLEANUP_RETRY_MAX_DELAY_MS = 1_000;
export class AcpxRuntimeHost {
readonly #runtime: AcpxRuntimePort;
readonly #binding: AcpxRecoveryBinding;
readonly #identity: AcpxIdentityRecord;
readonly #sandbox: AcpxRuntimeSandbox;
readonly #credential: ManagedCodexCredentialLease | null;
readonly #command: VerifiedAcpxCommandLease;
#closed = false;
private constructor(input: {
runtime: AcpxRuntimePort;
binding: AcpxRecoveryBinding;
identity: AcpxIdentityRecord;
sandbox: AcpxRuntimeSandbox;
credential: ManagedCodexCredentialLease | null;
command: VerifiedAcpxCommandLease;
}) {
this.#runtime = input.runtime;
this.#binding = input.binding;
this.#identity = input.identity;
this.#sandbox = input.sandbox;
this.#credential = input.credential;
this.#command = input.command;
}
static async open(
options: OpenAcpxRuntimeHostOptions,
dependencies: AcpxRuntimeHostDependencies,
): Promise<AcpxRuntimeHost> {
options.signal?.throwIfAborted();
const profile = resolveQualifiedAcpxProfile(options.agent, options.model);
const binding = await runAbortableAdmissionStage(options.signal, () =>
createAcpxRecoveryBinding({
runtimeDirectory: options.runtimeDirectory,
normalizedSessionId: options.normalizedSessionId,
workingDirectory: options.workingDirectory,
profile,
requestedModel: options.model,
permissionMode: options.permissionMode,
}),
);
if (options.expectedIdentity) {
verifyExpectedAcpxIdentity(options.expectedIdentity, binding, null);
}
if (
options.agent !== "codex" &&
options.managedCodexCredentialSourcePath !== undefined
) {
throw new Error(
"Managed Codex credentials require the Codex ACPX profile",
);
}
const installation = await runAbortableAdmissionStage(options.signal, () =>
(dependencies.verifyInstallation ?? verifyQualifiedAcpxInstallation)(
profile,
),
);
if (installation.commandDigest !== profile.commandDigest) {
throw new Error("Verified ACPX installation does not match its profile");
}
let command: VerifiedAcpxCommandLease | null = null;
let credential: ManagedCodexCredentialLease | null = null;
let runtime: AcpxRuntimePort | null = null;
try {
const sandbox = await runAbortableAdmissionStage(options.signal, () =>
prepareAcpxRuntimeSandbox({
binding,
agent: options.agent,
environment: options.environment,
}),
);
if (options.agent === "codex") {
credential = await acquireAbortableAdmissionResource({
signal: options.signal,
acquire: () =>
(dependencies.stageCredential ?? stageManagedCodexCredential)({
agentHomeDirectory: sandbox.agentHomeDirectory,
environment: options.environment,
sourcePath: options.managedCodexCredentialSourcePath,
}),
resource: "credential",
releaseLate: (lateCredential) => lateCredential.close(),
reportFailure: (failure) =>
dependencies.reportRetainedCleanupFailure(failure),
});
}
command = await acquireAbortableAdmissionResource({
signal: options.signal,
acquire: () => installation.openCommand(),
resource: "command",
releaseLate: (lateCommand) => lateCommand.close(),
reportFailure: (failure) =>
dependencies.reportRetainedCleanupFailure(failure),
});
runtime = await acquireAbortableAdmissionResource({
signal: options.signal,
acquire: () =>
dependencies.openRuntime({
command: command!,
profile,
cwd: binding.workspacePath,
stateDirectory: sandbox.stateDirectory,
providerSessionKey: binding.profileSessionKey,
permissionMode: binding.permissionMode,
permissionPolicy: acpxRuntimePermissionPolicy(
binding.permissionMode,
),
launchEnvironment: sandbox.launchEnvironment,
systemInstructions: boundedInstructions(options.systemInstructions),
...(options.signal === undefined ? {} : { signal: options.signal }),
}),
resource: "runtime",
releaseLate: (lateRuntime) =>
lateRuntime.close({
reason: "ACPX runtime admission aborted",
}),
reportFailure: (failure) =>
dependencies.reportRetainedCleanupFailure(failure),
});
await runAbortableAdmissionStage(options.signal, () =>
requireVerifiedAcpxModel(runtime!, profile),
);
const runtimeIdentity = await runAbortableAdmissionStage(
options.signal,
() => runtime!.identity(),
);
const observedIdentity: AcpxExpectedSessionIdentity = {
kind: "acpx",
normalizedSessionId: binding.normalizedSessionId,
...runtimeIdentity,
profileDigest: binding.profileDigest,
workspaceDigest: binding.workspaceDigest,
requestedModel: binding.requestedModel,
effectiveModel: binding.effectiveModel,
permissionMode: binding.permissionMode,
};
const identity = createAcpxIdentityRecord(observedIdentity, binding);
if (options.expectedIdentity) {
verifyExpectedAcpxIdentity(options.expectedIdentity, binding, identity);
}
options.signal?.throwIfAborted();
return new AcpxRuntimeHost({
runtime,
binding,
identity,
sandbox,
credential,
command,
});
} catch (error) {
const cleanupError = await cleanupRuntimeResources(
runtime,
credential,
command,
"ACPX runtime initialization failed",
);
if (cleanupError) {
throw new AggregateError(
[error, ...cleanupError.errors],
"ACPX runtime initialization and cleanup failed",
);
}
throw error;
}
}
identity(): AcpxIdentityRecord {
return structuredClone(this.#identity);
}
binding(): AcpxRecoveryBinding {
return structuredClone(this.#binding);
}
runtimeRoot(): string {
return this.#sandbox.root;
}
persistedEnvironment(): Readonly<NodeJS.ProcessEnv> {
return Object.freeze({ ...this.#sandbox.persistedEnvironment });
}
async close(input: { reason: string }): Promise<void> {
if (this.#closed) return;
const error = await cleanupRuntimeResources(
this.#runtime,
this.#credential,
this.#command,
boundedReason(input.reason),
);
if (error) throw error;
this.#closed = true;
}
}
async function runAbortableAdmissionStage<T>(
signal: AbortSignal | undefined,
operation: () => Promise<T>,
): Promise<T> {
if (signal === undefined) return await operation();
signal.throwIfAborted();
const pending = Promise.resolve().then(operation);
return await raceAdmissionWithAbort(pending, signal);
}
async function acquireAbortableAdmissionResource<T>(input: {
signal: AbortSignal | undefined;
acquire: () => Promise<T>;
resource: AcpxRetainedCleanupFailure["resource"];
releaseLate: (resource: T) => Promise<void>;
reportFailure: (failure: AcpxRetainedCleanupFailure) => void;
}): Promise<T> {
if (input.signal === undefined) return await input.acquire();
input.signal.throwIfAborted();
const pending = Promise.resolve().then(input.acquire);
try {
return await raceAdmissionWithAbort(pending, input.signal);
} catch (error) {
if (input.signal.aborted) {
retainRuntimeHostCleanup(
pending.then((resource) =>
releaseRetainedAdmissionResource({
resource,
resourceKind: input.resource,
release: input.releaseLate,
reportFailure: input.reportFailure,
}),
),
);
}
throw error;
}
}
function raceAdmissionWithAbort<T>(
pending: Promise<T>,
signal: AbortSignal,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
let settled = false;
const settle = (operation: () => void): void => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", onAbort);
operation();
};
const onAbort = (): void => settle(() => reject(signal.reason));
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) {
onAbort();
return;
}
void pending.then(
(value) => settle(() => resolve(value)),
(error: unknown) => settle(() => reject(error)),
);
});
}
async function releaseRetainedAdmissionResource<T>(input: {
resource: T;
resourceKind: AcpxRetainedCleanupFailure["resource"];
release: (resource: T) => Promise<void>;
reportFailure: (failure: AcpxRetainedCleanupFailure) => void;
}): Promise<void> {
let attempt = 0;
let retryDelayMs = RETAINED_CLEANUP_RETRY_INITIAL_DELAY_MS;
for (;;) {
attempt += 1;
try {
await input.release(input.resource);
return;
} catch (error) {
try {
input.reportFailure({
resource: input.resourceKind,
attempt,
error,
});
} catch {
// The required reporter is observational. A broken reporter must not
// relinquish ownership of the resource that still needs cleanup.
}
await waitForRetainedCleanupRetry(retryDelayMs);
retryDelayMs = Math.min(
retryDelayMs * 2,
RETAINED_CLEANUP_RETRY_MAX_DELAY_MS,
);
}
}
}
async function waitForRetainedCleanupRetry(delayMs: number): Promise<void> {
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, delayMs);
timer.unref?.();
});
}
function retainRuntimeHostCleanup(cleanup: Promise<unknown>): void {
activeRuntimeHostCleanupOwners.add(cleanup);
void cleanup
.finally(() => activeRuntimeHostCleanupOwners.delete(cleanup))
.catch(() => undefined);
}
async function cleanupRuntimeResources(
runtime: AcpxRuntimePort | null,
credential: ManagedCodexCredentialLease | null,
command: VerifiedAcpxCommandLease | null,
reason: string,
): Promise<AggregateError | null> {
const errors: unknown[] = [];
for (const close of [
runtime ? () => runtime.close({ reason }) : null,
credential ? () => credential.close() : null,
command ? () => command.close() : null,
]) {
if (!close) continue;
try {
await close();
} catch (error) {
errors.push(error);
}
}
return errors.length > 0
? new AggregateError(errors, "ACPX runtime cleanup failed")
: null;
}
function boundedInstructions(value: string | undefined): string {
const instructions = value ?? "";
if (Buffer.byteLength(instructions) > 256 * 1024) {
throw new Error("ACPX system instructions exceed their bounded size");
}
return instructions;
}
function boundedReason(value: string): string {
const reason = value.trim().slice(0, 1_000);
return reason || "ACPX runtime closed";
}