feat(web-probe): add semantic YAML origins

This commit is contained in:
Codex
2026-07-10 12:37:13 +02:00
parent 2d4c1a5ffa
commit 2c47fd6528
18 changed files with 545 additions and 48 deletions
+36 -8
View File
@@ -37,6 +37,7 @@ import { transPath } from "./runtime-common";
import { assertLane, assertNodeId, compactCommandResult, compactCommandResultRedacted, compactCommandResultWithStdoutTail, nullableRecord, optionValue, parseJsonObject, positiveIntegerOption, record, requiredOption, shellQuote } from "./utils";
import { runNodeWebProbeObserveCommand, runNodeWebProbeObserveGc, runNodeWebProbeObserveStart, runNodeWebProbeObserveStatus } from "./web-probe-observe-actions";
import { runNodeWebProbeObserveCollect } from "./web-probe-observe-collect";
import { parseWebProbeOriginName, resolveNodeWebProbeCliOrigin, resolveNodeWebProbeOrigin, webProbeOriginSummary } from "./web-probe-origin";
import { nodeWebObserveResolveStateDirShell, recoverWebObserveAnalyzeTurnDetails, renderWebObserveStartResult, renderWebProbeRunResult, upsertWebObserveIndexEntry, webObserveCommandLabel, webObserveIdFromOptions, webObserveIdFromStatus, webObserveIndexEntryFromOptions, webObserveNextCommands, withWebObserveAnalyzeRendered, withWebObserveShortcuts } from "./web-observe-render";
import { commandSummaryForOutput, isSafeWebObserveArchivePrefix, isSafeWebObserveCollectFile, isSafeWebObserveFindingId, isSafeWebObserveJobId, isSafeWebObserveStateDir, isSafeWebObserveTraceId, nodeWebObserveForceStopNodeScript, nodeWebObserveStatusNodeScript, nodeWebObserveWaitCommandShell, runNodeWebProbeScript, safeWebObserveSegment, safeWebObserveTargetSegment } from "./web-observe-scripts";
import { displayRepoPath, readBootstrapAdminPasswordMaterial, sleepSync } from "./web-probe";
@@ -387,6 +388,7 @@ export function parseNodeWebProbeObserveOptions(
assertKnownOptions(args.slice(1), new Set([
"--node",
"--lane",
"--origin",
"--url",
"--target-path",
"--viewport",
@@ -491,6 +493,25 @@ export function parseNodeWebProbeObserveOptions(
if (observeActionRaw !== "start" && observeActionRaw !== "gc" && stateDir === null && jobId === null) {
throw new Error("web-probe observe status|command|stop|collect|analyze requires --state-dir or --job-id");
}
const requestedOrigin = optionValue(args, "--origin");
const customUrl = optionValue(args, "--url");
const browserProxyModeRaw = optionValue(args, "--browser-proxy-mode");
if (observeActionRaw !== "start" && (requestedOrigin !== undefined || customUrl !== undefined || browserProxyModeRaw !== undefined)) {
throw new Error("web-probe observe --origin/--url/--browser-proxy-mode is only valid for start; later actions inherit the observer origin");
}
const requestedBrowserProxyMode = browserProxyModeRaw === undefined ? undefined : parseWebProbeBrowserProxyMode(browserProxyModeRaw);
const selectedOrigin = observeActionRaw === "start"
? resolveNodeWebProbeCliOrigin(spec, requestedOrigin, customUrl, requestedBrowserProxyMode)
: {
originName: indexed?.originName ?? "custom" as const,
originMode: indexed?.originMode ?? "custom-url" as const,
originConfigPath: indexed?.originConfigPath ?? null,
url: indexed?.url || spec.publicWebUrl,
browserProxyMode: requestedBrowserProxyMode ?? indexed?.browserProxyMode ?? spec.webProbe?.browserProxyMode ?? "auto" as const,
browserProxyModeSource: requestedBrowserProxyMode === undefined
? indexed?.browserProxyModeSource ?? (spec.webProbe?.browserProxyMode === undefined ? "default" as const : "yaml-web-probe" as const)
: "cli" as const,
};
const confirm = args.includes("--confirm");
const dryRun = args.includes("--dry-run") || !confirm;
if (confirm && args.includes("--dry-run")) throw new Error("web-probe observe gc accepts only one of --confirm or --dry-run");
@@ -585,10 +606,14 @@ export function parseNodeWebProbeObserveOptions(
id: observeId ?? jobId,
node,
lane,
url: optionValue(args, "--url") ?? (observeActionRaw === "start" ? nodeWebProbeDefaultUrl(spec) : indexed?.url ?? spec.publicWebUrl),
url: selectedOrigin.url,
originName: selectedOrigin.originName,
originMode: selectedOrigin.originMode,
originConfigPath: selectedOrigin.originConfigPath,
targetPath: optionValue(args, "--target-path") ?? "/workbench",
viewport: optionValue(args, "--viewport") ?? "1440x900",
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode") ?? spec.webProbe?.browserProxyMode),
browserProxyMode: selectedOrigin.browserProxyMode,
browserProxyModeSource: selectedOrigin.browserProxyModeSource,
sampleIntervalMs: positiveIntegerOption(args, "--sample-interval-ms", 5000, 600000),
screenshotIntervalMs: positiveIntegerOption(args, "--screenshot-interval-ms", 300000, 86_400_000),
observerRefreshIntervalMs: positiveIntegerOption(args, "--observer-refresh-interval-ms", 180000, 86_400_000),
@@ -715,10 +740,8 @@ export function parseWebProbeBrowserProxyMode(value: string | undefined): WebPro
}
export function nodeWebProbeDefaultUrl(spec: HwlabRuntimeLaneSpec): string {
const origin = spec.webProbe?.defaultOrigin;
if (origin === undefined || origin.mode === "public") return origin?.baseUrl ?? spec.publicWebUrl;
const clusterIp = resolveKubernetesServiceClusterIp(spec, origin.namespace, origin.serviceName, new Map());
return `${origin.scheme}://${clusterIp}:${origin.port}`;
const origin = parseWebProbeOriginName(spec.webProbe?.defaultOrigin ?? "public");
return resolveNodeWebProbeOrigin(spec, origin).url;
}
export function nodeWebProbeAutoCommandTimeoutSeconds(input: {
@@ -790,6 +813,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
lane: options.lane,
workspace: spec.workspace,
url: options.url,
origin: webProbeOriginSummary(options),
degradedReason: "web_login_secret_missing",
credential,
next: { secretStatus: `bun scripts/cli.ts hwlab nodes secret status --node ${options.node} --lane ${options.lane} --name ${secretSpec.bootstrapAdminSecret}` },
@@ -799,7 +823,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
if (options.action === "script") return runNodeWebProbeScript(options, spec, secretSpec, material, credential);
if (options.commandTimeoutSeconds > 55) return runNodeWebProbeAsync(options, spec, secretSpec, material, credential);
const probeArgs = nodeWebProbeRunArgs(options, "run");
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
const script = [
"set -eu",
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password)}`].join(" ")} ${probeArgs.map(shellQuote).join(" ")}`,
@@ -821,6 +845,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
lane: options.lane,
workspace: spec.workspace,
url: options.url,
origin: webProbeOriginSummary(options),
network: webProbeProxy.summary,
credential,
commandTimeout: {
@@ -884,6 +909,7 @@ export function runNodeWebProbeScreenshot(options: NodeWebProbeScreenshotOptions
workspace: spec.workspace,
route,
url: options.url,
origin: webProbeOriginSummary(options),
viewport: options.viewport,
localDir: options.localDir,
screenshot: compactScreenshot,
@@ -1139,7 +1165,7 @@ export function runNodeWebProbeAsync(
credential: Record<string, unknown>,
): Record<string, unknown> {
const startArgs = nodeWebProbeRunArgs(options, "start");
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
const startScript = [
"set -eu",
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password ?? "")}`].join(" ")} ${startArgs.map(shellQuote).join(" ")}`,
@@ -1156,6 +1182,7 @@ export function runNodeWebProbeAsync(
lane: options.lane,
workspace: spec.workspace,
url: options.url,
origin: webProbeOriginSummary(options),
network: webProbeProxy.summary,
credential,
mode: "async-start",
@@ -1194,6 +1221,7 @@ export function runNodeWebProbeAsync(
lane: options.lane,
workspace: spec.workspace,
url: options.url,
origin: webProbeOriginSummary(options),
network: webProbeProxy.summary,
credential,
mode: "async",