From a512ee49c2b60162c92c45054d83dfe0ea56c721 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 2 Sep 2026 23:14:22 -0500 Subject: [PATCH] ci(runner): classify ACPX admission stages --- .../runner-core/src/acpx_sidecar_transport.rs | 9 ++ .../src/cli/acpx-sidecar-input.ts | 34 ++++++- .../src/drivers/acpx/runtime-host.ts | 93 ++++++++++++++----- 3 files changed, 110 insertions(+), 26 deletions(-) diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs index 66fb60adc00..a23c36dacfc 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs @@ -638,6 +638,15 @@ fn response_error_classification(error: &ResponseError) -> &'static str { "ACPX_MODEL_STATUS_UNAVAILABLE" => return "model_status_unavailable", "ACPX_MODEL_SELECTION_UNAVAILABLE" => return "model_selection_unavailable", "ACPX_EFFECTIVE_MODEL_MISMATCH" => return "effective_model_mismatch", + "PAPERCLIP_ACPX_ADMISSION_RECOVERY_BINDING" => return "admission_recovery_binding", + "PAPERCLIP_ACPX_ADMISSION_INSTALLATION" => return "admission_installation", + "PAPERCLIP_ACPX_ADMISSION_SANDBOX" => return "admission_sandbox", + "PAPERCLIP_ACPX_ADMISSION_CREDENTIAL" => return "admission_credential", + "PAPERCLIP_ACPX_ADMISSION_PROVIDER_LIFETIME" => return "admission_provider_lifetime", + "PAPERCLIP_ACPX_ADMISSION_COMMAND" => return "admission_command", + "PAPERCLIP_ACPX_ADMISSION_TOOL_BRIDGE" => return "admission_tool_bridge", + "PAPERCLIP_ACPX_ADMISSION_RUNTIME" => return "admission_runtime", + "PAPERCLIP_ACPX_ADMISSION_VERIFICATION" => return "admission_verification", _ => {} } match error.message.as_str() { diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts index 9f508bb2898..83e81101a57 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts @@ -37,6 +37,7 @@ export function acpxBootstrapBlockedError( export function acpxSidecarErrorCode(error: Error): string { const pending: Error[] = [error]; const observed = new Set(); + let admissionStageCode: string | null = null; while (pending.length > 0 && observed.size < 16) { const current = pending.shift()!; if (observed.has(current)) continue; @@ -45,6 +46,9 @@ export function acpxSidecarErrorCode(error: Error): string { if (code !== null) return code; const details = current as Error & Record; + admissionStageCode ??= acpxAdmissionStageCode( + details.paperclipAcpxAdmissionStage, + ); if (current instanceof AggregateError) { for (const nested of current.errors) { if (nested instanceof Error) pending.push(nested); @@ -52,7 +56,14 @@ export function acpxSidecarErrorCode(error: Error): string { } if (details.cause instanceof Error) pending.push(details.cause); } - return "acpx_sidecar_command_failed"; + return admissionStageCode ?? "acpx_sidecar_command_failed"; +} + +function acpxAdmissionStageCode(value: unknown): string | null { + if (typeof value !== "string" || !ACPX_ADMISSION_STAGES.has(value)) { + return null; + } + return `PAPERCLIP_ACPX_ADMISSION_${value.toUpperCase()}`; } function directAcpxSidecarErrorCode(error: Error): string | null { @@ -129,6 +140,18 @@ const GENERIC_ACPX_OUTPUT_CODES = new Set([ "USAGE", ]); +const ACPX_ADMISSION_STAGES = new Set([ + "recovery_binding", + "installation", + "sandbox", + "credential", + "provider_lifetime", + "command", + "tool_bridge", + "runtime", + "verification", +]); + const STABLE_ACPX_SIDECAR_CODES = new Set([ "ACP_MODEL_UNSUPPORTED", "ACP_SESSION_INIT_FAILED", @@ -161,5 +184,14 @@ const STABLE_ACPX_SIDECAR_CODES = new Set([ "ACPX_SESSION_ENSURE_TYPE_ERROR", "ACPX_SESSION_HANDSHAKE_TIMEOUT", "ACPX_SIDECAR_STATUS_READ_TIMEOUT", + "PAPERCLIP_ACPX_ADMISSION_RECOVERY_BINDING", + "PAPERCLIP_ACPX_ADMISSION_INSTALLATION", + "PAPERCLIP_ACPX_ADMISSION_SANDBOX", + "PAPERCLIP_ACPX_ADMISSION_CREDENTIAL", + "PAPERCLIP_ACPX_ADMISSION_PROVIDER_LIFETIME", + "PAPERCLIP_ACPX_ADMISSION_COMMAND", + "PAPERCLIP_ACPX_ADMISSION_TOOL_BRIDGE", + "PAPERCLIP_ACPX_ADMISSION_RUNTIME", + "PAPERCLIP_ACPX_ADMISSION_VERIFICATION", ...GENERIC_ACPX_OUTPUT_CODES, ]); diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts index 0345ffcf44b..4afbb85ac1c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts @@ -249,15 +249,18 @@ export class AcpxRuntimeHost { ); } 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, - }), + const binding = await runAbortableAdmissionStage( + options.signal, + () => + createAcpxRecoveryBinding({ + runtimeDirectory: options.runtimeDirectory, + normalizedSessionId: options.normalizedSessionId, + workingDirectory: options.workingDirectory, + profile, + requestedModel: options.model, + permissionMode: options.permissionMode, + }), + "recovery_binding", ); if (options.expectedIdentity) { verifyExpectedAcpxIdentity(options.expectedIdentity, binding, null); @@ -272,10 +275,13 @@ export class AcpxRuntimeHost { ); } - const installation = await runAbortableAdmissionStage(options.signal, () => - (dependencies.verifyInstallation ?? verifyQualifiedAcpxInstallation)( - profile, - ), + const installation = await runAbortableAdmissionStage( + options.signal, + () => + (dependencies.verifyInstallation ?? verifyQualifiedAcpxInstallation)( + profile, + ), + "installation", ); if (installation.commandDigest !== profile.commandDigest) { throw new Error("Verified ACPX installation does not match its profile"); @@ -322,12 +328,15 @@ export class AcpxRuntimeHost { retainRuntimeHostCleanup(ownedCleanup); }; try { - const sandbox = await runAbortableAdmissionStage(options.signal, () => - prepareAcpxRuntimeSandbox({ - binding, - agent: options.agent, - environment: options.environment, - }), + const sandbox = await runAbortableAdmissionStage( + options.signal, + () => + prepareAcpxRuntimeSandbox({ + binding, + agent: options.agent, + environment: options.environment, + }), + "sandbox", ); if (options.agent === "codex") { credential = await acquireAbortableAdmissionResource({ @@ -443,6 +452,7 @@ export class AcpxRuntimeHost { profile, admissionVerificationTimeoutMs, ), + "verification", ); const observedIdentity: AcpxExpectedSessionIdentity = { kind: "acpx", @@ -629,11 +639,16 @@ export class AcpxRuntimeHost { async function runAbortableAdmissionStage( signal: AbortSignal | undefined, operation: () => Promise, + stage: string, ): Promise { - if (signal === undefined) return await operation(); - signal.throwIfAborted(); - const pending = Promise.resolve().then(operation); - return await raceAdmissionWithAbort(pending, signal); + try { + if (signal === undefined) return await operation(); + signal.throwIfAborted(); + const pending = Promise.resolve().then(operation); + return await raceAdmissionWithAbort(pending, signal); + } catch (error) { + throw attachAcpxAdmissionStage(error, stage); + } } async function acquireAbortableAdmissionResource(input: { @@ -644,7 +659,13 @@ async function acquireAbortableAdmissionResource(input: { onAbortedPending?: (pending: Promise) => void; reportFailure: (failure: AcpxRetainedCleanupFailure) => void; }): Promise { - if (input.signal === undefined) return await input.acquire(); + if (input.signal === undefined) { + try { + return await input.acquire(); + } catch (error) { + throw attachAcpxAdmissionStage(error, input.resource); + } + } input.signal.throwIfAborted(); const pending = Promise.resolve().then(input.acquire); try { @@ -666,7 +687,29 @@ async function acquireAbortableAdmissionResource(input: { ); } } - throw error; + throw attachAcpxAdmissionStage(error, input.resource); + } +} + +function attachAcpxAdmissionStage(error: unknown, stage: string): Error { + const normalized = + error instanceof Error + ? error + : new Error("ACPX admission rejected a non-error"); + try { + Object.defineProperty(normalized, "paperclipAcpxAdmissionStage", { + configurable: true, + enumerable: false, + value: stage, + }); + return normalized; + } catch { + return Object.assign( + new Error("ACPX admission stage failed", { cause: normalized }), + { + paperclipAcpxAdmissionStage: stage, + }, + ); } }