#!/usr/bin/env bash set -euo pipefail # 这是一个不依赖额外 cargo 插件的轻量依赖审计:它锁定 workspace 的 # 可解析依赖,并把最敏感的 kernel 反向依赖检查放在独立脚本中,供本地和 # Gitea CI 复用。它不替代带漏洞数据库的 cargo-audit/cargo-deny。 if (($# > 1)); then echo "用法:$0 [workspace/Cargo.toml]" >&2 exit 2 fi # 默认始终检查脚本所属 workspace,而不是把调用者的 cwd 当成 workspace。 # 这样从仓库外执行 `.../check-dependencies.sh` 也会得到同一份 Cargo.lock。 caller_pwd="$(pwd -P)" script_path="${BASH_SOURCE[0]}" case "$script_path" in /*) ;; *) script_path="$caller_pwd/$script_path" ;; esac script_dir="$(cd -- "$(dirname -- "$script_path")" && pwd -P)" script_workspace_root="$(cd -- "$script_dir/.." && pwd -P)" manifest_input="${1:-}" if [[ -z "$manifest_input" ]]; then workspace_manifest="$script_workspace_root/Cargo.toml" elif [[ "$manifest_input" == /* ]]; then workspace_manifest="$manifest_input" elif [[ -f "$caller_pwd/$manifest_input" ]]; then # 保留显式相对路径相对于调用者 cwd 的直觉语义(例如从父仓库传 rust/Cargo.toml)。 workspace_manifest="$caller_pwd/$manifest_input" else # 若调用者 cwd 没有该文件,再把相对路径解释为脚本 workspace 根目录下的路径。 workspace_manifest="$script_workspace_root/$manifest_input" fi manifest_dir="$(cd -- "$(dirname -- "$workspace_manifest")" && pwd -P)" workspace_manifest="$manifest_dir/$(basename -- "$workspace_manifest")" # 显式传入其它 workspace 时,边界检查应以该 manifest 所在目录为根,而不是 # 脚本所属 workspace;这也让复制验收和独立调用保持同一语义。 workspace_root="$manifest_dir" [[ -f "$workspace_manifest" && -r "$workspace_manifest" ]] || { echo "workspace manifest 不存在或不可读:$workspace_manifest" >&2 exit 1 } core_tree="$(cargo tree --locked --manifest-path "$workspace_manifest" --edges normal -p agent-runtime-core)" for forbidden in tokio reqwest rusqlite mcp codex agent-mcp agent-codex; do # 既匹配精确 crate 名,也匹配同前缀的实现 crate(例如 tokio-util)。 if grep -Eiq "(^|[[:space:]])${forbidden}(-[^[:space:]]*)?([[:space:]]|$)" <<<"$core_tree"; then echo "agent-runtime-core unexpectedly depends on ${forbidden} family" >&2 exit 1 fi done # Engine 只编排 Core 中立端口,不能反向依赖持久化控制面或具体适配器。 # 检查完整 normal 依赖树,避免通过中间 crate 间接带入这些职责。 engine_tree="$(cargo tree --locked --manifest-path "$workspace_manifest" --edges normal -p agent-runtime-engine)" for forbidden in rusqlite agent-storage-sqlite agent-runtime agent-runtime-contracts agent-runtime-sqlite agent-host agent-app agent-cli agent-provider-openai agent-provider-fake agent-mcp agent-skills agent-codex; do if grep -Eq "(^|[[:space:]])${forbidden}([[:space:]]|$)" <<<"$engine_tree"; then echo "agent-runtime-engine unexpectedly depends on ${forbidden}" >&2 exit 1 fi done # Durable command/view contracts must remain database and transport neutral too. # The portable runtime facade now shares that boundary; SQLite-specific service # assembly lives in the sibling agent-runtime-sqlite crate. contracts_tree="$(cargo tree --locked --manifest-path "$workspace_manifest" --edges normal -p agent-runtime-contracts)" for forbidden in tokio reqwest rusqlite agent-mcp agent-codex agent-runtime; do if grep -Eq "(^|[[:space:]])${forbidden}([[:space:]]|$)" <<<"$contracts_tree"; then echo "agent-runtime-contracts unexpectedly depends on ${forbidden}" >&2 exit 1 fi done # The portable runtime must stay usable without SQLite. The SQLite-specific # service is intentionally a sibling crate; checking the package in isolation # prevents workspace feature unification from hiding an accidental dependency. portable_runtime_tree="$(cargo tree --locked --manifest-path "$workspace_manifest" \ --no-default-features --edges normal -p agent-runtime)" # Runtime 接受中立命令;Engine 执行和 Provider/MCP/Skill/Codex 装配留在 Host。 for forbidden in rusqlite agent-storage-sqlite agent-runtime-sqlite agent-runtime-engine agent-host agent-app agent-cli agent-provider-openai agent-provider-fake agent-mcp agent-skills agent-codex; do if grep -Eq "(^|[[:space:]])${forbidden}([[:space:]]|$)" <<<"$portable_runtime_tree"; then echo "agent-runtime portable facade unexpectedly depends on ${forbidden}" >&2 exit 1 fi done # Conversely, the SQLite service must visibly depend on both portable runtime # contracts and the concrete storage adapter. This guards the intended # one-way dependency direction while keeping Core/Engine independent. sqlite_runtime_tree="$(cargo tree --locked --manifest-path "$workspace_manifest" \ --edges normal -p agent-runtime-sqlite)" for required in agent-runtime agent-storage-sqlite; do if ! grep -Eq "(^|[[:space:]])${required}([[:space:]]|$)" <<<"$sqlite_runtime_tree"; then echo "agent-runtime-sqlite is missing required dependency ${required}" >&2 exit 1 fi done # SQLite 层可以依赖存储实现,但不能借拆分 Host 把执行器和外部适配器搬进来。 for forbidden in agent-runtime-engine agent-host agent-app agent-cli agent-provider-openai agent-provider-fake agent-mcp agent-skills agent-codex; do if grep -Eq "(^|[[:space:]])${forbidden}([[:space:]]|$)" <<<"$sqlite_runtime_tree"; then echo "agent-runtime-sqlite unexpectedly depends on ${forbidden}" >&2 exit 1 fi done # The generic program configuration layer must remain below Host/Runtime. It # may depend on concrete protocol configuration types, but it must not acquire # durable state, worker lifecycle or the CLI's application backend by accident. app_tree="$(cargo tree --locked --manifest-path "$workspace_manifest" \ --edges normal -p agent-app)" for forbidden in agent-host agent-runtime agent-runtime-sqlite agent-storage-sqlite rusqlite; do if grep -Eq "(^|[[:space:]])${forbidden}([[:space:]]|$)" <<<"$app_tree"; then echo "agent-app unexpectedly depends on ${forbidden}" >&2 exit 1 fi done # 只允许 workspace 内部的 path crate 参与组装。用 cargo metadata 检查 # manifest 路径和无 source 的 path 依赖,避免把当前父仓库路径误当成 # 依赖(cargo tree 的根节点本身会显示这个路径)。 cargo metadata --locked --manifest-path "$workspace_manifest" --format-version 1 \ | WORKSPACE_ROOT="$workspace_root" python3 -c ' import json import os import pathlib import sys root = pathlib.Path(os.environ["WORKSPACE_ROOT"]).resolve() metadata = json.load(sys.stdin) packages = metadata["packages"] workspace_ids = set(metadata["workspace_members"]) names = {package["name"] for package in packages if package["id"] in workspace_ids} for package in packages: if package["id"] not in workspace_ids: continue manifest = pathlib.Path(package["manifest_path"]).resolve() if root not in manifest.parents and manifest != root / "Cargo.toml": raise SystemExit(f"package manifest escapes workspace: {manifest}") for dependency in package["dependencies"]: if dependency["source"] is None and dependency["name"] not in names: package_name = package["name"] dependency_name = dependency["name"] raise SystemExit( f"path dependency is outside workspace: {package_name} -> {dependency_name}" ) ' echo "dependency boundary check passed"