agent-exec transport cf117cc8acfd51c9b3a894e92ce8027f/c2dce0e31099
This commit is contained in:
@@ -52,6 +52,8 @@ jobs:
|
||||
"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", "")
|
||||
@@ -164,6 +166,8 @@ jobs:
|
||||
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: Install Node 22
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -226,6 +230,8 @@ jobs:
|
||||
"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", "")
|
||||
@@ -517,6 +523,8 @@ jobs:
|
||||
"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", "")
|
||||
|
||||
@@ -34,3 +34,6 @@
|
||||
[submodule "deps/agent-command"]
|
||||
path = deps/agent-command
|
||||
url = ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-Command.git
|
||||
[submodule "deps/development-manager"]
|
||||
path = deps/development-manager
|
||||
url = ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Development-Manager.git
|
||||
|
||||
+2
-2
@@ -72,9 +72,9 @@ profiles:
|
||||
integration:
|
||||
exempt_paths: [".task/**", "docs/superpowers/specs/**", "docs/superpowers/plans/**"]
|
||||
transport:
|
||||
command: python3 scripts/agent_exec.py apply
|
||||
command: scripts/agent-exec apply
|
||||
admission:
|
||||
command: python3 scripts/agent_exec.py admit
|
||||
command: scripts/agent-exec admit
|
||||
cleanup:
|
||||
command: make task-finish
|
||||
```
|
||||
|
||||
@@ -32,7 +32,7 @@ export TASK_INITIATIVE
|
||||
|
||||
setup:
|
||||
git config core.hooksPath scripts/hooks
|
||||
chmod +x scripts/hooks/pre-commit scripts/hooks/post-merge scripts/task.sh scripts/evidence.sh scripts/facts-verify.sh
|
||||
chmod +x scripts/hooks/pre-commit scripts/hooks/post-merge scripts/task.sh scripts/evidence.sh scripts/facts-verify.sh scripts/agent-exec
|
||||
@echo "OK: pre-commit/post-merge 钩子已启用(core.hooksPath=scripts/hooks)"
|
||||
|
||||
precheck:
|
||||
@@ -91,7 +91,7 @@ help:
|
||||
@echo "make dev 开发模式说明(前后端分离热更新)"
|
||||
@echo "make pin 推进 submodule 指针到各仓远端 main(提交前须 make test 通过)"
|
||||
@echo "make status submodule 状态"
|
||||
@echo "make agent-exec-validate 校验 AGENT-EXECUTION.md(agent_exec validate)"
|
||||
@echo "make agent-exec-validate 校验 AGENT-EXECUTION.md(scripts/agent-exec validate,经 dm-server agent-exec)"
|
||||
@echo "make agent-exec-apply RECEIPT=<path> 快进当前分支到成功收据的 transport"
|
||||
@echo "make agent-exec-admit 校验暂存路径具备 transport 来源或属于 host 豁免"
|
||||
@echo "make task-triage REQ='<需求>' TRIAGE_RECEIPT=<path> 以远端为准发现接纳中的 initiative 并写分流收据(候选存在时 exit 5,待决策)"
|
||||
@@ -100,14 +100,14 @@ help:
|
||||
@echo "make task-pr-check [PR=N] [FINAL=1] 核验当前任务分支 PR 的 base/head 门禁(远端为准;FINAL=1 用于 initiative 收口)"
|
||||
|
||||
agent-exec-validate:
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 scripts/agent_exec.py validate --repo .
|
||||
scripts/agent-exec validate --repo .
|
||||
|
||||
agent-exec-apply:
|
||||
@if [ -z "$(RECEIPT)" ]; then echo "usage: make agent-exec-apply RECEIPT=<path>"; exit 2; fi
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 scripts/agent_exec.py apply --repo . --receipt "$(RECEIPT)"
|
||||
scripts/agent-exec apply --repo . --receipt "$(RECEIPT)"
|
||||
|
||||
agent-exec-admit:
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 scripts/agent_exec.py admit --repo .
|
||||
scripts/agent-exec admit --repo .
|
||||
|
||||
sync:
|
||||
git submodule update --init --recursive
|
||||
|
||||
+1
Submodule deps/development-manager added at 42e675fec8
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# 共享 Development-Manager 执行器薄入口:核对 deps/development-manager 与 index 钉住的提交一致且无修改,
|
||||
# 构建(或复用缓存的)dm-server 二进制后执行 `dm-server agent-exec`。结果仅为本地完整性证据(SELF)。
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
root=$(cd -- "$script_dir/.." && pwd)
|
||||
sub_path="deps/development-manager"
|
||||
dm="$root/$sub_path"
|
||||
|
||||
die() {
|
||||
echo "agent-exec: $*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# 在 DM checkout 内执行 git;清除 pre-commit 可能继承的仓库环境变量,避免读取超级仓的索引或对象库。
|
||||
dm_git() {
|
||||
env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE git -C "$dm" "$@"
|
||||
}
|
||||
|
||||
index_entry=$(git -C "$root" ls-files -s -- "$sub_path") || index_entry=""
|
||||
if [ -z "$index_entry" ]; then
|
||||
die "index 中不存在 $sub_path 的 gitlink,拒绝执行"
|
||||
fi
|
||||
read -r mode pin _ <<<"$index_entry"
|
||||
if [ "$mode" != "160000" ]; then
|
||||
die "$sub_path 在 index 中不是 gitlink(mode=$mode),拒绝执行"
|
||||
fi
|
||||
if ! [[ "$pin" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
die "$sub_path 的 index 钉住值无效:$pin"
|
||||
fi
|
||||
|
||||
remedy="请运行 git submodule update --init deps/development-manager"
|
||||
if [ ! -e "$dm/.git" ]; then
|
||||
die "$sub_path 未检出;$remedy"
|
||||
fi
|
||||
top=$(dm_git rev-parse --show-toplevel 2>/dev/null) || top=""
|
||||
real_dm=$(cd -- "$dm" && pwd -P)
|
||||
if [ -z "$top" ] || [ "$top" != "$real_dm" ]; then
|
||||
die "$sub_path 不是独立的 checkout;$remedy"
|
||||
fi
|
||||
|
||||
head=$(dm_git rev-parse HEAD 2>/dev/null) || head=""
|
||||
if [ "$head" != "$pin" ]; then
|
||||
die "$sub_path 的 HEAD(${head:-未知})与 index 钉住的 $pin 不一致;$remedy"
|
||||
fi
|
||||
|
||||
dirty=$(dm_git status --porcelain --ignore-submodules=all) || die "无法读取 $sub_path 的工作区状态,拒绝执行"
|
||||
if [ -n "$dirty" ]; then
|
||||
die "$sub_path 存在未提交的修改,拒绝执行"
|
||||
fi
|
||||
|
||||
cache_dir="${AGENT_EXEC_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/agent-system-dev/agent-exec}"
|
||||
bin="$cache_dir/dm-server-$pin"
|
||||
if [ ! -x "$bin" ]; then
|
||||
command -v go >/dev/null 2>&1 || die "未找到 go,无法构建 dm-server"
|
||||
mkdir -p "$cache_dir"
|
||||
tmp=$(mktemp "$cache_dir/.dm-server-$pin.XXXXXX")
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
if ! (cd "$dm" && env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE GOWORK=off go build -trimpath -o "$tmp" ./cmd/dm-server); then
|
||||
die "构建 dm-server 失败"
|
||||
fi
|
||||
chmod 0755 "$tmp"
|
||||
mv -f "$tmp" "$bin"
|
||||
trap - EXIT
|
||||
fi
|
||||
|
||||
exec "$bin" agent-exec "$@"
|
||||
@@ -13,4 +13,4 @@ echo "$branch" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || {
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
# admit 只是本地完整性/篡改证据(SELF):同一 OS 用户可读取本地 HMAC 密钥并绕过本钩子,
|
||||
# 它不是 GATE,也不是可信身份。缺失或无效的本地记录仍拒绝;交付信任来自精确 HEAD 的 CI 或独立 REPLAY。
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 "$root/scripts/agent_exec.py" admit --repo "$root"
|
||||
"$root/scripts/agent-exec" admit --repo "$root"
|
||||
|
||||
@@ -5,7 +5,6 @@ import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -678,29 +677,6 @@ class AgentExecAdmissionTests(unittest.TestCase):
|
||||
self.assertEqual(rejected.returncode, agent_exec.EXIT_BLOCKED, rejected.stderr)
|
||||
self.assertIn("admit: no local integrity record", rejected.stderr)
|
||||
|
||||
def test_precommit_rejects_unproven_paths_after_branch_checks(self) -> None:
|
||||
self._install_project_hook()
|
||||
self._stage("src/extra.txt", "unproven\n")
|
||||
proc = self._hook()
|
||||
combined = proc.stdout + proc.stderr
|
||||
self.assertNotEqual(proc.returncode, 0, combined)
|
||||
self.assertIn("admit: no local integrity record", combined)
|
||||
|
||||
_git(self.repo, "checkout", "-B", "main", self.head)
|
||||
self._stage("src/extra.txt", "unproven\n")
|
||||
blocked = self._hook()
|
||||
text = blocked.stdout + blocked.stderr
|
||||
self.assertNotEqual(blocked.returncode, 0, text)
|
||||
self.assertIn("集成分支", text)
|
||||
self.assertNotIn("admit:", text)
|
||||
|
||||
def test_precommit_accepts_host_managed_exemptions(self) -> None:
|
||||
self._install_project_hook()
|
||||
self._stage("docs/superpowers/plans/admission.md", "plan\n")
|
||||
proc = self._hook()
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr + proc.stdout)
|
||||
self.assertIn("admit: ok", proc.stdout)
|
||||
|
||||
def test_make_agent_exec_apply_requires_receipt(self) -> None:
|
||||
env = os.environ.copy()
|
||||
env.pop("RECEIPT", None)
|
||||
@@ -719,21 +695,3 @@ class AgentExecAdmissionTests(unittest.TestCase):
|
||||
def _git_reset_index(self) -> None:
|
||||
_git(self.repo, "reset", "--hard", self.head)
|
||||
_git(self.repo, "clean", "-fd")
|
||||
|
||||
def _install_project_hook(self) -> None:
|
||||
dest = self.repo / "scripts"
|
||||
hooks = dest / "hooks"
|
||||
hooks.mkdir(parents=True)
|
||||
for name in ("agent_exec.py", "agent_exec_config.py", "agent_exec_adapters.py"):
|
||||
shutil.copy(SOURCE / "scripts" / name, dest / name)
|
||||
hook = hooks / "pre-commit"
|
||||
shutil.copy(SOURCE / "scripts" / "hooks" / "pre-commit", hook)
|
||||
hook.chmod(0o755)
|
||||
|
||||
def _hook(self) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["bash", str(self.repo / "scripts" / "hooks" / "pre-commit")],
|
||||
cwd=str(self.repo),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"""scripts/agent-exec 薄入口与 pre-commit 委派测试:临时真实 Git 仓库 + 假 dm-server,不访问网络。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[1]
|
||||
HAS_GO = shutil.which("go") is not None
|
||||
|
||||
GO_MAIN = r'''package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("fake-dm-server %s\n", strings.Join(os.Args[1:], " "))
|
||||
}
|
||||
'''
|
||||
|
||||
# 预置在缓存中的桩二进制:用于不依赖 go 的 pre-commit 委派测试(入口命中缓存即直接 exec)。
|
||||
STUB = "#!/bin/sh\nprintf 'fake-dm-server %s\\n' \"$*\"\n"
|
||||
|
||||
|
||||
def _clean_env(**extra: str) -> dict[str, str]:
|
||||
env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
|
||||
env.update(
|
||||
GIT_TERMINAL_PROMPT="0",
|
||||
GIT_AUTHOR_NAME="Agent Exec",
|
||||
GIT_AUTHOR_EMAIL="agent-exec@example.com",
|
||||
GIT_COMMITTER_NAME="Agent Exec",
|
||||
GIT_COMMITTER_EMAIL="agent-exec@example.com",
|
||||
)
|
||||
env.update(extra)
|
||||
return env
|
||||
|
||||
|
||||
def _git(cwd: Path, *args: str) -> str:
|
||||
proc = subprocess.run(
|
||||
["git", "-C", str(cwd), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=_clean_env(),
|
||||
check=True,
|
||||
)
|
||||
return proc.stdout.strip()
|
||||
|
||||
|
||||
class AgentExecEntryTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tmp = tempfile.TemporaryDirectory(prefix="agent-exec-entry-")
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.base = Path(os.path.realpath(self.tmp.name))
|
||||
self.cache = self.base / "cache"
|
||||
self.refused_cache = self.base / "refused-cache"
|
||||
self.dm_src = self._make_fake_dm()
|
||||
self.pin = _git(self.dm_src, "rev-parse", "HEAD")
|
||||
self.repo = self._make_super_repo()
|
||||
self.dm = self.repo / "deps" / "development-manager"
|
||||
|
||||
def _make_fake_dm(self) -> Path:
|
||||
src = self.base / "fake-dm"
|
||||
(src / "cmd" / "dm-server").mkdir(parents=True)
|
||||
(src / "go.mod").write_text("module example.com/fakedm\n\ngo 1.22\n", encoding="utf-8")
|
||||
(src / "cmd" / "dm-server" / "main.go").write_text(GO_MAIN, encoding="utf-8")
|
||||
(src / "README.md").write_text("fake dm\n", encoding="utf-8")
|
||||
_git(src, "init", "-q", "-b", "main")
|
||||
_git(src, "add", "-A")
|
||||
_git(src, "commit", "-q", "-m", "fake dm")
|
||||
return src
|
||||
|
||||
def _make_super_repo(self) -> Path:
|
||||
repo = self.base / "super"
|
||||
(repo / "scripts" / "hooks").mkdir(parents=True)
|
||||
for src, rel in (
|
||||
(SOURCE / "scripts" / "agent-exec", "scripts/agent-exec"),
|
||||
(SOURCE / "scripts" / "hooks" / "pre-commit", "scripts/hooks/pre-commit"),
|
||||
):
|
||||
dest = repo / rel
|
||||
shutil.copy(src, dest)
|
||||
dest.chmod(0o755)
|
||||
(repo / "README.md").write_text("super\n", encoding="utf-8")
|
||||
_git(repo, "init", "-q", "-b", "main")
|
||||
_git(repo, "update-index", "--add", "--cacheinfo",
|
||||
f"160000,{self.pin},deps/development-manager")
|
||||
dm = repo / "deps" / "development-manager"
|
||||
dm.parent.mkdir()
|
||||
_git(repo, "clone", "-q", "--no-checkout", str(self.dm_src), str(dm))
|
||||
_git(dm, "checkout", "-q", "--detach", self.pin)
|
||||
_git(repo, "add", "-A")
|
||||
_git(repo, "commit", "-q", "-m", "init")
|
||||
return repo
|
||||
|
||||
def _entry(self, *args: str, cache: Path | None = None) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[str(self.repo / "scripts" / "agent-exec"), *args],
|
||||
cwd=str(self.repo),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=_clean_env(AGENT_EXEC_CACHE=str(cache or self.cache)),
|
||||
)
|
||||
|
||||
def _binary(self, cache: Path | None = None) -> Path:
|
||||
return (cache or self.cache) / f"dm-server-{self.pin}"
|
||||
|
||||
def _assert_no_binary(self, cache: Path) -> None:
|
||||
leftovers = list(cache.glob("dm-server-*")) if cache.exists() else []
|
||||
self.assertEqual(leftovers, [])
|
||||
|
||||
def _assert_refused(self, proc: subprocess.CompletedProcess[str], needle: str) -> None:
|
||||
combined = proc.stdout + proc.stderr
|
||||
self.assertEqual(proc.returncode, 2, combined)
|
||||
self.assertIn(needle, proc.stderr)
|
||||
self.assertNotIn("fake-dm-server", combined)
|
||||
self._assert_no_binary(self.refused_cache)
|
||||
|
||||
@unittest.skipUnless(HAS_GO, "go is not on PATH")
|
||||
def test_positive_run_builds_and_execs_pinned_binary(self) -> None:
|
||||
proc = self._entry("validate", "--repo", ".")
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertIn("fake-dm-server agent-exec validate --repo .", proc.stdout)
|
||||
self.assertTrue(self._binary().is_file())
|
||||
|
||||
@unittest.skipUnless(HAS_GO, "go is not on PATH")
|
||||
def test_second_run_reuses_cached_binary(self) -> None:
|
||||
first = self._entry("version")
|
||||
self.assertEqual(first.returncode, 0, first.stderr)
|
||||
before = self._binary().stat().st_mtime_ns
|
||||
second = self._entry("version")
|
||||
self.assertEqual(second.returncode, 0, second.stderr)
|
||||
self.assertIn("fake-dm-server agent-exec version", second.stdout)
|
||||
self.assertEqual(self._binary().stat().st_mtime_ns, before)
|
||||
self.assertEqual(list(self.cache.glob(".dm-server-*")), [])
|
||||
|
||||
def test_refuses_when_dm_head_differs_from_pin(self) -> None:
|
||||
_git(self.dm, "commit", "-q", "--allow-empty", "-m", "move head")
|
||||
proc = self._entry("validate", "--repo", ".", cache=self.refused_cache)
|
||||
self._assert_refused(proc, "与 index 钉住的")
|
||||
self.assertIn("git submodule update --init deps/development-manager", proc.stderr)
|
||||
|
||||
def test_refuses_tracked_modification_in_dm_checkout(self) -> None:
|
||||
(self.dm / "README.md").write_text("changed\n", encoding="utf-8")
|
||||
proc = self._entry("validate", "--repo", ".", cache=self.refused_cache)
|
||||
self._assert_refused(proc, "存在未提交的修改")
|
||||
|
||||
def test_refuses_when_gitlink_absent(self) -> None:
|
||||
_git(self.repo, "rm", "--cached", "-q", "deps/development-manager")
|
||||
proc = self._entry("validate", "--repo", ".", cache=self.refused_cache)
|
||||
self._assert_refused(proc, "gitlink")
|
||||
|
||||
def test_precommit_delegates_admit_to_pinned_binary(self) -> None:
|
||||
_git(self.repo, "config", "core.hooksPath", "scripts/hooks")
|
||||
(self.repo / "src").mkdir()
|
||||
(self.repo / "src" / "b.txt").write_text("staged\n", encoding="utf-8")
|
||||
_git(self.repo, "add", "src/b.txt")
|
||||
|
||||
main_cache = self.base / "main-cache"
|
||||
rejected = self._commit(main_cache, "on main")
|
||||
combined = rejected.stdout + rejected.stderr
|
||||
self.assertNotEqual(rejected.returncode, 0, combined)
|
||||
self.assertIn("集成分支", combined)
|
||||
self.assertNotIn("fake-dm-server", combined)
|
||||
self._assert_no_binary(main_cache)
|
||||
|
||||
cache = self.base / "hook-cache"
|
||||
cache.mkdir()
|
||||
stub = self._binary(cache)
|
||||
stub.write_text(STUB, encoding="utf-8")
|
||||
stub.chmod(0o755)
|
||||
|
||||
_git(self.repo, "checkout", "-q", "-b", "feature/x")
|
||||
accepted = self._commit(cache, "feat: x")
|
||||
combined = accepted.stdout + accepted.stderr
|
||||
self.assertEqual(accepted.returncode, 0, combined)
|
||||
self.assertIn(f"fake-dm-server agent-exec admit --repo {self.repo}", combined)
|
||||
|
||||
def _commit(self, cache: Path, message: str) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["git", "-C", str(self.repo), "commit", "-q", "-m", message],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=_clean_env(AGENT_EXEC_CACHE=str(cache)),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user