diff --git a/packages/adapter-utils/src/paperclip-runner-permissions.ts b/packages/adapter-utils/src/paperclip-runner-permissions.ts index ed9b2dee71..c807a42608 100644 --- a/packages/adapter-utils/src/paperclip-runner-permissions.ts +++ b/packages/adapter-utils/src/paperclip-runner-permissions.ts @@ -125,7 +125,7 @@ export const PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES = { value: "approve-reads", label: "Allow Paperclip reads", description: - "Automatically allow assigned Paperclip read tools. Other operations stop with an approval-required message because this runner has no interactive approval handler.", + "Automatically allow assigned Paperclip read tools. Other operations request a supported permission decision. Company permissions and execution boundaries still apply.", }, { value: "deny-all", diff --git a/packages/paperclip-runner/protocol/manifest.json b/packages/paperclip-runner/protocol/manifest.json index 251ffd0d25..494781b88b 100644 --- a/packages/paperclip-runner/protocol/manifest.json +++ b/packages/paperclip-runner/protocol/manifest.json @@ -65,7 +65,7 @@ { "path": "schemas/provider-descriptor.schema.json", "id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json", - "sha256": "a56c0c8501171c34bb72f6fba08d3872839a8b45bbc10daa93620afc9dc2b669" + "sha256": "cb0f96f789536bb330aa6a773d8e5726fbbb844e6d5e2bad918ea9a5c18ff024" }, { "path": "schemas/provider-event.schema.json", diff --git a/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json b/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json index 53c174d983..5e516af3ae 100644 --- a/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json +++ b/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json @@ -18,7 +18,7 @@ "endpointQualifier": { "type": "string", "minLength": 1, "maxLength": 240 }, "memoryId": { "type": "string", "minLength": 1, "maxLength": 240 }, "eventExpiryDays": { "const": 90 }, - "agent": { "enum": ["pi", "claude", "codex"] }, + "agent": { "enum": ["pi", "claude", "codex", "cursor", "copilot"] }, "requestedModel": { "type": "string", "minLength": 1, "maxLength": 240 }, "acpProtocolVersion": { "const": 1 }, "agentServerPackage": { "type": "string", "minLength": 1, "maxLength": 240 }, diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts index b5a31b85f7..fdf5185c6e 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.ts @@ -77,7 +77,7 @@ export interface NativeAwsAgentCoreProfileSnapshot { eventExpiryDays: 90; } -export type NativeAcpxAgent = "pi" | "claude" | "codex"; +export type NativeAcpxAgent = "pi" | "claude" | "codex" | "cursor" | "copilot"; export type NativeCodexApprovalPolicy = "never" | "on-request" | "untrusted"; export type NativeOpenCodePermissionMode = "allow" | "ask" | "deny"; export type NativeAcpxPermissionMode = "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all"; @@ -87,7 +87,7 @@ export interface NativeAcpxProfileSnapshot { protocolVersion: 1; acpxVersion: "0.13.1"; agent: NativeAcpxAgent; - agentProfileVersion: 1; + agentProfileVersion: 1 | 2; agentServerPackage: string; agentServerVersion: string; agentRuntimePackage: string | null; @@ -530,8 +530,8 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput if (providerModel === null) { throw new NativeExecutionInputError("input.provider.model is required for acpx"); } - if (provider.agent !== "pi" && provider.agent !== "claude" && provider.agent !== "codex") { - throw new NativeExecutionInputError("input.provider.agent must be pi, claude, or codex"); + if (provider.agent !== "pi" && provider.agent !== "claude" && provider.agent !== "codex" && provider.agent !== "cursor" && provider.agent !== "copilot") { + throw new NativeExecutionInputError("input.provider.agent must be pi, claude, codex, cursor, or copilot"); } if (isV4) { if (provider.permissionMode !== "approve-all" && provider.permissionMode !== "approve-paperclip" && provider.permissionMode !== "approve-reads" && provider.permissionMode !== "deny-all") { @@ -558,7 +558,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput || profile.protocolVersion !== 1 || profile.acpxVersion !== "0.13.1" || profile.agent !== provider.agent - || profile.agentProfileVersion !== 1 + || (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2) ) { throw new NativeExecutionInputError("input.provider.profile does not match the qualified ACPX v1 profile"); } @@ -579,7 +579,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput protocolVersion: 1, acpxVersion: "0.13.1", agent: provider.agent, - agentProfileVersion: 1, + agentProfileVersion: profile.agentProfileVersion, agentServerPackage: text(profile.agentServerPackage, "input.provider.profile.agentServerPackage"), agentServerVersion: text(profile.agentServerVersion, "input.provider.profile.agentServerVersion"), agentRuntimePackage: runtimePackage, diff --git a/packages/paperclip-runner/src/drivers/acpx/capability-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/capability-profiles.ts new file mode 100644 index 0000000000..27d6d9e61e --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/capability-profiles.ts @@ -0,0 +1,61 @@ +import type { QualifiedAcpxAgent } from "./qualified-profiles.js"; + +export interface AcpxCapabilityProfile { + readonly displayName: string; + readonly qualification: "qualified" | "pending"; + readonly models: "explicit-provider-verified" | "exact-qualified"; + readonly permissions: "runner-policy" | "interactive"; + readonly questions: "form" | "cursor-extension" | "not-exposed"; + readonly plans: "native" | "cursor-decision" | "semantic-only"; + readonly tools: "authenticated-mcp" | "owned-extension"; + readonly recovery: "session-load" | "unverified"; + readonly usage: "reported" | "unverified"; + readonly steering: "unsupported" | "owned-extension-pending"; + readonly followUp: "controller-queue" | "owned-extension-pending"; + readonly artifacts: "policy_disabled" | "references-pending"; + readonly extensionRequests: readonly string[]; + readonly extensionNotifications: readonly string[]; +} + +/** These are runner integration claims, not a proxy for everything a harness can do. */ +export const ACPX_CAPABILITY_PROFILES: Readonly> = { + claude: { + displayName: "Claude", qualification: "qualified", models: "explicit-provider-verified", + permissions: "interactive", questions: "form", plans: "native", tools: "authenticated-mcp", + recovery: "session-load", usage: "reported", steering: "unsupported", followUp: "controller-queue", + artifacts: "policy_disabled", extensionRequests: [], extensionNotifications: [], + }, + codex: { + displayName: "Codex", qualification: "qualified", models: "exact-qualified", + permissions: "runner-policy", questions: "form", plans: "native", tools: "authenticated-mcp", + recovery: "session-load", usage: "reported", steering: "unsupported", followUp: "controller-queue", + artifacts: "policy_disabled", extensionRequests: [], extensionNotifications: [], + }, + cursor: { + displayName: "Cursor", qualification: "pending", models: "explicit-provider-verified", + permissions: "interactive", questions: "cursor-extension", plans: "cursor-decision", tools: "authenticated-mcp", + recovery: "session-load", usage: "unverified", steering: "unsupported", followUp: "controller-queue", + artifacts: "references-pending", + extensionRequests: ["cursor/ask_question", "cursor/create_plan", "cursor/update_todos", "cursor/task", "cursor/generate_image"], + extensionNotifications: ["cursor/update_todos", "cursor/task", "cursor/generate_image"], + }, + copilot: { + displayName: "GitHub Copilot", qualification: "pending", models: "explicit-provider-verified", + permissions: "interactive", questions: "not-exposed", plans: "native", tools: "authenticated-mcp", + recovery: "session-load", usage: "reported", steering: "unsupported", followUp: "controller-queue", + artifacts: "references-pending", extensionRequests: [], + extensionNotifications: ["github.com/copilot/sessionEvent"], + }, + pi: { + displayName: "Pi", qualification: "pending", models: "exact-qualified", + permissions: "interactive", questions: "form", plans: "semantic-only", tools: "owned-extension", + recovery: "session-load", usage: "reported", steering: "owned-extension-pending", followUp: "owned-extension-pending", + artifacts: "references-pending", extensionRequests: [], extensionNotifications: [], + }, +}; +for (const profile of Object.values(ACPX_CAPABILITY_PROFILES)) { + Object.freeze(profile.extensionRequests); + Object.freeze(profile.extensionNotifications); + Object.freeze(profile); +} +Object.freeze(ACPX_CAPABILITY_PROFILES); diff --git a/packages/paperclip-runner/src/drivers/acpx/driver-profile.test.ts b/packages/paperclip-runner/src/drivers/acpx/driver-profile.test.ts index acf5efce76..00181ffbad 100644 --- a/packages/paperclip-runner/src/drivers/acpx/driver-profile.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/driver-profile.test.ts @@ -84,7 +84,7 @@ describe("ACPX driver profile", () => { }), ).toMatchObject({ ok: false, - issues: [{ path: "agent", code: "invalid_agent" }], + issues: [{ path: "agent", code: "qualification_pending" }], }); expect( validateAcpxDriverConfig({ diff --git a/packages/paperclip-runner/src/drivers/acpx/driver-profile.ts b/packages/paperclip-runner/src/drivers/acpx/driver-profile.ts index 7fe49dcbac..22d22c4f35 100644 --- a/packages/paperclip-runner/src/drivers/acpx/driver-profile.ts +++ b/packages/paperclip-runner/src/drivers/acpx/driver-profile.ts @@ -12,7 +12,9 @@ import { type QualifiedAcpxAgent, } from "./qualified-profiles.js"; -const ACPX_AGENTS = ["claude", "codex"] as const; +import { ACPX_CAPABILITY_PROFILES } from "./capability-profiles.js"; + +const ACPX_AGENTS = ["claude", "codex", "pi", "cursor", "copilot"] as const; const ACPX_PERMISSION_MODES = [ "approve-all", "approve-paperclip", @@ -30,11 +32,12 @@ export interface ValidatedAcpxDriverConfig extends Record { export function acpxCapabilities( agent: QualifiedAcpxAgent, ): NativeSessionCapabilities { + const profile = ACPX_CAPABILITY_PROFILES[agent]; return { - resume: true, + resume: profile.recovery === "session-load", typedEvents: true, typedEventFamilies: providerFamilyCapabilities({ - plan: agent === "pi" ? "unsupported" : "available", + plan: profile.plans === "semantic-only" ? "unsupported" : "available", tool_execution: "available", model_identity: "available", review: "available", @@ -46,9 +49,9 @@ export function acpxCapabilities( structuredResult: true, read: true, reconciliation: true, - usage: true, + usage: profile.usage === "reported", dynamicTools: true, - runtimeRequestResolution: true, + runtimeRequestResolution: profile.permissions === "interactive" || profile.questions === "form", runtimeRequestHandoff: true, goals: false, threadLineage: false, @@ -96,9 +99,12 @@ export function validateAcpxDriverConfig( return invalid( "agent", "invalid_agent", - "ACPX agent must be claude or codex.", + "ACPX agent must be claude, codex, cursor, copilot, or pi.", ); } + if (ACPX_CAPABILITY_PROFILES[agent].qualification !== "qualified") { + return invalid("agent", "qualification_pending", `${ACPX_CAPABILITY_PROFILES[agent].displayName} requires local and Daytona qualification before use.`); + } const model = text(config.model); try { resolveQualifiedAcpxProfile(agent, model); @@ -144,9 +150,7 @@ function isPermissionMode(value: string): value is NativeAcpxPermissionMode { } function displayAgent(agent: QualifiedAcpxAgent): string { - if (agent === "pi") return "Pi"; - if (agent === "claude") return "Claude"; - return "Codex"; + return ACPX_CAPABILITY_PROFILES[agent].displayName; } function record(value: unknown): Record | null { diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index ee54598b11..a19feed85a 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -11,7 +11,9 @@ export interface QualifiedAcpxProfile { readonly protocolVersion: typeof ACPX_DRIVER_PROTOCOL_VERSION; readonly acpxVersion: typeof QUALIFIED_ACPX_VERSION; readonly agent: QualifiedAcpxAgent; - readonly agentProfileVersion: 1; + readonly agentProfileVersion: 1 | 2; + readonly qualificationStatus?: "pending"; + readonly modelPolicy?: "explicit-provider-verified"; readonly agentServerPackage: string; readonly agentServerVersion: string; readonly agentRuntimePackage: string | null; @@ -47,6 +49,28 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< reportedModelId: "openrouter/deepseek/deepseek-v4-flash-0731", permissionPolicy: "interactive", }, + cursor: { + driverKind: ACPX_DRIVER_KIND, protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION, + acpxVersion: QUALIFIED_ACPX_VERSION, agent: "cursor", agentProfileVersion: 2, + agentServerPackage: "cursor-agent", agentServerVersion: "2026.09.26-dd393fe", + agentRuntimePackage: null, agentRuntimeVersion: null, + commandDigest: "sha256:1157a5d071abbd57ab132f22bace75c65e84cc47a045b0023475488755e14899", + // Authenticated discovery has not established a qualification model. Never + // turn this empty declaration into a default; callers must select an ID. + qualificationModel: "", reportedModelId: "", permissionPolicy: "interactive", + modelPolicy: "explicit-provider-verified", qualificationStatus: "pending", + }, + copilot: { + driverKind: ACPX_DRIVER_KIND, protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION, + acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 2, + agentServerPackage: "@github/copilot", agentServerVersion: "1.0.88", + agentRuntimePackage: null, agentRuntimeVersion: null, + commandDigest: "sha256:b18c01603dd0169d233140709cfaa8bf5304a03cf5de78ca4f625f30013e8457", + // Authenticated discovery has not established a qualification model. Never + // turn this empty declaration into a default; callers must select an ID. + qualificationModel: "", reportedModelId: "", permissionPolicy: "interactive", + modelPolicy: "explicit-provider-verified", qualificationStatus: "pending", + }, claude: { driverKind: ACPX_DRIVER_KIND, protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION, @@ -87,7 +111,7 @@ export function resolveQualifiedAcpxProfile( ): QualifiedAcpxProfile { const profile = QUALIFIED_ACPX_PROFILES[agent]; if (!requestedModel.trim()) throw new Error("ACPX model must not be empty"); - if (agent !== "claude" && requestedModel !== profile.qualificationModel) { + if (agent !== "claude" && profile.modelPolicy !== "explicit-provider-verified" && requestedModel !== profile.qualificationModel) { throw new Error( `ACPX ${agent} profile requires exact model ${profile.qualificationModel}; received ${requestedModel}`, ); diff --git a/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts b/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts index c07ba8d721..b0136cb469 100644 --- a/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts +++ b/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts @@ -650,7 +650,9 @@ export const providerDescriptorSchema = { "enum": [ "pi", "claude", - "codex" + "codex", + "cursor", + "copilot" ] }, "requestedModel": {