From 2ddf380ff9f319ba22bc5a0088a1dd1d168c5a04 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 3 Nov 2026 22:58:54 -0600 Subject: [PATCH] fix(server): verify native Grok API keys during readiness checks Co-Authored-By: Paperclip --- server/src/adapters/registry.test.ts | 19 +++++++++++++++++++ server/src/adapters/registry.ts | 23 +++++++++++------------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/server/src/adapters/registry.test.ts b/server/src/adapters/registry.test.ts index a1cea55ab57..4c9210bafa5 100644 --- a/server/src/adapters/registry.test.ts +++ b/server/src/adapters/registry.test.ts @@ -4,6 +4,7 @@ import { listServerAdapters, requireServerAdapter } from "./registry.js"; import * as executionTarget from "@paperclipai/adapter-utils/execution-target"; import { BUILTIN_ADAPTER_TYPES } from "./builtin-adapter-types.js"; import { QUALIFIED_ACPX_PROFILES, KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../vendor/paperclip-runner/index.js"; +import { assertHarnessTestPassed } from "../services/agent-harness-verification.js"; const { probeInstallation, probeGrokInstallation, probePiInstallation, probeKimiInstallation, probeKimiModels, probeKimiRemote, verifyApiKey } = vi.hoisted(() => ({ verifyApiKey: vi.fn(), @@ -222,6 +223,24 @@ describe("native ACPX environment checks", () => { expect(result.checks.map(check => check.code)).toEqual(["acpx_runtime_ready", "ai_connection_api_key_reverified"]); }); + it.each(["verified", "rejected", "unavailable", "missing", "managed"])("keeps remote Grok adoption tied to account verification: %s", async state => { + if (state === "rejected") verifyApiKey.mockRejectedValue({ details: { code: "ai_connection_api_key_rejected" } }); + if (state === "unavailable") verifyApiKey.mockRejectedValue(new Error("Provider unavailable")); + const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context, + config: { provider: "acpx", acpxAgent: "grok", model: "grok-4.7", + env: state === "missing" ? {} : { XAI_API_KEY: "fixture-xai-key" }, + ...(state === "managed" ? { managedAiConnection: { method: "api_key" } } : {}) }, + executionTarget: { kind: "remote", transport: "sandbox", remoteCwd: "/workspace", providerKey: "test", + runner: { execute: vi.fn().mockResolvedValue({ exitCode: 0, timedOut: false, stdout: "Linux\nx86_64\n" }) } }, + }); + if (["missing", "managed"].includes(state)) expect(verifyApiKey).not.toHaveBeenCalled(); + else expect(verifyApiKey).toHaveBeenCalledExactlyOnceWith("xai", "fixture-xai-key"); + expect(result.checks[0]).toMatchObject({ code: "acpx_remote_runtime_unverified", level: "warn" }); + if (state === "verified") expect(() => assertHarnessTestPassed(result)).not.toThrow(); + else expect(() => assertHarnessTestPassed(result)).toThrow("The harness test failed"); + expect(probeGrokInstallation).not.toHaveBeenCalled(); + }); + it.each(["ai_connection_api_key_rejected", "ai_connection_verification_failed"])("fails setup on %s", async code => { verifyApiKey.mockRejectedValue({ details: { code } }); const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context, diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 7540a1e672a..a96f57763b1 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -150,29 +150,28 @@ import { resolvePaperclipRunnerProviderProfile, } from "../services/native-runtime/provider-profile.js"; -/** Reuse the existing provider key check without requiring a global Claude CLI. - * Readiness and account access are separate checks; model access is proven by a task. - */ -async function verifyNativeClaudeApiKey( +async function verifyNativeAcpxApiKey( config: Record, result: AdapterEnvironmentTestResult, ): Promise { const env = config.env as Record | undefined; - if (config.acpxAgent !== "claude" || !env || typeof env.ANTHROPIC_API_KEY !== "string" - || !env.ANTHROPIC_API_KEY || config.managedAiRouting || config.managedAiConnection - || ["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"].some(key => env[key])) return result; + const account = config.acpxAgent === "claude" ? { provider: "anthropic" as const, name: "Anthropic", key: "ANTHROPIC_API_KEY", + routing: ["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"] } + : config.acpxAgent === "grok" ? { provider: "xai" as const, name: "xAI", key: "XAI_API_KEY", routing: ["XAI_BASE_URL", "GROK_BASE_URL"] } : null; + if (!account || !env || typeof env[account.key] !== "string" || !env[account.key] + || config.managedAiRouting || config.managedAiConnection || account.routing.some(key => env[key])) return result; try { - await validateAiApiKey("anthropic", env.ANTHROPIC_API_KEY); + await validateAiApiKey(account.provider, env[account.key] as string); return { ...result, checks: [...result.checks, { code: "ai_connection_api_key_reverified", level: "info", - message: "Anthropic verified this API key. Model access is checked when the agent runs.", + message: `${account.name} verified this API key. Model access is checked when the agent runs.`, }] }; } catch (error) { const details = error && typeof error === "object" && "details" in error ? error.details as { code?: string } | undefined : undefined; return { ...result, status: "fail", checks: [...result.checks, { code: details?.code === "ai_connection_api_key_rejected" ? details.code : "ai_connection_verification_failed", - level: "error", message: "Could not verify the Anthropic API key. Check the connection and try again.", + level: "error", message: `Could not verify the ${account.name} API key. Check the connection and try again.`, }] }; } } @@ -527,7 +526,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = { if (!((os === "Linux" && (arch === "x86_64" || (profile.acpxAgent === "claude" && ["aarch64", "arm64"].includes(arch)))) || (os === "Darwin" && (arch === "arm64" || (!["grok"].includes(profile.acpxAgent) && arch === "x86_64"))))) { throw new Error(`ACPX ${profile.acpxAgent} requires a qualified Linux ${profile.acpxAgent === "claude" ? "x64/ARM64" : "x64"} or macOS architecture.`); } - return verifyNativeClaudeApiKey(context.config, { + return verifyNativeAcpxApiKey(context.config, { adapterType: "paperclip_runner", status: "warn" as const, testedAt: new Date().toISOString(), checks: [{ code: "acpx_remote_runtime_unverified", level: "warn" as const, message: "The remote platform is supported. Runtime package integrity and readiness must still be verified by the remote runner before launch." }], @@ -535,7 +534,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = { } const { probeAcpxClaudeInstallation, probeAcpxGrokInstallation, probeAcpxCursorInstallation, probeAcpxPiInstallation } = await import("../vendor/paperclip-runner/live/index.js"); await (profile.acpxAgent === "pi" ? probeAcpxPiInstallation : profile.acpxAgent === "grok" ? probeAcpxGrokInstallation : profile.acpxAgent === "cursor" ? probeAcpxCursorInstallation : probeAcpxClaudeInstallation)(profile.model); - return verifyNativeClaudeApiKey(context.config, { + return verifyNativeAcpxApiKey(context.config, { adapterType: "paperclip_runner", status: "pass" as const, testedAt: new Date().toISOString(), checks: [{ code: "acpx_runtime_ready", level: "info" as const, message: `ACPX ${profile.acpxAgent} runtime is installed and verified. Model access is checked when it runs.` }], });