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;
}