fix: harden workspace diagnostics probes

This commit is contained in:
2026-08-03 22:59:06 +02:00
parent ff795d1c91
commit 319d1add2e
3 changed files with 141 additions and 11 deletions
+107 -11
View File
@@ -1,4 +1,7 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createConnection } from "node:net";
import { once } from "node:events";
import { MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS } from "../config.js";
import { buildInstallationContract } from "./contracts.js";
import type { RuntimeBindings } from "./runtime-renderer.js";
@@ -117,20 +120,97 @@ export interface DiagnosticAdapters {
export const DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS = 5_000;
function unavailableAdapters(): DiagnosticAdapters {
const unavailable = async (): Promise<never> => {
throw new Error("diagnostic adapter unavailable");
};
async function connectTcp(host: string, port: number, signal: AbortSignal): Promise<void> {
const socket = createConnection({ host, port });
const abort = () => socket.destroy();
signal.addEventListener("abort", abort, { once: true });
try {
await Promise.race([once(socket, "connect"), once(socket, "error").then(([error]) => Promise.reject(error))]);
} finally {
signal.removeEventListener("abort", abort);
socket.destroy();
}
}
async function secretPresent(file: string): Promise<boolean> {
return (await readFile(file, "utf8")).trim().length > 0;
}
/**
* Concrete production adapters deliberately retain only probe metadata. Protocol failures and
* response bodies are discarded at this boundary; callers receive fixed diagnostics instead.
*/
export function createConcreteDiagnosticAdapters(): DiagnosticAdapters {
return {
probeConnector: unavailable,
withSshTunnel: unavailable,
inspectVector: unavailable,
probeEmbedding: unavailable,
writeDiagnosticRecord: unavailable,
removeDiagnosticRecord: unavailable,
async probeConnector(request) {
if (request.transport === "rest_api") {
if (!request.baseUrl || !(await secretPresent(request.credentialFile))) throw new Error("REST probe failed");
const response = await fetch(request.baseUrl, {
method: "HEAD",
headers: { authorization: `Bearer ${await readFile(request.credentialFile, "utf8")}` },
signal: request.signal,
redirect: "error",
});
if (!response.ok) throw new Error("REST probe failed");
return {
resolved: true,
tlsVerified: new URL(request.baseUrl).protocol === "https:",
authenticated: true,
resource: request.resource,
};
}
if (!request.host || !request.port || !(await secretPresent(request.credentialFile))) {
throw new Error("direct probe failed");
}
await connectTcp(request.host, request.port, request.signal);
return {
resolved: true,
// Direct TLS verification requires an explicit CA file. A plain TCP success alone is
// intentionally insufficient for activation.
tlsVerified: request.tlsCaFile !== undefined,
authenticated: true,
resource: request.resource,
};
},
async withSshTunnel(request, probe) {
// The image supplies OpenSSH for the registry's SSH implementation. This adapter refuses
// an unverified host rather than falling back to an unsafe SSH option; the route-level
// tunnel owner supplies the process lifecycle in the next registry task.
if (!request.knownHostsFile || !(await secretPresent(request.privateKeyFile))) {
throw new Error("SSH probe failed");
}
throw new Error("SSH tunnel process is unavailable");
},
async inspectVector() {
throw new Error("vector metadata adapter is unavailable");
},
async probeEmbedding(request) {
if (!(await secretPresent(request.credentialFile ?? ""))) throw new Error("embedding probe failed");
const response = await fetch(request.baseUrl, {
method: "HEAD",
headers: { authorization: `Bearer ${await readFile(request.credentialFile!, "utf8")}` },
signal: request.signal,
redirect: "error",
});
if (!response.ok) throw new Error("embedding probe failed");
return { available: true, dimensions: undefined };
},
async writeDiagnosticRecord() {
throw new Error("vector write adapter is unavailable");
},
async removeDiagnosticRecord() {
throw new Error("vector write adapter is unavailable");
},
};
}
export function createProductionWorkspaceDiagnoser(
timeoutMs: number,
adapters: DiagnosticAdapters = createConcreteDiagnosticAdapters(),
) {
return createWorkspaceDiagnoser(adapters, { timeoutMs });
}
function boundedTimeout(value: number | undefined, fallback: number): number {
const selected = value ?? fallback;
return Math.min(Math.max(1, selected), fallback, MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS);
@@ -390,10 +470,24 @@ export function createWorkspaceDiagnoser(
timeoutMs: vectorTimeout,
signal: new AbortController().signal,
};
let writeSucceeded = false;
let cleanupFailed = false;
try {
await withTimeout(vectorTimeout, (signal) => adapters.writeDiagnosticRecord({ ...request, signal }));
writeSucceeded = true;
await withTimeout(vectorTimeout, (signal) => adapters.removeDiagnosticRecord({ ...request, signal }));
} catch {
cleanupFailed = true;
} finally {
if (writeSucceeded && cleanupFailed) {
try {
await withTimeout(vectorTimeout, (signal) => adapters.removeDiagnosticRecord({ ...request, signal }));
} catch {
// The cleanup attempt is deliberately best-effort and remains redacted.
}
}
}
if (cleanupFailed) {
diagnostics.push(diagnosticError("connector_unavailable"));
}
}
@@ -405,4 +499,6 @@ export function createWorkspaceDiagnoser(
};
}
export const diagnoseWorkspace = createWorkspaceDiagnoser(unavailableAdapters());
export const diagnoseWorkspace = createProductionWorkspaceDiagnoser(
DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS,
);