fix: stabilize hwlab web-probe script navigation

This commit is contained in:
Codex
2026-06-18 10:21:02 +00:00
parent 5ba0e9b0c7
commit 79a0e53834
4 changed files with 309 additions and 7 deletions
+144 -5
View File
@@ -5131,17 +5131,25 @@ function runNodeWebProbeScript(
): Record<string, unknown> {
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.password ?? "");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const parsedReport = parseJsonObject(result.stdout);
const stdoutReport = parseJsonObject(result.stdout);
const runPaths = webProbeScriptRunPathsFromStderr(result.stderr);
const recoveredReport = stdoutReport === null ? readNodeWebProbeScriptReport(options, spec, runPaths.reportPath) : null;
const recoveredArtifacts = stdoutReport === null || result.timedOut ? readNodeWebProbeScriptArtifacts(options, spec, runPaths.runDir) : null;
const parsedReport = stdoutReport ?? recoveredReport?.report ?? null;
const report = compactWebProbeScriptResult(parsedReport);
const passed = result.exitCode === 0 && report?.ok === true;
const summary = nullableRecord(report?.summary);
const stdoutBytes = Buffer.byteLength(result.stdout, "utf8");
const outputFailureKind = parsedReport === null
? stdoutBytes > 64 * 1024
? result.timedOut
? "web-probe-command-timeout"
: stdoutBytes > 64 * 1024
? "web-probe-output-too-large"
: "web-probe-report-parse-failed"
: null;
const degradedReason = typeof summary?.degradedReason === "string"
const degradedReason = result.timedOut
? "web-probe-command-timeout"
: typeof summary?.degradedReason === "string"
? summary.degradedReason
: typeof report?.failureKind === "string"
? report.failureKind
@@ -5150,19 +5158,37 @@ function runNodeWebProbeScript(
: result.timedOut
? "web-probe-command-timeout"
: null;
const failureKind = typeof summary?.failureKind === "string"
const failureKind = result.timedOut
? "web-probe-command-timeout"
: typeof summary?.failureKind === "string"
? summary.failureKind
: typeof report?.failureKind === "string"
? report.failureKind
: outputFailureKind;
const effectiveSummary = summary ?? (outputFailureKind === null ? null : {
const effectiveSummary = summary !== null ? {
...summary,
transportTimedOut: result.timedOut,
recoveredFrom: stdoutReport !== null ? "stdout" : recoveredReport?.source ?? null,
} : (outputFailureKind === null ? null : {
ok: false,
status: "blocked",
degradedReason: outputFailureKind,
failureKind: outputFailureKind,
failedCondition: outputFailureKind === "web-probe-output-too-large"
? "remote web-probe stdout exceeded the bounded summary parser input"
: outputFailureKind === "web-probe-command-timeout"
? "remote web-probe command timed out before stdout returned a parseable report"
: "remote web-probe stdout did not contain a parseable JSON report",
runDir: runPaths.runDir,
reportPath: runPaths.reportPath,
reportLoad: recoveredReport === null ? null : {
source: recoveredReport.source,
path: recoveredReport.path,
degradedReason: recoveredReport.degradedReason,
result: recoveredReport.result === null ? null : compactCommandResultRedacted(recoveredReport.result, [material.password ?? ""]),
},
screenshots: recoveredArtifacts?.screenshots ?? [],
artifacts: recoveredArtifacts?.artifacts ?? null,
stdoutBytes,
stderrTail: result.stderr.trim().slice(-2000),
valuesRedacted: true,
@@ -5186,6 +5212,18 @@ function runNodeWebProbeScript(
failureKind,
summary: effectiveSummary,
probe: report,
reportLoad: stdoutReport !== null ? { source: "stdout", path: report?.reportPath ?? null, degradedReason: null } : recoveredReport === null ? null : {
source: recoveredReport.source,
path: recoveredReport.path,
degradedReason: recoveredReport.degradedReason,
result: recoveredReport.result === null ? null : compactCommandResultRedacted(recoveredReport.result, [material.password ?? ""]),
},
recoveredArtifacts: recoveredArtifacts === null ? null : {
source: recoveredArtifacts.source,
degradedReason: recoveredArtifacts.degradedReason,
result: recoveredArtifacts.result === null ? null : compactCommandResultRedacted(recoveredArtifacts.result, [material.password ?? ""]),
artifacts: recoveredArtifacts.artifacts,
},
result: compactResult,
valuesRedacted: true,
};
@@ -5199,7 +5237,10 @@ function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secre
"run_root=.state/web-probe-script",
"mkdir -p \"$run_root\"",
"run_dir=$(mktemp -d \"$run_root/run.XXXXXX\")",
"report_file=\"$run_dir/web-probe-script-report.json\"",
"chmod 700 \"$run_dir\"",
"printf 'UNIDESK_WEB_PROBE_RUN_DIR=%s\\n' \"$run_dir\" >&2",
"printf 'UNIDESK_WEB_PROBE_REPORT_PATH=%s\\n' \"$report_file\" >&2",
"user_script=\"$run_dir/user-script.mjs\"",
"runner=\"$run_dir/runner.mjs\"",
`node -e "require('fs').writeFileSync(process.argv[1], Buffer.from(process.argv[2], 'base64'))" "$user_script" ${shellQuote(userScriptB64)}`,
@@ -5217,6 +5258,104 @@ function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secre
].join("\n");
}
function webProbeScriptRunPathsFromStderr(stderr: string): { runDir: string | null; reportPath: string | null } {
const runDir = lineValueFromText(stderr, "UNIDESK_WEB_PROBE_RUN_DIR");
const markedReportPath = lineValueFromText(stderr, "UNIDESK_WEB_PROBE_REPORT_PATH");
const reportPath = markedReportPath ?? (runDir === null ? null : `${runDir.replace(/\/$/u, "")}/web-probe-script-report.json`);
return {
runDir: isSafeWebProbeScriptRunDir(runDir) ? runDir : null,
reportPath: isSafeWebProbeScriptReportPath(reportPath) ? reportPath : null,
};
}
function lineValueFromText(text: string, name: string): string | null {
const pattern = new RegExp(`^${name}=([^\\r\\n]+)$`, "mu");
const match = text.match(pattern);
return match ? match[1].trim() : null;
}
function readNodeWebProbeScriptReport(
options: NodeWebProbeScriptOptions,
spec: HwlabRuntimeLaneSpec,
reportPath: string | null,
): { source: string; report: Record<string, unknown> | null; result: CommandResult | null; degradedReason: string | null; path: string | null } | null {
if (!reportPath) return null;
if (!isSafeWebProbeScriptReportPath(reportPath)) return { source: "unsafe-path", report: null, result: null, degradedReason: "web-probe-report-path-invalid", path: reportPath };
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, `test -f ${shellQuote(reportPath)} && cat ${shellQuote(reportPath)}`, 55);
if (result.exitCode !== 0 || result.timedOut) {
return { source: "report-file", report: null, result, degradedReason: result.timedOut ? "web-probe-command-timeout" : "web-probe-report-read-failed", path: reportPath };
}
const report = parseJsonObject(result.stdout);
return {
source: "report-file",
report,
result,
degradedReason: report === null ? "web-probe-report-parse-failed" : null,
path: reportPath,
};
}
function readNodeWebProbeScriptArtifacts(
options: NodeWebProbeScriptOptions,
spec: HwlabRuntimeLaneSpec,
runDir: string | null,
): { source: string; artifacts: Record<string, unknown> | null; screenshots: Record<string, unknown>[]; result: CommandResult | null; degradedReason: string | null } | null {
if (!runDir) return null;
if (!isSafeWebProbeScriptRunDir(runDir)) return { source: "unsafe-path", artifacts: null, screenshots: [], result: null, degradedReason: "web-probe-run-dir-invalid" };
const script = [
"set -eu",
`test -d ${shellQuote(runDir)}`,
`find ${shellQuote(runDir)} -maxdepth 1 -type f \\( -name '*.png' -o -name '*.json' \\) -printf '%p\\t%s\\n' | tail -n 30`,
].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, 55);
if (result.exitCode !== 0 || result.timedOut) {
return { source: "run-dir", artifacts: null, screenshots: [], result, degradedReason: result.timedOut ? "web-probe-command-timeout" : "web-probe-artifact-list-failed" };
}
const items = result.stdout.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [path, sizeText] = line.split("\t");
const byteCount = Number(sizeText);
return {
kind: path.endsWith(".png") ? "screenshot" : "json",
path,
byteCount: Number.isFinite(byteCount) ? byteCount : null,
};
})
.filter((item) => isSafeWebProbeScriptArtifactPath(String(item.path)));
const screenshots = items.filter((item) => item.kind === "screenshot");
return {
source: "run-dir",
artifacts: { runDir, count: items.length, items },
screenshots,
result,
degradedReason: null,
};
}
function isSafeWebProbeScriptRunDir(value: string | null): value is string {
return typeof value === "string"
&& value.length > 0
&& !value.includes("\0")
&& !value.includes("..")
&& /\.state\/web-probe-script\/run\.[A-Za-z0-9]+$/u.test(value);
}
function isSafeWebProbeScriptReportPath(value: string | null): value is string {
return typeof value === "string"
&& isSafeWebProbeScriptArtifactPath(value)
&& value.endsWith("/web-probe-script-report.json");
}
function isSafeWebProbeScriptArtifactPath(value: string): boolean {
return typeof value === "string"
&& value.length > 0
&& !value.includes("\0")
&& !value.includes("..")
&& /\.state\/web-probe-script\/run\.[A-Za-z0-9]+\/[^/]+[.](?:png|json)$/u.test(value);
}
function parseSecretOptions(args: string[]): NodeSecretOptions {