fix: harden Pi management readiness

This commit is contained in:
2026-08-05 06:20:25 +02:00
parent 7a8bcacbfe
commit 7df21b5f21
24 changed files with 577 additions and 274 deletions
+23 -2
View File
@@ -1,6 +1,7 @@
import { execFile as nodeExecFile } from "node:child_process";
import { promisify } from "node:util";
import type { AppConfig } from "../config.js";
import { secretValue } from "../config/secret-bundle.js";
import {
loadSettings,
saveSettings,
@@ -12,6 +13,11 @@ import {
isPiManagedConfigError,
} from "./managed-config.js";
import { createPiProviderSmoke, type PiProviderSmoke } from "./provider-smoke.js";
import { loadPiAuthProviders } from "./auth-providers.js";
import {
piProviderCredentialStatus,
type PiCredentialStatus,
} from "./provider-credentials.js";
const execFile = promisify(nodeExecFile);
const REASONING_CHOICES = ["low", "medium", "high"] as const;
@@ -31,6 +37,7 @@ export interface PiInstallationConfig {
export interface PiStatus {
version?: string;
ready: boolean;
credentials: PiCredentialStatus;
config: PiInstallationConfig;
checkedAt: string;
message?: string;
@@ -89,6 +96,7 @@ interface PiManagementDeps {
readSettings?: () => Settings;
saveSettings?: (settings: Settings) => Settings;
readLogs?: () => string | Promise<string>;
credentialStatus?: (provider: string | undefined) => PiCredentialStatus;
now?: () => Date;
}
@@ -104,6 +112,18 @@ export function createPiManagement(config: AppConfig, deps: PiManagementDeps): P
const persistSettings = deps.saveSettings ?? ((settings) => saveSettings(config, settings));
const readLogs = deps.readLogs ?? (() => diagnostics.join("\n"));
const smokeProvider = deps.smokeProvider ?? createPiProviderSmoke(config);
const credentialStatus = deps.credentialStatus ?? ((provider: string | undefined) => {
try {
return piProviderCredentialStatus({
provider,
authProviders: loadPiAuthProviders(),
credentialValue: secretValue(config, "THT_MODEL_API_KEY"),
credentialFile: config.modelApiKeyFile,
});
} catch {
return "missing";
}
});
const closedOptions = async (): Promise<Omit<PiOptions, "checkedAt">> => {
let listed: PiModel[];
@@ -168,14 +188,15 @@ export function createPiManagement(config: AppConfig, deps: PiManagementDeps): P
async status(): Promise<PiStatus> {
const checkedAt = now().toISOString();
const current = installationConfig();
const credentials = credentialStatus(current.provider);
try {
const currentVersion = await version();
addDiagnostic("Pi version probe succeeded");
return { version: currentVersion, ready: true, config: current, checkedAt };
return { version: currentVersion, ready: true, credentials, config: current, checkedAt };
} catch (error) {
const message = stableMessage(error, "Pi runtime is unavailable");
addDiagnostic(message);
return { ready: false, config: current, checkedAt, message };
return { ready: false, credentials, config: current, checkedAt, message };
}
},
+21
View File
@@ -53,6 +53,8 @@ export function canonicalPiProvider(provider: string | undefined): string | unde
return value;
}
export type PiCredentialStatus = "present" | "missing";
export interface CredentialFsOps {
lstat(path: string): Stats;
open(path: string, flags: number): number;
@@ -172,3 +174,22 @@ export function buildPiChildEnv(opts: {
}
return env;
}
/** Report only whether the selected hosted provider has a usable credential source. */
export function piProviderCredentialStatus(opts: {
provider?: string;
credentialFile?: string;
credentialValue?: string;
authProviders?: ReadonlySet<string>;
fsOps?: CredentialFsOps;
}): PiCredentialStatus {
const provider = canonicalPiProvider(opts.provider);
if (!provider || LOCAL_PROVIDERS.has(provider)) return "missing";
if (opts.authProviders?.has(provider)) return "present";
try {
buildPiChildEnv({ ...opts, ambient: {} });
return "present";
} catch {
return "missing";
}
}
+32
View File
@@ -38,12 +38,14 @@ test("status parses only a Pi version from a fixed execFile argument array", asy
execute: successfulExec(calls),
listModels: async () => supportedModels,
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
credentialStatus: () => "missing",
now: () => new Date("2026-08-05T10:00:00.000Z"),
});
await expect(service.status()).resolves.toEqual({
version: "0.80.3",
ready: true,
credentials: "missing",
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:00:00.000Z",
});
@@ -53,6 +55,36 @@ test("status parses only a Pi version from a fixed execFile argument array", asy
expect(calls[0].timeout).toBeLessThanOrEqual(750);
});
// Catches credential presence being inferred from smoke success/failure or exposing any
// credential material instead of the installation's explicit sanitized presence state.
test.each(["present", "missing"] as const)(
"status reports configured-provider credentials only as %s",
async (credentials) => {
const checkedProviders: Array<string | undefined> = [];
const service = createPiManagement(configFor(), {
execute: successfulExec([]),
listModels: async () => supportedModels,
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
credentialStatus: (provider) => {
checkedProviders.push(provider);
return credentials;
},
now: () => new Date("2026-08-05T10:00:00.000Z"),
});
const status = await service.status();
expect(status).toEqual({
version: "0.80.3",
ready: true,
credentials,
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:00:00.000Z",
});
expect(checkedProviders).toEqual(["zai"]);
expect(JSON.stringify(status)).not.toMatch(/api.?key|token|password|secret/i);
},
);
// Catches an options response that leaks provider metadata or lets callers choose model IDs that
// Pi did not explicitly enable for this installation.
test("options expose only closed provider, model, and reasoning choices", async () => {
+14
View File
@@ -5,6 +5,7 @@ import {
PI_0803_CREDENTIAL_ENV_NAMES,
buildPiChildEnv,
canonicalPiProvider,
piProviderCredentialStatus,
} from "../src/pi/provider-credentials.js";
test("canonical provider aliases resolve to packaged Pi 0.80.3 IDs", () => {
@@ -130,6 +131,19 @@ test("local-qwen is an explicit local provider and needs no generic key", () =>
expect(env).not.toHaveProperty("THT_MODEL_API_KEY_FILE");
});
test("credential status reports only present or missing without treating local providers as credentialed", () => {
expect(piProviderCredentialStatus({
provider: "deepseek",
authProviders: new Set(["deepseek"]),
credentialValue: "must-not-be-returned",
})).toBe("present");
expect(piProviderCredentialStatus({ provider: "deepseek" })).toBe("missing");
expect(piProviderCredentialStatus({
provider: "local-qwen",
credentialValue: "must-not-be-returned",
})).toBe("missing");
});
test("bundle value is injected without exposing bundle metadata to Pi", () => {
const env = buildPiChildEnv({
ambient: {
@@ -10,6 +10,7 @@ function fakeService(): PiManagementService {
return {
status: vi.fn(async () => ({
version: "0.80.3", ready: true,
credentials: "present",
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:00:00.000Z",
})),
@@ -77,6 +78,7 @@ test("loopback-only AUTH_MODE=none may read the sanitized Pi status", async () =
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
version: "0.80.3", ready: true,
credentials: "present",
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:00:00.000Z",
});