mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 12:59:13 +01:00
ci(runner): classify ACPX admission stages
This commit is contained in:
1 parent
66efaf142a
commit
ae25fd4466
3 files changed
+110
-26
No files matched your search
@@ -677,6 +677,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() {
|
||||
|
||||
@@ -37,6 +37,7 @@ export function acpxBootstrapBlockedError(
|
||||
export function acpxSidecarErrorCode(error: Error): string {
|
||||
const pending: Error[] = [error];
|
||||
const observed = new Set<Error>();
|
||||
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<string, unknown>;
|
||||
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 {
|
||||
@@ -161,6 +172,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",
|
||||
@@ -200,5 +223,14 @@ const STABLE_ACPX_SIDECAR_CODES = new Set([
|
||||
"DIAGNOSTIC_ACPX_GUARDIAN_OWNERSHIP_TIMEOUT",
|
||||
"DIAGNOSTIC_ACPX_GUARDIAN_START_FAILED",
|
||||
"DIAGNOSTIC_ACPX_PROVIDER_EXIT_PROOF_MISSING",
|
||||
"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,
|
||||
]);
|
||||
@@ -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<T>(
|
||||
signal: AbortSignal | undefined,
|
||||
operation: () => Promise<T>,
|
||||
stage: string,
|
||||
): Promise<T> {
|
||||
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<T>(input: {
|
||||
@@ -644,7 +659,13 @@ async function acquireAbortableAdmissionResource<T>(input: {
|
||||
onAbortedPending?: (pending: Promise<T>) => void;
|
||||
reportFailure: (failure: AcpxRetainedCleanupFailure) => void;
|
||||
}): Promise<T> {
|
||||
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<T>(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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user