feat: bind evidence credentials through local files
This commit is contained in:
@@ -10,11 +10,17 @@ import {
|
||||
type WorkspaceDescriptor,
|
||||
} from "./schema.js";
|
||||
|
||||
export interface ResolvedEvidenceBinding {
|
||||
values: Record<string, string>;
|
||||
missing: string[];
|
||||
}
|
||||
|
||||
export interface RuntimeBindings {
|
||||
dwh: ResolvedBinding;
|
||||
vector: ResolvedBinding;
|
||||
vectorWriter: ResolvedBinding;
|
||||
embedding: ResolvedBinding;
|
||||
evidence: ResolvedEvidenceBinding;
|
||||
}
|
||||
|
||||
export interface ResolvedBinding {
|
||||
@@ -71,7 +77,7 @@ function isSafeSecretFile(path: string, secretRoots: readonly string[]): boolean
|
||||
|
||||
function requiredSuffixes(
|
||||
workspace: WorkspaceDescriptor,
|
||||
role: InstallationRole,
|
||||
role: Exclude<InstallationRole, "EVIDENCE">,
|
||||
transport: DwhTransport | VectorTransport,
|
||||
): readonly InstallationSuffix[] {
|
||||
if (role === "EMBEDDING") return EMBEDDING_REQUIRED_SUFFIXES;
|
||||
@@ -91,7 +97,7 @@ function requiredSuffixes(
|
||||
*/
|
||||
export function resolveBinding(
|
||||
workspace: WorkspaceDescriptor,
|
||||
role: InstallationRole,
|
||||
role: Exclude<InstallationRole, "EVIDENCE">,
|
||||
env: NodeJS.ProcessEnv,
|
||||
secretRoots: readonly string[],
|
||||
): ResolvedBinding {
|
||||
@@ -136,6 +142,39 @@ export function resolveBinding(
|
||||
return { transport: selectedTransport, values, missing };
|
||||
}
|
||||
|
||||
/** Resolve descriptor-selected Evidence credentials without reading any secret file contents. */
|
||||
export function resolveEvidenceBinding(
|
||||
workspace: WorkspaceDescriptor,
|
||||
env: NodeJS.ProcessEnv,
|
||||
secretRoots: readonly string[],
|
||||
): ResolvedEvidenceBinding {
|
||||
const descriptor = validateWorkspaceDescriptor(workspace);
|
||||
const variables = buildInstallationContract(descriptor).variables
|
||||
.filter((variable) => variable.role === "EVIDENCE");
|
||||
if (variables.length === 0) return { values: {}, missing: [] };
|
||||
|
||||
const source = "evidence" in descriptor ? descriptor.evidence?.source : undefined;
|
||||
const required = new Set<InstallationSuffix>(
|
||||
source?.type === "http"
|
||||
? ["SIGNED_URLS_FILE"]
|
||||
: source?.type === "s3"
|
||||
? ["ACCESS_KEY_FILE", "SECRET_KEY_FILE"]
|
||||
: [],
|
||||
);
|
||||
const values: Record<string, string> = {};
|
||||
const missing: string[] = [];
|
||||
for (const variable of variables) {
|
||||
const value = env[variable.name];
|
||||
const present = value !== undefined && value.trim() !== "";
|
||||
const safe = present && isSafeSecretFile(value, secretRoots);
|
||||
if ((required.has(variable.suffix) && !present) || (present && !safe)) {
|
||||
missing.push(variable.name);
|
||||
}
|
||||
if (safe) values[variable.name] = value;
|
||||
}
|
||||
return { values, missing };
|
||||
}
|
||||
|
||||
/** Resolve all runtime roles together so optional writer credentials cannot be smuggled into reader bindings. */
|
||||
export function resolveRuntimeBindings(
|
||||
workspace: WorkspaceDescriptor,
|
||||
@@ -149,6 +188,7 @@ export function resolveRuntimeBindings(
|
||||
vector: resolveBinding(descriptor, "VECTOR", env, secretRoots),
|
||||
vectorWriter: resolveBinding(descriptor, "VECTOR_WRITER", env, secretRoots),
|
||||
embedding: resolveBinding(descriptor, "EMBEDDING", env, secretRoots),
|
||||
evidence: resolveEvidenceBinding(descriptor, env, secretRoots),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,5 +197,7 @@ export function resolveRuntimeBindings(
|
||||
* session runtime has no tunnel owner. Keep activation fail-closed until that lifecycle exists.
|
||||
*/
|
||||
export function supportsSessionRuntime(bindings: RuntimeBindings): boolean {
|
||||
return bindings.dwh.transport !== "ssh_tunnel" && bindings.vector.transport !== "ssh_tunnel";
|
||||
return bindings.dwh.transport !== "ssh_tunnel"
|
||||
&& bindings.vector.transport !== "ssh_tunnel"
|
||||
&& (bindings.evidence?.missing.length ?? 0) === 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { validateWorkspaceDescriptor } from "./schema.js";
|
||||
import type { DwhTransport, VectorTransport, WorkspaceDescriptor } from "./schema.js";
|
||||
|
||||
export type InstallationRole = "DWH" | "VECTOR" | "VECTOR_WRITER" | "EMBEDDING";
|
||||
export type InstallationRole = "DWH" | "VECTOR" | "VECTOR_WRITER" | "EMBEDDING" | "EVIDENCE";
|
||||
export type InstallationSuffix =
|
||||
| "TRANSPORT"
|
||||
| "HOST"
|
||||
@@ -17,7 +17,11 @@ export type InstallationSuffix =
|
||||
| "SSH_PRIVATE_KEY_FILE"
|
||||
| "SSH_KNOWN_HOSTS_FILE"
|
||||
| "SSH_TARGET_HOST"
|
||||
| "SSH_TARGET_PORT";
|
||||
| "SSH_TARGET_PORT"
|
||||
| "SIGNED_URLS_FILE"
|
||||
| "ACCESS_KEY_FILE"
|
||||
| "SECRET_KEY_FILE"
|
||||
| "SESSION_TOKEN_FILE";
|
||||
|
||||
type ConnectorTransport = DwhTransport | VectorTransport;
|
||||
|
||||
@@ -119,6 +123,25 @@ function connectorVariables(
|
||||
];
|
||||
}
|
||||
|
||||
function evidenceVariables(
|
||||
namespace: string,
|
||||
workspace: WorkspaceDescriptor,
|
||||
): InstallationVariable[] {
|
||||
if (!("evidence" in workspace) || workspace.evidence === undefined) return [];
|
||||
const source = workspace.evidence.source;
|
||||
if (source.type === "http" && source.authentication === "signed_urls_file") {
|
||||
return [createVariable(namespace, "EVIDENCE", "SIGNED_URLS_FILE")];
|
||||
}
|
||||
if (source.type === "s3" && source.credentials === "static_files") {
|
||||
return [
|
||||
createVariable(namespace, "EVIDENCE", "ACCESS_KEY_FILE"),
|
||||
createVariable(namespace, "EVIDENCE", "SECRET_KEY_FILE"),
|
||||
createVariable(namespace, "EVIDENCE", "SESSION_TOKEN_FILE"),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function buildInstallationContract(workspace: WorkspaceDescriptor): InstallationContract {
|
||||
const descriptor = validateWorkspaceDescriptor(workspace);
|
||||
const namespace = namespaceFor(descriptor);
|
||||
@@ -143,6 +166,7 @@ export function buildInstallationContract(workspace: WorkspaceDescriptor): Insta
|
||||
...(descriptor.workspace.schema_version === 2
|
||||
? EMBEDDING_SUFFIXES.map((suffix) => createVariable(namespace, "EMBEDDING", suffix))
|
||||
: []),
|
||||
...evidenceVariables(namespace, descriptor),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -179,10 +203,10 @@ export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExampl
|
||||
"",
|
||||
"Use the following UI fields as installation bindings. Secret fields always contain file paths, never secret values.",
|
||||
"",
|
||||
...(["DWH", "VECTOR", "VECTOR_WRITER", "EMBEDDING"] as const)
|
||||
...(["DWH", "VECTOR", "VECTOR_WRITER", "EMBEDDING", "EVIDENCE"] as const)
|
||||
.filter((role) => variablesByRole.has(role))
|
||||
.flatMap((role) => [
|
||||
`## ${role === "DWH" ? "Data warehouse" : role === "VECTOR" ? "Vector store" : role === "VECTOR_WRITER" ? "Vector writer" : "Embedding service"}`,
|
||||
`## ${role === "DWH" ? "Data warehouse" : role === "VECTOR" ? "Vector store" : role === "VECTOR_WRITER" ? "Vector writer" : role === "EMBEDDING" ? "Embedding service" : "Evidence"}`,
|
||||
"",
|
||||
...(variablesByRole.get(role) ?? []).map((variable) => (
|
||||
`- \`${variable.name}\`${variable.transports ? ` (for: ${variable.transports.join(", ")})` : ""}`
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface Diagnostic {
|
||||
level: "error" | "warning" | "info";
|
||||
code: WorkspaceErrorCode | "binding_ok";
|
||||
field?: string;
|
||||
variable?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -640,9 +641,19 @@ async function diagnoseSchemaV3Workspace(
|
||||
timeoutMs: number,
|
||||
semanticRuntime: SemanticRuntimeConfig,
|
||||
): Promise<WorkspaceDiagnostics> {
|
||||
const diagnostics = [...bindings.dwh.missing]
|
||||
.sort()
|
||||
.map((field) => diagnosticError("binding_missing", field));
|
||||
const evidenceField = descriptor.evidence?.source.type === "http"
|
||||
? "evidence.source.authentication"
|
||||
: descriptor.evidence?.source.type === "s3"
|
||||
? "evidence.source.credentials"
|
||||
: undefined;
|
||||
const evidenceDiagnostics = [...bindings.evidence.missing].sort().map((variable): Diagnostic => ({
|
||||
...diagnosticError("binding_missing", evidenceField),
|
||||
variable,
|
||||
}));
|
||||
const diagnostics = [
|
||||
...[...bindings.dwh.missing].sort().map((field) => diagnosticError("binding_missing", field)),
|
||||
...evidenceDiagnostics,
|
||||
];
|
||||
if (diagnostics.length > 0) {
|
||||
return { activatable: false, diagnostics };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user