diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..df527cb --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,707 @@ +name: ci +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + test: + runs-on: ubuntu-latest + env: + GOPROXY: https://goproxy.cn,direct + GOTOOLCHAIN: local + GOWORK: "off" + GOPRIVATE: git-ssh.u1680832.nyat.app/weichen + steps: + - name: Checkout + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} + run: &trusted_acquisition_1 | + set -euo pipefail + python3 - <<'PY_TRUSTED' + # Trusted workflow code only: never import or execute checkout scripts here. + # Token scopes, workflow provenance and runner isolation are platform NOT_RUN. + import json + import os + from pathlib import Path + import re + import shlex + import subprocess + import sys + import tempfile + from urllib.parse import urlsplit + + SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") + parsed = urlsplit(SERVER) + if (parsed.scheme not in {"http", "https"} or not parsed.netloc or + parsed.username or parsed.password or parsed.query or parsed.fragment): + sys.exit("Acquisition requires the platform-injected http(s) server") + SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/" + GO_PREFIX = "git-ssh.u1680832.nyat.app/weichen/" + REPOS = {"Agent-System-Dev", "Logging-System", "Error-Code-System", "Agent-System-Core", "Agent-Native", + "Agent-Intent", "Agent-Command", "Agent-Squad", "Agent-System-Backend", + "Agent-System-Frontend", "Agent-System-UI", "Agent-System-RDM", + "Development-Guidelines", "Agent-System-Documents"} + LEGACY = { + "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/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", "pins/agent-system-backend": "Agent-System-Backend", + "pins/agent-system-core": "Agent-System-Core", "pins/agent-system-frontend": "Agent-System-Frontend", + "pins/agent-system-ui": "Agent-System-UI", + } + GO_REPOS = REPOS - {"Agent-System-Dev", "Agent-System-UI", "Agent-System-Frontend", "Development-Guidelines", "Agent-System-Documents"} + GO_MODULES = {GO_PREFIX + repo + ".git" for repo in GO_REPOS} + GO_MODULES |= {GO_PREFIX + "Agent-Squad.git/" + suffix for suffix in + ("adapters/native", "adapters/intent", "adapters/sqlite", "adapters/a2a", "cmd/agent-squad")} + root_token = os.environ.pop("TOKEN", "") + component_token = os.environ.pop("SUBMODULE_TOKEN", "") + # Go environment files and inherited Git tracing/config must not change acquisition. + clean = {k: v for k, v in os.environ.items() + if not k.startswith(("GIT_", "NPM_CONFIG_", "npm_config_")) and + not any(word in k.upper() for word in ("TOKEN", "PASSWORD", "SECRET"))} + clean.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null", + GIT_TERMINAL_PROMPT="0", GOENV="off", GOTOOLCHAIN="local", GOWORK="off", GOFLAGS="-mod=readonly", + GOPRIVATE=GO_PREFIX.rstrip("/"), GONOPROXY=GO_PREFIX.rstrip("/"), GONOSUMDB=GO_PREFIX.rstrip("/")) + if os.environ.get("npm_config_cache"): + clean["npm_config_cache"] = os.environ["npm_config_cache"] + 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." + parsed.scheme + ".allow": "always"} + root = Path.cwd() + mirrors = root / ".ci-mirrors" + + def process_env(values): + env = clean | {"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 env + + def git(directory, *args, token="", repo=""): + values = config.copy() + if token: + if repo not in REPOS: + raise ValueError("Credential destination is outside the reviewed repository allowlist") + values[f"http.{SERVER}/weichen/{repo}.git.extraheader"] = "AUTHORIZATION: token " + token + result = subprocess.run(["git", "-C", str(directory), *args], env=process_env(values), + check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return result.stdout + + def fixed_sha(value): + if not isinstance(value, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", value): + raise ValueError("Source acquisition requires a fixed commit SHA") + return value.lower() + + def mirror(repo, sha, token): + sha = fixed_sha(sha) + destination = mirrors / (repo + ".git") + if destination.is_symlink() or mirrors.is_symlink(): + raise ValueError("Mirror directory cannot be a symlink") + mirrors.mkdir(exist_ok=True) + if not destination.exists(): + destination.mkdir() + git(destination, "init", "--bare", "-q") + url = f"{SERVER}/weichen/{repo}.git" + try: + git(destination, "cat-file", "-e", sha + "^{commit}") + except subprocess.CalledProcessError: + git(destination, "fetch", "--depth=200", url, sha, token=token, repo=repo) + git(destination, "update-ref", "refs/heads/main", sha) + git(destination, "symbolic-ref", "HEAD", "refs/heads/main") + git(destination, "update-ref", "refs/ci/" + sha, sha) + return destination + + def mirror_env(): + values = {"protocol.file.allow": "always", "core.hooksPath": "/dev/null", "credential.helper": ""} + # Distinct keys use repeated .insteadOf values, represented as a sequence below. + pairs = list(values.items()) + for repo in sorted(REPOS): + destination = mirrors / (repo + ".git") + if not destination.exists(): + continue + key = "url." + destination.as_uri() + ".insteadOf" + pairs.extend((key, url) for url in (SSH + repo + ".git", "https://" + GO_PREFIX + repo + ".git", + SERVER + "/weichen/" + repo + ".git", "https://" + GO_PREFIX + repo)) + env = clean | {"CI_DEPENDENCY_MIRRORS": str(mirrors), "GIT_CONFIG_COUNT": str(len(pairs))} + for index, (key, value) in enumerate(pairs): + env[f"GIT_CONFIG_KEY_{index}"] = key + env[f"GIT_CONFIG_VALUE_{index}"] = value + exports = {k: v for k, v in env.items() if k.startswith("GIT_CONFIG_") or k == "CI_DEPENDENCY_MIRRORS"} + (root / ".ci-mirrors.env").write_text("".join("export " + k + "=" + shlex.quote(v) + "\n" for k, v in exports.items())) + return env + + def materialize_legacy(directory, depth=0): + if depth > 16: + raise ValueError("Historical submodule depth exceeds the checkout limit") + links = {} + for entry in git(directory, "ls-tree", "-rz", "HEAD").split(b"\0"): + if entry: + 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(directory, "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 fixed gitlink paths") + for name, fields in entries.items(): + repo = LEGACY.get(name) + if not repo or fields.get("path") != name or fields.get("url") not in { + SSH + repo + ".git", f"{SERVER}/weichen/{repo}.git"}: + raise ValueError("Historical submodule source is outside the reviewed allowlist") + for name in entries: + repo = LEGACY[name] + sha = links[name] + url = f"{SERVER}/weichen/{repo}.git" + git(directory, "config", f"submodule.{name}.url", url) + # Trusted explicit checkout prevents candidate update commands from running. + git(directory, "submodule", "update", "--init", "--checkout", "--", name, token=component_token, repo=repo) + child = directory / name + if git(child, "rev-parse", "HEAD").decode().strip() != sha: + raise ValueError("Historical submodule does not match its fixed SHA") + mirror(repo, sha, component_token) + materialize_legacy(child, depth + 1) + + def materialize_manifest(directory): + manifest = directory / "components.json" + lock = directory / "components.lock.json" + if not manifest.exists() and not lock.exists(): + materialize_legacy(directory) + return + data = json.loads(manifest.read_text()) + locked = json.loads(lock.read_text()) + if data.get("schema") != 1 or locked.get("schema") != 1: + raise ValueError("Unsupported source manifest schema") + entries = {} + for item in data["components"]: + repo = item["id"] + path = ".workspace/" + repo + if (repo not in REPOS or repo in entries or item["path"] != path or + item["repo"] not in {SSH + repo + ".git", f"{SERVER}/weichen/{repo}.git"} or + item.get("role") not in {"go-library", "npm-library", "application", "guidelines", "documentation"}): + raise ValueError("Candidate source manifest is outside the reviewed allowlist") + for module in item.get("modules", []): + if module.get("path") not in GO_MODULES or not module["path"].startswith(GO_PREFIX + repo + ".git"): + raise ValueError("Candidate module mapping is outside the reviewed allowlist") + entries[repo] = item + selected = locked["components"] + if set(selected) - set(entries): + raise ValueError("Source lock refers to an unregistered repository") + for repo, item in entries.items(): + if item["role"] in {"application", "guidelines", "documentation"} and repo not in selected: + raise ValueError("Application and documentation source needs a fixed lock") + for repo, pin in selected.items(): + fixed_sha(pin["commit"]) + destination = directory / entries[repo]["path"] + if destination.exists() or destination.is_symlink() or destination.parent.is_symlink(): + raise ValueError("Source materialization requires a fresh destination") + # Validate the complete lock before any authenticated component fetch. + for repo, pin in selected.items(): + sha = fixed_sha(pin["commit"]) + destination = directory / entries[repo]["path"] + destination.mkdir(parents=True) + url = f"{SERVER}/weichen/{repo}.git" + git(destination, "init", "-q") + git(destination, "remote", "add", "origin", url) + git(destination, "fetch", "--depth=1", "origin", sha, token=component_token, repo=repo) + git(destination, "checkout", "-q", "--detach", "FETCH_HEAD") + if git(destination, "rev-parse", "HEAD").decode().strip() != sha: + raise ValueError("Materialized source differs from the fixed lock") + # Persist the registered identity; process config chooses the transport. + git(destination, "remote", "set-url", "origin", entries[repo]["repo"]) + mirror(repo, sha, component_token) + materialize_legacy(destination) + + try: + if os.environ.get("GITHUB_REPOSITORY") != "weichen/Agent-System-Core": + raise ValueError("Checkout repository is outside the reviewed allowlist") + sha = fixed_sha(os.environ.get("GITHUB_SHA", "")) + if not root_token: + raise ValueError("Checkout requires TOKEN") + if (root / ".git").exists(): + raise ValueError("Checkout requires a fresh workspace") + git(root, "init", "-q") + git(root, "remote", "add", "origin", SERVER + "/weichen/Agent-System-Core.git") + git(root, "fetch", "--depth=1", "origin", sha, token=root_token, repo="Agent-System-Core") + git(root, "checkout", "-q", "--detach", "FETCH_HEAD") + if git(root, "rev-parse", "HEAD").decode().strip() != sha: + raise ValueError("Checked-out commit differs from the platform commit") + if (root / ".ci-mirrors").exists() or (root / ".ci-mirrors.env").exists(): + raise ValueError("Candidate cannot provide trusted prefetch state") + materialize_manifest(root) + mirror_env() + except (ValueError, KeyError, OSError, subprocess.CalledProcessError) as error: + # Do not print child stderr or environment: a failed credential fetch stays redacted. + sys.exit("Checkout refused: " + str(error)) + print("Fixed source checkout complete; credentials were not persisted") + PY_TRUSTED + 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: Prefetch Go modules without executing candidate code + env: + SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} + run: &trusted_acquisition_2 | + set -euo pipefail + python3 - <<'PY_TRUSTED' + # Trusted workflow code only: never import or execute checkout scripts here. + # Token scopes, workflow provenance and runner isolation are platform NOT_RUN. + import json + import os + from pathlib import Path + import re + import shlex + import subprocess + import sys + import tempfile + from urllib.parse import urlsplit + + SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") + parsed = urlsplit(SERVER) + if (parsed.scheme not in {"http", "https"} or not parsed.netloc or + parsed.username or parsed.password or parsed.query or parsed.fragment): + sys.exit("Acquisition requires the platform-injected http(s) server") + SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/" + GO_PREFIX = "git-ssh.u1680832.nyat.app/weichen/" + REPOS = {"Agent-System-Dev", "Logging-System", "Error-Code-System", "Agent-System-Core", "Agent-Native", + "Agent-Intent", "Agent-Command", "Agent-Squad", "Agent-System-Backend", + "Agent-System-Frontend", "Agent-System-UI", "Agent-System-RDM", + "Development-Guidelines", "Agent-System-Documents"} + LEGACY = { + "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/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", "pins/agent-system-backend": "Agent-System-Backend", + "pins/agent-system-core": "Agent-System-Core", "pins/agent-system-frontend": "Agent-System-Frontend", + "pins/agent-system-ui": "Agent-System-UI", + } + GO_REPOS = REPOS - {"Agent-System-Dev", "Agent-System-UI", "Agent-System-Frontend", "Development-Guidelines", "Agent-System-Documents"} + GO_MODULES = {GO_PREFIX + repo + ".git" for repo in GO_REPOS} + GO_MODULES |= {GO_PREFIX + "Agent-Squad.git/" + suffix for suffix in + ("adapters/native", "adapters/intent", "adapters/sqlite", "adapters/a2a", "cmd/agent-squad")} + root_token = os.environ.pop("TOKEN", "") + component_token = os.environ.pop("SUBMODULE_TOKEN", "") + # Go environment files and inherited Git tracing/config must not change acquisition. + clean = {k: v for k, v in os.environ.items() + if not k.startswith(("GIT_", "NPM_CONFIG_", "npm_config_")) and + not any(word in k.upper() for word in ("TOKEN", "PASSWORD", "SECRET"))} + clean.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null", + GIT_TERMINAL_PROMPT="0", GOENV="off", GOTOOLCHAIN="local", GOWORK="off", GOFLAGS="-mod=readonly", + GOPRIVATE=GO_PREFIX.rstrip("/"), GONOPROXY=GO_PREFIX.rstrip("/"), GONOSUMDB=GO_PREFIX.rstrip("/")) + if os.environ.get("npm_config_cache"): + clean["npm_config_cache"] = os.environ["npm_config_cache"] + 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." + parsed.scheme + ".allow": "always"} + root = Path.cwd() + mirrors = root / ".ci-mirrors" + + def process_env(values): + env = clean | {"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 env + + def git(directory, *args, token="", repo=""): + values = config.copy() + if token: + if repo not in REPOS: + raise ValueError("Credential destination is outside the reviewed repository allowlist") + values[f"http.{SERVER}/weichen/{repo}.git.extraheader"] = "AUTHORIZATION: token " + token + result = subprocess.run(["git", "-C", str(directory), *args], env=process_env(values), + check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return result.stdout + + def fixed_sha(value): + if not isinstance(value, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", value): + raise ValueError("Source acquisition requires a fixed commit SHA") + return value.lower() + + def mirror(repo, sha, token): + sha = fixed_sha(sha) + destination = mirrors / (repo + ".git") + if destination.is_symlink() or mirrors.is_symlink(): + raise ValueError("Mirror directory cannot be a symlink") + mirrors.mkdir(exist_ok=True) + if not destination.exists(): + destination.mkdir() + git(destination, "init", "--bare", "-q") + url = f"{SERVER}/weichen/{repo}.git" + try: + git(destination, "cat-file", "-e", sha + "^{commit}") + except subprocess.CalledProcessError: + git(destination, "fetch", "--depth=200", url, sha, token=token, repo=repo) + git(destination, "update-ref", "refs/heads/main", sha) + git(destination, "symbolic-ref", "HEAD", "refs/heads/main") + git(destination, "update-ref", "refs/ci/" + sha, sha) + return destination + + def mirror_env(): + values = {"protocol.file.allow": "always", "core.hooksPath": "/dev/null", "credential.helper": ""} + # Distinct keys use repeated .insteadOf values, represented as a sequence below. + pairs = list(values.items()) + for repo in sorted(REPOS): + destination = mirrors / (repo + ".git") + if not destination.exists(): + continue + key = "url." + destination.as_uri() + ".insteadOf" + pairs.extend((key, url) for url in (SSH + repo + ".git", "https://" + GO_PREFIX + repo + ".git", + SERVER + "/weichen/" + repo + ".git", "https://" + GO_PREFIX + repo)) + env = clean | {"CI_DEPENDENCY_MIRRORS": str(mirrors), "GIT_CONFIG_COUNT": str(len(pairs))} + for index, (key, value) in enumerate(pairs): + env[f"GIT_CONFIG_KEY_{index}"] = key + env[f"GIT_CONFIG_VALUE_{index}"] = value + exports = {k: v for k, v in env.items() if k.startswith("GIT_CONFIG_") or k == "CI_DEPENDENCY_MIRRORS"} + (root / ".ci-mirrors.env").write_text("".join("export " + k + "=" + shlex.quote(v) + "\n" for k, v in exports.items())) + return env + + def source_roots(): + bases = [root] + composition = root / ".ci-composition-dev" + if composition.is_dir() and not composition.is_symlink(): + bases.append(composition) + paths = [] + for base in bases: + paths += [base, base / "rdm"] + [base / path for path in LEGACY] + [base / ".workspace" / repo for repo in sorted(REPOS)] + return [path for path in paths if path.is_dir() and not path.is_symlink()] + try: + targets = [] + for source in source_roots(): + for suffix in (".", "examples/trace", "adapters/native", "adapters/intent", "adapters/sqlite", "adapters/a2a", "cmd/agent-squad"): + directory = source / suffix + if (directory / "go.mod").is_file() and directory.resolve() not in targets: + targets.append(directory.resolve()) + # Parse using the installed local Go tool without credentials or candidate execution. + original_files = {} + for directory in targets: + parsed_mod = json.loads(subprocess.check_output(["go", "mod", "edit", "-json"], cwd=directory, env=clean)) + canonical = parsed_mod["Module"]["Path"].startswith(GO_PREFIX) + for name in ("go.mod", "go.sum"): + dependency_file = directory / name + if dependency_file.is_file(): + original_files[(directory, name)] = dependency_file.read_bytes() + elif canonical and name == "go.sum": + original_files[(directory, name)] = None + for dep in (parsed_mod.get("Require") or []): + path = dep["Path"] + if path.startswith(GO_PREFIX) and path not in GO_MODULES: + raise ValueError("Go dependency is outside the reviewed module mapping") + for replacement in (parsed_mod.get("Replace") or []): + path = replacement["New"]["Path"] + if canonical and not replacement["New"].get("Version"): + raise ValueError("Canonical module cannot acquire a local path replacement") + if path.startswith(GO_PREFIX) and path not in GO_MODULES: + raise ValueError("Go replacement is outside the reviewed module mapping") + auth = config.copy() + for repo in sorted(GO_REPOS): + url = f"{SERVER}/weichen/{repo}.git" + auth[f"url.{url}.insteadOf"] = "https://" + GO_PREFIX + repo + auth[f"http.{url}.extraheader"] = "AUTHORIZATION: token " + component_token + def objects(output): + decoder = json.JSONDecoder() + remaining = output.strip() + while remaining: + item, end = decoder.raw_decode(remaining) + remaining = remaining[end:].lstrip() + yield item + def unchanged(): + for (owner, name), contents in original_files.items(): + dependency_file = owner / name + current = dependency_file.read_bytes() if dependency_file.exists() else None + if current != contents: + raise ValueError("Go acquisition modified a committed Go dependency file: " + str(dependency_file)) + for directory in targets: + # Resolve private go.mod metadata only through exact repository headers; + # validate the complete selected graph before acquiring module zip files. + graph = subprocess.run(["go", "list", "-mod=readonly", "-m", "-json", "all"], cwd=directory, + env=process_env(auth), capture_output=True, text=True) + if graph.returncode: + raise ValueError("Go selected graph resolution failed: " + graph.stderr.strip()) + for item in objects(graph.stdout): + if item.get("Main"): + continue + selected = item.get("Replace") or item + for path in (item.get("Path", ""), selected.get("Path", "")): + if path.startswith(GO_PREFIX) and path not in GO_MODULES: + raise ValueError("Selected Go module is outside the reviewed mapping") + unchanged() + # Go 1.27 can prune go.sum even under -mod=readonly for this + # metadata-only command. Use a disposable copy, then validate actual + # package loading against the unchanged committed source locks. + local_replacements = json.loads(subprocess.check_output( + ["go", "mod", "edit", "-json"], cwd=directory, env=clean)).get("Replace") or [] + downloaded = "" + if not any(not item["New"].get("Version") for item in local_replacements): + with tempfile.TemporaryDirectory(prefix="ci-go-prefetch-") as temporary: + download_root = Path(temporary) + for name in ("go.mod", "go.sum"): + if (directory / name).is_file(): + (download_root / name).write_bytes((directory / name).read_bytes()) + result = subprocess.run(["go", "mod", "download", "-json"], cwd=download_root, + env=process_env(auth), capture_output=True, text=True) + if result.returncode: + raise ValueError("Go module acquisition failed: " + result.stderr.strip()) + downloaded = result.stdout + unchanged() + for item in objects(downloaded): + path = item.get("Path", "") + if path.startswith(GO_PREFIX): + if path not in GO_MODULES: + raise ValueError("Resolved Go module is outside the reviewed mapping") + repo = path[len(GO_PREFIX):].split(".git", 1)[0] + origin = item.get("Origin") or json.loads(Path(item["Info"]).read_text()).get("Origin") or {} + mirror(repo, fixed_sha(origin.get("Hash")), component_token) + # Package metadata loads actual test/build dependencies without running + # vet, compilers, package init functions or candidate tests. No CGO tools. + packages = subprocess.run(["go", "list", "-mod=readonly", "-deps", "-test", "./..."], cwd=directory, + env=process_env(auth) | {"CGO_ENABLED": "0"}, capture_output=True, text=True) + if packages.returncode: + raise ValueError("Go test/build dependency acquisition failed: " + packages.stderr.strip()) + unchanged() + mirror_env() + except (ValueError, OSError, subprocess.CalledProcessError) as error: + sys.exit("Go prefetch refused: " + str(error)) + print("Go modules cached with GOTOOLCHAIN=local; no candidate code executed") + PY_TRUSTED + unset TOKEN SUBMODULE_TOKEN + - name: Vet, test, and race + run: | + set -euo pipefail + source .ci-mirrors.env + GOWORK=off go vet -mod=readonly ./... && GOWORK=off go test -mod=readonly -count=1 ./... && GOWORK=off go test -mod=readonly -count=1 -race ./... + - 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: Prefetch npm packages without lifecycle scripts + env: + SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} + run: &trusted_acquisition_3 | + set -euo pipefail + python3 - <<'PY_TRUSTED' + # Trusted workflow code only: never import or execute checkout scripts here. + # Token scopes, workflow provenance and runner isolation are platform NOT_RUN. + import json + import os + from pathlib import Path + import re + import shlex + import subprocess + import sys + import tempfile + from urllib.parse import urlsplit + + SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/") + parsed = urlsplit(SERVER) + if (parsed.scheme not in {"http", "https"} or not parsed.netloc or + parsed.username or parsed.password or parsed.query or parsed.fragment): + sys.exit("Acquisition requires the platform-injected http(s) server") + SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/" + GO_PREFIX = "git-ssh.u1680832.nyat.app/weichen/" + REPOS = {"Agent-System-Dev", "Logging-System", "Error-Code-System", "Agent-System-Core", "Agent-Native", + "Agent-Intent", "Agent-Command", "Agent-Squad", "Agent-System-Backend", + "Agent-System-Frontend", "Agent-System-UI", "Agent-System-RDM", + "Development-Guidelines", "Agent-System-Documents"} + LEGACY = { + "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/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", "pins/agent-system-backend": "Agent-System-Backend", + "pins/agent-system-core": "Agent-System-Core", "pins/agent-system-frontend": "Agent-System-Frontend", + "pins/agent-system-ui": "Agent-System-UI", + } + GO_REPOS = REPOS - {"Agent-System-Dev", "Agent-System-UI", "Agent-System-Frontend", "Development-Guidelines", "Agent-System-Documents"} + GO_MODULES = {GO_PREFIX + repo + ".git" for repo in GO_REPOS} + GO_MODULES |= {GO_PREFIX + "Agent-Squad.git/" + suffix for suffix in + ("adapters/native", "adapters/intent", "adapters/sqlite", "adapters/a2a", "cmd/agent-squad")} + root_token = os.environ.pop("TOKEN", "") + component_token = os.environ.pop("SUBMODULE_TOKEN", "") + # Go environment files and inherited Git tracing/config must not change acquisition. + clean = {k: v for k, v in os.environ.items() + if not k.startswith(("GIT_", "NPM_CONFIG_", "npm_config_")) and + not any(word in k.upper() for word in ("TOKEN", "PASSWORD", "SECRET"))} + clean.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null", GIT_CONFIG_GLOBAL="/dev/null", + GIT_TERMINAL_PROMPT="0", GOENV="off", GOTOOLCHAIN="local", GOWORK="off", GOFLAGS="-mod=readonly", + GOPRIVATE=GO_PREFIX.rstrip("/"), GONOPROXY=GO_PREFIX.rstrip("/"), GONOSUMDB=GO_PREFIX.rstrip("/")) + if os.environ.get("npm_config_cache"): + clean["npm_config_cache"] = os.environ["npm_config_cache"] + 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." + parsed.scheme + ".allow": "always"} + root = Path.cwd() + mirrors = root / ".ci-mirrors" + + def process_env(values): + env = clean | {"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 env + + def git(directory, *args, token="", repo=""): + values = config.copy() + if token: + if repo not in REPOS: + raise ValueError("Credential destination is outside the reviewed repository allowlist") + values[f"http.{SERVER}/weichen/{repo}.git.extraheader"] = "AUTHORIZATION: token " + token + result = subprocess.run(["git", "-C", str(directory), *args], env=process_env(values), + check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return result.stdout + + def fixed_sha(value): + if not isinstance(value, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", value): + raise ValueError("Source acquisition requires a fixed commit SHA") + return value.lower() + + def mirror(repo, sha, token): + sha = fixed_sha(sha) + destination = mirrors / (repo + ".git") + if destination.is_symlink() or mirrors.is_symlink(): + raise ValueError("Mirror directory cannot be a symlink") + mirrors.mkdir(exist_ok=True) + if not destination.exists(): + destination.mkdir() + git(destination, "init", "--bare", "-q") + url = f"{SERVER}/weichen/{repo}.git" + try: + git(destination, "cat-file", "-e", sha + "^{commit}") + except subprocess.CalledProcessError: + git(destination, "fetch", "--depth=200", url, sha, token=token, repo=repo) + git(destination, "update-ref", "refs/heads/main", sha) + git(destination, "symbolic-ref", "HEAD", "refs/heads/main") + git(destination, "update-ref", "refs/ci/" + sha, sha) + return destination + + def mirror_env(): + values = {"protocol.file.allow": "always", "core.hooksPath": "/dev/null", "credential.helper": ""} + # Distinct keys use repeated .insteadOf values, represented as a sequence below. + pairs = list(values.items()) + for repo in sorted(REPOS): + destination = mirrors / (repo + ".git") + if not destination.exists(): + continue + key = "url." + destination.as_uri() + ".insteadOf" + pairs.extend((key, url) for url in (SSH + repo + ".git", "https://" + GO_PREFIX + repo + ".git", + SERVER + "/weichen/" + repo + ".git", "https://" + GO_PREFIX + repo)) + env = clean | {"CI_DEPENDENCY_MIRRORS": str(mirrors), "GIT_CONFIG_COUNT": str(len(pairs))} + for index, (key, value) in enumerate(pairs): + env[f"GIT_CONFIG_KEY_{index}"] = key + env[f"GIT_CONFIG_VALUE_{index}"] = value + exports = {k: v for k, v in env.items() if k.startswith("GIT_CONFIG_") or k == "CI_DEPENDENCY_MIRRORS"} + (root / ".ci-mirrors.env").write_text("".join("export " + k + "=" + shlex.quote(v) + "\n" for k, v in exports.items())) + return env + + def source_roots(): + bases = [root] + composition = root / ".ci-composition-dev" + if composition.is_dir() and not composition.is_symlink(): + bases.append(composition) + paths = [] + for base in bases: + paths += [base, base / "rdm"] + [base / path for path in LEGACY] + [base / ".workspace" / repo for repo in sorted(REPOS)] + return [path for path in paths if path.is_dir() and not path.is_symlink()] + def git_package(value): + if not isinstance(value, str): + return None + if value.startswith(("file:", "link:", "workspace:")): + raise ValueError("Formal npm acquisition cannot consume a local package override") + if value.startswith(("http://", "https://")): + registry = urlsplit(value) + if registry.scheme != "https" or registry.hostname != "registry.npmjs.org" or registry.username or registry.password: + raise ValueError("npm tarball source is outside the reviewed public registry") + elif "/" in value and not value.startswith(("git+", "npm:")): + raise ValueError("npm repository shorthand is outside the fixed-SHA package mapping") + if value.startswith("git+") or value.startswith(("git://", "ssh://", "github:", "gitlab:", "bitbucket:")): + for repo in ("Agent-System-Core", "Agent-System-UI"): + prefix = "git+" + SSH + repo + ".git#" + if value.startswith(prefix): + return repo, fixed_sha(value[len(prefix):]) + raise ValueError("npm Git source is outside the reviewed fixed-SHA package mapping") + return None + + try: + targets = [] + selected = set() + for directory in source_roots(): + if not (directory / "package.json").is_file(): + continue + if (directory / ".npmrc").exists(): + raise ValueError("Candidate .npmrc is not allowed during private dependency acquisition") + package = json.loads((directory / "package.json").read_text()) + lock = json.loads((directory / "package-lock.json").read_text()) + if lock.get("lockfileVersion") not in {2, 3}: + raise ValueError("npm acquisition requires a package lock") + for group in ("dependencies", "devDependencies", "optionalDependencies"): + for value in package.get(group, {}).values(): + found = git_package(value) + if found: + selected.add(found) + for entry in lock.get("packages", {}).values(): + found = git_package(entry.get("resolved")) + if found: + selected.add(found) + for group in ("dependencies", "devDependencies", "optionalDependencies"): + for value in entry.get(group, {}).values(): + found = git_package(value) + if found: + selected.add(found) + targets.append(directory) + # Fetch only registered fixed Git package commits. npm never receives token config. + for repo, sha in sorted(selected): + repository = mirror(repo, sha, component_token) + package = json.loads(git(repository, "show", sha + ":package.json")) + forbidden = {"prepare", "prepack", "prepublish", "prepublishOnly", "preinstall", "install", "postinstall"} + if forbidden & set(package.get("scripts", {})): + raise ValueError("Git package provider must commit outputs without acquisition lifecycle scripts") + if not git(repository, "ls-tree", sha, "package/").strip(): + raise ValueError("Git package provider is missing committed package outputs") + no_auth = mirror_env() + empty_config = tempfile.NamedTemporaryFile(prefix="ci-npm-globalrc-") + for directory in targets: + result = subprocess.run(["npm", "ci", "--ignore-scripts", "--no-audit", "--no-fund", + "--userconfig=/dev/null", "--globalconfig=" + empty_config.name], + cwd=directory, env=no_auth, capture_output=True, text=True) + if result.returncode: + raise ValueError("npm dependency acquisition failed: " + result.stderr.strip()) + except (ValueError, OSError, subprocess.CalledProcessError) as error: + sys.exit("npm prefetch refused: " + str(error)) + print("npm packages cached without credentials or lifecycle execution") + PY_TRUSTED + unset TOKEN SUBMODULE_TOKEN + - name: Validate package and contracts without credentials + run: | + set -euo pipefail + source .ci-mirrors.env + npm test + npm run typecheck diff --git a/AGENTS.md b/AGENTS.md index 8a1c1dc..c7276c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Agent 工作说明(Agent-System-Core) -本仓开发遵循 Development-Guidelines(Dev 仓 `deps/development-guidelines`,gitlink 固定版本): +本仓开发遵循 Development-Guidelines(由 Dev 固定规范来源;新布局见 Dev components.lock.json,规则目录 .workspace/Development-Guidelines;历史布局仍读原 gitlink): 核心规则见其 `core/`,按任务触发阅读 `standards/` 专题;日常交付走 `workflows/delivery.md`。 ## 任务启动门禁 @@ -15,7 +15,7 @@ ## 仓内规则 - 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md。 -- 跨仓 gitlink 变更须经组合验证后在 Dev 仓推进;受保护分支经 PR 合入。 +- 跨仓语言依赖/应用源码锁变更须经固定组合验证后在消费方推进;源码提交到所属仓;受保护分支经 PR 合入。 - 验证命令见 PROJECT.md「验证」节;完成声明须附实际命令输出证据。 ## Gitea 与 Git 访问事实(2026-09-30 实测) diff --git a/PROJECT.md b/PROJECT.md index 65ac049..8277438 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -8,6 +8,7 @@ 在本仓根目录执行 `go test ./...` 验证 Go 契约与不变量;纯文档修改执行 `git diff --check` 并核对受影响链接、命令和来源。本仓没有 Makefile。 -修改 `ts/types.ts` 后在 Dev 仓运行 `make contracts` 同步消费者,再以 `make test` -验证固定组件组合;单独运行 Core 测试不能代替提供方与消费者的兼容验证。 +修改 `ts/types.ts` 后显式运行 `npm run build`,核验 `npm run typecheck` +与包内容并提交生成出口。消费者通过固定 `@agent-system/contracts` 版本更新;Dev 的 +`make test` 验证实际固定组合,不再复制契约到消费者源码。单独运行 Core 测试不能代替组合验证。 本仓提供公共契约,不是独立部署服务;当前能力以源码和对应验证记录为准。