diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 6d3e9f26..556d0bbd 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -217,7 +217,11 @@ lanes: sourceRef: hwlab/nc01-v03-admin.env sourceKey: HWLAB_API_KEY specRegistry: - runtimeDir: .state/hwpod-native/specs/runtime + database: + sourceRef: hwlab/nc01-v03-cloud-api-db.env + sourceKey: DATABASE_URL + schema: hwpod + table: runtime_specs builtInConfigRefs: - config/hwlab-hwpod-preinstalls/constart-71freq-c.yaml#hwpodPreinstall.specDocument publicExposure: diff --git a/config/platform-db/postgres-nc01.yaml b/config/platform-db/postgres-nc01.yaml index 826ccd4a..d7f9f9bc 100644 --- a/config/platform-db/postgres-nc01.yaml +++ b/config/platform-db/postgres-nc01.yaml @@ -166,6 +166,11 @@ postgres: user: tasktree_production address: 10.42.0.0/16 method: scram-sha-256 + - type: hostssl + database: hwlab_nc01_v03 + user: hwlab_nc01_v03_app + address: 10.42.0.0/16 + method: scram-sha-256 - type: hostssl database: hwlab_nc01_production user: hwlab_nc01_production_app @@ -318,6 +323,20 @@ secrets: TASKTREE_PRODUCTION_DB_NAME: tasktree_production randomHex: TASKTREE_PRODUCTION_DB_PASSWORD: 32 + - name: hwlab-nc01-v03-db-credentials + sourceRef: platform-db/hwlab-nc01-v03-db.env + type: env + requiredKeys: + - HWLAB_NC01_V03_DB_USER + - HWLAB_NC01_V03_DB_PASSWORD + - HWLAB_NC01_V03_DB_NAME + createIfMissing: + enabled: true + values: + HWLAB_NC01_V03_DB_USER: hwlab_nc01_v03_app + HWLAB_NC01_V03_DB_NAME: hwlab_nc01_v03 + randomHex: + HWLAB_NC01_V03_DB_PASSWORD: 32 - name: hwlab-nc01-production-db-credentials sourceRef: platform-db/hwlab-nc01-production-db.env type: env @@ -425,6 +444,15 @@ objects: createdb: false createrole: false superuser: false + - name: hwlab_nc01_v03_app + passwordRef: + sourceRef: platform-db/hwlab-nc01-v03-db.env + key: HWLAB_NC01_V03_DB_PASSWORD + login: true + attributes: + createdb: false + createrole: false + superuser: false - name: hwlab_nc01_production_app passwordRef: sourceRef: platform-db/hwlab-nc01-production-db.env @@ -490,6 +518,11 @@ objects: encoding: UTF8 locale: C.UTF-8 extensions: [] + - name: hwlab_nc01_v03 + owner: hwlab_nc01_v03_app + encoding: UTF8 + locale: C.UTF-8 + extensions: [] - name: hwlab_nc01_production owner: hwlab_nc01_production_app encoding: UTF8 @@ -640,6 +673,21 @@ exports: - scope: hwlab-production secret: hwlab-tasktree-db key: DATABASE_URL + - name: hwlab-nc01-v03-cloud-api-database-url + sourceSecretRef: platform-db/hwlab-nc01-v03-db.env + render: + envKey: DATABASE_URL + format: postgresql://$(HWLAB_NC01_V03_DB_USER):$(HWLAB_NC01_V03_DB_PASSWORD)@$(PGHOST):5432/$(HWLAB_NC01_V03_DB_NAME)?sslmode=require + variables: + PGHOST: 10.42.0.1 + writeToSecretSource: + sourceRef: hwlab/nc01-v03-cloud-api-db.env + key: DATABASE_URL + mode: update-or-insert + consumers: + - scope: hwlab-v03 + secret: hwlab-cloud-api-v03-db + key: DATABASE_URL - name: hwlab-nc01-production-cloud-api-database-url sourceSecretRef: platform-db/hwlab-nc01-production-db.env render: diff --git a/config/secrets-distribution.yaml b/config/secrets-distribution.yaml index 6c1e5b7e..895c2c28 100644 --- a/config/secrets-distribution.yaml +++ b/config/secrets-distribution.yaml @@ -585,6 +585,15 @@ kubernetesSecrets: - sourceRef: hwlab/web-probe-monitor-db.env sourceKey: DATABASE_URL targetKey: DATABASE_URL + - name: hwlab-nc01-v03-cloud-api-db + targetId: hwlab-nc01-v03 + secretName: hwlab-cloud-api-v03-db + type: Opaque + namespace: hwlab-v03 + data: + - sourceRef: hwlab/nc01-v03-cloud-api-db.env + sourceKey: DATABASE_URL + targetKey: database-url - name: hwlab-tasktree-db targetId: hwlab-nc01-v03 secretName: hwlab-tasktree-db diff --git a/project-management/PJ2026-01/specs/PJ2026-010102-hwpod-tools.md b/project-management/PJ2026-01/specs/PJ2026-010102-hwpod-tools.md index c774c3af..cf2c6f97 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010102-hwpod-tools.md +++ b/project-management/PJ2026-01/specs/PJ2026-010102-hwpod-tools.md @@ -98,15 +98,23 @@ HWPOD工具应提供 spec 新建、读取、列表、修改、删除、validate Spec 工具必须以 HWPOD 标准为准输出校验结果。服务端 registry 可以提供 authority 摘要,但工具不得用服务端缺省值静默补齐未声明的危险写操作能力。 -Spec CRUD 必须在 L0 与 L1 使用同一 repository 合同: +Spec CRUD 必须在 L0-L3 使用同一 PostgreSQL repository 合同: -- L0 由 `hwpod spec list|get|create|update|delete --local` 直接调用 native function; -- L1 由同一命令加 `--over-api` 调用 owning YAML 固定端口上的 HWPOD API; +- L0 由 `hwpod spec list|get|create|update|delete --local` 直接调用 native function, + 并从 owning YAML 的 Secret sourceRef 连接 development host PostgreSQL; +- L1 由同一命令加 `--over-api` 调用 owning YAML 固定端口上的 HWPOD API, + API 与 L0 连接同一个 development database; +- L2 的 development API 与 L0/L1 使用同一个 development database, + L3 使用 production owning YAML 声明的 production database; +- database、role、endpoint、Secret sourceRef、连接 key 和 schema/table identity + 必须来自 owning YAML,不得由命令行参数、进程目录或代码默认值补齐; - create 和 update 接收完整 spec document,并在写入前执行与 validate 相同的校验; - list 默认返回有界摘要,get 返回单个完整 document; - create、update 和 delete 必须输出 authority、mutable、frozen、hwpodId 和 mutation; - YAML-first 内置 spec 的 update 和 delete 必须返回 `hwpod_spec_frozen`,不得复制为 runtime spec、写入覆盖层或修改 owning YAML; - runtime spec 与内置 spec 同名时,create 必须返回 `hwpod_spec_frozen`,不得形成遮蔽或优先级覆盖。 +- filesystem JSON registry、`--runtime-spec-dir` 和 runtime directory env 属于 + `legacy-retire`,不得作为 fallback、双写、迁移后 overlay 或第二 authority 保留。 ### 6.2 HWPOD-TOOL-REQ-002 执行动作入口 diff --git a/project-management/PJ2026-01/specs/PJ2026-010103-hwpod-service.md b/project-management/PJ2026-01/specs/PJ2026-010103-hwpod-service.md index 4af93632..dcd0caed 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010103-hwpod-service.md +++ b/project-management/PJ2026-01/specs/PJ2026-010103-hwpod-service.md @@ -132,17 +132,35 @@ Registry 必须以 HWPOD 标准解释资源事实。缺失 HWPOD spec 的真实 Registry 必须区分两种 authority: - `yaml-first-builtin` 来自 owning YAML 解析后注入的内置 spec,`mutable=false`、`frozen=true`; -- `runtime` 来自 HWPOD 服务状态目录,`mutable=true`、`frozen=false`,重启后继续可读。 +- `runtime` 来自 owning YAML 声明的 host PostgreSQL,`mutable=true`、 + `frozen=false`,API、Worker、Pod 和 host 进程重启后继续可读。 + +Runtime repository 必须使用唯一 PostgreSQL authority: + +- development 的 L0、L1 和 L2 共用同一个 host PostgreSQL database; +- production 的 L3 使用 production owning YAML 声明的 host PostgreSQL database; +- database、role、endpoint、TLS、Secret sourceRef、sourceKey 和 schema/table identity + 只能来自 owning YAML; +- runtime 表以 `hwpod_id` 为唯一键,保存规范化后的完整 `document JSONB`、 + 固定 authority `runtime`、`created_at` 和 `updated_at`; +- repository 初始化只执行幂等 schema/table/index migration,不创建隐藏 database、 + role、凭据或 filesystem fallback; +- YAML-first 内置 spec 只在读取时与 PostgreSQL runtime 记录合并, + 不写入 runtime 表。 Runtime CRUD 只能修改 `runtime` authority: -- create 在 hwpodId 不存在时原子写入 runtime state; +- create 在 hwpodId 不存在时以唯一键约束和单条事务原子写入; - get 和 list 合并返回内置与 runtime spec,并保留 authority、mutable 和 frozen; -- update 只能完整替换已有 runtime spec; -- delete 只能删除已有 runtime spec; +- update 只能在单条事务中完整替换已有 runtime spec,并保留 `created_at`; +- delete 只能在单条事务中删除已有 runtime spec; - 内置 spec 的同名 create、update 和 delete 均返回 `hwpod_spec_frozen`; - runtime spec 不得遮蔽、覆盖、复制或回写 YAML-first 内置 spec。 +filesystem JSON registry、runtime spec directory 参数和目录环境变量属于 +`legacy-retire`。实现不得保留 filesystem fallback、双写、启动导入、overlay +或第二 authority;历史 JSON 只允许通过显式一次性迁移工具处理,不参与正常 CRUD。 + ### 6.2 HWPOD-SVC-REQ-002 租约与占用 | 编号 | 短名 | 主责模块 | 关联模块 | @@ -240,8 +258,17 @@ flowchart LR ### 7.4 验收契约 -- L0:spec、topology、workflow contract 和 typed error 测试通过;runtime CRUD 只修改 disposable state,内置 spec mutation 返回 `hwpod_spec_frozen`。 -- L1:通过 native API 完成 runtime spec create、list、get、update、delete,并证明 API 重启后 create/update 结果仍可读取。 +- L0:spec、topology、workflow contract 和 typed error 测试通过;runtime CRUD + 直接修改 owning YAML 选择的 development host PostgreSQL,内置 spec mutation + 返回 `hwpod_spec_frozen`。 +- L0/L1:对同一 runtime spec 做 `--local` 与 `--over-api` 对比复测, + 两条路径返回一致的 document、authority 和 fingerprint。 +- L1:通过 native API 完成 runtime spec create、list、get、update、delete, + 并证明 API 重启后 create/update 结果仍可读取。 +- L2:development API 完成同一 CRUD,并证明 Deployment/Pod 重启后记录仍可读取; + 该验证不得使用 Pod filesystem 作为持久化证据。 +- L3:完成 L2 回归并取得本次生产操作明确授权后,production API 完成有界 CRUD + 和 Pod 重启持久化验证;测试对象必须清理,内置对象指纹保持不变。 - L1:对 YAML-first 内置 spec 的同名 create、update 和 delete 均返回 `hwpod_spec_frozen`,内置 document 指纹保持不变。 - L1:API、worker、Web 可分别重启;worker 重启后未完成 workflow 可继续;Web 通过固定入口完成首屏、资源列表和 operation 状态交互。 - L1:D601 节点注册后,HWPOD topology 同时显示 node online、spec available 和 capability match;节点断开后显示 node offline,不得伪造硬件成功。 diff --git a/project-management/PJ2026-01/specs/PJ2026-01010305-71freq-hwpod-v03-preinstall.md b/project-management/PJ2026-01/specs/PJ2026-01010305-71freq-hwpod-v03-preinstall.md index 54aa2319..7dbfa04c 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01010305-71freq-hwpod-v03-preinstall.md +++ b/project-management/PJ2026-01/specs/PJ2026-01010305-71freq-hwpod-v03-preinstall.md @@ -154,7 +154,10 @@ flowchart TD Tool --> Evidence[operator validation summary] ``` -运行发现要求 `constart-71freq-c` 的 runtime authority 来自挂载目录和 discovery 结果。cloud-api 代码不得把 71-FREQ 路径、COM 口、probe UID、board-comm IP、HWPOD id 或旧 node id 写成隐藏默认值。 +运行发现要求 `constart-71freq-c` 的 `yaml-first-builtin` authority 来自挂载目录和 +discovery 结果。该内置对象不写入 runtime PostgreSQL 表。cloud-api 代码不得把 +71-FREQ 路径、COM 口、probe UID、board-comm IP、HWPOD id 或旧 node id 写成 +隐藏默认值。 ### 5.4 Windows outbound HWPOD node/gateway 时序图 @@ -229,6 +232,11 @@ YAML-first 预装 spec 在所有运行等级都必须标记为 `yaml-first-built 回写该对象;同名 create、update 和 delete 必须返回 `hwpod_spec_frozen`,并保留 owning configRef 与变更前 document 指纹。 +L0-L3 的 runtime spec 必须持久化到对应 owning YAML 声明的 host PostgreSQL。 +L0/L1/L2 共用 development database,L3 使用 production database;API、Worker、 +Pod 或 host 进程重启不得丢失 runtime spec。挂载目录只承载只读 YAML-first 内置 +spec,不得承载 runtime JSON、fallback、双写或覆盖层。 + ### 6.3 HWPOD-PRE-REQ-003 D601 Windows v0.3+ 出站托管 | 编号 | 短名 | 主责模块 | 关联模块 | diff --git a/scripts/src/hwlab-native-development.test.ts b/scripts/src/hwlab-native-development.test.ts index 160f0aa7..2972db42 100644 --- a/scripts/src/hwlab-native-development.test.ts +++ b/scripts/src/hwlab-native-development.test.ts @@ -56,10 +56,15 @@ test("L1 Workbench exposure uses YAML HTTPS origin and separate bind/probe addre expect(hwlabNativeDevelopmentHelp()).toMatchObject({ ok: true, command: "hwlab nodes native-development workbench" }); }); -test("L1 HWPOD spec registry resolves runtime state and frozen built-ins from owning YAML", () => { +test("L1 HWPOD spec registry resolves PostgreSQL authority and frozen built-ins from owning YAML", () => { const native = hwlabRuntimeLaneSpecForNode("v03", "NC01").nativeDevelopment?.hwpod; expect(native).toBeDefined(); - expect(native!.specRegistry.runtimeDir).toBe(".state/hwpod-native/specs/runtime"); + expect(native!.specRegistry.database).toEqual({ + sourceRef: "hwlab/nc01-v03-cloud-api-db.env", + sourceKey: "DATABASE_URL", + schema: "hwpod", + table: "runtime_specs", + }); expect(native!.specRegistry.builtInConfigRefs).toHaveLength(1); const resolved = resolveConfigRef(native!.specRegistry.builtInConfigRefs[0], "HWPOD built-in spec"); expect(resolved.value).toMatchObject({ kind: "Hwpod", metadata: { name: "constart-71freq-c" } }); diff --git a/scripts/src/hwlab-native-development.ts b/scripts/src/hwlab-native-development.ts index 7b7b3d1a..c76264cd 100644 --- a/scripts/src/hwlab-native-development.ts +++ b/scripts/src/hwlab-native-development.ts @@ -59,6 +59,25 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record 0) throw new Error(`native-development workbench status does not accept positional arguments: ${remaining.join(" ")}`); @@ -268,6 +287,11 @@ function hwlabHwpodNativeDevelopmentEnvironment(native: NonNullable { const resolved = resolveConfigRef(configRef, "nativeDevelopment.hwpod.specRegistry.builtInConfigRefs"); return { configRef, sha256: resolved.sha256, document: resolved.value }; @@ -284,7 +308,9 @@ function hwlabHwpodNativeDevelopmentEnvironment(native: NonNullable, key: string, path: string): s return value; } +function postgresIdentifierField(obj: Record, key: string, path: string): string { + const value = stringField(obj, key, path); + if (!/^[a-z_][a-z0-9_]*$/u.test(value)) throw new Error(`${path}.${key} must be a lowercase PostgreSQL identifier`); + return value; +} + function enumStringField(obj: Record, key: string, path: string, allowed: readonly T[]): T { const value = stringField(obj, key, path); if (!allowed.includes(value as T)) throw new Error(`${path}.${key} must be one of ${allowed.join(", ")}`); @@ -1624,6 +1638,7 @@ function nativeHwpodConfig(value: unknown, path: string): NonNullable