refactor(server): delegate ACPX model admission to the runner

Keep product defaults separate from qualification metadata and preserve explicit Cursor and Codex ACP model selection.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-06 11:59:16 -05:00
1 parent 30875812d2
commit 6f4a5e9e2a
4 files changed
+36 -13

No files matched your search

@@ -33,6 +33,8 @@ TypeScript profile registry, provider-pack builder, server pack verifier, and
Cursor installation code. `packages/paperclip-runner/acpx-profiles.json` now owns
the release/profile declarations; generated TypeScript and named Rust structs
share them. Model admission is explicit and separate from qualification examples.
Server admission calls the same resolver. Existing Claude/Grok product defaults
are named separately; Cursor and Codex ACP still require explicit selection.
Cursor closure pins are generated from the existing distribution manifest.
Build and typecheck reject declaration drift, including mismatches with the
Cursor release attestation and installed dependencies.
+4 -4
View File
@@ -138,7 +138,7 @@ import { httpAdapter } from "./http/index.js";
import {
DEFAULT_OPENCODE_RUNNER_MODEL,
PaperclipRunnerProviderProfileError,
QUALIFIED_ACPX_RUNNER_MODELS,
DEFAULT_ACPX_RUNNER_MODELS,
QUALIFIED_OPENCODE_RUNNER_VERSION,
resolvePaperclipRunnerProviderProfile,
} from "../services/native-runtime/provider-profile.js";
@@ -490,19 +490,19 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
models: [
...codexModels,
{ id: DEFAULT_OPENCODE_RUNNER_MODEL, label: "OpenRouter · DeepSeek V4 Flash 0731" },
{ id: QUALIFIED_ACPX_RUNNER_MODELS.claude, label: "Claude Sonnet 5" },
{ id: DEFAULT_ACPX_RUNNER_MODELS.claude, label: "Claude Sonnet 5" },
{ id: "global.anthropic.claude-sonnet-4-6", label: "Amazon Bedrock · Claude Sonnet 4.6 (global)" },
],
listModels: async () => [
...await listCodexModels(),
{ id: DEFAULT_OPENCODE_RUNNER_MODEL, label: "OpenRouter · DeepSeek V4 Flash 0731" },
{ id: QUALIFIED_ACPX_RUNNER_MODELS.claude, label: "Claude Sonnet 5" },
{ id: DEFAULT_ACPX_RUNNER_MODELS.claude, label: "Claude Sonnet 5" },
{ id: "global.anthropic.claude-sonnet-4-6", label: "Amazon Bedrock · Claude Sonnet 4.6 (global)" },
],
refreshModels: async () => [
...await refreshCodexModels(),
{ id: DEFAULT_OPENCODE_RUNNER_MODEL, label: "OpenRouter · DeepSeek V4 Flash 0731" },
{ id: QUALIFIED_ACPX_RUNNER_MODELS.claude, label: "Claude Sonnet 5" },
{ id: DEFAULT_ACPX_RUNNER_MODELS.claude, label: "Claude Sonnet 5" },
{ id: "global.anthropic.claude-sonnet-4-6", label: "Amazon Bedrock · Claude Sonnet 4.6 (global)" },
],
supportsLocalAgentJwt: false,
@@ -43,6 +43,24 @@ describe("host ACPX qualification admission", () => {
it("does not alter existing qualified providers", () => {
expect(resolveAcpxQualification({ ...provider, agent: "codex" } as typeof provider, authorize([]))).toBeUndefined();
});
it.each(["claude", "grok", "cursor"])("keeps %s model selection open to native verification", (acpxAgent) => {
expect(resolvePaperclipRunnerProviderProfile({ provider: "acpx", acpxAgent, model: "explicit-new-model" }))
.toMatchObject({ acpxAgent, model: "explicit-new-model" });
});
it("preserves exact Codex ACP admission and explicit selection", () => {
expect(resolvePaperclipRunnerProviderProfile({ provider: "acpx", acpxAgent: "codex", model: "gpt-5.6-sol" }))
.toMatchObject({ acpxAgent: "codex", model: "gpt-5.6-sol" });
for (const model of [undefined, "", "different-model"]) {
expect(() => resolvePaperclipRunnerProviderProfile({ provider: "acpx", acpxAgent: "codex", model }))
.toThrow(expect.objectContaining({ code: "paperclip_runner_acpx_model_unqualified" }));
}
});
it("preserves product defaults separately from qualification examples", () => {
for (const [acpxAgent, model] of [["claude", "claude-sonnet-5"], ["grok", "grok-4.7"]]) {
expect(resolvePaperclipRunnerProviderProfile({ provider: "acpx", acpxAgent }))
.toMatchObject({ acpxAgent, model });
}
});
it("binds the executor to host process environment rather than agent configuration", () => {
const source = readFileSync(new URL("./native-session-executor.ts", import.meta.url), "utf8");
expect(source).toContain("resolveAcpxQualification(input.execution.provider, process.env)");
@@ -1,3 +1,4 @@
import { resolveQualifiedAcpxProfile } from "../../vendor/paperclip-runner/index.js";
import {
isPaperclipRunnerProvider,
PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES,
@@ -21,16 +22,16 @@ export const DEFAULT_OPENCODE_RUNNER_MODEL =
"openrouter/deepseek/deepseek-v4-flash-0731" as const;
export const CLAUDE_MANAGED_BETA_VERSION = "managed-agents-2026-04-01" as const;
export const QUALIFIED_ACPX_RUNNER_MODELS = {
export const DEFAULT_ACPX_RUNNER_MODELS = {
grok: "grok-4.7",
claude: "claude-sonnet-5",
codex: "gpt-5.6-sol",
// Cursor models are explicit and provider-verified; there is no default.
cursor: "",
// These profiles require explicit configuration. Admission belongs to the runner.
codex: null,
cursor: null,
} as const;
export type QualifiedPaperclipRunnerAcpxAgent =
keyof typeof QUALIFIED_ACPX_RUNNER_MODELS;
keyof typeof DEFAULT_ACPX_RUNNER_MODELS;
type AdmittedPaperclipRunnerAcpxAgent = QualifiedPaperclipRunnerAcpxAgent | AcpxQualificationCandidate;
export type PaperclipRunnerProviderProfile =
@@ -473,17 +474,19 @@ export function resolvePaperclipRunnerProviderProfile(
"Paperclip Runner ACPX requires a qualified agent profile.",
);
}
const qualifiedModel = QUALIFIED_ACPX_RUNNER_MODELS[acpxAgent];
if (acpxAgent === "codex" && model !== qualifiedModel) {
const selectedModel = model || DEFAULT_ACPX_RUNNER_MODELS[acpxAgent] || "";
try {
resolveQualifiedAcpxProfile(acpxAgent, selectedModel);
} catch (error) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_acpx_model_unqualified",
`Paperclip Runner ACPX ${acpxAgent} requires exact model ${qualifiedModel}.`,
error instanceof Error ? error.message : "Paperclip Runner ACPX requires an admitted model.",
);
}
return {
provider: "acpx",
backend: "acpx_runtime",
model: model || qualifiedModel,
model: selectedModel,
acpxAgent,
};
}