mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 10:13:11 +02:00
Add explicit extended ACP Product E2E qualification suite
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
766dda2b4f
commit
af06ada99f
17 files changed
+295
-15
No files matched your search
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } 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";
|
||||
|
||||
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", () => {
|
||||
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");
|
||||
for (const entries of [[{ agent: "copilot", model: "exact-model" }], [{ agent: "cursor", model: "other-model" }]]) {
|
||||
expect(() => resolveAcpxQualification(provider, authorize(entries))).toThrow("exact model");
|
||||
}
|
||||
});
|
||||
it.each([[], {}, [{ agent: "cursor", model: "" }], [{ agent: "cursor", model: " exact-model" }],
|
||||
[{ agent: "cursor", model: "exact-model", allowAll: true }], [{ agent: "claude", model: "exact-model" }],
|
||||
[{ agent: "cursor", model: "exact-model" }, { agent: "cursor", model: "exact-model" }],
|
||||
])("rejects malformed or broad authorization %j", entries => {
|
||||
expect(() => resolveAcpxQualification(provider, authorize(entries))).toThrow("Invalid ACPX");
|
||||
});
|
||||
it("does not alter existing qualified providers", () => {
|
||||
expect(resolveAcpxQualification({ ...provider, agent: "codex" } as typeof provider, authorize([]))).toBeUndefined();
|
||||
});
|
||||
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)");
|
||||
expect(source).not.toContain("resolveAcpxQualification(input.execution.provider, effectiveRunnerEnvironment)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { NativeExecutionInput } from "../../vendor/paperclip-runner/index.js";
|
||||
|
||||
/** 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 function resolveAcpxQualification(
|
||||
provider: NativeExecutionInput["provider"],
|
||||
hostEnvironment: NodeJS.ProcessEnv,
|
||||
): Candidate | undefined {
|
||||
if (provider.kind !== "acpx" || !["cursor", "copilot", "pi"].includes(provider.agent)) return undefined;
|
||||
const encoded = hostEnvironment[ACPX_QUALIFICATION_ENV];
|
||||
if (!encoded) return undefined;
|
||||
let entries: unknown;
|
||||
try { entries = JSON.parse(encoded); } catch { throw new Error("Invalid ACPX qualification authorization"); }
|
||||
if (!Array.isArray(entries) || entries.length < 1 || entries.length > 3) {
|
||||
throw new Error("Invalid ACPX qualification authorization");
|
||||
}
|
||||
const agents = new Set<string>();
|
||||
for (const entry of entries) {
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)
|
||||
|| Object.keys(entry).sort().join(",") !== "agent,model"
|
||||
|| !["cursor", "copilot", "pi"].includes(entry.agent)
|
||||
|| typeof entry.model !== "string" || !entry.model.trim() || entry.model !== entry.model.trim()
|
||||
|| agents.has(entry.agent)) throw new Error("Invalid ACPX qualification authorization");
|
||||
agents.add(entry.agent);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveAcpxQualification } from "./acpx-qualification.js";
|
||||
import {
|
||||
isSupportedRemoteCodexVersion,
|
||||
parseCodexCliVersion,
|
||||
@@ -12258,6 +12259,8 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
...(input.execution.provider.kind === "acpx"
|
||||
? {
|
||||
acpxAgent: input.execution.provider.agent,
|
||||
// Read only the server operator environment, never agent/runtime env.
|
||||
acpxCandidateProfile: resolveAcpxQualification(input.execution.provider, process.env),
|
||||
acpxPermissionMode: input.execution.provider.permissionMode,
|
||||
acpxPermissionModePinned:
|
||||
input.execution.schema ===
|
||||
|
||||
Reference in new issue
Block a user