136 lines
5.8 KiB
TypeScript
136 lines
5.8 KiB
TypeScript
/**
|
|
* Installation-scoped host operations that must not impersonate an HTTP administrator.
|
|
* This command runs only through `docker compose exec core`; it never accepts credentials,
|
|
* headers, paths, or arbitrary code from the caller.
|
|
*/
|
|
import { pathToFileURL } from "node:url";
|
|
import { join } from "node:path";
|
|
import { loadConfig, type AppConfig } from "./config.js";
|
|
import { rolesToPermissions } from "./auth/config.js";
|
|
import type { PrincipalContext } from "./auth/principal.js";
|
|
import { createPiModelLister } from "./pi/list-models.js";
|
|
import { createPiManagement } from "./pi/management.js";
|
|
import { effectiveSettings } from "./routes/settings.js";
|
|
import { MaintenanceBarrier } from "./runtime/maintenance-gate.js";
|
|
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";
|
|
|
|
type OperatorAction = "maintenance-activate" | "maintenance-deactivate" | "maintenance-status"
|
|
| "session-inventory" | "workflow-doctor" | "workspace-integrity"
|
|
| "pi-options" | "pi-test" | "effective-settings";
|
|
|
|
const lifecyclePrincipal: PrincipalContext = {
|
|
issuer: "tht-operator-command",
|
|
subject: "installation-lifecycle",
|
|
displayName: "Installation lifecycle",
|
|
roles: ["admin"],
|
|
permissions: rolesToPermissions(["admin"]),
|
|
isAdmin: true,
|
|
};
|
|
|
|
function operatorRunner(config: AppConfig): ThtRunner {
|
|
const workspaceSecretStore = new WorkspaceSecretStore({
|
|
root: config.workspaceSecretStoreRoot,
|
|
runtimeRoot: config.workspaceSecretRuntimeRoot,
|
|
installationId: config.workspaceRegistry.installationId,
|
|
});
|
|
return new ThtRunner({
|
|
thtBin: config.thtBin,
|
|
harnessDir: config.harnessDir,
|
|
configPath: process.env.THT_CONFIG ?? "config/tht.yaml",
|
|
dataRoot: config.dataRoot,
|
|
runtimeSnapshotRoot: join(config.workspaceRegistry.root, "snapshots", "runtime"),
|
|
secretRoots: config.workspaceRegistry.secretRoots,
|
|
secretsFile: config.secretsFile,
|
|
secretFiles: config.secretFiles,
|
|
workspaceSecretStore,
|
|
semanticRuntime: {
|
|
internalQdrantUrl: config.internalQdrantUrl,
|
|
internalEmbeddingUrl: config.internalEmbeddingUrl,
|
|
internalEmbeddingModel: config.internalEmbeddingModel,
|
|
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
|
|
},
|
|
}).withPrincipal(lifecyclePrincipal);
|
|
}
|
|
|
|
async function sessionInventory(config: AppConfig): Promise<Array<Pick<SessionRow, "status" | "archived">>> {
|
|
const registry = new WorkspaceRegistry(config.workspaceRegistry);
|
|
const revisions = await registry.listRetainedSnapshots();
|
|
const runner = operatorRunner(config);
|
|
const sessions = new Map<string, SessionRow>();
|
|
for (const revision of revisions) {
|
|
for (const session of await runner.sessionList(revision.snapshotPath)) sessions.set(session.id, session);
|
|
}
|
|
return [...sessions.values()].map(({ status, archived }) => ({ status, archived: archived === true }));
|
|
}
|
|
|
|
async function workflowDiagnostics(config: AppConfig): Promise<{ ready: true; workspaces: number }> {
|
|
const registry = new WorkspaceRegistry(config.workspaceRegistry);
|
|
const revisions = await registry.listRetainedSnapshots();
|
|
if (revisions.length === 0) throw new Error("workflow diagnostics unavailable");
|
|
const runner = operatorRunner(config);
|
|
for (const revision of revisions) {
|
|
const result = await runner.run(["doctor", "--json"], revision.snapshotPath);
|
|
let payload: unknown;
|
|
try {
|
|
payload = JSON.parse(result.stdout);
|
|
} catch {
|
|
throw new Error("workflow diagnostics failed");
|
|
}
|
|
if (
|
|
result.code !== 0 || !payload || typeof payload !== "object"
|
|
|| (payload as { ok?: unknown }).ok !== true
|
|
) throw new Error("workflow diagnostics failed");
|
|
}
|
|
return { ready: true, workspaces: revisions.length };
|
|
}
|
|
|
|
async function workspaceIntegrity(config: AppConfig): Promise<{
|
|
ready: true;
|
|
state: "uninitialized" | "active";
|
|
workspaces: number;
|
|
fingerprint: string;
|
|
}> {
|
|
const integrity = await new WorkspaceRegistry(config.workspaceRegistry).verifyStoredState();
|
|
return { ready: true, ...integrity };
|
|
}
|
|
|
|
export async function runOperatorAction(
|
|
action: OperatorAction,
|
|
config: AppConfig,
|
|
): Promise<unknown> {
|
|
if (action.startsWith("maintenance-")) {
|
|
const barrier = new MaintenanceBarrier(config.maintenanceFile);
|
|
if (action === "maintenance-activate") await barrier.activate();
|
|
if (action === "maintenance-deactivate") barrier.deactivate();
|
|
return barrier.status();
|
|
}
|
|
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 === "effective-settings") return effectiveSettings(config, loadSettings(config));
|
|
const service = createPiManagement(config, { listModels: createPiModelLister(config) });
|
|
if (action === "pi-options") return await service.options();
|
|
if (action === "pi-test") return await service.test();
|
|
throw new Error("unsupported operator action");
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const action = process.argv[2] as OperatorAction | undefined;
|
|
if (!action || ![
|
|
"maintenance-activate", "maintenance-deactivate", "maintenance-status", "session-inventory",
|
|
"workflow-doctor", "workspace-integrity", "pi-options", "pi-test", "effective-settings",
|
|
].includes(action)) throw new Error("invalid operator action");
|
|
const result = await runOperatorAction(action, loadConfig(process.env));
|
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
void main().catch(() => {
|
|
process.stderr.write("operator command failed\n");
|
|
process.exitCode = 2;
|
|
});
|
|
}
|