fix(server): verify native Grok API keys during readiness checks

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-03 23:51:21 -06:00
1 parent 3e82aeaa60
commit 2ddf380ff9
2 files changed
+30 -12

No files matched your search

+19
View File
@@ -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,
+11 -12
View File
@@ -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<string, unknown>,
result: AdapterEnvironmentTestResult,
): Promise<AdapterEnvironmentTestResult> {
const env = config.env as Record<string, unknown> | 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.` }],
});