工具审批与 CodeAct 沙盒执行
注意:本文使用 AI 撰写。
上一篇从 Prompt 注入和信任边界出发,说明了输入校验、权限控制和数据保护为什么不能交给模型自行判断。这些措施要真正阻止危险操作,还需要落实到执行出口。
Agent 造成实际影响通常有两条路径:一是调用工具修改外部系统,二是执行模型生成的代码。工具审批负责在第一条路径上暂停执行,沙盒负责限制第二条路径能够接触的文件、网络和计算资源。两者解决的问题不同,实际项目中经常需要组合使用。
让高风险工具先等待确认
默认情况下,模型决定调用工具后,框架会直接执行。查询天气或搜索文档通常没有明显副作用,发送邮件、删除记录、提交订单等操作则不应只凭模型的一次判断放行。
ApprovalRequiredAIFunction 可以把普通 AIFunction 包装成需要审批的工具。当模型选择该工具时,框架不会立即调用函数,而是在响应中返回 ToolApprovalRequestContent。应用展示工具名和参数,由用户决定批准或拒绝,再把 ToolApprovalResponseContent 送回同一个会话。
using System.ComponentModel;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
[Description("Delete a record by id.")]
static string DeleteRecord([Description("The record id.")] int id)
=> $"Deleted record {id}.";
AIFunction deleteRecord = new ApprovalRequiredAIFunction(
AIFunctionFactory.Create(DeleteRecord));
AIAgent agent = chatClient.AsAIAgent(
instructions: "You manage records. Explain every destructive operation.",
tools: [deleteRecord]);
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync("删除编号 42 的记录", session);
while (true)
{
List<ToolApprovalRequestContent> requests = response.Messages
.SelectMany(message => message.Contents)
.OfType<ToolApprovalRequestContent>()
.ToList();
if (requests.Count == 0)
{
break;
}
List<ChatMessage> decisions = requests.ConvertAll(request =>
{
FunctionCallContent call = (FunctionCallContent)request.ToolCall;
Console.WriteLine($"工具:{call.Name},参数:{call.Arguments}");
Console.Write("批准执行?(Y/N):");
bool approved = Console.ReadLine()?.Equals(
"Y", StringComparison.OrdinalIgnoreCase) == true;
return new ChatMessage(
ChatRole.User,
[request.CreateResponse(approved, approved ? null : "用户拒绝执行")]);
});
response = await agent.RunAsync(decisions, session);
}
Console.WriteLine(response.Text);
审批不是一个额外的模型判断,而是应用与用户之间的控制流程。实现时要注意三件事:
- 必须复用同一个
AgentSession,否则 Agent 无法把审批结果与之前的工具请求对应起来。 - 界面应展示工具名称、参数和影响范围,不能只放一个没有上下文的“确认”按钮。
- 审批等待期间可能跨越多个 HTTP 请求,Web 应用需要持久化会话,并校验回复审批的人是否有相应权限。
记住审批决定
如果每次只读调用都要求确认,用户很快会习惯性点击批准。ToolApprovalAgent 可以在 Agent 外层管理“不再询问”规则,并在同一会话中记住决定:
#pragma warning disable MAAI001
AIAgent approvalAgent = agent
.AsBuilder()
.UseToolApproval()
.Build();
需要预配置允许列表时,可以通过 ToolApprovalAgentOptions.AutoApprovalRules 提供自动审批规则。不同预览版本调整过规则委托的上下文类型,应以项目引用版本的 API 为准。
运行时规则可以按工具名放行,也可以记录“工具名 + 完全相同参数”的组合。后者比永久批准整个工具更窄,适合“以后允许读取这个目录”或“以后允许查询这个项目”一类场景。框架提供 CreateAlwaysApproveToolResponse 和 CreateAlwaysApproveToolWithArgumentsResponse,分别创建这两种决定。
多个待审批请求出现时,ToolApprovalAgent 会一次向调用方返回一个,其余请求保存在会话状态中。应用处理完当前请求后再收到下一个,不需要同时展示一组确认框。
自动批准规则应该使用允许列表。ToolApprovalAgent.AllToolsAutoApprovalRule 会放行所有工具,实际效果等同于关闭审批,只适合完全可信的测试环境。生产系统通常采用分级策略:只读且低敏感度的工具自动通过,有副作用或会返回敏感数据的工具逐次确认。
审批也不是授权系统。用户批准只说明他接受这次操作,工具内部仍要校验调用者身份、资源权限和参数范围。Prompt 注入还可能误导用户点击批准,因此不可逆操作最好同时提供预览、二次确认、限额或延迟撤销机制。
CodeAct 解决什么问题
直接工具调用适合步骤少、边界清楚的任务。处理表格时,模型可能需要反复调用读取、过滤、排序和聚合工具,每一步都要把中间结果送回模型,不仅增加延迟和 token 消耗,也让原本简单的循环被拆成许多轮对话。
CodeAct 换了一种执行方式:向模型提供 execute_code,让模型把循环、分支和数据转换写成一段代码,再由受控运行时一次执行。Provider 还可以把少量宿主工具通过 call_tool(...) 暴露给代码。
这种方式适合以下任务:
- 多个只读工具需要循环或条件组合。
- 中间数据较大,不适合反复放入模型上下文。
- 需要生成报表、图表或其他文件。
- 希望把文件和网络能力集中在隔离环境中。
如果任务只有一两次调用,CodeAct 不会带来明显收益。转账、删除、发布等高风险工具也不宜藏在一段长代码里,因为用户很难审查完整执行路径。这些工具更适合保留为直接调用,并逐次审批。
需要特别区分两个概念:execute_code 是 Agent 看见的工具,真正的安全边界由它背后的运行时提供。仅仅把代码放进子进程,并不等于获得了沙盒。
选择代码执行环境
.NET SDK 提供 Hyperlight 和 LocalCodeAct 两种 CodeAct Provider。它们的使用方式相似,隔离强度却完全不同。
| 维度 | Hyperlight | LocalCodeAct |
|---|---|---|
| 隔离边界 | Hyperlight 微型虚拟机 | 本地 Python 子进程 |
| 不可信代码 | 可以作为隔离方案评估 | 不能直接在宿主机运行 |
| 文件访问 | 显式输入目录和挂载 | 映射宿主目录,仍依赖外部隔离 |
| 网络访问 | AllowedDomains 允许列表 | 由容器、虚拟机或网络策略限制 |
| 资源控制 | guest 堆栈大小和运行时隔离 | 超时、输出及捕获文件大小限制 |
| 当前状态 | 预览,依赖 Hyperlight SDK 和 guest 模块 | 预览,需要本地 Python |
Hyperlight
HyperlightCodeActProvider 在轻量虚拟机中执行生成的代码。未显式提供的文件和网络能力不会自动进入 guest,适合需要处理外部输入的场景。
using Microsoft.Agents.AI.Hyperlight;
string guestPath = Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH")
?? throw new InvalidOperationException(
"HYPERLIGHT_PYTHON_GUEST_PATH is not set.");
var options = HyperlightCodeActProviderOptions.CreateForWasm(guestPath);
options.HeapSize = "50Mi";
options.StackSize = "35Mi";
options.ApprovalMode = CodeActApprovalMode.AlwaysRequire;
options.HostInputDirectory = Path.GetFullPath("input");
options.AllowedDomains = [];
using var codeAct = new HyperlightCodeActProvider(options);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new()
{
Instructions = "需要计算或处理文件时使用 execute_code。",
},
AIContextProviders = [codeAct],
});
示例把网络允许列表留空,并让每次 execute_code 都需要审批。真实项目只挂载任务所需目录,网络也只开放必要域名。Hyperlight 当前仍是预览功能,采用前要确认 NuGet 包发布状态、平台虚拟化支持和 guest 模块版本。
LocalCodeAct
LocalCodeActProvider 会启动本地 Python 子进程,并在执行前使用 AST 允许列表检查代码。它还提供超时、输出大小、文件捕获和工具注册等控制,但这些措施只是纵深防御,不是安全沙盒。
using Microsoft.Agents.AI.LocalCodeAct;
var options = new LocalCodeActProviderOptions
{
ExecutionLimits = new ProcessExecutionLimits
{
TimeoutSeconds = 5,
MaxStdoutBytes = 1024 * 1024,
MaxStderrBytes = 1024 * 1024,
MaxResultBytes = 1024 * 1024,
},
Environment = new Dictionary<string, string>(),
FileMounts =
[
new FileMount(dataPath, "/input", FileMountMode.ReadOnly),
new FileMount(outputPath, "/output", FileMountMode.ReadWrite),
],
};
using var codeAct = new LocalCodeActProvider(pythonPath, options);
显式传入空的 Environment 可以避免把业务密钥交给子进程。在 Windows 上,Provider 会补充 Python 启动所需的少量系统变量。若确实需要环境变量,也应逐项加入允许列表,不能把整个宿主环境复制进去。
AST 校验默认阻止 eval、exec、subprocess、socket 等常见危险入口。自定义 AllowedImports、BlockedImports、AllowedBuiltins 或 BlockedBuiltins 时,新列表会替换默认值,而不是追加。ValidationDisabled = true 会移除这层检查,只能在代码完全可信或外层已有强隔离时使用。
LocalCodeAct 看到的仍是真实宿主路径,网络能力也不会自动隔离。它应该运行在专用容器、Azure Container Instances、虚拟机或等价的隔离环境中,由基础设施限制进程、文件、网络和凭据。不要在开发者工作站或生产宿主机上直接执行来自用户、网页、邮件或 RAG 文档的不可信代码。
如果底层模型服务支持托管代码解释器,也可以把代码执行交给服务端。这样不用维护本地运行时,但代码和输入文件会发送给服务提供商,仍需评估数据驻留、保留策略、网络能力和提供商支持范围。托管工具与 CodeAct Provider 不是同一套 API,不能把两者的配置混用。
组合审批与沙盒
审批回答“这次操作是否得到人的允许”,沙盒回答“即使代码有问题,它最多能影响什么”。典型执行路径如下:
用户输入
│
▼
Agent 选择工具
│
├── 普通只读工具 ────────────────► 参数校验后执行
│
├── 高风险工具 ─► 审批 ─────────► 授权与参数校验后执行
│
└── execute_code ─► 审批 ───────► 沙盒或外部隔离环境
│
├── 文件挂载允许列表
├── 网络出站允许列表
└── 时间、内存和输出限制
Hyperlight 的 CodeActApprovalMode.AlwaysRequire 会让每次代码执行都请求批准。默认的 NeverRequire 并不表示永远跳过审批:如果 Provider 注册的任一宿主工具被 ApprovalRequiredAIFunction 包装,execute_code 也会随之要求审批,因为代码可能在内部调用该工具。
这个传播机制只能判断代码“可能访问需要审批的工具”,不能让用户逐条批准代码里的每一次 call_tool(...)。因此,审批页面应展示完整代码、可访问的宿主工具、文件挂载和网络范围。对转账、删除等高风险动作,最清楚的做法仍是把它们留在 CodeAct 之外。
实际部署时,可以按下面的顺序收紧权限:先决定任务是否真的需要代码执行,再选择隔离边界,随后只开放必要文件、域名和宿主工具,最后为不可逆操作增加审批。下一篇会继续处理这里尚未解决的问题:如何跟踪进入上下文的数据是否可信,以及怎样用评估持续验证这些防线。