.NET 10控制台应用教程,使用GitHub Copilot做编程运行时、微软Agent框架做抽象,构建带审批边界的安全维护Agent。
让 AI 代理访问一个代码仓库只需要几行代码。真正需要学习的是:如何在不静默批准每一条命令的前提下,给它有用的访问权限。
在本文中,我们将构建一个 .NET 10 控制台应用程序,它以 GitHub Copilot harness 作为编码运行时,以 Microsoft Agent Framework 作为应用层面的代理抽象层。该代理将检查一个小型的代码仓库,修复一个失败的测试,在修改文件前请求确认,再次请求确认后才运行测试命令,最后输出一份简洁的维护报告。
它不会访问网络、安装包、提交代码、推送代码或创建拉取请求。
这是我之前那篇关于构建 AI 辅助依赖漏洞修复工具的文章的实践续篇。那套系统使用代理来处理仓库特定的异常,同时将审查和合并权限保留给仓库所有者。本文我们要构建的是那个思路背后更小的执行边界。
💡 代码仓库:完整的 .NET 和 Python 实现使用相同的 fixture,可在 github.com/sahansera/safe-repository-maintenance-agent 获取。

两个框架各自的贡献
GitHub Copilot SDK 提供了编码 harness。它负责管理代理循环,并提供面向仓库的能力,如读取文件、写入文件、运行 shell 命令、获取 URL 以及调用 MCP 工具。
Microsoft Agent Framework 将该运行时包装在其 AIAgent 抽象中——与其其他 providers 所用的抽象相同。这为应用程序提供了一致的运行接口、流式响应、会话管理、中间件和 OpenTelemetry 集成。
这个区别很重要。我们并不是在让 Agent Framework 围绕一个聊天模型重新创建一个编码循环。Copilot 负责规划和工具执行。Agent Framework 给我们的是应用程序其余部分可以依赖的表面。
Agent Framework 集成本身是稳定的,但底层的 GitHub Copilot SDK 仍被标记为公开预览。正因如此,我在下面锁定了确切的版本——在我起草本文的过程中,低层 API 变动了两次,我宁可你获得一个干净的构建,也不愿你在教程中途去追逐一个破坏性变更。
我们要修复的仓库
完整的示例接受任意仓库路径,包括类似 sahansera.dev 这样的 checkout。但为了演示写入功能,它包含了一个无依赖的一次性 fixture:
fixture/
├── AGENTS.md
├── package.json
├── src/
│ └── normalize-title.js
└── test/
└── normalize-title.test.js
实现是故意写错的:
export function normalizeTitle(value) {
return value.toLowerCase().replace(/\s+/g, "-");
}
第二个测试期望忽略前后空白字符。运行 npm test 会得到一个通过和一个失败,因为实际结果是 -safe-repository-agent-。
这个 fixture 让代理有一个真实的任务,其结果可以被客观地验证。这也意味着没有人需要在第一次实验时授予对重要仓库的写入权限。
前置条件和项目设置
一个有效的 GitHub Copilot 订阅
GitHub Copilot 和 Agent Framework 集成包
以下是我在它正常工作时安装的版本:
<ItemGroup>
<PackageReference Include="GitHub.Copilot.SDK" Version="1.0.9" />
<PackageReference Include="Microsoft.Agents.AI.GitHub.Copilot" Version="1.17.0" />
</ItemGroup>
SDK 捆绑了它兼容的 Copilot 运行时,所以当前的 .NET 包不需要单独的全局 CLI 安装。但你仍然需要身份验证和一个有效的订阅。
在创建代理之前定义权限策略
最快速的演示是一个返回 ApproveOnce() 批准一切的审批回调。但对于一个可以运行命令和重写 checkout 的应用程序来说,这也是一个糟糕的默认值。
我们的策略分离了四种决策:
未知的权限类型被拒绝。新的 SDK 能力不应该仅仅因为应用程序尚未更新以识别它就被授权。
public enum PolicyDecision
{
Approve,
Prompt,
Deny,
}
public static PolicyDecision Decide(string permissionKind) => permissionKind switch
{
"read" => PolicyDecision.Approve,
"write" or "shell" => PolicyDecision.Prompt,
"url" or "mcp" => PolicyDecision.Deny,
_ => PolicyDecision.Deny,
};
这个函数不包含代理或控制台依赖,所以很容易进行单元测试。示例中有六个用例覆盖了每个已知分支和 fail-closed 回退。
权限处理是应用程序策略,不是提示词措辞。把它放在普通代码中,测试每个分支,拒绝你不认识的 capabilities。
将策略决策转化为操作员提示
Copilot SDK 发送一个类型化的 PermissionRequest。这意味着我们可以向操作员展示实际的命令、文件名、diff、URL 或 MCP 工具,而不是让他们批准一个未经解释的动作。
public static Task<PermissionDecision> HandleAsync(
PermissionRequest request,
PermissionInvocation _)
{
PolicyDecision decision = PermissionPolicy.Decide(request.Kind);
Console.WriteLine($"\n[permission: {request.Kind}]");
Console.WriteLine(Describe(request));
return decision switch
{
PolicyDecision.Approve =>
Task.FromResult(PermissionDecision.ApproveOnce()),
PolicyDecision.Deny =>
Task.FromResult(PermissionDecision.Reject(
"Blocked by the repository agent policy.")),
_ => Task.FromResult(Prompt()),
};
}
对于写入请求,Describe 打印路径和提议的 diff。对于 shell 请求,它打印 FullCommandText。批准总是针对当前动作,而不是整个会话。
不要把显示的命令当作完整的安全解析器。Shell 语法、符号链接、子进程和包脚本使静态分类变得困难。提示改进了操作员的判断;但真正的隔离边界仍然应该是一个具有有限凭证和网络访问权限的一次性沙箱。
将 Copilot 运行时限定在一个仓库
应用程序在启动 Copilot 之前解析提供的路径:
string repositoryPath = Path.GetFullPath(args[0]);
if (!Directory.Exists(repositoryPath))
{
Console.Error.WriteLine(
$"Repository directory does not exist: {repositoryPath}");
return 2;
}
我们将该路径同时用于客户端进程和会话:
await using CopilotClient copilotClient = new(new CopilotClientOptions
{
WorkingDirectory = repositoryPath,
});
await copilotClient.StartAsync();
SessionConfig sessionConfig = new()
{
WorkingDirectory = repositoryPath,
EnableConfigDiscovery = false,
OnPermissionRequest = ConsolePermissionHandler.HandleAsync,
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Append,
Content = instructions,
},
};
这个修订后的系统指令告诉代理自行读取所选仓库的 AGENTS.md。禁用环境发现后,嵌套的 checkout 不再静默继承父级 checkout 的指令。
工作目录也限制了 Copilot 默认考虑可用的路径。这是一个有用的作用域,但它不是进程隔离。我仍然会在一个具有非 root 用户、可丢弃文件系统、无环境云凭证和严格网络策略的容器中运行针对不受信任仓库的代理。
创建 Agent Framework 代理并流式传输结果
在配置好客户端和会话后,集成是一行调用:
AIAgent agent = copilotClient.AsAIAgent(sessionConfig);
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(task))
{
Console.Write(update);
}
指令进一步约束了任务:
Work only inside the supplied working directory.
Read AGENTS.md before making changes.
Make the smallest change that satisfies the task.
Do not access the network, install packages, commit, push, or create a pull request.
Run focused validation and report the files changed, commands run, and result.
这些指令改善了代理行为,但不能替代权限回调。网络禁令在两处都存在是有意为之:提示告诉代理不要尝试,而回调则在它尝试时拒绝该能力。
针对 fixture 运行示例:
dotnet run --project src/SafeRepositoryAgent -- ../fixture

代理读取了仓库说明和测试。当它提议添加 .trim() 时,应用程序打印出确切的 diff 并暂停:
[permission: write]
File: .../fixture/src/normalize-title.js
- return value.toLowerCase().replace(/\s+/g, "-");
+ return value.trim().toLowerCase().replace(/\s+/g, "-");
Approve once? [y/N]
批准后,它在运行 npm test 之前再次请求确认。验证后的运行结果为:
Changed: src/normalize-title.js - added .trim() before lowercasing.
Command: npm test - 2/2 tests pass.
结果之所以重要,是因为它有一个小的 diff 和可重复的测试支撑,而不是因为代理自述成功了。
有用的维护结果包含提议的 diff、被允许运行的命令及其退出状态。最终的自然语言答案只是该证据的摘要。
对于生产服务会有什么不同?
交互式控制台提示适合教程和开发者工作站。服务需要的是一个持久的审批协议。
我会保留相同的策略函数,然后将 Console.ReadLine() 替换为与稳定 job 和工具调用身份绑定的审批记录。worker 会暂停、持久化请求、通知授权审查员,只有在收到有效决策后才恢复。每个决策都将是审计跟踪的一部分。
每个仓库 job 使用一次性的容器或 microVM
由 host 强制执行的 CPU、内存、磁盘、进程和 wall-clock 限制,由 CancellationToken 驱动而非仅仅请求 Copilot
短期仓库凭证且无合并权限
网络默认拒绝,仅在需要时明确允许目标地址
OpenTelemetry 导出,通过 worker 自身的生命周期连接,用于代理运行、权限延迟、工具调用和失败
在 job 大声失败而不是循环之前,最大修复尝试次数
最终的 diff 大小限制和必需的验证命令
通过 await using/finally 进行确定性清理,包括失败和取消的运行
Agent Framework 发出 OpenTelemetry 兼容的遥测数据,但 trace 可能包含提示、路径、命令和模型输出。除非你有明确的理由和合适的存储策略,否则保持敏感数据捕获禁用。
重要的部分在模型之外
代理本身做的是简单部分——两行代码的修复,这也是很多教程会停下的地方。真正花费迭代的是围绕那个决策的一切:限定工作目录、关闭配置发现的漏洞、使每一次写入和 shell 命令在运行前都可见,以及保持测试套件——而非模型自己的总结——作为成功的判断标准。
这是一个小应用程序。但它划定的边界才是关键。
本文的 Python 版本使用 async context manager、类型化选项字典和 pytest 构建了相同的代理和 fixture。保持任务和权限表一致使得两个 SDK 之间的差异更容易观察。
感谢阅读 ✌️
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
For further actions, you may consider blocking this person and/or reporting abuse