fix: align workspace diagnostic contracts
This commit is contained in:
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user