工具审批与 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);


审批不是一个额外的模型判断,而是应用与用户之间的控制流程。实现时要注意三件事:

  1. 必须复用同一个 AgentSession,否则 Agent 无法把审批结果与之前的工具请求对应起来。
  2. 界面应展示工具名称、参数和影响范围,不能只放一个没有上下文的“确认”按钮。
  3. 审批等待期间可能跨越多个 HTTP 请求,Web 应用需要持久化会话,并校验回复审批的人是否有相应权限。

记住审批决定

如果每次只读调用都要求确认,用户很快会习惯性点击批准。ToolApprovalAgent 可以在 Agent 外层管理“不再询问”规则,并在同一会话中记住决定:

#pragma warning disable MAAI001

AIAgent approvalAgent = agent
    .AsBuilder()
    .UseToolApproval()
    .Build();


需要预配置允许列表时,可以通过 ToolApprovalAgentOptions.AutoApprovalRules 提供自动审批规则。不同预览版本调整过规则委托的上下文类型,应以项目引用版本的 API 为准。

运行时规则可以按工具名放行,也可以记录“工具名 + 完全相同参数”的组合。后者比永久批准整个工具更窄,适合“以后允许读取这个目录”或“以后允许查询这个项目”一类场景。框架提供 CreateAlwaysApproveToolResponseCreateAlwaysApproveToolWithArgumentsResponse,分别创建这两种决定。

多个待审批请求出现时,ToolApprovalAgent 会一次向调用方返回一个,其余请求保存在会话状态中。应用处理完当前请求后再收到下一个,不需要同时展示一组确认框。

自动批准规则应该使用允许列表。ToolApprovalAgent.AllToolsAutoApprovalRule 会放行所有工具,实际效果等同于关闭审批,只适合完全可信的测试环境。生产系统通常采用分级策略:只读且低敏感度的工具自动通过,有副作用或会返回敏感数据的工具逐次确认。

审批也不是授权系统。用户批准只说明他接受这次操作,工具内部仍要校验调用者身份、资源权限和参数范围。Prompt 注入还可能误导用户点击批准,因此不可逆操作最好同时提供预览、二次确认、限额或延迟撤销机制。


CodeAct 解决什么问题

直接工具调用适合步骤少、边界清楚的任务。处理表格时,模型可能需要反复调用读取、过滤、排序和聚合工具,每一步都要把中间结果送回模型,不仅增加延迟和 token 消耗,也让原本简单的循环被拆成许多轮对话。

CodeAct 换了一种执行方式:向模型提供 execute_code,让模型把循环、分支和数据转换写成一段代码,再由受控运行时一次执行。Provider 还可以把少量宿主工具通过 call_tool(...) 暴露给代码。

这种方式适合以下任务:

  • 多个只读工具需要循环或条件组合。
  • 中间数据较大,不适合反复放入模型上下文。
  • 需要生成报表、图表或其他文件。
  • 希望把文件和网络能力集中在隔离环境中。

如果任务只有一两次调用,CodeAct 不会带来明显收益。转账、删除、发布等高风险工具也不宜藏在一段长代码里,因为用户很难审查完整执行路径。这些工具更适合保留为直接调用,并逐次审批。

需要特别区分两个概念:execute_code 是 Agent 看见的工具,真正的安全边界由它背后的运行时提供。仅仅把代码放进子进程,并不等于获得了沙盒。


选择代码执行环境

.NET SDK 提供 Hyperlight 和 LocalCodeAct 两种 CodeAct Provider。它们的使用方式相似,隔离强度却完全不同。

维度HyperlightLocalCodeAct
隔离边界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 校验默认阻止 evalexecsubprocesssocket 等常见危险入口。自定义 AllowedImportsBlockedImportsAllowedBuiltinsBlockedBuiltins 时,新列表会替换默认值,而不是追加。ValidationDisabled = true 会移除这层检查,只能在代码完全可信或外层已有强隔离时使用。

LocalCodeAct 看到的仍是真实宿主路径,网络能力也不会自动隔离。它应该运行在专用容器、Azure Container Instances、虚拟机或等价的隔离环境中,由基础设施限制进程、文件、网络和凭据。不要在开发者工作站或生产宿主机上直接执行来自用户、网页、邮件或 RAG 文档的不可信代码。

如果底层模型服务支持托管代码解释器,也可以把代码执行交给服务端。这样不用维护本地运行时,但代码和输入文件会发送给服务提供商,仍需评估数据驻留、保留策略、网络能力和提供商支持范围。托管工具与 CodeAct Provider 不是同一套 API,不能把两者的配置混用。


组合审批与沙盒

审批回答“这次操作是否得到人的允许”,沙盒回答“即使代码有问题,它最多能影响什么”。典型执行路径如下:

用户输入
   │
   ▼
Agent 选择工具
   │
   ├── 普通只读工具 ────────────────► 参数校验后执行
   │
   ├── 高风险工具 ─► 审批 ─────────► 授权与参数校验后执行
   │
   └── execute_code ─► 审批 ───────► 沙盒或外部隔离环境
                                      │
                                      ├── 文件挂载允许列表
                                      ├── 网络出站允许列表
                                      └── 时间、内存和输出限制


Hyperlight 的 CodeActApprovalMode.AlwaysRequire 会让每次代码执行都请求批准。默认的 NeverRequire 并不表示永远跳过审批:如果 Provider 注册的任一宿主工具被 ApprovalRequiredAIFunction 包装,execute_code 也会随之要求审批,因为代码可能在内部调用该工具。

这个传播机制只能判断代码“可能访问需要审批的工具”,不能让用户逐条批准代码里的每一次 call_tool(...)。因此,审批页面应展示完整代码、可访问的宿主工具、文件挂载和网络范围。对转账、删除等高风险动作,最清楚的做法仍是把它们留在 CodeAct 之外。

实际部署时,可以按下面的顺序收紧权限:先决定任务是否真的需要代码执行,再选择隔离边界,随后只开放必要文件、域名和宿主工具,最后为不可逆操作增加审批。下一篇会继续处理这里尚未解决的问题:如何跟踪进入上下文的数据是否可信,以及怎样用评估持续验证这些防线。

工具审批与 CodeAct 沙盒执行 | 猫咪文档