feat: wire web probe sentinel validation (#912)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -39,6 +39,7 @@ interface MaintenanceState {
|
||||
readonly startedAt: string | null;
|
||||
readonly stoppedAt: string | null;
|
||||
readonly quickVerifyPlannedAt: string | null;
|
||||
readonly quickVerifyPlannedRunId: string | null;
|
||||
}
|
||||
|
||||
interface CommandPlanStep {
|
||||
@@ -60,6 +61,8 @@ export interface WebProbeSentinelService {
|
||||
maintenance(): MaintenanceState;
|
||||
setMaintenance(active: boolean, input: Record<string, unknown>): MaintenanceState;
|
||||
planScenarioRun(scenarioId: string, reason: string): Record<string, unknown>;
|
||||
recordRun(input: Record<string, unknown>): Record<string, unknown>;
|
||||
report(view: string, runId: string | null): Record<string, unknown>;
|
||||
metrics(): string;
|
||||
dashboardHtml(): string;
|
||||
fetch(request: Request): Promise<Response>;
|
||||
@@ -172,6 +175,7 @@ export function createWebProbeSentinelService(options: WebProbeSentinelServiceOp
|
||||
startedAt: nowIso(),
|
||||
stoppedAt: current.stoppedAt,
|
||||
quickVerifyPlannedAt: current.quickVerifyPlannedAt,
|
||||
quickVerifyPlannedRunId: current.quickVerifyPlannedRunId,
|
||||
}
|
||||
: {
|
||||
active: false,
|
||||
@@ -180,11 +184,17 @@ export function createWebProbeSentinelService(options: WebProbeSentinelServiceOp
|
||||
startedAt: current.startedAt,
|
||||
stoppedAt: nowIso(),
|
||||
quickVerifyPlannedAt: nowIso(),
|
||||
quickVerifyPlannedRunId: null,
|
||||
};
|
||||
writeMetadata(db, "maintenance", next);
|
||||
if (!active) {
|
||||
const scenarioId = firstEnabledScenarioId(config);
|
||||
if (scenarioId !== null) this.planScenarioRun(scenarioId, "maintenance-stop-quick-verify");
|
||||
if (scenarioId !== null) {
|
||||
const planned = this.planScenarioRun(scenarioId, "maintenance-stop-quick-verify");
|
||||
const withPlan = { ...next, quickVerifyPlannedRunId: stringOrNull(planned.runId) };
|
||||
writeMetadata(db, "maintenance", withPlan);
|
||||
return withPlan;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
},
|
||||
@@ -198,6 +208,12 @@ export function createWebProbeSentinelService(options: WebProbeSentinelServiceOp
|
||||
.run(runId, scenarioId, config.node, config.lane, "planned", this.maintenance().active ? 1 : 0, createdAt, createdAt, JSON.stringify({ reason, commandPlan, valuesRedacted: true }));
|
||||
return { ok: true, runId, scenarioId, status: "planned", commandPlanSha256: sha256Json(commandPlan), valuesRedacted: true };
|
||||
},
|
||||
recordRun(input: Record<string, unknown>) {
|
||||
return recordRunResult(config, db, input);
|
||||
},
|
||||
report(view: string, runId: string | null) {
|
||||
return reportRunView(config, db, view, runId);
|
||||
},
|
||||
metrics() {
|
||||
return renderMetrics(config, db, this.health(), this.maintenance());
|
||||
},
|
||||
@@ -239,6 +255,16 @@ async function sentinelFetch(service: WebProbeSentinelService, request: Request)
|
||||
const body = await readJsonBody(request);
|
||||
return jsonResponse(service.planScenarioRun(stringField(body, "scenarioId"), stringOrNull(body.reason) ?? "manual"));
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/runs/record") {
|
||||
const body = await readJsonBody(request);
|
||||
return jsonResponse(service.recordRun(body));
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/api/report") {
|
||||
const view = url.searchParams.get("view") ?? stringOrNull(service.config.reportViews.defaultView) ?? "summary";
|
||||
const runId = url.searchParams.get("run") ?? url.searchParams.get("runId");
|
||||
const report = service.report(view, runId);
|
||||
return jsonResponse(report, report.ok === false ? 404 : 200);
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/metrics") {
|
||||
return new Response(service.metrics(), { headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" } });
|
||||
}
|
||||
@@ -478,7 +504,124 @@ function jsonResponse(value: unknown, status = 200): Response {
|
||||
}
|
||||
|
||||
function emptyMaintenance(): MaintenanceState {
|
||||
return { active: false, reason: null, releaseId: null, startedAt: null, stoppedAt: null, quickVerifyPlannedAt: null };
|
||||
return { active: false, reason: null, releaseId: null, startedAt: null, stoppedAt: null, quickVerifyPlannedAt: null, quickVerifyPlannedRunId: null };
|
||||
}
|
||||
|
||||
function recordRunResult(config: WebProbeSentinelServiceConfig, db: Database, input: Record<string, unknown>): Record<string, unknown> {
|
||||
const now = nowIso();
|
||||
const runId = stringOrNull(input.runId) ?? `sentinel-run-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
||||
const scenarioId = stringOrNull(input.scenarioId) ?? firstEnabledScenarioId(config);
|
||||
if (scenarioId === null) return { ok: false, error: "scenario-missing", valuesRedacted: true };
|
||||
const status = stringOrNull(input.status) ?? "analyzed";
|
||||
const observerId = stringOrNull(input.observerId);
|
||||
const stateDir = stringOrNull(input.stateDir);
|
||||
const reportJsonSha256 = stringOrNull(input.reportJsonSha256);
|
||||
const findings = arrayRecords(input.findings).slice(0, 50);
|
||||
const findingCount = numberOr(input.findingCount, findings.length);
|
||||
const artifactCount = numberOr(input.artifactCount, 0);
|
||||
const createdAt = stringOrNull(input.createdAt) ?? now;
|
||||
db.query(`
|
||||
INSERT INTO runs (id, scenario_id, node, lane, status, observer_id, state_dir, report_json_sha256, finding_count, artifact_count, maintenance, created_at, updated_at, command_plan_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
status = excluded.status,
|
||||
observer_id = excluded.observer_id,
|
||||
state_dir = excluded.state_dir,
|
||||
report_json_sha256 = excluded.report_json_sha256,
|
||||
finding_count = excluded.finding_count,
|
||||
artifact_count = excluded.artifact_count,
|
||||
updated_at = excluded.updated_at
|
||||
`).run(runId, scenarioId, config.node, config.lane, status, observerId, stateDir, reportJsonSha256, findingCount, artifactCount, thisMaintenanceFlag(input), createdAt, now, JSON.stringify({ source: "recorded-analyze-summary", valuesRedacted: true }));
|
||||
db.query("DELETE FROM findings WHERE run_id = ?").run(runId);
|
||||
for (const item of findings) {
|
||||
const findingId = stringOrNull(item.id) ?? stringOrNull(item.kind) ?? stringOrNull(item.code) ?? "finding";
|
||||
const severity = stringOrNull(item.severity) ?? stringOrNull(item.level) ?? "unknown";
|
||||
const summary = stringOrNull(item.summary) ?? stringOrNull(item.message) ?? findingId;
|
||||
db.query("INSERT INTO findings (run_id, finding_id, severity, count, summary, report_json_sha256, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
||||
.run(runId, findingId.slice(0, 160), severity.slice(0, 40), numberOr(item.count, 1), summary.slice(0, 500), reportJsonSha256, now);
|
||||
}
|
||||
writeMetadata(db, `run.report.${runId}`, {
|
||||
runId,
|
||||
scenarioId,
|
||||
observerId,
|
||||
stateDir,
|
||||
reportJsonSha256,
|
||||
summary: record(input.summary),
|
||||
views: record(input.views),
|
||||
publicOrigin: stringOrNull(input.publicOrigin),
|
||||
screenshot: record(input.screenshot),
|
||||
artifactCount,
|
||||
findingCount,
|
||||
valuesRedacted: true,
|
||||
});
|
||||
return { ok: true, runId, scenarioId, status, reportJsonSha256, findingCount, artifactCount, valuesRedacted: true };
|
||||
}
|
||||
|
||||
function reportRunView(config: WebProbeSentinelServiceConfig, db: Database, view: string, runId: string | null): Record<string, unknown> {
|
||||
if (!stringArrayAt(config.reportViews, "views").includes(view)) {
|
||||
return { ok: false, error: "unsupported-report-view", view, valuesRedacted: true };
|
||||
}
|
||||
const row = runId === null
|
||||
? db.query("SELECT * FROM runs WHERE report_json_sha256 IS NOT NULL ORDER BY updated_at DESC LIMIT 1").get() as Record<string, unknown> | null
|
||||
: db.query("SELECT * FROM runs WHERE id = ?").get(runId) as Record<string, unknown> | null;
|
||||
if (row === null) return { ok: false, error: "report-run-missing", runId, view, valuesRedacted: true };
|
||||
const selectedRunId = stringOrNull(row.id);
|
||||
if (selectedRunId === null) return { ok: false, error: "report-run-id-missing", view, valuesRedacted: true };
|
||||
const stored = readMetadata(db, `run.report.${selectedRunId}`) ?? {};
|
||||
const findings = db.query("SELECT finding_id, severity, count, summary, report_json_sha256, created_at FROM findings WHERE run_id = ? ORDER BY created_at DESC LIMIT 50")
|
||||
.all(selectedRunId) as Record<string, unknown>[];
|
||||
const views = record(stored.views);
|
||||
const storedView = record(views[view]);
|
||||
const renderedText = typeof storedView.renderedText === "string" ? storedView.renderedText : view === "summary" ? renderStoredSummary(row, stored, findings) : view === "findings" ? renderStoredFindings(row, findings) : null;
|
||||
if (renderedText === null) {
|
||||
return { ok: false, error: "report-view-not-indexed", runId: selectedRunId, view, availableViews: Object.keys(views), valuesRedacted: true };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
view,
|
||||
run: row,
|
||||
summary: record(stored.summary),
|
||||
findings,
|
||||
renderedText,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function renderStoredSummary(row: Record<string, unknown>, stored: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
|
||||
const summary = record(stored.summary);
|
||||
return [
|
||||
"Web Probe Sentinel Report",
|
||||
"=======================================================",
|
||||
`run=${stringOrNull(row.id) ?? "-"} scenario=${stringOrNull(row.scenario_id) ?? "-"} status=${stringOrNull(row.status) ?? "-"}`,
|
||||
`observer=${stringOrNull(row.observer_id) ?? "-"} stateDir=${stringOrNull(row.state_dir) ?? "-"}`,
|
||||
`report=${stringOrNull(row.report_json_sha256) ?? "-"} artifacts=${String(row.artifact_count ?? 0)} findings=${String(row.finding_count ?? findings.length)}`,
|
||||
`publicOrigin=${stringOrNull(stored.publicOrigin) ?? "-"}`,
|
||||
`analysisWindow=${JSON.stringify(record(summary.analysisWindow))}`,
|
||||
"",
|
||||
"Findings",
|
||||
findings.length === 0 ? "-" : findings.slice(0, 12).map((item) => `${item.severity ?? "-"} ${item.finding_id ?? "-"} count=${item.count ?? "-"} ${item.summary ?? ""}`).join("\n"),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderStoredFindings(row: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
|
||||
return [
|
||||
"Web Probe Sentinel Findings",
|
||||
"=======================================================",
|
||||
`run=${stringOrNull(row.id) ?? "-"} report=${stringOrNull(row.report_json_sha256) ?? "-"}`,
|
||||
findings.length === 0 ? "-" : findings.map((item) => `${item.severity ?? "-"} ${item.finding_id ?? "-"} count=${item.count ?? "-"} ${item.summary ?? ""}`).join("\n"),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function thisMaintenanceFlag(input: Record<string, unknown>): number {
|
||||
return input.maintenance === true ? 1 : 0;
|
||||
}
|
||||
|
||||
function numberOr(value: unknown, fallback: number): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function arrayRecords(value: unknown): Record<string, unknown>[] {
|
||||
return Array.isArray(value) ? value.map(record) : [];
|
||||
}
|
||||
|
||||
function checkPath(value: unknown, path: string): unknown {
|
||||
@@ -511,6 +654,12 @@ function arrayAt(value: unknown, path: string): Record<string, unknown>[] {
|
||||
return result.filter(record);
|
||||
}
|
||||
|
||||
function stringArrayAt(value: unknown, path: string): string[] {
|
||||
const result = checkPath(value, path);
|
||||
if (!Array.isArray(result)) throw new Error(`${path} must be an array`);
|
||||
return result.filter((item): item is string => typeof item === "string" && item.length > 0);
|
||||
}
|
||||
|
||||
function stringField(value: Record<string, unknown>, key: string): string {
|
||||
const found = value[key];
|
||||
if (typeof found !== "string" || found.length === 0) throw new Error(`${key} must be a non-empty string`);
|
||||
|
||||
Reference in New Issue
Block a user