diff --git a/Makefile b/Makefile index f95d380d..253954bb 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ PLUGINS := plugins COMBO := core=$(shell git -C $(CORE) rev-parse --short HEAD 2>/dev/null)+backend=$(shell git -C $(BACKEND) rev-parse --short HEAD 2>/dev/null)+frontend=$(shell git -C $(FRONTEND) rev-parse --short HEAD 2>/dev/null)+ui=$(shell git -C $(UI) rev-parse --short HEAD 2>/dev/null)+agentcmd=$(shell git -C deps/agent-command rev-parse --short HEAD 2>/dev/null)+documents=$(shell git -C modules/documents rev-parse --short HEAD 2>/dev/null)+native=$(shell git -C deps/agent-native rev-parse --short HEAD 2>/dev/null)+squad=$(shell git -C deps/agent-squad rev-parse --short HEAD 2>/dev/null)+intent=$(shell git -C deps/agent-intent rev-parse --short HEAD 2>/dev/null) VERSION := $(shell git describe --tags --always --dirty 2>/dev/null)+$(COMBO) -.PHONY: help sync status build frontend svc svc-list svc-info svc-commits svc-history svc-update svc-recover svc-watchdog test-backend test-backend-pg test-multi-instance test-lifecycle test-frontend test-ui test-core test-agentcmd test-pins test-libs dev pin clean setup precheck task-triage task-triage-decide task-start task-resume task-sync task-close task-finish task-pr-check ci-wait facts-verify evidence version agent-exec-validate agent-exec-apply agent-exec-admit +.PHONY: help sync status build frontend svc svc-list svc-info svc-commits svc-history svc-update svc-recover svc-watchdog test-backend test-backend-pg test-multi-instance test-lifecycle test-regulations-kb test-frontend test-ui test-core test-agentcmd test-pins test-libs dev pin clean setup precheck task-triage task-triage-decide task-start task-resume task-sync task-close task-finish task-pr-check ci-wait facts-verify evidence version agent-exec-validate agent-exec-apply agent-exec-admit version: @echo "agent-system $(VERSION)" @@ -87,6 +87,7 @@ help: @echo "make build 构建单二进制 bin/agent-system(契约同步→前端构建→embed→静态链接)" @echo "make test 全量测试(boundaries + pins + libs + core + backend + frontend + ui + agent-command)" @echo "make test-governance 临时真实仓库中的任务、证据、写守卫与 CI 凭据回归" + @echo "make test-regulations-kb 法规知识库组合门禁:需 KBREG_TEST_DATABASE_URL、SVC_RDM_SRC_DIR、KB_SRC_DIR(不纳入 make test)" @echo "make svc 服务级操作:make svc ENV= [ACTION=deploy|start|stop|restart|status|logs] [ARGS='--from dev@ --tag ...']" @echo "make svc-list 服务总览:make svc-list [ARGS='--tag core'];数据库运维:scripts/svc.sh db create|dsn|backup|backups|restore|clone|copy|ls|set" @echo "make svc-info 综合信息报告(只读):make svc-info [ARGS='pg1 --reveal']——运行服务/数据库实例与内部库/配置快照" @@ -164,6 +165,14 @@ test-lifecycle: bash scripts/fetch-rdm-pin.sh tests/integration/lifecycle/rdm.pin >/dev/null python3 tests/integration/lifecycle/lifecycle_suite.py +# 法规知识库组合门禁(U8):固定 RDM/KB 修订上的 KB test-pg、RDM kbreg 与客户端制品凭据扫描。 +# 缺环境直接失败,不静默跳过;不纳入 make test。见 tests/integration/regulations-kb/README.md。 +test-regulations-kb: + @test -n "$${KBREG_TEST_DATABASE_URL}" || { echo "test-regulations-kb: KBREG_TEST_DATABASE_URL 未设置(一次性 PostgreSQL+pgvector 必需,不静默跳过)"; exit 1; } + @test -n "$${SVC_RDM_SRC_DIR}" || { echo "test-regulations-kb: SVC_RDM_SRC_DIR 未设置(RDM 检出必需,不静默跳过)"; exit 1; } + @test -n "$${KB_SRC_DIR}" || { echo "test-regulations-kb: KB_SRC_DIR 未设置(KB-Regulations 检出必需,不静默跳过)"; exit 1; } + bash tests/integration/regulations-kb/run.sh + # 双进程 HTTP 级验收(协作存储方案 U7):两个真实 serve 进程共享隔离 PG。 # 缺环境时明确失败而非静默跳过;见 tests/integration/multi-instance/README.md。 test-multi-instance: diff --git a/PROJECT.md b/PROJECT.md index 5d7dca38..3fada607 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -82,3 +82,19 @@ U2 起实施与部署分别评审。 - [渐进工作台界面重设计 r0.2](modules/documents/docs/proposals/2026-10-12-work-companion-ui-design.md):用户已授权连续实施;[计划 r1](modules/documents/docs/proposals/2026-10-12-work-companion-ui-plan.md)覆盖任务摘要、对话/输入、窄屏和真实状态投影;进展见对应任务卡。 - [系统项目、成员工作入口与私域协作闭环 r1](modules/documents/docs/proposals/2026-10-13-system-project-membership.md):2026-10-13 用户批准阶段 1–9 连续推进;统一全局项目身份与名称、成员个人会话、每项目两树一网和私域权限;当前组合验证/CI/tag 消费推进中,部署与线上修复未授权。 + +## 法规知识库组合 + +[法规知识库计划](docs/superpowers/plans/2026-10-18-regulations-knowledge-base-plan.md) U8 的固定组合: + +- `modules/backend` 56a7f9f(EXT-20 法规检索/搜索扩展合同)、`modules/documents` 5bb4877(EXT-20 提案)、 + `modules/frontend` 50ec836 与 `modules/ui` 5c2b103(法规引用渲染)。 +- RDM ece91da(含 kbreg 组合)与 KB-Regulations 5dddad2,固定在 + `tests/integration/regulations-kb/components.pin`;KB 经 RDM 的 `pins/agent-system-kb-regulations` + gitlink 接入,门禁要求该 gitlink 是 `kb_sha` 的祖先。 +- 凭据变量:`KBREG_DATABASE_URL`、`KBREG_EMBED_API_KEY`、`KBREG_SEARCH_ZHIPU_API_KEY`、`KBREG_MINERU_TOKEN` + (测试库用 `KBREG_TEST_DATABASE_URL`)。按环境注入、轮换与撤销见 KB 仓 `docs/operations/runbook.md`;值不入仓、不回显。 +- 门禁:`make test-regulations-kb`(需 `KBREG_TEST_DATABASE_URL`、`SVC_RDM_SRC_DIR`、`KB_SRC_DIR`,不纳入 `make test`), + 说明见 `tests/integration/regulations-kb/README.md`。真实搜索/嵌入/MinerU 调用 NOT_RUN,待 owner 批准。 + +部署、版本发布与业务/法务验收是后续独立门禁,本组合门禁通过不代表其中任何一项完成。 diff --git a/modules/backend b/modules/backend index 276dd738..56a7f9f4 160000 --- a/modules/backend +++ b/modules/backend @@ -1 +1 @@ -Subproject commit 276dd73824116fb55064eefef17f069ee1545ada +Subproject commit 56a7f9f44095d51d20b7425be9b0fd93931b4e9b diff --git a/modules/documents b/modules/documents index e2f5729e..5bb48779 160000 --- a/modules/documents +++ b/modules/documents @@ -1 +1 @@ -Subproject commit e2f5729e8f384f3d57d04e3ef1f45fb18c73dd54 +Subproject commit 5bb487795cc2614e1e6fccd48780a76163fafab2 diff --git a/modules/frontend b/modules/frontend index 92864db8..50ec8365 160000 --- a/modules/frontend +++ b/modules/frontend @@ -1 +1 @@ -Subproject commit 92864db8d33bc0601f06e25d9ae2af4a404b9b6a +Subproject commit 50ec83657ac591b4e452298d86ea52af4f80bd0e diff --git a/modules/ui b/modules/ui index c9187473..5c2b1036 160000 --- a/modules/ui +++ b/modules/ui @@ -1 +1 @@ -Subproject commit c9187473fb1ae9e8f3946cdf9b67055ee732054a +Subproject commit 5c2b1036dd1aff89ec4eeae0c96493694e33ad4c diff --git a/tests/integration/regulations-kb/README.md b/tests/integration/regulations-kb/README.md new file mode 100644 index 00000000..a3c679e6 --- /dev/null +++ b/tests/integration/regulations-kb/README.md @@ -0,0 +1,53 @@ +# 法规知识库组合门禁(U8) + +在固定组件修订上验证 RDM + 法规知识库(KB-Regulations)的本地组合:数据与索引可恢复、 +双源与引用流程可用、凭据不进入客户端制品。对应 +[法规知识库计划](../../../docs/superpowers/plans/2026-10-18-regulations-knowledge-base-plan.md) 的 U8 +与 Verification Contract 的 Composition/recovery 行;引用对象见 +[法规引用合同](../../../docs/superpowers/specs/2026-10-18-regulations-citation-contract.md)。 + +## 固定组件 + +`components.pin` 只有两个键,均为 40 位 hex,`run.sh` 逐行解析、从不执行: + +- `rdm_sha`:Agent-System-RDM main(含 U6、U7 与 kbreg 组合)。 +- `kb_sha`:Agent-System-KB-Regulations main(含 provider 与 U8 恢复测试)。 + +`run.sh` 先核对两个检出的 HEAD 等于 pin,再核对 RDM 检出中 +`pins/agent-system-kb-regulations` gitlink 是 KB 检出里 `kb_sha` 的祖先(RDM 编译所用 KB 代码包含在被测修订中)。 +Dev 侧 Backend/Documents/Frontend/UI 的 gitlink 由本仓 `modules/` 固定,见 PROJECT.md「法规知识库组合」。 + +## 前置条件 + +- 一次性 PostgreSQL(带 pgvector),连接串放在 `KBREG_TEST_DATABASE_URL`;PATH 上有 `pg_dump`、`pg_restore`。 + 可用 KB 仓 `scripts/test-pg-local.sh` 起临时库。门禁会写入该库,不要指向任何真实数据。 +- `SVC_RDM_SRC_DIR`:RDM 检出,位于 `rdm_sha`,submodule 已递归物化。 +- `KB_SRC_DIR`:KB-Regulations 检出,位于 `kb_sha`,submodule 已物化。 +- Go 与 Node(RDM 测试及客户端制品构建所需)。 +- 客户端制品扫描需要已构建的 RDM 产物(RDM 检出的 `bin/`、`.build/product/frontend/dist`), + 或用 `REGKB_SCAN_DIRS` 指定待扫目录(空格分隔)。 + +## 用法 + +```bash +KBREG_TEST_DATABASE_URL=... SVC_RDM_SRC_DIR=/path/to/rdm KB_SRC_DIR=/path/to/kb \ + make test-regulations-kb +``` + +缺任一变量时直接以非零退出并指出变量名,不运行任何阶段。每个阶段打印 `PASS <阶段>` 或 +`FAIL <阶段>`,任一失败则整体非零退出。脚本不回显 `KBREG_TEST_DATABASE_URL` 或任何凭据值。 + +## 各阶段证明什么 + +| 阶段 | 命令 | 对应 U8 场景 | +|---|---|---| +| `kb-test-pg` | `make -C "$KB_SRC_DIR" test-pg` | provider 组合链;dump/restore 后重建索引仍保持语料修订哈希与历史引用解析;provider/解析器不可用;凭据注入、轮换与撤销 | +| `rdm-kbreg` | `GOWORK=off go test -count=1 ./plugins/kbreg/...`,以及 `-run 'Composed\|RegCite\|Citation\|RegCheck\|RegBatch\|RegAudit'` 覆盖 `./internal/rdmapp/...`、`./plugins/rdm/...` | 完整法规问答的双通道记录、条款引用与审计批次重放;缺一通道时报告降级而非双源通过;保密词留在本地;凭据不进入输出;审计封印校验 | +| `client-asset-secret-scan` | 扫描 `REGKB_SCAN_DIRS`(缺省为 RDM 的 `bin/` 与 `.build/product/frontend/dist`) | 凭据不出现在客户端制品:任何文件含字符串 `KBREG_`,或含环境中非空 `KBREG_*KEY*`、`KBREG_*TOKEN*`、`KBREG_DATABASE_URL` 的值即失败;只打印文件路径与变量名。没有可扫目录时失败 | + +跨项目与受限来源的拒绝/脱敏场景归入 `rdm-kbreg` 阶段所选的 RDM 命令总线测试;具体用例以固定 RDM 修订中的测试为准。 + +## 不在本门禁范围 + +- 真实网络搜索、嵌入与 MinerU 调用:NOT_RUN,待 owner 批准外部调用后另行执行。 +- 部署、版本发布与业务/法务验收:均为后续独立门禁,本门禁通过不代表其中任何一项完成。 diff --git a/tests/integration/regulations-kb/components.pin b/tests/integration/regulations-kb/components.pin new file mode 100644 index 00000000..f24d3315 --- /dev/null +++ b/tests/integration/regulations-kb/components.pin @@ -0,0 +1,6 @@ +# 法规知识库组合门禁的固定组件(U8)。只接受 rdm_sha、kb_sha 两个键,值为 40 位 hex。 +# run.sh 逐行解析本文件,不执行、不 source。升级走 PR。 +# rdm_sha:RDM main,含 U6、U7 与 kbreg 组合。 +# kb_sha:KB-Regulations main,含 provider 与 U8 恢复测试;须包含 RDM pins/agent-system-kb-regulations。 +rdm_sha=ece91dab9f23628be54a91e846389cb3753b88dd +kb_sha=5dddad2ae58e7d0d7fffc95464e80fa0040d5e08 diff --git a/tests/integration/regulations-kb/run.sh b/tests/integration/regulations-kb/run.sh new file mode 100755 index 00000000..b3231170 --- /dev/null +++ b/tests/integration/regulations-kb/run.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# 法规知识库组合门禁(U8)。缺环境直接失败,不静默跳过。 +# 不回显任何 KBREG_* 值;扫描结果只打印文件路径与变量名。 +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +pin="${REGKB_PIN:-$here/components.pin}" + +# 与 scripts/fetch-rdm-pin.sh 同规则:只认已知键、40 位 hex,不执行 pin 文件。 +[ -f "$pin" ] || { echo "components.pin 不存在:$pin" >&2; exit 1; } +declare -A got=() +while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|\#*) continue ;; + esac + case "$line" in + *=*) ;; + *) echo "components.pin 行格式错误(须为 key=sha)" >&2; exit 1 ;; + esac + key="${line%%=*}" + val="${line#*=}" + case "$key" in + rdm_sha|kb_sha) ;; + *) echo "components.pin 未知键:$key" >&2; exit 1 ;; + esac + [[ "$val" =~ ^[0-9a-f]{40}$ ]] || { echo "components.pin 的 $key 不是 40 位 hex" >&2; exit 1; } + [ -z "${got[$key]:-}" ] || { echo "components.pin 重复键:$key" >&2; exit 1; } + got["$key"]="$val" +done < "$pin" +[ "${#got[@]}" -eq 2 ] || { echo "components.pin 必须同时有 rdm_sha、kb_sha" >&2; exit 1; } +rdm_sha="${got[rdm_sha]}" +kb_sha="${got[kb_sha]}" + +missing=0 +for v in KBREG_TEST_DATABASE_URL SVC_RDM_SRC_DIR KB_SRC_DIR; do + if [ -z "${!v:-}" ]; then + echo "regulations-kb: $v 未设置(组合门禁必需,不静默跳过)" >&2 + missing=1 + fi +done +[ "$missing" -eq 0 ] || exit 2 + +rdm_dir="$SVC_RDM_SRC_DIR" +kb_dir="$KB_SRC_DIR" + +head_of() { git -C "$1" rev-parse --verify -q HEAD 2>/dev/null || true; } +rdm_head="$(head_of "$rdm_dir")" +[ "$rdm_head" = "$rdm_sha" ] || { echo "regulations-kb: SVC_RDM_SRC_DIR HEAD=${rdm_head:-<无>},须为 rdm_sha=$rdm_sha" >&2; exit 1; } +kb_head="$(head_of "$kb_dir")" +[ "$kb_head" = "$kb_sha" ] || { echo "regulations-kb: KB_SRC_DIR HEAD=${kb_head:-<无>},须为 kb_sha=$kb_sha" >&2; exit 1; } + +kb_link="$(git -C "$rdm_dir" rev-parse --verify -q "HEAD:pins/agent-system-kb-regulations" 2>/dev/null || true)" +[ -n "$kb_link" ] || { echo "regulations-kb: RDM 检出缺少 pins/agent-system-kb-regulations gitlink" >&2; exit 1; } +if ! git -C "$kb_dir" merge-base --is-ancestor "$kb_link" "$kb_sha" 2>/dev/null; then + echo "regulations-kb: RDM 的 KB gitlink $kb_link 不是 kb_sha=$kb_sha 的祖先(或 KB 检出缺该对象)" >&2 + exit 1 +fi +echo "regulations-kb: rdm=$rdm_sha kb=$kb_sha(RDM KB gitlink $kb_link 为其祖先)" + +failed=0 +# 在 if 条件中 set -e 不生效:阶段函数内多条命令须显式 && 串联。 +stage() { + local name="$1"; shift + echo "=== stage $name ===" + if ( "$@" ); then + echo "PASS $name" + else + echo "FAIL $name" + failed=1 + fi +} + +stage_kb() { + make -C "$kb_dir" test-pg +} + +stage_rdm() { + cd "$rdm_dir" && + GOWORK=off go test -count=1 ./plugins/kbreg/... && + GOWORK=off go test -count=1 -run 'Composed|RegCite|Citation|RegCheck|RegBatch|RegAudit' ./internal/rdmapp/... ./plugins/rdm/... +} + +stage_scan() { + local dirs=() d + if [ -n "${REGKB_SCAN_DIRS:-}" ]; then + read -r -a dirs <<< "$REGKB_SCAN_DIRS" + else + for d in "$rdm_dir/bin" "$rdm_dir/.build/product/frontend/dist"; do + [ -d "$d" ] && dirs+=("$d") + done + fi + if [ "${#dirs[@]}" -eq 0 ]; then + echo "scan: 没有可扫描的客户端制品目录(先构建 RDM 产品,或设置 REGKB_SCAN_DIRS)" >&2 + return 1 + fi + local secret_vars=() name + while IFS= read -r name; do + case "$name" in + KBREG_*KEY*|KBREG_*TOKEN*|KBREG_DATABASE_URL) + [ -n "${!name:-}" ] && secret_vars+=("$name") ;; + esac + done < <(compgen -e) + local tmp rc=0 hits f + tmp="$(mktemp)" || return 1 + for d in "${dirs[@]}"; do + if [ ! -d "$d" ]; then + echo "scan: 目录不存在:$d" >&2 + rc=1 + continue + fi + echo "scan: $d" + hits="$(grep -rlF -e 'KBREG_' -- "$d" || true)" + if [ -n "$hits" ]; then + while IFS= read -r f; do echo "scan: $f 含字符串 KBREG_" >&2; done <<< "$hits" + rc=1 + fi + for name in "${secret_vars[@]}"; do + printf '%s\n' "${!name}" | grep -v '^$' > "$tmp" || true + [ -s "$tmp" ] || continue + hits="$(grep -rlF -f "$tmp" -- "$d" || true)" + if [ -n "$hits" ]; then + while IFS= read -r f; do echo "scan: $f 含 $name 的值" >&2; done <<< "$hits" + rc=1 + fi + done + done + rm -f "$tmp" + echo "scan: 已核对 ${#secret_vars[@]} 个凭据变量:${secret_vars[*]:-<无>}" + return "$rc" +} + +stage kb-test-pg stage_kb +stage rdm-kbreg stage_rdm +stage client-asset-secret-scan stage_scan + +if [ "$failed" -ne 0 ]; then + echo "regulations-kb: FAIL" + exit 1 +fi +echo "regulations-kb: PASS" diff --git a/tests/test_regulations_kb_pin.py b/tests/test_regulations_kb_pin.py new file mode 100644 index 00000000..2bfb78ca --- /dev/null +++ b/tests/test_regulations_kb_pin.py @@ -0,0 +1,102 @@ +"""法规知识库组合门禁:components.pin 只接受固定 SHA;run.sh 缺环境直接失败。""" +import os +import re +import subprocess +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +GATE = ROOT / "tests" / "integration" / "regulations-kb" +PIN = GATE / "components.pin" +RUN = GATE / "run.sh" +HEX40 = re.compile(r"^[0-9a-f]{40}$") +KEYS = {"rdm_sha", "kb_sha"} +REQUIRED = ("KBREG_TEST_DATABASE_URL", "SVC_RDM_SRC_DIR", "KB_SRC_DIR") + + +def load_pin(text): + out = {} + for line in text.splitlines(): + if not line or line.startswith("#"): + continue + if "=" not in line: + raise ValueError("bad pin line") + key, value = line.split("=", 1) + if key not in KEYS: + raise ValueError("unknown pin key") + if not HEX40.match(value): + raise ValueError("sha must be 40 hex") + if key in out: + raise ValueError("duplicate pin key") + out[key] = value + if set(out) != KEYS: + raise ValueError("pin keys incomplete") + return out + + +def run_gate(env, timeout=20): + base = {"PATH": os.environ.get("PATH", "/usr/bin:/bin")} + base.update(env) + return subprocess.run( + ["bash", str(RUN)], cwd=ROOT, env=base, capture_output=True, text=True, timeout=timeout + ) + + +class RegulationsKbPinTests(unittest.TestCase): + def test_pin_is_two_fixed_shas(self): + doc = load_pin(PIN.read_text()) + self.assertEqual(set(doc), KEYS) + for value in doc.values(): + self.assertRegex(value, HEX40) + + def test_parser_rejects_url_short_sha_and_unknown_key(self): + kb = "kb_sha=" + "b" * 40 + "\n" + bad = [ + "rdm_sha=https://example.invalid/repo.git\n" + kb, + "rdm_sha=abc\n" + kb, + "rdm_sha=" + "a" * 40 + "\n" + kb + "url=" + "c" * 40 + "\n", + ] + for text in bad: + with self.assertRaises(ValueError): + load_pin(text) + + def test_run_sh_parser_rejects_bad_pins_before_env_check(self): + kb = "kb_sha=" + "b" * 40 + "\n" + cases = { + "rdm_sha=https://example.invalid/repo.git\n" + kb: "不是 40 位 hex", + "rdm_sha=abc\n" + kb: "不是 40 位 hex", + "rdm_sha=" + "a" * 40 + "\n" + kb + "url=" + "c" * 40 + "\n": "未知键", + "rdm_sha=$(touch /tmp/regkb-pwned)\n" + kb: "不是 40 位 hex", + kb: "必须同时有", + } + for text, expect in cases.items(): + with tempfile.NamedTemporaryFile("w", suffix=".pin", delete=False) as f: + f.write(text) + path = f.name + try: + proc = run_gate({"REGKB_PIN": path}) + finally: + os.unlink(path) + self.assertNotEqual(proc.returncode, 0, text) + self.assertIn(expect, proc.stderr, text) + self.assertNotIn("=== stage", proc.stdout) + + def test_run_sh_names_each_missing_variable_without_running_stages(self): + proc = run_gate({}) + self.assertNotEqual(proc.returncode, 0) + for name in REQUIRED: + self.assertIn(name, proc.stderr) + self.assertNotIn("=== stage", proc.stdout) + + for missing in REQUIRED: + env = {n: "/nonexistent-fake" for n in REQUIRED if n != missing} + proc = run_gate(env) + self.assertNotEqual(proc.returncode, 0, missing) + self.assertIn(missing + " 未设置", proc.stderr) + self.assertNotIn("=== stage", proc.stdout) + self.assertNotIn("/nonexistent-fake", proc.stdout + proc.stderr) + + +if __name__ == "__main__": + unittest.main()