feat(auth): integrate authentication with installation lifecycle
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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" | "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 };
|
||||
}
|
||||
|
||||
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 === "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", "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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user