Apply exact host ACP qualification at agent admission and execution

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 14:39:56 -05:00
1 parent 97bd3112de
commit 26dde3cfe9
7 files changed
+63 -12

No files matched your search

@@ -331,6 +331,21 @@ describe("server adapter registry", () => {
checks: [{ code: "paperclip_runner_acpx_agent_unavailable" }],
});
});
it("reports qualification-only readiness for an exact host-authorized candidate", async () => {
const key = "PAPERCLIP_RUNNER_ACPX_QUALIFICATION";
const previous = process.env[key];
process.env[key] = JSON.stringify([{ agent: "cursor", model: "exact-model" }]);
try {
const result = await requireServerAdapter("paperclip_runner").testEnvironment({
companyId: "company-1", adapterType: "paperclip_runner",
config: { provider: "acpx", acpxAgent: "cursor", model: "exact-model" },
});
expect(result).toMatchObject({ status: "warn", checks: [{ code: "acpx_candidate_qualification_only" }] });
} finally {
if (previous === undefined) delete process.env[key];
else process.env[key] = previous;
}
});
it("wraps built-in npm runtime installs with the sandbox-aware install helper", () => {
const expectedClaudeInstall = `if ! command -v 'claude' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("@anthropic-ai/claude-code")}; fi`;
const expectedCodexInstall = `if ! command -v 'codex' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("@openai/codex")}; fi`;
+9
View File
@@ -404,6 +404,15 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
};
}
if (profile.provider === "acpx") {
if (["cursor", "copilot", "pi"].includes(profile.acpxAgent)) {
// The profile resolver already validated the isolated host's exact
// qualification pair. Do not report a production readiness pass.
return {
adapterType: "paperclip_runner", status: "warn" as const, testedAt: new Date().toISOString(),
checks: [{ code: "acpx_candidate_qualification_only", level: "warn" as const,
message: "This exact candidate and model are admitted for operator-controlled qualification only. Verified runtime installation, bound credentials, and model access are checked before execution; production support remains pending." }],
};
}
try {
if (profile.acpxAgent !== "claude") throw new Error("Select Codex to use the native Codex runner.");
const target = context.executionTarget;
@@ -1,11 +1,24 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { ACPX_QUALIFICATION_ENV, resolveAcpxQualification } from "./acpx-qualification.js";
import type { NativeExecutionInput } from "../../vendor/paperclip-runner/index.js";
import { resolvePaperclipRunnerProviderProfile, resolvePaperclipRunnerNativeProviderInput } from "./provider-profile.js";
const provider = { kind: "acpx", agent: "cursor", model: "exact-model", permissionMode: "approve-all" } as NativeExecutionInput["provider"];
const authorize = (value: unknown) => ({ [ACPX_QUALIFICATION_ENV]: JSON.stringify(value) });
describe("host ACPX qualification admission", () => {
afterEach(() => vi.unstubAllEnvs());
it.each(["cursor", "copilot", "pi"])("admits %s through agent validation and native input only for the exact host pair", (agent) => {
const config = { provider: "acpx", acpxAgent: agent, model: "exact-model" };
vi.stubEnv(ACPX_QUALIFICATION_ENV, undefined);
expect(() => resolvePaperclipRunnerProviderProfile(config)).toThrow(expect.objectContaining({ code: "paperclip_runner_acpx_agent_unavailable" }));
expect(() => resolvePaperclipRunnerProviderProfile({ ...config, env: authorize([{ agent, model: config.model }]), [ACPX_QUALIFICATION_ENV]: JSON.stringify([{ agent, model: config.model }]) })).toThrow(expect.objectContaining({ code: "paperclip_runner_acpx_agent_unavailable" }));
vi.stubEnv(ACPX_QUALIFICATION_ENV, JSON.stringify([{ agent, model: config.model }]));
expect(resolvePaperclipRunnerProviderProfile(config)).toMatchObject({ acpxAgent: agent, model: config.model });
expect(resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig: config })).toMatchObject({ acpxAgent: agent, model: config.model });
expect(() => resolvePaperclipRunnerProviderProfile({ ...config, model: "other-model" })).toThrow(expect.objectContaining({ code: "paperclip_runner_acpx_qualification_invalid" }));
expect(() => resolvePaperclipRunnerProviderProfile({ ...config, model: "" })).toThrow(expect.objectContaining({ code: "paperclip_runner_acpx_model_required" }));
});
it("keeps normal candidate execution closed and admits only the exact operator pair", () => {
expect(resolveAcpxQualification(provider, {})).toBeUndefined();
expect(resolveAcpxQualification(provider, authorize([{ agent: "cursor", model: "exact-model" }]))).toBe("cursor");
@@ -2,12 +2,12 @@ import type { NativeExecutionInput } from "../../vendor/paperclip-runner/index.j
/** Operator-only admission for an exact candidate/model during qualification. */
export const ACPX_QUALIFICATION_ENV = "PAPERCLIP_RUNNER_ACPX_QUALIFICATION";
type Candidate = "cursor" | "copilot" | "pi";
export type AcpxQualificationCandidate = "cursor" | "copilot" | "pi";
export function resolveAcpxQualification(
provider: NativeExecutionInput["provider"],
provider: NativeExecutionInput["provider"] | { kind: "acpx"; agent: string; model: string },
hostEnvironment: NodeJS.ProcessEnv,
): Candidate | undefined {
): AcpxQualificationCandidate | undefined {
if (provider.kind !== "acpx" || !["cursor", "copilot", "pi"].includes(provider.agent)) return undefined;
const encoded = hostEnvironment[ACPX_QUALIFICATION_ENV];
if (!encoded) return undefined;
@@ -28,5 +28,5 @@ export function resolveAcpxQualification(
if (!entries.some(entry => entry.agent === provider.agent && entry.model === provider.model)) {
throw new Error("ACPX qualification requires the explicitly authorized candidate and exact model");
}
return provider.agent as Candidate;
return provider.agent as AcpxQualificationCandidate;
}
@@ -7639,11 +7639,11 @@ describe("native process ownership", () => {
},
);
it("rejects ACPX Pi before constructing a backend", async () => {
it.each(["pi", "cursor", "copilot"])("rejects ACPX candidate %s without host authorization before constructing a backend", async (agent) => {
const piExecution = {
...execution,
binding: { ...execution.binding, runId: "run-acpx-pi-rejected" },
provider: { kind: "acpx", agent: "pi", model: "pi-model" },
provider: { kind: "acpx", agent, model: "pi-model" },
session: { ...execution.session, driverKind: "acpx_runtime" },
} as unknown as NativeExecutionInputV1;
state.createBackend.mockClear();
@@ -7654,7 +7654,7 @@ describe("native process ownership", () => {
execution: piExecution,
runnerInstanceId: "runner",
}),
).rejects.toThrow("descriptor-confined verified launch");
).rejects.toThrow("exact host qualification authorization");
expect(state.createBackend).not.toHaveBeenCalled();
});
});
@@ -7297,10 +7297,11 @@ async function executePaperclipNativeSessionWithinScope(
}
if (
input.execution.provider.kind === "acpx" &&
input.execution.provider.agent === "pi"
["pi", "cursor", "copilot"].includes(input.execution.provider.agent) &&
!resolveAcpxQualification(input.execution.provider, process.env)
) {
throw new Error(
"paperclip_runner_provider_unsupported: ACPX Pi is unavailable until descriptor-confined verified launch is implemented",
"paperclip_runner_provider_unsupported: ACPX candidate requires exact host qualification authorization",
);
}
const preparationSpans = input.preparationSpans ?? [];
@@ -9,6 +9,7 @@ import {
AGENTCORE_QUALIFIED_MODEL,
CLAUDE_MANAGED_QUALIFIED_MODEL,
} from "../provider-profile-qualification.js";
import { resolveAcpxQualification, type AcpxQualificationCandidate } from "./acpx-qualification.js";
export const QUALIFIED_OPENCODE_RUNNER_VERSION = "1.18.32" as const;
export const DEFAULT_OPENCODE_RUNNER_MODEL =
@@ -22,6 +23,7 @@ export const QUALIFIED_ACPX_RUNNER_MODELS = {
export type QualifiedPaperclipRunnerAcpxAgent =
keyof typeof QUALIFIED_ACPX_RUNNER_MODELS;
type AdmittedPaperclipRunnerAcpxAgent = QualifiedPaperclipRunnerAcpxAgent | AcpxQualificationCandidate;
export type PaperclipRunnerProviderProfile =
| {
@@ -52,7 +54,7 @@ export type PaperclipRunnerProviderProfile =
provider: "acpx";
backend: "acpx_runtime";
model: string;
acpxAgent: QualifiedPaperclipRunnerAcpxAgent;
acpxAgent: AdmittedPaperclipRunnerAcpxAgent;
};
export type PaperclipRunnerNativeProviderInput =
@@ -109,7 +111,7 @@ export type PaperclipRunnerNativeProviderInput =
| {
provider: "acpx";
model: string;
acpxAgent: QualifiedPaperclipRunnerAcpxAgent;
acpxAgent: AdmittedPaperclipRunnerAcpxAgent;
acpxPermissionMode: "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all";
};
@@ -408,6 +410,17 @@ export function resolvePaperclipRunnerProviderProfile(
const pendingAcpxProfile = PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === acpxAgent && !profile.qualified);
if (pendingAcpxProfile) {
if (!model) throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_model_required", `${pendingAcpxProfile.label} requires an explicit model ID; there is no default model.`);
// Qualification authority belongs to the isolated server operator. Never
// read this allowlist from adapter config, credential refs, or run env.
let qualification: AcpxQualificationCandidate | undefined;
try {
qualification = resolveAcpxQualification({ kind: "acpx", agent: pendingAcpxProfile.value, model }, process.env);
} catch {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_qualification_invalid", "ACPX qualification requires an exact host-authorized candidate and model.");
}
if (qualification) {
return { provider: "acpx", backend: "acpx_runtime", model, acpxAgent: qualification };
}
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_agent_unavailable", `${pendingAcpxProfile.label} is awaiting local and Daytona qualification. Its profile is not enabled for production runs.`);
}
if (acpxAgent !== "claude" && acpxAgent !== "codex") {