feat(web-probe): add multi-sentinel registry
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user