fix(hwlab): configure d601 internal probe and local postgres (#725)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-23 08:41:06 +08:00
committed by GitHub
parent 5b5afbc92b
commit 4e508759b3
5 changed files with 179 additions and 46 deletions
+2 -1
View File
@@ -9,7 +9,7 @@ description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/H
## P0 防分叉原则
- 单一权威入口:先按 issue/CLI 明确的 node + lane 解析 workspace、public origin、namespace、sourceRef 和目标分支。没有明确目标时才读取受控 YAML;不得把 D601、G14、v0.2、v0.3、旧端口或本地 dev server 写成隐藏默认。
- 单一权威入口:先按 issue/CLI 明确的 node + lane 解析 workspace、web-probe origin、public origin、namespace、sourceRef 和目标分支。没有明确目标时才读取受控 YAML;不得把 D601、G14、v0.2、v0.3、旧端口、ClusterIP 或本地 dev server 写成隐藏默认。
- 防分叉原则:Web/Workbench 修复必须先确定唯一 authority、唯一 API 契约和唯一状态投影入口;不得写成“当字段缺失/请求失败/状态不符/超时/刷新后,再改走另一个 endpoint、旧 workspace/conversation 模型、localStorage、trace polling、result polling、GET read-through repair、内部 manager、legacy route 或测试专用后门”。字段缺失、状态滞后或投影不完整时,修 schema、projector、read model 或正式 mutation 源头;代码里只能保留单一路径内的输入规范化、校验和错误暴露,不能用 `A ?? B``if missing then fallback`、兼容分支或双写双读把缺口遮住。
- 禁止状态竞争行为:不得在代码、API、reducer、read model、fake-server 或测试中保留两个事实源,再用“覆盖、压过、override、优先级、优先采用、以 A 为准否则 B、A 赢过 B”等仲裁规则决定展示状态;这就是条件分叉和竞争投影。必须先定义唯一投影对象、唯一归属字段和唯一生成时机,让 UI/API 只消费该投影对象的单一状态;其他原始字段只能作为审计输入、诊断字段或待收敛数据,不能参与展示状态仲裁。
- 严禁读侧推理:REST GET、SSE consumer、compat wrapper、CLI renderer、Web reducer/selectors、Trace renderer、fake-server 和测试断言都不能从 trace tail、message text、tool event、result cache、session summary、list row、workspace snapshot 或 localStorage 推断 turn/session/message 的 lifecycle、terminal、final response 或 running 状态。读侧只能读取唯一投影对象已经写好的字段;字段缺失、投影滞后或多字段矛盾时必须暴露 diagnostic/blocker 并修 projection writer/finalizer/schema,不得用 `result?.status ?? trace?.status`、最后一条 event `completed`、message fallback、session fallback、elapsed timeout 或 UI heuristic 补造事实。
@@ -97,6 +97,7 @@ bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx
约束:
- `web-probe script` 不运行默认探针,必须通过 stdin heredoc 或 `--script-file <path>` 提供脚本;只需要 repo-owned 标准 DOM probe 时使用 `web-probe run`
- `web-probe run|script|observe start` 的默认 URL 和 browser proxy mode 必须来自 `config/hwlab-node-lanes.yaml``webProbe`;需要排除公网/FRP/跨国 proxy 抖动时,在 YAML 里把目标 node/lane 的 `webProbe.defaultOrigin` 配成内部 Service ClusterIP origin,不要在命令行长期手写 `--url` 或裸 Playwright。
- `web-probe observe start` 默认是被动观测:记录 DOM 摘要、自然页面 request/response/requestfailed、截图和 performance 样本,不主动 fetch Workbench API、不切换 control session、不拦截路由、不调用 repair helper。长程 Workbench 观测必须保留 control/observer 双页面模型:control 页面执行显式 commandobserver 页面只同步到同一 session URL 后被动采样,并按默认 180000ms 周期整页刷新同一 session 来模拟用户往返;周期刷新只作用于 observer,不得改变 control active session 或作为通过条件。两页的 `pageRole``pageId``sampleGroupSeq` 必须进入样本和 analyzer 报表。任何 `newSession``selectProvider``sendPrompt``goto``screenshot``mark``stop` 都必须通过 `observe command` 显式下发,并进入 `control.jsonl`;长 prompt 必须优先用 `sendPrompt --text-stdin`,不要为了绕开 shell quoting 退回裸 Playwright 或临时脚本。
- `web-probe observe` 的 issue evidence 优先记录 observer id、stateDir、report JSON/Markdown SHA、samples/control/network/artifact 计数、routeSessionId、activeSessionId、prompt hash/textBytes、traceId、AgentRun runId/commandId、最终 status 和必要摘要;不要把 prompt 原文、assistant 大段正文、完整 stdout/stderr 或 provider payload 粘贴到 issue。
- 多轮 Workbench 采样必须证明同一个 `sessionId` 连续承载所有轮次;每轮至少记录 prompt hash、traceId、终态、最终回答摘要和性能/产物表。若 Web UI 投影卡住但 Code Agent/AgentRun result 已 terminal,应同时登记“执行终态”和“Workbench 投影未收敛”,不得用 `goto`、reload、切 session 或 result polling 把 UI 失败伪装成通过。
+25 -21
View File
@@ -142,11 +142,36 @@ lanes:
renderDir: runtime-v03
runtimeStore:
postgres:
mode: local-k3s
secretName: hwlab-v03-postgres
statefulSet: hwlab-v03-postgres
serviceName: hwlab-v03-postgres
adminUser: hwlab_v03
cloudApi:
secretName: hwlab-cloud-api-v03-db
secretKey: database-url
database: hwlab_v03
role: hwlab_v03
openfga:
secretName: hwlab-v03-openfga
secretKey: datastore-uri
authnKey: authn-preshared-key
postgresPasswordKey: postgres-password
database: hwlab_openfga
role: hwlab_openfga
poolMax: 16
connectionTimeoutMs: 5000
queryRetryMaxAttempts: 5
queryRetryInitialDelayMs: 250
queryRetryMaxDelayMs: 5000
webProbe:
browserProxyMode: direct
defaultOrigin:
mode: k8s-service-cluster-ip
serviceName: hwlab-cloud-web
namespace: hwlab-v03
port: 8080
scheme: http
tektonDir: tekton-v03
argoApplicationFile: application-v03.yaml
registryPrefix: 127.0.0.1:5000/hwlab
@@ -362,27 +387,6 @@ lanes:
email: ops@pikapython.com
tls: auto
responseHeaderTimeoutSeconds: 600
externalPostgres:
provider: PK01
configRef: config/platform-db/postgres-pk01.yaml
serviceName: pk01-platform-postgres
endpointAddress: 82.156.23.220
port: 5432
sslmode: require
database: hwlab_d601_v03
cloudApi:
secretName: hwlab-cloud-api-v03-db
secretKey: database-url
sourceRef: hwlab/d601-v03-cloud-api-db.env
envKey: DATABASE_URL
role: hwlab_d601_v03_app
openfga:
secretName: hwlab-v03-openfga
secretKey: datastore-uri
sourceRef: hwlab/d601-v03-openfga-db.env
envKey: DATASTORE_URI
authnKey: authn-preshared-key
role: hwlab_d601_v03_app
networkProfiles:
node-ci-egress:
+4 -3
View File
@@ -73,9 +73,9 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
description: "Run target node/lane HWLAB Cloud Web DOM probes with Web login credentials resolved from YAML-declared bootstrap admin sourceRef and injected only as one-shot stdin/env.",
examples: [
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --wait-messages-ms 1000",
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --url https://hwlab.pikapython.com --fresh-session --message 'ping'",
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 --browser-proxy-mode direct --script-file .state/probes/workbench.mjs",
"bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000 --browser-proxy-mode direct",
"bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 --fresh-session --message 'ping'",
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 --script-file .state/probes/workbench.mjs",
"bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type newSession",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectProvider --provider codex-api",
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'",
@@ -90,6 +90,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
observe: "Start, inspect, control, stop, collect, and analyze a pure-client long-running Workbench observer on the target host. The observer runs a control page plus a passive observer page in a shared-auth browser context, receives commands through stateDir/commands files, writes JSONL artifacts, and does not expose any inbound service API.",
},
notes: [
"The default probe URL and browser proxy mode come from config/hwlab-node-lanes.yaml webProbe; pass --url only when intentionally overriding the YAML-selected origin.",
"Prefer --script-file for reusable probes; stdin heredocs remain supported for one-off probes.",
"Issue-ready evidence is available under issueEvidence and summary.issueEvidence; full script report is persisted under probe.reportPath with a SHA-256 fingerprint.",
"observe sampling is passive by default: it records DOM summaries and natural page request/response/requestfailed events with observerInitiated=false; it does not actively fetch Workbench APIs, reload, switch sessions, route/intercept, or call repair helpers.",
+44 -21
View File
@@ -461,6 +461,10 @@ function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, args: stri
};
}
function nodeRuntimeLocalPostgresExpectedAbsent(spec: HwlabRuntimeLaneSpec): boolean {
return spec.externalPostgres !== undefined || spec.runtimeStore?.postgres?.mode === "platform-service";
}
function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
return {
configPath: hwlabRuntimeLaneConfigPath(),
@@ -508,6 +512,10 @@ function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string, unknown
webUrl: spec.publicWebUrl,
apiUrl: spec.publicApiUrl,
},
webProbe: spec.webProbe === undefined ? null : {
browserProxyMode: spec.webProbe.browserProxyMode ?? null,
defaultOrigin: spec.webProbe.defaultOrigin ?? null,
},
bootstrapAdmin: spec.bootstrapAdmin === undefined ? null : {
username: spec.bootstrapAdmin.username,
displayName: spec.bootstrapAdmin.displayName,
@@ -575,7 +583,7 @@ function nodeRuntimeControlPlanePlan(scoped: ReturnType<typeof parseNodeScopedDe
externalPostgresDeclared: scoped.spec.externalPostgres !== undefined,
secretValuesPrinted: false,
runtimeNamespace: scoped.spec.runtimeNamespace,
localPostgresExpectedAbsent: scoped.spec.externalPostgres !== undefined,
localPostgresExpectedAbsent: nodeRuntimeLocalPostgresExpectedAbsent(scoped.spec),
publicExposureDeclared: scoped.spec.publicExposure !== null,
},
next: {
@@ -3013,7 +3021,7 @@ function withNodeRuntimeControlPlaneStatusRendered(result: Record<string, unknow
[
["pipeline", pipelineRun.ready === true ? "ok" : pipelineRun.exists === true ? webObserveText(pipelineRun.status) : "missing", webObserveShort(webObserveText(pipelineRun.reason ?? pipelineRun.message), 80)],
["argo", argo.ready === true ? "ok" : "failed", `${webObserveText(argo.syncStatus)}/${webObserveText(argo.health)} rev=${shortValue(argo.syncRevision)}`],
["runtime", runtime.ready === true ? "ok" : "failed", `workloads=${webObserveText(runtime.workloadReady)} pg=${webObserveText(runtime.externalPostgresReady)}`],
["runtime", runtime.ready === true ? "ok" : "failed", `workloads=${webObserveText(runtime.workloadReady)} pg=${webObserveText(runtime.externalPostgresReady ?? runtime.localPostgresReady)}`],
["public", publicProbe.ready === true ? "ok" : "failed", webObserveShort(webObserveText(diagnostic.kind ?? diagnostic.message), 80)],
["git-mirror", gitMirror.ready === true ? "ok" : "failed", `pending=${webObserveText(gitMirror.pendingFlush)} inSync=${webObserveText(gitMirror.githubInSync)}`],
],
@@ -3831,7 +3839,9 @@ function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNodeScoped
const gitMirrorCompact = compactNodeRuntimeGitMirrorStatus(gitMirror);
const controlPlaneReady = serviceAccount.exitCode === 0 && pipeline.exitCode === 0 && argo.exitCode === 0;
const workloadsReady = workloadReadiness.length > 0 && workloadReadiness.every((item) => item.ready);
const runtimeReady = namespaceExists && localPostgresObjects.length === 0 && workloadsReady && (spec.externalPostgres === undefined || (bridge.ready && secrets.ready));
const localPostgresExpectedAbsent = nodeRuntimeLocalPostgresExpectedAbsent(spec);
const localPostgresReady = localPostgresExpectedAbsent ? localPostgresObjects.length === 0 : localPostgresObjects.length > 0;
const runtimeReady = namespaceExists && localPostgresReady && workloadsReady && (spec.externalPostgres === undefined || (bridge.ready && secrets.ready));
const argoReady = argo.exitCode === 0 && repoURL === spec.argoRepoUrl && targetRevision === spec.gitopsBranch && path === spec.runtimePath && syncStatus === "Synced" && health === "Healthy";
const pipelineRunReady = pipelineRunProbe !== null && pipelineRunProbe.status === "True";
const pipelineRunDegradedReason = typeof pipelineRunDiagnostics?.degradedReason === "string"
@@ -3877,6 +3887,8 @@ function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNodeScoped
namespaceExists,
localPostgresObjects,
localPostgresAbsent: namespaceExists && localPostgresObjects.length === 0,
localPostgresExpectedAbsent,
localPostgresReady,
workloadNames,
workloadCount: workloadNames.length,
workloadReadiness,
@@ -4131,6 +4143,8 @@ function summarizeNodeRuntimeControlPlaneStatus(status: Record<string, unknown>,
workloadCount,
workloadReady: `${readyWorkloads}/${workloadReadiness.length}`,
localPostgresAbsent: runtime.localPostgresAbsent === true,
localPostgresExpectedAbsent: runtime.localPostgresExpectedAbsent === true,
localPostgresReady: runtime.localPostgresReady === true,
externalPostgresReady: runtime.externalPostgresBridge === undefined && runtime.externalPostgresSecrets === undefined
? null
: record(runtime.externalPostgresBridge).ready === true && record(runtime.externalPostgresSecrets).ready === true,
@@ -6793,10 +6807,10 @@ function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
action: "script",
node,
lane,
url: optionValue(args, "--url") ?? spec.publicWebUrl,
url: optionValue(args, "--url") ?? nodeWebProbeDefaultUrl(spec),
timeoutMs,
viewport: optionValue(args, "--viewport") ?? "1440x900",
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode")),
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode") ?? spec.webProbe?.browserProxyMode),
commandTimeoutSeconds,
scriptText,
scriptSource: {
@@ -6853,7 +6867,7 @@ function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions {
action: "run",
node,
lane,
url: optionValue(args, "--url") ?? spec.publicWebUrl,
url: optionValue(args, "--url") ?? nodeWebProbeDefaultUrl(spec),
timeoutMs,
waitAfterSubmitMs,
waitMessagesMs,
@@ -6952,10 +6966,10 @@ function parseNodeWebProbeObserveOptions(
id: observeId ?? jobId,
node,
lane,
url: optionValue(args, "--url") ?? spec.publicWebUrl,
url: optionValue(args, "--url") ?? (observeActionRaw === "start" ? nodeWebProbeDefaultUrl(spec) : indexed?.url ?? spec.publicWebUrl),
targetPath: optionValue(args, "--target-path") ?? "/workbench",
viewport: optionValue(args, "--viewport") ?? "1440x900",
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode")),
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode") ?? spec.webProbe?.browserProxyMode),
sampleIntervalMs: positiveIntegerOption(args, "--sample-interval-ms", 5000, 600000),
screenshotIntervalMs: positiveIntegerOption(args, "--screenshot-interval-ms", 300000, 86_400_000),
observerRefreshIntervalMs: positiveIntegerOption(args, "--observer-refresh-interval-ms", 180000, 86_400_000),
@@ -7001,6 +7015,13 @@ function parseWebProbeBrowserProxyMode(value: string | undefined): WebProbeBrows
throw new Error(`web-probe --browser-proxy-mode must be auto or direct, got ${value}`);
}
function nodeWebProbeDefaultUrl(spec: HwlabRuntimeLaneSpec): string {
const origin = spec.webProbe?.defaultOrigin;
if (origin === undefined || origin.mode === "public") return origin?.baseUrl ?? spec.publicWebUrl;
const clusterIp = resolveKubernetesServiceClusterIp(spec, origin.namespace, origin.serviceName, new Map());
return `${origin.scheme}://${clusterIp}:${origin.port}`;
}
function nodeWebProbeAutoCommandTimeoutSeconds(input: {
timeoutMs: number;
waitAfterSubmitMs: number;
@@ -10132,16 +10153,18 @@ function runtimeSecretSpec(input: { node: string; lane: string }): RuntimeSecret
const namespace = `hwlab-${input.lane}`;
const runtimeLaneSpec = isHwlabRuntimeLane(input.lane) ? hwlabRuntimeLaneSpecForNode(input.lane, input.node) : undefined;
const externalPostgres = runtimeLaneSpec?.externalPostgres;
const postgresStore = runtimeLaneSpec?.runtimeStore?.postgres;
const bootstrapAdmin = runtimeLaneSpec?.bootstrapAdmin;
const platformDb = externalPostgres !== undefined || /^v0*[3-9]\d*$/.test(input.lane);
const platformPostgresService = externalPostgres?.serviceName ?? "g14-platform-postgres";
const legacyPostgresHost = `${namespace}-postgres.${namespace}.svc.cluster.local`;
const platformDb = externalPostgres !== undefined || postgresStore?.mode === "platform-service" || (postgresStore?.mode === undefined && /^v0*[3-9]\d*$/.test(input.lane));
const localPostgresService = postgresStore?.serviceName ?? `${namespace}-postgres`;
const platformPostgresService = externalPostgres?.serviceName ?? postgresStore?.serviceName ?? "g14-platform-postgres";
const legacyPostgresHost = `${localPostgresService}.${namespace}.svc.cluster.local`;
const platformPostgresHost = `${platformPostgresService}.${namespace}.svc.cluster.local`;
const platformPostgresEndpointSlice = `${platformPostgresService}-host`;
const openFgaDbName = externalPostgres?.database ?? (platformDb ? `openfga_${input.lane}` : "hwlab_openfga");
const openFgaDbUser = externalPostgres?.openfga.role ?? (platformDb ? `openfga_${input.lane}_app` : "hwlab_openfga");
const cloudApiDbName = externalPostgres?.database ?? `hwlab_${input.lane}`;
const cloudApiDbUser = externalPostgres?.cloudApi.role ?? (platformDb ? `hwlab_${input.lane}_app` : `hwlab_${input.lane}`);
const openFgaDbName = externalPostgres?.database ?? postgresStore?.openfga?.database ?? (platformDb ? `openfga_${input.lane}` : "hwlab_openfga");
const openFgaDbUser = externalPostgres?.openfga.role ?? postgresStore?.openfga?.role ?? (platformDb ? `openfga_${input.lane}_app` : "hwlab_openfga");
const cloudApiDbName = externalPostgres?.database ?? postgresStore?.cloudApi?.database ?? `hwlab_${input.lane}`;
const cloudApiDbUser = externalPostgres?.cloudApi.role ?? postgresStore?.cloudApi?.role ?? (platformDb ? `hwlab_${input.lane}_app` : `hwlab_${input.lane}`);
return {
node: input.node,
lane: input.lane,
@@ -10152,10 +10175,10 @@ function runtimeSecretSpec(input: { node: string; lane: string }): RuntimeSecret
platformPostgresService,
platformPostgresEndpointAddress: externalPostgres?.endpointAddress,
platformPostgresEndpointSlice,
postgresSecret: `${namespace}-postgres`,
postgresStatefulSet: `${namespace}-postgres`,
postgresAdminUser: `hwlab_${input.lane}`,
openFgaSecret: externalPostgres?.openfga.secretName ?? `${namespace}-openfga`,
postgresSecret: postgresStore?.secretName ?? `${namespace}-postgres`,
postgresStatefulSet: postgresStore?.statefulSet ?? postgresStore?.secretName ?? `${namespace}-postgres`,
postgresAdminUser: postgresStore?.adminUser ?? `hwlab_${input.lane}`,
openFgaSecret: externalPostgres?.openfga.secretName ?? postgresStore?.openfga?.secretName ?? `${namespace}-openfga`,
openFgaDbName,
openFgaDbUser,
openFgaDbHost: platformDb ? platformPostgresHost : legacyPostgresHost,
@@ -10169,8 +10192,8 @@ function runtimeSecretSpec(input: { node: string; lane: string }): RuntimeSecret
...(bootstrapAdmin?.passwordHashTransform === undefined ? {} : { bootstrapAdminPasswordHashTransform: bootstrapAdmin.passwordHashTransform }),
bootstrapAdminSourceNamespace: BOOTSTRAP_ADMIN_SOURCE_NAMESPACE,
bootstrapAdminSourceSecret: BOOTSTRAP_ADMIN_SOURCE_SECRET,
cloudApiDbSecret: externalPostgres?.cloudApi.secretName ?? `hwlab-cloud-api-${input.lane}-db`,
cloudApiDbKey: externalPostgres?.cloudApi.secretKey ?? CLOUD_API_DB_KEY,
cloudApiDbSecret: externalPostgres?.cloudApi.secretName ?? postgresStore?.cloudApi?.secretName ?? `hwlab-cloud-api-${input.lane}-db`,
cloudApiDbKey: externalPostgres?.cloudApi.secretKey ?? postgresStore?.cloudApi?.secretKey ?? CLOUD_API_DB_KEY,
cloudApiDbName,
cloudApiDbUser,
cloudApiDbHost: platformDb ? platformPostgresHost : legacyPostgresHost,
+104
View File
@@ -80,6 +80,13 @@ export interface HwlabRuntimeExternalPostgresSpec {
}
export interface HwlabRuntimePostgresStoreSpec {
readonly mode?: "local-k3s" | "platform-service";
readonly secretName?: string;
readonly statefulSet?: string;
readonly serviceName?: string;
readonly adminUser?: string;
readonly cloudApi?: HwlabRuntimePostgresStoreComponentSpec;
readonly openfga?: HwlabRuntimePostgresStoreComponentSpec;
readonly poolMax: number;
readonly connectionTimeoutMs?: number;
readonly queryRetryMaxAttempts?: number;
@@ -87,10 +94,39 @@ export interface HwlabRuntimePostgresStoreSpec {
readonly queryRetryMaxDelayMs?: number;
}
export interface HwlabRuntimePostgresStoreComponentSpec {
readonly secretName: string;
readonly secretKey: string;
readonly database: string;
readonly role: string;
readonly authnKey?: string;
readonly postgresPasswordKey?: string;
}
export interface HwlabRuntimeStoreSpec {
readonly postgres?: HwlabRuntimePostgresStoreSpec;
}
export interface HwlabRuntimeWebProbeServiceOriginSpec {
readonly mode: "k8s-service-cluster-ip";
readonly serviceName: string;
readonly namespace: string;
readonly port: number;
readonly scheme: "http" | "https";
}
export interface HwlabRuntimeWebProbePublicOriginSpec {
readonly mode: "public";
readonly baseUrl?: string;
}
export type HwlabRuntimeWebProbeOriginSpec = HwlabRuntimeWebProbeServiceOriginSpec | HwlabRuntimeWebProbePublicOriginSpec;
export interface HwlabRuntimeWebProbeSpec {
readonly browserProxyMode?: "auto" | "direct";
readonly defaultOrigin?: HwlabRuntimeWebProbeOriginSpec;
}
export interface HwlabRuntimeBuildkitSpec {
readonly sidecarImage: string;
}
@@ -230,6 +266,7 @@ export interface HwlabRuntimeLaneSpec {
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
readonly runtimeStore?: HwlabRuntimeStoreSpec;
readonly webProbe?: HwlabRuntimeWebProbeSpec;
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
readonly observability: HwlabRuntimeObservabilitySpec;
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
@@ -272,6 +309,7 @@ interface HwlabLaneConfig {
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
readonly runtimeStore?: HwlabRuntimeStoreSpec;
readonly webProbe?: HwlabRuntimeWebProbeSpec;
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
readonly observability: HwlabRuntimeObservabilitySpec;
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
@@ -490,6 +528,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
bootstrapAdmin: bootstrapAdminConfig(raw.bootstrapAdmin, `lanes.${id}.bootstrapAdmin`),
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
runtimeStore: runtimeStoreConfig(raw.runtimeStore, `lanes.${id}.runtimeStore`),
webProbe: webProbeConfig(raw.webProbe, `lanes.${id}.webProbe`),
publicExposure: publicExposureConfig(raw.publicExposure, `lanes.${id}.publicExposure`),
observability: observabilityConfig(raw.observability, `lanes.${id}.observability`),
runtimeImageRewrites: runtimeImageRewritesConfig(raw.runtimeImageRewrites, `lanes.${id}.runtimeImageRewrites`),
@@ -510,6 +549,7 @@ function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<
bootstrapAdmin: mergeOptionalRecord(baseRaw.bootstrapAdmin, targetRaw.bootstrapAdmin),
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
runtimeStore: mergeOptionalRecord(baseRaw.runtimeStore, targetRaw.runtimeStore),
webProbe: mergeOptionalRecord(baseRaw.webProbe, targetRaw.webProbe),
publicExposure: mergeOptionalRecord(baseRaw.publicExposure, targetRaw.publicExposure),
observability: mergeOptionalRecord(baseRaw.observability, targetRaw.observability),
};
@@ -595,6 +635,13 @@ function runtimeStoreConfig(value: unknown, path: string): HwlabRuntimeStoreSpec
? {}
: {
postgres: {
...(postgres.mode === undefined ? {} : { mode: postgresModeField(postgres, `${path}.postgres`) }),
...(postgres.secretName === undefined ? {} : { secretName: stringField(postgres, "secretName", `${path}.postgres`) }),
...(postgres.statefulSet === undefined ? {} : { statefulSet: stringField(postgres, "statefulSet", `${path}.postgres`) }),
...(postgres.serviceName === undefined ? {} : { serviceName: stringField(postgres, "serviceName", `${path}.postgres`) }),
...(postgres.adminUser === undefined ? {} : { adminUser: stringField(postgres, "adminUser", `${path}.postgres`) }),
...(postgres.cloudApi === undefined ? {} : { cloudApi: runtimeStorePostgresComponentConfig(postgres.cloudApi, `${path}.postgres.cloudApi`) }),
...(postgres.openfga === undefined ? {} : { openfga: runtimeStorePostgresComponentConfig(postgres.openfga, `${path}.postgres.openfga`) }),
poolMax: numberField(postgres, "poolMax", `${path}.postgres`),
...(postgres.connectionTimeoutMs === undefined
? {}
@@ -613,6 +660,62 @@ function runtimeStoreConfig(value: unknown, path: string): HwlabRuntimeStoreSpec
};
}
function postgresModeField(raw: Record<string, unknown>, path: string): "local-k3s" | "platform-service" {
const mode = stringField(raw, "mode", path);
if (mode !== "local-k3s" && mode !== "platform-service") throw new Error(`${path}.mode must be local-k3s or platform-service`);
return mode;
}
function runtimeStorePostgresComponentConfig(value: unknown, path: string): HwlabRuntimePostgresStoreComponentSpec {
const raw = asRecord(value, path);
return {
secretName: stringField(raw, "secretName", path),
secretKey: secretKeyField(raw, "secretKey", path),
database: stringField(raw, "database", path),
role: stringField(raw, "role", path),
authnKey: optionalStringField(raw, "authnKey", path),
postgresPasswordKey: optionalStringField(raw, "postgresPasswordKey", path),
};
}
function webProbeConfig(value: unknown, path: string): HwlabRuntimeWebProbeSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const rawBrowserProxyMode = optionalStringField(raw, "browserProxyMode", path);
let browserProxyMode: "auto" | "direct" | undefined;
if (rawBrowserProxyMode !== undefined) {
if (rawBrowserProxyMode !== "auto" && rawBrowserProxyMode !== "direct") {
throw new Error(`${path}.browserProxyMode must be auto or direct`);
}
browserProxyMode = rawBrowserProxyMode;
}
return {
...(browserProxyMode === undefined ? {} : { browserProxyMode }),
...(raw.defaultOrigin === undefined ? {} : { defaultOrigin: webProbeOriginConfig(raw.defaultOrigin, `${path}.defaultOrigin`) }),
};
}
function webProbeOriginConfig(value: unknown, path: string): HwlabRuntimeWebProbeOriginSpec {
const raw = asRecord(value, path);
const mode = stringField(raw, "mode", path);
if (mode === "public") {
return {
mode,
...(raw.baseUrl === undefined ? {} : { baseUrl: stringField(raw, "baseUrl", path).replace(/\/+$/u, "") }),
};
}
if (mode !== "k8s-service-cluster-ip") throw new Error(`${path}.mode must be public or k8s-service-cluster-ip`);
const scheme = stringField(raw, "scheme", path);
if (scheme !== "http" && scheme !== "https") throw new Error(`${path}.scheme must be http or https`);
return {
mode,
serviceName: stringField(raw, "serviceName", path),
namespace: stringField(raw, "namespace", path),
port: numberField(raw, "port", path),
scheme,
};
}
function publicExposureProxyConfig(value: unknown, path: string): HwlabRuntimePublicExposureFrpcProxySpec {
const raw = asRecord(value, path);
return {
@@ -887,6 +990,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
...(config.bootstrapAdmin === undefined ? {} : { bootstrapAdmin: config.bootstrapAdmin }),
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
...(config.runtimeStore === undefined ? {} : { runtimeStore: config.runtimeStore }),
...(config.webProbe === undefined ? {} : { webProbe: config.webProbe }),
publicExposure: config.publicExposure,
observability: config.observability,
runtimeImageRewrites: config.runtimeImageRewrites,