Files
Genarrative/rust/scripts/check-dependencies.sh
T
kdletters 69fc3d8ca0 拆分 Agent Host 运行职责
将 durable 取消、失败收口、审批决议和外部结果对账迁入 RuntimeService

把 Host 工具、MCP、Skill 与外部 backend 桥接拆为私有模块并保持公开 API

补充 Runtime 控制测试、依赖反向门禁和架构验收文档
2026-09-09 19:20:54 +08:00

154 lines
7.5 KiB
Bash
Executable File

#!/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"