fix: align workspace diagnostic contracts

This commit is contained in:
2026-08-04 00:37:57 +02:00
parent f6494fd8ef
commit 565e93a456
12 changed files with 346 additions and 42 deletions
+30 -2
View File
@@ -10,6 +10,13 @@ import {
type WorkspaceDescriptor,
} from "./schema.js";
export interface RuntimeBindings {
dwh: ResolvedBinding;
vector: ResolvedBinding;
vectorWriter: ResolvedBinding;
embedding: ResolvedBinding;
}
export interface ResolvedBinding {
transport: DwhTransport | VectorTransport;
values: Record<string, string>;
@@ -63,12 +70,19 @@ function isSafeSecretFile(path: string, secretRoots: readonly string[]): boolean
}
function requiredSuffixes(
workspace: WorkspaceDescriptor,
role: InstallationRole,
transport: DwhTransport | VectorTransport,
): readonly InstallationSuffix[] {
if (role === "EMBEDDING") return EMBEDDING_REQUIRED_SUFFIXES;
if (role === "VECTOR_WRITER") return ["API_KEY_FILE"];
return REQUIRED_SUFFIXES[role][transport] ?? [];
const required = REQUIRED_SUFFIXES[role][transport] ?? [];
const diagnostic = role === "DWH"
? workspace.diagnostics?.dwh_rest
: workspace.diagnostics?.vector_rest?.metadata;
return transport === "rest_api" && diagnostic?.auth === "none"
? required.filter((suffix) => suffix !== "API_KEY_FILE")
: required;
}
/**
@@ -98,7 +112,7 @@ export function resolveBinding(
missing.push(transportVariable.name);
}
const required = new Set(requiredSuffixes(role, selectedTransport));
const required = new Set(requiredSuffixes(canonical, role, selectedTransport));
const values: Record<string, string> = {};
for (const variable of variables) {
if (variable.suffix === "TRANSPORT") continue;
@@ -115,3 +129,17 @@ export function resolveBinding(
return { transport: selectedTransport, values, missing };
}
/** Resolve all runtime roles together so optional writer credentials cannot be smuggled into reader bindings. */
export function resolveRuntimeBindings(
workspace: WorkspaceDescriptor,
env: NodeJS.ProcessEnv,
secretRoots: readonly string[],
): RuntimeBindings {
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),
};
}
+19 -9
View File
@@ -131,7 +131,9 @@ export interface WriteDiagnosticRecordRequest {
credentialFile?: string;
tlsCaFile?: string;
baseUrl?: string;
diagnostic?: RestDiagnosticRequest;
diagnostic?: RestDiagnosticRequest & {
response: { operation: string };
};
}
export interface DirectProtocolFactory {
@@ -145,7 +147,7 @@ export interface DatabaseDiagnosticClient {
export interface DatabaseDiagnosticClientFactory {
connect(request: {
host: string; port: number; database: string; user: string; credentialFile: string; tlsCaFile: string; signal: AbortSignal;
host: string; port: number; database: string; user: string; credentialFile: string; tlsCaFile?: string; signal: AbortSignal;
}): Promise<DatabaseDiagnosticClient>;
}
@@ -296,11 +298,13 @@ export function createConcreteDiagnosticAdapters(
},
};
const databaseClient = dependencies.databaseClient ?? {
async connect(request: { host: string; port: number; database: string; user: string; credentialFile: string; tlsCaFile: string; signal: AbortSignal }) {
async connect(request: { host: string; port: number; database: string; user: string; credentialFile: string; tlsCaFile?: string; signal: AbortSignal }) {
const client = new Client({
host: request.host, port: request.port, database: request.database, user: request.user,
password: (await readFile(request.credentialFile, "utf8")).trim(),
ssl: { ca: await readFile(request.tlsCaFile, "utf8"), rejectUnauthorized: true },
ssl: request.tlsCaFile
? { ca: await readFile(request.tlsCaFile, "utf8"), rejectUnauthorized: true }
: { rejectUnauthorized: true },
connectionTimeoutMillis: 5_000,
});
const abort = () => { void client.end(); };
@@ -317,7 +321,7 @@ export function createConcreteDiagnosticAdapters(
};
const directProtocol = dependencies.directProtocol ?? {
async probe(request: ConnectorDiagnosticRequest): Promise<ConnectorDiagnosticResult> {
if (!request.host || !request.port || !request.user || !request.credentialFile || !request.tlsCaFile
if (!request.host || !request.port || !request.user || !request.credentialFile
|| !(await secretPresent(request.credentialFile))) {
throw new Error("direct probe failed");
}
@@ -388,7 +392,7 @@ export function createConcreteDiagnosticAdapters(
async inspectVector(request) {
if (request.transport === "pgvector_direct" || request.transport === "ssh_tunnel") {
const resource = request.resource;
if (!request.host || !request.port || !request.user || !request.credentialFile || !request.tlsCaFile
if (!request.host || !request.port || !request.user || !request.credentialFile
|| !resource?.database || !resource.schema || !(await secretPresent(request.credentialFile))) {
throw new Error("vector metadata adapter is unavailable");
}
@@ -454,7 +458,10 @@ export function createConcreteDiagnosticAdapters(
signal: request.signal,
redirect: "error",
});
if (!response.ok) throw new Error("vector write adapter is unavailable");
const payload = await response.json().catch(() => undefined) as Record<string, unknown> | undefined;
if (!response.ok || !payload || payload[request.diagnostic.response.operation] !== "create") {
throw new Error("vector write adapter is unavailable");
}
},
async removeDiagnosticRecord(request) {
if (!request.baseUrl || !request.diagnostic || request.tlsCaFile) throw new Error("vector write adapter is unavailable");
@@ -468,7 +475,10 @@ export function createConcreteDiagnosticAdapters(
signal: request.signal,
redirect: "error",
});
if (!response.ok) throw new Error("vector write adapter is unavailable");
const payload = await response.json().catch(() => undefined) as Record<string, unknown> | undefined;
if (!response.ok || !payload || payload[request.diagnostic.response.operation] !== "remove") {
throw new Error("vector write adapter is unavailable");
}
},
};
}
@@ -789,7 +799,7 @@ export function createWorkspaceDiagnoser(
&& bindings.vector.transport === "rest_api"
&& !diagnostics.some((diagnostic) => diagnostic.level === "error")
) {
const credentialFile = bindings.vector.values[bindingName(canonical, "VECTOR_WRITER", "API_KEY_FILE")];
const credentialFile = bindings.vectorWriter.values[bindingName(canonical, "VECTOR_WRITER", "API_KEY_FILE")];
if (!credentialFile) return { activatable: true, diagnostics };
const readerCredentialFile = bindings.vector.values[bindingName(canonical, "VECTOR", "API_KEY_FILE")];
if (readerCredentialFile && await sameSecretFile(credentialFile, readerCredentialFile)) {
+2 -7
View File
@@ -1,13 +1,8 @@
import { stringify } from "yaml";
import { buildInstallationContract } from "./contracts.js";
import { validateCanonicalWorkspace, type WorkspaceDescriptor } from "./schema.js";
import type { ResolvedBinding } from "./bindings.js";
export interface RuntimeBindings {
dwh: ResolvedBinding;
vector: ResolvedBinding;
embedding: ResolvedBinding;
}
import type { ResolvedBinding, RuntimeBindings } from "./bindings.js";
export type { RuntimeBindings } from "./bindings.js";
export interface RuntimePaths {
sessions: string;
+10 -2
View File
@@ -25,7 +25,11 @@ export interface CanonicalDiagnostics {
metadata: RestDiagnosticRequest & {
response: { collection: string; dimensions: string; distance: string };
};
reversible_probe?: RestDiagnosticRequest & { method: "POST" };
reversible_probe?: RestDiagnosticRequest & {
method: "POST";
auth: Exclude<DiagnosticAuthMode, "none">;
response: { operation: string };
};
};
embedding?: RestDiagnosticRequest & { response: { model: string; dimensions: string } };
}
@@ -124,7 +128,11 @@ const vectorMetadataDiagnostic = restDiagnosticRequest.extend({
distance: responseField,
}).strict(),
}).strict();
const reversibleVectorProbe = restDiagnosticRequest.extend({ method: z.literal("POST") }).strict();
const reversibleVectorProbe = restDiagnosticRequest.extend({
method: z.literal("POST"),
auth: z.enum(["bearer", "x-api-key"]),
response: z.object({ operation: responseField }).strict(),
}).strict();
const embeddingDiagnostic = restDiagnosticRequest.extend({
response: z.object({ model: responseField, dimensions: responseField }).strict(),
}).strict();