fix: harden pi management verification

This commit is contained in:
2026-08-05 01:00:13 +02:00
parent d6b4a08a02
commit 55926c75f8
12 changed files with 502 additions and 25 deletions
+40 -13
View File
@@ -7,6 +7,7 @@ import {
type Settings,
} from "../settings/settings-store.js";
import type { PiModel } from "./list-models.js";
import { createPiProviderSmoke, type PiProviderSmoke } from "./provider-smoke.js";
const execFile = promisify(nodeExecFile);
const REASONING_CHOICES = ["low", "medium", "high"] as const;
@@ -80,6 +81,7 @@ export class PiManagementError extends Error {
interface PiManagementDeps {
execute?: PiExecFile;
listModels: () => Promise<PiModel[]>;
smokeProvider?: PiProviderSmoke;
readSettings?: () => Settings;
saveSettings?: (settings: Settings) => Settings;
readLogs?: () => string | Promise<string>;
@@ -97,6 +99,7 @@ export function createPiManagement(config: AppConfig, deps: PiManagementDeps): P
const readSettings = deps.readSettings ?? (() => loadSettings(config));
const persistSettings = deps.saveSettings ?? ((settings) => saveSettings(config, settings));
const readLogs = deps.readLogs ?? (() => diagnostics.join("\n"));
const smokeProvider = deps.smokeProvider ?? createPiProviderSmoke(config);
const closedOptions = async (): Promise<Omit<PiOptions, "checkedAt">> => {
let listed: PiModel[];
@@ -123,12 +126,12 @@ export function createPiManagement(config: AppConfig, deps: PiManagementDeps): P
return { providers, models, reasoning: [...REASONING_CHOICES] };
};
const version = async (): Promise<string> => {
const version = async (timeoutMs = config.piManagementTimeoutMs): Promise<string> => {
let output: { stdout: string; stderr: string };
try {
// The Pi executable and every argument are installation-owned constants. Do not add a shell.
output = await execute(config.piBin, ["--version"], {
timeout: config.piManagementTimeoutMs,
timeout: timeoutMs,
maxBuffer: MAX_EXEC_OUTPUT_BYTES,
});
} catch (error) {
@@ -193,20 +196,40 @@ export function createPiManagement(config: AppConfig, deps: PiManagementDeps): P
async test(): Promise<PiTestResult> {
const checkedAt = now().toISOString();
const deadline = Date.now() + config.piManagementTimeoutMs;
let timer: NodeJS.Timeout | undefined;
try {
await version();
const current = installationConfig();
if (!current.provider || !current.model || !current.reasoning) {
return smokeFailure("Pi installation configuration is incomplete", checkedAt, addDiagnostic);
}
const choices = await closedOptions();
if (!choices.models.some((model) => model.provider === current.provider && model.id === current.model)) {
return smokeFailure("Configured Pi provider and model are unavailable", checkedAt, addDiagnostic);
}
const check = async (): Promise<void> => {
await version(remainingBudget(deadline));
const current = installationConfig();
if (!current.provider || !current.model || !current.reasoning) {
throw new PiManagementError(
"pi_management_unavailable",
"Pi installation configuration is incomplete",
);
}
await smokeProvider({
provider: current.provider,
model: current.model,
reasoning: current.reasoning,
timeoutMs: remainingBudget(deadline),
});
};
await Promise.race([
check(),
new Promise<never>((_resolve, reject) => {
timer = setTimeout(
() => reject(new PiManagementError("pi_management_unavailable", "Pi smoke check timed out")),
config.piManagementTimeoutMs,
);
}),
]);
addDiagnostic("Pi smoke check succeeded");
return { ready: true, checkedAt };
} catch (error) {
return smokeFailure(stableMessage(error, "Pi smoke check failed"), checkedAt, addDiagnostic);
return smokeFailure(stableMessage(error, "Pi provider smoke check failed"), checkedAt, addDiagnostic);
} finally {
if (timer) clearTimeout(timer);
}
},
@@ -279,7 +302,11 @@ function smokeFailure(
export function redact(value: string): string {
return value
.replace(/(\bauthorization\b\s*:\s*Bearer\s+)[^\s,;]+/giu, "$1[REDACTED]")
.replace(/(\b(?:api[_-]?key|token|password|secret|authorization)\b\s*(?:=|:)\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;]+)/giu, "$1[REDACTED]")
.replace(/((?:["']?)[A-Za-z0-9_-]*(?:api[_-]?key|token|password|secret|authorization)[A-Za-z0-9_-]*(?:["']?)\s*(?:=|:)\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}]+)/giu, "$1[REDACTED]")
.replace(/(\bBearer\s+)[^\s,;]+/giu, "$1[REDACTED]")
.replace(/(\w+:\/\/[^:/\s]+:)[^@/\s]+@/gu, "$1[REDACTED]@");
}
function remainingBudget(deadline: number): number {
return Math.max(1, deadline - Date.now());
}