fix: fail closed v3 legacy semantic fallbacks

This commit is contained in:
2026-08-08 16:52:33 +02:00
parent 2f7f923c4d
commit ba1d7b0e78
10 changed files with 172 additions and 48 deletions
+21 -10
View File
@@ -4,7 +4,7 @@ import { buildInstallationContract, type InstallationRole, type InstallationSuff
import {
DWH_TRANSPORTS,
VECTOR_TRANSPORTS,
validateCanonicalWorkspace,
validateWorkspaceDescriptor,
type DwhTransport,
type VectorTransport,
type WorkspaceDescriptor,
@@ -95,14 +95,20 @@ export function resolveBinding(
env: NodeJS.ProcessEnv,
secretRoots: readonly string[],
): ResolvedBinding {
const canonical = validateCanonicalWorkspace(workspace);
const contract = buildInstallationContract(canonical);
const descriptor = validateWorkspaceDescriptor(workspace);
if (descriptor.workspace.schema_version === 3 && role !== "DWH") {
throw new Error("Schema version 3 semantic bindings are not supported by the legacy installation contract");
}
const contract = buildInstallationContract(descriptor);
const variables = contract.variables.filter((variable) => variable.role === role);
const transportVariable = variables.find((variable) => variable.suffix === "TRANSPORT");
const supported = role === "DWH"
? canonical.dwh.supported_transports
? descriptor.dwh.supported_transports
: role === "VECTOR"
? (canonical.semantic_index.vector_store.supported_transports ?? [])
? ("supported_transports" in descriptor.semantic_index.vector_store
? descriptor.semantic_index.vector_store.supported_transports
: [])
: ["rest_api"] as const;
const selectedValue = transportVariable ? env[transportVariable.name] : undefined;
const selectedTransport = isTransport(selectedValue) ? selectedValue : supported[0];
@@ -112,7 +118,7 @@ export function resolveBinding(
missing.push(transportVariable.name);
}
const required = new Set(requiredSuffixes(canonical, role, selectedTransport));
const required = new Set(requiredSuffixes(descriptor, role, selectedTransport));
const values: Record<string, string> = {};
for (const variable of variables) {
if (variable.suffix === "TRANSPORT") continue;
@@ -136,11 +142,16 @@ export function resolveRuntimeBindings(
env: NodeJS.ProcessEnv,
secretRoots: readonly string[],
): RuntimeBindings {
const descriptor = validateWorkspaceDescriptor(workspace);
if (descriptor.workspace.schema_version === 3) {
throw new Error("Schema version 3 semantic runtime bindings are not supported before the internal Qdrant/Ollama runtime lands");
}
return {
dwh: resolveBinding(workspace, "DWH", env, secretRoots),
vector: resolveBinding(workspace, "VECTOR", env, secretRoots),
vectorWriter: resolveBinding(workspace, "VECTOR_WRITER", env, secretRoots),
embedding: resolveBinding(workspace, "EMBEDDING", env, secretRoots),
dwh: resolveBinding(descriptor, "DWH", env, secretRoots),
vector: resolveBinding(descriptor, "VECTOR", env, secretRoots),
vectorWriter: resolveBinding(descriptor, "VECTOR_WRITER", env, secretRoots),
embedding: resolveBinding(descriptor, "EMBEDDING", env, secretRoots),
};
}
+23 -17
View File
@@ -1,4 +1,4 @@
import { validateCanonicalWorkspace } from "./schema.js";
import { validateWorkspaceDescriptor } from "./schema.js";
import type { DwhTransport, VectorTransport, WorkspaceDescriptor } from "./schema.js";
export type InstallationRole = "DWH" | "VECTOR" | "VECTOR_WRITER" | "EMBEDDING";
@@ -120,23 +120,29 @@ function connectorVariables(
}
export function buildInstallationContract(workspace: WorkspaceDescriptor): InstallationContract {
const canonical = validateCanonicalWorkspace(workspace);
const namespace = namespaceFor(canonical);
const descriptor = validateWorkspaceDescriptor(workspace);
const namespace = namespaceFor(descriptor);
return {
workspaceId: canonical.workspace.id,
workspaceId: descriptor.workspace.id,
namespace,
variables: [
...connectorVariables(namespace, "DWH", canonical.dwh.supported_transports),
...connectorVariables(
namespace,
"VECTOR",
canonical.semantic_index.vector_store.supported_transports ?? [],
),
...(canonical.semantic_index.vector_writer
...connectorVariables(namespace, "DWH", descriptor.dwh.supported_transports),
...(descriptor.workspace.schema_version === 2
? connectorVariables(
namespace,
"VECTOR",
"supported_transports" in descriptor.semantic_index.vector_store
? descriptor.semantic_index.vector_store.supported_transports
: [],
)
: []),
...(descriptor.workspace.schema_version === 2 && descriptor.semantic_index.vector_writer
? [createVariable(namespace, "VECTOR_WRITER", "API_KEY_FILE")]
: []),
...EMBEDDING_SUFFIXES.map((suffix) => createVariable(namespace, "EMBEDDING", suffix)),
...(descriptor.workspace.schema_version === 2
? EMBEDDING_SUFFIXES.map((suffix) => createVariable(namespace, "EMBEDDING", suffix))
: []),
],
};
}
@@ -148,8 +154,8 @@ function localizedIntroduction(workspace: WorkspaceDescriptor): string {
}
export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExample: string; markdown: string } {
const canonical = validateCanonicalWorkspace(workspace);
const contract = buildInstallationContract(canonical);
const descriptor = validateWorkspaceDescriptor(workspace);
const contract = buildInstallationContract(descriptor);
const variablesByRole = new Map<InstallationRole, InstallationVariable[]>();
for (const variable of contract.variables) {
const variables = variablesByRole.get(variable.role) ?? [];
@@ -158,7 +164,7 @@ export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExampl
}
const envExample = [
`# Generated installation bindings for ${canonical.workspace.id}`,
`# Generated installation bindings for ${descriptor.workspace.id}`,
"# Provide secret file paths only; never paste secret values here.",
...contract.variables.map((variable) => `${variable.name}=`),
"",
@@ -167,9 +173,9 @@ export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExampl
const markdown = [
"# Installation requirements",
"",
`**Workspace:** ${canonical.workspace.name}`,
`**Workspace:** ${descriptor.workspace.name}`,
"",
localizedIntroduction(canonical),
localizedIntroduction(descriptor),
"",
"Use the following UI fields as installation bindings. Secret fields always contain file paths, never secret values.",
"",
+11 -7
View File
@@ -10,9 +10,9 @@ import { buildInstallationContract } from "./contracts.js";
import type { RuntimeBindings } from "./runtime-renderer.js";
import {
resolveDiagnosticUrl,
validateCanonicalWorkspace,
type CanonicalWorkspace,
validateWorkspaceDescriptor,
type RestDiagnosticRequest,
type WorkspaceV2,
type WorkspaceDescriptor,
} from "./schema.js";
import type { WorkspaceErrorCode } from "./types.js";
@@ -542,7 +542,7 @@ function diagnosticError(code: WorkspaceErrorCode, field?: string): Diagnostic {
}
function bindingName(
workspace: CanonicalWorkspace,
workspace: WorkspaceV2,
role: "DWH" | "VECTOR" | "VECTOR_WRITER" | "EMBEDDING",
suffix: string,
): string {
@@ -559,7 +559,7 @@ function numericBinding(binding: Record<string, string>, name: string): number |
}
function diagnosticsForMissingBindings(
workspace: CanonicalWorkspace,
workspace: WorkspaceV2,
bindings: RuntimeBindings,
): Diagnostic[] {
const missing = new Set([
@@ -576,7 +576,7 @@ function diagnosticsForMissingBindings(
}
function connectorRequest(
workspace: CanonicalWorkspace,
workspace: WorkspaceV2,
role: ConnectorRole,
bindings: RuntimeBindings,
timeoutMs: number,
@@ -650,7 +650,7 @@ function connectorRequest(
}
function tunnelProbeRequest(
workspace: CanonicalWorkspace,
workspace: WorkspaceV2,
role: ConnectorRole,
bindings: RuntimeBindings,
timeoutMs: number,
@@ -694,7 +694,11 @@ export function createWorkspaceDiagnoser(
bindings: RuntimeBindings,
options: { writeProbe: boolean },
): Promise<WorkspaceDiagnostics> {
const canonical = validateCanonicalWorkspace(workspace);
const descriptor = validateWorkspaceDescriptor(workspace);
if (descriptor.workspace.schema_version !== 2) {
return { activatable: false, diagnostics: [diagnosticError("workspace_not_activatable")] };
}
const canonical = descriptor as WorkspaceV2;
const diagnostics = diagnosticsForMissingBindings(canonical, bindings);
if (diagnostics.length > 0) return { activatable: false, diagnostics };
+10 -2
View File
@@ -1,6 +1,6 @@
import { stringify } from "yaml";
import { buildInstallationContract } from "./contracts.js";
import { validateCanonicalWorkspace, type WorkspaceDescriptor } from "./schema.js";
import { validateWorkspaceDescriptor, type WorkspaceDescriptor, type WorkspaceV2 } from "./schema.js";
import type { ResolvedBinding, RuntimeBindings } from "./bindings.js";
export type { RuntimeBindings } from "./bindings.js";
@@ -86,7 +86,15 @@ export function renderRuntimeConfig(
identity?: RuntimeIdentity,
installation: RuntimeInstallationOverlay = {},
): string {
const canonical = validateCanonicalWorkspace(workspace);
const descriptor = validateWorkspaceDescriptor(workspace);
if (descriptor.workspace.schema_version !== 2) {
if (descriptor.workspace.schema_version === 1) {
throw new Error("Workspace descriptor requires explicit migration to schema version 2");
}
throw new Error("Schema version 3 runtime rendering is unsupported until the internal semantic runtime is implemented");
}
const canonical = descriptor as WorkspaceV2;
if ([...bindings.dwh.missing, ...bindings.vector.missing, ...bindings.embedding.missing].length > 0) {
throw new Error("runtime configuration requires complete bindings");
}
-5
View File
@@ -102,11 +102,6 @@ interface QdrantVectorStore {
collection: string;
dimensions: 1024;
distance: "cosine";
database?: string;
schema?: string;
port?: number;
timeout_ms?: number;
supported_transports?: VectorTransport[];
}
export interface WorkspaceV3 extends WorkspaceBase<3, QdrantVectorStore> {}
+1 -7
View File
@@ -21,18 +21,12 @@ export interface QdrantVectorStore {
collection: string;
dimensions: 1024;
distance: "cosine";
database?: string;
schema?: string;
port?: number;
timeout_ms?: number;
supported_transports?: ("pgvector_direct" | "rest_api" | "ssh_tunnel")[];
}
export interface InternalEmbedding {
provider: "ollama_internal";
model: "qwen3-embedding:0.6b";
dimensions: 1024;
timeout_ms?: number;
}
export interface WorkspaceV2 {
@@ -113,5 +107,5 @@ export interface WorkspaceV3 {
embedding: InternalEmbedding;
};
llm_policy: WorkspaceV2["llm_policy"];
diagnostics?: WorkspaceV2["diagnostics"];
diagnostics?: Pick<NonNullable<WorkspaceV2["diagnostics"]>, "dwh_rest">;
}