diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 0deca1c8..850b17ed 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -653,6 +653,12 @@ lanes: observability: prometheusOperator: false webProbe: + monitorRoot: + enabled: true + sentinelId: workbench-fake-echo-session-invariance-10x + publicBaseUrl: https://monitor.pikapython.com + routePrefix: / + caddyManagedBlockOwner: hwlab-web-probe-sentinel-active-root sentinels: - id: workbench-dsflash-go-tool-call-10x enabled: true diff --git a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js index f7edc83a..c0136040 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js @@ -159,7 +159,7 @@ createApp({ function currentHref(item) { if (!item || item.id === bootstrap.sentinelId) return bootstrap.basePath || "/"; - if (item.id === "workbench-dsflash-go-tool-call-10x") return "/"; + if (item.monitorRoot === true) return "/"; return `/sentinels/${encodeURIComponent(item.id)}/`; } diff --git a/scripts/src/hwlab-node-lanes.ts b/scripts/src/hwlab-node-lanes.ts index 0bf3e144..24831e94 100644 --- a/scripts/src/hwlab-node-lanes.ts +++ b/scripts/src/hwlab-node-lanes.ts @@ -164,6 +164,14 @@ export interface HwlabRuntimeWebProbeSentinelRegistryItemSpec { readonly configRef: string; } +export interface HwlabRuntimeWebProbeMonitorRootSpec { + readonly enabled: boolean; + readonly sentinelId: string; + readonly publicBaseUrl: string; + readonly routePrefix: "/"; + readonly caddyManagedBlockOwner: string; +} + export interface HwlabRuntimeWebProbeAlertThresholdsSpec { readonly sameOriginApiSlowMs: number; readonly partialApiSlowMs: number; @@ -206,6 +214,7 @@ export interface HwlabRuntimeObservabilitySpec { export interface HwlabRuntimeObservabilityWebProbeSpec { readonly sentinel?: HwlabRuntimeWebProbeSentinelSpec; readonly sentinels?: readonly HwlabRuntimeWebProbeSentinelRegistryItemSpec[]; + readonly monitorRoot?: HwlabRuntimeWebProbeMonitorRootSpec; } export interface HwlabRuntimeObservabilityMetricsEndpointSpec { @@ -1141,14 +1150,56 @@ function observabilityConfig(value: unknown, path: string): HwlabRuntimeObservab function observabilityWebProbeConfig(value: unknown, path: string): HwlabRuntimeObservabilityWebProbeSpec | undefined { if (value === undefined) return undefined; const raw = asRecord(value, path); - const allowed = new Set(["sentinel", "sentinels"]); + const allowed = new Set(["sentinel", "sentinels", "monitorRoot"]); 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/sentinels`); + if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; observability.webProbe currently only owns sentinel/sentinels/monitorRoot`); } if (raw.sentinel !== undefined && raw.sentinels !== undefined) throw new Error(`${path} may declare sentinel or sentinels, not both`); + const sentinel = raw.sentinel === undefined ? undefined : webProbeSentinelConfig(raw.sentinel, `${path}.sentinel`); + const sentinels = raw.sentinels === undefined ? undefined : webProbeSentinelRegistryConfig(raw.sentinels, `${path}.sentinels`); + const monitorRoot = raw.monitorRoot === undefined ? undefined : webProbeMonitorRootConfig(raw.monitorRoot, `${path}.monitorRoot`); + if (monitorRoot !== undefined) { + if (sentinels !== undefined && !sentinels.some((item) => item.id === monitorRoot.sentinelId)) { + throw new Error(`${path}.monitorRoot.sentinelId must reference one entry from ${path}.sentinels`); + } + if (sentinel !== undefined && monitorRoot.sentinelId !== "workbench-dsflash-go-tool-call-10x") { + throw new Error(`${path}.monitorRoot.sentinelId must be workbench-dsflash-go-tool-call-10x for legacy sentinel config`); + } + } return { - ...(raw.sentinel === undefined ? {} : { sentinel: webProbeSentinelConfig(raw.sentinel, `${path}.sentinel`) }), - ...(raw.sentinels === undefined ? {} : { sentinels: webProbeSentinelRegistryConfig(raw.sentinels, `${path}.sentinels`) }), + ...(sentinel === undefined ? {} : { sentinel }), + ...(sentinels === undefined ? {} : { sentinels }), + ...(monitorRoot === undefined ? {} : { monitorRoot }), + }; +} + +function webProbeMonitorRootConfig(value: unknown, path: string): HwlabRuntimeWebProbeMonitorRootSpec { + const raw = asRecord(value, path); + const allowed = new Set(["enabled", "sentinelId", "publicBaseUrl", "routePrefix", "caddyManagedBlockOwner"]); + for (const key of Object.keys(raw)) { + if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; monitorRoot may only contain enabled/sentinelId/publicBaseUrl/routePrefix/caddyManagedBlockOwner`); + } + const sentinelId = stringField(raw, "sentinelId", path); + if (!/^[a-z0-9][a-z0-9-]{1,80}$/u.test(sentinelId)) throw new Error(`${path}.sentinelId must be a stable lowercase sentinel id`); + const publicBaseUrl = stringField(raw, "publicBaseUrl", path).replace(/\/+$/u, ""); + try { + const parsed = new URL(publicBaseUrl); + if (parsed.protocol !== "https:") throw new Error("must use https"); + if (parsed.pathname !== "/" || parsed.search.length > 0 || parsed.hash.length > 0) throw new Error("must point to the public origin root"); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`${path}.publicBaseUrl must be an https origin root URL: ${message}`); + } + const routePrefix = stringField(raw, "routePrefix", path); + if (routePrefix !== "/") throw new Error(`${path}.routePrefix must be / for the monitor root switch`); + const caddyManagedBlockOwner = stringField(raw, "caddyManagedBlockOwner", path); + if (!/^[a-z0-9][a-z0-9-]{1,100}$/u.test(caddyManagedBlockOwner)) throw new Error(`${path}.caddyManagedBlockOwner must be a stable lowercase owner id`); + return { + enabled: booleanField(raw, "enabled", path), + sentinelId, + publicBaseUrl, + routePrefix, + caddyManagedBlockOwner, }; } diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 8b88f24e..cd626a55 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -12,7 +12,7 @@ import { repoRoot, rootPath } from "./config"; import { runCommand, type CommandResult } from "./command"; import { startJob } from "./jobs"; import { webProbeSentinelConfigPlan, withWebProbeSentinelConfigRendered } from "./hwlab-node-web-sentinel-config"; -import { requireSentinelIdForRegistry, resolveWebProbeSentinel } from "./hwlab-node-web-sentinel-resolver"; +import { effectiveWebProbeSentinelPublicExposure, requireSentinelIdForRegistry, resolveWebProbeSentinel } from "./hwlab-node-web-sentinel-resolver"; import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes"; import type { RenderedCliResult } from "./output"; import { runWebProbeRemoteArtifactJob } from "./web-probe-remote-artifact"; @@ -310,7 +310,8 @@ function loadSentinelCicdState(spec: HwlabRuntimeLaneSpec, sentinelId: string | const runtime = recordTarget(readConfigRefTarget(sentinel.configRefs.runtime), sentinel.configRefs.runtime); const cicd = recordTarget(readConfigRefTarget(sentinel.configRefs.cicd), sentinel.configRefs.cicd); const scenarios = readConfigRefTarget(sentinel.configRefs.scenarios); - const publicExposure = recordTarget(readConfigRefTarget(sentinel.configRefs.publicExposure), sentinel.configRefs.publicExposure); + const rawPublicExposure = recordTarget(readConfigRefTarget(sentinel.configRefs.publicExposure), sentinel.configRefs.publicExposure); + const publicExposure = effectiveWebProbeSentinelPublicExposure(spec, sentinel.id, rawPublicExposure); const secrets = recordTarget(readConfigRefTarget(sentinel.configRefs.secrets), sentinel.configRefs.secrets); const controlPlaneRef = stringField(cicd, "controlPlaneConfigRef"); const controlPlaneTarget = recordTarget(readConfigRefTarget(controlPlaneRef), controlPlaneRef); @@ -3584,6 +3585,9 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe const responseHeaderTimeoutSeconds = numberAt(state.publicExposure, "caddy.responseHeaderTimeoutSeconds"); const remotePort = numberAt(state.publicExposure, "frpc.httpProxy.remotePort"); const routePrefix = normalizeRoutePrefix(stringAtNullable(state.publicExposure, "routePrefix")); + const rootOrder = stringAtNullable(state.publicExposure, "caddy.rootOrder") ?? "normal"; + const monitorRoot = record(state.publicExposure.monitorRoot); + const cleanupOwner = monitorRoot.enabled === false ? stringAtNullable(monitorRoot, "caddyManagedBlockOwner") : null; const proxyLines = [ `reverse_proxy 127.0.0.1:${remotePort} {`, " transport http {", @@ -3605,6 +3609,8 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe `config_path=${shellQuote(configPath)}`, `service=${shellQuote(serviceName)}`, `route_prefix=${shellQuote(routePrefix)}`, + `root_order=${shellQuote(rootOrder)}`, + `cleanup_owner=${shellQuote(cleanupOwner ?? "")}`, `block_b64=${shellQuote(blockB64)}`, "marker=\"unidesk managed $owner\"", "tmp=$(mktemp -d)", @@ -3613,17 +3619,21 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe "next=\"$tmp/Caddyfile\"", "printf '%s' \"$block_b64\" | base64 -d >\"$block\"", "if [ -f \"$config_path\" ]; then cp \"$config_path\" \"$next\"; else : >\"$next\"; fi", - "python3 - \"$next\" \"$block\" \"$marker\" \"$hostname\" \"$route_prefix\" <<'PY' >/tmp/web-probe-sentinel-caddy-python.out 2>/tmp/web-probe-sentinel-caddy-python.err", + "python3 - \"$next\" \"$block\" \"$marker\" \"$hostname\" \"$route_prefix\" \"$root_order\" \"$cleanup_owner\" <<'PY' >/tmp/web-probe-sentinel-caddy-python.out 2>/tmp/web-probe-sentinel-caddy-python.err", "import pathlib, re, sys", "config = pathlib.Path(sys.argv[1])", "block = pathlib.Path(sys.argv[2]).read_text(encoding='utf-8')", "marker = sys.argv[3]", "hostname = sys.argv[4]", "route_prefix = sys.argv[5]", + "root_order = sys.argv[6]", + "cleanup_owner = sys.argv[7]", "text = config.read_text(encoding='utf-8') if config.exists() else ''", "begin = f'# BEGIN {marker}'", "end = f'# END {marker}'", - "pattern = re.compile(rf'(?ms)^[ \\t]*# BEGIN {re.escape(marker)}\\n.*?^[ \\t]*# END {re.escape(marker)}\\n*')", + "def managed_pattern(marker_text):", + " return re.compile(rf'(?ms)^[ \\t]*# BEGIN {re.escape(marker_text)}\\n.*?^[ \\t]*# END {re.escape(marker_text)}\\n*')", + "pattern = managed_pattern(marker)", "def collect_nested_managed(segment):", " preserved = []", " lines = segment.splitlines()", @@ -3647,6 +3657,10 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe "for match in pattern.finditer(text):", " preserved_blocks.extend(collect_nested_managed(match.group(0)))", "text = pattern.sub('', text)", + "if cleanup_owner:", + " cleanup_marker = f'unidesk managed {cleanup_owner}'", + " if cleanup_marker != marker:", + " text = managed_pattern(cleanup_marker).sub('', text)", "def site_span(src, host):", " match = re.search(rf'(?m)^([ \\t]*){re.escape(host)}[ \\t]*\\{{[ \\t]*\\n', src)", " if not match:", @@ -3696,7 +3710,9 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe " relative_open = open_end - start", " close_rel = close_index - start", " additions = ''.join(preserved_blocks) + managed", - " if route_prefix == '/':", + " if route_prefix == '/' and root_order == 'active':", + " replacement = site[:relative_open] + additions + site[relative_open:]", + " elif route_prefix == '/':", " replacement = append_before_close(site, close_rel, additions)", " else:", " insert_at = fallback_insert_pos(site, relative_open, close_rel)", @@ -3753,7 +3769,7 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe ].join("\n"); const result = runCommand(["trans", stringAt(state.publicExposure, "caddy.route"), "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const parsed = parseJsonObject(result.stdout); - return { ok: result.exitCode === 0 && parsed?.ok === true, routePrefix, ...record(parsed), result: compactCommand(result), valuesRedacted: true }; + return { ok: result.exitCode === 0 && parsed?.ok === true, routePrefix, rootOrder, ...record(parsed), result: compactCommand(result), valuesRedacted: true }; } function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: string, timeoutSeconds: number): Record { diff --git a/scripts/src/hwlab-node-web-sentinel-config.ts b/scripts/src/hwlab-node-web-sentinel-config.ts index b3caa6a8..3186d814 100644 --- a/scripts/src/hwlab-node-web-sentinel-config.ts +++ b/scripts/src/hwlab-node-web-sentinel-config.ts @@ -5,7 +5,7 @@ import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { rootPath } from "./config"; import { HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS, type HwlabRuntimeLaneSpec, type HwlabRuntimeWebProbeSentinelConfigRefKey } from "./hwlab-node-lanes"; -import { resolveWebProbeSentinel, webProbeSentinelRegistryRows, type WebProbeSentinelRegistryRow } from "./hwlab-node-web-sentinel-resolver"; +import { effectiveWebProbeSentinelPublicExposure, resolveWebProbeSentinel, webProbeSentinelRegistryRows, type WebProbeSentinelRegistryRow } from "./hwlab-node-web-sentinel-resolver"; import type { RenderedCliResult } from "./output"; export type WebProbeSentinelConfigAction = "plan" | "status"; @@ -211,7 +211,9 @@ export function webProbeSentinelConfigPlan(spec: HwlabRuntimeLaneSpec, action: W } const selected = resolveWebProbeSentinel(spec, sentinelId); - const refs = HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS.map((key) => readSentinelConfigRef(key, selected.configRefs[key])); + const refs = HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS + .map((key) => readSentinelConfigRef(key, selected.configRefs[key])) + .map((ref) => effectiveConfigRefStatus(spec, selected.id, ref)); const conflicts = selected.enabled ? crossReferenceConflicts(spec, refs) : []; const refBlocked = refs.some((ref) => !ref.present || !ref.targetPresent || ref.missingFields.length > 0 || ref.conflicts.length > 0 || ref.error !== null); const ok = selected.enabled && !refBlocked && conflicts.length === 0; @@ -232,6 +234,18 @@ export function webProbeSentinelConfigPlan(spec: HwlabRuntimeLaneSpec, action: W }; } +function effectiveConfigRefStatus(spec: HwlabRuntimeLaneSpec, sentinelId: string, ref: InternalConfigRefStatus): InternalConfigRefStatus { + if (ref.key !== "publicExposure" || !isRecord(ref.target)) return ref; + const target = effectiveWebProbeSentinelPublicExposure(spec, sentinelId, ref.target); + return { + ...ref, + targetKind: targetKindOf(target), + missingFields: missingFieldsForTarget(ref.key, target), + summary: summarizeTarget(ref.key, target), + target, + }; +} + export function withWebProbeSentinelConfigRendered(value: WebProbeSentinelConfigPlan): RenderedCliResult { return { ok: value.ok, @@ -416,7 +430,15 @@ function summarizeTarget(key: HwlabRuntimeWebProbeSentinelConfigRefKey, target: if (key === "runtime") return `namespace=${textAt(target, "namespace")} service=${textAt(target, "serviceName")} image=${short(textAt(target, "imageRef"), 48)}`; if (key === "promptSet") return `id=${textAt(target, "id")} provider=${textAt(target, "providerProfile")} prompts=${textAt(target, "promptCount")} markers=${arrayAt(target, "expectedMarkers").slice(0, 12).join(",") || "-"} source=${textAt(target, "promptSourceRef")}:${textAt(target, "promptSourceKey")}`; if (key === "reportViews") return `default=${textAt(target, "defaultView")} views=${arrayAt(target, "views").length}`; - if (key === "publicExposure") return `enabled=${textAt(target, "enabled")} mode=${textAt(target, "mode")} url=${textAt(target, "publicBaseUrl")}`; + if (key === "publicExposure") { + const monitorRoot = textAt(target, "monitorRoot.enabled"); + const rootOrder = textAt(target, "caddy.rootOrder"); + const suffix = [ + ...(monitorRoot === "-" ? [] : [`monitorRoot=${monitorRoot}`]), + ...(rootOrder === "-" ? [] : [`rootOrder=${rootOrder}`]), + ].join(" "); + return `enabled=${textAt(target, "enabled")} mode=${textAt(target, "mode")} url=${textAt(target, "publicBaseUrl")}${suffix.length === 0 ? "" : ` ${suffix}`}`; + } if (key === "cicd") return `gitops=${textAt(target, "gitopsPath")} image=${textAt(target, "image.repository")}:${textAt(target, "image.tagSource")} confirmWait=${textAt(target, "confirmWait.maxSeconds")} targetValidation=${textAt(target, "targetValidation.maxSeconds")}`; if (key === "secrets") return `sources=${arrayAt(target, "sources").length} runtimeSecrets=${arrayAt(target, "runtimeSecrets").length}`; return `keys=${Object.keys(target).length}`; @@ -442,8 +464,8 @@ function renderWebProbeSentinelConfigPlan(value: WebProbeSentinelConfigPlan): st ...(value.sentinels.length === 0 ? [] : [ "", sentinelTable( - ["SENTINEL", "ENABLED", "CONFIG_REF"], - value.sentinels.map((item) => [item.id, item.enabled, short(item.configRef, 110)]), + ["SENTINEL", "ENABLED", "ROOT", "CONFIG_REF"], + value.sentinels.map((item) => [item.id, item.enabled, item.monitorRoot ? "monitor-root" : "-", short(item.configRef, 110)]), ), ]), ...(value.refs.length === 0 ? [ diff --git a/scripts/src/hwlab-node-web-sentinel-dashboard-assets.ts b/scripts/src/hwlab-node-web-sentinel-dashboard-assets.ts index 15139dab..ed712ccb 100644 --- a/scripts/src/hwlab-node-web-sentinel-dashboard-assets.ts +++ b/scripts/src/hwlab-node-web-sentinel-dashboard-assets.ts @@ -61,10 +61,11 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig `; } -function sentinelRegistryRows(config: DashboardShellConfig): Array<{ readonly id: string; readonly enabled: boolean }> { +function sentinelRegistryRows(config: DashboardShellConfig): Array<{ readonly id: string; readonly enabled: boolean; readonly monitorRoot: boolean }> { return Array.isArray(config.plan.sentinels) ? config.plan.sentinels.map((item) => ({ id: stringOrNull(item.id) ?? "", enabled: item.enabled !== false, + monitorRoot: item.monitorRoot === true, })).filter((item) => item.id.length > 0) : []; } diff --git a/scripts/src/hwlab-node-web-sentinel-resolver.ts b/scripts/src/hwlab-node-web-sentinel-resolver.ts index 727ab839..bf062af0 100644 --- a/scripts/src/hwlab-node-web-sentinel-resolver.ts +++ b/scripts/src/hwlab-node-web-sentinel-resolver.ts @@ -18,18 +18,57 @@ export interface WebProbeSentinelRegistryRow { readonly id: string; readonly enabled: boolean; readonly configRef: string; + readonly monitorRoot: boolean; + readonly publicBaseUrl?: string; + readonly routePrefix?: string; } export function webProbeSentinelRegistryRows(spec: HwlabRuntimeLaneSpec): readonly WebProbeSentinelRegistryRow[] { const registry = spec.observability.webProbe?.sentinels; - if (registry !== undefined) return registry.map((item) => ({ id: item.id, enabled: item.enabled, configRef: item.configRef })); + if (registry !== undefined) return registry.map((item) => sentinelRegistryRow(spec, item.id, item.enabled, item.configRef)); const legacy = spec.observability.webProbe?.sentinel; if (legacy === undefined) return []; - return [{ - id: "workbench-dsflash-go-tool-call-10x", - enabled: legacy.enabled, - configRef: `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinel`, - }]; + return [sentinelRegistryRow( + spec, + "workbench-dsflash-go-tool-call-10x", + legacy.enabled, + `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinel`, + )]; +} + +export function effectiveWebProbeSentinelPublicExposure(spec: HwlabRuntimeLaneSpec, sentinelId: string, publicExposure: Record): Record { + const monitorRoot = spec.observability.webProbe?.monitorRoot; + if (monitorRoot === undefined || monitorRoot.sentinelId !== sentinelId) return publicExposure; + if (!monitorRoot.enabled) { + return { + ...publicExposure, + monitorRoot: { + enabled: false, + sentinelId, + caddyManagedBlockOwner: monitorRoot.caddyManagedBlockOwner, + valuesRedacted: true, + }, + }; + } + const caddy = isRecord(publicExposure.caddy) ? publicExposure.caddy : {}; + return { + ...publicExposure, + publicBaseUrl: monitorRoot.publicBaseUrl, + routePrefix: monitorRoot.routePrefix, + caddy: { + ...caddy, + managedBlockOwner: monitorRoot.caddyManagedBlockOwner, + rootOrder: "active", + }, + monitorRoot: { + enabled: true, + sentinelId, + publicBaseUrl: monitorRoot.publicBaseUrl, + routePrefix: monitorRoot.routePrefix, + caddyManagedBlockOwner: monitorRoot.caddyManagedBlockOwner, + valuesRedacted: true, + }, + }; } export function resolveWebProbeSentinel(spec: HwlabRuntimeLaneSpec, sentinelId: string | null | undefined): ResolvedWebProbeSentinel { @@ -87,6 +126,18 @@ function resolveRegistrySentinel(spec: HwlabRuntimeLaneSpec, registry: readonly }; } +function sentinelRegistryRow(spec: HwlabRuntimeLaneSpec, id: string, enabled: boolean, configRef: string): WebProbeSentinelRegistryRow { + const monitorRoot = spec.observability.webProbe?.monitorRoot; + const isMonitorRoot = monitorRoot?.enabled === true && monitorRoot.sentinelId === id; + return { + id, + enabled, + configRef, + monitorRoot: isMonitorRoot, + ...(isMonitorRoot ? { publicBaseUrl: monitorRoot.publicBaseUrl, routePrefix: monitorRoot.routePrefix } : {}), + }; +} + function normalizeSentinelConfigRefs(target: Record, ref: string): Record { const rawRefs = recordAt(target, "configRefs"); const normalized: Record = {}; diff --git a/scripts/src/hwlab-node-web-sentinel-service.ts b/scripts/src/hwlab-node-web-sentinel-service.ts index ac76026c..07900feb 100644 --- a/scripts/src/hwlab-node-web-sentinel-service.ts +++ b/scripts/src/hwlab-node-web-sentinel-service.ts @@ -13,7 +13,7 @@ import { rootPath } from "./config"; import { renderWebProbeSentinelDashboardHtml, webProbeSentinelDashboardAssetResponse } from "./hwlab-node-web-sentinel-dashboard-assets"; import { webProbeSentinelConfigPlan, type WebProbeSentinelConfigPlan } from "./hwlab-node-web-sentinel-config"; import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes"; -import { resolveWebProbeSentinel, readConfigRefTarget as readSentinelConfigRefTarget } from "./hwlab-node-web-sentinel-resolver"; +import { effectiveWebProbeSentinelPublicExposure, resolveWebProbeSentinel, readConfigRefTarget as readSentinelConfigRefTarget } from "./hwlab-node-web-sentinel-resolver"; const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-27-p11-monitor-web-observability-dashboard"; const DASHBOARD_MAX_TEXT_BYTES = 16_000; @@ -93,7 +93,8 @@ export function loadWebProbeSentinelServiceConfig(spec: HwlabRuntimeLaneSpec, op const runtime = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.runtime)); const scenarios = scenarioArrayTarget(readSentinelConfigRefTarget(sentinel.configRefs.scenarios)); const reportViews = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.reportViews)); - const publicExposure = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.publicExposure)); + const rawPublicExposure = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.publicExposure)); + const publicExposure = effectiveWebProbeSentinelPublicExposure(spec, sentinel.id, rawPublicExposure); const cicd = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.cicd)); const stateRoot = options.stateRootOverride ?? stringAt(runtime, "stateRoot"); const yamlSqlitePath = stringAt(runtime, "sqlite.path");