feat(web-probe): add multi-sentinel registry

This commit is contained in:
Codex
2026-06-26 12:42:04 +00:00
parent 7b3df965cc
commit 4e0f1cba21
25 changed files with 1038 additions and 140 deletions
+8
View File
@@ -4,6 +4,7 @@
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
// Responsibility: YAML-first node/lane operations, including Workbench observability control commands.
import { createHash, randomBytes } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -113,6 +114,10 @@ export type NodeWebProbeObserveAction = "start" | "status" | "command" | "stop"
export type NodeWebProbeObserveCommandType =
| "login"
| "loginAccount"
| "logout"
| "listSessions"
| "switchSessions"
| "preflight"
| "goto"
| "gotoProjectMdtodo"
@@ -199,6 +204,9 @@ export interface NodeWebProbeObserveOptions {
commandRequireComposerReady: boolean;
commandFindingId: string | null;
commandBlocking: boolean | null;
commandAccountId: string | null;
commandFromAccountId: string | null;
commandToAccountId: string | null;
commandSourceId: string | null;
commandFileRef: string | null;
commandFilename: string | null;
+41 -7
View File
@@ -4,6 +4,7 @@
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
// Responsibility: YAML-first node/lane operations, including Workbench observability control commands.
import { createHash, randomBytes } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -60,11 +61,15 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
"--run-id",
"--trace-id",
"--sample-seq",
"--sentinel",
"--sentinel-id",
]), new Set(["--dry-run", "--confirm", "--wait", "--quick-verify", "--raw", "--latest"]));
const node = requiredOption(args, "--node");
assertNodeId(node);
const lane = requiredOption(args, "--lane");
assertLane(lane);
const sentinelId = optionValue(args, "--sentinel") ?? optionValue(args, "--sentinel-id") ?? null;
if (sentinelId !== null && !/^[a-z0-9][a-z0-9-]{1,80}$/u.test(sentinelId)) throw new Error(`--sentinel must be a stable lowercase sentinel id, got ${sentinelId}`);
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
const confirm = args.includes("--confirm");
const dryRun = args.includes("--dry-run");
@@ -72,17 +77,17 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
const timeoutSeconds = positiveIntegerOption(args, "--timeout-seconds", 900, 3600);
let sentinel: WebProbeSentinelOptions;
if (sentinelActionRaw === "plan" || sentinelActionRaw === "status") {
sentinel = { kind: "config", action: sentinelActionRaw, node, lane, dryRun };
sentinel = { kind: "config", action: sentinelActionRaw, node, lane, sentinelId, dryRun };
} else if (sentinelActionRaw === "image") {
const imageAction = args[1];
if (imageAction !== "status" && imageAction !== "build") throw new Error("web-probe sentinel image usage: image status|build --node NODE --lane vNN [--dry-run|--confirm]");
sentinel = { kind: "image", action: imageAction, node, lane, dryRun: imageAction === "build" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
sentinel = { kind: "image", action: imageAction, node, lane, sentinelId, dryRun: imageAction === "build" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
} else if (sentinelActionRaw === "control-plane") {
const controlPlaneAction = args[1];
if (controlPlaneAction !== "plan" && controlPlaneAction !== "apply" && controlPlaneAction !== "status" && controlPlaneAction !== "trigger-current") {
throw new Error("web-probe sentinel control-plane usage: control-plane plan|apply|status|trigger-current --node NODE --lane vNN [--dry-run|--confirm]");
}
sentinel = { kind: "control-plane", action: controlPlaneAction, node, lane, dryRun: controlPlaneAction === "apply" || controlPlaneAction === "trigger-current" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
sentinel = { kind: "control-plane", action: controlPlaneAction, node, lane, sentinelId, dryRun: controlPlaneAction === "apply" || controlPlaneAction === "trigger-current" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
} else if (sentinelActionRaw === "maintenance") {
const maintenanceAction = args[1];
if (maintenanceAction !== "status" && maintenanceAction !== "start" && maintenanceAction !== "stop") {
@@ -93,6 +98,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
action: maintenanceAction,
node,
lane,
sentinelId,
dryRun: maintenanceAction === "status" ? dryRun : dryRun || !confirm,
confirm,
wait: args.includes("--wait"),
@@ -102,7 +108,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
quickVerify: maintenanceAction === "stop" || args.includes("--quick-verify"),
};
} else if (sentinelActionRaw === "validate") {
sentinel = { kind: "validate", action: "validate", node, lane, dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, quickVerify: args.includes("--quick-verify") };
sentinel = { kind: "validate", action: "validate", node, lane, sentinelId, dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, quickVerify: args.includes("--quick-verify") };
} else {
const view = parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary");
const latest = args.includes("--latest");
@@ -116,6 +122,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
action: "report",
node,
lane,
sentinelId,
view,
runId,
latest,
@@ -134,8 +141,8 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
}
function parseWebProbeSentinelReportView(value: string): WebProbeSentinelReportView {
if (value === "summary" || value === "turn-summary" || value === "findings" || value === "trace-frame") return value;
throw new Error(`web-probe sentinel report --view must be summary, turn-summary, findings, or trace-frame; got ${value}`);
if (value === "summary" || value === "turn-summary" || value === "findings" || value === "trace-frame" || value === "auth-session-switch-summary") return value;
throw new Error(`web-probe sentinel report --view must be summary, turn-summary, findings, trace-frame, or auth-session-switch-summary; got ${value}`);
}
export function normalizeNodeWebProbeObserveArgs(args: string[]): { args: string[]; id: string | null } {
@@ -202,6 +209,10 @@ export function parseNodeWebProbeObserveOptions(
"--label",
"--session-id",
"--provider",
"--account-id",
"--account",
"--from-account-id",
"--to-account-id",
"--after-round",
"--severity",
"--alternate-session-strategy",
@@ -272,6 +283,9 @@ export function parseNodeWebProbeObserveOptions(
}
const commandText = commandTextFromStdin ? readFileSync(0, "utf8") : commandTextOption;
const commandSourceId = optionValue(args, "--source-id") ?? null;
const commandAccountId = optionValue(args, "--account-id") ?? optionValue(args, "--account") ?? null;
const commandFromAccountId = optionValue(args, "--from-account-id") ?? null;
const commandToAccountId = optionValue(args, "--to-account-id") ?? null;
const commandFileRef = optionValue(args, "--file-ref") ?? null;
const commandFilename = optionValue(args, "--filename") ?? null;
const commandTaskRef = optionValue(args, "--task-ref") ?? null;
@@ -301,6 +315,9 @@ export function parseNodeWebProbeObserveOptions(
["--expected-sentinel-range", commandExpectedSentinelRange],
["--finding-id", commandFindingId],
["--source-id", commandSourceId],
["--account-id/--account", commandAccountId],
["--from-account-id", commandFromAccountId],
["--to-account-id", commandToAccountId],
["--file-ref", commandFileRef],
["--filename", commandFilename],
["--task-ref", commandTaskRef],
@@ -366,6 +383,9 @@ export function parseNodeWebProbeObserveOptions(
commandRequireComposerReady: args.includes("--require-composer-ready"),
commandFindingId,
commandBlocking,
commandAccountId,
commandFromAccountId,
commandToAccountId,
commandSourceId,
commandFileRef,
commandFilename,
@@ -386,6 +406,10 @@ export function parseNodeWebProbeObserveOptions(
export function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbeObserveCommandType {
if (
value === "login"
|| value === "loginAccount"
|| value === "logout"
|| value === "listSessions"
|| value === "switchSessions"
|| value === "preflight"
|| value === "goto"
|| value === "newSession"
@@ -423,7 +447,7 @@ export function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbe
|| value === "mark"
|| value === "stop"
) return value;
throw new Error(`web-probe observe command --type must be login, preflight, goto, gotoProjectMdtodo, newSession, sendPrompt, steer, cancel, selectProvider, clickSession, refreshCurrentSession, switchAwayAndBack, assertSessionInvariant, selectProjectSource, selectMdtodoSource, selectMdtodoFile, selectMdtodoTask, expandMdtodoTask, openMdtodoReportPreview, toggleMdtodoReportFullscreen, openMdtodoSourceConfig, configureMdtodoHwpodSource, probeMdtodoSource, reindexMdtodoSource, editMdtodoTaskInline, editMdtodoTaskTitle, editMdtodoTaskBody, toggleMdtodoTaskStatus, addMdtodoRootTask, addMdtodoSubTask, continueMdtodoTask, deleteMdtodoTask, launchWorkbenchFromTask, launchWorkbenchFromMdtodo, screenshot, mark, or stop; got ${value}`);
throw new Error(`web-probe observe command --type must be login, loginAccount, logout, listSessions, switchSessions, preflight, goto, gotoProjectMdtodo, newSession, sendPrompt, steer, cancel, selectProvider, clickSession, refreshCurrentSession, switchAwayAndBack, assertSessionInvariant, selectProjectSource, selectMdtodoSource, selectMdtodoFile, selectMdtodoTask, expandMdtodoTask, openMdtodoReportPreview, toggleMdtodoReportFullscreen, openMdtodoSourceConfig, configureMdtodoHwpodSource, probeMdtodoSource, reindexMdtodoSource, editMdtodoTaskInline, editMdtodoTaskTitle, editMdtodoTaskBody, toggleMdtodoTaskStatus, addMdtodoRootTask, addMdtodoSubTask, continueMdtodoTask, deleteMdtodoTask, launchWorkbenchFromTask, launchWorkbenchFromMdtodo, screenshot, mark, or stop; got ${value}`);
}
export function parseWebProbeBrowserProxyMode(value: string | undefined): WebProbeBrowserProxyMode {
@@ -1186,6 +1210,12 @@ export function normalizedProxyUrl(parsed: URL): string {
return value;
}
function webProbeAccountEnvAssignments(): string[] {
return Object.entries(process.env)
.filter(([key, value]) => value !== undefined && /^HWLAB_WEB_(?:ACCOUNT_)?[A-Z0-9_]+_(?:JSON|USER|PASS)$/u.test(key))
.map(([key, value]) => `${key}=${shellQuote(value ?? "")}`);
}
export function runNodeWebProbeObserve(
options: NodeWebProbeObserveOptions,
spec: HwlabRuntimeLaneSpec,
@@ -1223,6 +1253,7 @@ export function runNodeWebProbeObserveStart(
const projectManagement = nodeWebProbeProjectManagementConfig(spec);
const runnerEnvAssignments = [
...webProbeProxy.envAssignments,
...webProbeAccountEnvAssignments(),
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`,
`HWLAB_WEB_PASS=${shellQuote(material.password)}`,
@@ -1382,6 +1413,9 @@ export function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOption
requireComposerReady: options.commandRequireComposerReady,
findingId: options.commandFindingId,
blocking: options.commandBlocking,
accountId: options.commandAccountId,
fromAccountId: options.commandFromAccountId,
toAccountId: options.commandToAccountId,
sourceId: options.commandSourceId,
fileRef: options.commandFileRef,
filename: options.commandFilename,