refactor(runner): generate shared ACPX release declarations

Consolidate runtime pins and explicit model admission policy across Rust, TypeScript, and packaging while preserving recovery identity.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-06 11:46:36 -05:00
1 parent e75fde6098
commit fb25904052
20 files changed
+665 -320

No files matched your search

@@ -25,6 +25,29 @@ spend or enforceable per-run dollar accounting. Native AskQuestion is implemente
defensively but is not advertised or certified. See the
[Cursor capability contract](../architecture/runner-cursor-capabilities.md).
## Bounded declaration cleanup — 2026-10-06
The PR audit found duplicated release tuples in Rust descriptor validation,
duplicated model rules in Rust session validation, and independent pins in the
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.
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.
All provider versions, digests, model acceptance rules, and recovery identity
fields retain their previous meaning. A regression check pins the pre-cleanup
Cursor recovery digest. Historical rejection fixtures and evidence keep their
original identities. Cursor launch flags, credential names, and native extension
translation remain provider-adapter responsibilities; generic mode, lifecycle,
permission, and transport contracts retain the earlier cleanup.
This is a source cleanup after the live qualification recorded below. Its checks
do not relabel the earlier package/image as a build of this source, and no further
paid qualification or release is implied.
## Final installed-release qualification — 2026-10-06
This qualification follows the generic mode and provider lifecycle cleanup. Its
+18
View File
@@ -41,6 +41,24 @@ Legacy fresh attempts receive the same bounded handoff, including resume-failure
fallbacks. Provider
authentication repairs retain their existing fresh-session recovery behavior.
## ACPX release declarations
`acpx-profiles.json` owns the package versions, command/profile digests, model
admission policies, and required execution policies used by TypeScript and Rust.
After changing it, run `pnpm --filter @paperclipai/paperclip-runner generate:acpx-profiles`.
Normal build and typecheck reject stale generated declarations. Generation also
checks installed dependency pins and agreement with Cursor's distribution manifest
and immutable release attestation (`cursor-contract.json`). Cursor's per-platform
closure pins are generated from `cursor-distributions.json`.
Model admission is separate from qualification examples. `provider-verified`
requires an explicit caller-selected model and native verification; `exact`
retains the existing restriction for the Codex ACP and Pi profiles. Qualification
examples are neither a model catalog nor defaults. The legacy snapshot field
`qualificationModel` contains the selected model after resolution; its serialized
name is retained to preserve recovery identities. Historical profile fixtures
remain immutable evidence, not release declarations.
## Public package surfaces
- `@paperclipai/paperclip-runner` — production contracts, clients/backends,
@@ -0,0 +1,101 @@
{
"schema": "paperclip.acpx-profiles.v1",
"acpxVersion": "0.13.1",
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"profiles": {
"grok": {
"agentProfileVersion": 1,
"agentServerPackage": "builtin:grok-acp",
"agentServerVersion": "1",
"agentRuntimePackage": "native:grok",
"agentRuntimeVersion": "1.0.13",
"commandDigest": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
"modelAdmission": {
"kind": "provider-verified"
},
"requiresProviderPolicy": false,
"qualification": {
"model": "grok-4.7"
}
},
"pi": {
"agentProfileVersion": 1,
"agentServerPackage": "pi-acp",
"agentServerVersion": "0.0.33",
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
"agentRuntimeVersion": "0.84.2",
"commandDigest": "sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
"modelAdmission": {
"kind": "exact",
"model": "openrouter/deepseek/deepseek-v4-flash-0731"
},
"requiresProviderPolicy": true,
"qualification": {
"model": "openrouter/deepseek/deepseek-v4-flash-0731"
}
},
"cursor": {
"agentProfileVersion": 11,
"agentServerPackage": "cursor-agent",
"agentServerVersion": "2026.09.26-dd393fe",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:2feb50c7b0a317dff454c00115a5bbe4d5c757189691586577be9c80234d477e",
"modelAdmission": {
"kind": "provider-verified"
},
"requiresProviderPolicy": true,
"qualification": {
"model": ""
}
},
"copilot": {
"agentProfileVersion": 2,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:b18c01603dd0169d233140709cfaa8bf5304a03cf5de78ca4f625f30013e8457",
"modelAdmission": {
"kind": "provider-verified"
},
"requiresProviderPolicy": true,
"qualification": {
"model": "",
"status": "pending"
}
},
"claude": {
"agentProfileVersion": 1,
"agentServerPackage": "@agentclientprotocol/claude-agent-acp",
"agentServerVersion": "0.73.0",
"agentRuntimePackage": "@anthropic-ai/claude-agent-sdk",
"agentRuntimeVersion": "0.3.286",
"commandDigest": "sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
"modelAdmission": {
"kind": "provider-verified"
},
"requiresProviderPolicy": false,
"qualification": {
"model": "claude-sonnet-5"
}
},
"codex": {
"agentProfileVersion": 1,
"agentServerPackage": "@agentclientprotocol/codex-acp",
"agentServerVersion": "1.6.2",
"agentRuntimePackage": "@openai/codex",
"agentRuntimeVersion": "0.160.0",
"commandDigest": "sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
"modelAdmission": {
"kind": "exact",
"model": "gpt-5.6-sol"
},
"requiresProviderPolicy": false,
"qualification": {
"model": "gpt-5.6-sol"
}
}
}
}
+6 -4
View File
@@ -58,7 +58,7 @@
"sideEffects": false,
"scripts": {
"build": "pnpm run check:protocol-manifest && pnpm run build:typescript && pnpm run check:capability-contract && pnpm run check:capability-inventory && pnpm run check:protocol-coverage && pnpm run check:semantic-contracts && pnpm run check:runner-workflow-traceability && pnpm run build:binary && node scripts/generate-replay-goldens.mjs --check && node scripts/generate-semantic-action-catalog.mjs --check",
"build:typescript": "pnpm run ensure:eval-build-deps && pnpm run check:protocol-types && node ./node_modules/typescript/bin/tsc --version && node ./node_modules/typescript/bin/tsc -p tsconfig.json && node ./node_modules/typescript/bin/tsc -p tsconfig.surfaces.json && node scripts/build-verified-provider-entrypoints.mjs",
"build:typescript": "pnpm run check:acpx-profiles && pnpm run ensure:eval-build-deps && pnpm run check:protocol-types && node ./node_modules/typescript/bin/tsc --version && node ./node_modules/typescript/bin/tsc -p tsconfig.json && node ./node_modules/typescript/bin/tsc -p tsconfig.surfaces.json && node scripts/build-verified-provider-entrypoints.mjs",
"build:rust": "cargo build --manifest-path runner/Cargo.toml --locked --workspace --bins",
"build:binary": "cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd && node scripts/stage-runner-binary.mjs",
"stage:release-binaries": "node scripts/stage-release-runner-binaries.mjs",
@@ -70,8 +70,8 @@
"build:scenarios": "vite build --config vite.scenarios.config.ts && node scripts/check-capability-csp-bundle.mjs",
"build:issue-thread": "vite build --config vite.issue-thread.config.ts",
"typecheck": "pnpm run typecheck:typescript && pnpm run typecheck:rust",
"typecheck:typescript": "pnpm run ensure:eval-build-deps && node --check scripts/protocol-contract.mjs && node --check scripts/generate-protocol-manifest.mjs && node --check scripts/generate-protocol-schema-module.mjs && node --check scripts/generate-acpx-sidecar-contract.mjs && node --check scripts/generate-replay-goldens.mjs && node --check scripts/generate-semantic-action-catalog.mjs && pnpm run check:protocol-types && node ./node_modules/typescript/bin/tsc --version && node ./node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node ./node_modules/typescript/bin/tsc -p tsconfig.surfaces.json --noEmit",
"typecheck:rust": "cargo fmt --manifest-path runner/Cargo.toml --all -- --check && cargo check --manifest-path runner/Cargo.toml --locked --workspace",
"typecheck:typescript": "pnpm run check:acpx-profiles && pnpm run ensure:eval-build-deps && node --check scripts/protocol-contract.mjs && node --check scripts/generate-protocol-manifest.mjs && node --check scripts/generate-protocol-schema-module.mjs && node --check scripts/generate-acpx-sidecar-contract.mjs && node --check scripts/generate-replay-goldens.mjs && node --check scripts/generate-semantic-action-catalog.mjs && pnpm run check:protocol-types && node ./node_modules/typescript/bin/tsc --version && node ./node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node ./node_modules/typescript/bin/tsc -p tsconfig.surfaces.json --noEmit",
"typecheck:rust": "pnpm run check:acpx-profiles && cargo fmt --manifest-path runner/Cargo.toml --all -- --check && cargo check --manifest-path runner/Cargo.toml --locked --workspace",
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
"test": "pnpm run test:typescript && pnpm run test:rust",
"test:typescript": "pnpm run test:typescript:prep && vitest run",
@@ -161,7 +161,9 @@
"browser:preview": "vite preview --config vite.config.ts",
"verify": "pnpm run build && pnpm run typecheck && pnpm run check:replay-goldens && pnpm run test && pnpm run test:sdk && pnpm run test:scenarios && pnpm run check:browser-tokens && pnpm run test:browser && pnpm run test:browser:sdk && pnpm run test:browser:scenarios && pnpm run check:forbidden-imports && pnpm run check:tracked-imports && pnpm run check:numbered-milestones && pnpm run check:package-boundaries && pnpm run check:clean-consumers && pnpm run docs:validate && pnpm run check:conformance-parity && pnpm run check:replay-parity && pnpm run trace:conformance && pnpm run replay:fixture && pnpm run trace:local-runner -- --quiet",
"verify:rootless": "bash scripts/verify-rootless-linux.sh",
"test:opencode:qualification": "PAPERCLIP_OPENCODE_QUALIFY=1 node --test scripts/qualify-opencode-runtime.test.mjs"
"test:opencode:qualification": "PAPERCLIP_OPENCODE_QUALIFY=1 node --test scripts/qualify-opencode-runtime.test.mjs",
"generate:acpx-profiles": "node scripts/generate-acpx-profiles.mjs",
"check:acpx-profiles": "node scripts/generate-acpx-profiles.mjs --check"
},
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.73.0",
@@ -1,3 +1,6 @@
use crate::generated_acpx_profiles::{
acpx_release_profile, ACPX_DRIVER_KIND, QUALIFIED_ACPX_VERSION,
};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::fs::{self, DirBuilder, File};
use std::io::{Read, Write};
@@ -185,67 +188,15 @@ impl AcpxProviderDescriptor {
&self,
context: &AcpxEventProjectionContext,
) -> Result<(), DurableRunnerError> {
let expected = match self.agent.as_str() {
"claude" => (
"claude-sonnet-5",
"@agentclientprotocol/claude-agent-acp",
"0.73.0",
Some("@anthropic-ai/claude-agent-sdk"),
Some("0.3.286"),
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
),
"codex" => (
"gpt-5.6-sol",
"@agentclientprotocol/codex-acp",
"1.6.2",
Some("@openai/codex"),
Some("0.160.0"),
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
),
"pi" => (
"openrouter/deepseek/deepseek-v4-flash-0731",
"pi-acp",
"0.0.33",
Some("@earendil-works/pi-coding-agent"),
Some("0.84.2"),
"sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
),
"cursor" => (
self.model.as_str(),
"cursor-agent",
"2026.09.26-dd393fe",
None,
None,
"sha256:2feb50c7b0a317dff454c00115a5bbe4d5c757189691586577be9c80234d477e",
),
"copilot" => (
self.model.as_str(),
"@github/copilot",
"1.0.88",
None,
None,
"sha256:b18c01603dd0169d233140709cfaa8bf5304a03cf5de78ca4f625f30013e8457",
),
"grok" => (
"grok-4.7",
"builtin:grok-acp",
"1",
Some("native:grok"),
Some("1.0.13"),
"sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
),
_ => {
return Err(DurableRunnerError::invalid(
"ACPX agent must name a known immutable profile",
))
}
};
let expected = acpx_release_profile(&self.agent).ok_or_else(|| {
DurableRunnerError::invalid("ACPX agent must name a known immutable profile")
})?;
if self.kind != "acpx"
|| self.provider != "acpx"
|| self.driver != "acpx_runtime"
|| self.provider_version != "0.13.1"
|| self.acpx_version != "0.13.1"
|| (self.agent != "claude" && self.agent != "grok" && self.model != expected.0)
|| self.driver != ACPX_DRIVER_KIND
|| self.provider_version != QUALIFIED_ACPX_VERSION
|| self.acpx_version != QUALIFIED_ACPX_VERSION
|| !expected.model_admission.accepts(&self.model)
|| self.model.trim().is_empty()
|| self.model.len() > 240
|| self.model.contains('\0')
@@ -254,13 +205,12 @@ impl AcpxProviderDescriptor {
|| mode.chars().count() > 240
|| mode.chars().any(char::is_control)
})
|| (matches!(self.agent.as_str(), "pi" | "cursor" | "copilot")
&& self.provider_policy.is_none())
|| self.agent_server_package != expected.1
|| self.agent_server_version != expected.2
|| self.agent_runtime_package.as_deref() != expected.3
|| self.agent_runtime_version.as_deref() != expected.4
|| self.command_digest != expected.5
|| (expected.requires_provider_policy && self.provider_policy.is_none())
|| self.agent_server_package != expected.agent_server_package
|| self.agent_server_version != expected.agent_server_version
|| self.agent_runtime_package.as_deref() != expected.agent_runtime_package
|| self.agent_runtime_version.as_deref() != expected.agent_runtime_version
|| self.command_digest != expected.command_digest
{
return Err(DurableRunnerError::invalid(
"ACPX provider descriptor does not match a qualified immutable profile",
@@ -2328,48 +2278,28 @@ mod tests {
}
fn descriptor(agent: &str) -> Value {
let (model, package, version, runtime_package, runtime_version, digest) =
if agent == "claude" {
(
"claude-sonnet-5",
"@agentclientprotocol/claude-agent-acp",
"0.73.0",
json!("@anthropic-ai/claude-agent-sdk"),
json!("0.3.286"),
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
)
} else if agent == "cursor" {
(
"gpt-5.6-luna[context=272k,reasoning=medium,fast=false]",
"cursor-agent",
"2026.09.26-dd393fe",
Value::Null,
Value::Null,
"sha256:2feb50c7b0a317dff454c00115a5bbe4d5c757189691586577be9c80234d477e",
)
} else {
(
"gpt-5.6-sol",
"@agentclientprotocol/codex-acp",
"1.6.2",
json!("@openai/codex"),
json!("0.160.0"),
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
)
};
let profile = acpx_release_profile(agent).unwrap();
let model = match agent {
"claude" => "claude-sonnet-5",
"cursor" => "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]",
"pi" => "openrouter/deepseek/deepseek-v4-flash-0731",
"grok" => "grok-4.7",
"copilot" => "explicit-test-model",
_ => "gpt-5.6-sol",
};
let mut value = json!({
"kind": "acpx",
"provider": "acpx",
"driver": "acpx_runtime",
"providerVersion": "0.13.1",
"providerVersion": QUALIFIED_ACPX_VERSION,
"agent": agent,
"model": model,
"acpxVersion": "0.13.1",
"agentServerPackage": package,
"agentServerVersion": version,
"agentRuntimePackage": runtime_package,
"agentRuntimeVersion": runtime_version,
"commandDigest": digest,
"acpxVersion": QUALIFIED_ACPX_VERSION,
"agentServerPackage": profile.agent_server_package,
"agentServerVersion": profile.agent_server_version,
"agentRuntimePackage": profile.agent_runtime_package,
"agentRuntimeVersion": profile.agent_runtime_version,
"commandDigest": profile.command_digest,
"sidecarCommand": "/qualified/node",
"sidecarArgs": ["/qualified/acpx-sidecar.js"],
"runtimeDirectory": "/runtime/acpx",
@@ -2381,9 +2311,11 @@ mod tests {
"permissionModePinned": true,
"runtimeContext": null,
});
if profile.requires_provider_policy {
value["providerPolicy"] = json!({"readOnly": true});
}
if agent == "cursor" {
value["mode"] = json!("agent");
value["providerPolicy"] = json!({"readOnly": true});
}
value
}
@@ -2822,50 +2754,50 @@ mod tests {
}
#[test]
fn admits_candidate_descriptors_only_with_explicit_policy_and_exact_distribution() {
for (agent, package, version, digest, runtime_package, runtime_version, model) in [
(
"cursor",
"cursor-agent",
"2026.09.26-dd393fe",
"sha256:2feb50c7b0a317dff454c00115a5bbe4d5c757189691586577be9c80234d477e",
None,
None,
"explicit-model",
),
(
"copilot",
"@github/copilot",
"1.0.88",
"sha256:b18c01603dd0169d233140709cfaa8bf5304a03cf5de78ca4f625f30013e8457",
None,
None,
"explicit-model",
),
(
"pi",
"pi-acp",
"0.0.33",
"sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
Some("@earendil-works/pi-coding-agent"),
Some("0.84.2"),
"openrouter/deepseek/deepseek-v4-flash-0731",
),
] {
let mut value = descriptor("codex");
value["agent"] = json!(agent);
value["model"] = json!(model);
value["agentServerPackage"] = json!(package);
value["agentServerVersion"] = json!(version);
value["agentRuntimePackage"] = json!(runtime_package);
value["agentRuntimeVersion"] = json!(runtime_version);
value["commandDigest"] = json!(digest);
let missing: AcpxProviderDescriptor = serde_json::from_value(value.clone()).unwrap();
assert!(missing.validate(&context()).is_err());
value["providerPolicy"] = json!({"readOnly":true});
if agent == "cursor" {
value["mode"] = json!("agent");
fn model_admission_is_independent_of_qualification_examples() {
for agent in ["claude", "grok", "cursor", "copilot", "codex", "pi"] {
let original: AcpxProviderDescriptor =
serde_json::from_value(descriptor(agent)).unwrap();
original.validate_session(&context()).unwrap();
let mut alternative = descriptor(agent);
alternative["model"] = json!("explicit-model-not-in-a-catalog");
let alternative: AcpxProviderDescriptor = serde_json::from_value(alternative).unwrap();
assert_eq!(
alternative.validate_session(&context()).is_ok(),
!matches!(agent, "codex" | "pi")
);
for field in [
"agentServerPackage",
"agentServerVersion",
"agentRuntimePackage",
"agentRuntimeVersion",
"commandDigest",
"acpxVersion",
"providerVersion",
] {
let mut tampered = descriptor(agent);
tampered[field] = json!("untrusted");
let tampered: AcpxProviderDescriptor = serde_json::from_value(tampered).unwrap();
assert!(
tampered.validate_session(&context()).is_err(),
"{agent}: {field}"
);
}
let mut empty = descriptor(agent);
empty["model"] = json!(" ");
let empty: AcpxProviderDescriptor = serde_json::from_value(empty).unwrap();
assert!(empty.validate_session(&context()).is_err());
}
}
#[test]
fn admits_candidate_descriptors_only_with_explicit_policy_and_exact_distribution() {
for agent in ["cursor", "copilot", "pi"] {
let mut value = descriptor(agent);
let mut missing = value.clone();
missing.as_object_mut().unwrap().remove("providerPolicy");
let missing: AcpxProviderDescriptor = serde_json::from_value(missing).unwrap();
assert!(missing.validate(&context()).is_err());
let valid: AcpxProviderDescriptor = serde_json::from_value(value.clone()).unwrap();
valid.validate(&context()).unwrap();
if matches!(agent, "cursor" | "copilot") {
@@ -1,3 +1,4 @@
use crate::generated_acpx_profiles::{acpx_release_profile, ModelAdmission};
use std::path::{Path, PathBuf};
use std::time::Duration;
@@ -83,31 +84,22 @@ pub struct AcpxProviderSessionConfig {
impl AcpxProviderSessionConfig {
pub fn validate(&self) -> Result<(), LocalRunnerError> {
self.transport.validate()?;
let qualified_model = match self.agent.as_str() {
"claude" => "claude-sonnet-5",
"grok" => "grok-4.7",
"codex" => "gpt-5.6-sol",
"pi" => "openrouter/deepseek/deepseek-v4-flash-0731",
"cursor" | "copilot" => self.model.as_str(),
_ => {
return Err(LocalRunnerError::invalid(
"ACPX agent must name a known immutable profile",
))
let profile = acpx_release_profile(&self.agent).ok_or_else(|| {
LocalRunnerError::invalid("ACPX agent must name a known immutable profile")
})?;
if let ModelAdmission::Exact(qualified_model) = profile.model_admission {
if self.model != qualified_model {
return Err(LocalRunnerError::invalid(format!(
"ACPX {} profile requires exact model {qualified_model}",
self.agent
)));
}
};
if self.agent != "claude" && self.agent != "grok" && self.model != qualified_model {
return Err(LocalRunnerError::invalid(format!(
"ACPX {} profile requires exact model {qualified_model}",
self.agent
)));
}
validate_text(&self.model, MAX_MODEL_CHARS, "ACPX model")?;
if let Some(mode) = self.mode.as_deref() {
validate_text(mode, MAX_ID_CHARS, "ACPX provider mode")?;
}
if matches!(self.agent.as_str(), "pi" | "cursor" | "copilot")
&& self.provider_policy.is_none()
{
if profile.requires_provider_policy && self.provider_policy.is_none() {
return Err(LocalRunnerError::invalid(
"ACPX candidate requires explicit provider read-only policy",
));
@@ -0,0 +1,95 @@
// Generated from acpx-profiles.json and cursor-distributions.json. Do not edit.
pub(crate) const QUALIFIED_ACPX_VERSION: &str = "0.13.1";
pub(crate) const ACPX_DRIVER_KIND: &str = "acpx_runtime";
#[derive(Debug)]
pub(crate) enum ModelAdmission {
Exact(&'static str),
ProviderVerified,
}
impl ModelAdmission {
pub(crate) fn accepts(&self, model: &str) -> bool {
match self {
Self::Exact(expected) => model == *expected,
Self::ProviderVerified => true,
}
}
}
#[derive(Debug)]
pub(crate) struct AcpxReleaseProfile {
pub agent_server_package: &'static str,
pub agent_server_version: &'static str,
pub agent_runtime_package: Option<&'static str>,
pub agent_runtime_version: Option<&'static str>,
pub command_digest: &'static str,
pub model_admission: ModelAdmission,
pub requires_provider_policy: bool,
}
pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
Some(match agent {
"grok" => AcpxReleaseProfile {
agent_server_package: "builtin:grok-acp",
agent_server_version: "1",
agent_runtime_package: Some("native:grok"),
agent_runtime_version: Some("1.0.13"),
command_digest:
"sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
model_admission: ModelAdmission::ProviderVerified,
requires_provider_policy: false,
},
"pi" => AcpxReleaseProfile {
agent_server_package: "pi-acp",
agent_server_version: "0.0.33",
agent_runtime_package: Some("@earendil-works/pi-coding-agent"),
agent_runtime_version: Some("0.84.2"),
command_digest:
"sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
model_admission: ModelAdmission::Exact("openrouter/deepseek/deepseek-v4-flash-0731"),
requires_provider_policy: true,
},
"cursor" => AcpxReleaseProfile {
agent_server_package: "cursor-agent",
agent_server_version: "2026.09.26-dd393fe",
agent_runtime_package: None,
agent_runtime_version: None,
command_digest:
"sha256:2feb50c7b0a317dff454c00115a5bbe4d5c757189691586577be9c80234d477e",
model_admission: ModelAdmission::ProviderVerified,
requires_provider_policy: true,
},
"copilot" => AcpxReleaseProfile {
agent_server_package: "@github/copilot",
agent_server_version: "1.0.88",
agent_runtime_package: None,
agent_runtime_version: None,
command_digest:
"sha256:b18c01603dd0169d233140709cfaa8bf5304a03cf5de78ca4f625f30013e8457",
model_admission: ModelAdmission::ProviderVerified,
requires_provider_policy: true,
},
"claude" => AcpxReleaseProfile {
agent_server_package: "@agentclientprotocol/claude-agent-acp",
agent_server_version: "0.73.0",
agent_runtime_package: Some("@anthropic-ai/claude-agent-sdk"),
agent_runtime_version: Some("0.3.286"),
command_digest:
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
model_admission: ModelAdmission::ProviderVerified,
requires_provider_policy: false,
},
"codex" => AcpxReleaseProfile {
agent_server_package: "@agentclientprotocol/codex-acp",
agent_server_version: "1.6.2",
agent_runtime_package: Some("@openai/codex"),
agent_runtime_version: Some("0.160.0"),
command_digest:
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
model_admission: ModelAdmission::Exact("gpt-5.6-sol"),
requires_provider_policy: false,
},
_ => return None,
})
}
@@ -14,6 +14,7 @@ pub mod codex_provider;
mod codex_startup_trust;
pub mod durable;
pub mod fake_harness;
mod generated_acpx_profiles;
pub mod generated_acpx_sidecar_contract;
pub mod local_runner;
pub mod managed_provider;
@@ -1,3 +1,4 @@
import profiles from "../acpx-profiles.json" with { type: "json" };
import { createHash } from "node:crypto";
import { execFileSync, spawnSync } from "node:child_process";
import {
@@ -310,12 +311,12 @@ try {
const payload = {
pins: {
nodeMinimum: minimumNodeVersion.join("."),
codex: "0.160.0",
codex: profiles.profiles.codex.agentRuntimeVersion,
opencode: "1.18.34",
acpx: "0.13.1",
grok: "1.0.13",
claudeAcp: "0.73.0",
codexAcp: "1.6.2",
acpx: profiles.acpxVersion,
grok: profiles.profiles.grok.agentRuntimeVersion,
claudeAcp: profiles.profiles.claude.agentServerVersion,
codexAcp: profiles.profiles.codex.agentServerVersion,
},
target: { platform: process.platform, architecture: process.arch },
runnerSourceRevision: `${revision}${dirty ? "-dirty" : ""}`,
@@ -328,11 +329,11 @@ try {
.update(distDigest)
.digest("hex")}`,
acpxProfileDigests: {
grok: "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
grok: profiles.profiles.grok.commandDigest,
claude:
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
profiles.profiles.claude.commandDigest,
codex:
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
profiles.profiles.codex.commandDigest,
},
...providerPackManifestFields(candidateProviders, candidates),
artifacts: {
@@ -1,3 +1,4 @@
import profiles from "../acpx-profiles.json" with { type: "json" };
import { materializePinnedCursorDistribution } from "./materialize-cursor-distribution.mjs";
const CANDIDATES = new Set(["cursor", "copilot", "pi"]);
@@ -35,7 +36,7 @@ export async function materializeCandidateProviderPack({ provider, outputRoot })
if (provider === "cursor") {
const result = await materializePinnedCursorDistribution({ destination: outputRoot });
return { version: result.version,
profileDigest: "sha256:2feb50c7b0a317dff454c00115a5bbe4d5c757189691586577be9c80234d477e",
profileDigest: profiles.profiles.cursor.commandDigest,
closureDigest: `sha256:${result.closureSha256}` };
}
throw new Error(`The ${provider} candidate distribution builder is not included in this source revision`);
@@ -0,0 +1,120 @@
import assert from "node:assert/strict";
import { readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const root = new URL("../", import.meta.url);
const readJson = async path => JSON.parse(await readFile(new URL(path, root), "utf8"));
const manifest = await readJson("acpx-profiles.json");
const distributions = await readJson("cursor-distributions.json");
const contract = await readJson("cursor-contract.json");
const pkg = await readJson("package.json");
assert.equal(manifest.schema, "paperclip.acpx-profiles.v1");
assert.equal(manifest.acpxVersion, pkg.dependencies.acpx);
const entries = Object.entries(manifest.profiles);
for (const [agent, profile] of entries) {
assert.match(agent, /^[a-z]+$/);
assert.ok(Number.isSafeInteger(profile.agentProfileVersion) && profile.agentProfileVersion > 0);
for (const key of ["agentServerPackage", "agentServerVersion"]) assert.ok(typeof profile[key] === "string" && profile[key].length > 0);
assert.equal(profile.agentRuntimePackage === null, profile.agentRuntimeVersion === null);
assert.match(profile.commandDigest, /^sha256:[a-f0-9]{64}$/);
assert.equal(typeof profile.requiresProviderPolicy, "boolean");
assert.equal(typeof profile.qualification.model, "string");
assert.ok([undefined, "pending"].includes(profile.qualification.status));
assert.ok(["exact", "provider-verified"].includes(profile.modelAdmission.kind));
if (profile.modelAdmission.kind === "exact") assert.ok(profile.modelAdmission.model?.trim());
else assert.equal(profile.modelAdmission.model, undefined);
for (const [name, version] of [[profile.agentServerPackage, profile.agentServerVersion], [profile.agentRuntimePackage, profile.agentRuntimeVersion]]) {
if (name && Object.hasOwn(pkg.dependencies, name)) assert.equal(version, pkg.dependencies[name], `${agent}: installed dependency drift`);
}
}
// The Cursor contract is an immutable release attestation, not another runtime
// lookup table. Refuse generation if a release declaration disagrees with it.
assert.equal(manifest.profiles.cursor.agentServerVersion, distributions.version);
assert.equal(manifest.profiles.cursor.agentServerVersion, contract.vendor);
assert.equal(manifest.profiles.cursor.agentProfileVersion, contract.profileVersion);
assert.equal(manifest.profiles.cursor.commandDigest, contract.commandDigest);
assert.equal(distributions.patchVersion, contract.nativePatch);
for (const distribution of Object.values(distributions.platforms)) assert.match(distribution.closureSha256, /^[a-f0-9]{64}$/);
const quote = JSON.stringify;
const profiles = Object.fromEntries(entries.map(([agent, declaration]) => {
const { modelAdmission, requiresProviderPolicy, qualification, ...runtime } = declaration;
return [agent, {
driverKind: manifest.driverKind, protocolVersion: manifest.protocolVersion,
acpxVersion: manifest.acpxVersion, agent, ...runtime,
...(qualification.status ? { qualificationStatus: qualification.status } : {}),
qualificationModel: qualification.model, reportedModelId: qualification.model,
permissionPolicy: "interactive",
}];
}));
const policies = Object.fromEntries(entries.map(([agent, profile]) => [agent, profile.modelAdmission]));
const header = "Generated from acpx-profiles.json and cursor-distributions.json. Do not edit.";
const typescript = `// ${header}
export const QUALIFIED_ACPX_VERSION = ${quote(manifest.acpxVersion)} as const;
export const ACPX_DRIVER_KIND = ${quote(manifest.driverKind)} as const;
export const ACPX_DRIVER_PROTOCOL_VERSION = ${manifest.protocolVersion} as const;
export const QUALIFIED_ACPX_PROFILE_DATA = ${JSON.stringify(profiles, null, 2)} as const;
export const ACPX_MODEL_ADMISSION = ${JSON.stringify(policies, null, 2)} as const;
export const CURSOR_DISTRIBUTION_PINS = ${JSON.stringify(Object.fromEntries(Object.entries(distributions.platforms).map(([platform, { closureSha256, executable, entrypoint }]) => [platform, { closureSha256, executable, entrypoint }])), null, 2)} as const;
`;
const option = value => value === null ? "None" : `Some(${quote(value)})`;
const rust = `// ${header}
pub(crate) const QUALIFIED_ACPX_VERSION: &str = ${quote(manifest.acpxVersion)};
pub(crate) const ACPX_DRIVER_KIND: &str = ${quote(manifest.driverKind)};
#[derive(Debug)]
pub(crate) enum ModelAdmission {
Exact(&'static str),
ProviderVerified,
}
impl ModelAdmission {
pub(crate) fn accepts(&self, model: &str) -> bool {
match self {
Self::Exact(expected) => model == *expected,
Self::ProviderVerified => true,
}
}
}
#[derive(Debug)]
pub(crate) struct AcpxReleaseProfile {
pub agent_server_package: &'static str,
pub agent_server_version: &'static str,
pub agent_runtime_package: Option<&'static str>,
pub agent_runtime_version: Option<&'static str>,
pub command_digest: &'static str,
pub model_admission: ModelAdmission,
pub requires_provider_policy: bool,
}
pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
Some(match agent {
${entries.map(([agent, p]) => ` ${quote(agent)} => AcpxReleaseProfile {
agent_server_package: ${quote(p.agentServerPackage)},
agent_server_version: ${quote(p.agentServerVersion)},
agent_runtime_package: ${option(p.agentRuntimePackage)},
agent_runtime_version: ${option(p.agentRuntimeVersion)},
command_digest:
${quote(p.commandDigest)},
model_admission: ${p.modelAdmission.kind === "exact" ? `ModelAdmission::Exact(${quote(p.modelAdmission.model)})` : "ModelAdmission::ProviderVerified"},
requires_provider_policy: ${p.requiresProviderPolicy},
},`).join("\n")}
_ => return None,
})
}
`;
for (const [path, content] of [
["src/drivers/acpx/generated-profiles.ts", typescript],
["runner/crates/runner-core/src/generated_acpx_profiles.rs", rust],
]) {
const url = new URL(path, root);
if (process.argv.includes("--check")) {
assert.equal(await readFile(url, "utf8"), content, `Stale ACPX profiles: ${fileURLToPath(url)}; run generate:acpx-profiles`);
} else await writeFile(url, content);
}
console.log("TypeScript/Rust ACPX profiles match the release manifest.");
@@ -1,3 +1,4 @@
import { QUALIFIED_ACPX_VERSION } from "../drivers/acpx/generated-profiles.js";
import { isSupportedAcpxProfileVersion, type AcpxProfileVersion } from "../drivers/acpx/profile-compatibility.js";
import { isProviderMode } from "./provider-mode.js";
import { createHash } from "node:crypto";
@@ -90,7 +91,7 @@ export type NativeAcpxPermissionMode = "approve-all" | "approve-paperclip" | "ap
export interface NativeAcpxProfileSnapshot {
driverKind: "acpx_runtime";
protocolVersion: 1;
acpxVersion: "0.13.1";
acpxVersion: typeof QUALIFIED_ACPX_VERSION;
agent: NativeAcpxAgent;
agentProfileVersion: AcpxProfileVersion;
agentServerPackage: string;
@@ -606,7 +607,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
if (
profile.driverKind !== "acpx_runtime"
|| profile.protocolVersion !== 1
|| profile.acpxVersion !== "0.13.1"
|| profile.acpxVersion !== QUALIFIED_ACPX_VERSION
|| profile.agent !== provider.agent
|| !isSupportedAcpxProfileVersion(provider.agent, profile.agentProfileVersion)
) {
@@ -628,7 +629,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
profile: {
driverKind: "acpx_runtime",
protocolVersion: 1,
acpxVersion: "0.13.1",
acpxVersion: QUALIFIED_ACPX_VERSION,
agent: provider.agent,
agentProfileVersion: profile.agentProfileVersion,
agentServerPackage: text(profile.agentServerPackage, "input.provider.profile.agentServerPackage"),
@@ -2,16 +2,12 @@ import { join } from "node:path";
import { verifyNativeAcpxInstallation, type VerifiedAcpxInstallation } from "./installation-integrity.js";
import type { NativeAcpxDistributionInput } from "./native-distribution-integrity.js";
import { CURSOR_FIXED_ARGUMENTS } from "./cursor-launch-policy.js";
import { CURSOR_PINNED_VERSION, CURSOR_FIXED_ARGUMENTS } from "./cursor-launch-policy.js";
import { QUALIFIED_ACPX_PROFILES, type QualifiedAcpxProfile } from "./qualified-profiles.js";
import { resolveRunnerProviderAssetsRoot } from "./provider-assets-root.js";
export const CURSOR_PINNED_VERSION = "2026.09.26-dd393fe";
const CLOSURE_PINS = Object.freeze({
"darwin-arm64": "257424bd48e35412091c6adfc61e4648e836757ec1d240d890bba81a24918c30",
"darwin-x64": "6f28c799c5afdc64fbdff8a2157f565f17ae7615efe014ac389d63bf70cf2be2",
"linux-x64": "eadb8bb8ffb0450455b15b88c9b230307a9e149958a0c452d16dd157b4633d74",
});
import { CURSOR_DISTRIBUTION_PINS } from "./generated-profiles.js";
export { CURSOR_PINNED_VERSION } from "./cursor-launch-policy.js";
/** Trusted package assets only: no workspace, PATH, executable override or env root. */
export function cursorNativeDistributionSpec(
@@ -19,14 +15,15 @@ export function cursorNativeDistributionSpec(
architecture: string = process.arch,
): NativeAcpxDistributionInput {
const key = `${platform}-${architecture}`;
if (!Object.hasOwn(CLOSURE_PINS, key)) throw new Error(`Cursor ${CURSOR_PINNED_VERSION} has no pinned distribution for ${key}`);
if (!Object.hasOwn(CURSOR_DISTRIBUTION_PINS, key)) throw new Error(`Cursor ${CURSOR_PINNED_VERSION} has no pinned distribution for ${key}`);
const distribution = CURSOR_DISTRIBUTION_PINS[key as keyof typeof CURSOR_DISTRIBUTION_PINS];
const distributionRoot = join(resolveRunnerProviderAssetsRoot(import.meta.url, "cursor"), key);
return {
distributionRoot,
manifestPath: join(distributionRoot, ".paperclip-cursor-closure.json"),
expectedClosureSha256: CLOSURE_PINS[key as keyof typeof CLOSURE_PINS],
executable: "node",
entrypoint: "index.js",
expectedClosureSha256: distribution.closureSha256,
executable: distribution.executable,
entrypoint: distribution.entrypoint,
fixedArguments: [...CURSOR_FIXED_ARGUMENTS],
};
}
@@ -1,7 +1,9 @@
import { lstat, readFile, realpath } from "node:fs/promises";
import { dirname, join } from "node:path";
export const CURSOR_PINNED_VERSION = "2026.09.26-dd393fe";
import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js";
export const CURSOR_PINNED_VERSION = QUALIFIED_ACPX_PROFILES.cursor.agentServerVersion;
export const CURSOR_FIXED_ARGUMENTS = ["--disable-project-configs", "--disable-auto-update", "acp"] as const;
export const CURSOR_CREDENTIAL_ENVIRONMENT_NAMES = ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"] as const;
@@ -0,0 +1,139 @@
// Generated from acpx-profiles.json and cursor-distributions.json. Do not edit.
export const QUALIFIED_ACPX_VERSION = "0.13.1" as const;
export const ACPX_DRIVER_KIND = "acpx_runtime" as const;
export const ACPX_DRIVER_PROTOCOL_VERSION = 1 as const;
export const QUALIFIED_ACPX_PROFILE_DATA = {
"grok": {
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "grok",
"agentProfileVersion": 1,
"agentServerPackage": "builtin:grok-acp",
"agentServerVersion": "1",
"agentRuntimePackage": "native:grok",
"agentRuntimeVersion": "1.0.13",
"commandDigest": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
"qualificationModel": "grok-4.7",
"reportedModelId": "grok-4.7",
"permissionPolicy": "interactive"
},
"pi": {
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "pi",
"agentProfileVersion": 1,
"agentServerPackage": "pi-acp",
"agentServerVersion": "0.0.33",
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
"agentRuntimeVersion": "0.84.2",
"commandDigest": "sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
"qualificationModel": "openrouter/deepseek/deepseek-v4-flash-0731",
"reportedModelId": "openrouter/deepseek/deepseek-v4-flash-0731",
"permissionPolicy": "interactive"
},
"cursor": {
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "cursor",
"agentProfileVersion": 11,
"agentServerPackage": "cursor-agent",
"agentServerVersion": "2026.09.26-dd393fe",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:2feb50c7b0a317dff454c00115a5bbe4d5c757189691586577be9c80234d477e",
"qualificationModel": "",
"reportedModelId": "",
"permissionPolicy": "interactive"
},
"copilot": {
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "copilot",
"agentProfileVersion": 2,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:b18c01603dd0169d233140709cfaa8bf5304a03cf5de78ca4f625f30013e8457",
"qualificationStatus": "pending",
"qualificationModel": "",
"reportedModelId": "",
"permissionPolicy": "interactive"
},
"claude": {
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "claude",
"agentProfileVersion": 1,
"agentServerPackage": "@agentclientprotocol/claude-agent-acp",
"agentServerVersion": "0.73.0",
"agentRuntimePackage": "@anthropic-ai/claude-agent-sdk",
"agentRuntimeVersion": "0.3.286",
"commandDigest": "sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
"qualificationModel": "claude-sonnet-5",
"reportedModelId": "claude-sonnet-5",
"permissionPolicy": "interactive"
},
"codex": {
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "codex",
"agentProfileVersion": 1,
"agentServerPackage": "@agentclientprotocol/codex-acp",
"agentServerVersion": "1.6.2",
"agentRuntimePackage": "@openai/codex",
"agentRuntimeVersion": "0.160.0",
"commandDigest": "sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
"qualificationModel": "gpt-5.6-sol",
"reportedModelId": "gpt-5.6-sol",
"permissionPolicy": "interactive"
}
} as const;
export const ACPX_MODEL_ADMISSION = {
"grok": {
"kind": "provider-verified"
},
"pi": {
"kind": "exact",
"model": "openrouter/deepseek/deepseek-v4-flash-0731"
},
"cursor": {
"kind": "provider-verified"
},
"copilot": {
"kind": "provider-verified"
},
"claude": {
"kind": "provider-verified"
},
"codex": {
"kind": "exact",
"model": "gpt-5.6-sol"
}
} as const;
export const CURSOR_DISTRIBUTION_PINS = {
"darwin-arm64": {
"closureSha256": "257424bd48e35412091c6adfc61e4648e836757ec1d240d890bba81a24918c30",
"executable": "node",
"entrypoint": "index.js"
},
"darwin-x64": {
"closureSha256": "6f28c799c5afdc64fbdff8a2157f565f17ae7615efe014ac389d63bf70cf2be2",
"executable": "node",
"entrypoint": "index.js"
},
"linux-x64": {
"closureSha256": "eadb8bb8ffb0450455b15b88c9b230307a9e149958a0c452d16dd157b4633d74",
"executable": "node",
"entrypoint": "index.js"
}
} as const;
@@ -18,19 +18,24 @@ describe("qualified ACPX profiles", () => {
}
});
it.each(["claude-opus-5-5", "claude-fable-5-1", "custom-model-not-in-catalog"])("accepts the exact Claude model %s", (model) => {
expect(resolveQualifiedAcpxProfile("claude", model)).toMatchObject({
it.each(["claude", "grok", "cursor", "copilot"] as const)("lets %s verify an explicit model absent from the catalog", (agent) => {
const model = "custom-model-not-in-catalog";
expect(resolveQualifiedAcpxProfile(agent, model)).toMatchObject({
qualificationModel: model, reportedModelId: model,
commandDigest: QUALIFIED_ACPX_PROFILES.claude.commandDigest,
commandDigest: QUALIFIED_ACPX_PROFILES[agent].commandDigest,
});
});
it("rejects unqualified model substitutions", () => {
it.each(["codex", "pi"] as const)("preserves the existing exact-model restriction for %s", (agent) => {
expect(() =>
resolveQualifiedAcpxProfile("codex", "some-other-model"),
resolveQualifiedAcpxProfile(agent, "some-other-model"),
).toThrow("requires exact model");
});
it.each(["claude", "codex", "grok", "cursor", "copilot", "pi"] as const)("never selects a qualification model by default for %s", (agent) => {
expect(() => resolveQualifiedAcpxProfile(agent, " ")).toThrow("must not be empty");
});
it("binds Codex ACP to the CLI runtime it launches", () => {
expect(QUALIFIED_ACPX_PROFILES.codex).toMatchObject({
agentRuntimePackage: "@openai/codex",
@@ -1,6 +1,5 @@
export const QUALIFIED_ACPX_VERSION = "0.13.1" as const;
export const ACPX_DRIVER_KIND = "acpx_runtime" as const;
export const ACPX_DRIVER_PROTOCOL_VERSION = 1 as const;
import { ACPX_DRIVER_KIND, ACPX_DRIVER_PROTOCOL_VERSION, QUALIFIED_ACPX_VERSION, QUALIFIED_ACPX_PROFILE_DATA, ACPX_MODEL_ADMISSION } from "./generated-profiles.js";
export { ACPX_DRIVER_KIND, ACPX_DRIVER_PROTOCOL_VERSION, QUALIFIED_ACPX_VERSION } from "./generated-profiles.js";
import type { NativeAcpxAgent, NativeAcpxProfileSnapshot } from "../../contracts/native-execution.js";
@@ -13,13 +12,15 @@ export interface QualifiedAcpxProfile {
readonly agent: QualifiedAcpxAgent;
readonly agentProfileVersion: NativeAcpxProfileSnapshot["agentProfileVersion"];
readonly qualificationStatus?: "pending";
readonly modelPolicy?: "explicit-provider-verified";
/** Wire identity: an npm package name or a runner-owned builtin: identifier. */
readonly agentServerPackage: string;
readonly agentServerVersion: string;
readonly agentRuntimePackage: string | null;
readonly agentRuntimeVersion: string | null;
readonly commandDigest: string;
/** Legacy snapshot field: after resolution this is the caller's selected model.
* Keep its serialized name for recovery identity compatibility. The unresolved
* value is qualification metadata, never a product default or admission rule. */
readonly qualificationModel: string;
/** Exact model ID sent to ACP; catalogs are suggestions, not an allowlist. */
readonly reportedModelId: string;
@@ -33,86 +34,7 @@ export interface QualifiedAcpxProfile {
*/
export const QUALIFIED_ACPX_PROFILES: Readonly<
Record<QualifiedAcpxAgent, QualifiedAcpxProfile>
> = deepFreeze({
grok: {
driverKind: ACPX_DRIVER_KIND, protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION,
acpxVersion: QUALIFIED_ACPX_VERSION, agent: "grok", agentProfileVersion: 1,
agentServerPackage: "builtin:grok-acp", agentServerVersion: "1",
agentRuntimePackage: "native:grok", agentRuntimeVersion: "1.0.13",
commandDigest: "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
qualificationModel: "grok-4.7", reportedModelId: "grok-4.7", permissionPolicy: "interactive",
},
pi: {
driverKind: ACPX_DRIVER_KIND,
protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION,
acpxVersion: QUALIFIED_ACPX_VERSION,
agent: "pi",
agentProfileVersion: 1,
agentServerPackage: "pi-acp",
agentServerVersion: "0.0.33",
agentRuntimePackage: "@earendil-works/pi-coding-agent",
agentRuntimeVersion: "0.84.2",
commandDigest:
"sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f",
qualificationModel: "openrouter/deepseek/deepseek-v4-flash-0731",
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: 11,
agentServerPackage: "cursor-agent", agentServerVersion: "2026.09.26-dd393fe",
agentRuntimePackage: null, agentRuntimeVersion: null,
commandDigest: "sha256:2feb50c7b0a317dff454c00115a5bbe4d5c757189691586577be9c80234d477e",
// Every caller selects a model explicitly; the native provider must verify
// that exact ID. Qualification never turns Luna into a default model.
qualificationModel: "", reportedModelId: "", permissionPolicy: "interactive",
modelPolicy: "explicit-provider-verified",
},
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,
acpxVersion: QUALIFIED_ACPX_VERSION,
agent: "claude",
agentProfileVersion: 1,
agentServerPackage: "@agentclientprotocol/claude-agent-acp",
agentServerVersion: "0.73.0",
agentRuntimePackage: "@anthropic-ai/claude-agent-sdk",
agentRuntimeVersion: "0.3.286",
commandDigest:
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
qualificationModel: "claude-sonnet-5",
reportedModelId: "claude-sonnet-5",
permissionPolicy: "interactive",
},
codex: {
driverKind: ACPX_DRIVER_KIND,
protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION,
acpxVersion: QUALIFIED_ACPX_VERSION,
agent: "codex",
agentProfileVersion: 1,
agentServerPackage: "@agentclientprotocol/codex-acp",
agentServerVersion: "1.6.2",
agentRuntimePackage: "@openai/codex",
agentRuntimeVersion: "0.160.0",
commandDigest:
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
qualificationModel: "gpt-5.6-sol",
reportedModelId: "gpt-5.6-sol",
permissionPolicy: "interactive",
},
});
> = deepFreeze(QUALIFIED_ACPX_PROFILE_DATA);
export function resolveQualifiedAcpxProfile(
agent: QualifiedAcpxAgent,
@@ -120,9 +42,10 @@ 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" && agent !== "grok" && profile.modelPolicy !== "explicit-provider-verified" && requestedModel !== profile.qualificationModel) {
const admission = ACPX_MODEL_ADMISSION[agent];
if (admission.kind === "exact" && requestedModel !== admission.model) {
throw new Error(
`ACPX ${agent} profile requires exact model ${profile.qualificationModel}; received ${requestedModel}`,
`ACPX ${agent} profile requires exact model ${admission.model}; received ${requestedModel}`,
);
}
return { ...structuredClone(profile), qualificationModel: requestedModel, reportedModelId: requestedModel };
@@ -26,6 +26,19 @@ afterEach(async () => {
});
describe("ACPX recovery identity", () => {
it("preserves the pre-manifest Cursor recovery profile identity", async () => {
const fixture = await recoveryFixture();
const requestedModel = "explicit-test-model";
const binding = await createAcpxRecoveryBinding({
...fixture.input,
requestedModel,
profile: resolveQualifiedAcpxProfile("cursor", requestedModel),
});
// Captured from e75fde6098b0ddd8cec765bfb6ecaeecb88a26a6 before
// consolidating release declarations; this is historical evidence.
expect(binding.profileDigest).toBe("sha256:801bf9f450a2776ec879f2d85c05d8950172565a87f8f79177e0fc1211ae0bab");
});
it("derives one stable, filesystem-safe runtime directory name", () => {
expect(acpxRuntimeSessionDirectoryName("session/1")).toMatch(
/^session_1-[0-9a-f]{16}$/,
@@ -33,17 +33,8 @@ const claudePatch = await readFile(
),
"utf8",
);
const qualifiedProfiles = await readFile(
new URL("../src/drivers/acpx/qualified-profiles.ts", import.meta.url),
"utf8",
);
const runnerdAcpxBackend = await readFile(
new URL(
"../runner/crates/runner-core/src/acpx_provider_backend.rs",
import.meta.url,
),
"utf8",
);
const profiles = JSON.parse(await readFile(new URL("../acpx-profiles.json", import.meta.url), "utf8"));
const runnerdProfiles = await readFile(new URL("../runner/crates/runner-core/src/generated_acpx_profiles.rs", import.meta.url), "utf8");
const providerPackBuilder = await readFile(
new URL("../scripts/build-provider-pack.mjs", import.meta.url),
"utf8",
@@ -75,29 +66,16 @@ test("the runner pins every qualified ACPX production dependency", () => {
});
test("the patched Codex ACP executable digest stays aligned across launch boundaries", async () => {
const profileMatch =
/agent: "codex"[\s\S]*?commandDigest:\s*"(sha256:[a-f0-9]{64})"/.exec(
qualifiedProfiles,
);
assert.ok(profileMatch, "qualified Codex ACPX profile digest");
const digest = profileMatch[1];
const digest = profiles.profiles.codex.commandDigest;
const packagePath = createRequire(import.meta.url).resolve("@agentclientprotocol/codex-acp/package.json");
const installed = JSON.parse(await readFile(packagePath, "utf8"));
const executable = await readFile(resolve(dirname(packagePath), installed.bin["codex-acp"]));
assert.equal(digest, `sha256:${createHash("sha256").update(executable).digest("hex")}`,
"the identity binds installed executable bytes, not the patch file");
assert.match(runnerdAcpxBackend, new RegExp(`"codex"[\\s\\S]*?${digest}`));
assert.match(
providerPackBuilder,
new RegExp(`acpxProfileDigests:[\\s\\S]*?codex:[\\s\\S]*?${digest}`),
);
assert.match(
nativeSessionExecutor,
new RegExp(
`REMOTE_PROVIDER_PACK_PROFILE_DIGESTS[\\s\\S]*?codex:[\\s\\S]*?${digest}`,
),
);
assert.match(runnerdProfiles, new RegExp(`"codex"[\\s\\S]*?${digest}`));
assert.match(providerPackBuilder, /codex:\s*profiles\.profiles\.codex\.commandDigest/);
assert.match(nativeSessionExecutor, /codex:\s*QUALIFIED_ACPX_PROFILES\.codex\.commandDigest/);
});
test("the package exposes only the reviewed runner CLI binaries", () => {
@@ -1,3 +1,4 @@
import { QUALIFIED_ACPX_PROFILES, QUALIFIED_ACPX_VERSION } from "../../vendor/paperclip-runner/index.js";
import { isProviderMode } from "../../vendor/paperclip-runner/index.js";
import { bundledRemoteProviderPackManifestPath, bundledRemoteRunnerBinary } from "../../vendor/paperclip-runner/index.js";
import { nativeRetryCancellationEligible, rethrowNativeCancellationLockConflict, assertCancellationRequest, cancellationIntentId as callerCancellationIntentId, cancellationRequestId } from "./native-cancellation-request.js";
@@ -9483,19 +9484,19 @@ const RUNNERD_BINARY_CONTRACT_VERSION = 2;
const REMOTE_PROVIDER_PACK_SCHEMA = "paperclip-runner/remote-provider-pack/v1";
const REMOTE_PROVIDER_PACK_PINS = {
nodeMinimum: "24.11.0",
codex: "0.160.0",
codex: QUALIFIED_ACPX_PROFILES.codex.agentRuntimeVersion,
opencode: "1.18.34",
acpx: "0.13.1",
claudeAcp: "0.73.0",
codexAcp: "1.6.2",
grok: "1.0.13",
acpx: QUALIFIED_ACPX_VERSION,
claudeAcp: QUALIFIED_ACPX_PROFILES.claude.agentServerVersion,
codexAcp: QUALIFIED_ACPX_PROFILES.codex.agentServerVersion,
grok: QUALIFIED_ACPX_PROFILES.grok.agentRuntimeVersion,
} as const;
const REMOTE_PROVIDER_PACK_PROFILE_DIGESTS = {
grok: "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
grok: QUALIFIED_ACPX_PROFILES.grok.commandDigest,
claude:
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
QUALIFIED_ACPX_PROFILES.claude.commandDigest,
codex:
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
QUALIFIED_ACPX_PROFILES.codex.commandDigest,
} as const;
const REMOTE_PROVIDER_PACK_ARTIFACT_PATHS = {
grokLauncher: "dist/providers/grok/launcher.cjs",