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
+199
View File
@@ -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>,