69fc3d8ca0
将 durable 取消、失败收口、审批决议和外部结果对账迁入 RuntimeService 把 Host 工具、MCP、Skill 与外部 backend 桥接拆为私有模块并保持公开 API 补充 Runtime 控制测试、依赖反向门禁和架构验收文档
154 lines
7.5 KiB
Bash
Executable File
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"
|