mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 06:04:11 +01:00
feat(runner): add Grok Build native ACP transport and qualification coverage
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
f447990d77
commit
78eb3f19f3
65 files changed
+1017
-111
No files matched your search
@@ -405,7 +405,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
|
||||
}
|
||||
if (profile.provider === "acpx") {
|
||||
try {
|
||||
if (profile.acpxAgent !== "claude") throw new Error("Select Codex to use the native Codex runner.");
|
||||
if (profile.acpxAgent !== "claude" && profile.acpxAgent !== "grok") throw new Error("Select Codex to use the native Codex runner.");
|
||||
const target = context.executionTarget;
|
||||
if (target?.kind === "remote") {
|
||||
const probe = await runAdapterExecutionTargetShellCommand(
|
||||
@@ -414,8 +414,8 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
|
||||
);
|
||||
if (probe.timedOut || probe.exitCode !== 0) throw new Error("Could not verify the remote ACPX runner platform.");
|
||||
const [os, arch] = probe.stdout.trim().split(/\s+/);
|
||||
if (!((os === "Linux" && arch === "x86_64") || (os === "Darwin" && ["arm64", "x86_64"].includes(arch ?? "")))) {
|
||||
throw new Error("ACPX Claude requires Linux x64 or macOS ARM64/x64.");
|
||||
if (!((os === "Linux" && arch === "x86_64") || (os === "Darwin" && (arch === "arm64" || (profile.acpxAgent !== "grok" && arch === "x86_64"))))) {
|
||||
throw new Error(`ACPX ${profile.acpxAgent} requires a qualified Linux x64 or macOS architecture.`);
|
||||
}
|
||||
return {
|
||||
adapterType: "paperclip_runner", status: "warn" as const, testedAt: new Date().toISOString(),
|
||||
@@ -423,11 +423,11 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
|
||||
message: "The remote platform is supported. Runtime package integrity and readiness must still be verified by the remote runner before launch." }],
|
||||
};
|
||||
}
|
||||
const { probeAcpxClaudeInstallation } = await import("@paperclipai/paperclip-runner/live");
|
||||
await probeAcpxClaudeInstallation(profile.model);
|
||||
const { probeAcpxClaudeInstallation, probeAcpxGrokInstallation } = await import("@paperclipai/paperclip-runner/live");
|
||||
await (profile.acpxAgent === "grok" ? probeAcpxGrokInstallation : probeAcpxClaudeInstallation)(profile.model);
|
||||
return {
|
||||
adapterType: "paperclip_runner", status: "pass" as const, testedAt: new Date().toISOString(),
|
||||
checks: [{ code: "acpx_runtime_ready", level: "info" as const, message: "ACPX Claude runtime is installed and verified. Model access is checked when Claude runs." }],
|
||||
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.` }],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -510,7 +510,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
|
||||
)
|
||||
: buildNpmRuntimeCommandSpec(config, "codex", "@openai/codex@0.153.4"),
|
||||
agentConfigurationDoc:
|
||||
"# Paperclip Runner\n\nAdapter: paperclip_runner\n\nRuns Codex, OpenCode, Claude Managed, AWS AgentCore, or ACPX Claude through the Rust Paperclip runner and authenticated PRP transport. Pi is not available through the qualified ACPX profile. Managed providers use company-scoped qualified profiles, explicit retention acknowledgement, and spend limits.\n",
|
||||
"# Paperclip Runner\n\nAdapter: paperclip_runner\n\nRuns Codex, OpenCode, Claude Managed, AWS AgentCore, or ACPX Claude/Grok Build through the Rust Paperclip runner and authenticated PRP transport. Pi is not available through the qualified ACPX profile. Managed providers use company-scoped qualified profiles, explicit retention acknowledgement, and spend limits.\n",
|
||||
getConfigSchema: () => ({
|
||||
fields: [
|
||||
{
|
||||
@@ -527,6 +527,11 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
|
||||
],
|
||||
hint: "Select a local provider, company-qualified managed provider, or ACPX Claude.",
|
||||
},
|
||||
{
|
||||
key: "acpxAgent", label: "ACP agent", type: "select" as const, default: "claude",
|
||||
options: [{ value: "claude", label: "Claude" }, { value: "grok", label: "Grok Build" }],
|
||||
meta: { visibleWhen: { key: "provider", value: "acpx" } },
|
||||
},
|
||||
{
|
||||
key: "codexPermissionMode",
|
||||
label: "Codex permission mode",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { mkdtemp, mkdir, realpath, 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 { prepareGrokRunnerCredentials } from "./grok-runner-credentials.js";
|
||||
|
||||
const { companyHome } = vi.hoisted(() => ({ companyHome: vi.fn() }));
|
||||
vi.mock("@paperclipai/adapter-grok-local/server", () => ({
|
||||
resolveManagedGrokHomeDir: companyHome,
|
||||
grokHomeHasUsableAuth: async () => false,
|
||||
}));
|
||||
const roots: string[] = [];
|
||||
async function home() { const root = await realpath(await mkdtemp(join(tmpdir(), "grok-controller-test-"))); roots.push(root); return root; }
|
||||
afterEach(async () => { await Promise.all(roots.splice(0).map(path => rm(path, { recursive: true, force: true }))); vi.clearAllMocks(); });
|
||||
|
||||
describe("Grok runner company credentials", () => {
|
||||
it("requires the company login for remote runs and never borrows a configured host home", async () => {
|
||||
const root = await home();
|
||||
companyHome.mockReturnValue(join(root, "company-without-login"));
|
||||
const host = join(root, "host"); await mkdir(host, { mode: 0o700 });
|
||||
await writeFile(join(host, "auth.json"), "host-secret", { mode: 0o600 });
|
||||
await expect(prepareGrokRunnerCredentials({ companyId: "test-company", remote: true, environment: { GROK_HOME: host } })).rejects.toThrow("subscription login is unavailable");
|
||||
});
|
||||
it("uses only an explicitly selected API key and strips subscription payloads and homes", async () => {
|
||||
expect(await prepareGrokRunnerCredentials({ companyId: "test-company", remote: true, environment: { XAI_API_KEY: "selected", GROK_HOME: "/host", PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET: "stale" } })).toEqual({ environment: { XAI_API_KEY: "selected" }, home: null });
|
||||
});
|
||||
it("reads the controller-selected managed connection privately", async () => {
|
||||
const root = await home(); companyHome.mockReturnValue(join(root, "unused"));
|
||||
await writeFile(join(root, "auth.json"), '{"private":"fixture"}', { mode: 0o600 });
|
||||
const value = await prepareGrokRunnerCredentials({ companyId: "test-company", remote: true, managedHome: root, environment: { GROK_HOME: "/untrusted", XAI_API_KEY: "" } });
|
||||
expect(value).toEqual({ home: root, environment: { PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET: '{"private":"fixture"}' } });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { grokHomeHasUsableAuth, resolveManagedGrokHomeDir } from "@paperclipai/adapter-grok-local/server";
|
||||
import { readLocalAiCredentialFile } from "../local-ai-credential-file.js";
|
||||
|
||||
/** Resolve credentials on the controller. Remote runtimes receive only the
|
||||
* selected credential, never an ambient host home or an implicit API key.
|
||||
*/
|
||||
export async function prepareGrokRunnerCredentials(input: {
|
||||
companyId: string;
|
||||
environment: NodeJS.ProcessEnv;
|
||||
remote: boolean;
|
||||
managedHome?: string;
|
||||
}): Promise<{ environment: NodeJS.ProcessEnv; home: string | null }> {
|
||||
const environment = { ...input.environment };
|
||||
delete environment.PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET;
|
||||
const configuredHome = input.managedHome?.trim();
|
||||
delete environment.GROK_HOME;
|
||||
if (environment.XAI_API_KEY?.trim()) return { environment, home: null };
|
||||
delete environment.XAI_API_KEY;
|
||||
const companyHome = resolveManagedGrokHomeDir(input.environment, input.companyId);
|
||||
const home = configuredHome ?? (await grokHomeHasUsableAuth(companyHome)
|
||||
? companyHome : input.remote ? companyHome : join(homedir(), ".grok"));
|
||||
try {
|
||||
environment.PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET = await readLocalAiCredentialFile(join(home, "auth.json"));
|
||||
} catch {
|
||||
throw new Error("Grok subscription login is unavailable. Connect Grok Build or select an xAI API key.");
|
||||
}
|
||||
return { environment, home };
|
||||
}
|
||||
@@ -970,6 +970,9 @@ describe("remote provider pack manifest", () => {
|
||||
const lockfile = "lockfileVersion: '9.0'\n";
|
||||
const opencodeCommand = "#!/bin/sh\n";
|
||||
const opencodeExecutable = "opencode-binary\n";
|
||||
const grokExecutable = "grok-binary\n";
|
||||
await mkdir(join(root, "node_modules/@paperclipai/grok-acp/bin"), { recursive: true });
|
||||
await writeFile(join(root, "node_modules/@paperclipai/grok-acp/bin/grok"), grokExecutable);
|
||||
await writeFile(
|
||||
join(root, "dist", "cli", "opencode-app-server-proxy.cjs"),
|
||||
proxy,
|
||||
@@ -1000,18 +1003,21 @@ describe("remote provider pack manifest", () => {
|
||||
acpx: "0.13.1",
|
||||
claudeAcp: "0.73.0",
|
||||
codexAcp: "1.6.2",
|
||||
grok: "1.0.13",
|
||||
},
|
||||
target: { platform: "linux", architecture: "x64" },
|
||||
runnerSourceRevision: "1".repeat(40),
|
||||
distDigest: sha256DirectoryTree(join(root, "dist")),
|
||||
bridgeDigest: "",
|
||||
acpxProfileDigests: {
|
||||
grok: "sha256:42fe296ec6fc0715c3509cec9671451bcd5bfdc7f185041101c8aac1e9ac8718",
|
||||
claude:
|
||||
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
|
||||
codex:
|
||||
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
|
||||
},
|
||||
artifacts: {
|
||||
grokExecutable: { path: "node_modules/@paperclipai/grok-acp/bin/grok", sha256: digest(grokExecutable) },
|
||||
nodeCommand: {
|
||||
path: "node_modules/node/bin/node",
|
||||
sha256: digest(node),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { readLocalAiCredentialFile } from "../local-ai-credential-file.js";
|
||||
import { prepareGrokRunnerCredentials } from "./grok-runner-credentials.js";
|
||||
import { copyBackGrokAuth } from "@paperclipai/adapter-grok-local/server";
|
||||
import { createNativeGitHubAccess, type NativeGitHubAccess } from "./native-github-access.js";
|
||||
import { resolveGitHubOperationCredentials } from "../github-operation-credentials.js";
|
||||
import { bindManagedNativeCredentialTurn, completeManagedNativeCredentialTurn } from "./managed-native-credentials.js";
|
||||
@@ -5250,7 +5253,10 @@ export function resolveNativeHarnessPersistenceProfile(
|
||||
// nor necessary to restore. Credentials and launch-time config
|
||||
// are also re-materialized in the replacement sandbox.
|
||||
excludeEntries:
|
||||
execution.provider.agent === "codex"
|
||||
execution.provider.agent === "grok"
|
||||
? ["auth.json", "auth-refresh.json", "auth-refresh.json.tmp", "config.toml"].map((entry) =>
|
||||
`acpx/${acpxRuntimeSessionDirectoryName(nativeSessionKey(execution))}/grok-home/${entry}`)
|
||||
: execution.provider.agent === "codex"
|
||||
? CODEX_HOME_NON_PERSISTENT_ENTRIES.map(
|
||||
(entry) =>
|
||||
`acpx/${acpxRuntimeSessionDirectoryName(nativeSessionKey(execution))}/codex-home/${entry}`,
|
||||
@@ -9132,14 +9138,17 @@ const REMOTE_PROVIDER_PACK_PINS = {
|
||||
acpx: "0.13.1",
|
||||
claudeAcp: "0.73.0",
|
||||
codexAcp: "1.6.2",
|
||||
grok: "1.0.13",
|
||||
} as const;
|
||||
const REMOTE_PROVIDER_PACK_PROFILE_DIGESTS = {
|
||||
grok: "sha256:42fe296ec6fc0715c3509cec9671451bcd5bfdc7f185041101c8aac1e9ac8718",
|
||||
claude:
|
||||
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
|
||||
codex:
|
||||
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
|
||||
} as const;
|
||||
const REMOTE_PROVIDER_PACK_ARTIFACT_PATHS = {
|
||||
grokExecutable: "node_modules/@paperclipai/grok-acp/bin/grok",
|
||||
nodeCommand: "node_modules/node/bin/node",
|
||||
productionLock: "pnpm-lock.yaml",
|
||||
opencodeCommand: "node_modules/.bin/opencode",
|
||||
@@ -9159,6 +9168,7 @@ type RemoteProviderPackManifest = {
|
||||
bridgeDigest: string;
|
||||
acpxProfileDigests: typeof REMOTE_PROVIDER_PACK_PROFILE_DIGESTS;
|
||||
artifacts: {
|
||||
grokExecutable: { path: string; sha256: string };
|
||||
nodeCommand: { path: string; sha256: string };
|
||||
productionLock: { path: string; sha256: string };
|
||||
opencodeCommand: { path: string; sha256: string };
|
||||
@@ -9257,6 +9267,7 @@ export function readRemoteProviderPackManifest(
|
||||
);
|
||||
}
|
||||
const artifactEntries = [
|
||||
["Grok executable", payload.artifacts?.grokExecutable, REMOTE_PROVIDER_PACK_ARTIFACT_PATHS.grokExecutable],
|
||||
[
|
||||
"provider Node",
|
||||
payload.artifacts?.nodeCommand,
|
||||
@@ -10680,14 +10691,14 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
"if(canonical(manifest)!==expected)throw new Error('manifest mismatch')",
|
||||
"const hash=(p)=>'sha256:'+crypto.createHash('sha256').update(fs.readFileSync(path.join(root,p))).digest('hex')",
|
||||
"const tree=(treeRoot)=>{const digest=crypto.createHash('sha256');const visit=(directory,prefix='')=>{for(const entry of fs.readdirSync(directory,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name))){const relative=prefix?prefix+'/'+entry.name:entry.name;const absolute=path.join(directory,entry.name);if(entry.isDirectory()){digest.update('directory\\0'+relative+'\\n');visit(absolute,relative)}else if(entry.isFile()){digest.update('file\\0'+relative+'\\0'+'sha256:'+crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex')+'\\n')}else if(entry.isSymbolicLink()){digest.update('symlink\\0'+relative+'\\0'+fs.readlinkSync(absolute)+'\\n')}else throw new Error('unsupported dist entry '+relative)}};visit(treeRoot);return 'sha256:'+digest.digest('hex')}",
|
||||
"for(const name of ['nodeCommand','productionLock','opencodeCommand','opencodeExecutable','opencodeProxy','acpxSidecar']){const artifact=manifest.payload.artifacts[name];if(hash(artifact.path)!==artifact.sha256)throw new Error(name+' digest mismatch')}",
|
||||
"for(const name of ['nodeCommand','productionLock','opencodeCommand','opencodeExecutable','opencodeProxy','acpxSidecar','grokExecutable']){const artifact=manifest.payload.artifacts[name];if(hash(artifact.path)!==artifact.sha256)throw new Error(name+' digest mismatch')}",
|
||||
"if(tree(path.join(root,'dist'))!==manifest.payload.distDigest)throw new Error('dist tree digest mismatch')",
|
||||
"const version=process.versions.node.split('.').map(Number)",
|
||||
"const minimum=manifest.payload.pins.nodeMinimum.split('.').map(Number)",
|
||||
"if(version[0]<minimum[0]||(version[0]===minimum[0]&&(version[1]<minimum[1]||(version[1]===minimum[1]&&version[2]<minimum[2]))))throw new Error('Node version incompatible')",
|
||||
"if(process.platform!==manifest.payload.target.platform||process.arch!==manifest.payload.target.architecture)throw new Error('provider pack target mismatch')",
|
||||
"const packageVersion=(pkg)=>JSON.parse(fs.readFileSync(path.join(root,'node_modules',...pkg.split('/'),'package.json'),'utf8')).version",
|
||||
"const expectedPackages={acpx:manifest.payload.pins.acpx,'@agentclientprotocol/claude-agent-acp':manifest.payload.pins.claudeAcp,'@agentclientprotocol/codex-acp':manifest.payload.pins.codexAcp,'opencode-ai':manifest.payload.pins.opencode}",
|
||||
"const expectedPackages={acpx:manifest.payload.pins.acpx,'@agentclientprotocol/claude-agent-acp':manifest.payload.pins.claudeAcp,'@agentclientprotocol/codex-acp':manifest.payload.pins.codexAcp,'opencode-ai':manifest.payload.pins.opencode,'@paperclipai/grok-acp':manifest.payload.pins.grok}",
|
||||
"for(const [pkg,version] of Object.entries(expectedPackages))if(packageVersion(pkg)!==version)throw new Error(pkg+' version mismatch')",
|
||||
].join(";");
|
||||
const verified = await remoteCommandRunner.execute({
|
||||
@@ -11975,9 +11986,15 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
},
|
||||
}
|
||||
: input.execution;
|
||||
const effectiveRunnerEnvironmentBase: NodeJS.ProcessEnv = {
|
||||
...(input.runnerEnvironment ?? process.env),
|
||||
const isGrok = input.execution.provider.kind === "acpx" && input.execution.provider.agent === "grok";
|
||||
let effectiveRunnerEnvironmentBase: NodeJS.ProcessEnv = {
|
||||
...(input.runnerEnvironment ?? (isGrok ? {} : process.env)),
|
||||
};
|
||||
const grokCredential = isGrok ? await prepareGrokRunnerCredentials({
|
||||
companyId: input.execution.binding.companyId, environment: effectiveRunnerEnvironmentBase, remote: Boolean(remoteTarget),
|
||||
managedHome: input.managedAiCredentialHome,
|
||||
}) : null;
|
||||
if (grokCredential) effectiveRunnerEnvironmentBase = grokCredential.environment;
|
||||
// This authority bit is derived only from the selected execution target.
|
||||
// Never let an agent, environment binding, or host variable disable the
|
||||
// Codex sandbox for a local runner by supplying the same key.
|
||||
@@ -12547,6 +12564,38 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
});
|
||||
const boundManagedSessions = new WeakSet<NativeSession>();
|
||||
const wrapManagedSession = (session: NativeSession): NativeSession => {
|
||||
if (isGrok && grokCredential?.home && !boundManagedSessions.has(session)) {
|
||||
boundManagedSessions.add(session);
|
||||
const relativeHome = `acpx/${acpxRuntimeSessionDirectoryName(nativeSessionKey(input.execution))}/grok-home`;
|
||||
const localHome = join(root, "acpx", relativeHome);
|
||||
const remoteHome = remoteRunnerFilesystemRoot ? posix.join(remoteRunnerFilesystemRoot, "acpx", relativeHome) : null;
|
||||
const readAuth = async (name: string): Promise<Buffer> => {
|
||||
if (!remoteHome || !remoteCommandRunner) return Buffer.from(await readLocalAiCredentialFile(join(localHome, name)));
|
||||
const script = `const fs=require('node:fs'),path=require('node:path');let fd;try{const file=process.argv[1];let parent=path.dirname(file);while(true){if(!fs.lstatSync(parent).isDirectory())throw Error('directory');const next=path.dirname(parent);if(next===parent)break;parent=next;}fd=fs.openSync(file,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW);const st=fs.fstatSync(fd);if(!st.isFile()||st.uid!==process.getuid()||(st.mode&511)!==384||st.size>65536)throw Error('credential');const b=Buffer.alloc(65537);let n=0;while(n<b.length){const k=fs.readSync(fd,b,n,b.length-n,n);if(!k)break;n+=k;}if(n>65536)throw Error('size');process.stdout.write(b.subarray(0,n).toString('base64'));b.fill(0);}catch(e){process.exitCode=e.code==='ENOENT'?66:1;}finally{if(fd!==undefined)fs.closeSync(fd);}`;
|
||||
const result = await remoteCommandRunner.execute({ command: "node", args: ["-e", script, posix.join(remoteHome, name)], bypassSession: true, timeoutMs: 10000 });
|
||||
if (result.exitCode !== 0 || result.timedOut) throw Object.assign(new Error("Grok credential refresh handoff unavailable"), { code: result.exitCode === 66 ? "ENOENT" : "INVALID_CREDENTIAL" });
|
||||
return Buffer.from(result.stdout, "base64");
|
||||
};
|
||||
return bindManagedNativeCredentialTurn(session, {
|
||||
copyBack: async () => {
|
||||
await copyBackGrokAuth({ hostHomeDir: grokCredential.home!, log: () => {},
|
||||
readSandboxAuth: () => readAuth("auth.json").catch((error) => {
|
||||
if (error.code !== "ENOENT") throw error;
|
||||
return readAuth("auth-refresh.json");
|
||||
}),
|
||||
});
|
||||
},
|
||||
remove: async () => {
|
||||
for (const name of ["auth.json", "auth-refresh.json", "auth-refresh.json.tmp"]) {
|
||||
rmSync(join(localHome, name), { force: true });
|
||||
if (remoteHome && remoteCommandRunner) {
|
||||
const result = await remoteCommandRunner.execute({ command: "rm", args: ["-f", "--", posix.join(remoteHome, name)], bypassSession: true, timeoutMs: 10000 });
|
||||
if (result.exitCode !== 0 || result.timedOut) throw new Error("Grok credential cleanup failed");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!input.managedAiCredentialHome || input.execution.provider.kind !== "codex" || boundManagedSessions.has(session)) return session;
|
||||
boundManagedSessions.add(session);
|
||||
const remoteAuth = remoteRunnerFilesystemRoot ? posix.join(remoteRunnerFilesystemRoot, "codex-home", "auth.json") : null;
|
||||
|
||||
@@ -15,6 +15,7 @@ export const DEFAULT_OPENCODE_RUNNER_MODEL =
|
||||
export const CLAUDE_MANAGED_BETA_VERSION = "managed-agents-2026-04-01" as const;
|
||||
|
||||
export const QUALIFIED_ACPX_RUNNER_MODELS = {
|
||||
grok: "grok-4.7",
|
||||
claude: "claude-sonnet-5",
|
||||
codex: "gpt-5.6-sol",
|
||||
} as const;
|
||||
@@ -404,7 +405,7 @@ export function resolvePaperclipRunnerProviderProfile(
|
||||
}
|
||||
|
||||
const acpxAgent = config.acpxAgent ?? "claude";
|
||||
if (acpxAgent !== "claude" && acpxAgent !== "codex") {
|
||||
if (acpxAgent !== "claude" && acpxAgent !== "codex" && acpxAgent !== "grok") {
|
||||
throw new PaperclipRunnerProviderProfileError(
|
||||
"paperclip_runner_acpx_agent_unavailable",
|
||||
"Paperclip Runner ACPX requires the qualified Claude or Codex agent profile; Pi is not available.",
|
||||
|
||||
Reference in new issue
Block a user