fix: harden workspace registry deployment
This commit is contained in:
@@ -87,6 +87,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
|
||||
sessionRoutes(app, {
|
||||
mgr, tht: tht as ThtRunner, hub, getSettings, readiness, listModels, workspaceRegistry,
|
||||
dwhPrecheck: config.dwhPrecheck,
|
||||
legacyWorkspaceMode: config.legacyWorkspaceMode,
|
||||
});
|
||||
sqlRoutes(app, { tht: tht as ThtRunner, getSettings });
|
||||
metaRoutes(app, { harnessDir: config.harnessDir, listModels });
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface AppConfig {
|
||||
* pay the probe; the local dev launcher (run-stack.sh) opts in via THT_DWH_PRECHECK.
|
||||
*/
|
||||
dwhPrecheck: boolean;
|
||||
/** Explicit compatibility mode for old loopback clients that send `workspace` in POST /sessions. */
|
||||
legacyWorkspaceMode: boolean;
|
||||
workspaceDiagnosticTimeoutMs: number;
|
||||
workspaceRegistry: WorkspaceRegistryConfig;
|
||||
}
|
||||
@@ -103,6 +105,13 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
if (sessionStorageMode === "local" && env.THOTH_PUBLIC_EXPOSURE === "true") {
|
||||
throw new Error("local session storage requires loopback-only deployment");
|
||||
}
|
||||
const legacyWorkspaceMode = env.THT_LEGACY_WORKSPACE_MODE;
|
||||
if (legacyWorkspaceMode !== undefined && legacyWorkspaceMode !== "local") {
|
||||
throw new Error("legacy workspace mode configuration is invalid");
|
||||
}
|
||||
if (legacyWorkspaceMode === "local" && sessionStorageMode !== "local") {
|
||||
throw new Error("legacy workspace mode requires local session storage");
|
||||
}
|
||||
const sessionStorage: AppConfig["sessionStorage"] = { mode: sessionStorageMode };
|
||||
if (sessionStorageMode === "postgres") {
|
||||
const host = env.THT_SESSION_DB_HOST;
|
||||
@@ -203,6 +212,7 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
secretFiles,
|
||||
modelApiKeyFile,
|
||||
dwhPrecheck: env.THT_DWH_PRECHECK === "true" || env.THT_DWH_PRECHECK === "1",
|
||||
legacyWorkspaceMode: legacyWorkspaceMode === "local",
|
||||
workspaceDiagnosticTimeoutMs: diagnosticTimeout(env.THT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS),
|
||||
workspaceRegistry,
|
||||
};
|
||||
|
||||
@@ -32,6 +32,8 @@ export function sessionRoutes(
|
||||
workspaceRegistry: WorkspaceRegistry;
|
||||
/** Local-only guard: probe DWH reachability before creating a session (run-stack.sh). */
|
||||
dwhPrecheck?: boolean;
|
||||
/** Explicit loopback-only compatibility path for old clients that send `workspace`. */
|
||||
legacyWorkspaceMode?: boolean;
|
||||
},
|
||||
) {
|
||||
const lifecycleTails = new Map<string, Promise<void>>();
|
||||
@@ -267,9 +269,10 @@ export function sessionRoutes(
|
||||
try { s = await d.getSettings(principal); } catch { return storageFailure(reply); }
|
||||
const runner = runnerFor(principal);
|
||||
// `workspace` was the legacy request field before browser-local registry preferences.
|
||||
// It opts a pre-registry caller into the existing installation-default config only; modern
|
||||
// `workspaceId` and saved preferences must continue to resolve an immutable snapshot.
|
||||
const legacyWorkspaceRequest = typeof b.workspace === "string" && b.workspace.length > 0;
|
||||
// It is available only through the explicit loopback-only compatibility mode; every normal
|
||||
// new session must resolve and pin an immutable registry revision.
|
||||
const legacyWorkspaceRequest = d.legacyWorkspaceMode
|
||||
&& typeof b.workspace === "string" && b.workspace.length > 0;
|
||||
const requestedWorkspaceId = b.workspaceId ?? (legacyWorkspaceRequest ? undefined : s.workspace);
|
||||
if (!requestedWorkspaceId && !legacyWorkspaceRequest) {
|
||||
return reply.code(409).send({
|
||||
|
||||
@@ -175,19 +175,35 @@ export async function writeMigratedWorkspace(result: LegacyMigrationResult, repo
|
||||
return destination;
|
||||
}
|
||||
|
||||
function parseCliArguments(argv: readonly string[]): { input: string; output: string } {
|
||||
if (argv.length !== 4 || argv[0] !== "--input" || argv[2] !== "--output") {
|
||||
throw new Error("usage: migrate-legacy --input <legacy-workspace.yaml> --output <repository-root>");
|
||||
function parseCliArguments(argv: readonly string[]): { input: string; output: string; id?: string } {
|
||||
if (argv.length !== 4 && argv.length !== 6) {
|
||||
throw new Error("usage: migrate-legacy --input <legacy-workspace.yaml> --output <repository-root> [--id <workspace-id>]");
|
||||
}
|
||||
if (!isAbsolute(argv[1]) || !isAbsolute(argv[3])) {
|
||||
const options = new Map<string, string>();
|
||||
for (let index = 0; index < argv.length; index += 2) {
|
||||
const flag = argv[index];
|
||||
const value = argv[index + 1];
|
||||
if ((flag !== "--input" && flag !== "--output" && flag !== "--id") || value === undefined || options.has(flag)) {
|
||||
throw new Error("usage: migrate-legacy --input <legacy-workspace.yaml> --output <repository-root> [--id <workspace-id>]");
|
||||
}
|
||||
options.set(flag, value);
|
||||
}
|
||||
const input = options.get("--input");
|
||||
const output = options.get("--output");
|
||||
const id = options.get("--id");
|
||||
if (input === undefined || output === undefined) {
|
||||
throw new Error("usage: migrate-legacy --input <legacy-workspace.yaml> --output <repository-root> [--id <workspace-id>]");
|
||||
}
|
||||
if (!isAbsolute(input) || !isAbsolute(output)) {
|
||||
throw new Error("migration input and output paths must be absolute");
|
||||
}
|
||||
return { input: argv[1], output: argv[3] };
|
||||
if (id !== undefined && !workspaceId.test(id)) throw new Error("legacy workspace ID is invalid");
|
||||
return { input, output, id };
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
||||
const { input, output } = parseCliArguments(argv);
|
||||
const id = basename(input, ".yaml");
|
||||
const { input, output, id: explicitId } = parseCliArguments(argv);
|
||||
const id = explicitId ?? basename(input, ".yaml");
|
||||
const result = migrateLegacyWorkspace(await readFile(input, "utf8"), { id });
|
||||
const destination = await writeMigratedWorkspace(result, output);
|
||||
process.stdout.write(`${destination}\n`);
|
||||
|
||||
Reference in New Issue
Block a user