feat: bind evidence credentials through local files

This commit is contained in:
2026-08-09 19:03:36 +02:00
parent c7a369f436
commit 7126b567b0
11 changed files with 839 additions and 18 deletions
+45 -3
View File
@@ -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;
}
+28 -4
View File
@@ -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(", ")})` : ""}`
+14 -3
View File
@@ -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 };
}