feat: add web probe sentinel config plan (#895)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-25 22:07:22 +08:00
committed by GitHub
parent fd874ceef5
commit 9309efabb8
13 changed files with 786 additions and 3 deletions
+63
View File
@@ -1,4 +1,5 @@
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// Responsibility: YAML source-of-truth parsing for HWLAB node/lane Workbench observability.
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
@@ -131,6 +132,15 @@ export interface HwlabRuntimeWebProbeSpec {
readonly projectManagement?: HwlabRuntimeWebProbeProjectManagementSpec;
}
export type HwlabRuntimeWebProbeSentinelConfigRefKey = "runtime" | "scenarios" | "promptSet" | "reportViews" | "publicExposure" | "cicd" | "secrets";
export const HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS = ["runtime", "scenarios", "promptSet", "reportViews", "publicExposure", "cicd", "secrets"] as const satisfies readonly HwlabRuntimeWebProbeSentinelConfigRefKey[];
export interface HwlabRuntimeWebProbeSentinelSpec {
readonly enabled: boolean;
readonly configRefs: Record<HwlabRuntimeWebProbeSentinelConfigRefKey, string>;
}
export interface HwlabRuntimeWebProbeAlertThresholdsSpec {
readonly sameOriginApiSlowMs: number;
readonly partialApiSlowMs: number;
@@ -163,10 +173,15 @@ export interface HwlabRuntimeObservabilitySpec {
readonly traceExplorerUrlTemplate?: string;
readonly metricsEndpoint?: HwlabRuntimeObservabilityMetricsEndpointSpec;
readonly workbench?: HwlabRuntimeObservabilityWorkbenchSpec;
readonly webProbe?: HwlabRuntimeObservabilityWebProbeSpec;
readonly recordingRules: readonly HwlabRuntimeObservabilityRecordingRuleSpec[];
readonly warningAlerts: readonly HwlabRuntimeObservabilityWarningAlertSpec[];
}
export interface HwlabRuntimeObservabilityWebProbeSpec {
readonly sentinel?: HwlabRuntimeWebProbeSentinelSpec;
}
export interface HwlabRuntimeObservabilityMetricsEndpointSpec {
readonly serviceName: string;
readonly containerName: string;
@@ -732,6 +747,41 @@ function webProbeConfig(value: unknown, path: string): HwlabRuntimeWebProbeSpec
};
}
function webProbeSentinelConfig(value: unknown, path: string): HwlabRuntimeWebProbeSentinelSpec {
const raw = asRecord(value, path);
const allowed = new Set(["enabled", "configRefs"]);
for (const key of Object.keys(raw)) {
if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; root sentinel YAML may only contain enabled/configRefs`);
}
const refs = asRecord(raw.configRefs, `${path}.configRefs`);
const allowedRefKeys = new Set(HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS);
for (const key of Object.keys(refs)) {
if (!allowedRefKeys.has(key as HwlabRuntimeWebProbeSentinelConfigRefKey)) throw new Error(`${path}.configRefs.${key} is not a supported sentinel configRef`);
}
const configRefs = Object.fromEntries(HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS.map((key) => {
const ref = stringField(refs, key, `${path}.configRefs`);
validateConfigRef(ref, `${path}.configRefs.${key}`);
return [key, ref];
})) as Record<HwlabRuntimeWebProbeSentinelConfigRefKey, string>;
return {
enabled: booleanField(raw, "enabled", path),
configRefs,
};
}
function validateConfigRef(ref: string, path: string): void {
const [file, fragment, extra] = ref.split("#");
if (extra !== undefined || file === undefined || fragment === undefined || file.length === 0 || fragment.length === 0) {
throw new Error(`${path} must use path/to/file.yaml#object.path syntax`);
}
if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
throw new Error(`${path} must reference a repo-relative config/*.yaml file without ..`);
}
if (!/^[A-Za-z0-9_.\-[\]]+$/u.test(fragment)) {
throw new Error(`${path} has an unsupported YAML path fragment`);
}
}
function webProbeProjectManagementConfig(value: unknown, path: string): HwlabRuntimeWebProbeProjectManagementSpec {
const raw = asRecord(value, path);
const targetPaths = nonEmptyStringArrayField(raw, "targetPaths", path);
@@ -853,11 +903,24 @@ function observabilityConfig(value: unknown, path: string): HwlabRuntimeObservab
traceExplorerUrlTemplate,
metricsEndpoint: observabilityMetricsEndpointConfig(raw.metricsEndpoint, `${path}.metricsEndpoint`),
workbench: observabilityWorkbenchConfig(raw.workbench, `${path}.workbench`),
webProbe: observabilityWebProbeConfig(raw.webProbe, `${path}.webProbe`),
recordingRules,
warningAlerts,
};
}
function observabilityWebProbeConfig(value: unknown, path: string): HwlabRuntimeObservabilityWebProbeSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const allowed = new Set(["sentinel"]);
for (const key of Object.keys(raw)) {
if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; observability.webProbe currently only owns sentinel`);
}
return {
...(raw.sentinel === undefined ? {} : { sentinel: webProbeSentinelConfig(raw.sentinel, `${path}.sentinel`) }),
};
}
function validateTraceExplorerUrlTemplate(template: string, path: string): void {
if (!template.includes("{trace_id}")) throw new Error(`${path} must include {trace_id}`);
if (template.startsWith("//")) throw new Error(`${path} must not be protocol-relative`);