mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
Carry the final Pi integration through launch and resume
This commit is contained in:
commit
4f873cc59a
41 files changed
+3672
-152
No files matched your search
@@ -43,12 +43,15 @@ packages/paperclip-runner/test/**
|
||||
!packages/paperclip-runner/test/fixtures
|
||||
packages/paperclip-runner/test/fixtures/**
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v16-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v17-identity.json
|
||||
!packages/paperclip-runner/test-fixtures
|
||||
packages/paperclip-runner/test-fixtures/**
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp
|
||||
packages/paperclip-runner/test-fixtures/pi-acp/**
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v18-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v19-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v20-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v21-identity.json
|
||||
!packages/paperclip-runner/docs/capability-contract.md
|
||||
# check:runner-workflow-traceability access()es every regression test the
|
||||
# stress-traceability spec names — those are src/**/*.test.ts files, so
|
||||
|
||||
@@ -194,7 +194,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f scripts/verify-grok-npm-install.mjs || { echo "Selected source does not provide the public-install verifier"; exit 1; }
|
||||
pnpm install --frozen-lockfile --ignore-scripts > remote-verification/npm-setup.log 2>&1
|
||||
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile > remote-verification/npm-setup.log 2>&1
|
||||
pnpm install --frozen-lockfile --ignore-scripts >> remote-verification/npm-setup.log 2>&1
|
||||
pnpm build > remote-verification/npm-build.log 2>&1
|
||||
PAPERCLIP_RUNNER_QUALIFICATION_PACKAGES_DIR="$PWD/remote-verification/qualification-packages" \
|
||||
node scripts/verify-grok-npm-install.mjs > remote-verification/public-npm-install.log 2>&1
|
||||
|
||||
+3
-1
@@ -49,7 +49,9 @@ COPY packages/plugins/plugin-workspace-diff/package.json packages/plugins/plugin
|
||||
COPY patches/ patches/
|
||||
COPY scripts/link-plugin-dev-sdk.mjs scripts/
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
# CI owns repository lock updates. Resolve manifest changes in the image's
|
||||
# private build tree, then install from that exact generated lock.
|
||||
RUN pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile && pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS rust-toolchain
|
||||
WORKDIR /app
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,3 +1,4 @@
|
||||
import { resolvePaperclipRunnerPiThinkingLevel } from "./paperclip-runner-permissions.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
@@ -11,11 +12,12 @@ import {
|
||||
} from "./paperclip-runner-permissions.js";
|
||||
|
||||
describe("Paperclip Runner permission defaults", () => {
|
||||
it("holds intermediate Pi admission and preserves upstream Cursor qualification", () => {
|
||||
it("admits Pi with provider credentials while keeping Copilot pending", () => {
|
||||
expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "pi"))
|
||||
.toMatchObject({ qualified: false, credentialEnvironment: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"] });
|
||||
expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "copilot")?.qualified).toBe(false);
|
||||
expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "cursor")?.qualified).toBe(true);
|
||||
.toMatchObject({ qualified: true, credentialEnvironment: expect.arrayContaining(["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"]) });
|
||||
for (const agent of ["copilot"]) {
|
||||
expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === agent)?.qualified).toBe(false);
|
||||
}
|
||||
});
|
||||
it("defaults Codex to the only qualified non-interactive mode", () => {
|
||||
expect(resolvePaperclipRunnerPermissionMode("codex", undefined)).toBe(
|
||||
@@ -91,3 +93,17 @@ describe("Cursor session mode admission", () => {
|
||||
expect(() => resolvePaperclipRunnerCursorMode(provider, agent, "plan")).toThrow("only for Cursor");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Pi thinking configuration", () => {
|
||||
it.each([undefined, "off", "low", "high", "max"] as const)("persists exact level %s with low as the fresh-config default", value => {
|
||||
expect(resolvePaperclipRunnerPiThinkingLevel("acpx", "pi", value)).toBe(value ?? "low");
|
||||
});
|
||||
it.each(["medium", "minimal", "xhigh", "", null, 1])("rejects unsupported alias %s", value => {
|
||||
expect(() => resolvePaperclipRunnerPiThinkingLevel("acpx", "pi", value)).toThrow();
|
||||
});
|
||||
it("rejects foreign provider settings", () => {
|
||||
expect(() => resolvePaperclipRunnerPiThinkingLevel("codex", "pi", "low")).toThrow(/only/);
|
||||
expect(() => resolvePaperclipRunnerPiThinkingLevel("acpx", "cursor", "low")).toThrow(/only/);
|
||||
expect(resolvePaperclipRunnerPiThinkingLevel("codex", undefined, undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -246,7 +246,7 @@ export const PAPERCLIP_RUNNER_ACPX_PROFILES = Object.freeze([
|
||||
{ value: "claude", label: "Claude", qualified: true, credentialEnvironment: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"] },
|
||||
{ value: "cursor", label: "Cursor", qualified: true, credentialEnvironment: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"] },
|
||||
{ value: "copilot", label: "GitHub Copilot", qualified: false, credentialEnvironment: ["COPILOT_GITHUB_TOKEN"] },
|
||||
{ value: "pi", label: "Pi", qualified: false, credentialEnvironment: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"] },
|
||||
{ value: "pi", label: "Pi", qualified: true, credentialEnvironment: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"] },
|
||||
] as const);
|
||||
|
||||
/** Exact Pi native thinking levels; aliases that silently clamp are not configuration. */
|
||||
|
||||
@@ -98,6 +98,10 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string
|
||||
const provider = isPaperclipRunnerProvider(providerCandidate)
|
||||
? providerCandidate
|
||||
: "codex";
|
||||
if (provider === "openai_dot") {
|
||||
return { provider, lifecycleMode: "per_turn", allowUnmeteredProvider: schemaValues.allowUnmeteredProvider === true, dotWorkspaceAccess: schemaValues.dotWorkspaceAccess === true, dotAttachmentAccess: schemaValues.dotAttachmentAccess === true,
|
||||
...(typeof schemaValues.dotBindingId === "string" ? { dotBindingId: schemaValues.dotBindingId } : {}) };
|
||||
}
|
||||
const selectedAcpxProfile = PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === schemaValues.acpxAgent);
|
||||
if (provider === "acpx" && selectedAcpxProfile && !selectedAcpxProfile.qualified) {
|
||||
throw new Error(`${selectedAcpxProfile.label} is not enabled for production`);
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
"requiresProviderPolicy": false
|
||||
},
|
||||
"pi": {
|
||||
"agentProfileVersion": 20,
|
||||
"agentProfileVersion": 21,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:465ae72460f05cae961873f09cd7cd3652dc3a6949930b7ee16303925cd3dd0e",
|
||||
"commandDigest": "sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"cursor": {
|
||||
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 16,
|
||||
"agentProfileVersion": 17,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:8591f9a78a16aac4cf558733cd512f09def483fc11c673504bf93be7476d994c",
|
||||
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"requiresProviderPolicy": true,
|
||||
"qualificationStatus": "pending"
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: Some("@earendil-works/pi-coding-agent"),
|
||||
agent_runtime_version: Some("1.0.0"),
|
||||
command_digest:
|
||||
"sha256:465ae72460f05cae961873f09cd7cd3652dc3a6949930b7ee16303925cd3dd0e",
|
||||
"sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"cursor" => AcpxReleaseProfile {
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:8591f9a78a16aac4cf558733cd512f09def483fc11c673504bf93be7476d994c",
|
||||
"sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
|
||||
const CODEX_ACPX_DIGEST: &str =
|
||||
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3";
|
||||
const PI_ACPX_DIGEST: &str =
|
||||
"sha256:465ae72460f05cae961873f09cd7cd3652dc3a6949930b7ee16303925cd3dd0e";
|
||||
"sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1";
|
||||
|
||||
fn temporary_directory(label: &str) -> PathBuf {
|
||||
let nonce = SystemTime::now()
|
||||
|
||||
@@ -129,6 +129,7 @@ export async function bundlePiProvisioner({ write = true, outputRoot = resolve(p
|
||||
["scripts/pi-distribution/package.json", "package.json"],
|
||||
["scripts/pi-distribution/package-lock.json", "package-lock.json"],
|
||||
["../../patches/pi-acp@0.0.33.patch", "pi-acp.patch"],
|
||||
["../../patches/brace-expansion@5.0.9.patch", "brace-expansion.patch"],
|
||||
["src/drivers/acpx/pi-acp-runtime.ts", "pi-acp-runtime.ts"],
|
||||
["src/drivers/acpx/pi-runtime-extension.ts", "pi-runtime-extension.ts"],
|
||||
]) await cp(resolve(packageRoot, source), resolve(inputs, name));
|
||||
|
||||
@@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract;
|
||||
assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`);
|
||||
assert.equal(contract.acpxPatchSha256, createHash("sha256")
|
||||
.update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex"));
|
||||
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v20-identity.json"], ["copilot", "test/fixtures/copilot-profile-v16-identity.json"]]) {
|
||||
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v21-identity.json"], ["copilot", "test/fixtures/copilot-profile-v17-identity.json"]]) {
|
||||
const identity = await readJson(path);
|
||||
assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion);
|
||||
assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest);
|
||||
|
||||
@@ -16,6 +16,8 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const workspaceRoot = resolve(packageRoot, "../..");
|
||||
const lockDirectory = join(packageRoot, "scripts/pi-distribution");
|
||||
const patchPath = join(workspaceRoot, "patches/pi-acp@0.0.33.patch");
|
||||
const securityPatchPath = join(workspaceRoot, "patches/brace-expansion@5.0.9.patch");
|
||||
export const PI_BRACE_SECURITY_PATCH_SHA256 = "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814";
|
||||
const supportedTargets = new Set(["darwin-arm64", "darwin-x64", "linux-x64"]);
|
||||
export const PI_DISTRIBUTION_PINS = Object.freeze({
|
||||
wrapper: "0.0.33", runtime: "1.0.0", sdk: "0.26.0", zod: "3.25.76", nodeVersion: PI_NODE_VERSION, undici: "8.10.2", nodeBundledUndici: "7.29.1",
|
||||
@@ -24,7 +26,7 @@ export const PI_DISTRIBUTION_PINS = Object.freeze({
|
||||
});
|
||||
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
/** npm ci preserves the complete Pi 1.0.0 published shrinkwrap unchanged. */
|
||||
/** Install the published Pi graph; then apply the one pinned security correction. */
|
||||
export function piDistributionInstallCommand() {
|
||||
return ["ci", "--ignore-scripts", "--include=optional", "--omit=dev", "--no-audit", "--no-fund", "--registry=https://registry.npmjs.org", "--userconfig=.npmrc", "--globalconfig=.npmrc-global"];
|
||||
}
|
||||
@@ -56,6 +58,14 @@ export async function verifyLockedPiPackageGraph(root, lock, target = { platform
|
||||
for (const [path, entry] of Object.entries(shrinkwrap.packages)) {
|
||||
if (!path) continue;
|
||||
const pinned = lock.packages[`${piPath}/${path}`];
|
||||
// Pi bundles its dependency payload, so npm overrides do not replace it.
|
||||
// Only this reviewed build-owned patch may differ from the upstream graph.
|
||||
if (path === "node_modules/brace-expansion" && entry.version === "5.0.9"
|
||||
&& entry.integrity === "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="
|
||||
&& entry.resolved === "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz"
|
||||
&& pinned?.version === "5.0.12"
|
||||
&& pinned.integrity === "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="
|
||||
&& pinned.resolved === "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz") continue;
|
||||
if (!pinned || pinned.version !== entry.version || (entry.integrity !== undefined && pinned.integrity !== entry.integrity) || pinned.resolved !== entry.resolved) throw new Error(`Pi distribution dropped or changed shrinkwrapped package ${path}`);
|
||||
}
|
||||
return verified;
|
||||
@@ -130,6 +140,7 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
|
||||
checkCancelled();
|
||||
const inputLockDirectory = inputs?.lockDirectory ?? lockDirectory;
|
||||
const inputPatchPath = inputs?.patchPath ?? patchPath;
|
||||
const inputSecurityPatchPath = inputs?.securityPatchPath ?? securityPatchPath;
|
||||
const helperSourcePath = inputs?.helperSourcePath ?? join(packageRoot, "src/drivers/acpx/pi-acp-runtime.ts");
|
||||
const extensionSourcePath = inputs?.extensionSourcePath ?? join(packageRoot, "src/drivers/acpx/pi-runtime-extension.ts");
|
||||
if (typeof outputRoot !== "string" || !outputRoot || !isAbsolute(outputRoot)) throw new Error("Pi distribution output must be absolute");
|
||||
@@ -183,6 +194,10 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
|
||||
const installedLockBytes = await readFile(join(runtimeRoot, "package-lock.json"));
|
||||
if (!installedLockBytes.equals(await readFile(join(inputLockDirectory, "package-lock.json")))) throw new Error("Pi installation changed its committed lock");
|
||||
const lock = JSON.parse(installedLockBytes.toString("utf8"));
|
||||
if (hash(await readFile(inputSecurityPatchPath)) !== PI_BRACE_SECURITY_PATCH_SHA256) throw new Error("Pi dependency security patch differs from its reviewed pin");
|
||||
const braceRoot = join(runtimeRoot, "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion");
|
||||
await runOwned("git", ["apply", "--check", inputSecurityPatchPath], { cwd: braceRoot, env: environment, timeout: 10_000 });
|
||||
await runOwned("git", ["apply", inputSecurityPatchPath], { cwd: braceRoot, env: environment, timeout: 10_000 });
|
||||
const packageCount = await verifyLockedPiPackageGraph(runtimeRoot, lock);
|
||||
const wrapper = join(runtimeRoot, "node_modules/pi-acp");
|
||||
await runOwned("git", ["apply", "--check", inputPatchPath], { cwd: wrapper, env: environment, timeout: 10_000 });
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtemp, mkdir, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { assertPiNodeSystemDependencies, PI_DISTRIBUTION_PINS, materializePiDistribution, resolvePiBundledNpm, piDistributionInstallCommand, verifyLockedPiPackageGraph, writePiDistributionManifest } from "./materialize-pi-distribution.mjs";
|
||||
import { assertPiNodeSystemDependencies, PI_BRACE_SECURITY_PATCH_SHA256, PI_DISTRIBUTION_PINS, materializePiDistribution, resolvePiBundledNpm, piDistributionInstallCommand, verifyLockedPiPackageGraph, writePiDistributionManifest } from "./materialize-pi-distribution.mjs";
|
||||
import { verifyPiRuntimeManifest } from "../src/drivers/acpx/pi-verified-runtime.ts";
|
||||
|
||||
async function fixture(t) {
|
||||
@@ -30,6 +31,9 @@ test("distribution lock closes exact wrapper, SDK and upstream Pi graph with int
|
||||
const lock = JSON.parse(await readFile(new URL("./pi-distribution/package-lock.json", import.meta.url), "utf8"));
|
||||
assert.deepEqual(lock.packages[""].dependencies, pkg.dependencies);
|
||||
assert.equal(pkg.overrides, undefined);
|
||||
assert.equal(lock.packages["node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion"].version, "5.0.12");
|
||||
const patch = await readFile(new URL("../../../patches/brace-expansion@5.0.9.patch", import.meta.url));
|
||||
assert.equal(createHash("sha256").update(patch).digest("hex"), PI_BRACE_SECURITY_PATCH_SHA256);
|
||||
assert.equal(lock.packages["node_modules/@earendil-works/pi-coding-agent/node_modules/undici"].version, "8.10.2");
|
||||
assert.equal(PI_DISTRIBUTION_PINS.nodeVersion, "24.21.0");
|
||||
assert.equal(PI_DISTRIBUTION_PINS.nodeBundledUndici, "7.29.1");
|
||||
@@ -46,6 +50,25 @@ test("distribution lock closes exact wrapper, SDK and upstream Pi graph with int
|
||||
assert.equal(piDistributionInstallCommand()[0], "ci");
|
||||
});
|
||||
|
||||
test("only the exact brace-expansion security correction may differ from Pi's shrinkwrap", async (t) => {
|
||||
const { root, write } = await fixture(t);
|
||||
const pi = "node_modules/@earendil-works/pi-coding-agent";
|
||||
const old = { version: "5.0.9", resolved: "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", integrity: "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==" };
|
||||
const lock = JSON.parse(await readFile(new URL("./pi-distribution/package-lock.json", import.meta.url), "utf8"));
|
||||
const brace = lock.packages[`${pi}/node_modules/brace-expansion`];
|
||||
const minimal = { lockfileVersion: 3, packages: { [pi]: lock.packages[pi], [`${pi}/node_modules/brace-expansion`]: brace } };
|
||||
await write(`${pi}/package.json`, JSON.stringify({ version: "1.0.0" }));
|
||||
await write(`${pi}/npm-shrinkwrap.json`, JSON.stringify({ version: "1.0.0", lockfileVersion: 3, packages: { "node_modules/brace-expansion": old } }));
|
||||
await write(`${pi}/node_modules/brace-expansion/package.json`, JSON.stringify({ version: "5.0.9" }));
|
||||
await assert.rejects(verifyLockedPiPackageGraph(root, minimal), /differs from its lock/);
|
||||
await write(`${pi}/node_modules/brace-expansion/package.json`, JSON.stringify({ version: "5.0.12" }));
|
||||
assert.equal(await verifyLockedPiPackageGraph(root, minimal), 2);
|
||||
for (const field of ["version", "resolved", "integrity"]) {
|
||||
const tampered = structuredClone(minimal); tampered.packages[`${pi}/node_modules/brace-expansion`][field] += "-changed";
|
||||
await assert.rejects(verifyLockedPiPackageGraph(root, tampered), /unpin|differs|dropped or changed/);
|
||||
}
|
||||
});
|
||||
|
||||
test("package verification rejects missing, version-shifted and incomplete shrinkwrap graphs", async (t) => {
|
||||
const { root, write } = await fixture(t);
|
||||
const pi = "node_modules/@earendil-works/pi-coding-agent";
|
||||
|
||||
@@ -1242,9 +1242,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
||||
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
|
||||
@@ -18,7 +18,7 @@ test("public server tar layout carries a self-contained host provisioner and exa
|
||||
await writeFile(join(cli, "acpx-runtime-sidecar.cjs"), "// layout fixture only");
|
||||
await bundlePiProvisioner({ outputRoot: cli });
|
||||
const inputs = join(cli, "pi-provision-inputs");
|
||||
assert.deepEqual((await readdir(inputs)).sort(), ["package-lock.json", "package.json", "pi-acp-runtime.ts", "pi-acp.patch", "pi-runtime-extension.ts"]);
|
||||
assert.deepEqual((await readdir(inputs)).sort(), ["brace-expansion.patch", "package-lock.json", "package.json", "pi-acp-runtime.ts", "pi-acp.patch", "pi-runtime-extension.ts"]);
|
||||
const packageRoot = resolve(dirname(new URL(import.meta.url).pathname), "..");
|
||||
for (const [source, destination] of [
|
||||
["scripts/pi-distribution/package.json", "package.json"], ["scripts/pi-distribution/package-lock.json", "package-lock.json"],
|
||||
|
||||
@@ -75,6 +75,7 @@ export async function provisionPi(entrypoint, checkCancelled = () => {}) {
|
||||
const inputs = join(cli, "pi-provision-inputs");
|
||||
await materializePiDistribution({ outputRoot: stagedOutput, checkCancelled, inputs: {
|
||||
lockDirectory: inputs, patchPath: join(inputs, "pi-acp.patch"),
|
||||
securityPatchPath: join(inputs, "brace-expansion.patch"),
|
||||
helperSourcePath: join(inputs, "pi-acp-runtime.ts"), extensionSourcePath: join(inputs, "pi-runtime-extension.ts"),
|
||||
} });
|
||||
await verifiedInstallation(stagingRoot, join(stagingRoot, "package.json"));
|
||||
|
||||
@@ -519,12 +519,13 @@ describe("native backend factory", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps direct Pi execution held in the prerequisite layer", async () => {
|
||||
it("constructs the qualified Pi backend without a diagnostic opt-in or provider launch", async () => {
|
||||
const input = acpxExecution();
|
||||
if (input.provider.kind !== "acpx") throw new Error("invalid fixture");
|
||||
const profile = resolveQualifiedAcpxProfile("pi", "custom/explicit-test-model");
|
||||
Object.assign(input.provider, { agent: "pi", model: profile.qualificationModel, piThinkingLevel: "low", profile });
|
||||
expect(() => createNativeSessionBackend(input, { acpxRuntimeDirectory: "/runtime" })).toThrow(/candidate direct execution requires completed qualification/);
|
||||
const backend = createNativeSessionBackend(input, { acpxRuntimeDirectory: "/runtime" });
|
||||
await expect(backend.descriptor()).resolves.toMatchObject({ name: "acpx_runtime", version: "0.13.1" });
|
||||
});
|
||||
|
||||
it.each(["copilot"] as const)("rejects unqualified %s direct execution even with an exact persisted profile", agent => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { createCapabilityFixtureState } from "../mock-core/capability-control-plane-types.js";
|
||||
|
||||
import { parseNativeRuntimeContext } from "../contracts/runtime-context.js";
|
||||
import { NativeSessionCloseUnrecoverableError } from "../contracts/native-session-backend.js";
|
||||
import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
|
||||
import type { CapabilityLiveSessionSnapshot } from "../live/live-session.js";
|
||||
import {
|
||||
@@ -35,7 +36,8 @@ function request(overrides: Record<string, unknown> = {}): unknown {
|
||||
maxAgentTurns: 1,
|
||||
maxEstimatedCostNanodollars: 100_000_000,
|
||||
},
|
||||
session: overrides.acpxAgent === "pi" ? { piThinkingLevel: "low" } : {},
|
||||
session: {},
|
||||
...(overrides.acpxAgent === "pi" ? { piThinkingLevel: "low", session: { piThinkingLevel: "low" } } : {}),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -67,19 +69,28 @@ function agentCoreProfile(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe("eval-session request contract", () => {
|
||||
it.each(["pi", "copilot"] as const)("admits %s only with the matching CLI diagnostic opt-in", (agent) => {
|
||||
const value = request({ provider: "acpx", acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), model: "explicit-provider-model" });
|
||||
it.each(["copilot"] as const)("admits %s only with the matching CLI diagnostic opt-in", (agent) => {
|
||||
const value = request({ provider: "acpx", acpxAgent: agent, model: "explicit-provider-model" });
|
||||
expect(() => parseEvalSessionRequest(value)).toThrow("--candidate-profile");
|
||||
expect(parseEvalSessionRequest(value, { candidateProfile: agent })).toMatchObject({ acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), model: "explicit-provider-model" });
|
||||
expect(parseEvalSessionRequest(value, { candidateProfile: agent })).toMatchObject({ acpxAgent: agent, model: "explicit-provider-model" });
|
||||
expect(() => parseEvalSessionRequest(value, { candidateProfile: agent === "pi" ? "cursor" : "pi" })).toThrow("must match");
|
||||
expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), model: "" }), { candidateProfile: agent })).toThrow("request.model");
|
||||
expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, model: "" }), { candidateProfile: agent })).toThrow("request.model");
|
||||
expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: "codex", session: { acpxAgent: agent } }))).toThrow("session.acpxAgent must match");
|
||||
expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), candidateProfile: agent }))).toThrow("--candidate-profile");
|
||||
expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, candidateProfile: agent }))).toThrow("--candidate-profile");
|
||||
});
|
||||
|
||||
it("admits qualified Cursor without a diagnostic flag and preserves its explicit model", () => {
|
||||
expect(parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: "cursor", model: "exact-cursor-model" })))
|
||||
.toMatchObject({ acpxAgent: "cursor", model: "exact-cursor-model" });
|
||||
it("admits Pi without a diagnostic flag and retains matching historical opt-in", () => {
|
||||
const value = request({ provider: "acpx", acpxAgent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731" });
|
||||
expect(parseEvalSessionRequest(value)).toMatchObject({ acpxAgent: "pi" });
|
||||
expect(parseEvalSessionRequest(value, { candidateProfile: "pi" })).toMatchObject({ acpxAgent: "pi" });
|
||||
expect(() => parseEvalSessionRequest(value, { candidateProfile: "cursor" })).toThrow("must match");
|
||||
});
|
||||
|
||||
it("requires exact matching Pi mode in request and session and rejects aliases", () => {
|
||||
const base = { provider: "acpx", acpxAgent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731" };
|
||||
for (const value of [undefined, "medium", "minimal", "xhigh", ""]) expect(() => parseEvalSessionRequest(request({ ...base, piThinkingLevel: value }))).toThrow();
|
||||
expect(() => parseEvalSessionRequest(request({ ...base, piThinkingLevel: "low", session: { piThinkingLevel: "high" } }))).toThrow(/must match/);
|
||||
expect(() => parseEvalSessionRequest(request({ piThinkingLevel: "low" }))).toThrow(/only supported/);
|
||||
});
|
||||
|
||||
it("accepts only known diagnostic flags and rejects ambiguous repeated arguments", () => {
|
||||
@@ -98,17 +109,17 @@ describe("eval-session request contract", () => {
|
||||
expect(() => parseEvalSessionCliArgs([...args, "--expected-acpx-profile", " ".repeat(4097)])).toThrow("4096-byte bound");
|
||||
});
|
||||
|
||||
it.each(["cursor"] as const)("rejects stale or incomplete %s identities before runtime construction", async (agent) => {
|
||||
it.each(["pi", "cursor", "copilot"] as const)("rejects stale or incomplete %s identities before runtime construction", async (agent) => {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "paperclip-eval-profile-"));
|
||||
const model = "explicit-provider-model";
|
||||
const model = agent === "pi" ? "openrouter/deepseek/deepseek-v4-flash-0731" : "explicit-provider-model";
|
||||
const expected = resolveQualifiedAcpxProfile(agent, model);
|
||||
const requestPath = join(workspace, "request.json");
|
||||
const args = ["--request", requestPath, "--output", join(workspace, "output.json"), "--candidate-profile", agent];
|
||||
const serviceFactory = vi.fn(() => { throw new Error("provider must not start"); });
|
||||
try {
|
||||
await writeFile(requestPath, JSON.stringify(request({ provider: "acpx", acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), model,
|
||||
await writeFile(requestPath, JSON.stringify(request({ provider: "acpx", acpxAgent: agent, model,
|
||||
runnerd: { path: join(workspace, "missing-runnerd"), sha256: "a".repeat(64) },
|
||||
session: { workingDirectory: workspace },
|
||||
session: { workingDirectory: workspace, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}) },
|
||||
})));
|
||||
const { commandDigest: _removed, ...incomplete } = expected;
|
||||
for (const stale of [
|
||||
@@ -148,6 +159,7 @@ describe("eval-session request contract", () => {
|
||||
expect(systemInstructions).toContain("Paperclip direct live evaluation");
|
||||
expect(systemInstructions).toContain("Task-state changes in this mock control plane use finish_task and block_task");
|
||||
expect(systemInstructions).toContain("The current user request defines the work for this turn");
|
||||
expect(systemInstructions).toContain("read only the context needed for that request, perform the requested action, and end the turn");
|
||||
expect(systemInstructions).toContain("Do not finish or block the mock task unless the current request asks for that state change");
|
||||
expect(systemInstructions).toContain(
|
||||
`Read-only instruction sibling root: ${context.instructions.bundle.rootPath}`,
|
||||
@@ -213,11 +225,12 @@ describe("eval-session request contract", () => {
|
||||
})))).toBe("17");
|
||||
});
|
||||
|
||||
it("requires explicit Pi diagnosis and accepts both qualified remote provider profiles", () => {
|
||||
expect(() => parseEvalSessionRequest(request({
|
||||
it("admits Pi and both qualified remote provider profiles", () => {
|
||||
expect(parseEvalSessionRequest(request({
|
||||
provider: "acpx",
|
||||
acpxAgent: "pi", piThinkingLevel: "low",
|
||||
}))).toThrow("--candidate-profile");
|
||||
acpxAgent: "pi",
|
||||
piThinkingLevel: "low",
|
||||
}))).toMatchObject({ acpxAgent: "pi", piThinkingLevel: "low" });
|
||||
expect(parseEvalSessionRequest(request({
|
||||
provider: "aws_agentcore",
|
||||
driver: "aws_agentcore_harness_api",
|
||||
@@ -337,7 +350,7 @@ describe("eval-session usage", () => {
|
||||
|
||||
it.each(["pi", "cursor", "copilot"] as const)("keeps %s oracle results when the current turn explicitly has unavailable usage", (agent) => {
|
||||
const parsed = parseEvalSessionRequest(request({
|
||||
provider: "acpx", acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), model: "exact-provider-model",
|
||||
provider: "acpx", acpxAgent: agent, model: "exact-provider-model",
|
||||
}), { candidateProfile: agent });
|
||||
const unavailable = {
|
||||
turnId: "turn-candidate", attemptId: "attempt-1", agent,
|
||||
@@ -442,7 +455,7 @@ describe("eval-session usage", () => {
|
||||
estimatedCostNanodollars: null, pricingVersion: null, ratesUsdPerMillionTokens: null,
|
||||
costCoverage: "unpriced",
|
||||
});
|
||||
const parsed = parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), model: "exact-provider-model[context=272k]" }), { candidateProfile: agent as "pi" | "cursor" | "copilot" });
|
||||
const parsed = parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, model: "exact-provider-model[context=272k]" }), { candidateProfile: agent as "pi" | "cursor" | "copilot" });
|
||||
expect(() => boundedEvalSessionUsage(parsed, {
|
||||
turnId: "candidate-turn", status: "completed", assistantText: "Completed provider response", snapshot,
|
||||
})).toThrow("budget cost coverage is unavailable");
|
||||
@@ -459,7 +472,7 @@ describe("eval-session usage", () => {
|
||||
});
|
||||
|
||||
describe("eval-session budget settlement", () => {
|
||||
it.each(["no_receipt", "grok_no_receipt", "unpriced", "mixed", "over_limit"] as const)("retains the completed provider outcome while failing %s accounting", async (kind) => {
|
||||
it.each(["no_receipt", "grok_no_receipt", "unpriced", "mixed", "over_limit", "suspension"] as const)("retains evidence while failing %s settlement", async (kind) => {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "eval-budget-settlement-"));
|
||||
try {
|
||||
const binary = join(workspace, "runnerd");
|
||||
@@ -474,7 +487,7 @@ describe("eval-session budget settlement", () => {
|
||||
providerModel: { id: model, provider: agent === "grok" ? "xai" : "github" }, status: "idle", activeTurnId: null,
|
||||
createdAt: "2026-09-28T19:00:00.000Z", updatedAt: "2026-09-28T19:00:01.000Z",
|
||||
authority: { companyId: "company-1", actorId: "actor-1", taskId: "task-1", runId: "run-1", sessionId: "session-1", scenarioId: "budget-test" },
|
||||
config: { provider: "acpx", acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), driver: "acpx_runtime", seedState: state, workingDirectory: workspace, scenario: { id: "budget-test" }, capabilities: [], explicitClaims: [], turnTimeoutMs: 1000 },
|
||||
config: { provider: "acpx", acpxAgent: agent, driver: "acpx_runtime", seedState: state, workingDirectory: workspace, scenario: { id: "budget-test" }, capabilities: [], explicitClaims: [], turnTimeoutMs: 1000 },
|
||||
mockState: state, process: null, networkEvidence: { realPaperclipRequests: 0, childPaperclipEnvironmentKeys: [] },
|
||||
transcript: [{ id: "assistant-1", role: "assistant", text: "Retained actual provider response", turnId: "turn-1", at: "2026-09-28T19:00:01.000Z" }],
|
||||
evidence: [], authorizationRecords: [], attempts: [], terminalTurns: [{ turnId: "turn-1", status: "completed" }],
|
||||
@@ -483,10 +496,18 @@ describe("eval-session budget settlement", () => {
|
||||
} as unknown as CapabilityLiveSessionSnapshot;
|
||||
const sendMessage = vi.fn(async () => ({ turnId: "turn-1", status: "completed", assistantText: "Retained actual provider response", snapshot }));
|
||||
const completeAttempt = vi.fn(async () => undefined);
|
||||
const shutdown = vi.fn(async () => { snapshot.status = "closed"; });
|
||||
const input = request({ provider: "acpx", acpxAgent: agent, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}), model,
|
||||
const shutdown = vi.fn(async () => {
|
||||
snapshot.status = "closed";
|
||||
if (kind === "suspension") throw new NativeSessionCloseUnrecoverableError({
|
||||
runnerSuspended: false, providerDrained: true,
|
||||
suspensionState: { commandStatus: "pending", runnerLifecycle: "ready", runnerIdentityMatches: true },
|
||||
semanticTools: { privateToolId: "private-value-must-not-be-copied" },
|
||||
unrelated: "private-value-must-not-be-copied",
|
||||
});
|
||||
});
|
||||
const input = request({ provider: "acpx", acpxAgent: agent, model,
|
||||
runnerd: { path: binary, sha256: createHash("sha256").update("unused fake runner").digest("hex") },
|
||||
session: { workingDirectory: workspace }, limits: { turnTimeoutMs: 1000, maxAgentTurns: 2, maxEstimatedCostNanodollars: 100_000_000 },
|
||||
session: { workingDirectory: workspace, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}) }, limits: { turnTimeoutMs: 1000, maxAgentTurns: 2, maxEstimatedCostNanodollars: 100_000_000 },
|
||||
});
|
||||
const requestPath = join(workspace, "request.json");
|
||||
const outputPath = join(workspace, "output.json");
|
||||
@@ -496,6 +517,19 @@ describe("eval-session budget settlement", () => {
|
||||
});
|
||||
const artifact = JSON.parse(await readFile(outputPath, "utf8"));
|
||||
expect(exitCode).toBe(2);
|
||||
if (kind === "suspension") {
|
||||
expect(artifact.infrastructureFailure).toEqual({
|
||||
class: "runner_infrastructure_failure", category: "runner_infrastructure", retryable: false,
|
||||
diagnostics: { runnerSuspended: false, providerDrained: true,
|
||||
suspensionCommandStatus: "pending", runnerLifecycle: "ready", runnerIdentityMatches: true },
|
||||
});
|
||||
expect(JSON.stringify(artifact)).not.toContain("private-value-must-not-be-copied");
|
||||
expect(artifact.snapshot.terminalTurns).toEqual([{ turnId: "turn-1", status: "completed" }]);
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(shutdown).toHaveBeenCalledTimes(1);
|
||||
expect(completeAttempt).toHaveBeenCalledWith("succeeded", null);
|
||||
return;
|
||||
}
|
||||
expect(artifact).not.toHaveProperty("infrastructureError");
|
||||
expect(artifact.accountingFailure).toMatchObject({ class: kind === "over_limit" ? "provider_budget_reached" : "provider_budget_coverage_unknown", retryable: false });
|
||||
expect(artifact.turn).toMatchObject({ status: "completed", assistantText: "Retained actual provider response" });
|
||||
|
||||
@@ -3470,7 +3470,7 @@ function runnerEnvironment(
|
||||
if (bound[key] !== undefined) environment[key] = bound[key];
|
||||
}
|
||||
}
|
||||
Object.assign(environment, githubCredentialEnvironment(explicitSource));
|
||||
Object.assign(environment, githubCredentialEnvironment(explicitSource), configuredEnvironment(explicitSource));
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const ACPX_CAPABILITY_PROFILES: Readonly<Record<QualifiedAcpxAgent, AcpxC
|
||||
extensionNotifications: ["github.com/copilot/sessionEvent"],
|
||||
},
|
||||
pi: {
|
||||
displayName: "Pi", qualification: "pending", models: "explicit-provider-verified",
|
||||
displayName: "Pi", qualification: "qualified", models: "explicit-provider-verified",
|
||||
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: ["paperclip/pi_notice"],
|
||||
|
||||
@@ -87,8 +87,8 @@ describe("Copilot build-owned installation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("admits the current v16 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v16-identity.json", import.meta.url), "utf8"));
|
||||
it("admits the current v17 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
expect(identity.declaration.systemInstructionDelivery).toBe(COPILOT_SYSTEM_INSTRUCTION_DELIVERY);
|
||||
expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.declaration.permissionContextContract).toBe(COPILOT_PERMISSION_CONTEXT_CONTRACT);
|
||||
@@ -119,7 +119,7 @@ describe("Copilot build-owned installation", () => {
|
||||
expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]);
|
||||
});
|
||||
it.each([...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v16-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
identity.declaration[field] = "0".repeat(64);
|
||||
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).not.toBe(identity.commandDigest);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { configuredEnvironment } from "../../configured-environment.js";
|
||||
import { PI_CREDENTIAL_NAMES, piCredentialNames } from "./pi-provider-config.js";
|
||||
import type { QualifiedAcpxAgent } from "./qualified-profiles.js";
|
||||
|
||||
@@ -89,7 +90,7 @@ export function createSanitizedAcpxSpawnInput(
|
||||
): SanitizedAcpxSpawnInput {
|
||||
const source = environment ?? process.env;
|
||||
const candidate = isCandidate(agent);
|
||||
const result: NodeJS.ProcessEnv = {};
|
||||
const result: NodeJS.ProcessEnv = configuredEnvironment(environment);
|
||||
const credentialNames = agent === "pi" ? piCredentialNames(environment) : ACPX_CREDENTIAL_NAMES[agent];
|
||||
const allowed = new Set([
|
||||
"PATH",
|
||||
|
||||
@@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 20,
|
||||
"agentProfileVersion": 21,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:465ae72460f05cae961873f09cd7cd3652dc3a6949930b7ee16303925cd3dd0e",
|
||||
"commandDigest": "sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"cursor": {
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 16,
|
||||
"agentProfileVersion": 17,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:8591f9a78a16aac4cf558733cd512f09def483fc11c673504bf93be7476d994c",
|
||||
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"qualificationStatus": "pending",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* three pins. Never accept a digest supplied only by an installed manifest.
|
||||
*/
|
||||
export const PI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({
|
||||
"darwin-arm64": "e076276c674dfffccbb488883e18571d77d7225d801fabf5c8391773ddbbfc6c",
|
||||
"darwin-x64": "eafd44e672dec4966ef6f038620f003e5d492c889d475886cd66be2e9c2bff1d",
|
||||
"linux-x64": "f493df174cfecba3c31c524aa486ae37663cd68f8fcc0d9556e3f615c4a40ce9",
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410",
|
||||
});
|
||||
@@ -72,11 +72,19 @@ describe("Pi installation factory", () => {
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves profile 20 and binds only the configured environment and fixed dependency payload", async () => {
|
||||
const read = async (version: number) => JSON.parse(await readFile(new URL(`../../../test-fixtures/pi-acp/profile-v${version}-identity.json`, import.meta.url), "utf8"));
|
||||
const prior = await read(20), current = await read(21);
|
||||
expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort())
|
||||
.toEqual(["agentProfileVersion", "closure", "credentialEnvironmentSourceSha256", "dependencySecurityPatchSha256"]);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("binds the profile declaration to the reviewed patch and platform closure pins", async () => {
|
||||
const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
||||
const canonical = (value: any): string => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
|
||||
: value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}` : JSON.stringify(value);
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v20-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v21-identity.json", import.meta.url), "utf8"));
|
||||
const declaration = identity.declaration;
|
||||
expect(declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.commandDigest).toBe(`sha256:${hash(canonical(declaration))}`);
|
||||
@@ -85,6 +93,7 @@ describe("Pi installation factory", () => {
|
||||
expect(declaration[key]).toBe(QUALIFIED_ACPX_PROFILES.pi[key]);
|
||||
}
|
||||
expect(declaration.closure).toEqual(PI_DISTRIBUTION_CLOSURE_SHA256);
|
||||
expect(hash(await readFile(new URL("../../../../../patches/brace-expansion@5.0.9.patch", import.meta.url)))).toBe(declaration.dependencySecurityPatchSha256);
|
||||
expect(declaration.nodeVersion).toBe(PI_NODE_VERSION);
|
||||
for (const [path, field] of [["pi-provider-config.ts", "providerConfigurationSourceSha256"], ["environment.ts", "credentialEnvironmentSourceSha256"], ["runtime-sandbox.ts", "runtimeSandboxSourceSha256"], ["recovery-identity.ts", "recoveryIdentitySourceSha256"]]) {
|
||||
expect(hash(await readFile(new URL(`./${path}`, import.meta.url)))).toBe(declaration[field!]);
|
||||
@@ -106,21 +115,21 @@ describe("Pi installation factory", () => {
|
||||
|
||||
it("rejects legacy profiles and tampered executable identities", () => {
|
||||
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 20");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 20");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 21");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 21");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 21");
|
||||
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) {
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
|
||||
|
||||
/** Decodable historical profile revisions, not permission to launch them.
|
||||
* Exact current profile/package/digest admission is checked separately. */
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19;
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20;
|
||||
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
|
||||
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
|
||||
|
||||
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
};
|
||||
|
||||
@@ -28,19 +28,19 @@ import { persistedCursorUsageNotice } from "../drivers/acpx/usage-accounting.js"
|
||||
import { captureTurnRejection } from "../../test/capture-turn-rejection.js";
|
||||
import * as workspaceDiff from "./workspace-diff.js";
|
||||
|
||||
it.each(["pi", "copilot"] as const)("requires separately bound evaluation opt-in for %s", async (acpxAgent) => {
|
||||
it.each(["copilot"] as const)("requires separately bound evaluation opt-in for %s", async (acpxAgent) => {
|
||||
const service = new CapabilityLiveSessionService();
|
||||
await expect(service.create({ provider: "acpx", acpxAgent, ...(acpxAgent === "pi" ? { piThinkingLevel: "low" as const } : {}), requestedModel: "explicit-model" }))
|
||||
await expect(service.create({ provider: "acpx", acpxAgent, requestedModel: "explicit-model" }))
|
||||
.rejects.toThrow("explicit evaluation opt-in");
|
||||
const mismatched = new CapabilityLiveSessionService({ transportOptions: {
|
||||
acpxCandidateProfile: acpxAgent === "pi" ? "copilot" : "pi",
|
||||
acpxCandidateProfile: "pi",
|
||||
} });
|
||||
await expect(mismatched.create({ provider: "acpx", acpxAgent, ...(acpxAgent === "pi" ? { piThinkingLevel: "low" as const } : {}), requestedModel: "explicit-model" }))
|
||||
await expect(mismatched.create({ provider: "acpx", acpxAgent, requestedModel: "explicit-model" }))
|
||||
.rejects.toThrow("explicit evaluation opt-in");
|
||||
});
|
||||
|
||||
it("admits Pi evaluation sessions only with candidate opt-in while preserving the exact profile", async () => {
|
||||
const service = new CapabilityLiveSessionService({ transportFactory: fakeTransportFactory(providerState()), transportOptions: { acpxCandidateProfile: "pi" } });
|
||||
it("admits Pi live sessions without candidate opt-in while preserving the exact profile", async () => {
|
||||
const service = new CapabilityLiveSessionService({ transportFactory: fakeTransportFactory(providerState()) });
|
||||
const session = await service.create({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", requestedModel: "openrouter/deepseek/deepseek-v4-flash-0731" });
|
||||
try {
|
||||
expect(session.snapshot().config.acpxProfile).toMatchObject({
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"commandDigest": "sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 21,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "2735ec4b46929f4f59c26cea42e623dfc81016aa7da7bd8399a24caa68a75d6b",
|
||||
"credentialEnvironmentSourceSha256": "c49186bd73da7f1c9401d8ab10fe581ccccad294efe5da8621dd03adb91dc094",
|
||||
"runtimeSandboxSourceSha256": "1c1ddb0b24c3417250ce161cda897ba59b1adce7c7609ed553be298464e6c1c4",
|
||||
"recoveryIdentitySourceSha256": "f333a24b19ef1b5b5d23ea9778e753f7051690e835ef9e4476a962d8a231c375",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 17,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "91629734a80f0d32295cbce2c573b32af9e345f45302ff4525c269f66811aa06",
|
||||
"semanticDirectDriverSourceSha256": "2b7b7eaadee658312816ecdcbcb311ce491521fedf39e2ecb571036c36ff7b8e",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "fe82ac2cbf194d31ac801de6a3cacfb165cb34146c5c26557f283b8c87a5f01e",
|
||||
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
--- a/dist/commonjs/index.d.ts
|
||||
+++ b/dist/commonjs/index.d.ts
|
||||
@@ -1,8 +1,12 @@
|
||||
export declare const EXPANSION_MAX = 100000;
|
||||
export declare const EXPANSION_MAX_LENGTH = 4000000;
|
||||
+export declare const EXPANSION_MAX_DEPTH = 1000;
|
||||
+export declare const EXPANSION_MAX_REWRITES = 1000;
|
||||
export type BraceExpansionOptions = {
|
||||
max?: number;
|
||||
maxLength?: number;
|
||||
+ maxDepth?: number;
|
||||
+ maxRewrites?: number;
|
||||
};
|
||||
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
\ No newline at end of file
|
||||
--- a/dist/commonjs/index.d.ts.map
|
||||
+++ b/dist/commonjs/index.d.ts.map
|
||||
@@ -1 +1 @@
|
||||
-{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAwD7C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
|
||||
\ No newline at end of file
|
||||
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAU7C,eAAO,MAAM,mBAAmB,OAAQ,CAAA;AAUxC,eAAO,MAAM,sBAAsB,OAAQ,CAAA;AAyE3C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YA+BtE"}
|
||||
\ No newline at end of file
|
||||
--- a/dist/commonjs/index.js
|
||||
+++ b/dist/commonjs/index.js
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
-exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
|
||||
+exports.EXPANSION_MAX_REWRITES = exports.EXPANSION_MAX_DEPTH = exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
|
||||
exports.expand = expand;
|
||||
const balanced_match_1 = require("balanced-match");
|
||||
const escSlash = '\0SLASH' + Math.random() + '\0';
|
||||
@@ -30,6 +30,24 @@
|
||||
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
||||
// characters) so legitimate input is unaffected.
|
||||
exports.EXPANSION_MAX_LENGTH = 4_000_000;
|
||||
+// `expand_` recurses once per level of brace *nesting* - both when expanding a
|
||||
+// set's comma members and when re-wrapping a set whose body is a single part.
|
||||
+// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
|
||||
+// level per chained group), which left nesting depth unbounded: about 3,100
|
||||
+// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
|
||||
+// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
|
||||
+// will follow nesting. It sits far above any realistic pattern and well below
|
||||
+// the depth at which the stack runs out.
|
||||
+exports.EXPANSION_MAX_DEPTH = 1_000;
|
||||
+// Bash keeps a quirk where a brace group followed by a comma set still expands
|
||||
+// (`{a},b}`). The parser implements it by rewriting the string and restarting
|
||||
+// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
|
||||
+// full passes over a string that itself grows by one `escClose` sentinel each
|
||||
+// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
|
||||
+// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
|
||||
+// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
|
||||
+// times the scan may restart. Real `{a},b}` input needs a handful.
|
||||
+exports.EXPANSION_MAX_REWRITES = 1_000;
|
||||
function numeric(str) {
|
||||
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
||||
}
|
||||
@@ -49,37 +67,52 @@
|
||||
.replace(escCommaPattern, ',')
|
||||
.replace(escPeriodPattern, '.');
|
||||
}
|
||||
+// Like `target.push(...items)` but doesn't overflow the stack
|
||||
+function pushAll(target, items) {
|
||||
+ for (let i = 0; i < items.length; i++) {
|
||||
+ target.push(items[i]);
|
||||
+ }
|
||||
+}
|
||||
/**
|
||||
* Basically just str.split(","), but handling cases
|
||||
* where we have nested braced sections, which should be
|
||||
* treated as individual members, like {a,{b,c},d}
|
||||
*/
|
||||
function parseCommaParts(str) {
|
||||
- if (!str) {
|
||||
- return [''];
|
||||
- }
|
||||
const parts = [];
|
||||
- const m = (0, balanced_match_1.balanced)('{', '}', str);
|
||||
- if (!m) {
|
||||
- return str.split(',');
|
||||
- }
|
||||
- const { pre, body, post } = m;
|
||||
- const p = pre.split(',');
|
||||
- p[p.length - 1] += '{' + body + '}';
|
||||
- const postParts = parseCommaParts(post);
|
||||
- if (post.length) {
|
||||
- ;
|
||||
- p[p.length - 1] += postParts.shift();
|
||||
- p.push.apply(p, postParts);
|
||||
+ // Walk the brace groups iteratively. Recursing on `post` once per group let a
|
||||
+ // chain of them exhaust the stack - the parsing-side counterpart to
|
||||
+ // the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
|
||||
+ // `maxLength` can bound, since it happens before expansion.
|
||||
+ //
|
||||
+ // The part the next chunk continues
|
||||
+ let carry = '';
|
||||
+ for (;;) {
|
||||
+ const m = (0, balanced_match_1.balanced)('{', '}', str);
|
||||
+ if (!m) {
|
||||
+ const tail = str.split(',');
|
||||
+ tail[0] = carry + tail[0];
|
||||
+ pushAll(parts, tail);
|
||||
+ return parts;
|
||||
+ }
|
||||
+ const { pre, body, post } = m;
|
||||
+ const p = pre.split(',');
|
||||
+ p[0] = carry + p[0];
|
||||
+ p[p.length - 1] += '{' + body + '}';
|
||||
+ if (!post.length) {
|
||||
+ pushAll(parts, p);
|
||||
+ return parts;
|
||||
+ }
|
||||
+ carry = p.pop();
|
||||
+ pushAll(parts, p);
|
||||
+ str = post;
|
||||
}
|
||||
- parts.push.apply(parts, p);
|
||||
- return parts;
|
||||
}
|
||||
function expand(str, options = {}) {
|
||||
if (!str) {
|
||||
return [];
|
||||
}
|
||||
- const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = options;
|
||||
+ const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH, maxDepth = exports.EXPANSION_MAX_DEPTH, maxRewrites = exports.EXPANSION_MAX_REWRITES, } = options;
|
||||
// I don't know why Bash 4.3 does this, but it does.
|
||||
// Anything starting with {} will have the first two bytes preserved
|
||||
// but *only* at the top level, so {},a}b will not expand to anything,
|
||||
@@ -89,7 +122,7 @@
|
||||
if (str.slice(0, 2) === '{}') {
|
||||
str = '\\{\\}' + str.slice(2);
|
||||
}
|
||||
- return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
||||
+ return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
|
||||
}
|
||||
function embrace(str) {
|
||||
return '{' + str + '}';
|
||||
@@ -184,7 +217,13 @@
|
||||
}
|
||||
return N;
|
||||
}
|
||||
-function expand_(str, max, maxLength, isTop) {
|
||||
+function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
|
||||
+ // Too deeply nested to keep following: treat the rest as literal, the same
|
||||
+ // way a group that cannot expand is already handled. Truncating rather than
|
||||
+ // throwing keeps `expand` total, matching `max` and `maxLength`.
|
||||
+ if (depth > maxDepth) {
|
||||
+ return [str];
|
||||
+ }
|
||||
// Consume the string's top-level brace groups left to right, threading a
|
||||
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
||||
// rather than recursing on `m.post` once per group - keeps the native stack
|
||||
@@ -196,6 +235,9 @@
|
||||
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
||||
// is on the final strings, so it is applied to whichever `combine` produces
|
||||
// them (the one with no brace set left in the tail).
|
||||
+ // How many times the `{a},b}` rewrite below has restarted the scan. Each pass
|
||||
+ // re-reads the whole string, so leaving this unbounded is quadratic.
|
||||
+ let rewrites = 0;
|
||||
let dropEmpties = false;
|
||||
let firstGroup = true;
|
||||
for (;;) {
|
||||
@@ -220,7 +262,8 @@
|
||||
const isOptions = m.body.indexOf(',') >= 0;
|
||||
if (!isSequence && !isOptions) {
|
||||
// {a},b}
|
||||
- if (m.post.match(/,(?!,).*\}/)) {
|
||||
+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
|
||||
+ rewrites++;
|
||||
str = m.pre + '{' + m.body + escClose + m.post;
|
||||
isTop = true;
|
||||
continue;
|
||||
@@ -240,7 +283,7 @@
|
||||
let n = parseCommaParts(m.body);
|
||||
if (n.length === 1 && n[0] !== undefined) {
|
||||
// x{{a,b}}y ==> x{a}y x{b}y
|
||||
- n = expand_(n[0], max, maxLength, false).map(embrace);
|
||||
+ n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
|
||||
//XXX is this necessary? Can't seem to hit it in tests.
|
||||
/* c8 ignore start */
|
||||
if (n.length === 1) {
|
||||
@@ -266,12 +309,13 @@
|
||||
values = [];
|
||||
let valuesLength = 0;
|
||||
outer: for (let j = 0; j < n.length; j++) {
|
||||
- const expanded = expand_(n[j], max, maxLength, false);
|
||||
+ const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
|
||||
for (let k = 0; k < expanded.length; k++) {
|
||||
const v = expanded[k];
|
||||
if (dropsEmpties && !v)
|
||||
continue;
|
||||
- if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||
+ if (values.length >= max ||
|
||||
+ valuesLength + v.length > maxLength) {
|
||||
break outer;
|
||||
}
|
||||
values.push(v);
|
||||
--- a/dist/commonjs/index.js.map
|
||||
+++ b/dist/commonjs/index.js.map
|
||||
@@ -1 +1 @@
|
||||
-{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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
Line truncated
|
||||
\ No newline at end of file
|
||||
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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
Line truncated
|
||||
\ No newline at end of file
|
||||
--- a/dist/esm/index.d.ts
|
||||
+++ b/dist/esm/index.d.ts
|
||||
@@ -1,8 +1,12 @@
|
||||
export declare const EXPANSION_MAX = 100000;
|
||||
export declare const EXPANSION_MAX_LENGTH = 4000000;
|
||||
+export declare const EXPANSION_MAX_DEPTH = 1000;
|
||||
+export declare const EXPANSION_MAX_REWRITES = 1000;
|
||||
export type BraceExpansionOptions = {
|
||||
max?: number;
|
||||
maxLength?: number;
|
||||
+ maxDepth?: number;
|
||||
+ maxRewrites?: number;
|
||||
};
|
||||
export declare function expand(str: string, options?: BraceExpansionOptions): string[];
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
\ No newline at end of file
|
||||
--- a/dist/esm/index.d.ts.map
|
||||
+++ b/dist/esm/index.d.ts.map
|
||||
@@ -1 +1 @@
|
||||
-{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAwD7C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YAkBtE"}
|
||||
\ No newline at end of file
|
||||
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,aAAa,SAAU,CAAA;AAYpC,eAAO,MAAM,oBAAoB,UAAY,CAAA;AAU7C,eAAO,MAAM,mBAAmB,OAAQ,CAAA;AAUxC,eAAO,MAAM,sBAAsB,OAAQ,CAAA;AAyE3C,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,YA+BtE"}
|
||||
\ No newline at end of file
|
||||
--- a/dist/esm/index.js
|
||||
+++ b/dist/esm/index.js
|
||||
@@ -26,6 +26,24 @@
|
||||
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
||||
// characters) so legitimate input is unaffected.
|
||||
export const EXPANSION_MAX_LENGTH = 4_000_000;
|
||||
+// `expand_` recurses once per level of brace *nesting* - both when expanding a
|
||||
+// set's comma members and when re-wrapping a set whose body is a single part.
|
||||
+// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
|
||||
+// level per chained group), which left nesting depth unbounded: about 3,100
|
||||
+// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
|
||||
+// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
|
||||
+// will follow nesting. It sits far above any realistic pattern and well below
|
||||
+// the depth at which the stack runs out.
|
||||
+export const EXPANSION_MAX_DEPTH = 1_000;
|
||||
+// Bash keeps a quirk where a brace group followed by a comma set still expands
|
||||
+// (`{a},b}`). The parser implements it by rewriting the string and restarting
|
||||
+// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
|
||||
+// full passes over a string that itself grows by one `escClose` sentinel each
|
||||
+// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
|
||||
+// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
|
||||
+// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
|
||||
+// times the scan may restart. Real `{a},b}` input needs a handful.
|
||||
+export const EXPANSION_MAX_REWRITES = 1_000;
|
||||
function numeric(str) {
|
||||
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
||||
}
|
||||
@@ -45,37 +63,52 @@
|
||||
.replace(escCommaPattern, ',')
|
||||
.replace(escPeriodPattern, '.');
|
||||
}
|
||||
+// Like `target.push(...items)` but doesn't overflow the stack
|
||||
+function pushAll(target, items) {
|
||||
+ for (let i = 0; i < items.length; i++) {
|
||||
+ target.push(items[i]);
|
||||
+ }
|
||||
+}
|
||||
/**
|
||||
* Basically just str.split(","), but handling cases
|
||||
* where we have nested braced sections, which should be
|
||||
* treated as individual members, like {a,{b,c},d}
|
||||
*/
|
||||
function parseCommaParts(str) {
|
||||
- if (!str) {
|
||||
- return [''];
|
||||
- }
|
||||
const parts = [];
|
||||
- const m = balanced('{', '}', str);
|
||||
- if (!m) {
|
||||
- return str.split(',');
|
||||
- }
|
||||
- const { pre, body, post } = m;
|
||||
- const p = pre.split(',');
|
||||
- p[p.length - 1] += '{' + body + '}';
|
||||
- const postParts = parseCommaParts(post);
|
||||
- if (post.length) {
|
||||
- ;
|
||||
- p[p.length - 1] += postParts.shift();
|
||||
- p.push.apply(p, postParts);
|
||||
+ // Walk the brace groups iteratively. Recursing on `post` once per group let a
|
||||
+ // chain of them exhaust the stack - the parsing-side counterpart to
|
||||
+ // the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
|
||||
+ // `maxLength` can bound, since it happens before expansion.
|
||||
+ //
|
||||
+ // The part the next chunk continues
|
||||
+ let carry = '';
|
||||
+ for (;;) {
|
||||
+ const m = balanced('{', '}', str);
|
||||
+ if (!m) {
|
||||
+ const tail = str.split(',');
|
||||
+ tail[0] = carry + tail[0];
|
||||
+ pushAll(parts, tail);
|
||||
+ return parts;
|
||||
+ }
|
||||
+ const { pre, body, post } = m;
|
||||
+ const p = pre.split(',');
|
||||
+ p[0] = carry + p[0];
|
||||
+ p[p.length - 1] += '{' + body + '}';
|
||||
+ if (!post.length) {
|
||||
+ pushAll(parts, p);
|
||||
+ return parts;
|
||||
+ }
|
||||
+ carry = p.pop();
|
||||
+ pushAll(parts, p);
|
||||
+ str = post;
|
||||
}
|
||||
- parts.push.apply(parts, p);
|
||||
- return parts;
|
||||
}
|
||||
export function expand(str, options = {}) {
|
||||
if (!str) {
|
||||
return [];
|
||||
}
|
||||
- const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options;
|
||||
+ const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES, } = options;
|
||||
// I don't know why Bash 4.3 does this, but it does.
|
||||
// Anything starting with {} will have the first two bytes preserved
|
||||
// but *only* at the top level, so {},a}b will not expand to anything,
|
||||
@@ -85,7 +118,7 @@
|
||||
if (str.slice(0, 2) === '{}') {
|
||||
str = '\\{\\}' + str.slice(2);
|
||||
}
|
||||
- return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
||||
+ return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
|
||||
}
|
||||
function embrace(str) {
|
||||
return '{' + str + '}';
|
||||
@@ -180,7 +213,13 @@
|
||||
}
|
||||
return N;
|
||||
}
|
||||
-function expand_(str, max, maxLength, isTop) {
|
||||
+function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
|
||||
+ // Too deeply nested to keep following: treat the rest as literal, the same
|
||||
+ // way a group that cannot expand is already handled. Truncating rather than
|
||||
+ // throwing keeps `expand` total, matching `max` and `maxLength`.
|
||||
+ if (depth > maxDepth) {
|
||||
+ return [str];
|
||||
+ }
|
||||
// Consume the string's top-level brace groups left to right, threading a
|
||||
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
||||
// rather than recursing on `m.post` once per group - keeps the native stack
|
||||
@@ -192,6 +231,9 @@
|
||||
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
||||
// is on the final strings, so it is applied to whichever `combine` produces
|
||||
// them (the one with no brace set left in the tail).
|
||||
+ // How many times the `{a},b}` rewrite below has restarted the scan. Each pass
|
||||
+ // re-reads the whole string, so leaving this unbounded is quadratic.
|
||||
+ let rewrites = 0;
|
||||
let dropEmpties = false;
|
||||
let firstGroup = true;
|
||||
for (;;) {
|
||||
@@ -216,7 +258,8 @@
|
||||
const isOptions = m.body.indexOf(',') >= 0;
|
||||
if (!isSequence && !isOptions) {
|
||||
// {a},b}
|
||||
- if (m.post.match(/,(?!,).*\}/)) {
|
||||
+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
|
||||
+ rewrites++;
|
||||
str = m.pre + '{' + m.body + escClose + m.post;
|
||||
isTop = true;
|
||||
continue;
|
||||
@@ -236,7 +279,7 @@
|
||||
let n = parseCommaParts(m.body);
|
||||
if (n.length === 1 && n[0] !== undefined) {
|
||||
// x{{a,b}}y ==> x{a}y x{b}y
|
||||
- n = expand_(n[0], max, maxLength, false).map(embrace);
|
||||
+ n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
|
||||
//XXX is this necessary? Can't seem to hit it in tests.
|
||||
/* c8 ignore start */
|
||||
if (n.length === 1) {
|
||||
@@ -262,12 +305,13 @@
|
||||
values = [];
|
||||
let valuesLength = 0;
|
||||
outer: for (let j = 0; j < n.length; j++) {
|
||||
- const expanded = expand_(n[j], max, maxLength, false);
|
||||
+ const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
|
||||
for (let k = 0; k < expanded.length; k++) {
|
||||
const v = expanded[k];
|
||||
if (dropsEmpties && !v)
|
||||
continue;
|
||||
- if (values.length >= max || valuesLength + v.length > maxLength) {
|
||||
+ if (values.length >= max ||
|
||||
+ valuesLength + v.length > maxLength) {
|
||||
break outer;
|
||||
}
|
||||
values.push(v);
|
||||
--- a/dist/esm/index.js.map
|
||||
+++ b/dist/esm/index.js.map
|
||||
@@ -1 +1 @@
|
||||
-{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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
Line truncated
|
||||
\ No newline at end of file
|
||||
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AAC/C,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACjD,MAAM,SAAS,GAAG,UAAU,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAA;AACnD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,cAAc,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAC/C,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;AACjD,MAAM,gBAAgB,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;AACnD,MAAM,YAAY,GAAG,OAAO,CAAA;AAC5B,MAAM,WAAW,GAAG,MAAM,CAAA;AAC1B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,YAAY,GAAG,MAAM,CAAA;AAC3B,MAAM,aAAa,GAAG,OAAO,CAAA;AAE7B,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAA;AAEpC,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,6EAA6E;AAC7E,sEAAsE;AACtE,4EAA4E;AAC5E,0EAA0E;AAC1E,wEAAwE;AACxE,iDAAiD;AACjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAA;AAE7C,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,4EAA4E;AAC5E,gFAAgF;AAChF,4EAA4E;AAC5E,8EAA8E;AAC9E,yCAAyC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAA;AAExC,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,mEAAmE;AACnE,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAA;AAE3C,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,CAAC,KAAK,CAAC,GAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AACnE,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG;SACP,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC;SAC7B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC;SAC/B,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;AACtC,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,GAAG;SACP,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC;SAC9B,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAA;AACnC,CAAC;AAED,8DAA8D;AAC9D,SAAS,OAAO,CAAC,MAAgB,EAAE,KAAe;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC,CAAA;IACjC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,GAAW;IAClC,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,8EAA8E;IAC9E,oEAAoE;IACpE,8EAA8E;IAC9E,4DAA4D;IAC5D,EAAE;IACF,oCAAoC;IACpC,IAAI,KAAK,GAAG,EAAE,CAAA;IAEd,SAAS,CAAC;QACR,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;QAEjC,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,GAAI,IAAI,CAAC,CAAC,CAAY,CAAA;YACrC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;YACpB,OAAO,KAAK,CAAA;QACd,CAAC;QAED,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;QAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACxB,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,GAAI,CAAC,CAAC,CAAC,CAAY,CAAA;QAC/B,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,CAAA;QAEnC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;YACjB,OAAO,KAAK,CAAA;QACd,CAAC;QAED,KAAK,GAAG,CAAC,CAAC,GAAG,EAAY,CAAA;QACzB,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;QACjB,GAAG,GAAG,IAAI,CAAA;IACZ,CAAC;AACH,CAAC;AASD,MAAM,UAAU,MAAM,CAAC,GAAW,EAAE,UAAiC,EAAE;IACrE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,EACJ,GAAG,GAAG,aAAa,EACnB,SAAS,GAAG,oBAAoB,EAChC,QAAQ,GAAG,mBAAmB,EAC9B,WAAW,GAAG,sBAAsB,GACrC,GAAG,OAAO,CAAA;IAEX,oDAAoD;IACpD,oEAAoE;IACpE,sEAAsE;IACtE,6CAA6C;IAC7C,oEAAoE;IACpE,+DAA+D;IAC/D,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7B,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC/B,CAAC;IAED,OAAO,OAAO,CACZ,YAAY,CAAC,GAAG,CAAC,EACjB,GAAG,EACH,SAAS,EACT,QAAQ,EACR,CAAC,EACD,WAAW,EACX,IAAI,CACL,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;AACvB,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU;IAC1B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAC1B,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,0EAA0E;AAC1E,gFAAgF;AAChF,2EAA2E;AAC3E,gFAAgF;AAChF,iCAAiC;AACjC,SAAS,OAAO,CACd,GAAa,EACb,GAAW,EACX,MAAgB,EAChB,GAAW,EACX,SAAiB,EACjB,WAAoB;IAEpB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;gBAAE,OAAO,GAAG,CAAA;YACjC,MAAM,SAAS,GAAI,GAAG,CAAC,CAAC,CAAY,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;YACtD,yEAAyE;YACzE,+DAA+D;YAC/D,IAAI,WAAW,IAAI,CAAC,SAAS;gBAAE,SAAQ;YACvC,IAAI,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS;gBAAE,OAAO,GAAG,CAAA;YACrD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YACnB,MAAM,IAAI,SAAS,CAAC,MAAM,CAAA;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,SA
Line truncated
|
||||
\ No newline at end of file
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "brace-expansion",
|
||||
"description": "Brace expansion as known from sh/bash",
|
||||
- "version": "5.0.9",
|
||||
+ "version": "5.0.12",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
@@ -29,6 +29,7 @@
|
||||
"test": "tap",
|
||||
"snap": "tap",
|
||||
"format": "prettier --write .",
|
||||
+ "format:check": "prettier --check .",
|
||||
"benchmark": "node benchmark/index.js",
|
||||
"typedoc": "typedoc --tsconfig .tshy/esm.json ./src/*.ts"
|
||||
},
|
||||
Generated
+5
-4
@@ -43,7 +43,7 @@ patchedDependencies:
|
||||
hash: 6st5fzvttegjv64llnfegwwpca
|
||||
path: patches/acpx@0.12.0.patch
|
||||
acpx@0.13.1:
|
||||
hash: j4huaoi2baejrlq7vdcgzlx54m
|
||||
hash: btis2ixbxfyq422xwpnza3ncle
|
||||
path: patches/acpx@0.13.1.patch
|
||||
embedded-postgres@18.1.0-beta.16:
|
||||
hash: 55uhvnotpqyiy37rn3pqpukhei
|
||||
@@ -508,7 +508,7 @@ importers:
|
||||
version: 0.160.0
|
||||
acpx:
|
||||
specifier: 0.13.1
|
||||
version: 0.13.1(patch_hash=j4huaoi2baejrlq7vdcgzlx54m)
|
||||
version: 0.13.1(patch_hash=btis2ixbxfyq422xwpnza3ncle)
|
||||
ajv:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
@@ -979,7 +979,7 @@ importers:
|
||||
version: 8.18.0
|
||||
acpx:
|
||||
specifier: 0.13.1
|
||||
version: 0.13.1(patch_hash=j4huaoi2baejrlq7vdcgzlx54m)
|
||||
version: 0.13.1(patch_hash=btis2ixbxfyq422xwpnza3ncle)
|
||||
ai:
|
||||
specifier: 7.0.130
|
||||
version: 7.0.130(zod@4.4.3)
|
||||
@@ -7757,6 +7757,7 @@ packages:
|
||||
|
||||
opencode-ai@1.18.34:
|
||||
resolution: {integrity: sha512-9WUS2T0t4HHDVzXvuwTHF0nvhXvZ9mQ0r+ozCvKdJu0LVoQpOzqAW4qWTC3ygNc5Oedcc7j+Oct24JYlQYnySA==}
|
||||
cpu: [arm64, x64]
|
||||
os: [darwin, linux, win32]
|
||||
hasBin: true
|
||||
|
||||
@@ -13499,7 +13500,7 @@ snapshots:
|
||||
- bare-buffer
|
||||
- react-native-b4a
|
||||
|
||||
acpx@0.13.1(patch_hash=j4huaoi2baejrlq7vdcgzlx54m):
|
||||
acpx@0.13.1(patch_hash=btis2ixbxfyq422xwpnza3ncle):
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 1.4.0(zod@4.4.3)
|
||||
commander: 15.0.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { probeAcpxClaudeInstallation, probeAcpxCursorInstallation } from "@paperclipai/paperclip-runner/live";
|
||||
import { probeAcpxClaudeInstallation, probeAcpxPiInstallation } from "../vendor/paperclip-runner/live/index.js";
|
||||
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
||||
import { buildSandboxNpmInstallCommand } from "@paperclipai/adapter-utils";
|
||||
import type { ServerAdapterModule } from "../adapters/index.js";
|
||||
@@ -17,9 +17,10 @@ import {
|
||||
setOverridePaused,
|
||||
} from "../adapters/registry.js";
|
||||
|
||||
vi.mock("@paperclipai/paperclip-runner/live", () => ({
|
||||
vi.mock("../vendor/paperclip-runner/live/index.js", () => ({
|
||||
probeAcpxClaudeInstallation: vi.fn(async () => undefined),
|
||||
probeAcpxGrokInstallation: vi.fn(async () => undefined),
|
||||
probeAcpxPiInstallation: vi.fn(async () => undefined),
|
||||
probeAcpxCursorInstallation: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
@@ -327,23 +328,10 @@ describe("server adapter registry", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([true, false])("checks ordinary Cursor runtime readiness (%s)", async (ready) => {
|
||||
const probe = vi.mocked(probeAcpxCursorInstallation);
|
||||
it.each([true, false])("probes the exact Pi installation without qualification opt-in (ready=%s)", async (ready) => {
|
||||
const probe = vi.mocked(probeAcpxPiInstallation);
|
||||
if (ready) probe.mockResolvedValueOnce(undefined);
|
||||
else probe.mockRejectedValueOnce(new Error("Run paperclipai runtime setup cursor"));
|
||||
const model = "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]";
|
||||
const result = await requireServerAdapter("paperclip_runner").testEnvironment({
|
||||
companyId: "company-1", adapterType: "paperclip_runner",
|
||||
config: { provider: "acpx", acpxAgent: "cursor", model },
|
||||
});
|
||||
expect(probe).toHaveBeenLastCalledWith(model);
|
||||
expect(result).toMatchObject({
|
||||
status: ready ? "pass" : "fail",
|
||||
checks: [expect.objectContaining({ code: ready ? "acpx_runtime_ready" : "acpx_runtime_unavailable" })],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the ACPX Pi profile unavailable", async () => {
|
||||
else probe.mockRejectedValueOnce(new Error("Pi closure unavailable"));
|
||||
const result = await requireServerAdapter("paperclip_runner").testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "paperclip_runner",
|
||||
@@ -355,9 +343,10 @@ describe("server adapter registry", () => {
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "fail",
|
||||
checks: [{ code: "paperclip_runner_acpx_agent_unavailable" }],
|
||||
status: ready ? "pass" : "fail",
|
||||
checks: [{ code: ready ? "acpx_runtime_ready" : "acpx_runtime_unavailable" }],
|
||||
});
|
||||
expect(probe).toHaveBeenLastCalledWith("openrouter/deepseek/deepseek-v4-flash-0731");
|
||||
});
|
||||
it("reports qualification-only readiness for an exact host-authorized candidate", async () => {
|
||||
const key = "PAPERCLIP_RUNNER_ACPX_QUALIFICATION";
|
||||
|
||||
@@ -4,13 +4,15 @@ import { listServerAdapters, requireServerAdapter } from "./registry.js";
|
||||
import * as executionTarget from "@paperclipai/adapter-utils/execution-target";
|
||||
import { BUILTIN_ADAPTER_TYPES } from "./builtin-adapter-types.js";
|
||||
|
||||
const { probeInstallation, probeGrokInstallation } = vi.hoisted(() => ({
|
||||
const { probeInstallation, probeGrokInstallation, probePiInstallation } = vi.hoisted(() => ({
|
||||
probeInstallation: vi.fn(),
|
||||
probeGrokInstallation: vi.fn(),
|
||||
probePiInstallation: vi.fn(),
|
||||
}));
|
||||
vi.mock("@paperclipai/paperclip-runner/live", () => ({
|
||||
vi.mock("../vendor/paperclip-runner/live/index.js", () => ({
|
||||
probeAcpxClaudeInstallation: probeInstallation,
|
||||
probeAcpxGrokInstallation: probeGrokInstallation,
|
||||
probeAcpxPiInstallation: probePiInstallation,
|
||||
probeAcpxCursorInstallation: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
@@ -96,6 +98,7 @@ describe("native ACPX environment checks", () => {
|
||||
beforeEach(() => {
|
||||
probeInstallation.mockReset().mockResolvedValue(undefined);
|
||||
probeGrokInstallation.mockReset().mockResolvedValue(undefined);
|
||||
probePiInstallation.mockReset().mockResolvedValue(undefined);
|
||||
});
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
@@ -132,6 +135,19 @@ describe("native ACPX environment checks", () => {
|
||||
expect(probeInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([true, false])("checks qualified Pi's own installation readiness (%s)", async (ready) => {
|
||||
if (!ready) probePiInstallation.mockRejectedValueOnce(new Error("Pi installation integrity mismatch"));
|
||||
const model = "openrouter/deepseek/deepseek-v4-flash-0731";
|
||||
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({
|
||||
...context,
|
||||
config: { provider: "acpx", acpxAgent: "pi", model },
|
||||
});
|
||||
expect(result.status).toBe(ready ? "pass" : "fail");
|
||||
expect(probePiInstallation).toHaveBeenCalledWith(model);
|
||||
expect(probeInstallation).not.toHaveBeenCalled();
|
||||
expect(probeGrokInstallation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not use the host platform to reject a remote environment", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
assertAgentCoreProfileRecoveryBinding,
|
||||
@@ -456,12 +456,12 @@ describe("Paperclip Runner native provider configuration", () => {
|
||||
).toThrow("provider changed after this run selected its native backend");
|
||||
});
|
||||
|
||||
it("rejects an unqualified Pi model before a native descriptor is persisted", () => {
|
||||
expect(() =>
|
||||
it("preserves a caller-selected Pi model in the native descriptor", () => {
|
||||
expect(
|
||||
resolvePaperclipRunnerNativeProviderInput({
|
||||
backend: "acpx_runtime",
|
||||
adapterConfig: { provider: "acpx", acpxAgent: "pi", model: "pi-model" },
|
||||
}),
|
||||
).toThrow("requires exact model openrouter/deepseek/deepseek-v4-flash-0731");
|
||||
).toMatchObject({ provider: "acpx", acpxAgent: "pi", model: "pi-model" });
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ const provider = { kind: "acpx", agent: "copilot", model: "exact-model", permiss
|
||||
const authorize = (value: unknown) => ({ [ACPX_QUALIFICATION_ENV]: JSON.stringify(value) });
|
||||
describe("host ACPX qualification admission", () => {
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
it.each(["copilot", "pi"])("admits %s through agent validation and native input only for the exact host pair", (agent) => {
|
||||
it.each(["copilot"])("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" }));
|
||||
@@ -19,6 +19,21 @@ describe("host ACPX qualification admission", () => {
|
||||
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("admits only the exact Pi model without host authorization and preserves permission modes", () => {
|
||||
const config = { provider: "acpx", acpxAgent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731" };
|
||||
for (const authorization of [undefined, "malformed unrelated candidate authorization"]) {
|
||||
vi.stubEnv(ACPX_QUALIFICATION_ENV, authorization);
|
||||
expect(resolvePaperclipRunnerProviderProfile(config)).toMatchObject(config);
|
||||
for (const acpxPermissionMode of [undefined, "approve-all", "approve-paperclip", "approve-reads", "deny-all"]) {
|
||||
expect(resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig: { ...config, acpxPermissionMode } }))
|
||||
.toMatchObject({ ...config, acpxPermissionMode: acpxPermissionMode ?? "approve-all" });
|
||||
}
|
||||
for (const model of [undefined, ""]) {
|
||||
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: "copilot", model: "exact-model" }]))).toBe("copilot");
|
||||
@@ -43,7 +58,7 @@ 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", "codex", "grok", "cursor"])("keeps %s model selection open to native verification", (acpxAgent) => {
|
||||
it.each(["claude", "codex", "grok", "cursor", "pi"])("keeps %s model selection open to native verification", (acpxAgent) => {
|
||||
const adapterConfig = { provider: "acpx", acpxAgent, model: "explicit-new-model" };
|
||||
expect(resolvePaperclipRunnerProviderProfile(adapterConfig))
|
||||
.toMatchObject({ acpxAgent, model: "explicit-new-model" });
|
||||
@@ -68,3 +83,9 @@ describe("host ACPX qualification admission", () => {
|
||||
expect(source).not.toContain("resolveAcpxQualification(input.execution.provider, effectiveRunnerEnvironment)");
|
||||
});
|
||||
});
|
||||
|
||||
it.each([undefined, "off", "low", "high", "max"] as const)("validates saved Pi thinking %s and projects it explicitly", piThinkingLevel => {
|
||||
const adapterConfig = { provider: "acpx", acpxAgent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", piThinkingLevel };
|
||||
expect(resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig })).toMatchObject({ piThinkingLevel: piThinkingLevel ?? "low" });
|
||||
for (const alias of ["medium", "minimal", "xhigh"]) expect(() => resolvePaperclipRunnerProviderProfile({ ...adapterConfig, piThinkingLevel: alias })).toThrow(expect.objectContaining({ code: "paperclip_runner_pi_thinking_invalid" }));
|
||||
});
|
||||
@@ -116,16 +116,14 @@ describe("resolveNativeRuntimeMode", () => {
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: "paperclip_runner_opencode_model_invalid",
|
||||
}));
|
||||
expect(() => resolveNativeRuntimeMode({
|
||||
...eligible,
|
||||
adapterConfig: {
|
||||
provider: "acpx",
|
||||
acpxAgent: "pi",
|
||||
model: "openrouter/deepseek/deepseek-v4-flash-0731",
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: "paperclip_runner_acpx_agent_unavailable",
|
||||
}));
|
||||
for (const acpxAgent of ["copilot"]) {
|
||||
expect(() => resolveNativeRuntimeMode({
|
||||
...eligible,
|
||||
adapterConfig: { provider: "acpx", acpxAgent, model: "explicit-provider-model" },
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: "paperclip_runner_acpx_agent_unavailable",
|
||||
}));
|
||||
}
|
||||
expect(() => resolveNativeRuntimeMode({
|
||||
...eligible,
|
||||
adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5" },
|
||||
|
||||
@@ -27,7 +27,9 @@ async function renderMarkup(node: ReactNode, expand?: string, inspect?: (contain
|
||||
return html;
|
||||
}
|
||||
|
||||
async function renderRunner(config: Record<string, unknown>, expand?: string, inspect?: (container: HTMLElement) => void): Promise<string> {
|
||||
async function renderRunner(config: Record<string, unknown>, expand?: string, inspectOrDot?: ((container: HTMLElement) => void) | boolean): Promise<string> {
|
||||
const openAiDotEnabled = inspectOrDot === true;
|
||||
const inspect = typeof inspectOrDot === "function" ? inspectOrDot : undefined;
|
||||
return renderMarkup(
|
||||
<TooltipProvider>
|
||||
<CodexLocalConfigFields
|
||||
|
||||
@@ -1,32 +1,93 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { paperclipRunnerUIAdapter } from "./index";
|
||||
import { createPiProfileExtensionAdapter, PI_NOTICE_METHOD } from "../../../../packages/paperclip-runner/src/drivers/acpx/pi-extension-adapter";
|
||||
import { transcriptToTaskChatItems } from "../../components/task-chat/transcript-adapter";
|
||||
|
||||
const piFailure = (overrides: Record<string, unknown> = {}, eventOverrides: Record<string, unknown> = {}) => ({
|
||||
type: "paperclip.prp.event",
|
||||
event: {
|
||||
runId: "run-1", turnId: "turn-1", normalizedSessionId: "session-1",
|
||||
eventType: "provider.notice.recorded",
|
||||
payload: {
|
||||
schema: "paperclip.provider.notice.v1", noticeId: "notice-1", category: "pi.runtime_failure",
|
||||
severity: "error", summary: "Pi process exited with code 4", scope: "session",
|
||||
recoverable: false, userActionable: false,
|
||||
details: [{ name: "source.method", value: PI_NOTICE_METHOD },
|
||||
{ name: "source.nativeEvent", value: "runtime_failure" }, { name: "source.sessionId", value: "session-1" },
|
||||
{ name: "reason", value: "native_process_exited" }],
|
||||
...overrides,
|
||||
},
|
||||
...eventOverrides,
|
||||
},
|
||||
});
|
||||
|
||||
describe("paperclip runner transcript projection", () => {
|
||||
it("omits unrelated provider diagnostics and legacy notices from streaming chat", () => {
|
||||
it("keeps both pinned Pi failure facts while rendering one consecutive diagnostic", async () => {
|
||||
const adapter = createPiProfileExtensionAdapter({ workspacePath: "/fixture", sessionId: "session-1", turnId: "turn-1" });
|
||||
const notice = { sessionId: "session-1", category: "runtime_failure", severity: "error",
|
||||
summary: "Pi process exited with code 4", details: { reason: "native_process_exited" } };
|
||||
const raw = [...await adapter.notification(PI_NOTICE_METHOD, notice),
|
||||
...await adapter.notification(PI_NOTICE_METHOD, structuredClone(notice))];
|
||||
expect(raw).toHaveLength(2);
|
||||
expect(raw[0]!.payload.noticeId).not.toBe(raw[1]!.payload.noticeId);
|
||||
const before = structuredClone(raw);
|
||||
const parse = paperclipRunnerUIAdapter.createStdoutParser!().parseLine;
|
||||
const event = (eventType: string, payload: Record<string, unknown>) => parse(JSON.stringify({
|
||||
type: "paperclip.prp.event",
|
||||
event: { eventType, payload },
|
||||
}), "2026-10-02T12:00:00.000Z");
|
||||
const legacyNotice = {
|
||||
schema: "paperclip.provider.notice.v1",
|
||||
severity: "warning",
|
||||
category: "warning",
|
||||
summary: "ignored unrelated provider information",
|
||||
userActionable: true,
|
||||
};
|
||||
expect(event("harness.diagnostic", { code: "codex_unrelated_information" })).toEqual([]);
|
||||
expect(event("provider.notice.recorded", legacyNotice)).toEqual([]);
|
||||
expect(event("provider.notice.recorded", { ...legacyNotice, summary: "Repository is not trusted" }))
|
||||
.toEqual([expect.objectContaining({ family: "provider_notice" })]);
|
||||
expect(event("provider.notice.recorded", { ...legacyNotice, severity: "error" }))
|
||||
.toEqual([expect.objectContaining({ family: "provider_notice" })]);
|
||||
expect(event("provider.notice.recorded", { ...legacyNotice, category: "configWarning" }))
|
||||
.toEqual([expect.objectContaining({ family: "provider_notice" })]);
|
||||
expect(event("harness.diagnostic", { code: "provider_identity_failure", message: "Thread mismatch" }))
|
||||
.toEqual([expect.objectContaining({ kind: "system", text: "Runner: Thread mismatch" })]);
|
||||
expect(event("item.completed", { kind: "agentMessage", channel: "final", text: "Here is the answer." }))
|
||||
.toEqual([expect.objectContaining({ kind: "assistant", text: "Here is the answer." })]);
|
||||
const entries = raw.flatMap(event => parse(JSON.stringify({ type: "paperclip.prp.event", event: {
|
||||
runId: "run-1", normalizedSessionId: "session-1", turnId: "turn-1", ...event,
|
||||
} }), "2026-10-03T07:00:00.000Z"));
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(transcriptToTaskChatItems(entries, { runId: "run-1", agentName: "Pi", running: false }))
|
||||
.toEqual([expect.objectContaining({ kind: "protocol", family: "provider_notice",
|
||||
details: expect.arrayContaining([{ label: "Severity", value: "error", mono: false }]) })]);
|
||||
expect(raw).toEqual(before);
|
||||
});
|
||||
|
||||
it("preserves different failure details, severity, authority and run/turn/session scope", () => {
|
||||
const changed = [
|
||||
piFailure({ summary: "Provider request failed" }),
|
||||
piFailure({ severity: "warning" }),
|
||||
piFailure({ recoverable: true }),
|
||||
piFailure({ userActionable: true }),
|
||||
piFailure({ details: piFailure().event.payload.details.map(detail => detail.name === "reason"
|
||||
? { ...detail, value: "provider_http_429" } : detail) }),
|
||||
piFailure({ details: piFailure().event.payload.details.map(detail => detail.name === "source.sessionId"
|
||||
? { ...detail, value: "session-2" } : detail) }),
|
||||
piFailure({}, { runId: "run-2" }),
|
||||
piFailure({}, { turnId: "turn-2" }),
|
||||
piFailure({}, { normalizedSessionId: "session-2" }),
|
||||
piFailure({}, { runId: undefined }),
|
||||
piFailure({}, { turnId: undefined }),
|
||||
];
|
||||
for (const event of changed) {
|
||||
const parse = paperclipRunnerUIAdapter.createStdoutParser!().parseLine;
|
||||
expect(parse(JSON.stringify(piFailure()), "first")).toHaveLength(1);
|
||||
expect(parse(JSON.stringify(event), "second")).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("starts a new diagnostic after retry activity, another event, or parser reset", () => {
|
||||
const parser = paperclipRunnerUIAdapter.createStdoutParser!();
|
||||
const parse = (event: unknown) => parser.parseLine(JSON.stringify(event), "2026-10-03T07:00:00.000Z");
|
||||
expect(parse(piFailure())).toHaveLength(1);
|
||||
expect(parse(piFailure({ noticeId: "notice-2" }))).toEqual([]);
|
||||
for (const intervening of [piFailure({ category: "pi.auto_retry_start" }),
|
||||
piFailure({}, { eventType: "item.delta", payload: { kind: "agentMessage", text: "Retrying" } }),
|
||||
{ type: "other" }]) {
|
||||
parse(intervening);
|
||||
expect(parse(piFailure())).toHaveLength(1);
|
||||
}
|
||||
parser.reset();
|
||||
expect(parse(piFailure())).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("never coalesces other providers or unscoped/unsupported notices", () => {
|
||||
for (const event of [piFailure({ category: "other.runtime_failure" }),
|
||||
piFailure({ details: [] }), piFailure({}, { normalizedSessionId: undefined }),
|
||||
piFailure({}, { runId: undefined }), piFailure({}, { turnId: undefined })]) {
|
||||
const parse = paperclipRunnerUIAdapter.createStdoutParser!().parseLine;
|
||||
expect(parse(JSON.stringify(event), "first")).toHaveLength(1);
|
||||
expect(parse(JSON.stringify(event), "second")).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("renders committed PRP semantic tool items with the existing chat parts", () => {
|
||||
|
||||
@@ -289,7 +289,7 @@ export function TaskChatProtocolActivityRow({ item }: { item: TaskChatProtocolIt
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-1.5 py-1 text-xs" data-testid="task-chat-protocol-activity-row" data-activity-family="provider_notice">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<NoticeIcon className="h-3.5 w-3.5 shrink-0" aria-hidden data-testid="task-chat-protocol-activity-icon" />
|
||||
<NoticeIcon className="size-(--sz-protocol-notice-icon) shrink-0" aria-hidden data-testid="task-chat-protocol-activity-icon" />
|
||||
<span className="font-medium">{severity === "error" ? "Error" : severity === "info" ? "Provider update" : "Warning"}</span>
|
||||
</div>
|
||||
<p className="min-w-0 whitespace-pre-wrap break-words text-foreground">{summary}</p>
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
`in_queue` is the BLOCKED shape recoloured to the in_progress blue
|
||||
(replaces the bespoke teal "covered" state). Light values here; the
|
||||
`.dark` block below overrides the hues that need a mode-specific value. */
|
||||
--sz-protocol-notice-icon: calc(var(--spacing) * 3.5);
|
||||
--status-task-icon-backlog: #52585d; /* gray darkened — 7.21:1 on paper */
|
||||
--status-task-icon-todo: #cc7a00; /* amber darkened — clears 3:1 */
|
||||
--status-task-icon-in_progress: var(--color-blue-600); /* matches nav dots and live labels */
|
||||
|
||||
Reference in new issue
Block a user