fix: reject executable pi configuration
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
closeSync, constants, fstatSync, lstatSync, openSync, readFileSync,
|
||||
} from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const MAX_AGENT_CONFIG_BYTES = 1024 * 1024;
|
||||
|
||||
export const PI_MANAGED_CONFIG_ERROR_CODE = "PI_MANAGED_CONFIG_INVALID";
|
||||
export const PI_MANAGED_CONFIG_ERROR_MESSAGE = "Pi provider/model configuration is invalid";
|
||||
|
||||
export class PiManagedConfigError extends Error {
|
||||
readonly code = PI_MANAGED_CONFIG_ERROR_CODE;
|
||||
|
||||
constructor() {
|
||||
super(PI_MANAGED_CONFIG_ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
export function isPiManagedConfigError(error: unknown): boolean {
|
||||
return Boolean(
|
||||
error && typeof error === "object"
|
||||
&& (error as { code?: unknown }).code === PI_MANAGED_CONFIG_ERROR_CODE,
|
||||
);
|
||||
}
|
||||
|
||||
export function parsePiConfigJson(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
throw new PiManagedConfigError();
|
||||
}
|
||||
}
|
||||
|
||||
/** Reject every Pi shell-backed configuration value, including unknown future nested fields. */
|
||||
export function assertDeclarativePiConfig(value: unknown): void {
|
||||
const pending: unknown[] = [value];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
if (typeof current === "string") {
|
||||
if (current.startsWith("!")) throw new PiManagedConfigError();
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
for (const nested of current) pending.push(nested);
|
||||
continue;
|
||||
}
|
||||
if (current && typeof current === "object") {
|
||||
for (const nested of Object.values(current as Record<string, unknown>)) pending.push(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateDeclarativePiConfig(raw: string): void {
|
||||
assertDeclarativePiConfig(parsePiConfigJson(raw));
|
||||
}
|
||||
|
||||
export function readConfiguredPiAgentFile(name: "auth.json"): string;
|
||||
export function readConfiguredPiAgentFile(name: "models.json", optional: true): string | undefined;
|
||||
export function readConfiguredPiAgentFile(
|
||||
name: "auth.json" | "models.json",
|
||||
optional = false,
|
||||
): string | undefined {
|
||||
const configuredAgentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
||||
const path = join(configuredAgentDir, name);
|
||||
let fd: number | undefined;
|
||||
try {
|
||||
const before = lstatSync(path);
|
||||
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_AGENT_CONFIG_BYTES) {
|
||||
throw new PiManagedConfigError();
|
||||
}
|
||||
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
||||
const opened = fstatSync(fd);
|
||||
if (!opened.isFile() || opened.size > MAX_AGENT_CONFIG_BYTES
|
||||
|| before.dev !== opened.dev || before.ino !== opened.ino) {
|
||||
throw new PiManagedConfigError();
|
||||
}
|
||||
return readFileSync(fd, "utf8");
|
||||
} catch (error) {
|
||||
if (optional && (error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
|
||||
throw new PiManagedConfigError();
|
||||
} finally {
|
||||
if (fd !== undefined) {
|
||||
try { closeSync(fd); } catch { /* preserve the stable validation outcome */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user