wip: guided standalone installation and workspace checks
This commit is contained in:
@@ -16,12 +16,16 @@ import { loadSettings } from "./settings/settings-store.js";
|
||||
import { ThtRunner, type SessionRow } from "./tht/tht-runner.js";
|
||||
import { WorkspaceRegistry } from "./workspaces/registry.js";
|
||||
import { WorkspaceSecretStore } from "./workspaces/secret-store.js";
|
||||
import { createProductionWorkspaceDiagnoser } from "./workspaces/diagnostics.js";
|
||||
import { resolveCatalogRuntimeBinding } from "./catalog/runtime-binding.js";
|
||||
import { CatalogService } from "./catalog/service.js";
|
||||
import { validateOperationalWorkspace } from "./workspaces/schema.js";
|
||||
import { createCatalogRepository } from "./catalog/repository.js";
|
||||
import type { CatalogRepository } from "./catalog/types.js";
|
||||
|
||||
type OperatorAction = "maintenance-activate" | "maintenance-deactivate" | "maintenance-status"
|
||||
| "session-inventory" | "workflow-doctor" | "workspace-integrity"
|
||||
| "pi-test" | "effective-settings";
|
||||
| "workspace-pull" | "workspace-test" | "pi-test" | "effective-settings";
|
||||
|
||||
const lifecyclePrincipal: PrincipalContext = {
|
||||
issuer: "tht-operator-command",
|
||||
@@ -119,6 +123,119 @@ async function workspaceIntegrity(config: AppConfig): Promise<{
|
||||
return { ready: true, ...integrity };
|
||||
}
|
||||
|
||||
async function workspacePull(config: AppConfig): Promise<{
|
||||
ready: boolean;
|
||||
status: "succeeded" | "degraded";
|
||||
branch: string;
|
||||
head?: string;
|
||||
degraded: boolean;
|
||||
}> {
|
||||
const status = await new WorkspaceRegistry(config.workspaceRegistry).pull();
|
||||
return {
|
||||
ready: !status.degraded,
|
||||
status: status.degraded ? "degraded" : "succeeded",
|
||||
branch: status.branch,
|
||||
...(status.head ? { head: status.head } : {}),
|
||||
degraded: status.degraded,
|
||||
};
|
||||
}
|
||||
|
||||
interface WorkspaceTestReport {
|
||||
id: string;
|
||||
status: "ready" | "failed";
|
||||
database: "reachable" | "not_configured" | "failed";
|
||||
diagnostics: string[];
|
||||
}
|
||||
|
||||
async function workspaceTest(config: AppConfig): Promise<{
|
||||
ready: boolean;
|
||||
workspaces: WorkspaceTestReport[];
|
||||
}> {
|
||||
if (!config.catalogDatabase) throw new Error("Catalog database is not configured");
|
||||
const registry = new WorkspaceRegistry(config.workspaceRegistry);
|
||||
const revisions = await registry.list();
|
||||
const repository = createCatalogRepository(config.catalogDatabase);
|
||||
try {
|
||||
const secretStore = new WorkspaceSecretStore({
|
||||
root: config.workspaceSecretStoreRoot,
|
||||
runtimeRoot: config.workspaceSecretRuntimeRoot,
|
||||
installationId: config.workspaceRegistry.installationId,
|
||||
});
|
||||
const catalogService = new CatalogService(
|
||||
repository,
|
||||
registry,
|
||||
secretStore,
|
||||
config.workspaceRegistry.secretRoots,
|
||||
config.workspaceDiagnosticTimeoutMs,
|
||||
);
|
||||
const diagnose = createProductionWorkspaceDiagnoser(config.workspaceDiagnosticTimeoutMs, undefined, {
|
||||
internalQdrantUrl: config.internalQdrantUrl,
|
||||
internalEmbeddingUrl: config.internalEmbeddingUrl,
|
||||
internalEmbeddingId: config.internalEmbeddingId,
|
||||
internalEmbeddingModel: config.internalEmbeddingModel,
|
||||
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
|
||||
});
|
||||
const databases = await repository.list();
|
||||
const reports: WorkspaceTestReport[] = [];
|
||||
for (const revision of revisions) {
|
||||
const diagnostics: string[] = [];
|
||||
let workspace: ReturnType<typeof validateOperationalWorkspace>;
|
||||
try {
|
||||
workspace = validateOperationalWorkspace((await registry.read(revision.id)).workspace);
|
||||
} catch {
|
||||
reports.push({ id: revision.id, status: "failed", database: "not_configured", diagnostics: ["workspace_invalid"] });
|
||||
continue;
|
||||
}
|
||||
const database = databases.find((candidate) => candidate.workspaceId === revision.id);
|
||||
if (!database) {
|
||||
reports.push({ id: revision.id, status: "failed", database: "not_configured", diagnostics: ["database_binding_missing"] });
|
||||
continue;
|
||||
}
|
||||
let tested;
|
||||
try {
|
||||
tested = await catalogService.test(database);
|
||||
} catch {
|
||||
reports.push({ id: revision.id, status: "failed", database: "failed", diagnostics: ["database_unreachable"] });
|
||||
continue;
|
||||
}
|
||||
if (!tested || tested.connectionStatus !== "reachable") {
|
||||
reports.push({ id: revision.id, status: "failed", database: "failed", diagnostics: ["database_unreachable"] });
|
||||
continue;
|
||||
}
|
||||
let lease: ReturnType<typeof resolveCatalogRuntimeBinding>;
|
||||
try {
|
||||
lease = resolveCatalogRuntimeBinding({
|
||||
workspace,
|
||||
database: tested,
|
||||
environment: process.env,
|
||||
secretRoots: config.workspaceRegistry.secretRoots,
|
||||
secretStore,
|
||||
});
|
||||
} catch {
|
||||
reports.push({ id: revision.id, status: "failed", database: "reachable", diagnostics: ["binding_missing"] });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// CatalogService.test() above is the authoritative database probe and records its
|
||||
// outcome. The remaining diagnoser pass checks Evidence and internal semantic services;
|
||||
// skipping its legacy DWH probe avoids requiring a second response-shape contract for a
|
||||
// REST health endpoint.
|
||||
const result = await diagnose(lease.workspace, lease.bindings, { writeProbe: false, skipDwh: true });
|
||||
diagnostics.push(...result.diagnostics.map((diagnostic) => diagnostic.code));
|
||||
const ready = result.activatable;
|
||||
reports.push({ id: revision.id, status: ready ? "ready" : "failed", database: "reachable", diagnostics });
|
||||
} catch {
|
||||
reports.push({ id: revision.id, status: "failed", database: "reachable", diagnostics: ["connector_unavailable"] });
|
||||
} finally {
|
||||
lease.release();
|
||||
}
|
||||
}
|
||||
return { ready: reports.length > 0 && reports.every((report) => report.status === "ready"), workspaces: reports };
|
||||
} finally {
|
||||
await repository.close?.();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runOperatorAction(
|
||||
action: OperatorAction,
|
||||
config: AppConfig,
|
||||
@@ -132,6 +249,8 @@ export async function runOperatorAction(
|
||||
if (action === "session-inventory") return await sessionInventory(config);
|
||||
if (action === "workflow-doctor") return await workflowDiagnostics(config);
|
||||
if (action === "workspace-integrity") return await workspaceIntegrity(config);
|
||||
if (action === "workspace-pull") return await workspacePull(config);
|
||||
if (action === "workspace-test") return await workspaceTest(config);
|
||||
const modelCatalog = loadRuntimeModelCatalog(config.modelCatalogFile);
|
||||
if (action === "effective-settings") {
|
||||
return effectiveSettings(config, loadSettings(config), modelCatalog);
|
||||
@@ -146,6 +265,7 @@ async function main(): Promise<void> {
|
||||
if (!action || ![
|
||||
"maintenance-activate", "maintenance-deactivate", "maintenance-status", "session-inventory",
|
||||
"workflow-doctor", "workspace-integrity", "pi-test", "effective-settings",
|
||||
"workspace-pull", "workspace-test",
|
||||
].includes(action)) throw new Error("invalid operator action");
|
||||
const result = await runOperatorAction(action, loadConfig(process.env));
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
|
||||
Reference in New Issue
Block a user