Python版教程,构建可检查代码库、修复测试、请求审批后才执行的维护Agent,完整源码可参考。
在 .NET 教程中,我们构建了一个仓库 Agent,它能够检查代码 checkout、提出一行修复方案、运行测试并报告结果。每次写入和 shell 命令都需要显式批准。
现在我们用 Python 来构建同样的 Agent。
设计结果和安全策略保持一致——我不想因为 Python 让某些操作更便捷就悄无声息地选一个更简单的演示。两个实现针对同一个失败的 fixture、接收相同的指令、在相同的批准边界停止、并且必须通过相同的测试。唯一变化的是宿主语言实现。
最终得到的是一个异步 Python 命令行应用,使用 GitHub Copilot harness 做仓库工作,用 Microsoft Agent Framework 做 Agent 抽象、流式处理、会话和遥测。
💡 代码仓库:完整的 Python 和 .NET 实现共用同一个 fixture,地址在 github.com/sahansera/safe-repository-maintenance-agent。

Agent 接收一个本地仓库路径和一个维护任务。它的系统指令要求它:
仅在该仓库内工作。
在修改任何内容之前读取它的 AGENTS.md。
做最小化的连贯修复。
避免网络访问、包安装、提交、推送和拉取请求。
运行聚焦的验证。
报告变更的文件、执行的命令和结果。
包含的 fixture 是一个小的 JavaScript 函数。Agent 用 Python 编写,但目标仓库不必须是 Python 的:
export function normalizeTitle(value) {
return value.toLowerCase().replace(/\s+/g, "-");
}
一个测试期望普通的标题规范化。第二个测试期望忽略标题周围的空白。第二个测试在实现 trim 输入之前会失败。
使用语言无关的目标是刻意的。Python Agent 可以维护 .NET、JavaScript、Go 或文档仓库。Agent 的宿主语言决定了我们如何集成和操作 harness,而不是 harness 能理解哪些源文件。
你需要 Python 3.11 或更高版本,以及一个有效的 GitHub Copilot 订阅。示例使用 Python 3.12 作为文档基线,也已在 Python 3.13 上验证过。
这是我的实际测试环境:
[project]
requires-python = ">=3.11"
dependencies = [
"agent-framework-github-copilot==1.0.1",
"github-copilot-sdk==1.0.2",
]
[project.optional-dependencies]
dev = ["pytest>=8.4,<9"]
创建隔离环境并安装项目:
python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
端到端运行时的效果:

当前的 Python SDK 包包含了支持平台的 Copilot 运行时。身份验证仍然依赖 GitHub Copilot,首次运行 Agent 时可能会要求你登录。
Agent Framework 集成是稳定的;但底层的 GitHub Copilot SDK 不是,我写这两篇文章期间它的预览版标签并没有阻止它多次改变形态。锁定这些版本——这是对未来 SDK 升级可能悄悄改变你正在阅读的内容的一份廉价保险。
Agent 可以请求多种能力类型,包括 read、write、shell、url 和 mcp。我们不会对它们一视同仁:
策略是一个普通函数:
from enum import Enum
class PolicyDecision(Enum):
APPROVE = "approve"
PROMPT = "prompt"
DENY = "deny"
def decide(permission_kind: str) -> PolicyDecision:
if permission_kind == "read":
return PolicyDecision.APPROVE
if permission_kind in {"write", "shell"}:
return PolicyDecision.PROMPT
return PolicyDecision.DENY
默认拒绝 URL 访问、MCP 调用、新的 SDK 权限类型以及任何格式错误的值。必须主动修改应用才能让其中某种能力变得可用。
测试套件让这个契约变得可见:
@pytest.mark.parametrize(
("kind", "expected"),
[
("read", PolicyDecision.APPROVE),
("write", PolicyDecision.PROMPT),
("shell", PolicyDecision.PROMPT),
("url", PolicyDecision.DENY),
("mcp", PolicyDecision.DENY),
("unknown", PolicyDecision.DENY),
],
)
def test_decide_returns_expected_decision(kind, expected):
assert decide(kind) is expected
这些测试不需要模型、Copilot 订阅或仓库。它们测试的是应用权限而非概率行为。
你不需要 LLM 评估来证明 URL 访问被拒绝了。把确定性授权策略从 Agent 中分离出来,像其他安全敏感函数一样测试它。
权限处理器接收一个类型化的请求和一个上下文字典。它首先打印足够的细节让操作员理解该操作:
async def handle_permission(request, context):
decision = decide(request.kind)
print(f"\n[permission: {request.kind}]")
print(describe(request))
if decision is PolicyDecision.APPROVE:
return PermissionHandler.approve_all(request, context)
if decision is PolicyDecision.DENY:
return PermissionDecisionReject(
feedback="Blocked by the repository agent policy."
)
answer = (
await asyncio.to_thread(input, "Approve once? [y/N] ")
).strip().lower()
if answer == "y":
return PermissionHandler.approve_all(request, context)
return PermissionDecisionReject(
feedback="The operator denied this action."
)
input() 是阻塞的,所以 asyncio.to_thread 把它从事件循环中移走。在控制台示例中这个细节很容易忽略,但当应用同时输出流式内容或处理多个会话时,它就变得更加重要。
对于写请求,describe 打印 file_name 和 diff。对于 shell 请求,它打印 full_command_text。URL 和 MCP 请求在拒绝之前会先显示,留下一个对审计友好的记录,说明 Agent 尝试了什么。
在这个上下文中,帮助函数名 approve_all 可能会让人误解。它为当前请求构造一个批准响应。我们的应用仍然决定哪些请求到达那一行。
命令在创建 Agent 之前解析仓库路径:
repository = args.repository.expanduser().resolve(strict=True)
if not repository.is_dir():
raise NotADirectoryError(repository)
Copilot 会话选项包含工作目录和我们的权限回调:
options = GitHubCopilotOptions(
working_directory=str(repository),
enable_config_discovery=False,
on_permission_request=handle_permission,
)
agent = GitHubCopilotAgent(
instructions=INSTRUCTIONS,
default_options=options,
)
仓库指令确实有用,所以关闭配置发现乍一看似乎本末倒置。我在构建这个项目的 .NET 版本时遇到了原因:fixture 在开发期间位于另一个 Git checkout 内部,运行时愉快地向上走到了父仓库的 AGENTS.md,而从未注意到 fixture 自己的。嵌套仓库、monorepo 和临时 worktree 都容易在无意间模糊这个边界。
所以应用指令显式告诉 Agent 读取其工作目录内的 AGENTS.md。这使得项目指导的来源在工具活动中可见,而不是让一个无关的父目录悄悄改变 Agent 认为的规则。
这并不意味着仓库指令是可信的。仓库可能包含 prompt 注入,就像它可能包含恶意构建脚本一样。当指令要求禁止的操作时,宿主策略仍然拥有权威。
仓库指令可以改进补丁,但不能扩大 Agent 的权限。当项目指导要求一个禁止的副作用时,宿主权限策略胜出。
GitHubCopilotAgent 拥有一个异步客户端,所以自然的 Python 生命周期是一个异步上下文管理器:
async with agent:
async for update in agent.run(args.task, stream=True):
print(update.text, end="", flush=True)
上下文管理器启动和停止 Copilot 客户端,即使运行抛出异常。命令入口点保持同步边界很小:
def run() -> None:
raise SystemExit(asyncio.run(main()))
这个生命周期在运行时间较长的应用中很重要。Agent 会话拥有进程、连接、历史记录,有时还有临时文件。异常不应该让这些资源无限期地附加到 worker 上。
用包含的 fixture 启动 Agent:
safe-repo-agent ../fixture
它读取源代码和测试,然后提出与 .NET 版本相同的一行更改:
- return value.toLowerCase().replace(/\s+/g, "-");
+ return value.trim().toLowerCase().replace(/\s+/g, "-");
在操作员批准之前写入不会发生。后续的 npm test 请求有自己的提示,所以批准一个补丁并不意味着授予持续执行任意命令的权限。
验证后的运行结果是:
File changed: src/normalize-title.js
Fix: Added .trim() before .toLowerCase().
Validation: Both tests pass (npm test exit 0).
单独运行确定性策略测试:
pytest
所有六个策略用例在不启动 Copilot 的情况下通过。
Python GitHubCopilotAgent 包含 Agent Framework 的遥测层。对于本地探索,可以在创建 Agent 之前配置控制台导出器:
from agent_framework.observability import configure_otel_providers
configure_otel_providers(enable_console_exporters=True)
在服务中,通过 OTLP 导出到你正常的可观测性后端,并在周围 trace 中附加仓库作业标识符。有用的信号包括:
端到端运行耗时
等待人工批准的时间
按权限类型分类的工具调用
命令耗时和退出状态
修复尝试和验证失败
不要随意启用 prompt 和 completion 捕获。仓库路径、源代码、终端输出和环境相关错误可能包含敏感信息。遥测应该解释运行情况,而不应该成为 Agent 可能看到的每个秘密的另一个副本。
控制台应用展示了控制点,而不是一个完整的隔离平台。在让它处理不可信仓库之前,我会添加:
每个作业一个全新的容器或 microVM
只读的基础镜像和可丢弃的可写工作区
对 CPU、内存、磁盘和挂钟限制的 asyncio 感知超时和取消,不只是写在配置文件里的一个数字
网络默认拒绝
无环境中的开发者或云凭证
无合并权限的短期仓库凭证
持久化批准记录而非终端输入
有界的修复循环和 diff 大小限制
在 finally 中清理和审计记录,包括已取消的任务
同样的顾虑也适用于运行仓库脚本的确定性工具。Agent 使风险更容易看到,因为它动态选择命令,但仓库及其依赖本来就是不可信的可执行输入。
架构边界没有改变。两个实现使用相同的 harness、相同的工作目录、相同的权限表、相同的任务和相同的 fixture。
Python 版本使用 async with 表达生命周期,通过 GitHubCopilotOptions 传递 Copilot 会话设置,并把阻塞的操作员输入移到一个线程。.NET 版本使用 CopilotClient、SessionConfig、类型化的权限请求子类以及 IAsyncEnumerable 做流式处理。
这些是生态系统的差异,不是不同的安全模型。
把同一个 Agent 构建两次让我明白了一件事:包装模型的语言几乎不重要。重要的是它周围工具的权限边界,以及「它工作了」是否有 diff 和通过的测试支撑,还是只是模型自己说的话。
感谢阅读 ✌️
Build Production-Ready Agents with the GitHub Copilot Harness and Agent Framework
GitHub Copilot integration with Microsoft Agent Framework
GitHub Copilot agents in Microsoft Agent Framework