fix: fail closed v3 legacy semantic fallbacks
This commit is contained in:
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
"",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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">;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,29 @@ semantic_index:
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
const workspaceV3 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct, rest_api]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
const paths: RuntimePaths = {
|
||||
sessions: "/data/workspaces/psd-clinical/sessions",
|
||||
artifacts: "/data/workspaces/psd-clinical/artifacts",
|
||||
@@ -149,6 +172,12 @@ test("refuses to render a v1 descriptor until an explicit migration creates v2",
|
||||
expect(() => renderRuntimeConfig(legacyWorkspace, directBindings, paths)).toThrow(/migrat/i);
|
||||
});
|
||||
|
||||
test("fails closed for v3 runtime rendering and session support", () => {
|
||||
expect(supportsSessionRuntime(directBindings)).toBe(true);
|
||||
expect(() => renderRuntimeConfig(workspaceV3, directBindings, paths))
|
||||
.toThrow(/unsupported|schema version 3|qdrant|ollama_internal/i);
|
||||
});
|
||||
|
||||
test("omits direct TLS fields when binding validation did not retain a file path", () => {
|
||||
const dwhValues = { ...directBindings.dwh.values };
|
||||
const vectorValues = { ...directBindings.vector.values };
|
||||
|
||||
@@ -31,6 +31,29 @@ semantic_index:
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
const workspaceV3 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct, rest_api]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
@@ -196,3 +219,12 @@ test("never treats a vector reader credential as the optional writer binding", (
|
||||
values: {},
|
||||
});
|
||||
});
|
||||
|
||||
test("fails closed for v3 external semantic bindings", () => {
|
||||
expect(() => resolveBinding(workspaceV3, "VECTOR", {}, ["/run/secrets"]))
|
||||
.toThrow(/unsupported|schema version 3|semantic/i);
|
||||
expect(() => resolveBinding(workspaceV3, "EMBEDDING", {}, ["/run/secrets"]))
|
||||
.toThrow(/unsupported|schema version 3|semantic/i);
|
||||
expect(() => resolveRuntimeBindings(workspaceV3, {}, ["/run/secrets"]))
|
||||
.toThrow(/unsupported|schema version 3|semantic/i);
|
||||
});
|
||||
|
||||
@@ -39,6 +39,29 @@ llm_policy:
|
||||
- zai/glm-5.2
|
||||
- openai/gpt-5
|
||||
`);
|
||||
const workspaceV3 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct, rest_api]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
|
||||
test("generates stable FILE-based secret requirements from an immutable ID", () => {
|
||||
const contract = buildInstallationContract(validWorkspace);
|
||||
@@ -278,3 +301,13 @@ test("validates public contract and documentation inputs at runtime", () => {
|
||||
expect(() => buildInstallationContract(unsafeWorkspace)).toThrow(/id/i);
|
||||
expect(() => renderWorkspaceDocs(unsafeWorkspace)).toThrow(/id/i);
|
||||
});
|
||||
|
||||
test("v3 installation contract omits external vector and embedding bindings", () => {
|
||||
const contract = buildInstallationContract(workspaceV3);
|
||||
const names = contract.variables.map((variable) => variable.name);
|
||||
|
||||
expect(names).toContain("THT_WS_PSD_CLINICAL_DWH_TRANSPORT");
|
||||
expect(names.some((name) => name.includes("_VECTOR_"))).toBe(false);
|
||||
expect(names.some((name) => name.includes("_EMBEDDING_"))).toBe(false);
|
||||
expect(renderWorkspaceDocs(workspaceV3).markdown).not.toContain("Embedding service");
|
||||
});
|
||||
|
||||
@@ -128,6 +128,18 @@ test("rejects unknown fields in schema v3 semantic identity", () => {
|
||||
expect(() => parseWorkspaceYaml(withUnknownField)).toThrow(/unrecognized key/i);
|
||||
});
|
||||
|
||||
test("rejects legacy semantic connector fields and diagnostics in schema v3", () => {
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace(
|
||||
" collection: psd-clinical\n",
|
||||
" collection: psd-clinical\n database: postgres\n",
|
||||
))).toThrow(/unrecognized key|database/i);
|
||||
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace(
|
||||
"llm_policy:\n",
|
||||
"diagnostics:\n vector_rest:\n metadata:\n method: GET\n path: /metadata\n auth: bearer\n response:\n collection: collection\n dimensions: dimensions\n distance: distance\nllm_policy:\n",
|
||||
))).toThrow(/unrecognized key|vector_rest/i);
|
||||
});
|
||||
|
||||
test("keeps v1 and v2 descriptors parseable but non-operational", () => {
|
||||
const v1Yaml = `workspace:
|
||||
schema_version: 1
|
||||
|
||||
Reference in New Issue
Block a user