feat: add web-probe observe control views (#848)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-25 03:38:05 +08:00
committed by GitHub
parent 922f8f3f63
commit baf0c2bb05
8 changed files with 5756 additions and 5419 deletions
+50 -415
View File
@@ -10,7 +10,10 @@ import { classifySshTcpPoolFailure } from "./ssh";
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "./hwlab-node-control-plane";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec } from "./hwlab-node-lanes";
import { nodeWebProbeScriptRunnerSource } from "./hwlab-node-web-probe-runner-source";
import { nodeWebObserveAnalyzerSource, nodeWebObserveRunnerSource } from "./hwlab-node-web-observe-runner-source";
import { nodeWebObserveAnalyzerSource } from "./hwlab-node-web-observe-analyzer-source";
import { nodeWebObserveRunnerSource } from "./hwlab-node-web-observe-runner-source";
import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "./hwlab-node-web-observe-collect";
import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "./hwlab-node-web-observe-render";
import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "./hwlab-node-help";
import { compactWebProbeResult, compactWebProbeScriptResult } from "./hwlab-node-web-probe-summary";
import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "./hwlab-node-observability-promql";
@@ -61,7 +64,7 @@ interface NodeWebProbeScriptOptions {
}
type NodeWebProbeObserveAction = "start" | "status" | "command" | "stop" | "collect" | "analyze";
type NodeWebProbeObserveCommandType = "login" | "preflight" | "goto" | "newSession" | "sendPrompt" | "selectProvider" | "clickSession" | "screenshot" | "mark" | "stop";
type NodeWebProbeObserveCommandType = "login" | "preflight" | "goto" | "newSession" | "sendPrompt" | "steer" | "cancel" | "selectProvider" | "clickSession" | "screenshot" | "mark" | "stop";
interface NodeWebProbeObserveOptions {
action: "observe";
@@ -81,9 +84,14 @@ interface NodeWebProbeObserveOptions {
waitMs: number;
tailLines: number;
maxFiles: number;
collectView: NodeWebProbeObserveCollectView;
collectFile: string | null;
collectFinding: string | null;
collectGrep: string | null;
collectTraceId: string | null;
collectSampleSeq: number | null;
collectTimestamp: string | null;
collectTurn: number | null;
analyzeArchivePrefix: string | null;
analyzeTailSamples: number | null;
full: boolean;
@@ -7359,9 +7367,14 @@ function parseNodeWebProbeObserveOptions(
"--wait-ms",
"--tail-lines",
"--max-files",
"--view",
"--file",
"--finding",
"--grep",
"--trace-id",
"--sample-seq",
"--timestamp",
"--turn",
"--archive-prefix",
"--tail-samples",
"--state-dir",
@@ -7379,12 +7392,23 @@ function parseNodeWebProbeObserveOptions(
const jobId = optionValue(args, "--job-id") ?? observeId ?? indexed?.id ?? null;
if (stateDir !== null && !isSafeWebObserveStateDir(stateDir)) throw new Error(`unsafe web-probe observe --state-dir: ${stateDir}`);
if (jobId !== null && !isSafeWebObserveJobId(jobId)) throw new Error(`unsafe web-probe observe --job-id: ${jobId}`);
const collectView = parseNodeWebProbeObserveCollectView(optionValue(args, "--view") ?? "files");
const collectFile = optionValue(args, "--file") ?? null;
if (collectFile !== null && !isSafeWebObserveCollectFile(collectFile)) throw new Error(`unsafe web-probe observe --file: ${collectFile}`);
const collectFinding = optionValue(args, "--finding") ?? null;
if (collectFinding !== null && !isSafeWebObserveFindingId(collectFinding)) throw new Error(`unsafe web-probe observe --finding: ${collectFinding}`);
const collectGrep = optionValue(args, "--grep") ?? null;
if (collectGrep !== null && (collectGrep.includes("\0") || collectGrep.length > 200)) throw new Error("unsafe web-probe observe --grep: expected 1-200 non-NUL chars");
const collectTraceId = optionValue(args, "--trace-id") ?? null;
if (collectTraceId !== null && !isSafeWebObserveTraceId(collectTraceId)) throw new Error(`unsafe web-probe observe --trace-id: ${collectTraceId}`);
const collectSampleSeqRaw = optionValue(args, "--sample-seq") ?? null;
const collectSampleSeq = collectSampleSeqRaw === null ? null : Number(collectSampleSeqRaw);
if (collectSampleSeq !== null && (!Number.isInteger(collectSampleSeq) || collectSampleSeq < 1)) throw new Error("unsafe web-probe observe --sample-seq: expected positive integer");
const collectTimestamp = optionValue(args, "--timestamp") ?? null;
if (collectTimestamp !== null && (collectTimestamp.includes("\0") || collectTimestamp.length > 80 || !Number.isFinite(Date.parse(collectTimestamp)))) throw new Error("unsafe web-probe observe --timestamp: expected parseable timestamp");
const collectTurnRaw = optionValue(args, "--turn") ?? null;
const collectTurn = collectTurnRaw === null ? null : Number(collectTurnRaw);
if (collectTurn !== null && (!Number.isInteger(collectTurn) || collectTurn < 1)) throw new Error("unsafe web-probe observe --turn: expected positive integer");
const analyzeArchivePrefix = optionValue(args, "--archive-prefix") ?? null;
if (analyzeArchivePrefix !== null && !isSafeWebObserveArchivePrefix(analyzeArchivePrefix)) throw new Error(`unsafe web-probe observe --archive-prefix: ${analyzeArchivePrefix}`);
const analyzeTailSamplesRaw = optionValue(args, "--tail-samples") ?? null;
@@ -7422,9 +7446,14 @@ function parseNodeWebProbeObserveOptions(
waitMs: positiveIntegerOption(args, "--wait-ms", 0, 600000),
tailLines: positiveIntegerOption(args, "--tail-lines", 5, 200),
maxFiles: positiveIntegerOption(args, "--max-files", 80, 5000),
collectView,
collectFile,
collectFinding,
collectGrep,
collectTraceId,
collectSampleSeq,
collectTimestamp,
collectTurn,
analyzeArchivePrefix,
analyzeTailSamples,
full: args.includes("--full"),
@@ -7447,13 +7476,15 @@ function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbeObserve
|| value === "goto"
|| value === "newSession"
|| value === "sendPrompt"
|| value === "steer"
|| value === "cancel"
|| value === "selectProvider"
|| value === "clickSession"
|| value === "screenshot"
|| value === "mark"
|| value === "stop"
) return value;
throw new Error(`web-probe observe command --type must be login, preflight, goto, newSession, sendPrompt, selectProvider, clickSession, screenshot, mark, or stop; got ${value}`);
throw new Error(`web-probe observe command --type must be login, preflight, goto, newSession, sendPrompt, steer, cancel, selectProvider, clickSession, screenshot, mark, or stop; got ${value}`);
}
function parseWebProbeBrowserProxyMode(value: string | undefined): WebProbeBrowserProxyMode {
@@ -8082,153 +8113,6 @@ function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec:
});
}
function withWebObserveStatusRendered(value: Record<string, unknown>): RenderedCliResult {
return {
ok: value.ok !== false,
command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe status",
contentType: "text/plain",
renderedText: renderWebObserveStatusTable(value),
};
}
function renderWebObserveStatusTable(value: Record<string, unknown>): string {
const observer = record(value.observer);
const manifest = record(observer?.manifest);
const heartbeat = record(observer?.heartbeat);
const tails = record(observer?.tails);
const result = record(value.result);
const samples = Array.isArray(tails?.samples) ? tails.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-5) : [];
const controls = Array.isArray(tails?.control) ? tails.control.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-5) : [];
const completedControlIds = new Set(controls
.filter((item) => item.phase === "completed" || item.phase === "failed")
.map((item) => webObserveText(item.commandId))
.filter((item) => item !== "-"));
const activeControl = controls.slice().reverse().find((item) => item.phase === "started" && !completedControlIds.has(webObserveText(item.commandId))) ?? null;
const activeControlAgeSeconds = activeControl === null ? null : Math.max(0, Math.round((Date.now() - Date.parse(webObserveText(activeControl.ts))) / 1000));
const network = Array.isArray(tails?.network)
? tails.network.map(record).filter((item): item is Record<string, unknown> => item !== null).filter((item) => {
const status = typeof item.status === "number" ? item.status : null;
return status === null || status >= 400 || item.failure !== null && item.failure !== undefined;
}).slice(-5)
: [];
const next = record(value.next);
const heartbeatError = record(heartbeat?.error) ?? record(manifest?.error);
const heartbeatAuth = record(heartbeat?.auth) ?? record(heartbeatError?.auth);
const manifestNetwork = record(manifest?.network);
const manifestBrowser = record(manifestNetwork?.browser);
const manifestProxy = record(manifestNetwork?.proxy);
const lines = [
`hwlab nodes web-probe observe status (${webObserveText(value.status)})`,
"",
webObserveTable(["ID", "NODE", "LANE", "ALIVE", "PID", "SAMPLE", "UPDATED", "TARGET"], [[
webObserveText(value.id),
webObserveText(value.node),
webObserveText(value.lane),
webObserveText(observer?.processAlive),
webObserveText(observer?.pid),
webObserveText(heartbeat?.sampleSeq),
webObserveShort(webObserveText(heartbeat?.updatedAt ?? heartbeat?.lastSampleAt), 24),
webObserveShort(`${webObserveText(manifest?.baseUrl)}${webObserveText(manifest?.targetPath) === "-" ? "" : webObserveText(manifest?.targetPath)}`, 52),
]]),
"",
...(value.ok === false ? [
"Blocked detail:",
webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDOUT", "STDERR"], [[
webObserveShort(webObserveText(value.degradedReason), 48),
webObserveText(result.exitCode),
webObserveText(result.timedOut),
webObserveShort(webObserveText(result.stdoutTail), 120),
webObserveShort(webObserveText(result.stderr), 120),
]]),
"",
] : []),
...(heartbeatError !== null ? [
"Observer error:",
webObserveTable(["STATUS", "RETRY", "EXHAUSTED", "BROWSER_PROXY", "MESSAGE"], [[
webObserveText(heartbeat?.status ?? manifest?.status),
webObserveText(heartbeatAuth?.lastRetryLabel),
webObserveText(heartbeatAuth?.retryExhausted),
webObserveShort(webObserveText(manifestBrowser?.proxyMode ?? manifestProxy?.enabled), 32),
webObserveShort(webObserveText(heartbeatError.message ?? heartbeatAuth?.lastError), 120),
]]),
"",
] : []),
...(heartbeatAuth !== null ? [
"Auth progress:",
webObserveTable(["PHASE", "RETRY", "DELAY_MS", "STATUS", "RETRYABLE", "COOKIE", "EXHAUSTED", "LAST_ERROR"], [[
webObserveText(heartbeatAuth.phase),
webObserveText(heartbeatAuth.lastRetryLabel),
webObserveText(heartbeatAuth.retryDelayMs),
webObserveText(heartbeatAuth.lastStatusText === undefined ? heartbeatAuth.lastStatus : `${webObserveText(heartbeatAuth.lastStatus)} ${webObserveText(heartbeatAuth.lastStatusText)}`),
webObserveText(heartbeatAuth.retryable),
webObserveText(heartbeatAuth.cookiePresent),
webObserveText(heartbeatAuth.retryExhausted),
webObserveShort(webObserveText(heartbeatAuth.lastError), 80),
]]),
"",
] : []),
...(activeControl !== null ? [
"Active command:",
webObserveTable(["TYPE", "COMMAND", "AGE_S", "STARTED_AT", "DETAIL"], [[
webObserveText(activeControl.type),
webObserveShort(webObserveText(activeControl.commandId), 28),
webObserveText(activeControlAgeSeconds),
webObserveShort(webObserveText(activeControl.ts), 24),
webObserveShort(webObserveText(activeControl.detail), 80),
]]),
"",
] : []),
"Recent samples:",
webObserveTable(["SEQ", "TS", "PATH", "ROUTE_SESSION", "ACTIVE_SESSION", "MSG", "TRACE"], samples.length > 0 ? samples.map((sample) => [
webObserveText(sample.seq),
webObserveShort(webObserveText(sample.ts), 24),
webObserveShort(webObserveText(sample.path), 24),
webObserveShort(webObserveText(sample.routeSessionId), 20),
webObserveShort(webObserveText(sample.activeSessionId), 20),
webObserveText(sample.messageCount),
webObserveText(sample.traceRowCount),
]) : [["-", "-", "-", "-", "-", "-", "-"]]),
"",
"Recent control:",
webObserveTable(["SEQ", "TS", "PHASE", "TYPE", "COMMAND", "RETRY", "DETAIL"], controls.length > 0 ? controls.map((control) => [
webObserveText(control.seq),
webObserveShort(webObserveText(control.ts), 24),
webObserveText(control.phase),
webObserveText(control.type),
webObserveShort(webObserveText(control.commandId), 28),
webObserveText(control.retry),
webObserveShort(webObserveText(control.detail), 80),
]) : [["-", "-", "-", "-", "-", "-", "-"]]),
"",
"Network alerts:",
webObserveTable(["TS", "METHOD", "STATUS", "TYPE", "URL_OR_FAILURE"], network.length > 0 ? network.map((item) => [
webObserveShort(webObserveText(item.ts), 24),
webObserveText(item.method),
webObserveText(item.status),
webObserveText(item.type),
webObserveShort(webObserveText(item.failure ?? item.url), 80),
]) : [["-", "-", "-", "-", "-"]]),
"",
"Next:",
];
for (const key of ["status", "command", "stop", "analyze"] as const) {
const command = webObserveText(next?.[key]);
if (command !== "-") lines.push(` ${command}`);
}
lines.push("", "Disclosure:");
lines.push(" default view is a bounded table; use observe collect/analyze for artifact-level drill-down.");
return lines.join("\n");
}
function webObserveTable(headers: string[], rows: string[][]): string {
const normalizedRows = rows.map((row) => headers.map((_, index) => row[index] ?? ""));
const widths = headers.map((header, index) => Math.max(header.length, ...normalizedRows.map((row) => row[index].length)));
return [
headers.map((header, index) => header.padEnd(widths[index])).join(" ").trimEnd(),
...normalizedRows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd()),
].join("\n");
}
function webObserveText(value: unknown): string {
if (value === null || value === undefined || value === "") return "-";
if (typeof value === "string") return value;
@@ -8307,57 +8191,21 @@ function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOptions, spec
});
}
function withWebObserveCommandRendered(value: Record<string, unknown>): RenderedCliResult {
return {
ok: value.ok !== false,
command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe command",
contentType: "text/plain",
renderedText: renderWebObserveCommandTable(value),
};
}
function renderWebObserveCommandTable(value: Record<string, unknown>): string {
const observerCommand = record(value.observerCommand);
const observer = record(value.observer);
const result = record(observer?.result);
const id = webObserveText(value.id);
const full = value.full === true;
const lines = [
`hwlab nodes web-probe observe command (${webObserveText(value.status)})`,
"",
webObserveTable(["OBSERVER", "COMMAND", "TYPE", "STATUS", "TEXT_BYTES", "TEXT_HASH", "DETAIL"], [[
id,
webObserveText(value.commandId),
webObserveText(observerCommand?.type),
webObserveText(observer?.ok === false ? "failed" : observer?.completedAt !== undefined ? "completed" : value.status),
webObserveText(observerCommand?.textBytes),
webObserveShort(webObserveText(observerCommand?.textHash), 24),
webObserveShort(webObserveText(result?.mark ?? result?.currentUrl ?? observer?.error ?? observer?.queued), 80),
]]),
...(full ? [
"",
"Full command result:",
"```json",
JSON.stringify(observer ?? {}, null, 2),
"```",
] : []),
"",
"Next:",
` bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`,
` bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`,
"",
"Disclosure:",
" default view is a bounded command result; pass --full to expose the complete command result JSON.",
" use observe status/analyze for sampled state.",
];
return lines.join("\n");
}
function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
const collectScript = options.collectView === "files"
? nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding, options.collectGrep)
: nodeWebObserveCollectViewNodeScript({
maxFiles: options.maxFiles,
view: options.collectView,
traceId: options.collectTraceId,
sampleSeq: options.collectSampleSeq,
timestamp: options.collectTimestamp,
turn: options.collectTurn,
});
const script = [
"set -eu",
nodeWebObserveResolveStateDirShell(options),
nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding, options.collectGrep),
collectScript,
].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const collect = parseJsonObject(result.stdout);
@@ -8369,6 +8217,7 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec
node: options.node,
lane: options.lane,
workspace: spec.workspace,
view: options.collectView,
requestedFile: options.collectFile,
requestedGrep: options.collectGrep,
degradedReason: collect === null ? "collect-json-parse-failed" : null,
@@ -8378,224 +8227,6 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec
});
}
function withWebObserveCollectRendered(value: Record<string, unknown>): RenderedCliResult {
return {
ok: value.ok !== false,
command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe collect",
contentType: "text/plain",
renderedText: renderWebObserveCollectTable(value),
};
}
function renderWebObserveCollectTable(value: Record<string, unknown>): string {
const collect = nullableRecord(value.collect);
const result = record(value.result);
const file = record(collect?.file);
const files = Array.isArray(collect?.files) ? collect.files.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 20) : [];
const jsonlTail = Array.isArray(file.jsonlTail) ? file.jsonlTail.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-8) : [];
const jsonSummary = record(file.jsonSummary);
const grepSummary = record(file.grep);
const grepLines = Array.isArray(grepSummary?.lines) ? grepSummary.lines.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 40) : [];
const jsonContentPreview = file.jsonContent === undefined || file.jsonContent === null ? "" : JSON.stringify(file.jsonContent, null, 2);
const jsonRunnerErrors = Array.isArray(jsonSummary.runnerErrors) ? jsonSummary.runnerErrors.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-8) : [];
const jsonFindings = Array.isArray(jsonSummary.findings) ? jsonSummary.findings.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8) : [];
const jsonFindingDetail = record(jsonSummary.findingDetail);
const jsonFindingSamples = Array.isArray(jsonFindingDetail?.samples) ? jsonFindingDetail.samples.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 12) : [];
const lines = [
`hwlab nodes web-probe observe collect (${webObserveText(value.status)})`,
"",
webObserveTable(["ID", "NODE", "LANE", "MODE", "STATE_DIR"], [[
webObserveText(value.id),
webObserveText(value.node),
webObserveText(value.lane),
webObserveText(collect?.mode ?? "files"),
webObserveShort(webObserveText(collect?.stateDir), 88),
]]),
"",
];
if (collect === null) {
lines.push(
"Collect command:",
webObserveTable(["REQUESTED", "REASON", "EXIT", "TIMED_OUT", "STDOUT_BYTES", "STDOUT_TAIL", "STDERR"], [[
webObserveShort(webObserveText(value.requestedFile), 64),
webObserveShort(webObserveText(value.degradedReason), 40),
webObserveText(result.exitCode),
webObserveText(result.timedOut),
webObserveText(result.stdoutBytes),
webObserveShort(webObserveText(result.stdoutTail), 160),
webObserveShort(webObserveText(result.stderr), 160),
]]),
"",
"Disclosure:",
" collect stdout was not valid JSON; fix the collect command or rerun with a narrower --file after the root cause is visible.",
);
return lines.join("\n");
}
if (collect.mode === "file") {
lines.push(
"File:",
webObserveTable(["RELATIVE", "BYTES", "TRUNC", "SHA256"], [[
webObserveShort(webObserveText(file.relative), 64),
webObserveText(file.byteCount),
webObserveText(file.truncated),
webObserveShort(webObserveText(file.sha256), 28),
]]),
"",
);
if (Object.keys(jsonSummary).length > 0) {
lines.push(
"JSON summary:",
webObserveTable(["KEYS", "COUNTS", "PARSE_ERROR"], [[
webObserveShort(webObserveText(jsonSummary.topLevelKeys), 80),
webObserveShort(webObserveText(jsonSummary.counts), 80),
webObserveShort(webObserveText(jsonSummary.parseError), 80),
]]),
"",
);
}
if (grepSummary !== null) {
lines.push(
"Grep matches:",
webObserveTable(["PATTERN_SHA", "MATCHES", "RETURNED", "TRUNC"], [[
webObserveShort(webObserveText(grepSummary.patternSha256), 24),
webObserveText(grepSummary.matchCount),
webObserveText(grepSummary.returnedLineCount),
webObserveText(grepSummary.truncated),
]]),
webObserveTable(["LINE", "MATCH", "TEXT"], grepLines.length > 0 ? grepLines.map((item) => [
webObserveText(item.lineNo),
webObserveText(item.match === true),
webObserveShort(webObserveText(item.text), 360),
]) : [["-", "-", "-"]]),
"Grep context:",
grepLines.length > 0
? grepLines.map((item) => "- " + webObserveText(item.lineNo) + (item.match === true ? " * " : " ") + webObserveShort(webObserveText(item.text), 500)).join("\n")
: "-",
"",
);
}
if (jsonContentPreview.length > 0) {
lines.push("JSON content:", webObserveShort(jsonContentPreview, 2400), "");
}
if (jsonRunnerErrors.length > 0) {
lines.push(
"Runner errors:",
webObserveTable(["TS", "TYPE", "ATTEMPTS", "READY", "MESSAGE"], jsonRunnerErrors.map((item) => [
webObserveShort(webObserveText(item.ts), 24),
webObserveShort(webObserveText(item.type), 20),
webObserveText(item.attemptCount),
webObserveShort(webObserveText(item.lastReadinessReason), 24),
webObserveShort(webObserveText(item.message ?? item.lastError), 96),
])),
"",
);
}
if (jsonFindings.length > 0) {
lines.push(
"Findings:",
webObserveTable(["KIND", "SEVERITY", "COUNT", "SUMMARY"], jsonFindings.map((item) => [
webObserveShort(webObserveText(item.kind ?? item.id ?? item.code), 48),
webObserveText(item.severity ?? item.level),
webObserveText(item.count ?? item.sampleCount),
webObserveShort(webObserveText(item.summary ?? item.message), 96),
])),
"",
);
}
if (jsonFindingDetail !== null) {
lines.push(
"Finding detail:",
webObserveTable(["ID", "SEVERITY", "COUNT", "SAMPLE_SOURCE", "SUMMARY"], [[
webObserveShort(webObserveText(jsonFindingDetail.id), 48),
webObserveText(jsonFindingDetail.severity),
webObserveText(jsonFindingDetail.count),
webObserveText(jsonFindingDetail.sampleSource),
webObserveShort(webObserveText(jsonFindingDetail.summary), 96),
]]),
"",
"Finding samples:",
webObserveTable(["SEQ", "TS", "KIND", "FROM", "TO", "DELTA", "SAMPLE_S", "ALLOWED", "EXCESS", "SESSION/PATH", "TRACE", "DETAIL"], jsonFindingSamples.length > 0 ? jsonFindingSamples.map((item) => [
webObserveText(item.seq ?? item.sampleSeq ?? item.fromSeq),
webObserveShort(webObserveText(item.ts ?? item.sampleTs ?? item.fromTs), 24),
webObserveShort(webObserveText(item.diffKind ?? item.kind ?? item.type ?? item.metric ?? item.anomaly), 28),
webObserveShort(webObserveText(item.fromValue ?? item.from ?? item.control ?? item.controlValue ?? item.beforeValue), 28),
webObserveShort(webObserveText(item.toValue ?? item.to ?? item.observer ?? item.observerValue ?? item.afterValue), 28),
webObserveText(item.delta),
webObserveText(item.sampleDeltaSeconds),
webObserveText(item.allowedIncreaseSeconds),
webObserveText(item.excessiveIncreaseSeconds),
webObserveShort(webObserveText(item.sessionId ?? item.routeSessionId ?? item.activeSessionId ?? item.path ?? item.urlPath ?? item.url), 48),
webObserveShort(webObserveText(item.trace ?? item.traceId ?? item.controlTraceId ?? item.observerTraceId), 48),
webObserveShort(webObserveText(item.detail ?? item.summary ?? item.message ?? item.preview), 96),
]) : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]),
"",
);
}
if (jsonlTail.length > 0) {
lines.push(
"JSONL tail:",
webObserveTable(["TS", "ROLE", "TYPE", "COMMAND", "MSG", "TRACE", "TRACE_IDS", "MSG_STATUS", "LOAD", "ATTEMPTS", "READY", "DOM", "PATH", "MESSAGE"], jsonlTail.map((item) => {
const error = record(item.error);
const attempts = Array.isArray(error.attempts) ? error.attempts : [];
const lastAttempt = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {};
const rawReadiness = record(lastAttempt.readiness ?? error.navigationReadiness);
const readiness = record(rawReadiness.snapshot ?? rawReadiness);
const domBits = [
`shell=${readiness.workbenchShellVisible === true ? "Y" : "n"}`,
`create=${readiness.sessionCreateVisible === true ? "Y" : "n"}`,
`input=${readiness.commandInputPresent === true ? "Y" : "n"}`,
`tab=${readiness.activeTabPresent === true ? "Y" : "n"}`,
`login=${readiness.loginVisible === true ? "Y" : "n"}`,
].join(" ");
return [
webObserveShort(webObserveText(item.ts), 24),
webObserveShort(webObserveText(item.pageRole ?? item.role), 12),
webObserveShort(webObserveText(item.type), 20),
webObserveShort(webObserveText(item.commandId), 28),
webObserveText(item.messageCount),
webObserveText(item.traceRowCount ?? item.traceEventCount),
webObserveShort(webObserveText(item.traceIds), 48),
webObserveShort(webObserveText(item.messageStatuses), 48),
webObserveText(item.loadingCount),
webObserveText(attempts.length),
webObserveShort(webObserveText(readiness.reason), 24),
webObserveShort(domBits, 48),
webObserveShort(webObserveText(item.path ?? item.urlPath ?? item.url ?? item.currentUrl ?? item.status), 60),
webObserveShort(webObserveText(error.message ?? item.message ?? item.text ?? item.preview), 96),
];
})),
"",
);
} else if (typeof file.content === "string" && file.content.length > 0) {
lines.push("Content preview:", webObserveShort(file.content, 4000), "");
}
} else {
lines.push(
"Files:",
webObserveTable(["RELATIVE", "BYTES", "SHA256"], files.length > 0 ? files.map((item) => [
webObserveShort(webObserveText(item.relative), 72),
webObserveText(item.byteCount),
webObserveShort(webObserveText(item.sha256), 28),
]) : [["-", "-", "-"]]),
"",
);
}
if (value.ok === false) {
lines.push(
"Blocked detail:",
webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDERR"], [[
webObserveShort(webObserveText(collect.reason ?? value.degradedReason), 48),
webObserveText(result.exitCode),
webObserveText(result.timedOut),
webObserveShort(webObserveText(result.stderr), 120),
]]),
"",
);
}
lines.push("Disclosure:", " collect is bounded; use --file <relative> plus --finding <id> for id-specific drill-down; exact --grep <finding-id> also expands finding samples.");
return lines.join("\n");
}
function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
const analyzerB64 = Buffer.from(nodeWebObserveAnalyzerSource(), "utf8").toString("base64");
const alertThresholds = nodeWebProbeAlertThresholds(spec);
@@ -10703,6 +10334,10 @@ function isSafeWebObserveCollectFile(value: string): boolean {
&& /^[A-Za-z0-9_.\/-]+$/u.test(value);
}
function isSafeWebObserveTraceId(value: string): boolean {
return /^trc_[A-Za-z0-9_-]{6,120}$/u.test(value);
}
function isSafeWebObserveFindingId(value: string): boolean {
return value.length > 0
&& value.length <= 120