feat: expose safe pi management api

This commit is contained in:
2026-08-05 00:31:44 +02:00
parent 09eeea17e5
commit d6b4a08a02
10 changed files with 744 additions and 61 deletions
+163
View File
@@ -0,0 +1,163 @@
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { loadConfig } from "../src/config.js";
import {
PiManagementError,
createPiManagement,
type PiExecFile,
} from "../src/pi/management.js";
function configFor(settingsFile = join(mkdtempSync(join(tmpdir(), "tht-pi-management-")), "settings.json")) {
return loadConfig({
THT_HARNESS_DIR: "../harness",
SETTINGS_FILE: settingsFile,
PI_BIN: "/usr/local/bin/pi",
PI_MANAGEMENT_TIMEOUT_MS: "750",
});
}
const supportedModels = [
{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true },
{ provider: "deepseek", id: "deepseek-v4", name: "DeepSeek V4", reasoning: true },
];
function successfulExec(calls: Array<{ command: string; args: string[]; timeout: number }>): PiExecFile {
return async (command, args, options) => {
calls.push({ command, args, timeout: options.timeout });
return { stdout: "pi 0.80.3\n", stderr: "" };
};
}
// Catches a Pi executable that emits unexpected text or is invoked through a shell, which could
// turn a version display into a command-injection or information-disclosure surface.
test("status parses only a Pi version from a fixed execFile argument array", async () => {
const calls: Array<{ command: string; args: string[]; timeout: number }> = [];
const service = createPiManagement(configFor(), {
execute: successfulExec(calls),
listModels: async () => supportedModels,
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
now: () => new Date("2026-08-05T10:00:00.000Z"),
});
await expect(service.status()).resolves.toEqual({
version: "0.80.3",
ready: true,
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:00:00.000Z",
});
expect(calls).toEqual([{ command: "/usr/local/bin/pi", args: ["--version"], timeout: 750 }]);
});
// 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 () => {
const service = createPiManagement(configFor(), {
execute: successfulExec([]),
listModels: async () => supportedModels,
now: () => new Date("2026-08-05T10:00:00.000Z"),
});
await expect(service.options()).resolves.toEqual({
providers: ["zai", "deepseek"],
models: [
{ provider: "zai", id: "glm-5.2" },
{ provider: "deepseek", id: "deepseek-v4" },
],
reasoning: ["low", "medium", "high"],
checkedAt: "2026-08-05T10:00:00.000Z",
});
});
// Catches configuration writes that accept whitespace, unknown choices, or extra free-form fields
// before reaching the durable installation settings file.
test("config rejects invalid free-form values before writing settings", async () => {
const directory = mkdtempSync(join(tmpdir(), "tht-pi-management-invalid-"));
try {
let writes = 0;
const service = createPiManagement(configFor(join(directory, "settings.json")), {
execute: successfulExec([]),
listModels: async () => supportedModels,
readSettings: () => ({}),
saveSettings: () => { writes += 1; return {}; },
});
await expect(service.configure({
provider: "zai ", model: "glm-5.2", reasoning: "medium", unexpected: "value",
} as any)).rejects.toMatchObject<PiManagementError>({ code: "pi_management_invalid_config" });
expect(writes).toBe(0);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
// Catches a non-atomic implementation that can leave partial settings or temporary files after a
// normal installation-default update.
test("config validates closed choices and atomically persists non-secret defaults", async () => {
const directory = mkdtempSync(join(tmpdir(), "tht-pi-management-write-"));
const settingsFile = join(directory, "settings.json");
try {
const service = createPiManagement(configFor(settingsFile), {
execute: successfulExec([]),
listModels: async () => supportedModels,
now: () => new Date("2026-08-05T10:00:00.000Z"),
});
await expect(service.configure({
provider: "zai", model: "glm-5.2", reasoning: "high",
})).resolves.toEqual({
provider: "zai", model: "glm-5.2", reasoning: "high", updatedAt: "2026-08-05T10:00:00.000Z",
});
expect(JSON.parse(readFileSync(settingsFile, "utf8"))).toEqual({
provider: "zai", model: "glm-5.2", thinking: "high",
});
expect(readdirSync(directory)).toEqual(["settings.json"]);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
// Catches a hung Pi smoke check that leaves an operator waiting indefinitely or returns raw child
// diagnostics containing provider credentials.
test("smoke uses the configured timeout and reports a sanitized timeout", async () => {
const calls: Array<{ command: string; args: string[]; timeout: number }> = [];
const service = createPiManagement(configFor(), {
execute: async (command, args, options) => {
calls.push({ command, args, timeout: options.timeout });
throw Object.assign(new Error("provider token=raw-provider-token"), { code: "ETIMEDOUT" });
},
listModels: async () => supportedModels,
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
now: () => new Date("2026-08-05T10:00:00.000Z"),
});
await expect(service.test()).resolves.toEqual({
ready: false,
message: "Pi smoke check timed out",
checkedAt: "2026-08-05T10:00:00.000Z",
});
expect(calls).toEqual([{ command: "/usr/local/bin/pi", args: ["--version"], timeout: 750 }]);
});
// Catches an unbounded diagnostics endpoint or one that returns bearer tokens and connection
// passwords captured in Pi output.
test("logs keep only the latest 200 redacted lines", async () => {
const source = Array.from({ length: 205 }, (_, index) => `line-${index + 1}`);
source[203] = "Authorization: Bearer raw-bearer-token";
source[204] = "database_url=postgres://thoth:raw-db-password@example.invalid/db";
const service = createPiManagement(configFor(), {
execute: successfulExec([]),
listModels: async () => supportedModels,
readLogs: () => source.join("\n"),
now: () => new Date("2026-08-05T10:00:00.000Z"),
});
const logs = await service.logs();
expect(logs.checkedAt).toBe("2026-08-05T10:00:00.000Z");
expect(logs.lines).toHaveLength(200);
expect(logs.lines[0]).toBe("line-6");
expect(logs.lines.join("\n")).not.toContain("raw-bearer-token");
expect(logs.lines.join("\n")).not.toContain("raw-db-password");
expect(logs.lines.join("\n")).toContain("[REDACTED]");
});
+112
View File
@@ -0,0 +1,112 @@
import { expect, test, vi } from "vitest";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import type { PiManagementService } from "../src/pi/management.js";
import { piManagementRoutes } from "../src/routes/pi-management.js";
void piManagementRoutes;
function fakeService(): PiManagementService {
return {
status: vi.fn(async () => ({
version: "0.80.3", ready: true,
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:00:00.000Z",
})),
options: vi.fn(async () => ({
providers: ["zai"], models: [{ provider: "zai", id: "glm-5.2" }],
reasoning: ["low", "medium", "high"], checkedAt: "2026-08-05T10:00:00.000Z",
})),
configure: vi.fn(async (value) => ({ ...value, updatedAt: "2026-08-05T10:00:00.000Z" })),
test: vi.fn(async () => ({ ready: true, checkedAt: "2026-08-05T10:00:00.000Z" })),
logs: vi.fn(async () => ({ lines: ["Pi smoke check succeeded"], checkedAt: "2026-08-05T10:00:00.000Z" })),
};
}
function appWith(service: PiManagementService, env: Record<string, string> = {}) {
return buildApp(loadConfig({ THT_HARNESS_DIR: "../harness", ...env }), {
thtRunner: {} as any,
piManagement: service,
});
}
const adminHeaders = {
"x-thoth-principal-issuer": "portal",
"x-thoth-principal-subject": "operator",
"x-thoth-is-admin": "1",
};
const exposedServerEnv = {
AUTH_MODE: "upstream",
THOTH_PUBLIC_EXPOSURE: "true",
THT_SESSION_STORAGE: "postgres",
THT_SESSION_DB_HOST: "db.example.invalid",
THT_SESSION_DB_NAME: "thoth_sessions",
THT_SESSION_RUNTIME_USER: "thoth_runtime",
THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session-password",
THT_SESSION_DB_SSLMODE: "verify-full",
THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session-ca.pem",
};
// Catches a server deployment that lets an ordinary authenticated user inspect or mutate
// installation-wide Pi configuration without the trusted upstream admin claim.
test("exposed upstream deployments reject Pi Management without a trusted admin identity", async () => {
const service = fakeService();
const app = appWith(service, exposedServerEnv);
try {
const response = await app.inject({
method: "GET", url: "/pi-management/status",
headers: { ...adminHeaders, "x-thoth-is-admin": "0" },
});
expect(response.statusCode).toBe(403);
expect(response.json()).toEqual({ code: "pi_management_forbidden", error: "Pi management is not permitted" });
expect(service.status).not.toHaveBeenCalled();
} finally {
await app.close();
}
});
// Catches an accidental privilege regression that blocks safe loopback-only installations or
// returns fields beyond the sanctioned Pi Management status contract.
test("loopback-only AUTH_MODE=none may read the sanitized Pi status", async () => {
const app = appWith(fakeService());
try {
const response = await app.inject({ method: "GET", url: "/pi-management/status" });
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
version: "0.80.3", ready: true,
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:00:00.000Z",
});
} finally {
await app.close();
}
});
// Catches route wiring that bypasses closed service validation or gives the browser a Docker/image
// lifecycle endpoint rather than only installation-default configuration and diagnostics.
test("trusted admins receive only configuration, smoke, options, and log endpoints", async () => {
const service = fakeService();
const app = appWith(service, exposedServerEnv);
try {
const options = await app.inject({ method: "GET", url: "/pi-management/options", headers: adminHeaders });
const configured = await app.inject({
method: "PUT", url: "/pi-management/config", headers: adminHeaders,
payload: { provider: "zai", model: "glm-5.2", reasoning: "high" },
});
const smoke = await app.inject({ method: "POST", url: "/pi-management/test", headers: adminHeaders });
const logs = await app.inject({ method: "GET", url: "/pi-management/logs", headers: adminHeaders });
expect(options.statusCode).toBe(200);
expect(configured.statusCode).toBe(200);
expect(configured.json()).toMatchObject({ provider: "zai", model: "glm-5.2", reasoning: "high" });
expect(smoke.statusCode).toBe(200);
expect(logs.statusCode).toBe(200);
expect(app.printRoutes()).not.toContain("update");
expect(app.printRoutes()).not.toContain("rollback");
} finally {
await app.close();
}
});