name: ci on: pull_request: push: branches: [main] jobs: test: runs-on: ubuntu-latest env: GOPROXY: https://goproxy.cn,direct steps: - name: Checkout env: TOKEN: ${{ secrets.GITHUB_TOKEN }} SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} run: | set -euo pipefail # Credential handling stays inline: no file from the candidate is executed. # Workflow provenance and runner isolation still require platform controls. python3 - <<'PY_CHECKOUT' import os from pathlib import Path import re import subprocess import sys from urllib.parse import urlsplit # 服务端基址取运行器注入的 GITHUB_SERVER_URL(平台控制、候选 PR 不可影响)。 # 2026-10-05 修复(run 948/949 实测):注入值为内网直连形态 # 'http://172.17.0.1:3000'(docker 网关 → 宿主 Gitea),与公网拼写不同且为 # http——此前硬编码常量严格比对会拒绝合法检出。校验收宽为「http(s) + 非空 # netloc」;凭据仍只附着于「该基址 + 下方仓库白名单」构造的 URL。 SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") parsed_server = urlsplit(SERVER) if parsed_server.scheme not in {"http", "https"} or not parsed_server.netloc: sys.exit("Checkout server must be the platform-injected http(s) URL") SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/" # Reviewed allowlist, independent of candidate .gitmodules. Add repositories # here only with an explicit review of the checkout credential's scope. REPOSITORIES = { "modules/core": "Agent-System-Core", "modules/backend": "Agent-System-Backend", "modules/frontend": "Agent-System-Frontend", "modules/ui": "Agent-System-UI", "modules/documents": "Agent-System-Documents", "deps/agent-system-core": "Agent-System-Core", "deps/agent-document-renderer": "Agent-Document-Renderer", "deps/development-guidelines": "Development-Guidelines", "deps/agent-native": "Agent-Native", "deps/agent-squad": "Agent-Squad", "deps/agent-intent": "Agent-Intent", "deps/agent-command": "Agent-Command", "deps/error-code-system": "Error-Code-System", "deps/logging-system": "Logging-System", "deps/development-manager": "Development-Manager", "development/guidelines": "Development-Guidelines", } root_token = os.environ.pop("TOKEN", "") component_token = os.environ.pop("SUBMODULE_TOKEN", "") if not root_token or not component_token: sys.exit("Checkout requires TOKEN and SUBMODULE_TOKEN") if os.environ.get("GITHUB_REPOSITORY") != "weichen/Agent-System-Dev": sys.exit("Checkout repository is outside the reviewed allowlist") sha = os.environ.get("GITHUB_SHA", "") if not re.fullmatch(r"[0-9a-fA-F]{40}", sha): sys.exit("Checkout requires a fixed commit SHA") # Discard inherited Git tracing/config/SSH hooks. Auth lives only in one # Git process tree, in an exact URL-scoped header, never in a URL or file. clean_env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")} clean_env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0") config = { "core.hooksPath": "/dev/null", "core.fsmonitor": "false", "init.templateDir": "", "credential.helper": "", "fetch.recurseSubmodules": "false", "submodule.recurse": "false", "http.followRedirects": "false", "protocol.allow": "never", "protocol." + urlsplit(SERVER).scheme + ".allow": "always", } def git(repo, *args, extra=None): values = config | (extra or {}) env = clean_env | {"GIT_CONFIG_COUNT": str(len(values))} for index, (key, value) in enumerate(values.items()): env[f"GIT_CONFIG_KEY_{index}"] = key env[f"GIT_CONFIG_VALUE_{index}"] = value return subprocess.run(["git", "-C", str(repo), *args], env=env, check=True, stdout=subprocess.PIPE).stdout def materialize(repo, depth=0): if depth > 16: raise ValueError("Submodule depth exceeds the reviewed checkout limit") links = {} for entry in git(repo, "ls-tree", "-rz", "HEAD").split(b"\0"): if not entry: continue metadata, path = entry.split(b"\t", 1) mode, kind, commit = metadata.decode().split() if mode == "160000": links[path.decode()] = commit if not links: return raw = git(repo, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list") entries = {} for item in raw.split(b"\0"): if not item: continue key, value = item.decode().split("\n", 1) match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key) if not match: raise ValueError("Unsupported candidate .gitmodules setting") name, field = match.groups() if field in entries.setdefault(name, {}): raise ValueError("Duplicate candidate .gitmodules setting") entries[name][field] = value if set(entries) != set(links): raise ValueError("Submodule names must match the fixed gitlink paths") # Validate the entire level before any authenticated component fetch. for name, fields in entries.items(): repository = REPOSITORIES.get(name) if not repository or fields.get("path") != name: raise ValueError("Submodule path is outside the reviewed allowlist") trusted_url = f"{SERVER}/weichen/{repository}.git" if fields.get("url") not in {SSH + repository + ".git", trusted_url}: raise ValueError("Submodule URL is outside the reviewed allowlist") for name in entries: url = f"{SERVER}/weichen/{REPOSITORIES[name]}.git" # Persist only the reviewed public URL so Git recognizes the # initialized submodule after process-scoped config is gone. git(repo, "config", f"submodule.{name}.url", url) git(repo, "submodule", "update", "--init", "--checkout", "--", name, extra={f"submodule.{name}.url": url, f"submodule.{name}.update": "checkout", f"http.{url}.extraheader": "AUTHORIZATION: token " + component_token}) child = repo / name if git(child, "rev-parse", "HEAD").decode().strip() != links[name]: raise ValueError("Materialized submodule does not match the fixed SHA") materialize(child, depth + 1) try: root = Path.cwd() # An ephemeral clean checkout is required; do not reuse prior Git state. if (root / ".git").exists(): raise ValueError("Checkout requires a fresh workspace") git(root, "init", "-q") url = SERVER + "/weichen/Agent-System-Dev.git" git(root, "remote", "add", "origin", url) git(root, "fetch", "--depth=1", "origin", sha, extra={f"http.{url}.extraheader": "AUTHORIZATION: token " + root_token}) git(root, "checkout", "-q", "--detach", "FETCH_HEAD") materialize(root) except (ValueError, OSError, subprocess.CalledProcessError) as error: sys.exit(f"Checkout refused: {error}") print("Checkout complete; credentials were not persisted in Git configuration") PY_CHECKOUT unset TOKEN SUBMODULE_TOKEN - name: PR base gate run: python3 scripts/task_pr_check.py ci - name: Install Go 1.27.1 run: | set -euo pipefail curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz" ln -sfn /usr/local/go/bin/go /usr/local/bin/go ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt - name: Validate AGENT-EXECUTION.md with the pinned shared executor run: scripts/agent-exec validate --repo . - name: Governance check with the pinned kit run: scripts/dm governance check --repo . - name: Install Node 22 run: | set -euo pipefail curl -fsSL -o "$RUNNER_TEMP/node.tar.xz" https://npmmirror.com/mirrors/node/v22.23.3/node-v22.23.3-linux-x64.tar.xz tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz" - name: Install house-style Python packages run: python3 -m pip install --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple python-docx python-pptx openpyxl - name: make test (boundaries, pins, libs, core, backend, frontend, ui, agent-command) run: make test - name: Build the single binary run: make build # 协作存储方案 U2:真实 PostgreSQL + pgvector + Redis 门禁。 # 缺服务时失败而非跳过(make test-backend-pg 硬校验环境变量); # 组件版本随 job 输出固定,供验收记录对齐(方案 KTD12/U2)。 test-pg: # 分级门禁:base 非 main 的 PR(L2 child → L1 initiative)只跑轻量 CI(test 作业); # 进入 main 的 PR 与 main 推送跑全量门禁。 if: ${{ github.event_name != 'pull_request' || github.base_ref == 'main' }} runs-on: ubuntu-latest env: GOPROXY: https://goproxy.cn,direct steps: - name: Checkout env: TOKEN: ${{ secrets.GITHUB_TOKEN }} SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} run: | set -euo pipefail # Credential handling stays inline: no file from the candidate is executed. # Workflow provenance and runner isolation still require platform controls. python3 - <<'PY_CHECKOUT' import os from pathlib import Path import re import subprocess import sys from urllib.parse import urlsplit # 服务端基址取运行器注入的 GITHUB_SERVER_URL(平台控制、候选 PR 不可影响)。 # 2026-10-05 修复(run 948/949 实测):注入值为内网直连形态 # 'http://172.17.0.1:3000'(docker 网关 → 宿主 Gitea),与公网拼写不同且为 # http——此前硬编码常量严格比对会拒绝合法检出。校验收宽为「http(s) + 非空 # netloc」;凭据仍只附着于「该基址 + 下方仓库白名单」构造的 URL。 SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") parsed_server = urlsplit(SERVER) if parsed_server.scheme not in {"http", "https"} or not parsed_server.netloc: sys.exit("Checkout server must be the platform-injected http(s) URL") SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/" # Reviewed allowlist, independent of candidate .gitmodules. Add repositories # here only with an explicit review of the checkout credential's scope. REPOSITORIES = { "modules/core": "Agent-System-Core", "modules/backend": "Agent-System-Backend", "modules/frontend": "Agent-System-Frontend", "modules/ui": "Agent-System-UI", "modules/documents": "Agent-System-Documents", "deps/agent-system-core": "Agent-System-Core", "deps/agent-document-renderer": "Agent-Document-Renderer", "deps/development-guidelines": "Development-Guidelines", "deps/agent-native": "Agent-Native", "deps/agent-squad": "Agent-Squad", "deps/agent-intent": "Agent-Intent", "deps/agent-command": "Agent-Command", "deps/error-code-system": "Error-Code-System", "deps/logging-system": "Logging-System", "deps/development-manager": "Development-Manager", "development/guidelines": "Development-Guidelines", } root_token = os.environ.pop("TOKEN", "") component_token = os.environ.pop("SUBMODULE_TOKEN", "") if not root_token or not component_token: sys.exit("Checkout requires TOKEN and SUBMODULE_TOKEN") if os.environ.get("GITHUB_REPOSITORY") != "weichen/Agent-System-Dev": sys.exit("Checkout repository is outside the reviewed allowlist") sha = os.environ.get("GITHUB_SHA", "") if not re.fullmatch(r"[0-9a-fA-F]{40}", sha): sys.exit("Checkout requires a fixed commit SHA") # Discard inherited Git tracing/config/SSH hooks. Auth lives only in one # Git process tree, in an exact URL-scoped header, never in a URL or file. clean_env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")} clean_env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0") config = { "core.hooksPath": "/dev/null", "core.fsmonitor": "false", "init.templateDir": "", "credential.helper": "", "fetch.recurseSubmodules": "false", "submodule.recurse": "false", "http.followRedirects": "false", "protocol.allow": "never", "protocol." + urlsplit(SERVER).scheme + ".allow": "always", } def git(repo, *args, extra=None): values = config | (extra or {}) env = clean_env | {"GIT_CONFIG_COUNT": str(len(values))} for index, (key, value) in enumerate(values.items()): env[f"GIT_CONFIG_KEY_{index}"] = key env[f"GIT_CONFIG_VALUE_{index}"] = value return subprocess.run(["git", "-C", str(repo), *args], env=env, check=True, stdout=subprocess.PIPE).stdout def materialize(repo, depth=0): if depth > 16: raise ValueError("Submodule depth exceeds the reviewed checkout limit") links = {} for entry in git(repo, "ls-tree", "-rz", "HEAD").split(b"\0"): if not entry: continue metadata, path = entry.split(b"\t", 1) mode, kind, commit = metadata.decode().split() if mode == "160000": links[path.decode()] = commit if not links: return raw = git(repo, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list") entries = {} for item in raw.split(b"\0"): if not item: continue key, value = item.decode().split("\n", 1) match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key) if not match: raise ValueError("Unsupported candidate .gitmodules setting") name, field = match.groups() if field in entries.setdefault(name, {}): raise ValueError("Duplicate candidate .gitmodules setting") entries[name][field] = value if set(entries) != set(links): raise ValueError("Submodule names must match the fixed gitlink paths") # Validate the entire level before any authenticated component fetch. for name, fields in entries.items(): repository = REPOSITORIES.get(name) if not repository or fields.get("path") != name: raise ValueError("Submodule path is outside the reviewed allowlist") trusted_url = f"{SERVER}/weichen/{repository}.git" if fields.get("url") not in {SSH + repository + ".git", trusted_url}: raise ValueError("Submodule URL is outside the reviewed allowlist") for name in entries: url = f"{SERVER}/weichen/{REPOSITORIES[name]}.git" # Persist only the reviewed public URL so Git recognizes the # initialized submodule after process-scoped config is gone. git(repo, "config", f"submodule.{name}.url", url) git(repo, "submodule", "update", "--init", "--checkout", "--", name, extra={f"submodule.{name}.url": url, f"submodule.{name}.update": "checkout", f"http.{url}.extraheader": "AUTHORIZATION: token " + component_token}) child = repo / name if git(child, "rev-parse", "HEAD").decode().strip() != links[name]: raise ValueError("Materialized submodule does not match the fixed SHA") materialize(child, depth + 1) try: root = Path.cwd() # An ephemeral clean checkout is required; do not reuse prior Git state. if (root / ".git").exists(): raise ValueError("Checkout requires a fresh workspace") git(root, "init", "-q") url = SERVER + "/weichen/Agent-System-Dev.git" git(root, "remote", "add", "origin", url) git(root, "fetch", "--depth=1", "origin", sha, extra={f"http.{url}.extraheader": "AUTHORIZATION: token " + root_token}) git(root, "checkout", "-q", "--detach", "FETCH_HEAD") materialize(root) except (ValueError, OSError, subprocess.CalledProcessError) as error: sys.exit(f"Checkout refused: {error}") print("Checkout complete; credentials were not persisted in Git configuration") PY_CHECKOUT unset TOKEN SUBMODULE_TOKEN - name: Install Go 1.27.1 run: | set -euo pipefail curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz" ln -sfn /usr/local/go/bin/go /usr/local/bin/go ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt - name: Install house-style Python packages run: python3 -m pip install --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple python-docx python-pptx openpyxl - name: Install PostgreSQL, pgvector and Redis run: | set -euo pipefail SUDO="" if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" != 0 ]; then SUDO=sudo; fi $SUDO apt-get update -y $SUDO apt-get install -y postgresql postgresql-contrib postgresql-16-pgvector redis-server # 固定组件/扩展版本输出(随日志归档,作为验收对齐依据)。 psql --version redis-server --version - name: Start PostgreSQL and Redis run: | set -euo pipefail SUDO="" if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" != 0 ]; then SUDO=sudo; fi # 运行器容器以 root 运行且无 sudo:以 postgres 身份执行用 runuser/su # (实测 $SUDO -u 在 root 容器展开成裸 "-u" → exit 127)。 pg_as_postgres() { if command -v runuser >/dev/null 2>&1; then runuser -u postgres -- "$@"; else su postgres -s /bin/bash -c "$*"; fi } $SUDO service postgresql start || $SUDO pg_ctlcluster 16 main start # 预装 pg_trgm 到 public:与测试辅助的幂等预装一致,锁定的组件契约; # pgvector 版本一并输出(随日志归档,作为验收对齐依据)。 pg_as_postgres psql -tAc "CREATE EXTENSION IF NOT EXISTS vector" pg_as_postgres psql -tAc "SELECT extname || '=' || extversion FROM pg_extension WHERE extname IN ('vector','pg_trgm')" pg_as_postgres psql -c "CREATE USER ci_agent PASSWORD 'ci_job_local_only' CREATEDB" pg_as_postgres psql -c "CREATE DATABASE agent_test OWNER ci_agent" pg_as_postgres psql -d agent_test -c "CREATE EXTENSION IF NOT EXISTS pg_trgm" redis-server --daemonize yes --port 6399 --save '' --appendonly no redis-cli -p 6399 ping - name: Backend tests against real PostgreSQL and Redis (required, no silent skip) env: AGENT_PG_TEST_DSN: postgres://ci_agent:ci_job_local_only@127.0.0.1:5432/agent_test?sslmode=disable AGENT_REDIS_TEST_ADDR: 127.0.0.1:6399 run: make test-backend-pg - name: Two-process HTTP acceptance over shared PostgreSQL (U7, required) env: AGENT_PG_TEST_DSN: postgres://ci_agent:ci_job_local_only@127.0.0.1:5432/agent_test?sslmode=disable run: make test-multi-instance - name: RDM lifecycle pin is three fixed SHAs run: python3 -m unittest tests.test_lifecycle_pin -q && bash scripts/fetch-rdm-pin.sh tests/integration/lifecycle/rdm.pin >/dev/null - name: Clone Agent-System-RDM with nested pins (reviewed allowlist addition) env: SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} run: | set -euo pipefail # 凭据纪律与 Checkout 块同源:经 GIT_CONFIG_* 环境注入 URL 级 token 头, # 不落文件/命令行;allowlist 扩项 = Agent-System-RDM 及其 pins(同一 # 组件集合),由本 PR 评审背书。SSH pins URL 以 insteadOf 改写到受信 # https 基址后再取。只追加 pin 的三个 40 位 hex,不 source pin 文件。 SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')" scheme="${GITHUB_SERVER_URL%%://*}" export GIT_CONFIG_COUNT=4 export GIT_CONFIG_KEY_0="protocol.$scheme.allow" export GIT_CONFIG_VALUE_0="always" export GIT_CONFIG_KEY_1="http.$SERVER/.extraheader" export GIT_CONFIG_VALUE_1="AUTHORIZATION: token $SUBMODULE_TOKEN" export GIT_CONFIG_KEY_2="url.$SERVER/weichen/.insteadOf" export GIT_CONFIG_VALUE_2="ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/" export GIT_CONFIG_KEY_3="core.hooksPath" export GIT_CONFIG_VALUE_3="/dev/null" git clone --depth=1 "$SERVER/weichen/Agent-System-RDM.git" rdm mapfile -t pins < <(python3 - <<'PY' import pathlib got = {} for line in pathlib.Path("tests/integration/lifecycle/rdm.pin").read_text().splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) if key not in ("sha", "prev_sha", "legacy_sha") or len(value) != 40: raise SystemExit("rdm.pin rejected") if any(c not in "0123456789abcdef" for c in value): raise SystemExit("rdm.pin rejected") got[key] = value if set(got) != {"sha", "prev_sha", "legacy_sha"}: raise SystemExit("rdm.pin rejected") print(got["prev_sha"]) print(got["legacy_sha"]) print(got["sha"]) PY ) [ "${#pins[@]}" -eq 3 ] # depth=1 会把三个 pin 变成互不相关的浅根,update 会把 prev→sha 误判成分叉。 # 当前 first-parent 距离是 39,200 留余量,仍不拉全史。 git -C rdm fetch --depth=200 origin "${pins[@]}" for rev in "${pins[@]}"; do git -C rdm checkout --detach "$rev" git -C rdm submodule update --init --recursive done echo "RDM commit: $(git -C rdm rev-parse HEAD)" # 凭据保留到构建循环内的 submodule update;make 经 without_creds # 剥离,避免 token 进入构建进程。不写凭据文件、不把 token 放进命令行。 without_creds() { env -u GIT_CONFIG_COUNT \ -u GIT_CONFIG_KEY_0 -u GIT_CONFIG_VALUE_0 \ -u GIT_CONFIG_KEY_1 -u GIT_CONFIG_VALUE_1 \ -u GIT_CONFIG_KEY_2 -u GIT_CONFIG_VALUE_2 \ -u GIT_CONFIG_KEY_3 -u GIT_CONFIG_VALUE_3 \ -u SUBMODULE_TOKEN \ "$@" } : > rdm/pin-build-digests.txt for rev in "${pins[@]}"; do git -C rdm checkout --detach "$rev" git -C rdm submodule update --init --recursive # 不能用「有无 go.mod」判定落地:pin 集合混有 Go 仓与非 Go 仓 #(例如前端仓根目录没有 go.mod)。真正要保证的是每个 pins/* # gitlink 子模块 HEAD 等于记录的提交,且工作树已有已跟踪文件—— # 否则 Go 可能静默回退到远端解析。 while read -r mode _type gitlink path; do [ "$mode" = "160000" ] || continue head="$(git -C rdm -C "$path" rev-parse HEAD)" if [ "$head" != "$gitlink" ]; then echo "rdm pin ${rev} 的 ${path} 未落地:HEAD 不等于 gitlink 提交" >&2 exit 1 fi if [ -z "$(git -C rdm -C "$path" ls-files)" ]; then echo "rdm pin ${rev} 的 ${path} 未落地:工作树无已跟踪文件" >&2 exit 1 fi done < <(git -C rdm ls-tree "$rev" pins/) without_creds make -C rdm build sha256sum rdm/bin/agent-system-rdm | awk -v rev="$rev" '{print rev, $1}' >> rdm/pin-build-digests.txt done unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 \ GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1 GIT_CONFIG_KEY_2 \ GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3 unset SUBMODULE_TOKEN - name: Install Node 22 run: | set -euo pipefail curl -fsSL -o "$RUNNER_TEMP/node.tar.xz" https://npmmirror.com/mirrors/node/v22.23.3/node-v22.23.3-linux-x64.tar.xz tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz" - name: Build the real agent-system binary run: make build - name: Real-combination lifecycle gate (make test-lifecycle, required) env: AGENT_PG_TEST_DSN: postgres://ci_agent:ci_job_local_only@127.0.0.1:5432/agent_test?sslmode=disable SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} run: | set -euo pipefail # 路径变量在脚本内解析(YAML env: 不做 shell 展开)。 # 不设 SVC_RDM_ORIGIN:全轨 git 部署由 svc-realcombo 作业覆盖。 # pins 物化没有 SSH known_hosts。token 只经 SVC_CI_GIT_TOKEN 进 # ci_git_env 的 URL 级头,不写文件。 export SVC_TEST_REAL=1 SVC_TEST_PG=1 export SVC_REAL_BIN="$PWD/bin/agent-system" export SVC_REAL_RDM_BIN="$PWD/rdm/bin/agent-system-rdm" export SVC_RDM_SRC_DIR="$PWD/rdm" export SVC_CI_GIT_SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')" export SVC_CI_GIT_TOKEN="$SUBMODULE_TOKEN" unset SUBMODULE_TOKEN make test-lifecycle # U3(统一部署方案):真实 Dev/RDM 制品的 svc.sh 生命周期门禁——桩套件 # (test_svc*.py)只证明脚本逻辑;本作业证明「声明≠能力」的最后一道:真实 # identity 领养/verify、release 切换、伴生工作台、RDM 全轨部署。缺件即失败, # 不静默跳过(SVC_TEST_REAL=1 硬开关)。 svc-realcombo: # 分级门禁:base 非 main 的 PR(L2 child → L1 initiative)只跑轻量 CI(test 作业); # 进入 main 的 PR 与 main 推送跑全量门禁。 if: ${{ github.event_name != 'pull_request' || github.base_ref == 'main' }} runs-on: ubuntu-latest env: GOPROXY: https://goproxy.cn,direct steps: - name: Checkout env: TOKEN: ${{ secrets.GITHUB_TOKEN }} SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} run: | set -euo pipefail # Credential handling stays inline: no file from the candidate is executed. # Workflow provenance and runner isolation still require platform controls. python3 - <<'PY_CHECKOUT' import os from pathlib import Path import re import subprocess import sys from urllib.parse import urlsplit # 服务端基址取运行器注入的 GITHUB_SERVER_URL(平台控制、候选 PR 不可影响)。 # 2026-10-05 修复(run 948/949 实测):注入值为内网直连形态 # 'http://172.17.0.1:3000'(docker 网关 → 宿主 Gitea),与公网拼写不同且为 # http——此前硬编码常量严格比对会拒绝合法检出。校验收宽为「http(s) + 非空 # netloc」;凭据仍只附着于「该基址 + 下方仓库白名单」构造的 URL。 SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") parsed_server = urlsplit(SERVER) if parsed_server.scheme not in {"http", "https"} or not parsed_server.netloc: sys.exit("Checkout server must be the platform-injected http(s) URL") SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/" # Reviewed allowlist, independent of candidate .gitmodules. Add repositories # here only with an explicit review of the checkout credential's scope. REPOSITORIES = { "modules/core": "Agent-System-Core", "modules/backend": "Agent-System-Backend", "modules/frontend": "Agent-System-Frontend", "modules/ui": "Agent-System-UI", "modules/documents": "Agent-System-Documents", "deps/agent-system-core": "Agent-System-Core", "deps/agent-document-renderer": "Agent-Document-Renderer", "deps/development-guidelines": "Development-Guidelines", "deps/agent-native": "Agent-Native", "deps/agent-squad": "Agent-Squad", "deps/agent-intent": "Agent-Intent", "deps/agent-command": "Agent-Command", "deps/error-code-system": "Error-Code-System", "deps/logging-system": "Logging-System", "deps/development-manager": "Development-Manager", "development/guidelines": "Development-Guidelines", } root_token = os.environ.pop("TOKEN", "") component_token = os.environ.pop("SUBMODULE_TOKEN", "") if not root_token or not component_token: sys.exit("Checkout requires TOKEN and SUBMODULE_TOKEN") if os.environ.get("GITHUB_REPOSITORY") != "weichen/Agent-System-Dev": sys.exit("Checkout repository is outside the reviewed allowlist") sha = os.environ.get("GITHUB_SHA", "") if not re.fullmatch(r"[0-9a-fA-F]{40}", sha): sys.exit("Checkout requires a fixed commit SHA") # Discard inherited Git tracing/config/SSH hooks. Auth lives only in one # Git process tree, in an exact URL-scoped header, never in a URL or file. clean_env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")} clean_env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0") config = { "core.hooksPath": "/dev/null", "core.fsmonitor": "false", "init.templateDir": "", "credential.helper": "", "fetch.recurseSubmodules": "false", "submodule.recurse": "false", "http.followRedirects": "false", "protocol.allow": "never", "protocol." + urlsplit(SERVER).scheme + ".allow": "always", } def git(repo, *args, extra=None): values = config | (extra or {}) env = clean_env | {"GIT_CONFIG_COUNT": str(len(values))} for index, (key, value) in enumerate(values.items()): env[f"GIT_CONFIG_KEY_{index}"] = key env[f"GIT_CONFIG_VALUE_{index}"] = value return subprocess.run(["git", "-C", str(repo), *args], env=env, check=True, stdout=subprocess.PIPE).stdout def materialize(repo, depth=0): if depth > 16: raise ValueError("Submodule depth exceeds the reviewed checkout limit") links = {} for entry in git(repo, "ls-tree", "-rz", "HEAD").split(b"\0"): if not entry: continue metadata, path = entry.split(b"\t", 1) mode, kind, commit = metadata.decode().split() if mode == "160000": links[path.decode()] = commit if not links: return raw = git(repo, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list") entries = {} for item in raw.split(b"\0"): if not item: continue key, value = item.decode().split("\n", 1) match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key) if not match: raise ValueError("Unsupported candidate .gitmodules setting") name, field = match.groups() if field in entries.setdefault(name, {}): raise ValueError("Duplicate candidate .gitmodules setting") entries[name][field] = value if set(entries) != set(links): raise ValueError("Submodule names must match the fixed gitlink paths") # Validate the entire level before any authenticated component fetch. for name, fields in entries.items(): repository = REPOSITORIES.get(name) if not repository or fields.get("path") != name: raise ValueError("Submodule path is outside the reviewed allowlist") trusted_url = f"{SERVER}/weichen/{repository}.git" if fields.get("url") not in {SSH + repository + ".git", trusted_url}: raise ValueError("Submodule URL is outside the reviewed allowlist") for name in entries: url = f"{SERVER}/weichen/{REPOSITORIES[name]}.git" # Persist only the reviewed public URL so Git recognizes the # initialized submodule after process-scoped config is gone. git(repo, "config", f"submodule.{name}.url", url) git(repo, "submodule", "update", "--init", "--checkout", "--", name, extra={f"submodule.{name}.url": url, f"submodule.{name}.update": "checkout", f"http.{url}.extraheader": "AUTHORIZATION: token " + component_token}) child = repo / name if git(child, "rev-parse", "HEAD").decode().strip() != links[name]: raise ValueError("Materialized submodule does not match the fixed SHA") materialize(child, depth + 1) try: root = Path.cwd() # An ephemeral clean checkout is required; do not reuse prior Git state. if (root / ".git").exists(): raise ValueError("Checkout requires a fresh workspace") git(root, "init", "-q") url = SERVER + "/weichen/Agent-System-Dev.git" git(root, "remote", "add", "origin", url) git(root, "fetch", "--depth=1", "origin", sha, extra={f"http.{url}.extraheader": "AUTHORIZATION: token " + root_token}) git(root, "checkout", "-q", "--detach", "FETCH_HEAD") materialize(root) except (ValueError, OSError, subprocess.CalledProcessError) as error: sys.exit(f"Checkout refused: {error}") print("Checkout complete; credentials were not persisted in Git configuration") PY_CHECKOUT unset TOKEN SUBMODULE_TOKEN - name: Install Go 1.27.1 run: | set -euo pipefail curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz" ln -sfn /usr/local/go/bin/go /usr/local/bin/go ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt - name: Install Node 22 run: | set -euo pipefail curl -fsSL -o "$RUNNER_TEMP/node.tar.xz" https://npmmirror.com/mirrors/node/v22.23.3/node-v22.23.3-linux-x64.tar.xz tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz" - name: Build the real agent-system binary run: make build - name: Clone Agent-System-RDM with nested pins (reviewed allowlist addition) env: SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} run: | set -euo pipefail # 凭据纪律与 Checkout 块同源:经 GIT_CONFIG_* 环境注入 URL 级 token 头, # 不落文件/命令行;allowlist 扩项 = Agent-System-RDM 及其 pins(同一 # 组件集合),由本 PR 评审背书。SSH pins URL 以 insteadOf 改写到受信 # https 基址后再取。 SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')" scheme="${GITHUB_SERVER_URL%%://*}" export GIT_CONFIG_COUNT=4 export GIT_CONFIG_KEY_0="protocol.$scheme.allow" export GIT_CONFIG_VALUE_0="always" export GIT_CONFIG_KEY_1="http.$SERVER/.extraheader" export GIT_CONFIG_VALUE_1="AUTHORIZATION: token $SUBMODULE_TOKEN" export GIT_CONFIG_KEY_2="url.$SERVER/weichen/.insteadOf" export GIT_CONFIG_VALUE_2="ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/" export GIT_CONFIG_KEY_3="core.hooksPath" export GIT_CONFIG_VALUE_3="/dev/null" git clone --depth=1 "$SERVER/weichen/Agent-System-RDM.git" rdm # 凭据环境须保持到 pins 子模块取完(insteadOf + token 头覆盖嵌套 SSH URL), # 之后再 unset。 git -C rdm submodule update --init --recursive echo "RDM commit: $(git -C rdm rev-parse HEAD)" unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 \ GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1 GIT_CONFIG_KEY_2 \ GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3 unset SUBMODULE_TOKEN make -C rdm build - name: Run real-combination lifecycle gates (SQLite, U3, required) env: SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} run: | set -euo pipefail # 路径变量在脚本内解析(YAML env: 不做 shell 展开)。 export SVC_TEST_REAL=1 export SVC_REAL_BIN="$PWD/bin/agent-system" export SVC_REAL_RDM_BIN="$PWD/rdm/bin/agent-system-rdm" export SVC_RDM_ORIGIN="$PWD/rdm" # CI 无 SSH key:把受信基址与 URL 级 token 头经环境交给套件(仅环境传递)。 export SVC_CI_GIT_SERVER="${GITHUB_SERVER_URL%/}" export SVC_CI_GIT_TOKEN="${SUBMODULE_TOKEN}" python3 -m unittest tests.test_svc_realcombo -v