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 };
}
+96
View File
@@ -2797,3 +2797,99 @@ test("POST /sessions bootstrap failure emits only a fixed recovery message", asy
expect(clientOutput).not.toContain("DO_NOT_LEAK");
expect(clientOutput).not.toContain("/srv/private/model-key");
});
test.each([
{ mode: "missing signed Evidence file", evidence: true, safe: false, expectedStatus: 409, reachesReadiness: false },
{ mode: "safe signed Evidence file", evidence: true, safe: true, expectedStatus: 503, reachesReadiness: true },
{ mode: "no Evidence descriptor", evidence: false, safe: false, expectedStatus: 503, reachesReadiness: true },
])("real buildApp admission handles $mode before Pi spawn", async ({
evidence, safe, expectedStatus, reachesReadiness,
}) => {
const root = mkdtempSync(path.join(tmpdir(), "thoth-evidence-admission-"));
const signedFile = path.join(root, "signed-urls.json");
writeFileSync(signedFile, '["CANARY-SIGNED-QUERY"]');
const variable = "THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE";
const previous = {
transport: process.env.THT_WS_PSD_CLINICAL_DWH_TRANSPORT,
baseUrl: process.env.THT_WS_PSD_CLINICAL_DWH_BASE_URL,
signed: process.env[variable],
};
process.env.THT_WS_PSD_CLINICAL_DWH_TRANSPORT = "rest_api";
process.env.THT_WS_PSD_CLINICAL_DWH_BASE_URL = "https://dwh.example.test";
if (safe) process.env[variable] = signedFile;
else delete process.env[variable];
const descriptor = {
...operationalWorkspace("psd-clinical"),
dwh: {
...operationalWorkspace("psd-clinical").dwh,
supported_transports: ["rest_api"],
},
diagnostics: {
dwh_rest: {
method: "GET", path: "/health", auth: "none",
response: { database: "database", schema: "schema" },
},
},
...(evidence ? {
evidence: {
source: {
type: "http",
uris: ["https://evidence.example.test/guide.md"],
authentication: "signed_urls_file",
connect_timeout_ms: 5_000,
read_timeout_ms: 30_000,
max_bytes: 10 * 1024 * 1024,
max_redirects: 5,
allow_private_hosts: false,
max_cache_bytes: 64 * 1024 * 1024,
},
policy: { max_chunk_chars: 4_000, retain_published_generations: 3 },
},
} : {}),
} as any;
const canonicalBefore = JSON.stringify(descriptor);
const ensure = vi.fn(async () => ({ ok: false, code: "workspace_not_activatable" as const }));
const createFor = vi.fn();
const abort = vi.fn(async () => {});
const revision = {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/psd-clinical.yaml`,
state: "operational" as const,
};
try {
const app = buildRealApp(loadConfig({
THT_HARNESS_DIR: "../harness",
THT_WORKSPACE_SECRET_ROOTS: root,
}), {
thtRunner: { sessionNew: vi.fn(), searchPack: async () => {} } as any,
readiness: { ensure } as any,
mgr: { get: () => undefined, createFor } as any,
getSettings: () => ({ workspace: "psd-clinical" }) as any,
workspaceRegistry: {
acquireSessionRevision: vi.fn(async () => ({
workspace: descriptor, revision, abort, markPersisted: vi.fn(async () => {}),
})),
} as any,
});
const response = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
expect(response.statusCode).toBe(expectedStatus);
expect(ensure).toHaveBeenCalledTimes(reachesReadiness ? 1 : 0);
expect(createFor).not.toHaveBeenCalled();
expect(JSON.stringify(descriptor)).toBe(canonicalBefore);
expect(revision.commit).toBe("a".repeat(40));
expect(response.body).not.toContain("CANARY-SIGNED-QUERY");
} finally {
const restore = (name: string, value: string | undefined) => {
if (value === undefined) delete process.env[name];
else process.env[name] = value;
};
restore("THT_WS_PSD_CLINICAL_DWH_TRANSPORT", previous.transport);
restore("THT_WS_PSD_CLINICAL_DWH_BASE_URL", previous.baseUrl);
restore(variable, previous.signed);
rmSync(root, { recursive: true, force: true });
}
});
+117 -2
View File
@@ -1,8 +1,8 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, test } from "vitest";
import { resolveBinding, resolveRuntimeBindings } from "../src/workspaces/bindings.js";
import { resolveBinding, resolveEvidenceBinding, resolveRuntimeBindings, supportsSessionRuntime } from "../src/workspaces/bindings.js";
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
const workspace = parseWorkspaceYaml(`workspace:
@@ -254,3 +254,118 @@ test("schema v3 ignores external semantic binding variables and reports only DWH
expect(bindings.vector.values).toEqual({});
expect(bindings.embedding.values).toEqual({});
});
function withEvidence(source: Record<string, unknown>) {
return parseWorkspaceYaml(`workspace:
schema_version: 3
id: psd-clinical
name: Policlinico San Donato
language: it
dwh:
engine: postgres
database: postgres
schema: datawarehouse
supported_transports: [postgres_direct]
semantic_index:
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
llm_policy: { allowed: [zai/glm-5.2] }
evidence:
source: ${JSON.stringify(source)}
`);
}
const evidenceVariable = (suffix: string) => `THT_WS_PSD_CLINICAL_EVIDENCE_${suffix}`;
test.each([
{ type: "filesystem", uri: "workspace-content/psd-clinical/evidence" },
{ type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "none" },
{ type: "s3", uri: "s3://clinical-evidence/published/", credentials: "ambient" },
])("does not resolve Evidence variables for $type modes without file credentials", (source) => {
expect(resolveEvidenceBinding(withEvidence(source), {
[evidenceVariable("SIGNED_URLS_FILE")]: "/CANARY/http",
[evidenceVariable("ACCESS_KEY_FILE")]: "/CANARY/access",
}, ["/run/secrets"])).toEqual({ values: {}, missing: [] });
});
test("requires only a safe HTTP signed-URL file and never reads its contents", () => {
const signed = secretPath("evidence-signed-urls");
writeFileSync(signed.path, "CANARY-SIGNED-URL-CONTENT");
const source = withEvidence({
type: "http",
uris: ["https://evidence.example.test/guide.md"],
authentication: "signed_urls_file",
});
const variable = evidenceVariable("SIGNED_URLS_FILE");
expect(resolveEvidenceBinding(source, {}, [signed.root]).missing).toEqual([variable]);
const resolved = resolveEvidenceBinding(source, {
[variable]: signed.path,
[evidenceVariable("ACCESS_KEY_FILE")]: signed.path,
}, [signed.root]);
expect(resolved).toEqual({ values: { [variable]: signed.path }, missing: [] });
expect(JSON.stringify(resolved)).not.toContain("CANARY-SIGNED-URL-CONTENT");
});
test("requires S3 access and secret files together while accepting an optional safe session token", () => {
const access = secretPath("evidence-access");
const secret = secretPath("evidence-secret");
const token = secretPath("evidence-token");
const source = withEvidence({
type: "s3", uri: "s3://clinical-evidence/published/", credentials: "static_files",
});
const env = {
[evidenceVariable("ACCESS_KEY_FILE")]: access.path,
[evidenceVariable("SECRET_KEY_FILE")]: secret.path,
[evidenceVariable("SESSION_TOKEN_FILE")]: token.path,
[evidenceVariable("SIGNED_URLS_FILE")]: access.path,
};
expect(resolveEvidenceBinding(source, {
[evidenceVariable("ACCESS_KEY_FILE")]: access.path,
}, [access.root]).missing).toEqual([evidenceVariable("SECRET_KEY_FILE")]);
expect(resolveEvidenceBinding(source, env, [access.root, secret.root, token.root])).toEqual({
values: {
[evidenceVariable("ACCESS_KEY_FILE")]: access.path,
[evidenceVariable("SECRET_KEY_FILE")]: secret.path,
[evidenceVariable("SESSION_TOKEN_FILE")]: token.path,
},
missing: [],
});
});
test("rejects relative, missing, directory, unreadable, and escaping symlink Evidence paths", () => {
const allowed = secretPath("valid");
const outside = secretPath("outside");
const directory = join(allowed.root, "directory");
mkdirSync(directory);
const link = join(allowed.root, "escape");
symlinkSync(outside.path, link);
const unreadable = join(allowed.root, "unreadable");
writeFileSync(unreadable, "secret");
chmodSync(unreadable, 0o000);
const source = withEvidence({
type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "signed_urls_file",
});
const variable = evidenceVariable("SIGNED_URLS_FILE");
for (const path of ["relative", join(allowed.root, "missing"), directory, unreadable, link]) {
expect(resolveEvidenceBinding(source, { [variable]: path }, [allowed.root])).toEqual({
values: {}, missing: [variable],
});
}
chmodSync(unreadable, 0o600);
});
test("includes Evidence binding completeness in session runtime support without changing v3 compatibility", () => {
const unsigned = resolveRuntimeBindings(workspaceV3, {}, ["/run/secrets"]);
expect(unsigned.evidence).toEqual({ values: {}, missing: [] });
expect(supportsSessionRuntime(unsigned)).toBe(true);
const signed = resolveRuntimeBindings(withEvidence({
type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "signed_urls_file",
}), {}, ["/run/secrets"]);
expect(signed.evidence.missing).toEqual([evidenceVariable("SIGNED_URLS_FILE")]);
expect(supportsSessionRuntime(signed)).toBe(false);
});
+59
View File
@@ -190,6 +190,7 @@ llm_policy:
missing: [],
values: { THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.internal" },
},
evidence: { missing: [], values: {} },
};
const writerVariables = buildInstallationContract(writerWorkspace).variables
@@ -326,3 +327,61 @@ test("v3 installation contract omits external vector and embedding bindings", ()
expect(names.some((name) => name.includes("_EMBEDDING_"))).toBe(false);
expect(renderWorkspaceDocs(workspaceV3).markdown).not.toContain("Embedding service");
});
test.each([
{
mode: "signed HTTP",
source: {
type: "http", uris: ["https://evidence.example.test/guide.md"],
authentication: "signed_urls_file",
},
expected: ["THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE"],
},
{
mode: "static S3",
source: { type: "s3", uri: "s3://clinical-evidence/published/", credentials: "static_files" },
expected: [
"THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE",
"THT_WS_PSD_CLINICAL_EVIDENCE_SECRET_KEY_FILE",
"THT_WS_PSD_CLINICAL_EVIDENCE_SESSION_TOKEN_FILE",
],
},
])("generates source-specific $mode Evidence file bindings", ({ source, expected }) => {
const descriptor = parseWorkspaceYaml(`${renderWorkspaceWithoutEvidence()}evidence:\n source: ${JSON.stringify(source)}\n`);
const contract = buildInstallationContract(descriptor);
const evidence = contract.variables.filter((variable) => variable.role === "EVIDENCE");
expect(contract.namespace).toBe("PSD_CLINICAL");
expect(evidence.map((variable) => variable.name)).toEqual(expected);
expect(evidence.every((variable) => variable.secret)).toBe(true);
expect(renderWorkspaceDocs(descriptor).envExample).not.toContain("CANARY-SECRET");
});
test.each([
{ type: "filesystem", uri: "workspace-content/psd-clinical/evidence" },
{ type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "none" },
{ type: "s3", uri: "s3://clinical-evidence/published/", credentials: "ambient" },
])("omits Evidence installation variables for $type modes without file credentials", (source) => {
const descriptor = parseWorkspaceYaml(`${renderWorkspaceWithoutEvidence()}evidence:\n source: ${JSON.stringify(source)}\n`);
expect(buildInstallationContract(descriptor).variables.some((variable) => variable.role === "EVIDENCE"))
.toBe(false);
});
function renderWorkspaceWithoutEvidence(): string {
return `workspace:
schema_version: 3
id: psd-clinical
name: Policlinico San Donato
language: it
dwh:
engine: postgres
database: postgres
schema: datawarehouse
supported_transports: [postgres_direct]
semantic_index:
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
llm_policy: { allowed: [zai/glm-5.2] }
`;
}
@@ -141,6 +141,7 @@ const bindings: RuntimeBindings = {
THT_WS_PSD_CLINICAL_EMBEDDING_TLS_CA_FILE: "/run/secrets/embedding-ca",
},
},
evidence: { missing: [], values: {} },
};
const writerBindings: RuntimeBindings = {
@@ -190,6 +191,7 @@ const bindingsV3: RuntimeBindings = {
vector: { transport: "rest_api", missing: [], values: {} },
vectorWriter: { transport: "rest_api", missing: [], values: {} },
embedding: { transport: "rest_api", missing: [], values: {} },
evidence: { missing: [], values: {} },
};
function successfulAdapters(overrides: Partial<DiagnosticAdapters> = {}): DiagnosticAdapters {
@@ -960,3 +962,55 @@ test("attempts bounded cleanup when a timed-out write may already have created t
expect(adapters.removeDiagnosticRecord).toHaveBeenCalledOnce();
expect(result.activatable).toBe(false);
});
test.each([
{
source: {
type: "http", uris: ["https://evidence.example.test/guide.md"],
authentication: "signed_urls_file",
},
field: "evidence.source.authentication",
variable: "THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE",
},
{
source: { type: "s3", uri: "s3://clinical-evidence/published/", credentials: "static_files" },
field: "evidence.source.credentials",
variable: "THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE",
},
])("reports sanitized v3 Evidence binding diagnostics for $field", async ({ source, field, variable }) => {
const descriptor = parseWorkspaceYaml(`${renderEvidenceWorkspace()}evidence:\n source: ${JSON.stringify(source)}\n`);
const resolved: RuntimeBindings = {
...resolveRuntimeBindings(descriptor, {
[variable]: "CANARY-UNSAFE-RELATIVE-PATH",
}, ["/run/secrets"]),
dwh: bindings.dwh,
};
const result = await createProductionWorkspaceDiagnoser(5_000)(descriptor, resolved, { writeProbe: false });
expect(result).toEqual({
activatable: false,
diagnostics: expect.arrayContaining([expect.objectContaining({
code: "binding_missing", field, variable,
})]),
});
expect(JSON.stringify(result)).not.toContain("CANARY-UNSAFE-RELATIVE-PATH");
});
function renderEvidenceWorkspace(): string {
return `workspace:
schema_version: 3
id: psd-clinical
name: Policlinico San Donato
language: it
dwh:
engine: postgres
database: warehouse
schema: datawarehouse
supported_transports: [postgres_direct]
semantic_index:
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
llm_policy: { allowed: [zai/glm-5.2] }
`;
}