feat: add monitor evidence inspect cli
This commit is contained in:
@@ -263,6 +263,205 @@ export function runSentinelErrorDetail(state: SentinelCicdState, options: Extrac
|
||||
return rendered(response.ok && body.ok !== false, command, renderSentinelErrorDetailResult({ command, node: state.spec.nodeId, lane: state.spec.lane, sentinelId: state.sentinelId, response, body, valuesRedacted: true }));
|
||||
}
|
||||
|
||||
export function runSentinelInspect(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "inspect" }>): RenderedCliResult {
|
||||
const command = `web-probe sentinel ${options.action}`;
|
||||
const evidence = parseMonitorEvidenceInput(options.input);
|
||||
const runId = firstEvidenceValue(evidence, "run");
|
||||
const traceId = firstEvidenceValue(evidence, "trace");
|
||||
const sampleSeq = firstEvidenceValue(evidence, "sample-seq");
|
||||
let reportPayload: Record<string, unknown> | null = null;
|
||||
if (runId !== null) {
|
||||
const query = new URLSearchParams({ view: options.view, run: runId });
|
||||
if (traceId !== null) query.set("traceId", traceId);
|
||||
if (sampleSeq !== null) query.set("sampleSeq", sampleSeq);
|
||||
const report = callSentinelService(state, "GET", `/api/report?${query.toString()}`, null, options.timeoutSeconds);
|
||||
const body = record(report.bodyJson);
|
||||
const artifactSummary = readSentinelReportArtifactSummary(state, body, Math.min(options.timeoutSeconds, SENTINEL_REPORT_ARTIFACT_READ_TIMEOUT_SECONDS));
|
||||
reportPayload = compactSentinelReportRawPayload(state, body, report, artifactSummary);
|
||||
}
|
||||
const payload = {
|
||||
ok: reportPayload === null ? true : reportPayload.ok !== false,
|
||||
command,
|
||||
mode: options.action,
|
||||
input: options.input,
|
||||
inputKind: evidence.inputKind,
|
||||
node: state.spec.nodeId,
|
||||
lane: state.spec.lane,
|
||||
sentinelId: state.sentinelId,
|
||||
monitorWebHttpDiagnostic: options.diagnoseMonitorWeb ? monitorWebHttpDiagnosticPlan(state, options.input) : null,
|
||||
warnings: options.resolutionWarnings,
|
||||
evidenceIds: evidence.ids,
|
||||
evidenceCommands: monitorEvidenceCommands(state, evidence),
|
||||
report: reportPayload,
|
||||
next: monitorInspectNext(state, evidence),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
if (options.full || options.raw) return rendered(payload.ok, command, JSON.stringify(payload, null, 2));
|
||||
return rendered(payload.ok, command, renderMonitorInspect(payload));
|
||||
}
|
||||
|
||||
type MonitorEvidenceKind = "run" | "observer" | "trace" | "sample-seq" | "turn" | "command" | "artifact" | "report-sha" | "finding" | "unknown";
|
||||
|
||||
function parseMonitorEvidenceInput(input: string): { readonly inputKind: "url" | "id"; readonly ids: readonly Record<string, unknown>[] } {
|
||||
const ids: Record<string, unknown>[] = [];
|
||||
const url = parseEvidenceUrl(input);
|
||||
if (url !== null) {
|
||||
const addParam = (name: string, kind: MonitorEvidenceKind, normalizedName = name) => {
|
||||
const value = url.searchParams.get(name);
|
||||
if (value !== null && value.trim().length > 0) ids.push({ kind, name: normalizedName, value: value.trim(), source: "url-query", valuesRedacted: true });
|
||||
};
|
||||
addParam("run", "run");
|
||||
addParam("runId", "run", "run");
|
||||
addParam("observer", "observer");
|
||||
addParam("observerId", "observer", "observer");
|
||||
addParam("traceId", "trace", "traceId");
|
||||
addParam("sampleSeq", "sample-seq", "sampleSeq");
|
||||
addParam("turn", "turn");
|
||||
addParam("commandId", "command", "commandId");
|
||||
addParam("artifact", "artifact");
|
||||
addParam("file", "artifact", "artifact");
|
||||
addParam("reportSha", "report-sha", "reportSha");
|
||||
addParam("finding", "finding");
|
||||
addParam("findingId", "finding", "finding");
|
||||
return { inputKind: "url", ids: ids.length > 0 ? ids : [{ kind: "unknown", value: input, source: "url", valuesRedacted: true }] };
|
||||
}
|
||||
return { inputKind: "id", ids: [classifyMonitorEvidenceId(input)] };
|
||||
}
|
||||
|
||||
function parseEvidenceUrl(input: string): URL | null {
|
||||
try {
|
||||
return new URL(input);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function classifyMonitorEvidenceId(input: string): Record<string, unknown> {
|
||||
const value = input.trim();
|
||||
const kind: MonitorEvidenceKind = /^sentinel-run-[A-Za-z0-9_-]+$/u.test(value) ? "run"
|
||||
: /^webobs-[A-Za-z0-9_-]+$/u.test(value) ? "observer"
|
||||
: /^trc[_-][A-Za-z0-9_-]+$/u.test(value) ? "trace"
|
||||
: /^cmd[_-][A-Za-z0-9_-]+$/u.test(value) || /^command[_-][A-Za-z0-9_-]+$/u.test(value) ? "command"
|
||||
: /^sha256[:_-]?[0-9a-f]{64}$/iu.test(value) ? "report-sha"
|
||||
: /^[0-9]+$/u.test(value) ? "sample-seq"
|
||||
: "unknown";
|
||||
return { kind, value, source: "bare-id", valuesRedacted: true };
|
||||
}
|
||||
|
||||
function firstEvidenceValue(evidence: { readonly ids: readonly Record<string, unknown>[] }, kind: MonitorEvidenceKind): string | null {
|
||||
for (const item of evidence.ids) {
|
||||
if (item.kind === kind) {
|
||||
const value = stringAtNullable(item, "value");
|
||||
if (value !== null) return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function monitorEvidenceCommands(state: SentinelCicdState, evidence: { readonly ids: readonly Record<string, unknown>[] }): readonly Record<string, unknown>[] {
|
||||
return evidence.ids.flatMap((item) => commandsForEvidenceId(state, item));
|
||||
}
|
||||
|
||||
function commandsForEvidenceId(state: SentinelCicdState, item: Record<string, unknown>): readonly Record<string, unknown>[] {
|
||||
const kind = stringAtNullable(item, "kind") ?? "unknown";
|
||||
const value = stringAtNullable(item, "value") ?? "<id>";
|
||||
const base = `bun scripts/cli.ts web-probe sentinel report --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)}`;
|
||||
const observe = `bun scripts/cli.ts web-probe observe collect ${shellQuote(value)} --node ${state.spec.nodeId} --lane ${state.spec.lane}`;
|
||||
if (kind === "run") {
|
||||
return [
|
||||
{ kind, view: "summary", command: `${base} --run ${shellQuote(value)} --view summary`, valuesRedacted: true },
|
||||
{ kind, view: "findings", command: `${base} --run ${shellQuote(value)} --view findings`, valuesRedacted: true },
|
||||
{ kind, view: "raw", command: `${base} --run ${shellQuote(value)} --view summary --raw`, valuesRedacted: true },
|
||||
];
|
||||
}
|
||||
if (kind === "observer") {
|
||||
return [
|
||||
{ kind, view: "turn-summary", command: `${observe} --view turn-summary`, valuesRedacted: true },
|
||||
{ kind, view: "timeline", command: `${observe} --view timeline`, valuesRedacted: true },
|
||||
{ kind, view: "analyze", command: `bun scripts/cli.ts web-probe observe analyze ${shellQuote(value)} --node ${state.spec.nodeId} --lane ${state.spec.lane}`, valuesRedacted: true },
|
||||
];
|
||||
}
|
||||
if (kind === "trace") {
|
||||
return [
|
||||
{ kind, view: "sentinel-trace-frame", command: `${base} --view trace-frame --trace-id ${shellQuote(value)}`, valuesRedacted: true },
|
||||
{ kind, view: "observe-trace-frame", command: `bun scripts/cli.ts web-probe observe collect <observerId> --node ${state.spec.nodeId} --lane ${state.spec.lane} --view trace-frame --trace-id ${shellQuote(value)} --sample-seq <seq>`, valuesRedacted: true },
|
||||
];
|
||||
}
|
||||
if (kind === "command") {
|
||||
return [{ kind, view: "observe-command-window", command: `bun scripts/cli.ts web-probe observe collect <observerId> --node ${state.spec.nodeId} --lane ${state.spec.lane} --view timeline --command-id ${shellQuote(value)}`, valuesRedacted: true }];
|
||||
}
|
||||
if (kind === "sample-seq" || kind === "turn") {
|
||||
return [{ kind, view: "sentinel-trace-frame", command: `${base} --view trace-frame --sample-seq ${shellQuote(value)}`, valuesRedacted: true }];
|
||||
}
|
||||
if (kind === "artifact") {
|
||||
return [{ kind, view: "observe-file", command: `bun scripts/cli.ts web-probe observe collect <observerId> --node ${state.spec.nodeId} --lane ${state.spec.lane} --view files --file ${shellQuote(value)}`, valuesRedacted: true }];
|
||||
}
|
||||
if (kind === "finding") {
|
||||
return [{ kind, view: "report-findings", command: `${base} --view findings --raw`, valuesRedacted: true }];
|
||||
}
|
||||
return [{ kind, view: "classification-needed", command: `bun scripts/cli.ts web-probe sentinel inspect-id ${shellQuote(value)} --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --raw`, valuesRedacted: true }];
|
||||
}
|
||||
|
||||
function monitorInspectNext(state: SentinelCicdState, evidence: { readonly ids: readonly Record<string, unknown>[] }): Record<string, unknown> {
|
||||
const runId = firstEvidenceValue(evidence, "run");
|
||||
const observerId = firstEvidenceValue(evidence, "observer");
|
||||
return {
|
||||
reportSummary: runId === null ? null : `bun scripts/cli.ts web-probe sentinel report --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --run ${shellQuote(runId)} --view summary`,
|
||||
reportFindings: runId === null ? null : `bun scripts/cli.ts web-probe sentinel report --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --run ${shellQuote(runId)} --view findings`,
|
||||
observerTurnSummary: observerId === null ? null : `bun scripts/cli.ts web-probe observe collect ${shellQuote(observerId)} --node ${state.spec.nodeId} --lane ${state.spec.lane} --view turn-summary`,
|
||||
raw: `bun scripts/cli.ts web-probe sentinel inspect-id <id> --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --raw`,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function monitorWebHttpDiagnosticPlan(state: SentinelCicdState, input: string): Record<string, unknown> {
|
||||
const publicBaseUrl = stringAtNullable(state.publicExposure, "publicBaseUrl");
|
||||
return {
|
||||
purpose: "monitor-web-shell-api-render-diagnostic-only",
|
||||
note: "HTTP probes are monitor-web evidence, not HWLAB business evidence; prefer the controlled report/observe commands for run, observer, trace, command and artifact ids.",
|
||||
input,
|
||||
publicBaseUrl,
|
||||
suggestedChecks: [
|
||||
publicBaseUrl === null ? null : `${publicBaseUrl.replace(/\/$/u, "")}/api/overview`,
|
||||
publicBaseUrl === null ? null : `${publicBaseUrl.replace(/\/$/u, "")}/api/runs`,
|
||||
].filter((item): item is string => item !== null),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function renderMonitorInspect(payload: Record<string, unknown>): string {
|
||||
const ids = Array.isArray(payload.evidenceIds) ? payload.evidenceIds.map(record) : [];
|
||||
const commands = Array.isArray(payload.evidenceCommands) ? payload.evidenceCommands.map(record) : [];
|
||||
const report = record(payload.report);
|
||||
const run = record(report.run);
|
||||
const findings = Array.isArray(report.findings) ? report.findings.map(record) : [];
|
||||
return [
|
||||
"Monitor Evidence Inspect",
|
||||
"=======================================================",
|
||||
`target=${payload.node}/${payload.lane}/${payload.sentinelId} inputKind=${payload.inputKind}`,
|
||||
Array.isArray(payload.warnings) && payload.warnings.length > 0 ? `warnings=${payload.warnings.join("; ")}` : "",
|
||||
"",
|
||||
"IDs",
|
||||
table(["KIND", "VALUE", "SOURCE"], ids.map((item) => [
|
||||
item.kind ?? "-",
|
||||
item.value ?? "-",
|
||||
item.source ?? "-",
|
||||
])),
|
||||
"",
|
||||
report.ok === undefined ? "" : "Report",
|
||||
report.ok === undefined ? "" : `run=${run.id ?? "-"} observer=${run.observerId ?? "-"} status=${run.status ?? "-"} report=${run.reportJsonSha256 ?? "-"} findings=${run.findingCount ?? findings.length}`,
|
||||
findings.length === 0 ? "" : table(["ID", "SEVERITY", "COUNT", "SUMMARY"], findings.slice(0, 8).map((item) => [
|
||||
item.id ?? "-",
|
||||
item.severity ?? "-",
|
||||
item.count ?? "-",
|
||||
reportText(item.summary, 100) ?? "-",
|
||||
])),
|
||||
"",
|
||||
"Next",
|
||||
commands.length === 0 ? "-" : commands.slice(0, 8).map((item) => ` ${item.view ?? item.kind}: ${item.command}`).join("\n"),
|
||||
].filter((line) => line !== "").join("\n");
|
||||
}
|
||||
|
||||
function compactSentinelReportRawPayload(
|
||||
state: SentinelCicdState,
|
||||
body: Record<string, unknown>,
|
||||
|
||||
Reference in New Issue
Block a user