feat: add monitor evidence inspect cli

This commit is contained in:
Codex
2026-07-07 00:38:50 +00:00
parent acc21b191b
commit e9c91c78e6
8 changed files with 356 additions and 10 deletions
+129 -8
View File
@@ -56,8 +56,10 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
&& sentinelActionRaw !== "dashboard"
&& sentinelActionRaw !== "error"
&& sentinelActionRaw !== "report"
&& sentinelActionRaw !== "inspect-url"
&& sentinelActionRaw !== "inspect-id"
) {
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane|publish-current|validate|maintenance|dashboard|error|report --node NODE --lane vNN [--dry-run|--confirm]");
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane|publish-current|validate|maintenance|dashboard|error|report|inspect-url|inspect-id --node NODE --lane vNN [--dry-run|--confirm]");
}
assertKnownOptions(args, new Set([
"--node",
@@ -83,20 +85,47 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
"--source-stage-ref",
"--source-mirror-commit",
"--source-authority",
]), new Set(["--dry-run", "--confirm", "--wait", "--rerun", "--manual-recovery", "--recovery", "--quick-verify", "--raw", "--full", "--latest", "--full-page", "--no-full-page"]));
const node = requiredOption(args, "--node");
assertNodeId(node);
const lane = requiredOption(args, "--lane");
assertLane(lane);
]), new Set(["--dry-run", "--confirm", "--wait", "--rerun", "--manual-recovery", "--recovery", "--quick-verify", "--raw", "--full", "--latest", "--full-page", "--no-full-page", "--diagnose-monitor-web"]));
const nodeOption = optionValue(args, "--node") ?? null;
const laneOption = optionValue(args, "--lane") ?? null;
const inspectMode = sentinelActionRaw === "inspect-url" || sentinelActionRaw === "inspect-id";
let node = nodeOption ?? "";
let lane = laneOption ?? "";
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");
if (confirm && dryRun) throw new Error("web-probe sentinel accepts only one of --confirm or --dry-run");
const timeoutSeconds = positiveIntegerOption(args, "--timeout-seconds", 900, 3600);
const sourceOverride = parseWebProbeSentinelSourceOverride(args);
let sentinel: WebProbeSentinelOptions;
if (inspectMode) {
const input = args[1];
if (input === undefined || input.startsWith("--")) throw new Error(`web-probe sentinel ${sentinelActionRaw} requires a URL or id argument`);
const resolved = resolveWebProbeSentinelInspectTarget(input, sentinelActionRaw, nodeOption, laneOption, sentinelId);
node = resolved.node;
lane = resolved.lane;
sentinel = {
kind: "inspect",
action: sentinelActionRaw,
node,
lane,
sentinelId: resolved.sentinelId,
input,
view: parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary"),
raw: args.includes("--raw"),
full: args.includes("--full"),
diagnoseMonitorWeb: args.includes("--diagnose-monitor-web"),
resolutionWarnings: resolved.warnings,
timeoutSeconds,
};
} else {
node = requiredOption(args, "--node");
lane = requiredOption(args, "--lane");
assertNodeId(node);
assertLane(lane);
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
}
if (sentinelActionRaw === "plan" || sentinelActionRaw === "status") {
sentinel = { kind: "config", action: sentinelActionRaw, node, lane, sentinelId, dryRun };
} else if (sentinelActionRaw === "image") {
@@ -164,7 +193,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
const errorId = requiredOption(args, "--error-id");
if (!/^wbe_[a-f0-9]{20}$/u.test(errorId)) throw new Error(`web-probe sentinel error get --error-id must look like wbe_<20 hex>, got ${errorId}`);
sentinel = { kind: "error", action: "get", node, lane, sentinelId, errorId, raw: args.includes("--raw"), full: args.includes("--full"), timeoutSeconds };
} else {
} else if (sentinelActionRaw === "report") {
const view = parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary");
const latest = args.includes("--latest");
const runId = optionValue(args, "--run") ?? optionValue(args, "--run-id") ?? null;
@@ -196,6 +225,98 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
};
}
function resolveWebProbeSentinelInspectTarget(
input: string,
action: "inspect-url" | "inspect-id",
nodeOption: string | null,
laneOption: string | null,
sentinelOption: string | null,
): { readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly warnings: readonly string[] } {
if ((nodeOption === null) !== (laneOption === null)) throw new Error(`web-probe sentinel ${action} requires both --node and --lane when overriding target`);
if (nodeOption !== null && laneOption !== null) {
assertNodeId(nodeOption);
assertLane(laneOption);
if (!isHwlabRuntimeLane(laneOption)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${laneOption}`);
const spec = hwlabRuntimeLaneSpecForNode(laneOption, nodeOption);
const fallbackSentinel = spec.observability.webProbe?.monitorRoot?.sentinelId ?? null;
return { node: nodeOption, lane: laneOption, sentinelId: sentinelOption ?? fallbackSentinel, warnings: [] };
}
const candidates = webProbeMonitorRootCandidates();
const url = parseMonitorInspectUrl(input);
const urlSentinel = url === null ? null : sentinelIdFromMonitorUrl(url);
const desiredSentinel = sentinelOption ?? urlSentinel;
const matched = candidates.filter((candidate) => {
if (desiredSentinel !== null && candidate.sentinelId !== desiredSentinel) return false;
if (url === null) return true;
return monitorUrlMatchesRoot(url, candidate.publicBaseUrl);
});
if (matched.length === 0) {
const hint = candidates.slice(0, 8).map((candidate) => `${candidate.node}/${candidate.lane}/${candidate.sentinelId}`).join(", ");
throw new Error(`web-probe sentinel ${action} could not map input to a YAML monitorRoot; pass --node and --lane. candidates: ${hint}`);
}
const selected = matched.length === 1 ? matched[0] : preferredMonitorRootCandidate(matched);
if (selected === null) {
const hint = matched.slice(0, 8).map((candidate) => `--node ${candidate.node} --lane ${candidate.lane} --sentinel ${candidate.sentinelId}`).join(" | ");
throw new Error(`web-probe sentinel ${action} target is ambiguous; pass one target explicitly. candidates: ${hint}`);
}
const warnings = matched.length > 1
? [`multiple YAML monitorRoot entries matched; selected ${selected.node}/${selected.lane}/${selected.sentinelId} by source-of-truth preference`]
: [];
return { node: selected.node, lane: selected.lane, sentinelId: selected.sentinelId, warnings };
}
function webProbeMonitorRootCandidates(): readonly { readonly node: string; readonly lane: string; readonly sentinelId: string; readonly publicBaseUrl: string }[] {
const candidates: { node: string; lane: string; sentinelId: string; publicBaseUrl: string }[] = [];
for (const lane of hwlabRuntimeLaneIds()) {
for (const node of hwlabRuntimeNodeIds()) {
let spec: HwlabRuntimeLaneSpec;
try {
spec = hwlabRuntimeLaneSpecForNode(lane, node);
} catch {
continue;
}
const root = spec.observability.webProbe?.monitorRoot;
if (root?.enabled !== true) continue;
candidates.push({ node: spec.nodeId, lane: spec.lane, sentinelId: root.sentinelId, publicBaseUrl: root.publicBaseUrl });
}
}
return candidates;
}
function parseMonitorInspectUrl(input: string): URL | null {
try {
return new URL(input);
} catch {
return null;
}
}
function sentinelIdFromMonitorUrl(url: URL): string | null {
const query = url.searchParams.get("sentinel") ?? url.searchParams.get("sentinelId");
if (query !== null && /^[a-z0-9][a-z0-9-]{1,80}$/u.test(query)) return query;
const match = /\/sentinels\/([a-z0-9][a-z0-9-]{1,80})(?:\/|$)/u.exec(url.pathname);
return match?.[1] ?? null;
}
function monitorUrlMatchesRoot(url: URL, publicBaseUrl: string): boolean {
let base: URL;
try {
base = new URL(publicBaseUrl);
} catch {
return false;
}
if (url.origin !== base.origin) return false;
const basePath = base.pathname.replace(/\/+$/u, "");
if (basePath.length === 0) return true;
return url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
}
function preferredMonitorRootCandidate(candidates: readonly { readonly node: string; readonly lane: string; readonly sentinelId: string }[]): { readonly node: string; readonly lane: string; readonly sentinelId: string } | null {
return candidates.find((candidate) => candidate.node === "JD01" && candidate.lane === "v03" && candidate.sentinelId === "jd01-web-probe-sentinel")
?? candidates.find((candidate) => candidate.node === "JD01" && candidate.lane === "v03")
?? null;
}
function parseWebProbeSentinelSourceOverride(args: string[]): WebProbeSentinelSourceOverrideOptions {
const sourceCommit = optionValue(args, "--source-commit") ?? null;
const sourceStageRef = optionValue(args, "--source-stage-ref") ?? null;