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
61 changes: 61 additions & 0 deletions packages/adapters/pi/src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {

export { withNodeRuntimeOnPath } from "@codexhost/harness-discovery";

const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
const PI_PACKAGE_PATH = path.join("node_modules", "@earendil-works", "pi-coding-agent");

export interface PiExecutableDependencies {
platform: NodeJS.Platform;
homeDirectory: string;
Expand Down Expand Up @@ -52,6 +55,64 @@ export function resolvePiExecutable(
return resolution?.executable ?? input.command ?? input.environment.PI_COMMAND ?? "pi";
}

/** Find the package backing the executable selected by the Pi Harness resolver. */
export function findPiPackageDirectory(executable: string): string | undefined {
let directory = path.dirname(executable);
while (true) {
for (const candidate of [directory, path.join(directory, PI_PACKAGE_PATH)]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify that the discovered package backs the executable

When the selected executable is a wrapper or non-npm Pi binary beneath any directory containing a separate node_modules/@earendil-works/pi-coding-agent—for example, a custom wrapper inside a project with a stale Pi dependency—this loop accepts that unrelated package from any ancestor without establishing that the executable points to it. installation() then imports authentication modules, performs collision checks, and generates extension paths using a different Pi installation, contradicting its guarantee to reject unrelated installs; restrict the fallback to a verified npm shim layout or validate the shim's target.

Useful? React with 👍 / 👎.

try {
const manifest: unknown = JSON.parse(
fs.readFileSync(path.join(candidate, "package.json"), "utf8"),
);
if (
typeof manifest === "object" &&
manifest !== null &&
"name" in manifest &&
manifest.name === PI_PACKAGE_NAME
) {
return candidate;
}
} catch {
// Keep looking through the executable's install roots and npm shim directories.
}
}
const parent = path.dirname(directory);
if (parent === directory) return undefined;
directory = parent;
}
}

/** Resolve Pi's declared JavaScript CLI entry instead of trying to execute a Windows .cmd shim via Node. */
export function findPiCliEntry(packageDirectory: string): string | undefined {
try {
const manifest: unknown = JSON.parse(
fs.readFileSync(path.join(packageDirectory, "package.json"), "utf8"),
);
if (typeof manifest !== "object" || manifest === null || !("bin" in manifest)) return undefined;
const bin = manifest.bin;
const entry =
typeof bin === "string"
? bin
: typeof bin === "object" && bin !== null && "pi" in bin
? bin.pi
: undefined;
if (typeof entry !== "string" || !entry.trim()) return undefined;
const resolved = path.resolve(packageDirectory, entry);
const relative = path.relative(packageDirectory, resolved);
if (
relative === ".." ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative) ||
!fs.statSync(resolved).isFile()
) {
return undefined;
}
return resolved;
} catch {
return undefined;
}
}

function isFilesystemRoot(cwd: string): boolean {
const resolved = path.resolve(cwd);
return resolved === path.parse(resolved).root;
Expand Down
20 changes: 6 additions & 14 deletions packages/adapters/pi/src/pi-credential-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
type CredentialImportRecord,
type CredentialOtherLogin,
} from "@codexhost/shared-contracts";
import { resolvePiExecutable } from "./command.js";
import { findPiCliEntry, findPiPackageDirectory, resolvePiExecutable } from "./command.js";

type LockBackend = {
withLock<T>(fn: (current: string | undefined) => { result: T; next?: string }): T;
Expand Down Expand Up @@ -118,18 +118,10 @@ async function installation(
const executable = await realpath(
resolvePiExecutable({ ...(command ? { command } : {}), environment }),
);
let directory = path.dirname(executable);
while (true) {
const manifest = path.join(directory, "package.json");
if (existsSync(manifest)) {
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
if (pkg.name === "@earendil-works/pi-coding-agent") break;
}
const parent = path.dirname(directory);
if (parent === directory)
throw new Error("Pi credential import requires a supported npm Pi installation");
directory = parent;
}
const directory = findPiPackageDirectory(executable);
const cliEntry = directory ? findPiCliEntry(directory) : undefined;
if (!directory || !cliEntry)
throw new Error("Pi credential import requires a supported npm Pi installation");
// Native lock implementation coordinates with Pi's own auth.json writers.
const storage = await import(
pathToFileURL(path.join(directory, "dist/core/auth-storage.js")).href
Expand Down Expand Up @@ -158,7 +150,7 @@ async function installation(
// This creates no Turn and passes no credential material on the command line.
const { stdout } = await promisify(execFile)(
process.execPath,
[executable, "--offline", "--no-approve", "--list-models"],
[cliEntry, "--offline", "--no-approve", "--list-models"],
{
cwd: existsSync(path.dirname(authPath)) ? path.dirname(authPath) : os.homedir(),
env: { ...environment, PI_CODING_AGENT_DIR: path.dirname(authPath), PI_OFFLINE: "1" },
Expand Down
85 changes: 85 additions & 0 deletions packages/adapters/pi/test/command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { findPiCliEntry, findPiPackageDirectory } from "../src/command.js";

const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

function temporaryRoot(): string {
const root = mkdtempSync(path.join(os.tmpdir(), "pi-discovery-test-"));
roots.push(root);
return root;
}

function writePiPackage(directory: string): void {
mkdirSync(directory, { recursive: true });
writeFileSync(
path.join(directory, "package.json"),
JSON.stringify({ name: "@earendil-works/pi-coding-agent", bin: { pi: "dist/bundle/cli.js" } }),
);
}

describe("Pi package discovery", () => {
it("finds the package next to a version-manager npm shim", () => {
const versionRoot = path.join(temporaryRoot(), "nvm", "v24.13.1");
mkdirSync(versionRoot, { recursive: true });
const shim = path.join(versionRoot, "pi.cmd");
writeFileSync(shim, "@echo off\r\n");
const packageDirectory = path.join(
versionRoot,
"node_modules",
"@earendil-works",
"pi-coding-agent",
);
writePiPackage(packageDirectory);

expect(findPiPackageDirectory(shim)).toBe(packageDirectory);
const cliEntry = path.join(packageDirectory, "dist", "bundle", "cli.js");
mkdirSync(path.dirname(cliEntry), { recursive: true });
writeFileSync(cliEntry, "");
expect(findPiCliEntry(packageDirectory)).toBe(cliEntry);
});

it("finds the package when the executable is inside its package directory", () => {
const packageDirectory = path.join(
temporaryRoot(),
"node_modules",
"@earendil-works",
"pi-coding-agent",
);
const executable = path.join(packageDirectory, "dist", "cli.js");
mkdirSync(path.dirname(executable), { recursive: true });
writeFileSync(executable, "");
writePiPackage(packageDirectory);

expect(findPiPackageDirectory(executable)).toBe(packageDirectory);
});

it("rejects a CLI entry that escapes the Pi package directory", () => {
const packageDirectory = path.join(temporaryRoot(), "pi-package");
mkdirSync(packageDirectory, { recursive: true });
writeFileSync(
path.join(packageDirectory, "package.json"),
JSON.stringify({ name: "@earendil-works/pi-coding-agent", bin: { pi: "../outside.js" } }),
);
writeFileSync(path.join(path.dirname(packageDirectory), "outside.js"), "");

expect(findPiCliEntry(packageDirectory)).toBeUndefined();
});

it("does not treat an unrelated npm package as Pi", () => {
const executable = path.join(temporaryRoot(), "bin", "pi.cmd");
mkdirSync(path.dirname(executable), { recursive: true });
writeFileSync(executable, "@echo off\r\n");
writeFileSync(
path.join(path.dirname(executable), "package.json"),
JSON.stringify({ name: "some-other-package" }),
);

expect(findPiPackageDirectory(executable)).toBeUndefined();
});
});
Loading