Bind Pi launches to verified immutable provider assets

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 15:14:57 -05:00
1 parent 9aa68a41c2
commit 742f9bef15
7 files changed
+307 -43

No files matched your search

@@ -190,3 +190,27 @@ archive hashes were checked against the [official Node release checksums](https:
The x64 closure pins combine the identical locked package/resource graph with each
verified official x64 interpreter. They are candidate artifact identity, not proof
that those targets have executed successfully.
### Installation authority and token semantics
`verifyPiInstallation(profile)` admits only profile version 2 and the source-owned
Pi identity. It resolves `provider-assets/pi/<platform>-<arch>` inside the verified
Runner package, checks the complete runtime against source-pinned closure hashes,
and opens a guarded immutable native snapshot. The snapshot bootstrap binds Node,
Pi, and the extension relative to itself, overriding inherited launch paths.
Unconfigured installations return an explicit bound-credential error and expose
no terminal login option. The materializer returns the runtime `version`,
`profileDigest`, and complete `closureDigest` for the provider-pack manifest.
For the exact OpenRouter qualification model, Pi AI 0.84.2's
`dist/providers/data/openrouter.json` selects `openai-completions`. Its
`dist/api/openai-completions.js` assigns `usage.output` from `completion_tokens`,
which already includes reasoning tokens; `usage.reasoning` is a subset. PRP must
therefore use zero additional thought tokens for this pinned profile, rather than
counting that subset twice. Missing cache categories remain unknown at the wrapper
receipt boundary. `usage.cost` is a Pi catalog pricing estimate, never an invoice.
The closed snapshot has been exercised against actual pinned Pi through ACP
initialization and the missing-credential admission path without a model call.
The exact reported `currentModelId` and billable receipt still need credentialed
local and Linux/Daytona qualification.
@@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { PI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/pi-closure-pins.ts";
import { PI_NODE_DISTRIBUTIONS, PI_NODE_VERSION } from "../src/drivers/acpx/pi-node-pins.ts";
import { QUALIFIED_ACPX_PROFILES } from "../src/drivers/acpx/qualified-profiles.ts";
import { inventoryPiRuntimeFiles, verifyPiRuntimeManifest } from "../src/drivers/acpx/pi-verified-runtime.ts";
const run = promisify(execFile);
@@ -17,7 +18,7 @@ const patchPath = join(workspaceRoot, "patches/pi-acp@0.0.33.patch");
const supportedTargets = new Set(["darwin-arm64", "darwin-x64", "linux-x64"]);
export const PI_DISTRIBUTION_PINS = Object.freeze({
wrapper: "0.0.33", runtime: "0.84.2", sdk: "0.26.0", zod: "3.25.76", nodeVersion: PI_NODE_VERSION,
wrapperSha256: "d2a2cf67bebedb7cff46b2cebdc98dda8e91f3e64b15f26c660715e5145c7cc6",
wrapperSha256: "18375d30e7eca9fddf6c024f49899d6da38c064d8587aa1678fd4c2d70308fbc",
helperSha256: "57dd03e3d1e59d0191f0dd66f5a809e6c825de3c4b6660bff697686c160a659f",
});
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -93,6 +94,20 @@ export async function writePiDistributionManifest(runtimeRoot) {
return { manifest, ...verified };
}
export function piDistributionBootstrapSource() {
return [
'const path=require("node:path");',
'process.env.PAPERCLIP_PI_NODE_EXECUTABLE=path.join(__dirname,"node/bin/node");',
'process.env.PAPERCLIP_PI_ENTRYPOINT=path.join(__dirname,"node_modules/@earendil-works/pi-coding-agent/dist/cli.js");',
'process.env.PAPERCLIP_PI_EXTENSION_PATH=path.join(__dirname,"extensions/paperclip.js");',
'process.env.PAPERCLIP_PI_MODULE_GUARD_PATH=path.join(__dirname,".paperclip-native-module-guard.cjs");',
'const protectedRoots=JSON.parse(process.env.PAPERCLIP_PI_PROTECTED_ROOTS??"[]");if(!Array.isArray(protectedRoots))throw new Error("Invalid Pi protected roots");',
'process.env.PAPERCLIP_PI_PROTECTED_ROOTS=JSON.stringify([...protectedRoots,__dirname]);',
'import(require("node:url").pathToFileURL(path.join(__dirname,"node_modules/pi-acp/dist/index.js")).href).catch(()=>{process.exitCode=1;});',
'',
].join("\n");
}
/** Build on the target platform. No lifecycle scripts, model requests, or auth. */
export async function materializePiDistribution({ outputRoot, nodeExecutable, npmExecutable = "npm" }) {
if (typeof outputRoot !== "string" || !outputRoot || !isAbsolute(outputRoot)) throw new Error("Pi distribution output must be absolute");
@@ -170,15 +185,7 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
}
}
await rm(join(runtimeRoot, "node_modules/.package-lock.json"), { force: true });
await writeFile(join(runtimeRoot, "pi-entry.cjs"), [
'const path=require("node:path");',
'process.env.PAPERCLIP_PI_NODE_EXECUTABLE=path.join(__dirname,"node/bin/node");',
'process.env.PAPERCLIP_PI_ENTRYPOINT=path.join(__dirname,"node_modules/@earendil-works/pi-coding-agent/dist/cli.js");',
'process.env.PAPERCLIP_PI_EXTENSION_PATH=path.join(__dirname,"extensions/paperclip.js");',
'process.env.PAPERCLIP_PI_MODULE_GUARD_PATH=path.join(__dirname,".paperclip-native-module-guard.cjs");',
'import(require("node:url").pathToFileURL(path.join(__dirname,"node_modules/pi-acp/dist/index.js")).href).catch(()=>{process.exitCode=1;});',
'',
].join("\n"));
await writeFile(join(runtimeRoot, "pi-entry.cjs"), piDistributionBootstrapSource());
for (const item of await readdir(staging)) if (item !== "runtime") await rm(join(staging, item), { recursive: true, force: true });
const { manifest, manifestDigest } = await writePiDistributionManifest(runtimeRoot);
const entries = await Promise.all(manifest.files.map(async (file) => {
@@ -200,7 +207,12 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
await rename(staging, output);
const finalRoot = join(output, "runtime");
const binding = await verifyPiRuntimeManifest(finalRoot, manifest);
return { outputRoot: output, runtimeRoot: finalRoot, manifestPath: join(output, "pi-distribution.json"), metadata, ...binding };
return {
version: PI_DISTRIBUTION_PINS.runtime,
profileDigest: QUALIFIED_ACPX_PROFILES.pi.commandDigest,
closureDigest: `sha256:${nativeClosureSha256}`,
outputRoot: output, runtimeRoot: finalRoot, manifestPath: join(output, "pi-distribution.json"), metadata, ...binding,
};
} catch (error) { await rm(staging, { recursive: true, force: true }); throw error; }
}
@@ -6,7 +6,7 @@
* three pins. Never accept a digest supplied only by an installed manifest.
*/
export const PI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({
"darwin-arm64": "277568e3adb63faefafb1aedd8e8d33441608e94d9b764fbfd980e390ff07c8c",
"darwin-x64": "f8f271ce09284ef0bf52a99adaa280798f2135b8f8a6ee628394324b923b5d7f",
"linux-x64": "b0d71abf0cfe464c147fcc5bfd875e349f94773d84e46a1bdcbadfc0184f8690",
"darwin-arm64": "f2c95f532e4b56a57e938ceb62b5ea1a86bc721cba29b41de6270afe3fe0041f",
"darwin-x64": "93f3c06c91fc9e06084dff083f3b43ac100dee3ac2c5f5981e3d281f1e561a20",
"linux-x64": "99707d2c78656bebdf5df4b85e4674a72d5d30a3a0c730536246251a2889d0a8",
});
@@ -0,0 +1,91 @@
import { cp, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { once } from "node:events";
import { StringDecoder } from "node:string_decoder";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { assertPiInstallationProfile, verifyPiInstallation } from "./pi-installation.js";
import { QUALIFIED_ACPX_PROFILES, type QualifiedAcpxProfile } from "./qualified-profiles.js";
const roots: string[] = [];
afterEach(async () => { vi.unstubAllEnvs(); await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
const candidate = (): QualifiedAcpxProfile => ({ ...QUALIFIED_ACPX_PROFILES.pi, agentProfileVersion: 2 });
async function fixture() {
const root = await mkdtemp(join(tmpdir(), "pi-installation-test-")); roots.push(root);
await writeFile(join(root, "package.json"), JSON.stringify({ name: "@paperclipai/paperclip-runner" }));
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", root);
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", join(root, "package.json"));
const assets = join(root, "provider-assets/pi", `${process.platform}-${process.arch}`);
await mkdir(join(assets, "runtime"), { recursive: true });
return { root, assets };
}
describe("Pi installation factory", () => {
it("rejects legacy profiles and caller-selected identities", () => {
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 2");
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { reportedModelId: "different" }, { agentRuntimePackage: "ambient-pi" }]) {
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
}
});
it("never takes closure authority from installed metadata", async () => {
const { assets } = await fixture();
await writeFile(join(assets, "pi-distribution.json"), JSON.stringify({
schema: "paperclip.pi-distribution.v1", runtimeRoot: "runtime", nativeClosureSha256: "0".repeat(64),
target: { platform: process.platform, architecture: process.arch, nodeVersion: "24.19.0" },
pins: { nodeVersion: "24.19.0", wrapper: "0.0.33", runtime: "0.84.2", sdk: "0.26.0", zod: "3.25.76" },
}));
await expect(verifyPiInstallation(candidate())).rejects.toThrow("trusted target pin");
});
it.runIf(process.env.PAPERCLIP_TEST_PI_DISTRIBUTION_ROOT)("launches the actual closed Pi snapshot with bound subprocess paths", async () => {
const { root, assets } = await fixture();
await cp(process.env.PAPERCLIP_TEST_PI_DISTRIBUTION_ROOT!, assets, { recursive: true });
const workspace = join(root, "workspace"); const agentHome = join(root, "agent");
await mkdir(workspace); await mkdir(agentHome);
const installation = await verifyPiInstallation(candidate());
const command = await installation.openCommand();
const child = command.spawn([], { cwd: workspace, env: {
PATH: "/usr/bin:/bin", HOME: agentHome, PI_CODING_AGENT_DIR: agentHome,
PAPERCLIP_ACPX_ISOLATED_CONTEXT: "1", PAPERCLIP_PI_READ_ONLY: "1",
PAPERCLIP_PI_READ_ROOTS: "[]", PAPERCLIP_PI_PROTECTED_ROOTS: JSON.stringify([agentHome]),
PAPERCLIP_PI_ENTRYPOINT: "/unverified/ignored.js", PI_TELEMETRY: "0",
} });
let stderr = ""; child.stderr!.on("data", chunk => { stderr += String(chunk); });
const pending = new Map<number, { resolve(value: any): void; reject(error: Error): void }>();
const decoder = new StringDecoder("utf8"); let buffer = ""; let id = 0;
child.stdout!.on("data", chunk => {
buffer += decoder.write(chunk);
for (;;) {
const at = buffer.indexOf("\n"); if (at < 0) break;
const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); if (!line.trim()) continue;
const value = JSON.parse(line); const waiter = pending.get(value.id);
if (waiter) { pending.delete(value.id); if (value.error) waiter.reject(new Error(JSON.stringify(value.error))); else waiter.resolve(value.result); }
}
});
child.once("exit", () => { for (const waiter of pending.values()) waiter.reject(new Error(`Pi exited: ${stderr}`)); pending.clear(); });
const call = (method: string, params: unknown) => new Promise<any>((resolve, reject) => {
const requestId = ++id;
const timer = setTimeout(() => reject(new Error(`Pi snapshot admission timed out: ${stderr}`)), 15_000);
pending.set(requestId, { resolve: value => { clearTimeout(timer); resolve(value); }, reject: error => { clearTimeout(timer); reject(error); } });
child.stdin!.write(`${JSON.stringify({ jsonrpc: "2.0", id: requestId, method, params })}\n`);
});
try {
const initialized = await call("initialize", { protocolVersion: 1, clientCapabilities: { elicitation: { form: {} } } });
expect(initialized.agentCapabilities._meta.paperclipPi.steering).toBe(true);
expect(initialized.authMethods).toEqual([]);
await expect(call("session/new", { cwd: workspace, mcpServers: [] })).rejects.toThrow("Bind the configured OpenRouter API credential");
} finally {
child.stdin!.end();
if (child.exitCode === null) { const timer = setTimeout(() => child.kill("SIGKILL"), 3000); await once(child, "close"); clearTimeout(timer); }
await command.close();
}
}, 90_000);
it("refuses symlinked target assets and metadata", async () => {
const { root, assets } = await fixture();
await rm(assets, { recursive: true });
await symlink(root, assets);
await expect(verifyPiInstallation(candidate())).rejects.toThrow("fixed asset directory");
});
});
@@ -0,0 +1,94 @@
import { constants } from "node:fs";
import { lstat, open, realpath } from "node:fs/promises";
import { join } from "node:path";
import { verifyNativeAcpxInstallation, type VerifiedAcpxInstallation } from "./installation-integrity.js";
import { PI_DISTRIBUTION_CLOSURE_SHA256 } from "./pi-closure-pins.js";
import { PI_NODE_VERSION } from "./pi-node-pins.js";
import { verifyPiRuntimeManifest, type PiRuntimeManifest } from "./pi-verified-runtime.js";
import { resolveRunnerProviderAssetsRoot } from "./provider-assets-root.js";
import { QUALIFIED_ACPX_PROFILES, type QualifiedAcpxProfile } from "./qualified-profiles.js";
const MAX_DISTRIBUTION_METADATA_BYTES = 4 * 1024 * 1024;
const PI_MODEL = "openrouter/deepseek/deepseek-v4-flash-0731";
const FIXED_PATHS = Object.freeze({
node: "node/bin/node",
piEntrypoint: "node_modules/@earendil-works/pi-coding-agent/dist/cli.js",
extension: "extensions/paperclip.js",
wrapperEntrypoint: "node_modules/pi-acp/dist/index.js",
});
function record(value: unknown): Record<string, unknown> {
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Pi distribution metadata is invalid");
return value as Record<string, unknown>;
}
/** Candidate identity is build-owned; model selection cannot substitute a CLI. */
export function assertPiInstallationProfile(profile: QualifiedAcpxProfile): void {
const trusted = QUALIFIED_ACPX_PROFILES.pi;
if (profile.agentProfileVersion !== 2) throw new Error("Pi rich ACP requires profile version 2; reopen the previous session");
if (profile.agent !== "pi" || profile.driverKind !== trusted.driverKind || profile.protocolVersion !== trusted.protocolVersion || profile.acpxVersion !== trusted.acpxVersion || profile.agentServerPackage !== "pi-acp" || profile.agentServerVersion !== "0.0.33" || profile.agentRuntimePackage !== "@earendil-works/pi-coding-agent" || profile.agentRuntimeVersion !== "0.84.2" || profile.commandDigest !== trusted.commandDigest || profile.permissionPolicy !== "interactive" || profile.qualificationModel !== PI_MODEL || profile.reportedModelId !== PI_MODEL) throw new Error("Pi distribution profile differs from its trusted declaration");
}
async function readDistributionMetadata(path: string): Promise<Record<string, unknown>> {
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
const before = await file.stat({ bigint: true });
if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > BigInt(MAX_DISTRIBUTION_METADATA_BYTES)) throw new Error("Pi distribution metadata is not a bounded private file");
const bytes = await file.readFile();
const after = await file.stat({ bigint: true });
const named = await lstat(path, { bigint: true });
if (bytes.length !== Number(before.size) || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink()) throw new Error("Pi distribution metadata changed while read");
return record(JSON.parse(bytes.toString("utf8")));
} finally { await file.close(); }
}
/**
* Resolve only fixed runner package assets. The independently source-pinned
* native closure supplies the snapshot and provider-group lifetime guardian.
* Qualification status remains the caller's production admission gate.
*/
export async function verifyPiInstallation(profile: QualifiedAcpxProfile): Promise<VerifiedAcpxInstallation> {
assertPiInstallationProfile(profile);
const target = `${process.platform}-${process.arch}`;
if (!Object.hasOwn(PI_DISTRIBUTION_CLOSURE_SHA256, target)) throw new Error("Pi distribution target is unsupported");
const expectedClosure = PI_DISTRIBUTION_CLOSURE_SHA256[target as keyof typeof PI_DISTRIBUTION_CLOSURE_SHA256];
const assets = join(resolveRunnerProviderAssetsRoot(import.meta.url, "pi"), target);
const runtimeRoot = join(assets, "runtime");
const assertDirectories = async (): Promise<void> => {
for (const path of [assets, runtimeRoot]) {
const stat = await lstat(path);
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(path) !== path) throw new Error("Pi distribution escaped its fixed asset directory");
}
};
await assertDirectories();
const metadataPath = join(assets, "pi-distribution.json");
const metadata = await readDistributionMetadata(metadataPath);
const targetMetadata = record(metadata.target);
const pins = record(metadata.pins);
if (metadata.schema !== "paperclip.pi-distribution.v1" || metadata.runtimeRoot !== "runtime" || metadata.nativeClosureSha256 !== expectedClosure || targetMetadata.platform !== process.platform || targetMetadata.architecture !== process.arch || targetMetadata.nodeVersion !== PI_NODE_VERSION || pins.nodeVersion !== PI_NODE_VERSION || pins.wrapper !== "0.0.33" || pins.runtime !== "0.84.2" || pins.sdk !== "0.26.0" || pins.zod !== "3.25.76") throw new Error("Pi distribution metadata does not match its trusted target pin");
const manifest = record(metadata.manifest) as unknown as PiRuntimeManifest;
for (const [key, value] of Object.entries(FIXED_PATHS)) {
if (manifest[key as keyof typeof FIXED_PATHS] !== value) throw new Error("Pi distribution changed a fixed launch path");
}
const verified = await verifyPiRuntimeManifest(runtimeRoot, manifest);
if (verified.manifestDigest !== metadata.manifestDigest) throw new Error("Pi runtime manifest digest does not match its distribution");
const native = await verifyNativeAcpxInstallation({
distributionRoot: runtimeRoot,
manifestPath: join(assets, "native-closure.json"),
expectedClosureSha256: expectedClosure,
executable: FIXED_PATHS.node,
entrypoint: "pi-entry.cjs",
fixedArguments: [],
});
return Object.freeze({
commandDigest: profile.commandDigest,
agentServerPackageJsonPath: join(runtimeRoot, "node_modules/pi-acp/package.json"),
agentRuntimePackageJsonPath: join(runtimeRoot, "node_modules/@earendil-works/pi-coding-agent/package.json"),
async openCommand() {
await assertDirectories();
// The native primitive reads every admitted file through held descriptors
// into a new immutable snapshot. The bootstrap derives its own launch
// environment from that snapshot, never these mutable installation paths.
return native.openCommand();
},
});
}
@@ -83,6 +83,7 @@ async function fixture(t, extra = {}) {
test("actual patched ACP process streams thinking and waits for settlement with usage", async (t) => {
const f = await fixture(t);
assert.equal(f.initialized.agentCapabilities._meta.paperclipPi.steering, true);
assert.deepEqual(f.initialized.authMethods, []);
const session = await f.call("session/new", { cwd: join(f.root, "workspace"), mcpServers: [] });
const result = await f.call("session/prompt", { sessionId: session.sessionId, prompt: [{ type: "text", text: "hello" }] });
assert.equal(result.stopReason, "end_turn"); assert.equal(result.usage.inputTokens, 11); assert.equal(result.usage.totalTokens, 16);
+71 -29
View File
@@ -1,7 +1,15 @@
diff --git a/dist/index.js b/dist/index.js
--- a/dist/index.js
+++ b/dist/index.js
@@ -53,7 +53,7 @@
@@ -11,6 +11,7 @@
// src/acp/auth.ts
var PI_SETUP_METHOD_ID = "pi_terminal_login";
function getAuthMethods(opts) {
+ if (process.env.PAPERCLIP_ACPX_ISOLATED_CONTEXT === "1") return [];
const supportsTerminalAuthMeta = opts?.supportsTerminalAuthMeta ?? true;
const method = {
id: PI_SETUP_METHOD_ID,
@@ -53,7 +54,7 @@
// src/pi-rpc/process.ts
import { spawn } from "child_process";
@@ -10,7 +18,7 @@ diff --git a/dist/index.js b/dist/index.js
// src/pi-rpc/command.ts
import { platform } from "os";
@@ -96,49 +96,38 @@
@@ -96,49 +97,38 @@
preludeLines = [];
constructor(child) {
this.child = child;
@@ -83,7 +91,7 @@ diff --git a/dist/index.js b/dist/index.js
});
try {
await new Promise((resolve4, reject) => {
@@ -181,21 +170,30 @@
@@ -181,21 +171,30 @@
const { dirname: dirname3 } = await import("path");
mkdirSync2(dirname3(sessionFile), { recursive: true });
}
@@ -119,7 +127,7 @@ diff --git a/dist/index.js b/dist/index.js
}
}
/**
@@ -288,12 +286,17 @@
@@ -288,12 +287,17 @@
const withId = { ...cmd, id };
const line = `${JSON.stringify(withId)}
`;
@@ -142,7 +150,16 @@ diff --git a/dist/index.js b/dist/index.js
});
}
writeLine(line) {
@@ -714,6 +717,7 @@
@@ -337,7 +341,7 @@
{
authMethods: getAuthMethods()
},
- "Configure an API key or log in with an OAuth provider."
+ "Bind the configured OpenRouter API credential to this Paperclip runtime."
);
}
@@ -714,6 +718,7 @@
try {
proc = await PiRpcProcess.spawn({
cwd: params.cwd,
@@ -150,7 +167,7 @@ diff --git a/dist/index.js b/dist/index.js
piCommand: params.piCommand
});
} catch (e) {
@@ -808,6 +812,9 @@
@@ -808,6 +813,9 @@
this.proc = opts.proc;
this.conn = opts.conn;
this.fileCommands = opts.fileCommands ?? [];
@@ -160,7 +177,7 @@ diff --git a/dist/index.js b/dist/index.js
this.proc.onEvent((ev) => this.handlePiEvent(ev));
}
setStartupInfo(text) {
@@ -852,6 +859,7 @@
@@ -852,6 +860,7 @@
}
async cancel() {
this.cancelRequested = true;
@@ -168,7 +185,7 @@ diff --git a/dist/index.js b/dist/index.js
if (this.turnQueue.length) {
const queued = this.turnQueue.splice(0, this.turnQueue.length);
for (const t of queued) t.resolve("cancelled");
@@ -920,12 +928,16 @@
@@ -920,12 +929,16 @@
this.cancelRequested = false;
this.inAgentLoop = false;
this.pendingTurn = { resolve: t.resolve, reject: t.reject };
@@ -185,7 +202,7 @@ diff --git a/dist/index.js b/dist/index.js
const authErr = maybeAuthRequiredError(err);
if (authErr) {
this.pendingTurn?.reject(authErr);
@@ -946,6 +958,23 @@
@@ -946,6 +959,23 @@
handlePiEvent(ev) {
const type = String(ev.type ?? "");
switch (type) {
@@ -209,7 +226,7 @@ diff --git a/dist/index.js b/dist/index.js
case "message_update": {
const ame = ev.assistantMessageEvent;
if (ame?.type === "text_delta" && typeof ame.delta === "string") {
@@ -1047,7 +1076,7 @@
@@ -1047,7 +1077,7 @@
if (p) {
try {
const abs = isAbsolute(p) ? p : resolvePath(this.cwd, p);
@@ -218,7 +235,7 @@ diff --git a/dist/index.js b/dist/index.js
this.fileSnapshots.set(toolCallId, { path: p, oldText: snapshotOldText });
if (toolName === "edit") {
for (const needle of getEditOldTexts(args)) {
@@ -1125,7 +1154,7 @@
@@ -1125,7 +1155,7 @@
if (!isError && snapshot) {
try {
const abs = isAbsolute(snapshot.path) ? snapshot.path : resolvePath(this.cwd, snapshot.path);
@@ -227,7 +244,7 @@ diff --git a/dist/index.js b/dist/index.js
if (snapshot.oldText === null || newText !== snapshot.oldText) {
hasStructuredDiff = true;
content = [
@@ -1154,14 +1183,11 @@
@@ -1154,14 +1184,11 @@
break;
}
case "extension_ui_request": {
@@ -246,7 +263,7 @@ diff --git a/dist/index.js b/dist/index.js
break;
}
case "auto_retry_start": {
@@ -1210,9 +1236,12 @@
@@ -1210,9 +1237,12 @@
break;
}
case "agent_settled": {
@@ -260,7 +277,7 @@ diff --git a/dist/index.js b/dist/index.js
this.pendingTurn = null;
this.inAgentLoop = false;
const next = this.turnQueue.shift();
@@ -1725,6 +1754,7 @@
@@ -1725,6 +1755,7 @@
return process.env.PI_CODING_AGENT_DIR ? resolve3(process.env.PI_CODING_AGENT_DIR) : join4(homedir4(), ".pi", "agent");
}
function getEnableSkillCommands(cwd) {
@@ -268,7 +285,7 @@ diff --git a/dist/index.js b/dist/index.js
const merged = getMergedSettings(cwd);
const direct = merged.enableSkillCommands;
if (typeof direct === "boolean") return direct;
@@ -1733,6 +1763,7 @@
@@ -1733,6 +1764,7 @@
return true;
}
function getQuietStartup(cwd) {
@@ -276,7 +293,15 @@ diff --git a/dist/index.js b/dist/index.js
const merged = getMergedSettings(cwd);
const direct = merged.quietStartup;
if (typeof direct === "boolean") return direct;
@@ -1884,11 +1915,13 @@
@@ -1824,6 +1856,7 @@
const out = [];
const seen = /* @__PURE__ */ new Set();
for (const c of [...a, ...b]) {
+ if (process.env.PAPERCLIP_ACPX_ISOLATED_CONTEXT === "1" && !["compact", "session", "autocompact"].includes(c.name)) continue;
if (seen.has(c.name)) continue;
seen.add(c.name);
out.push(c);
@@ -1884,11 +1917,13 @@
throw RequestError3.invalidParams(`Unknown sessionId: ${sessionId}`);
}
const cwd = opts?.cwd ?? stored.cwd;
@@ -290,7 +315,7 @@ diff --git a/dist/index.js b/dist/index.js
piCommand: process.env.PI_ACP_PI_COMMAND
});
} catch (e) {
@@ -1897,7 +1930,7 @@
@@ -1897,7 +1932,7 @@
}
throw e;
}
@@ -299,7 +324,7 @@ diff --git a/dist/index.js b/dist/index.js
const session = this.sessions.getOrCreate(sessionId, {
cwd,
mcpServers: opts?.mcpServers ?? [],
@@ -1917,6 +1950,7 @@
@@ -1917,6 +1952,7 @@
}
}
async initialize(params) {
@@ -307,7 +332,7 @@ diff --git a/dist/index.js b/dist/index.js
const supportedVersion = 1;
const requested = params.protocolVersion;
return {
@@ -1933,7 +1967,8 @@
@@ -1933,7 +1969,8 @@
}),
agentCapabilities: {
loadSession: true,
@@ -317,7 +342,7 @@ diff --git a/dist/index.js b/dist/index.js
promptCapabilities: {
image: true,
audio: false,
@@ -1942,8 +1977,7 @@
@@ -1942,8 +1979,7 @@
sessionCapabilities: {
// **UNSTABLE** ACP capability used by Zed's codex-acp adapter.
// Enables a native session picker in clients that support it.
@@ -327,7 +352,7 @@ diff --git a/dist/index.js b/dist/index.js
}
}
};
@@ -1953,7 +1987,7 @@
@@ -1953,7 +1989,7 @@
throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
}
this.lastSessionCwd = params.cwd;
@@ -336,7 +361,24 @@ diff --git a/dist/index.js b/dist/index.js
const enableSkillCommands = getEnableSkillCommands(params.cwd);
const session = await this.sessions.create({
cwd: params.cwd,
@@ -2066,6 +2100,8 @@
@@ -1994,14 +2030,14 @@
this.cleanupFailedNewSession(session.sessionId, state);
throw RequestError3.authRequired(
{ authMethods: getAuthMethods() },
- "Configure an API key or log in with an OAuth provider."
+ "Bind the configured OpenRouter API credential to this Paperclip runtime."
);
}
if (stateErr && maybeAuthRequiredError(stateErr)) {
this.cleanupFailedNewSession(session.sessionId, state);
throw RequestError3.authRequired(
{ authMethods: getAuthMethods() },
- "Configure an API key or log in with an OAuth provider."
+ "Bind the configured OpenRouter API credential to this Paperclip runtime."
);
}
const { configOptions, models, modes } = await getSessionConfiguration(session.proc, {
@@ -2066,6 +2102,8 @@
const session = await this.restoreSession(params.sessionId);
const { message, images } = promptToPiMessage(params.prompt);
if (images.length === 0 && message.trimStart().startsWith("/")) {
@@ -345,7 +387,7 @@ diff --git a/dist/index.js b/dist/index.js
const trimmed = message.trim();
const space = trimmed.indexOf(" ");
const cmd = space === -1 ? trimmed.slice(1) : trimmed.slice(1, space);
@@ -2432,8 +2468,18 @@
@@ -2432,8 +2470,18 @@
}
}
const result = await session.prompt(message, images);
@@ -366,7 +408,7 @@ diff --git a/dist/index.js b/dist/index.js
}
async cancel(params) {
const session = this.sessions.maybeGet(params.sessionId);
@@ -2473,7 +2519,7 @@
@@ -2473,7 +2521,7 @@
mcpServers: params.mcpServers
});
const proc = session.proc;
@@ -375,7 +417,7 @@ diff --git a/dist/index.js b/dist/index.js
this.sessions.closeAllExcept?.(session.sessionId);
this.store.upsert({
sessionId: params.sessionId,
@@ -2607,6 +2653,7 @@
@@ -2607,6 +2655,7 @@
return response;
}
async deleteSession(params) {
@@ -383,7 +425,7 @@ diff --git a/dist/index.js b/dist/index.js
const stored = this.store.get(params.sessionId);
const piSession = findPiSession(params.sessionId);
if (!stored && !piSession) {
@@ -2828,6 +2875,7 @@
@@ -2828,6 +2877,7 @@
return 0;
}
function buildUpdateNotice() {
@@ -391,7 +433,7 @@ diff --git a/dist/index.js b/dist/index.js
try {
const piVersion = spawnSync("pi", ["--version"], { encoding: "utf-8" });
const installed = (String(piVersion.stdout ?? "").trim() || String(piVersion.stderr ?? "").trim()).replace(
@@ -2848,6 +2896,7 @@
@@ -2848,6 +2898,7 @@
}
}
function buildStartupInfo(opts) {
@@ -399,7 +441,7 @@ diff --git a/dist/index.js b/dist/index.js
void opts.fileCommands;
const md = [];
try {
@@ -2980,6 +3029,7 @@
@@ -2980,6 +3031,7 @@
// src/index.ts
if (process.argv.includes("--terminal-login")) {
@@ -407,7 +449,7 @@ diff --git a/dist/index.js b/dist/index.js
const { spawnSync: spawnSync2 } = await import("child_process");
const cmd = getPiCommand(process.env.PI_ACP_PI_COMMAND);
const res = spawnSync2(cmd, [], {
@@ -3019,11 +3069,12 @@
@@ -3019,11 +3071,12 @@
}
});
var stream = ndJsonStream(input, output);