feat: unify installation model catalog

This commit is contained in:
Codex
2026-09-02 18:45:33 +02:00
parent ae053961a3
commit 7b7927bfe5
169 changed files with 3696 additions and 4572 deletions
-14
View File
@@ -3,10 +3,8 @@ import { server } from "../test/msw";
import {
asPiManagementApiError,
getPiManagementLogs,
getPiManagementOptions,
getPiManagementStatus,
runPiManagementTest,
savePiManagementConfig,
} from "./pi-management";
test("Pi management client calls only the sanctioned sanitized endpoints", async () => {
@@ -16,14 +14,6 @@ test("Pi management client calls only the sanctioned sanitized endpoints", async
calls.push({ method: request.method, path: new URL(request.url).pathname });
return HttpResponse.json({ ready: true, version: "0.80.3", credentials: "present", config: {}, checkedAt: "2026-08-05T10:00:00.000Z" });
}),
http.get("/api/pi-management/options", ({ request }) => {
calls.push({ method: request.method, path: new URL(request.url).pathname });
return HttpResponse.json({ providers: ["zai"], models: [{ provider: "zai", id: "glm-5.2" }], reasoning: ["low", "medium", "high"], checkedAt: "2026-08-05T10:00:00.000Z" });
}),
http.put("/api/pi-management/config", async ({ request }) => {
calls.push({ method: request.method, path: new URL(request.url).pathname, body: await request.json() });
return HttpResponse.json({ provider: "zai", model: "glm-5.2", reasoning: "high", updatedAt: "2026-08-05T10:00:00.000Z" });
}),
http.post("/api/pi-management/test", ({ request }) => {
calls.push({ method: request.method, path: new URL(request.url).pathname });
return HttpResponse.json({ ready: true, checkedAt: "2026-08-05T10:00:00.000Z" });
@@ -35,15 +25,11 @@ test("Pi management client calls only the sanctioned sanitized endpoints", async
);
await expect(getPiManagementStatus()).resolves.toMatchObject({ version: "0.80.3", ready: true, credentials: "present" });
await expect(getPiManagementOptions()).resolves.toMatchObject({ providers: ["zai"] });
await expect(savePiManagementConfig({ provider: "zai", model: "glm-5.2", reasoning: "high" })).resolves.toMatchObject({ reasoning: "high" });
await expect(runPiManagementTest()).resolves.toMatchObject({ ready: true });
await expect(getPiManagementLogs()).resolves.toMatchObject({ lines: ["Pi smoke check succeeded"] });
expect(calls).toEqual([
{ method: "GET", path: "/api/pi-management/status" },
{ method: "GET", path: "/api/pi-management/options" },
{ method: "PUT", path: "/api/pi-management/config", body: { provider: "zai", model: "glm-5.2", reasoning: "high" } },
{ method: "POST", path: "/api/pi-management/test" },
{ method: "GET", path: "/api/pi-management/logs" },
]);
+1 -17
View File
@@ -17,13 +17,6 @@ export interface PiManagementStatus {
message?: string;
}
export interface PiManagementOptions {
providers: string[];
models: Array<{ provider: string; id: string }>;
reasoning: PiReasoning[];
checkedAt: string;
}
export interface PiManagementTestResult {
ready: boolean;
checkedAt: string;
@@ -37,9 +30,7 @@ export interface PiManagementLogs {
export type PiManagementApiErrorCode =
| "pi_management_forbidden"
| "pi_management_unavailable"
| "pi_management_invalid_config"
| "pi_management_write_failed";
| "pi_management_unavailable";
export interface PiManagementApiError {
status: number;
@@ -50,8 +41,6 @@ export interface PiManagementApiError {
const errorCodes = new Set<PiManagementApiErrorCode>([
"pi_management_forbidden",
"pi_management_unavailable",
"pi_management_invalid_config",
"pi_management_write_failed",
]);
/** Narrows the sanctioned error code and derives its message locally. */
@@ -65,11 +54,6 @@ export function asPiManagementApiError(error: unknown): PiManagementApiError | u
}
export const getPiManagementStatus = () => apiFetch<PiManagementStatus>("/pi-management/status");
export const getPiManagementOptions = () => apiFetch<PiManagementOptions>("/pi-management/options");
export const savePiManagementConfig = (config: PiInstallationConfig) =>
apiFetch<PiInstallationConfig & { updatedAt: string }>("/pi-management/config", {
method: "PUT", body: JSON.stringify(config),
});
export const runPiManagementTest = () =>
apiFetch<PiManagementTestResult>("/pi-management/test", { method: "POST" });
export const getPiManagementLogs = () => apiFetch<PiManagementLogs>("/pi-management/logs");
+2 -57
View File
@@ -6,7 +6,7 @@ import {
type AuthOperationPrecondition,
} from "../auth/authOperation";
import { getSettings } from "./settings";
import { getWorkspace, listWorkspaces } from "./workspaces";
import { listWorkspaces } from "./workspaces";
import {
WORKSPACE_POLICY_ERROR, WORKSPACE_SUMMARY_ERROR, WorkspaceSelectionError, workspacePolicyGate,
workspacePreferences, type WorkspacePreference,
@@ -34,24 +34,6 @@ async function selectedPreferences(precondition?: AuthOperationPrecondition): Pr
});
}
function reconcileWorkspacePolicy(preferences: WorkspacePreference, allowed: readonly string[], defaultModel?: string) {
if (allowed.length === 0) throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR);
const selected = preferences.provider && preferences.model
? `${preferences.provider}/${preferences.model}`
: undefined;
if (selected && allowed.includes(selected)) return preferences;
const replacement = defaultModel && allowed.includes(defaultModel) ? defaultModel : allowed[0];
const separator = replacement.indexOf("/");
if (separator <= 0 || separator === replacement.length - 1) {
throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR);
}
return workspacePreferences.save({
...preferences,
provider: replacement.slice(0, separator),
model: replacement.slice(separator + 1),
});
}
async function ensureWorkspaceSelectionPolicy(precondition?: AuthOperationPrecondition): Promise<WorkspacePreference> {
while (true) {
requireAuthOperationPrecondition(precondition);
@@ -104,45 +86,8 @@ async function ensureWorkspaceSelectionPolicy(precondition?: AuthOperationPrecon
throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR);
}
workspacePolicyGate.select(workspaceId);
const outcome = await Promise.race([
getWorkspace(workspaceId).then(
(record) => ({ kind: "record" as const, record }),
() => ({ kind: "error" as const }),
),
workspacePolicyGate.waitForCurrent(() => workspacePreferences.load()).then(
(selection) => ({ kind: "selection" as const, selection }),
),
]);
requireAuthOperationPrecondition(precondition);
if (outcome.kind === "selection") {
if (outcome.selection.workspaceId !== workspaceId) continue;
return outcome.selection;
}
if (outcome.kind === "error") {
requireAuthOperationPrecondition(precondition);
if (workspacePreferences.load().workspaceId !== workspaceId) continue;
workspacePolicyGate.reject(workspaceId);
throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR);
}
const { record } = outcome;
requireAuthOperationPrecondition(precondition);
if (workspacePreferences.load().workspaceId !== workspaceId) continue;
let selection: WorkspacePreference;
try {
selection = reconcileWorkspacePolicy(
preferences,
record.workspace.llm_policy.allowed,
record.workspace.llm_policy.default,
);
} catch (error) {
requireAuthOperationPrecondition(precondition);
workspacePolicyGate.reject(workspaceId);
throw error;
}
requireAuthOperationPrecondition(precondition);
workspacePolicyGate.resolve(workspaceId);
requireAuthOperationPrecondition(precondition);
if (workspacePreferences.load().workspaceId === workspaceId) return selection;
return workspacePreferences.load();
}
}
+1 -15
View File
@@ -62,7 +62,7 @@ export interface WorkspaceEvidence {
export interface CanonicalWorkspace {
workspace: {
schema_version: 3;
schema_version: 4;
id: string;
name: string;
description?: string;
@@ -76,20 +76,6 @@ export interface CanonicalWorkspace {
timeout_ms?: number;
supported_transports: ("postgres_direct" | "rest_api" | "ssh_tunnel")[];
};
semantic_index: {
vector_store: {
engine: "qdrant";
collection: string;
dimensions: 1024;
distance: "cosine";
}
embedding: {
provider: "ollama_internal";
model: "qwen3-embedding:0.6b";
dimensions: 1024;
};
};
llm_policy: { default?: `${string}/${string}`; allowed: `${string}/${string}`[] };
diagnostics?: CanonicalDiagnostics;
evidence?: WorkspaceEvidence;
}
+74 -331
View File
@@ -13,111 +13,42 @@ const readyStatus = {
checkedAt: "2026-08-05T10:00:00.000Z",
};
const options = {
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",
};
function renderManagement() {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(<QueryClientProvider client={client}><PiManagement open onClose={() => undefined} /></QueryClientProvider>);
return render(
<QueryClientProvider client={client}>
<PiManagement open onClose={() => undefined} />
</QueryClientProvider>,
);
}
beforeEach(() => {
server.use(
http.get("/api/pi-management/status", () => HttpResponse.json(readyStatus)),
http.get("/api/pi-management/options", () => HttpResponse.json(options)),
);
server.use(http.get("/api/pi-management/status", () => HttpResponse.json(readyStatus)));
});
test("loads Pi version and readiness as an accessible operational rail", async () => {
test("shows the catalog default as read-only installation state", async () => {
renderManagement();
expect(screen.getByText("Loading Pi management…")).toBeVisible();
expect(await screen.findByRole("heading", { name: "Pi management" })).toBeVisible();
await screen.findByLabelText("Provider");
const dialog = screen.getByRole("dialog", { name: "Pi management" });
expect(dialog).toHaveAttribute("aria-modal", "false");
expect(dialog).toHaveAttribute("data-work-area-panel");
expect(dialog.querySelectorAll(':scope > [data-work-area-panel-region="header"]')).toHaveLength(1);
expect(dialog.querySelectorAll(':scope > [data-work-area-panel-region="body"]')).toHaveLength(1);
expect(await screen.findByText("Pi 0.80.3")).toBeVisible();
expect(screen.getByTestId("pi-readiness-rail")).toHaveTextContent("Runtime ready");
expect(screen.getByRole("status", { name: "Pi readiness" })).toHaveTextContent("Ready");
expect(screen.getByText("Pi 0.80.3")).toBeVisible();
expect(screen.getByText("Defaults ready")).toBeVisible();
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeVisible();
});
test("uses closed provider, model, and reasoning choices without a secret field or terminal", async () => {
renderManagement();
const provider = await screen.findByLabelText("Provider");
expect(provider).toHaveValue("zai");
expect(screen.getByLabelText("Model")).toHaveValue("glm-5.2");
expect(screen.getByLabelText("Reasoning level")).toHaveValue("medium");
expect(within(provider).getAllByRole("option").map((option) => option.textContent)).toEqual(["zai", "deepseek"]);
expect(screen.getByLabelText("Model")).toHaveTextContent("GLM 5.2");
expect(screen.queryByRole("textbox", { name: /provider|model|reasoning|credential/i })).not.toBeInTheDocument();
const catalog = screen.getByRole("region", { name: "Installation catalog default" });
expect(catalog).toHaveTextContent("zai");
expect(catalog).toHaveTextContent("glm-5.2");
expect(catalog).toHaveTextContent("medium");
expect(screen.getByRole("button", { name: "Test catalog default" })).toBeVisible();
expect(screen.queryByRole("button", { name: /save defaults/i })).not.toBeInTheDocument();
expect(screen.queryByRole("combobox", { name: /provider|model|reasoning/i })).not.toBeInTheDocument();
expect(document.querySelector('input[type="password"]')).toBeNull();
expect(screen.queryByRole("button", { name: /terminal|shell access/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Update Pi" })).not.toBeInTheDocument();
});
test("saves only a selected non-secret configuration", async () => {
const user = userEvent.setup();
let saved: unknown;
server.use(http.put("/api/pi-management/config", async ({ request }) => {
saved = await request.json();
return HttpResponse.json({ ...saved as object, updatedAt: "2026-08-05T10:02:00.000Z" });
}));
renderManagement();
await user.selectOptions(await screen.findByLabelText("Provider"), "deepseek");
await user.selectOptions(screen.getByLabelText("Reasoning level"), "high");
await user.click(screen.getByRole("button", { name: "Save defaults" }));
await waitFor(() => expect(saved).toEqual({ provider: "deepseek", model: "deepseek-v4", reasoning: "high" }));
expect(screen.getByRole("status", { name: "Pi management feedback" })).toHaveTextContent("Defaults saved");
});
// Catches a provider switch updating only the saved config while leaving the credential rail
// attached to the previously selected provider.
test("shows authoritative credential presence after saving a different provider", async () => {
const user = userEvent.setup();
let saved = false;
server.use(
http.get("/api/pi-management/status", () => HttpResponse.json(saved ? {
...readyStatus,
credentials: "missing",
config: { provider: "deepseek", model: "deepseek-v4", reasoning: "medium" },
checkedAt: "2026-08-05T10:02:00.000Z",
} : readyStatus)),
http.put("/api/pi-management/config", async ({ request }) => {
saved = true;
return HttpResponse.json({
...await request.json() as object,
updatedAt: "2026-08-05T10:01:00.000Z",
});
}),
);
renderManagement();
expect(await screen.findByText("Credentials present")).toBeVisible();
await user.selectOptions(screen.getByLabelText("Provider"), "deepseek");
await user.click(screen.getByRole("button", { name: "Save defaults" }));
expect(await screen.findByText("Credentials missing")).toBeVisible();
expect(screen.queryByText("Credentials present")).not.toBeInTheDocument();
expect(screen.getByLabelText("Provider")).toHaveValue("deepseek");
expect(screen.getByRole("status", { name: "Pi management feedback" })).toHaveTextContent("Defaults saved");
});
test("runs the saved-configuration test without changing credential presence", async () => {
test("tests the catalog default without mutating credential presence", async () => {
const user = userEvent.setup();
let tests = 0;
server.use(http.post("/api/pi-management/test", () => {
@@ -128,204 +59,95 @@ test("runs the saved-configuration test without changing credential presence", a
}));
renderManagement();
const testButton = await screen.findByRole("button", { name: "Test saved defaults" });
expect(screen.getByText("Defaults ready")).toBeVisible();
expect(screen.queryByText("Changes are not saved yet.")).not.toBeInTheDocument();
expect(testButton).toBeEnabled();
const testButton = await screen.findByRole("button", { name: "Test catalog default" });
await user.click(testButton);
await waitFor(() => expect(tests).toBe(1));
expect(await screen.findByText("Saved configuration test passed")).toBeVisible();
expect(await screen.findByText("Catalog default test passed.")).toBeVisible();
expect(screen.getByText("Credentials present")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Test saved defaults" }));
expect(await screen.findByText("Saved configuration test failed")).toBeVisible();
await user.click(testButton);
expect(await screen.findByText(/Catalog default test failed/)).toBeVisible();
expect(screen.getByText("Credentials present")).toBeVisible();
});
test("fetches and displays bounded sanitized diagnostic logs only on request", async () => {
test("fetches bounded sanitized diagnostics only on request", async () => {
const user = userEvent.setup();
let logRequests = 0;
server.use(http.get("/api/pi-management/logs", () => {
logRequests += 1;
return HttpResponse.json({ lines: ["Pi smoke check succeeded", "provider token=[REDACTED]"], checkedAt: "2026-08-05T10:04:00.000Z" });
return HttpResponse.json({
lines: ["Pi smoke check succeeded", "provider token=[REDACTED]"],
checkedAt: "2026-08-05T10:04:00.000Z",
});
}));
renderManagement();
await screen.findByLabelText("Provider");
await screen.findByText("Pi 0.80.3");
expect(logRequests).toBe(0);
await user.click(screen.getByRole("button", { name: "Show sanitized logs" }));
expect(await screen.findByLabelText("Sanitized Pi diagnostics")).toHaveTextContent("provider token=[REDACTED]");
expect(logRequests).toBe(1);
expect(screen.queryByText("raw-provider-token")).not.toBeInTheDocument();
});
test("explains forbidden management access without offering mutation controls", async () => {
server.use(
http.get("/api/pi-management/status", () =>
HttpResponse.json({ code: "pi_management_forbidden", error: "Pi management is not permitted" }, { status: 403 })),
);
test("explains forbidden management access without offering controls", async () => {
server.use(http.get("/api/pi-management/status", () =>
HttpResponse.json(
{ code: "pi_management_forbidden", error: "Pi management is not permitted" },
{ status: 403 },
)));
renderManagement();
expect(await screen.findByRole("alert", { name: "Pi management unavailable" })).toHaveTextContent("Pi management is not permitted");
expect(screen.queryByLabelText("Provider")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Save defaults" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Test saved defaults" })).not.toBeInTheDocument();
expect(await screen.findByRole("alert", { name: "Pi management unavailable" }))
.toHaveTextContent("Pi management is not permitted");
expect(screen.queryByRole("button", { name: "Test catalog default" })).not.toBeInTheDocument();
});
test("shows a seven-step host-terminal workflow in scrollable platform tabs", async () => {
test("shows the single-source host workflow in accessible platform tabs", async () => {
const user = userEvent.setup();
renderManagement();
const tablist = await screen.findByRole("tablist", { name: "Pi host platform" });
const [linuxTab, macosTab, windowsTab] = within(tablist).getAllByRole("tab");
expect([linuxTab, macosTab, windowsTab].map((tab) => tab.textContent)).toEqual(["Linux", "macOS", "Windows"]);
expect(screen.getByRole("dialog", { name: "Pi management" })).toHaveAttribute("data-width", "standard");
expect(screen.getByTestId("pi-platform-instructions-scroll")).toHaveClass("overflow-y-scroll");
expect(screen.queryByRole("tabpanel")).not.toBeInTheDocument();
expect([linuxTab, macosTab, windowsTab].every((tab) => tab.getAttribute("aria-selected") === "false")).toBe(true);
expect(linuxTab).toHaveAttribute("tabindex", "0");
expect(macosTab).toHaveAttribute("tabindex", "-1");
expect(windowsTab).toHaveAttribute("tabindex", "-1");
for (const tab of [linuxTab, macosTab, windowsTab]) {
const panelId = tab.getAttribute("aria-controls");
const panel = panelId ? document.getElementById(panelId) : null;
expect(panel).toBeInTheDocument();
expect(panel).toHaveAttribute("role", "tabpanel");
expect(panel).toHaveAttribute("aria-labelledby", tab.id);
expect(panel).toHaveAttribute("hidden");
expect(panel).toHaveAttribute("tabindex", "-1");
}
expect(screen.getByText("Using the host terminal:")).toBeVisible();
expect(screen.queryByText(/not this browser page/i)).not.toBeInTheDocument();
expect(screen.queryByRole("region", { name: "Host update" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Copy update command" })).not.toBeInTheDocument();
expect(screen.queryByText(/:5173/)).not.toBeInTheDocument();
expect(await screen.findByText("Select the provider, model, and reasoning used for new Pi work. Credentials stay in protected host files.")).toBeVisible();
expect(screen.getByText("Shows at most 200 recent lines with declared secret values removed.")).toBeVisible();
linuxTab.focus();
await user.keyboard("{ArrowRight}");
expect(macosTab).toHaveFocus();
expect(macosTab).toHaveAttribute("aria-selected", "true");
expect(macosTab).toHaveAttribute("tabindex", "0");
expect(linuxTab).toHaveAttribute("tabindex", "-1");
await user.keyboard("{ArrowRight}");
expect(windowsTab).toHaveFocus();
expect(windowsTab).toHaveAttribute("aria-selected", "true");
expect(windowsTab).toHaveAttribute("tabindex", "0");
expect(macosTab).toHaveAttribute("tabindex", "-1");
await user.keyboard("{ArrowRight}");
expect(linuxTab).toHaveFocus();
expect(linuxTab).toHaveAttribute("aria-selected", "true");
await user.keyboard("{ArrowLeft}");
expect(windowsTab).toHaveFocus();
await user.keyboard("{Home}");
expect(linuxTab).toHaveFocus();
await user.keyboard("{End}");
expect(windowsTab).toHaveFocus();
await user.click(windowsTab);
expect(windowsTab).toHaveAttribute("aria-selected", "false");
expect(screen.queryByRole("tabpanel")).not.toBeInTheDocument();
await user.keyboard(" ");
expect(windowsTab).toHaveAttribute("aria-selected", "true");
await user.keyboard(" ");
expect(windowsTab).toHaveAttribute("aria-selected", "false");
await user.keyboard("{Enter}");
expect(windowsTab).toHaveAttribute("aria-selected", "true");
await user.keyboard("{Enter}");
expect(windowsTab).toHaveAttribute("aria-selected", "false");
await user.click(linuxTab);
const linux = screen.getByRole("tabpanel", { name: "Linux" });
expect(within(linux).getAllByRole("listitem")).toHaveLength(7);
expect(within(linux).getAllByRole("heading", { level: 4 }).map((heading) => heading.textContent)).toEqual([
"Open the project root",
"Edit the provider catalog",
"Enable the model",
"Edit the Installation Model Catalog",
"Choose eligibility and defaults",
"Complete the provider setup",
"Reload Pi configuration",
"Update the Pi version",
"Diagnose and recover a failed operation",
]);
expect(linux).toHaveTextContent("cd /absolute/path/to/ThothII");
expect(linux).toHaveTextContent("tht pi status");
expect(linux).toHaveTextContent("tht pi doctor");
expect(linux).toHaveTextContent("Invalid JSON");
expect(linux).toHaveTextContent("Provider connectivity");
expect(linux).toHaveTextContent("If maintenance is inactive");
expect(linux).toHaveTextContent("active after an update");
expect(linux).toHaveTextContent("active after a restart");
expect(linux).toHaveTextContent("run every command in this list from that directory");
expect(linux).toHaveTextContent("deploy/pi/models.json");
expect(linux).toHaveTextContent("deploy/pi/settings.json");
expect(linux).toHaveTextContent("baseUrl is the provider API endpoint");
expect(linux).toHaveTextContent("enabledModels uses provider/model identifiers");
expect(linux).toHaveTextContent("During the initial installation");
expect(linux).toHaveTextContent("Pi credentials file location");
expect(linux).toHaveTextContent("tht pi restart --yes --drain");
expect(linux).toHaveTextContent("tht pi update");
expect(linux).toHaveTextContent("tht pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
expect(linux).toHaveTextContent("tht pi maintenance status");
expect(linux).toHaveTextContent("tht pi logs");
expect(linux).toHaveTextContent("tht pi rollback --yes");
expect(linux).toHaveTextContent("tht pi maintenance recover --yes");
expect(linux).not.toHaveTextContent("PI_AUTH_FILE");
expect(linux).not.toHaveTextContent("thothctl");
expect(linux).not.toHaveTextContent("~/.pi/agent/");
for (const expected of [
"deploy/<installation-id>/thothii-installation.yaml",
"modelCatalog.defaults.session",
"provider/model",
"generated projections",
"tht pi restart --yes --drain",
"tht pi doctor",
"Invalid YAML",
]) expect(linux).toHaveTextContent(expected);
for (const retired of ["deploy/pi/models.json", "deploy/pi/settings.json", "tht pi configure", "enabledModels"])
expect(linux).not.toHaveTextContent(retired);
await user.click(macosTab);
const macos = screen.getByRole("tabpanel", { name: "macOS" });
expect(within(macos).getAllByRole("listitem")).toHaveLength(7);
expect(within(macos).getAllByRole("heading", { level: 4 }).map((heading) => heading.textContent)).toEqual([
"Open the project root",
"Edit the provider catalog",
"Enable the model",
"Complete the provider setup",
"Reload Pi configuration",
"Update the Pi version",
"Diagnose and recover a failed operation",
]);
expect(macos).toHaveTextContent("cd /absolute/path/to/ThothII");
expect(macos).toHaveTextContent("run every command in this list from that directory");
expect(macos).toHaveTextContent("deploy/pi/models.json");
expect(macos).toHaveTextContent("deploy/pi/settings.json");
expect(macos).toHaveTextContent("tht pi restart --yes --drain");
expect(macos).toHaveTextContent("tht pi update");
expect(macos).toHaveTextContent("tht pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
expect(macos).toHaveTextContent("tht pi maintenance status");
expect(macos).toHaveTextContent("tht pi logs");
expect(macos).toHaveTextContent("tht pi rollback --yes");
expect(macos).toHaveTextContent("tht pi maintenance recover --yes");
expect(screen.getByRole("tabpanel", { name: "macOS" }))
.toHaveTextContent("deploy/<installation-id>/thothii-installation.yaml");
await user.click(windowsTab);
const windows = screen.getByRole("tabpanel", { name: "Windows" });
expect(within(windows).getAllByRole("listitem")).toHaveLength(7);
expect(within(windows).getAllByRole("heading", { level: 4 }).map((heading) => heading.textContent)).toEqual([
"Open the project root",
"Edit the provider catalog",
"Enable the model",
"Complete the provider setup",
"Reload Pi configuration",
"Update the Pi version",
"Diagnose and recover a failed operation",
]);
expect(windows).toHaveTextContent("Set-Location C:\\absolute\\path\\to\\ThothII");
expect(windows).toHaveTextContent("run every command in this list from that directory");
expect(windows).toHaveTextContent("deploy\\pi\\models.json");
expect(windows).toHaveTextContent("deploy\\pi\\settings.json");
expect(windows).toHaveTextContent('tht pi restart --yes --drain');
expect(windows).toHaveTextContent('tht pi update');
expect(windows).toHaveTextContent('tht pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain');
expect(windows).toHaveTextContent('tht pi maintenance status');
expect(windows).toHaveTextContent('tht pi logs');
expect(windows).toHaveTextContent('tht pi rollback --yes');
expect(windows).toHaveTextContent('tht pi maintenance recover --yes');
expect(windows).not.toHaveTextContent("PI_AUTH_FILE");
expect(windows).not.toHaveTextContent("thothctl");
expect(screen.getByRole("tabpanel", { name: "Windows" }))
.toHaveTextContent("deploy\\<installation-id>\\thothii-installation.yaml");
});
test("reloads installation defaults when the panel is reopened", async () => {
test("reloads the catalog-derived default when reopened", async () => {
let statusCalls = 0;
server.use(http.get("/api/pi-management/status", () => {
statusCalls += 1;
@@ -334,70 +156,32 @@ test("reloads installation defaults when the panel is reopened", async () => {
: { ...readyStatus, config: { provider: "deepseek", model: "deepseek-v4", reasoning: "high" } });
}));
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = render(<QueryClientProvider client={client}><PiManagement open onClose={() => undefined} /></QueryClientProvider>);
const view = render(
<QueryClientProvider client={client}><PiManagement open onClose={() => undefined} /></QueryClientProvider>,
);
expect(await screen.findByLabelText("Provider")).toHaveValue("zai");
view.rerender(<QueryClientProvider client={client}><PiManagement open={false} onClose={() => undefined} /></QueryClientProvider>);
expect(await screen.findByText("glm-5.2")).toBeVisible();
view.rerender(
<QueryClientProvider client={client}><PiManagement open={false} onClose={() => undefined} /></QueryClientProvider>,
);
await waitFor(() => expect(screen.queryByRole("dialog", { name: "Pi management" })).not.toBeInTheDocument());
view.rerender(<QueryClientProvider client={client}><PiManagement open onClose={() => undefined} /></QueryClientProvider>);
await waitFor(() => expect(screen.getByLabelText("Provider")).toHaveValue("deepseek"));
view.rerender(
<QueryClientProvider client={client}><PiManagement open onClose={() => undefined} /></QueryClientProvider>,
);
expect(await screen.findByText("deepseek-v4")).toBeVisible();
});
test("shows an explicit recoverable incomplete state when no provider model is available", async () => {
server.use(
http.get("/api/pi-management/status", () => HttpResponse.json({ ...readyStatus, credentials: "present" })),
http.get("/api/pi-management/options", () => HttpResponse.json({
...options,
providers: ["zai"],
models: [],
})),
);
test("shows an explicit incomplete state when the catalog default is unavailable", async () => {
server.use(http.get("/api/pi-management/status", () => HttpResponse.json({
...readyStatus,
config: { reasoning: "medium" },
})));
renderManagement();
const incomplete = await screen.findByRole("alert", { name: "Pi configuration incomplete" });
expect(incomplete).toHaveTextContent("No provider or model options are available");
expect(incomplete).toHaveTextContent("Check the host-managed Pi model configuration");
expect(screen.queryByText("Loading Pi management…")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Save defaults" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeDisabled();
});
test("keeps suggested draft choices distinct from invalid persisted defaults until save succeeds", async () => {
let configured = false;
const persisted = {
...readyStatus,
credentials: "missing",
config: { provider: "retired", model: "old-model", reasoning: "medium" },
};
server.use(
http.get("/api/pi-management/status", () => HttpResponse.json(configured ? {
...readyStatus,
credentials: "missing",
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:05:00.000Z",
} : persisted)),
http.put("/api/pi-management/config", () => {
configured = true;
return HttpResponse.json({
provider: "zai",
model: "glm-5.2",
reasoning: "medium",
updatedAt: "2026-08-05T10:05:00.000Z",
});
}),
);
const user = userEvent.setup();
renderManagement();
expect(await screen.findByLabelText("Provider")).toHaveValue("zai");
expect(screen.getByText("Defaults incomplete")).toBeVisible();
expect(screen.getByText("Suggested choices are not saved yet.")).toBeVisible();
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeDisabled();
await user.click(screen.getByRole("button", { name: "Save defaults" }));
expect(await screen.findByText("Defaults ready")).toBeVisible();
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeEnabled();
expect(incomplete).toHaveTextContent("The catalog default is unavailable");
expect(incomplete).toHaveTextContent("modelCatalog.defaults.session");
expect(screen.getByRole("button", { name: "Test catalog default" })).toBeDisabled();
});
test.each(["present", "missing"] as const)(
@@ -408,47 +192,6 @@ test.each(["present", "missing"] as const)(
credentials,
})));
renderManagement();
expect(await screen.findByText(`Credentials ${credentials}`)).toBeVisible();
},
);
test("labels smoke only as a saved-configuration test and resets it when the draft changes", async () => {
server.use(
http.get("/api/pi-management/status", () => HttpResponse.json({ ...readyStatus, credentials: "present" })),
http.post("/api/pi-management/test", () => HttpResponse.json({
ready: true,
checkedAt: "2026-08-05T10:06:00.000Z",
})),
);
const user = userEvent.setup();
renderManagement();
await user.click(await screen.findByRole("button", { name: "Test saved defaults" }));
expect(await screen.findByText("Saved configuration test passed")).toBeVisible();
expect(screen.getByText("Credentials present")).toBeVisible();
await user.selectOptions(screen.getByLabelText("Provider"), "deepseek");
expect(screen.getByText("Saved configuration test not run")).toBeVisible();
expect(screen.getByText("Save these changes before testing. The test always uses saved defaults.")).toBeVisible();
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeDisabled();
expect(screen.getByText("Credentials present")).toBeVisible();
});
test("a failed saved-configuration test does not change backend credential presence", async () => {
server.use(
http.get("/api/pi-management/status", () => HttpResponse.json({ ...readyStatus, credentials: "present" })),
http.post("/api/pi-management/test", () => HttpResponse.json({
ready: false,
message: "Pi runtime is unavailable",
checkedAt: "2026-08-05T10:07:00.000Z",
})),
);
const user = userEvent.setup();
renderManagement();
await user.click(await screen.findByRole("button", { name: "Test saved defaults" }));
expect(await screen.findByText("Saved configuration test failed")).toBeVisible();
expect(screen.getByText("Credentials present")).toBeVisible();
expect(screen.queryByText("Credentials missing")).not.toBeInTheDocument();
});
+53 -169
View File
@@ -1,59 +1,18 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CheckCircle2, CircleAlert, ClipboardCheck, FlaskConical, LoaderCircle, ScrollText } from "lucide-react";
import { CheckCircle2, CircleAlert, FlaskConical, LoaderCircle, ScrollText } from "lucide-react";
import {
asPiManagementApiError,
getPiManagementLogs,
getPiManagementOptions,
getPiManagementStatus,
runPiManagementTest,
savePiManagementConfig,
type PiInstallationConfig,
type PiManagementOptions,
} from "../api/pi-management";
import { Button } from "../components/ui/button";
import { WorkAreaPanel } from "./WorkAreaPanel";
const fieldClass = "h-9 w-full rounded-md border border-input bg-background px-2.5 text-sm shadow-xs outline-none focus-visible:ring-3 focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-60";
type Feedback = { tone: "success" | "error"; message: string } | undefined;
type SmokeState = "passed" | "failed" | undefined;
function suggestedConfig(status: { config: Partial<PiInstallationConfig> }, options: PiManagementOptions): PiInstallationConfig | undefined {
const provider = status.config.provider && options.providers.includes(status.config.provider)
? status.config.provider
: options.providers[0];
const model = status.config.model && options.models.some((item) => item.provider === provider && item.id === status.config.model)
? status.config.model
: options.models.find((item) => item.provider === provider)?.id;
const reasoning = status.config.reasoning && options.reasoning.includes(status.config.reasoning)
? status.config.reasoning
: options.reasoning[0];
return provider && model && reasoning ? { provider, model, reasoning } : undefined;
}
function isSupportedConfig(
config: Partial<PiInstallationConfig> | undefined,
options: PiManagementOptions | undefined,
): config is PiInstallationConfig {
return Boolean(
config?.provider
&& config.model
&& config.reasoning
&& options?.providers.includes(config.provider)
&& options.models.some((model) => model.provider === config.provider && model.id === config.model)
&& options.reasoning.includes(config.reasoning),
);
}
function sameConfig(a: Partial<PiInstallationConfig> | undefined, b: PiInstallationConfig | undefined): boolean {
return Boolean(
a?.provider === b?.provider
&& a?.model === b?.model
&& a?.reasoning === b?.reasoning,
);
}
function errorMessage(error: unknown, fallback: string): string {
return asPiManagementApiError(error)?.message ?? fallback;
}
@@ -97,15 +56,10 @@ function RailItem({ label, value, state }: { label: string; value: string; state
</div>;
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return <label className="grid gap-1.5 text-xs font-semibold text-foreground"><span>{label}</span>{children}</label>;
}
type PiPlatform = "linux" | "macos" | "windows";
type PiPlatformDetails = {
modelsPath: string;
settingsPath: string;
catalogPath: string;
terminal: string;
changeDirectoryCommand: string;
credentialProtection: string;
@@ -123,8 +77,7 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
id: "linux",
label: "Linux",
details: {
modelsPath: "deploy/pi/models.json",
settingsPath: "deploy/pi/settings.json",
catalogPath: "deploy/<installation-id>/thothii-installation.yaml",
terminal: "a terminal",
changeDirectoryCommand: "cd /absolute/path/to/ThothII",
credentialProtection: "a protected host file with mode 0600",
@@ -141,8 +94,7 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
id: "macos",
label: "macOS",
details: {
modelsPath: "deploy/pi/models.json",
settingsPath: "deploy/pi/settings.json",
catalogPath: "deploy/<installation-id>/thothii-installation.yaml",
terminal: "Terminal",
changeDirectoryCommand: "cd /absolute/path/to/ThothII",
credentialProtection: "a protected host file with mode 0600",
@@ -159,8 +111,7 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
id: "windows",
label: "Windows",
details: {
modelsPath: "deploy\\pi\\models.json",
settingsPath: "deploy\\pi\\settings.json",
catalogPath: "deploy\\<installation-id>\\thothii-installation.yaml",
terminal: "PowerShell",
changeDirectoryCommand: "Set-Location C:\\absolute\\path\\to\\ThothII",
credentialProtection: "a protected host file with a user-only ACL",
@@ -189,37 +140,37 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
<PiCodeBlock className="mt-1">project-root/
├── compose.yaml
├── deploy/
│ └── pi/
│ ├── models.json
│ └── settings.json
│ └── &lt;installation-id&gt;/
│ └── thothii-installation.yaml
└── docker/</PiCodeBlock>
<p className="mt-2 text-muted-foreground">The <code>deploy/</code> directory contains the selected installation descriptor and profile files. You do not need to create or manage a <code>bin/</code> directory: <code>tht</code> is the installed host CLI. If discovery finds multiple descriptors, pass the intended one explicitly with <code>--installation &lt;absolute-path&gt;/thothii-installation.yaml</code>. If <code>tht</code> is not on your <code>PATH</code>, install it using the installation guide.</p>
</li>
<li>
<h4 className="font-semibold text-foreground">Edit the provider catalog</h4>
<p className="mt-1 text-muted-foreground">Edit <code>{details.modelsPath}</code> only when adding or correcting a provider/model definition. The provider catalog is an address book/map of the services Pi can call: each entry supplies the API endpoint and format, and lists the model identifiers offered there. It does not enable a model and never contains credentials. Provider integrations must remain declarative; do not add model-provider code under <code>harness/.pi/extensions/</code>.</p>
<h4 className="font-semibold text-foreground">Edit the Installation Model Catalog</h4>
<p className="mt-1 text-muted-foreground">Edit <code>{details.catalogPath}</code> when adding or correcting a provider or model. Its <code>modelCatalog</code> block is the only authored model source for sessions, metadata generation, and embedding. It declares endpoint references and authentication modes, but never secret values. Provider integrations remain declarative; do not add model-provider code under <code>harness/.pi/extensions/</code> or edit files under <code>generated/</code>.</p>
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-2 gap-y-1 text-muted-foreground">
<dt className="font-mono text-foreground">baseUrl </dt><dd>is the provider API endpoint.</dd>
<dt className="font-mono text-foreground">api </dt><dd>selects the provider API format.</dd>
<dt className="font-mono text-foreground">models </dt><dd>lists that provider&apos;s available models.</dd>
<dt className="font-mono text-foreground">id </dt><dd>is the model identifier.</dd>
<dt className="font-mono text-foreground">name </dt><dd>is the model name shown to operators.</dd>
<dt className="font-mono text-foreground">providers </dt><dd>maps stable provider keys to endpoint, authentication, and runtime adapters.</dd>
<dt className="font-mono text-foreground">models </dt><dd>maps upstream model keys; the canonical identity is provider/model.</dd>
<dt className="font-mono text-foreground">session </dt><dd>makes a model selectable for interactive work.</dd>
<dt className="font-mono text-foreground">metadataGeneration </dt><dd>makes a model available to metadata generation.</dd>
<dt className="font-mono text-foreground">embedding </dt><dd>declares the one installation embedding identity and its dimensions.</dd>
</dl>
</li>
<li>
<h4 className="font-semibold text-foreground">Enable the model</h4>
<p className="mt-1 text-muted-foreground">Enable a model after the provider setup is ready. For a custom provider, first define the provider and model ID in <code>{details.modelsPath}</code>; built-in Pi models may already be known without a local catalog entry. Then edit <code>{details.settingsPath}</code> in the project root, add the exact <code>provider/model</code> identifier to <code>enabledModels</code>, and reload Pi. Do not edit it merely to choose the default provider/model: use this page or <code>tht pi configure</code>.</p>
<h4 className="font-semibold text-foreground">Choose eligibility and defaults</h4>
<p className="mt-1 text-muted-foreground">Use <code>modelCatalog.defaults.session</code> as the single default for new sessions. Add a <code>session</code> or <code>metadataGeneration</code> block to make a model eligible for that use. When metadata generation is present, set <code>defaults.metadataGeneration</code>. Runtime files such as Pi <code>models.json</code> and <code>settings.json</code> are generated projections and must not be edited.</p>
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-2 gap-y-1 text-muted-foreground">
<dt className="font-mono text-foreground">enabledModels </dt><dd>uses provider/model identifiers to choose the models available for new Pi work.</dd>
<dt className="font-mono text-foreground">defaults.session </dt><dd>contains exactly one canonical provider/model identity.</dd>
<dt className="font-mono text-foreground">defaults.metadataGeneration </dt><dd>selects the metadata model when that use exists.</dd>
</dl>
</li>
<li>
<h4 className="font-semibold text-foreground">Complete the provider setup</h4>
<p className="mt-1 text-muted-foreground">During the initial installation, run <code>tht setup</code> and complete the prompt named “Pi credentials file location”; it can create the empty protected template. On an existing installation with a missing credential, correct the protected credential file selected during setup by following the installation guide. Keep {details.credentialProtection}, then restart and run <code>tht pi doctor</code> and <code>tht pi test</code>. Never paste credentials into either JSON file, the page, a command, or a log.</p>
<p className="mt-1 text-muted-foreground">During the initial installation, run <code>tht setup</code> and complete the prompt named “Pi credentials file location”; it can create the empty protected template. On an existing installation with a missing credential, correct the protected credential file selected during setup by following the installation guide. Keep {details.credentialProtection}, then restart and run <code>tht pi doctor</code> and <code>tht pi test</code>. Never paste credentials into the installation YAML, this page, a command, or a log: YAML may contain only approved environment-variable names or authentication modes.</p>
</li>
<li>
<h4 className="font-semibold text-foreground">Reload Pi configuration</h4>
<p className="mt-1 text-muted-foreground">After changing <code>models.json</code>, <code>settings.json</code>, or the provider credential, reload the running <code>core</code> service so it reads the new files. This is a configuration reload, not a Pi version update; it keeps the current image.</p>
<p className="mt-1 text-muted-foreground">After changing the Installation Model Catalog or a provider credential, reload the running <code>core</code> service. The command validates the YAML and regenerates every runtime projection before recreation. This is a configuration reload, not a Pi version update; it keeps the current image.</p>
<PiCodeBlock className="mt-2">{details.restartCommand}</PiCodeBlock>
</li>
<li>
@@ -234,9 +185,9 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
<p className="mt-1 text-muted-foreground">Start with these diagnostics, in order:</p>
<PiCodeBlock className="mt-2 text-[11px] text-muted-foreground">{details.diagnosticCommands}</PiCodeBlock>
<dl className="mt-2 grid gap-2 text-muted-foreground">
<dt className="font-semibold text-foreground">Configuration</dt><dd>Invalid JSON: fix the reported file and validate it. A provider/model ID mismatch or a model missing from <code>enabledModels</code>: correct the IDs or policy, then reload.</dd>
<dt className="font-semibold text-foreground">Configuration</dt><dd>Invalid YAML, an unknown model default, or an incompatible use/adapter combination: fix the reported <code>modelCatalog</code> field, then reload. Do not repair generated JSON directly.</dd>
<dt className="font-semibold text-foreground">Credentials</dt><dd>Missing or unreadable credential: correct the protected credential file selected during setup and its permissions, without printing the file.</dd>
<dt className="font-semibold text-foreground">Provider connectivity</dt><dd>Wrong <code>baseUrl</code>, network, or provider error: correct the endpoint or network, then retry.</dd>
<dt className="font-semibold text-foreground">Provider connectivity</dt><dd>Wrong <code>baseUrl</code>, network, or provider error: correct the catalog endpoint or network, then retry.</dd>
<dt className="font-semibold text-foreground">Docker and disk</dt><dd>Unhealthy Docker or insufficient disk: restore Docker health or free space before retrying.</dd>
</dl>
<p className="mt-2 text-muted-foreground">If maintenance is inactive, no recovery is needed: correct the cause and retry the original command.</p>
@@ -325,30 +276,19 @@ function PiPlatformInstructions() {
export function PiManagement({ open, onClose }: { open: boolean; onClose: () => void }) {
const queryClient = useQueryClient();
const [draft, setDraft] = useState<PiInstallationConfig>();
const [feedback, setFeedback] = useState<Feedback>();
const [smokeState, setSmokeState] = useState<SmokeState>();
const [logsRequested, setLogsRequested] = useState(false);
const statusQuery = useQuery({ queryKey: ["pi-management", "status"], queryFn: getPiManagementStatus, enabled: open });
const optionsQuery = useQuery({ queryKey: ["pi-management", "options"], queryFn: getPiManagementOptions, enabled: open });
const logsQuery = useQuery({ queryKey: ["pi-management", "logs"], queryFn: getPiManagementLogs, enabled: open && logsRequested });
const models = useMemo(
() => optionsQuery.data?.models.filter((model) => model.provider === draft?.provider) ?? [],
[draft?.provider, optionsQuery.data?.models],
const catalogReady = Boolean(
statusQuery.data?.config.provider
&& statusQuery.data.config.model
&& statusQuery.data.config.reasoning,
);
const initialDraft = useMemo(
() => statusQuery.data && optionsQuery.data
? suggestedConfig(statusQuery.data, optionsQuery.data)
: undefined,
[optionsQuery.data, statusQuery.data],
);
const persistedReady = isSupportedConfig(statusQuery.data?.config, optionsQuery.data);
const validDraft = isSupportedConfig(draft, optionsQuery.data);
const dirty = Boolean(draft && !sameConfig(statusQuery.data?.config, draft));
useEffect(() => {
if (!open) {
setDraft(undefined);
setFeedback(undefined);
setSmokeState(undefined);
setLogsRequested(false);
@@ -356,22 +296,6 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
}
}, [open, queryClient]);
useEffect(() => {
if (draft || !initialDraft) return;
setDraft(initialDraft);
}, [draft, initialDraft]);
const saveMutation = useMutation({
mutationFn: savePiManagementConfig,
onSuccess: async (saved) => {
const config = { provider: saved.provider, model: saved.model, reasoning: saved.reasoning };
setDraft(config);
setSmokeState(undefined);
await queryClient.invalidateQueries({ queryKey: ["pi-management", "status"] });
setFeedback({ tone: "success", message: "Defaults saved." });
},
onError: (error) => setFeedback({ tone: "error", message: errorMessage(error, "Could not save Pi defaults.") }),
});
const smokeMutation = useMutation({
mutationFn: runPiManagementTest,
onSuccess: (result) => {
@@ -379,41 +303,27 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
setFeedback({
tone: result.ready ? "success" : "error",
message: result.ready
? "Saved configuration test passed."
: `Saved configuration test failed.${result.message ? ` ${result.message}` : ""}`,
? "Catalog default test passed."
: `Catalog default test failed.${result.message ? ` ${result.message}` : ""}`,
});
},
onError: (error) => {
setSmokeState("failed");
setFeedback({ tone: "error", message: errorMessage(error, "Could not test the saved Pi defaults.") });
setFeedback({ tone: "error", message: errorMessage(error, "Could not test the catalog default.") });
},
});
function saveDefaults() {
if (!draft || !validDraft) {
setFeedback({ tone: "error", message: "Choose a supported provider, model, and reasoning level." });
return;
}
saveMutation.mutate(draft);
}
function updateDraft(update: (current: PiInstallationConfig) => PiInstallationConfig) {
setDraft((current) => current ? update(current) : current);
setSmokeState(undefined);
setFeedback(undefined);
}
function testSavedDefaults() {
if (!persistedReady || dirty) {
setFeedback({ tone: "error", message: "Save supported defaults before running the test." });
function testCatalogDefault() {
if (!catalogReady) {
setFeedback({ tone: "error", message: "Fix the Installation Model Catalog before running the test." });
return;
}
smokeMutation.mutate();
}
const forbidden = asPiManagementApiError(statusQuery.error)?.code === "pi_management_forbidden";
const loading = statusQuery.isLoading || optionsQuery.isLoading || Boolean(!draft && initialDraft);
const unavailable = statusQuery.isError || optionsQuery.isError;
const loading = statusQuery.isLoading;
const unavailable = statusQuery.isError;
return (
<WorkAreaPanel
@@ -421,7 +331,7 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
ariaLabel="Pi management"
eyebrow="Installation controls"
title="Pi management"
description="Review the bundled runtime, set safe defaults, and test the saved provider configuration."
description="Review the bundled runtime, inspect the catalog default, and test its provider configuration."
onClose={onClose}
closeLabel="Close Pi management"
>
@@ -430,14 +340,14 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
{forbidden ? (
<div role="alert" aria-label="Pi management unavailable" className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm">
<p className="font-semibold">Pi management is not permitted</p>
<p className="mt-1 text-muted-foreground">Ask an installation administrator to manage Pi defaults and diagnostics.</p>
<p className="mt-1 text-muted-foreground">Ask an installation administrator to manage the model catalog and diagnostics.</p>
</div>
) : unavailable ? (
<div role="alert" aria-label="Pi management unavailable" className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm">
<p className="font-semibold">{errorMessage(statusQuery.error ?? optionsQuery.error, "Pi management is unavailable")}</p>
<Button className="mt-3" size="sm" variant="outline" onClick={() => { void statusQuery.refetch(); void optionsQuery.refetch(); }}>Retry</Button>
<p className="font-semibold">{errorMessage(statusQuery.error, "Pi management is unavailable")}</p>
<Button className="mt-3" size="sm" variant="outline" onClick={() => { void statusQuery.refetch(); }}>Retry</Button>
</div>
) : loading ? <p className="text-sm text-muted-foreground">Loading Pi management…</p> : statusQuery.data && optionsQuery.data && (
) : loading ? <p className="text-sm text-muted-foreground">Loading Pi management…</p> : statusQuery.data && (
<div className="grid gap-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
@@ -451,7 +361,7 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
<ReadinessRail
ready={statusQuery.data.ready}
configured={persistedReady}
configured={catalogReady}
credentials={statusQuery.data.credentials ?? "missing"}
smokeState={smokeState}
/>
@@ -462,56 +372,30 @@ export function PiManagement({ open, onClose }: { open: boolean; onClose: () =>
{feedback.message}
</p>}
<section aria-label="Installation defaults" className="border-t border-border/70 pt-5">
<section aria-label="Installation catalog default" className="border-t border-border/70 pt-5">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<div><h3 className="font-heading text-sm font-semibold">Installation defaults</h3><p className="mt-1 text-sm text-muted-foreground">Select the provider, model, and reasoning used for new Pi work. Credentials stay in protected host files.</p></div>
<div><h3 className="font-heading text-sm font-semibold">Installation catalog default</h3><p className="mt-1 text-sm text-muted-foreground">This value is read from <code>modelCatalog.defaults.session</code>. Change the installation YAML and reload Pi; this page never creates a second model default.</p></div>
</div>
{draft ? (
{catalogReady ? (
<>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<Field label="Provider">
<select aria-label="Provider" className={fieldClass} value={draft.provider} onChange={(event) => {
const provider = event.target.value;
updateDraft((current) => ({
...current,
provider,
model: optionsQuery.data.models.find((model) => model.provider === provider)?.id ?? "",
}));
}}>
{optionsQuery.data.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
</select>
</Field>
<Field label="Model">
<select aria-label="Model" className={fieldClass} value={draft.model} onChange={(event) => updateDraft((current) => ({ ...current, model: event.target.value }))}>
{models.map((model) => <option key={model.id} value={model.id}>{model.id === "glm-5.2" ? "GLM 5.2" : model.id}</option>)}
</select>
</Field>
<Field label="Reasoning level">
<select aria-label="Reasoning level" className={fieldClass} value={draft.reasoning} onChange={(event) => updateDraft((current) => ({ ...current, reasoning: event.target.value as PiInstallationConfig["reasoning"] }))}>
{optionsQuery.data.reasoning.map((reasoning) => <option key={reasoning} value={reasoning}>{reasoning}</option>)}
</select>
</Field>
<div className="mt-4 grid gap-3 text-sm sm:grid-cols-3">
<div><p className="thot-label">Provider</p><p className="mt-1 font-mono font-semibold">{statusQuery.data.config.provider}</p></div>
<div><p className="thot-label">Model</p><p className="mt-1 font-mono font-semibold">{statusQuery.data.config.model}</p></div>
<div><p className="thot-label">Reasoning</p><p className="mt-1 font-mono font-semibold">{statusQuery.data.config.reasoning}</p></div>
</div>
{dirty && <p className="mt-3 text-sm text-amber-800 dark:text-amber-300">
{persistedReady ? "Changes are not saved yet." : "Suggested choices are not saved yet."}
</p>}
<div className="mt-4 flex flex-wrap gap-2">
<Button size="sm" variant="outline" disabled={saveMutation.isPending || !validDraft || !dirty} onClick={saveDefaults}>{saveMutation.isPending ? <LoaderCircle className="animate-spin motion-reduce:animate-none" /> : <ClipboardCheck />}Save defaults</Button>
<Button size="sm" variant="outline" disabled={smokeMutation.isPending || dirty || !persistedReady} onClick={testSavedDefaults}>{smokeMutation.isPending ? <LoaderCircle className="animate-spin motion-reduce:animate-none" /> : <FlaskConical />}Test saved defaults</Button>
<Button size="sm" variant="outline" disabled={smokeMutation.isPending} onClick={testCatalogDefault}>{smokeMutation.isPending ? <LoaderCircle className="animate-spin motion-reduce:animate-none" /> : <FlaskConical />}Test catalog default</Button>
</div>
{dirty && <p className="mt-2 text-xs text-muted-foreground">Save these changes before testing. The test always uses saved defaults.</p>}
{!dirty && !persistedReady && <p className="mt-2 text-xs text-muted-foreground">Save supported defaults before testing. The test always uses saved defaults.</p>}
</>
) : (
<div className="mt-4">
<div role="alert" aria-label="Pi configuration incomplete" className="rounded-md border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-900 dark:text-amber-200">
<p className="font-semibold">No provider or model options are available.</p>
<p className="mt-1">Check the host-managed Pi model configuration, then retry this panel.</p>
<p className="font-semibold">The catalog default is unavailable.</p>
<p className="mt-1">Fix <code>modelCatalog.defaults.session</code> in the installation YAML, reload Pi, then retry this panel.</p>
</div>
<div className="mt-4 flex flex-wrap gap-2">
<Button size="sm" variant="outline" disabled><ClipboardCheck />Save defaults</Button>
<Button size="sm" variant="outline" disabled><FlaskConical />Test saved defaults</Button>
<Button size="sm" variant="ghost" onClick={() => { void statusQuery.refetch(); void optionsQuery.refetch(); }}>Retry choices</Button>
<Button size="sm" variant="outline" disabled><FlaskConical />Test catalog default</Button>
<Button size="sm" variant="ghost" onClick={() => { void statusQuery.refetch(); }}>Retry status</Button>
</div>
</div>
)}
+6 -227
View File
@@ -185,58 +185,6 @@ test("a settings preflight from user A prevents session POST after user B logs i
view.unmount();
});
test("a workspace-policy preflight from user A prevents session POST after user B logs in", async () => {
workspacePreferences.save({
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
});
let releaseWorkspaces!: () => void;
let workspacesStarted!: () => void;
let workspacesSettled!: () => void;
let sessionPosts = 0;
const workspacesGate = new Promise<void>((resolve) => { releaseWorkspaces = resolve; });
const started = new Promise<void>((resolve) => { workspacesStarted = resolve; });
const settled = new Promise<void>((resolve) => { workspacesSettled = resolve; });
server.use(
http.get("/api/workspaces", () => HttpResponse.json([{
...workspaceSummaryFixture("psd-clinical", {
displayName: "PSD Clinical",
revision: workspaceRevisionFixture("psd-clinical"),
}),
}])),
http.get("/api/workspaces/psd-clinical", async () => {
workspacesStarted();
try {
await workspacesGate;
return HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical"),
revision: workspaceRevisionFixture("psd-clinical"),
});
} finally {
workspacesSettled();
}
}),
http.post("/api/sessions", () => {
sessionPosts += 1;
return HttpResponse.json({ id: "a-session" });
}),
);
const userB = { ...userA, subject: "user-b", csrfToken: "b".repeat(43) };
const view = render(<SteerInput sessionId={null} />);
const input = screen.getByRole("textbox", { name: /new question/i });
await userEvent.type(input, "A-policy-question");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
await started;
act(() => setAuthState(userB));
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "B-entry" }));
releaseWorkspaces();
await act(async () => { await settled; });
expect(sessionPosts).toBe(0);
expect(useSessionStore.getState().lastUserEntry).toEqual({ kind: "input", text: "B-entry" });
view.unmount();
});
test("pressing Enter in the input submits the steer", async () => {
let captured: unknown = null;
server.use(
@@ -338,7 +286,7 @@ test("footer shows cumulative k-token counters after workspace and context gauge
expect(thinking.compareDocumentPosition(gauge) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
test("footer limits model choices to the selected workspace policy", async () => {
test("footer exposes every session model from the installation catalog", async () => {
server.use(
http.get("/api/settings", () => HttpResponse.json({
workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
@@ -363,10 +311,10 @@ test("footer limits model choices to the selected workspace policy", async () =>
const selector = await screen.findByRole("combobox", { name: "Model" });
await waitFor(() => expect(selector).toHaveTextContent("GLM-5.2"));
await waitFor(() => expect(selector).not.toHaveTextContent("DeepSeek V4 Pro"));
await waitFor(() => expect(selector).toHaveTextContent("DeepSeek V4 Pro"));
});
test("switching workspaces replaces an out-of-policy model before session creation", async () => {
test("switching workspaces preserves a globally available catalog model", async () => {
let body: unknown;
workspacePreferences.save({
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
@@ -405,119 +353,14 @@ test("switching workspaces replaces an out-of-policy model before session creati
const workspaceSelector = await screen.findByRole("combobox", { name: "Workspace" });
await waitFor(() => expect(workspaceSelector).toHaveTextContent("psd-clinical"));
await userEvent.selectOptions(workspaceSelector, "psd-clinical");
await waitFor(() => expect(screen.getByRole("combobox", { name: "Model" })).toHaveValue("glm-5.2"));
expect(screen.getByRole("combobox", { name: "Model" })).not.toHaveTextContent("DeepSeek V4 Pro");
await waitFor(() => expect(screen.getByRole("combobox", { name: "Model" })).toHaveValue("deepseek-v4-pro"));
expect(screen.getByRole("combobox", { name: "Model" })).toHaveTextContent("DeepSeek V4 Pro");
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
await waitFor(() => expect(body).toEqual({
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
}));
});
test("immediate submit waits for a switched workspace policy before creating a session", async () => {
let body: unknown;
let releasePolicy!: () => void;
let policyRequestStarted = false;
const policyMayFinish = new Promise<void>((resolve) => { releasePolicy = resolve; });
workspacePreferences.save({
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
});
const revision = (id: string) => ({
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
});
server.use(
http.get("/api/settings", () => HttpResponse.json({ workspace: "research" })),
http.get("/api/workspaces", () => HttpResponse.json([
workspaceSummaryFixture("research", { displayName: "Research", revision: revision("research") }),
workspaceSummaryFixture("psd-clinical", { displayName: "PSD Clinical", revision: revision("psd-clinical") }),
])),
http.get("/api/workspaces/research", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("research", ["deepseek/deepseek-v4-pro"]), revision: revision("research"),
})),
http.get("/api/workspaces/psd-clinical", async () => {
policyRequestStarted = true;
await policyMayFinish;
return HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical", ["zai/glm-5.2"], "zai/glm-5.2"),
revision: revision("psd-clinical"),
});
}),
http.get("/api/models", () => HttpResponse.json({ models: [
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
] })),
http.post("/api/sessions", async ({ request }) => {
body = await request.json();
return HttpResponse.json({ id: "s1" });
}),
);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} /></QueryClientProvider>);
const workspaceSelector = await screen.findByRole("combobox", { name: "Workspace" });
await waitFor(() => expect(workspaceSelector).toHaveTextContent("psd-clinical"));
await userEvent.selectOptions(workspaceSelector, "psd-clinical");
await waitFor(() => expect(policyRequestStarted).toBe(true));
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
expect(body).toBeUndefined();
expect(screen.getByRole("button", { name: /send/i })).toBeDisabled();
releasePolicy();
await waitFor(() => expect(body).toEqual({
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
}));
});
test("initial restored workspace waits for its delayed policy before creating a session", async () => {
let body: unknown;
let releasePolicy!: () => void;
let policyRequestStarted = false;
const policyMayFinish = new Promise<void>((resolve) => { releasePolicy = resolve; });
workspacePreferences.save({
workspaceId: "psd-clinical", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
});
const revision = (id: string) => ({
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
});
server.use(
http.get("/api/settings", () => HttpResponse.json({ workspace: "psd-clinical" })),
http.get("/api/workspaces", () => HttpResponse.json([
workspaceSummaryFixture("psd-clinical", { displayName: "PSD Clinical", revision: revision("psd-clinical") }),
])),
http.get("/api/workspaces/psd-clinical", async () => {
policyRequestStarted = true;
await policyMayFinish;
return HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical", ["zai/glm-5.2"], "zai/glm-5.2"),
revision: revision("psd-clinical"),
});
}),
http.get("/api/models", () => HttpResponse.json({ models: [
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
] })),
http.post("/api/sessions", async ({ request }) => {
body = await request.json();
return HttpResponse.json({ id: "s1" });
}),
);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} /></QueryClientProvider>);
await waitFor(() => expect(policyRequestStarted).toBe(true));
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
expect(body).toBeUndefined();
expect(screen.getByRole("button", { name: /send/i })).toBeDisabled();
releasePolicy();
await waitFor(() => expect(body).toEqual({
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
question: "q", workspaceId: "psd-clinical", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
}));
});
@@ -591,70 +434,6 @@ test("failed workspace summaries block creation and report a safe error", async
expect(body).toBeUndefined();
});
test("submit follows a rapid workspace switch instead of waiting for an abandoned policy", async () => {
let body: unknown;
let releaseC!: () => void;
let bPolicyRequestStarted = false;
let cPolicyRequestStarted = false;
const cPolicyMayFinish = new Promise<void>((resolve) => { releaseC = resolve; });
workspacePreferences.save({
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
});
const revision = (id: string) => ({
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
});
server.use(
http.get("/api/settings", () => HttpResponse.json({ workspace: "research" })),
http.get("/api/workspaces", () => HttpResponse.json([
workspaceSummaryFixture("research", { displayName: "Research", revision: revision("research") }),
workspaceSummaryFixture("workspace-b", { displayName: "Workspace B", revision: revision("workspace-b") }),
workspaceSummaryFixture("workspace-c", { displayName: "Workspace C", revision: revision("workspace-c") }),
])),
http.get("/api/workspaces/research", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("research", ["deepseek/deepseek-v4-pro"]), revision: revision("research"),
})),
http.get("/api/workspaces/workspace-b", async () => {
bPolicyRequestStarted = true;
await new Promise(() => undefined);
return HttpResponse.json({});
}),
http.get("/api/workspaces/workspace-c", async () => {
cPolicyRequestStarted = true;
await cPolicyMayFinish;
return HttpResponse.json({
workspace: canonicalWorkspaceFixture("workspace-c", ["zai/glm-5.2"], "zai/glm-5.2"),
revision: revision("workspace-c"),
});
}),
http.get("/api/models", () => HttpResponse.json({ models: [
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
] })),
http.post("/api/sessions", async ({ request }) => {
body = await request.json();
return HttpResponse.json({ id: "s1" });
}),
);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} /></QueryClientProvider>);
const workspaceSelector = await screen.findByRole("combobox", { name: "Workspace" });
await waitFor(() => expect(screen.getByRole("option", { name: "workspace-b" })).toBeInTheDocument());
await userEvent.selectOptions(workspaceSelector, "workspace-b");
await waitFor(() => expect(bPolicyRequestStarted).toBe(true));
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
await userEvent.selectOptions(workspaceSelector, "workspace-c");
await waitFor(() => expect(cPolicyRequestStarted).toBe(true));
expect(body).toBeUndefined();
releaseC();
await waitFor(() => expect(body).toEqual({
question: "q", workspaceId: "workspace-c", provider: "zai", model: "glm-5.2", thinking: "medium",
}));
});
test("pulses the stop dot only while the harness is working", () => {
const { rerender } = render(<SteerInput sessionId="s1" working />);
const dot = () =>
+4 -38
View File
@@ -5,7 +5,7 @@ import { useQuery } from "@tanstack/react-query";
import { postSteer, createSession } from "../api/sessions";
import { ApiError, apiErrorMessage } from "../api/client";
import { getSettings } from "../api/settings";
import { getWorkspace, listWorkspaces } from "../api/workspaces";
import { listWorkspaces } from "../api/workspaces";
import { listModels } from "../api/models";
import { useSessionStore } from "../store/sessionStore";
import {
@@ -202,18 +202,10 @@ export function ComposerFooter() {
const workspace = preferences.workspaceId ?? settings?.workspace ?? "";
const selectedWorkspace = workspaces.find((candidate) => candidate.id === workspace);
const selectedWorkspaceHasRevision = Boolean(selectedWorkspace?.revision);
const { data: workspaceRecord, isError: workspacePolicyError } = useQuery({
queryKey: ["workspace", workspace],
queryFn: () => getWorkspace(workspace),
enabled: selectedWorkspaceHasRevision,
});
const model = preferences.model ?? settings?.model ?? "";
const thinking = preferences.thinking ?? settings?.thinking ?? "medium";
const allowedModels = workspaceRecord?.workspace.llm_policy.allowed;
const policyModels = allowedModels
? models.filter((candidate) => allowedModels.includes(`${candidate.provider}/${candidate.id}`))
: models;
const policyModels = models;
useEffect(() => {
if (!workspace) {
@@ -226,38 +218,12 @@ export function ComposerFooter() {
workspacePolicyGate.rejectSummary(workspace);
} else if (selectedWorkspace?.revision) {
workspacePolicyGate.select(workspace);
workspacePolicyGate.resolve(workspace);
} else {
workspacePolicyGate.allowLegacy(workspace);
}
}, [selectedWorkspace, workspace, workspaceSummariesError, workspacesLoading]);
useEffect(() => {
if (!allowedModels?.length) return;
const selected = preferences.provider && preferences.model
? `${preferences.provider}/${preferences.model}`
: undefined;
if (selected && allowedModels.some((allowed) => allowed === selected)) return;
const replacement = workspaceRecord?.workspace.llm_policy.default
&& allowedModels.includes(workspaceRecord.workspace.llm_policy.default)
? workspaceRecord.workspace.llm_policy.default
: allowedModels[0];
const separator = replacement.indexOf("/");
if (separator <= 0 || separator === replacement.length - 1) return;
const next = {
...preferences,
provider: replacement.slice(0, separator),
model: replacement.slice(separator + 1),
};
workspacePreferences.save(next);
setPreferences(next);
}, [allowedModels, preferences, workspaceRecord]);
useEffect(() => {
if (!selectedWorkspace?.revision) return;
if (workspaceRecord) workspacePolicyGate.resolve(workspace);
else if (workspacePolicyError) workspacePolicyGate.reject(workspace);
}, [selectedWorkspace, workspace, workspacePolicyError, workspaceRecord]);
function update(patch: WorkspacePreference) {
if (patch.workspaceId && patch.workspaceId !== workspace) {
const selected = workspaces.find((candidate) => candidate.id === patch.workspaceId);
@@ -275,7 +241,7 @@ export function ComposerFooter() {
}
const knownModel = policyModels.some((m) => m.id === model);
const showModelFallback = !allowedModels && !knownModel;
const showModelFallback = !knownModel;
const contextPct = tokenUsage && tokenUsage.contextWindow > 0
? tokenUsage.totalTokens / tokenUsage.contextWindow
: 0;
+3 -12
View File
@@ -2,24 +2,15 @@ import type { CanonicalWorkspace, WorkspaceRevision, WorkspaceSummary } from "..
export function canonicalWorkspaceFixture(
id: string,
allowed: `${string}/${string}`[] = ["zai/glm-5.2"],
defaultModel?: `${string}/${string}`,
_allowed: `${string}/${string}`[] = ["zai/glm-5.2"],
_defaultModel?: `${string}/${string}`,
): CanonicalWorkspace {
return {
workspace: { schema_version: 3, id, name: id, language: "en" },
workspace: { schema_version: 4, id, name: id, language: "en" },
dwh: {
engine: "postgres", database: "database", schema: "public",
supported_transports: ["postgres_direct"],
},
semantic_index: {
vector_store: {
engine: "qdrant", collection: id, dimensions: 1024, distance: "cosine",
},
embedding: {
provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024,
},
},
llm_policy: { ...(defaultModel ? { default: defaultModel } : {}), allowed },
};
}
+4 -49
View File
@@ -130,10 +130,6 @@ function workspaceId(value: unknown): string | undefined {
return typeof value === "string" && /^[a-z][a-z0-9-]{2,62}$/.test(value) ? value : undefined;
}
function modelReference(value: unknown): `${string}/${string}` | undefined {
return typeof value === "string" && /^[^/\s]+\/[^/\s]+$/.test(value) ? value as `${string}/${string}` : undefined;
}
function positiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= max ? value : undefined;
}
@@ -348,17 +344,6 @@ function uniqueChoices<T extends string>(value: unknown, choices: readonly T[]):
return result;
}
function uniqueModels(value: unknown): `${string}/${string}`[] | undefined {
if (!Array.isArray(value) || value.length === 0) return undefined;
const result: `${string}/${string}`[] = [];
for (const item of value) {
const model = modelReference(item);
if (!model || result.includes(model)) return undefined;
result.push(model);
}
return result;
}
function originRelativePath(value: unknown): string | undefined {
return typeof value === "string" && /^\/(?!\/)[^\\\u0000-\u001F\u007F?#]*$/.test(value) && !/%5c/i.test(value)
? value
@@ -391,17 +376,11 @@ function copyDiagnostics(value: unknown): CanonicalDiagnostics | undefined {
/** Drops unknown fields before a server response can become a browser draft or conflict view. */
export function sanitizeCanonicalWorkspace(value: unknown): CanonicalWorkspace | undefined {
const source = exactRecord(value, [
"workspace", "dwh", "semantic_index", "llm_policy", "diagnostics", "evidence",
]);
const source = exactRecord(value, ["workspace", "dwh", "diagnostics", "evidence"]);
const metadata = exactRecord(source?.workspace, ["schema_version", "id", "name", "description", "language"]);
const dwh = exactRecord(source?.dwh, ["engine", "database", "schema", "port", "timeout_ms", "supported_transports"]);
const semanticIndex = exactRecord(source?.semantic_index, ["vector_store", "embedding"]);
const vectorStore = exactRecord(semanticIndex?.vector_store, ["engine", "collection", "dimensions", "distance"]);
const embedding = exactRecord(semanticIndex?.embedding, ["provider", "model", "dimensions"]);
const policy = exactRecord(source?.llm_policy, ["default", "allowed"]);
const diagnostics = source?.diagnostics === undefined ? undefined : copyDiagnostics(source.diagnostics);
if (!metadata || !dwh || !semanticIndex || !vectorStore || !embedding || !policy) return undefined;
if (!metadata || !dwh) return undefined;
const id = workspaceId(metadata.id);
const evidence = id && source?.evidence !== undefined ? copyEvidence(source.evidence, id) : undefined;
const name = text(metadata.name);
@@ -412,29 +391,16 @@ export function sanitizeCanonicalWorkspace(value: unknown): CanonicalWorkspace |
const dwhPort = dwh.port === undefined ? undefined : positiveInteger(dwh.port, 65_535);
const dwhTimeout = dwh.timeout_ms === undefined ? undefined : positiveInteger(dwh.timeout_ms);
const dwhTransports = uniqueChoices(dwh.supported_transports, ["postgres_direct", "rest_api", "ssh_tunnel"] as const);
const collection = workspaceId(vectorStore.collection);
const vectorDimensions = positiveInteger(vectorStore.dimensions, 32_768);
const distance = oneOf(vectorStore.distance, ["cosine"] as const);
const embeddingProvider = oneOf(embedding.provider, ["ollama_internal"] as const);
const embeddingModel = text(embedding.model);
const embeddingDimensions = positiveInteger(embedding.dimensions, 32_768);
const allowedModels = uniqueModels(policy.allowed);
const defaultModel = policy.default === undefined ? undefined : modelReference(policy.default);
if (
metadata.schema_version !== 3 || !id || !name || !language || (metadata.description !== undefined && !description)
metadata.schema_version !== 4 || !id || !name || !language || (metadata.description !== undefined && !description)
|| dwh.engine !== "postgres" || !database || !schema || (dwh.port !== undefined && !dwhPort) || (dwh.timeout_ms !== undefined && !dwhTimeout) || !dwhTransports
|| vectorStore.engine !== "qdrant" || !collection || !vectorDimensions || !distance
|| !embeddingProvider || !embeddingModel || !embeddingDimensions || !allowedModels
|| (defaultModel !== undefined && !allowedModels.includes(defaultModel)) || vectorDimensions !== embeddingDimensions
|| vectorDimensions !== 1024 || embeddingDimensions !== 1024
|| embeddingModel !== "qwen3-embedding:0.6b"
) return undefined;
if (source?.diagnostics !== undefined && !diagnostics) return undefined;
if (source?.evidence !== undefined && !evidence) return undefined;
if (diagnostics?.dwh_rest && !dwhTransports.includes("rest_api")) return undefined;
return {
workspace: {
schema_version: 3,
schema_version: 4,
id,
name,
...(description ? { description } : {}),
@@ -446,17 +412,6 @@ export function sanitizeCanonicalWorkspace(value: unknown): CanonicalWorkspace |
...(dwhTimeout ? { timeout_ms: dwhTimeout } : {}),
supported_transports: dwhTransports,
},
semantic_index: {
vector_store: {
engine: "qdrant", collection, dimensions: 1024, distance: "cosine",
},
embedding: {
provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024,
},
},
llm_policy: {
...(defaultModel ? { default: defaultModel } : {}), allowed: allowedModels,
},
...(diagnostics ? { diagnostics } : {}),
...(evidence ? { evidence } : {}),
};