feat: wire web probe sentinel validation (#912)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-26 00:46:45 +08:00
committed by GitHub
parent 4bd179e898
commit db3fed012e
6 changed files with 1205 additions and 13 deletions
+151 -2
View File
@@ -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`);