feat(cli): validate prerequisites and seal installation plans

This commit is contained in:
Codex
2026-09-28 17:03:25 +02:00
parent b9c3369e7b
commit 55f3569e55
30 changed files with 2271 additions and 4 deletions
@@ -13,6 +13,8 @@ const database = databaseConfigurationSchema.extend({
});
const bootstrap = z.object({ schemaVersion: z.literal(1), databases: z.array(database).min(1).max(1000) }).strict();
export const parseDatabaseBootstrap = (value: unknown) => bootstrap.parse(value);
export interface BootstrapReference { field: string; path: string }
/** Offline bootstrap boundary: runtime Catalog owns the resulting bindings after import. */
@@ -0,0 +1,47 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { parseWorkspaceYaml } from "../workspaces/schema.js";
import { evidencePolicy } from "../workspaces/evidence/preprocessing.js";
import type { parseDatabaseBootstrap } from "./bootstrap-documents.js";
import { probePublicEvidenceUrl, publicEvidenceAgent } from "./evidence-probe-http.js";
type Entry = ReturnType<typeof parseDatabaseBootstrap>["databases"][number];
/** Read-only availability probes; domain correctness and materialization remain runtime gates. */
export async function probeEvidence(entry: Entry, root: string): Promise<void> {
const evidence = parseWorkspaceYaml(readFileSync(join(root, entry.workspaceId, "workspace.yaml"), "utf8")).evidence;
if (!evidence || evidence.source.type === "filesystem") return;
if (evidencePolicy(evidence)) throw new Error("Evidence egress policy refused");
const secret = (name: keyof NonNullable<Entry["evidenceSecretFiles"]>) => {
const path = entry.evidenceSecretFiles?.[name];
if (!path) throw new Error("Evidence credential missing");
return readFileSync(path, "utf8").trim();
};
const source = evidence.source;
if (source.type === "http") {
const urls: unknown = source.authentication === "signed_urls_file"
? JSON.parse(secret("evidence.signed_urls")) : source.uris;
if (!Array.isArray(urls) || urls.length !== source.uris.length || urls.length > 1000) throw new Error("Invalid signed URLs");
for (const [index, value] of urls.entries()) {
if (typeof value !== "string") throw new Error("Invalid signed URL");
const url = new URL(value);
const provenance = new URL(source.uris[index]);
// Signed queries may authorize the same identity, never a different host/path.
if (url.origin !== provenance.origin || url.pathname !== provenance.pathname || url.username || url.password || url.hash) throw new Error("Invalid signed URL identity");
await probePublicEvidenceUrl(url);
}
return;
}
// The shared runtime policy currently permits trusted AWS endpoints with explicit file credentials.
const location = new URL(source.uri);
const client = new S3Client({
region: source.region ?? "us-east-1", maxAttempts: 1,
requestHandler: { httpsAgent: publicEvidenceAgent(), connectionTimeout: 5_000, requestTimeout: 5_000 },
credentials: { accessKeyId: secret("evidence.access_key"), secretAccessKey: secret("evidence.secret_key"),
...(entry.evidenceSecretFiles?.["evidence.session_token"] ? { sessionToken: secret("evidence.session_token") } : {}) },
});
try {
await client.send(new ListObjectsV2Command({ Bucket: location.hostname, Prefix: decodeURIComponent(location.pathname.slice(1)), MaxKeys: 1 }), { abortSignal: AbortSignal.timeout(5_000) });
} finally { client.destroy(); }
}
+53
View File
@@ -0,0 +1,53 @@
import { readFileSync } from "node:fs";
import { parse } from "yaml";
import { parseDatabaseBootstrap } from "./bootstrap-documents.js";
import { runBootstrapValidation } from "./bootstrap-cli.js";
import { createConcreteDiagnosticAdapters, type DiagnosticAdapters } from "../workspaces/diagnostics.js";
import { probeEvidence } from "./bootstrap-evidence-probes.js";
interface ProbeCheck { id: string; outcome: "passed" | "error"; field: string; action: string }
/** Uses the same read-only, authenticated connector diagnostics as the Catalog. */
export async function probeBootstrapDependencies(value: unknown, adapters: DiagnosticAdapters = createConcreteDiagnosticAdapters(), workspaceRoot?: string) {
const document = parseDatabaseBootstrap(value);
const checks: ProbeCheck[] = [];
for (const [index, entry] of document.databases.entries()) {
let outcome: ProbeCheck["outcome"] = "passed";
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5_000);
try {
if (entry.binding.transport === "ssh_tunnel") throw new Error("session transport unavailable");
await adapters.probeConnector({
role: "dwh", transport: entry.binding.transport,
host: entry.binding.host, port: entry.binding.port, user: entry.binding.username,
baseUrl: entry.binding.baseUrl,
credentialFile: entry.binding.transport === "rest_api" ? entry.secretFiles.apiKey : entry.secretFiles.password,
tlsCaFile: entry.secretFiles.tlsCa, tlsServername: entry.binding.tlsServername,
resource: { database: entry.databaseName, schema: entry.schema },
timeoutMs: 5_000, signal: controller.signal,
diagnostic: { method: "GET", path: entry.binding.restPath ?? "/health", auth: entry.binding.restAuth ?? "bearer" },
});
} catch { outcome = "error"; } finally { clearTimeout(timer); }
checks.push({ id: `database-${index}`, outcome, field: `database-bootstrap.databases.${index}`,
action: entry.binding.transport === "ssh_tunnel"
? "Choose postgres_direct or rest_api for NL-to-SQL practice; SSH diagnostics alone cannot establish session readiness."
: "Require an authenticated read-only connection and access to the configured database/schema; correct endpoint, permissions or protected credentials." });
if (workspaceRoot) {
let evidenceOutcome: ProbeCheck["outcome"] = "passed";
try { await probeEvidence(entry, workspaceRoot); } catch { evidenceOutcome = "error"; }
checks.push({ id: `evidence-${index}`, outcome: evidenceOutcome, field: `workspaces.${index}.evidence`, action: "Require readable local Evidence or authenticated bounded HTTP/S3 access under the canonical egress policy; domain meaning is verified during practice." });
}
}
return { schema_version: 1, ok: checks.every((check) => check.outcome === "passed"), checks };
}
export async function runBootstrapProbes(args: string[]) {
const validation = runBootstrapValidation(args);
if (validation.status !== 0) return validation;
try {
const report = await probeBootstrapDependencies(parse(readFileSync(args[3], "utf8")), undefined, args[1]);
return { status: report.ok ? 0 : 1, output: JSON.stringify(report) };
} catch {
return { status: 1, output: JSON.stringify({ schema_version: 1, ok: false, checks: [{ id: "database-probes", outcome: "error", field: "database-bootstrap", action: "Revalidate prepared documents and protected credential references." }] }) };
}
}
@@ -0,0 +1,57 @@
import { lookup } from "node:dns/promises";
import { request as httpRequest } from "node:http";
import { request as httpsRequest, Agent } from "node:https";
import type { LookupFunction } from "node:net";
import ipaddr from "ipaddr.js";
const refused = () => new Error("Evidence network policy refused");
function normalizedPublicAddress(value: string): string {
const address = ipaddr.process(value);
if (address.range() !== "unicast") throw refused();
return address.toString();
}
/** Reject the entire DNS answer set, then pin the connection to that verified set. */
export async function resolvePublicEvidenceHost(hostname: string) {
const values = await lookup(hostname.replace(/^\[|\]$/g, ""), { all: true });
if (!values.length) throw refused();
values.forEach((value) => normalizedPublicAddress(value.address));
return values;
}
const publicLookup: LookupFunction = (hostname, options, callback) => {
void resolvePublicEvidenceHost(hostname).then((values) => {
if (options.all) callback(null, values);
else callback(null, values[0].address, values[0].family);
}, () => callback(refused(), "", 0));
};
// Node's direct agent does not inherit HTTP proxy environment or ambient credentials.
export const publicEvidenceAgent = () => new Agent({ lookup: publicLookup });
export async function probePublicEvidenceUrl(url: URL): Promise<void> {
if (!['http:', 'https:'].includes(url.protocol)) throw refused();
const signal = AbortSignal.timeout(5_000);
const values = await Promise.race([
resolvePublicEvidenceHost(url.hostname),
new Promise<never>((_, reject) => signal.addEventListener("abort", () => reject(refused()), { once: true })),
]);
signal.throwIfAborted();
const allowed = new Set(values.map((value) => normalizedPublicAddress(value.address)));
const pinned: LookupFunction = (_hostname, options, callback) => {
if (options.all) callback(null, values);
else callback(null, values[0].address, values[0].family);
};
await new Promise<void>((resolve, reject) => {
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
method: "GET", lookup: pinned, signal, agent: false,
}, (response) => {
try {
const peer = response.socket.remoteAddress;
if (!peer || !allowed.has(normalizedPublicAddress(peer)) || !response.statusCode || response.statusCode < 200 || response.statusCode >= 300) throw refused();
resolve();
} catch { reject(refused()); } finally { response.destroy(); }
});
request.on("error", () => reject(refused()));
request.end();
});
}
+3 -1
View File
@@ -1,8 +1,10 @@
/** Compiled with its runtime for the host CLI: no installation, Docker or host Node required. */
import { runWorkspaceDocuments } from "./workspaces/documents.js";
import { runBootstrapValidation } from "./catalog/bootstrap-cli.js";
import { runBootstrapProbes } from "./catalog/bootstrap-probes.js";
const args = process.argv.slice(2);
const result = args[0] === "bootstrap" ? runBootstrapValidation(args.slice(1)) : runWorkspaceDocuments(args);
const result = args[0] === "probe" ? await runBootstrapProbes(args.slice(1))
: args[0] === "bootstrap" ? runBootstrapValidation(args.slice(1)) : runWorkspaceDocuments(args);
console.log(result.output);
process.exitCode = result.status;