Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 45 additions & 45 deletions apps/desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.0",
"vite": "^8.3.5",
"vitest": "^4.1.11",
"vitest": "^4.1.12",
"yaml": "^2.8.3"
},
"overrides": {
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/runtimes/pi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ pinned in this directory, matching the validated Zeron integration. The adapter
runs with NeverWrite's embedded Node runtime; the Pi
CLI itself remains user-managed and must be available as `pi` on `PATH`.

The runtime overrides Zod to `4.6.6` for GHSA-mrhx-c7xc-m3w5. The adapter
delegates validation to `@agentclientprotocol/sdk`, which already imports
`zod/v4` and supports Zod 4. Keep the runtime tests passing when updating this
override; it does not add size limits to otherwise unbounded ACP messages.

From `apps/desktop`, run `npm run pi:prepare` to build the validated local runtime
under `.cache/pi-runtime`. Release staging performs the same preparation and
copies it to `native-backend/embedded/pi-acp` outside the Electron ASAR.
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/runtimes/pi/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions apps/desktop/runtimes/pi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
"private": true,
"dependencies": {
"pi-acp": "0.0.33"
},
"overrides": {
"zod": "4.6.6"
}
}
110 changes: 86 additions & 24 deletions apps/desktop/scripts/pi-runtime.test.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { createRequire } from "node:module";
import path from "node:path";
import readline from "node:readline";
import test from "node:test";
import { pathToFileURL } from "node:url";

import {
piInputs,
Expand All @@ -20,42 +22,102 @@ test("Pi ACP is pinned and its prepared runtime is valid", async () => {
}
});

test("bundled Pi ACP initializes as protocol v1 and advertises session load", async (t) => {
async function startPiRuntime(t) {
const runtime = await preparePiRuntime();
const child = spawn(process.execPath, [path.join(runtime, "dist", "index.js")], {
stdio: ["pipe", "pipe", "pipe"],
});
t.after(() => child.kill());

const response = await new Promise((resolve, reject) => {
const lines = readline.createInterface({ input: child.stdout });
const timer = setTimeout(() => reject(new Error("Pi ACP initialize timed out")), 5_000);
child.once("error", reject);
child.once("exit", (code, signal) => {
reject(new Error(`Pi ACP exited before initialize (${code ?? signal})`));
});
lines.once("line", (line) => {
const lines = readline.createInterface({ input: child.stdout });
t.after(() => {
lines.close();
child.kill();
});
let nextId = 1;
return (method, params) => new Promise((resolve, reject) => {
const id = nextId++;
const finish = (error, response) => {
clearTimeout(timer);
lines.off("line", onLine);
child.off("error", onError);
child.off("exit", onExit);
if (error) reject(error);
else resolve(response);
};
const onError = (error) => finish(error);
const onExit = (code, signal) => finish(
new Error(`Pi ACP exited during ${method} (${code ?? signal})`),
);
const onLine = (line) => {
try {
resolve(JSON.parse(line));
const response = JSON.parse(line);
if (response.id === id) finish(null, response);
} catch (error) {
reject(error);
finish(error);
}
});
child.stdin.write(`${JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: 1,
clientCapabilities: {},
clientInfo: { name: "neverwrite-test", version: "0" },
},
})}\n`);
};
const timer = setTimeout(() => {
finish(new Error(`Pi ACP ${method} timed out`));
}, 5_000);
lines.on("line", onLine);
child.once("error", onError);
child.once("exit", onExit);
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
});
}

const initializeParams = {
protocolVersion: 1,
clientCapabilities: {},
clientInfo: { name: "neverwrite-test", version: "0" },
};

test("bundled Pi ACP initializes as protocol v1 and advertises session load", async (t) => {
const request = await startPiRuntime(t);
const response = await request("initialize", initializeParams);

assert.equal(response.result.protocolVersion, 1);
assert.equal(response.result.agentInfo.name, "pi-acp");
assert.equal(response.result.agentInfo.version, (await piInputs()).version);
assert.equal(response.result.agentCapabilities.loadSession, true);
});

test("bundled Pi ACP rejects malformed requests and remains usable", async (t) => {
const request = await startPiRuntime(t);
await request("initialize", initializeParams);
const malformedRequests = [
["initialize", { ...initializeParams, protocolVersion: "1" }],
["session/new", { cwd: process.cwd(), mcpServers: [null] }],
["session/load", { cwd: process.cwd(), mcpServers: [], sessionId: 1 }],
["session/prompt", { sessionId: "unused", prompt: [{ type: "text", text: 1 }] }],
];
for (const [method, params] of malformedRequests) {
const response = await request(method, params);
assert.equal(response.error?.code, -32602, method);
}
const response = await request("initialize", initializeParams);
assert.equal(response.result.protocolVersion, 1);
});

test("bundled Zod rejects oversized constrained collections before parsing elements", async (t) => {
const runtime = await preparePiRuntime();
const require = createRequire(path.join(runtime, "package.json"));
const { z } = await import(pathToFileURL(require.resolve("zod/v4")).href);
const invalid = Array.from({ length: 512 }, (_, index) => index);
const cases = [
["array max", () => z.array(z.string()).max(2), invalid, ["a"]],
["array length", () => z.array(z.string()).length(2), invalid, ["a", "b"]],
["set max", () => z.set(z.string()).max(2), new Set(invalid), new Set(["a"])],
["map max", () => z.map(z.string(), z.string()).max(2),
new Map(invalid.map((value) => [value, value])), new Map([["a", "b"]])],
];
for (const [name, createSchema, oversized, valid] of cases) {
await t.test(name, () => {
const schema = createSchema();
const result = schema.safeParse(oversized);
assert.equal(result.success, false);
assert.equal(result.error.issues.length, 1);
assert.equal(result.error.issues[0].code, "too_big");
assert.deepEqual(schema.parse(valid), valid);
});
}
});
2 changes: 1 addition & 1 deletion apps/web-clipper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"tailwindcss": "^4.2.4",
"typescript": "^6.0.3",
"vite": "^8.3.5",
"vitest": "^4.1.11",
"vitest": "^4.1.12",
"wxt": "^0.21.4"
}
}
Loading
Loading