feat: 统一 L1 公网调试入口配置

This commit is contained in:
Codex
2026-07-17 16:20:50 +02:00
parent 60fdf72489
commit 9d3f53f7b5
7 changed files with 178 additions and 1 deletions
+5
View File
@@ -47,6 +47,11 @@ description: >-
- Web 使用 `$unidesk-webdev``web-probe` custom/local 入口访问 native Web。
- 微服务项目只启动当前微服务的前端、API、Worker及必要依赖。
- 前端、API 和 Worker可以分别启动、查看日志、重启和停止。
- 对用户披露的 native API/Web 调试入口必须是 owning YAML 声明的公网 `IP:port`
- `0.0.0.0` 只表示进程 bind
- `127.0.0.1` 只用于本机 probe 或进程间代理;
- bind/probe 地址不得作为 L1 用户入口返回;
- 公网 IP、API port 和 Web port 缺失时必须明确失败,不得回退到 localhost 或代码默认值。
- 适合前后端联调、异步作业、Workflow、网络接口和页面交互的快速开发。
## L2 Development
+16
View File
@@ -111,6 +111,22 @@ lanes:
browserCommand: npx playwright install chromium
stateDir: /tmp/hwlab-v03-source-workspace-bootstrap
timeoutSeconds: 1800
nativeDevelopment:
workbench:
publicHostRef: config/unidesk-host-k8s.yaml#runtime.publicHost
stateDir: .state/workbench-native/services
api:
bindHost: 0.0.0.0
probeHost: 127.0.0.1
port: 6677
worker:
bindHost: 127.0.0.1
probeHost: 127.0.0.1
healthPort: 6678
web:
bindHost: 0.0.0.0
probeHost: 127.0.0.1
port: 5173
sourceAuthority:
extends: templates.hwlabV03.sourceAuthorityGiteaSnapshot
giteaMirrorRepoKey: hwlab-nc01-v03
@@ -0,0 +1,16 @@
import { expect, test } from "bun:test";
import { hwlabNativeDevelopmentEnvironment, hwlabNativeDevelopmentHelp } from "./hwlab-native-development";
import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes";
test("L1 Workbench exposure uses YAML public IP and separate bind/probe addresses", () => {
const native = hwlabRuntimeLaneSpecForNode("v03", "NC01").nativeDevelopment?.workbench;
expect(native).toBeDefined();
const env = hwlabNativeDevelopmentEnvironment(native!);
expect(env.WORKBENCH_NATIVE_SERVICE_STATE_DIR).toBe(native!.stateDir);
expect(env.WORKBENCH_API_BIND_HOST).toBe(native!.api.bindHost);
expect(env.WORKBENCH_API_PROBE_HOST).toBe(native!.api.probeHost);
expect(env.WORKBENCH_API_PUBLIC_HOST).toBe(native!.publicHost);
expect(env.WORKBENCH_WEB_PUBLIC_HOST).toBe(native!.publicHost);
expect(hwlabNativeDevelopmentHelp()).toMatchObject({ ok: true, command: "hwlab nodes native-development workbench" });
});
+94
View File
@@ -0,0 +1,94 @@
import { runCommand } from "./command";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
type Service = "api" | "worker" | "web";
type Action = "start" | "stop" | "restart" | "status" | "logs";
export function hwlabNativeDevelopmentHelp(): Record<string, unknown> {
return {
ok: true,
command: "hwlab nodes native-development workbench",
usage: [
"bun scripts/cli.ts hwlab nodes native-development workbench api|worker|web start|stop|restart|status|logs --node <node> --lane <lane>",
],
contract: "L1 API/Web 用户入口只输出 owning YAML 解析出的公网 IP 和 portbind/probe 地址不作为用户入口。",
configPath: hwlabRuntimeLaneConfigPath(),
};
}
export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string, unknown> {
if (args.length === 0 || args.includes("--help") || args.includes("-h")) return hwlabNativeDevelopmentHelp();
const [application, serviceRaw, actionRaw] = args;
if (application !== "workbench") throw new Error("native-development currently supports workbench");
const service = parseService(serviceRaw);
const action = parseAction(actionRaw);
const node = requiredOption(args, "--node");
const lane = requiredOption(args, "--lane");
if (!isHwlabRuntimeLane(lane)) throw new Error(`--lane must be declared in ${hwlabRuntimeLaneConfigPath()}`);
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
const native = spec.nativeDevelopment?.workbench;
if (native === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench is required`);
const env = hwlabNativeDevelopmentEnvironment(native);
const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", "workbench", "service", service, action];
const result = runCommand(command, spec.workspace, { timeoutMs: 15_000, env: { ...process.env, ...env } });
const payload = parsePayload(result.stdout);
const publicEndpoint = service === "api"
? { ip: native.publicHost, port: native.api.port, url: `http://${native.publicHost}:${native.api.port}` }
: service === "web"
? { ip: native.publicHost, port: native.web.port, url: `http://${native.publicHost}:${native.web.port}/workbench` }
: null;
return {
ok: result.exitCode === 0 && payload.ok !== false,
command: `hwlab nodes native-development workbench ${service} ${action}`,
node,
lane,
service,
action,
configSource: `${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench`,
publicHostSource: native.publicHostRef,
userEndpoint: publicEndpoint,
serviceResult: payload,
...(result.exitCode === 0 ? {} : { error: { code: "hwlab_native_service_failed", exitCode: result.exitCode, stderr: result.stderr.trim().slice(-2000) } }),
};
}
export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDevelopmentSpec["workbench"]): NodeJS.ProcessEnv {
return {
WORKBENCH_NATIVE_SERVICE_STATE_DIR: native.stateDir,
WORKBENCH_API_BIND_HOST: native.api.bindHost,
WORKBENCH_API_PROBE_HOST: native.api.probeHost,
WORKBENCH_API_PUBLIC_HOST: native.publicHost,
WORKBENCH_API_PORT: String(native.api.port),
WORKBENCH_WORKER_BIND_HOST: native.worker.bindHost,
WORKBENCH_WORKER_PROBE_HOST: native.worker.probeHost,
WORKBENCH_WORKER_HEALTH_PORT: String(native.worker.healthPort),
WORKBENCH_WEB_BIND_HOST: native.web.bindHost,
WORKBENCH_WEB_PROBE_HOST: native.web.probeHost,
WORKBENCH_WEB_PUBLIC_HOST: native.publicHost,
WORKBENCH_WEB_PORT: String(native.web.port),
WORKBENCH_NATIVE_API_URL: `http://${native.api.probeHost}:${native.api.port}`,
};
}
function parsePayload(stdout: string): Record<string, unknown> {
const parsed = JSON.parse(stdout) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("HWLAB Workbench CLI returned invalid JSON");
return parsed as Record<string, unknown>;
}
function parseService(value: string | undefined): Service {
if (value === "api" || value === "worker" || value === "web") return value;
throw new Error("native-development workbench service must be api, worker, or web");
}
function parseAction(value: string | undefined): Action {
if (value === "start" || value === "stop" || value === "restart" || value === "status" || value === "logs") return value;
throw new Error("native-development workbench action must be start, stop, restart, status, or logs");
}
function requiredOption(args: string[], option: string): string {
const index = args.indexOf(option);
const value = index < 0 ? undefined : args[index + 1];
if (!value || value.startsWith("--")) throw new Error(`${option} requires a value`);
return value;
}
+2
View File
@@ -49,6 +49,7 @@ export function hwlabNodeHelp(scope: "legacy-cicd" | "platform-maintenance" | nu
"bun scripts/cli.ts hwlab nodes hwpod-preinstall plan --node <node> --lane <lane> --dry-run",
"bun scripts/cli.ts hwlab nodes hwpod-node plan --node G14-WSL --lane v03",
"bun scripts/cli.ts hwlab nodes fake-model-provider plan --node D518 --lane v03 --provider fake-echo",
"bun scripts/cli.ts hwlab nodes native-development workbench web status --node <node> --lane <lane>",
"bun scripts/cli.ts hwlab nodes secret status --node <node> --lane <lane> --name <secret>",
"bun scripts/cli.ts hwlab nodes test-accounts status --node <node> --lane <lane>",
"bun scripts/cli.ts hwlab nodes observability performance-summary --node <node> --lane <lane>",
@@ -62,6 +63,7 @@ export function hwlabNodeHelp(scope: "legacy-cicd" | "platform-maintenance" | nu
"hwpod-preinstall": "Render YAML-first HWPOD preinstall configRefs, runtime mount targets, PM MDTODO source, and gateway profile status.",
"hwpod-node": "通过 YAML 和 trans 部署、查询 Windows 原生 Python HWPOD 节点。",
"fake-model-provider": "Materialize and operate YAML-declared fake Responses model providers for HWLAB/AgentRun sentinel checks.",
"native-development": "按 owning YAML 独立管理 L1 API、Worker 与 Web,并为 API/Web 披露公网 IP 和 port。",
secret: "Inspect YAML-declared runtime Secrets without printing secret values; writes are documented only by platform-maintenance help.",
"test-accounts": "Prepare YAML-declared HWLAB admin/test account API keys with redacted sourceRef/fingerprint output.",
observability: "Read runtime metrics and authenticated Web Performance summaries.",
+40
View File
@@ -4,10 +4,12 @@
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
// Responsibility: YAML source-of-truth parsing for HWLAB node/lane Workbench observability.
import { readFileSync } from "node:fs";
import { isIP } from "node:net";
import { rootPath } from "./config";
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH } from "./hwlab-node-control-plane-model";
import { parseWebProbeConsoleVerificationProfiles, type HwlabRuntimeWebProbeConsoleVerificationProfileSpec } from "./hwlab-node-web-probe-console-profile";
import { materializeYamlComposition } from "./yaml-composition";
import { resolveConfigRefString } from "./ops/config-refs";
export type HwlabRuntimeLane = string;
@@ -660,6 +662,17 @@ export interface HwlabRuntimeSourceWorkspaceHostDependenciesSpec {
};
}
export interface HwlabRuntimeNativeDevelopmentSpec {
readonly workbench: {
readonly publicHostRef: string;
readonly publicHost: string;
readonly stateDir: string;
readonly api: { readonly bindHost: string; readonly probeHost: string; readonly port: number };
readonly worker: { readonly bindHost: string; readonly probeHost: string; readonly healthPort: number };
readonly web: { readonly bindHost: string; readonly probeHost: string; readonly port: number };
};
}
export interface HwlabRuntimePipelineProvenanceSpec {
readonly configRef: string;
readonly renderer: "hwlab-runtime-lane";
@@ -711,6 +724,7 @@ export interface HwlabRuntimeLaneSpec {
readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec;
readonly codeAgentRuntime?: HwlabRuntimeCodeAgentRuntimeSpec;
readonly sourceWorkspace?: HwlabRuntimeSourceWorkspaceSpec;
readonly nativeDevelopment?: HwlabRuntimeNativeDevelopmentSpec;
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
readonly runtimeStore?: HwlabRuntimeStoreSpec;
readonly webProbe?: HwlabRuntimeWebProbeSpec;
@@ -777,6 +791,7 @@ interface HwlabLaneConfig {
readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec;
readonly codeAgentRuntime?: HwlabRuntimeCodeAgentRuntimeSpec;
readonly sourceWorkspace?: HwlabRuntimeSourceWorkspaceSpec;
readonly nativeDevelopment?: HwlabRuntimeNativeDevelopmentSpec;
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
readonly runtimeStore?: HwlabRuntimeStoreSpec;
readonly webProbe?: HwlabRuntimeWebProbeSpec;
@@ -1036,6 +1051,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
codeAgentProvider: codeAgentProviderConfig(raw.codeAgentProvider, `lanes.${id}.codeAgentProvider`),
codeAgentRuntime: codeAgentRuntimeConfig(raw.codeAgentRuntime, `lanes.${id}.codeAgentRuntime`),
sourceWorkspace: sourceWorkspaceConfig(raw.sourceWorkspace, `lanes.${id}.sourceWorkspace`),
nativeDevelopment: nativeDevelopmentConfig(raw.nativeDevelopment, `lanes.${id}.nativeDevelopment`),
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
runtimeStore: runtimeStoreConfig(raw.runtimeStore, `lanes.${id}.runtimeStore`),
webProbe: webProbeConfig(raw.webProbe, `lanes.${id}.webProbe`),
@@ -1074,6 +1090,7 @@ function laneTargetConfig(
sourceAuthority: mergeOptionalRecord(baseRaw.sourceAuthority, targetRaw.sourceAuthority),
sourceSnapshot: mergeOptionalRecord(baseRaw.sourceSnapshot, targetRaw.sourceSnapshot),
sourceWorkspace: mergeOptionalRecord(baseRaw.sourceWorkspace, targetRaw.sourceWorkspace),
nativeDevelopment: mergeOptionalRecord(baseRaw.nativeDevelopment, targetRaw.nativeDevelopment),
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
runtimeStore: mergeOptionalRecord(baseRaw.runtimeStore, targetRaw.runtimeStore),
webProbe: mergeOptionalRecord(baseRaw.webProbe, targetRaw.webProbe),
@@ -1394,6 +1411,28 @@ function sourceWorkspaceConfig(value: unknown, path: string): HwlabRuntimeSource
};
}
function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNativeDevelopmentSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const workbench = asRecord(raw.workbench, `${path}.workbench`);
const publicHostRef = stringField(workbench, "publicHostRef", `${path}.workbench`);
const publicHost = resolveConfigRefString(publicHostRef, `${path}.workbench.publicHostRef`);
if (isIP(publicHost) === 0) throw new Error(`${path}.workbench.publicHostRef must resolve to a public IP address`);
const api = asRecord(workbench.api, `${path}.workbench.api`);
const worker = asRecord(workbench.worker, `${path}.workbench.worker`);
const web = asRecord(workbench.web, `${path}.workbench.web`);
return {
workbench: {
publicHostRef,
publicHost,
stateDir: relativeWorkspacePathField(stringField(workbench, "stateDir", `${path}.workbench`), `${path}.workbench.stateDir`),
api: { bindHost: stringField(api, "bindHost", `${path}.workbench.api`), probeHost: stringField(api, "probeHost", `${path}.workbench.api`), port: numberField(api, "port", `${path}.workbench.api`) },
worker: { bindHost: stringField(worker, "bindHost", `${path}.workbench.worker`), probeHost: stringField(worker, "probeHost", `${path}.workbench.worker`), healthPort: numberField(worker, "healthPort", `${path}.workbench.worker`) },
web: { bindHost: stringField(web, "bindHost", `${path}.workbench.web`), probeHost: stringField(web, "probeHost", `${path}.workbench.web`), port: numberField(web, "port", `${path}.workbench.web`) },
},
};
}
function sourceWorkspaceHostDependenciesConfig(value: unknown, path: string): HwlabRuntimeSourceWorkspaceHostDependenciesSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
@@ -2465,6 +2504,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
...(config.codeAgentProvider === undefined ? {} : { codeAgentProvider: config.codeAgentProvider }),
...(config.codeAgentRuntime === undefined ? {} : { codeAgentRuntime: config.codeAgentRuntime }),
...(config.sourceWorkspace === undefined ? {} : { sourceWorkspace: config.sourceWorkspace }),
...(config.nativeDevelopment === undefined ? {} : { nativeDevelopment: config.nativeDevelopment }),
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
...(config.runtimeStore === undefined ? {} : { runtimeStore: config.runtimeStore }),
...(config.webProbe === undefined ? {} : { webProbe: config.webProbe }),
+5 -1
View File
@@ -47,6 +47,7 @@ import { legacyHwlabNodeWebProbeUnsupported } from "../web-probe";
import { startNodeDelegatedJob } from "./web-probe";
import { assertKnownOptions } from "./web-probe-observe";
import { hwlabNodeDeliveryMutationDecision } from "./delivery-authority";
import { runHwlabNativeDevelopmentCommand } from "../hwlab-native-development";
export { hwlabNodeHelp, hwlabNodeWebProbeHelp, hwlabNodeObservabilityHelp } from "../hwlab-node-help";
@@ -569,6 +570,9 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom
if (domain === "fake-model-provider") {
return runHwlabFakeModelProviderCommand(_config, args.slice(1));
}
if (domain === "native-development") {
return runHwlabNativeDevelopmentCommand(args.slice(1));
}
if (domain === "web-probe") {
return legacyHwlabNodeWebProbeUnsupported(args.slice(1));
}
@@ -584,7 +588,7 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom
return runNodeDelegatedDomain(_config, domain, args.slice(1));
}
if (domain !== "secret") {
return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes hwpod-preinstall, hwlab nodes hwpod-node, hwlab nodes fake-model-provider, hwlab nodes observability, hwlab nodes secret, hwlab nodes test-accounts. web-probe moved to top-level: bun scripts/cli.ts web-probe --help" };
return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes native-development, hwlab nodes hwpod-preinstall, hwlab nodes hwpod-node, hwlab nodes fake-model-provider, hwlab nodes observability, hwlab nodes secret, hwlab nodes test-accounts. web-probe moved to top-level: bun scripts/cli.ts web-probe --help" };
}
const options = parseSecretOptions(args.slice(1));
return runNodeSecret(options);