跳到正文

Coding Agent 原理与实践

08:权限与信任——怎样控制 Agent 的执行风险

更新于:

作者:折腾客

约 5,567 字183 行代码

文章目录

1. Agent 的执行风险从哪里来?

让 Coding Agent 修复一个项目时,我们通常希望它读取代码、修改相关文件,再运行测试。但在这个过程中,有两类风险需要留意。

一类来自项目附带的资源。 刚克隆的仓库中,可能带有 Agent 配置、扩展或 Skill。文字内容可能影响模型的判断,扩展和脚本还可能直接执行代码。如果启动 Agent 时就自动加载它们,用户还没提交任务,项目中的代码可能已经开始访问本机资源。

另一类来自执行任务时的工具操作。 为了修复测试,Agent 可能尝试修改全局配置,或者下载并运行一个脚本。这些操作可能影响其他项目,甚至把本地数据发送到外部,超出用户对这次任务的预期。

本篇要弄明白的是:Agent 怎样控制资源加载和工具执行的风险,减少非预期操作,并限制操作可能造成的影响? 下面先看 Ink 怎样处理这两类风险、还存在哪些局限,再结合主流 Agent 的做法,讨论怎样补上这些限制。

2. 加载项目资源时,怎样控制风险?

为了适应不同项目,Coding Agent 通常允许项目提供自己的配置和工作规则。例如,Claude Code 支持在项目的 .claude/settings.json 中配置 Hooks,让指定命令在会话开始、工具执行前后等时刻运行。这样,团队就可以把一些自动化处理随项目一起维护。Claude Code 配置与 Hooks 文档

Ink 也支持加载项目资源。例如,.ink/extensions 中的扩展是 JavaScript 代码。启动时加载扩展,就会执行其中的代码,不必等到模型调用工具。 下面节选自 loadExtensions:

// 文件:src/extensions/loader.ts
// 所在函数:loadExtensions,遍历待加载扩展的循环内部。
// 从项目 .ink/extensions 目录自动发现的扩展,需通过项目信任检查才会进入这里。
// 全局扩展和通过 -e 显式指定的扩展不受这道检查控制。
// candidate.path 是扩展文件路径。省略类型、导出校验、注册管理和异常处理。

// 导入模块时,就会执行模块顶层代码。
mod = await import(pathToFileURL(candidate.path).href);
const factory = mod.default;

// 随后调用默认导出的初始化函数,这也会执行扩展自己的代码。
await factory(
  extensionApi(
    registry,
    {
      cwd: options.cwd,
      version: VERSION,
      origin: candidate.origin,
      name: candidate.name,
    },
    report
  )
);

extensionApi(...) 为扩展提供注册工具、订阅事件等接口,但扩展仍然是运行在 Ink 进程中的 JavaScript 代码。例如,某个扩展在模块顶层或初始化函数里写了读取本机配置、发送到外部服务器的逻辑,加载它就可能触发数据外传,此时模型还没有开始处理任务。

这与 AGENTS.md 这类文字规则不同:文字通过影响模型来影响后续操作,扩展代码则可以直接使用当前进程的权限访问文件或网络。

对受项目信任检查控制的资源,Ink 会先根据用户指定的决定或已有信任记录,判断是否允许加载;需要确认且支持交互时,再询问用户。获准后,才加载这些资源,包括其中的项目扩展。

项目资源:先检查信任,再决定是否加载

Agent 启动
项目信任检查是否允许加载受这道检查控制的项目资源?
↓ 允许加载这些项目资源其中的扩展代码在此时执行
↓ 拒绝或未获准跳过这些项目资源不加载其中的扩展代码

↓ 两条路径均继续启动

准备处理任务
信任检查先采用显式选项或已有记录;需要询问且支持交互时,再由用户决定。 本图只展示受信任检查控制的项目资源,不包括用户自行安装的全局资源。

下面来看 Ink 怎样判断项目是否受信任,以及这道检查控制哪些资源的加载。

2.1 信任检查决定加载什么

Ink 的 trustRequiringResources 会检查项目中的以下资源:

项目资源为什么需要先决定是否加载
.ink/extensions扩展可以执行代码、注册工具和处理函数
.ink/agents、.ink/commands、.ink/skills提供子代理配置或影响模型行为的内容
.ink/settings.json改变模型和运行设置
.ink/SYSTEM.md、.ink/APPEND_SYSTEM.md替换或补充系统指令
项目范围内的 .agents/skills加载技能;查找范围包括到 Git 根目录之间的祖先目录
项目目录中的 .mcp.json、mcp.json指定要启动的本地 MCP 服务或要连接的远端服务

MCP 服务向 Agent 提供外部工具。项目里的 MCP 配置虽然是 JSON,也可能指定启动本地进程的命令,或连接远端服务的地址。因此,Ink 会在采用这些项目配置前检查信任决定;未获信任时,跳过这两个项目配置文件。MCP 工具怎样接入,留到第 12 篇展开。

发现这些资源后,启动流程调用 resolveProjectTrust 决定是否放行。下面节选它的判断逻辑:cwd 是当前工作目录,resources 是筛选后需要信任的项目资源列表。

// 文件:src/cli.ts
// 所在函数:resolveProjectTrust
// 节选函数体;省略 cwd、resources 的准备、界面提示与返回值类型。
if (resources.length === 0) return true;
const store = defaultTrustStorePath(homedir());

// 优先采用用户通过 --trust / --no-trust 明确指定的决定。
// setTrust 的第四个参数 true 允许重建损坏的信任记录文件。
if (opts.trustDecision === "trust") {
  setTrust(store, cwd, true, true);
  return true;
}
if (opts.trustDecision === "no-trust") {
  setTrust(store, cwd, false, true);
  return false;
}

// 没有显式指定时,查找当前目录及其最近祖先的记录。
const entry = nearestTrustEntry(readTrustFile(store), cwd);
if (entry !== null) {
  return entry.trusted;
}

if (interactive) {
  const answer =
    askTui !== undefined
      ? await askTui(cwd, resources)
      : await askTrustOnTty(cwd, resources);
  if (answer === null) return false; // 取消:本次不加载,不保存决定。
  if (answer === "session") return true; // 仅本次信任,不保存决定。

  // TUI 返回 "yes" / "no",文本确认返回 true / false。
  const trusted = answer === true || answer === "yes";
  setTrust(store, cwd, trusted);
  return trusted;
}

// 非交互模式且没有信任记录:本次不加载,不记录永久拒绝。
return false;

拒绝时,加载器会跳过受这道检查控制的项目资源;不是退出 Agent,也不是禁止读取仓库代码。用户自己安装的全局资源,以及通过 -e 明确指定的扩展,不受项目扩展目录的这道检查控制。

普通 AGENTS.md 在当前 Ink 中也不受这道信任检查控制。它仍可能进入模型上下文,因此不能把“项目未受信任”理解成“项目中的文字不会影响模型”。

2.2 信任决定怎样保存和继承?

长期决定保存在 ~/.ink/trust.json。例如下面这份示意记录,信任 /work/team,但拒绝它下面的一个目录:

{
  "/work/team": true,
  "/work/team/unreviewed": false
}

Ink 从当前目录向上查找,遇到最近的一条记录就停止:

// 文件:src/core/trust.ts
// 所在函数:nearestTrustEntry
// 节选函数体;canonicalizeDir 优先使用 realpath 统一路径。
let current = canonicalizeDir(dir);
for (;;) {
  const value = data[current];
  if (value === true || value === false) {
    return { path: current, trusted: value };
  }
  const parent = dirname(current);
  if (parent === current) return null;
  current = parent;
}

打开 /work/team/app 时,会继承 /work/team 的信任;打开 /work/team/unreviewed/demo 时,则先遇到更近的拒绝记录。realpath 让同一目录的符号链接入口与真实路径尽量对应到同一份决定。

2.3 信任项目之后,还有哪些风险?

这道检查能阻止尚未获准的项目资源被自动加载,但它没有审查资源的内容,也没有限制加载后的代码能做什么:

  • 信任范围可能大于眼前的项目。 信任一个父目录,可能连同它以后新增的子项目一起放行。
  • 资源变化不会自动触发重新确认。 当前记录只保存目录与布尔值,不绑定扩展内容或版本。
  • 获准加载的扩展仍使用当前用户权限。 即使后面设置了工具审批,扩展直接调用文件或网络 API,也不必经过 Agent 循环中的工具检查。

所以,“信任这个项目”表示同意启用指定资源,不能据此判断这些资源的后续行为都安全。任务开始后,模型提出的具体操作还需要单独检查。

3. 执行工具操作时,怎样控制风险?

回到修复测试的场景。当前项目在 /work/app,Agent 却准备修改 /work/shared/test-config.json。这份配置可能被其他项目共用,修改它虽然能修好当前测试,也可能影响其他项目。

控制这类风险的思路,是在工具执行之前检查操作及其目标,根据规则决定直接放行、请求用户批准,还是拒绝执行。Ink 的 Agent 循环为此提供了执行前钩子 onToolCall。开发者可以在扩展中通过 api.on("tool_call", handler) 注册检查逻辑;工具执行前,onToolCall 会触发这些处理函数,等待检查结果后再决定是否执行工具。具体的扩展接入过程会在第 09 篇介绍。

Ink 参考了 pi 的扩展设计,把具体的风险判断规则交给扩展实现,默认不启用这类规则。 仓库附带的 Guardian 扩展示例 读取 ~/.ink/guardian.json,按其中的 deny 和 ask 规则检查 bash、write、edit 调用。扩展需要安装并成功加载,配置中也需要有相应规则,才会阻止或询问这些操作。 下面的共享配置写入示例,假设用户已配置了对应的 ask 规则。

一次项目外写入,怎样经过执行前检查?

Agent 循环收到 write 调用目标:/work/shared/test-config.json
查找工具、校验参数失败时直接返回错误;通过后进入策略检查
onToolCall:执行前钩子触发扩展注册的 tool_call 处理函数,等待检查结果
Guardian:匹配配置规则本例未命中 deny,命中共享目录的 ask 规则
↓ 用户允许执行 write把实际输出或执行错误交回循环
↓ 拒绝、取消或超时不执行 write把拒绝原因作为工具结果交回循环
本例假设已配置共享目录的 ask 规则,且仅加载 Guardian 检查。嵌套表示调用关系,Guardian 是扩展提供的检查逻辑,并非 onToolCall 的内置规则。 其他调用可能直接放行或拒绝,不一定进入询问。

3.1 Agent 循环怎样等待检查,并决定是否执行?

先看 loop.ts 中的 executeToolCall。prepareToolCall 完成工具查找与参数校验,通过后把执行入口保存在 prepared.run 中。此时工具还没有执行,循环会先等待 onToolCall 的检查结果:

// 文件:src/core/loop.ts
// 所在函数:executeToolCall
// 简化节选:省略签名与取消后的等待封装,末尾只展示执行入口。
// 工具查找、参数校验见第 03 篇。
const call: ToolCallRef = { id, name, arguments: args };
const prepared = prepareToolCall(call, toolMap);
if ("result" in prepared) return prepared.result;

// 等待执行前检查;钩子可以异步等待用户回答。
const decision = await onToolCall?.({
  toolCallId: id,
  name,
  args: args as Record<string, unknown>,
});
if (decision?.block) {
  // 被拒绝时,不调用工具,直接返回错误结果。
  return {
    toolCallId: id,
    toolName: name,
    content: `Tool "${name}" blocked by ${blockSource(decision)}: ${decision.reason ?? "no reason given"}`,
    isError: true,
  };
}
return prepared.run(signal);

这段代码的关键在于检查和执行的顺序:

  1. await onToolCall(...) 等待检查完成。在此期间,这次工具操作不会开始执行。
  2. 如果返回的 decision 中有 block: true,直接返回拒绝原因,不执行工具。
  3. 没有接入钩子,或检查未要求阻止时,才调用 prepared.run(signal) 执行工具。

这个钩子怎样找到扩展注册的检查?runner.ts 在调用 Agent 循环时传入 onToolCall,让它把本次调用交给扩展系统的 emitToolCall:

// 文件:src/runner.ts
// 所在函数:RunnerImpl.runTurnInner
// 节选 runAgentLoop 的调用;省略模型、历史等其他参数,不能单独运行。
const result = await runAgentLoop({
  onToolCall: call =>
    this.options.extensions?.emitToolCall({
      type: "tool_call",
      ...call, // 本次工具调用的 ID、名称和参数。
      cwd: this.options.cwd, // 让扩展按当前工作目录解释相对路径。
    }),
});

emitToolCall 再从已注册的处理函数中,取出监听 tool_call 的函数,逐个等待它们返回检查结果:

// 文件:src/extensions/registry.ts
// 所在函数:ExtensionRegistry.emitToolCall
// 节选函数体;省略类型断言及异常处理。实际实现还会拦截处理函数抛出的异常。
for (const stored of this.handlers) {
  if (stored.event !== "tool_call") continue;
  const decision = await stored.handler(event);
  if (decision?.block === true) return decision; // 首次拒绝后,不再继续检查。
}
return undefined; // 所有检查都没有要求阻止这次调用。

处理函数按扩展加载时的注册顺序执行。一个扩展没有阻止调用,并不代表工具立即执行;后面的检查仍可能拒绝它。 所有检查都未阻止调用时,Agent 循环才继续执行 prepared.run(signal)。

3.2 Guardian 怎样检查一次文件写入?

Guardian 通过 api.on("tool_call", handler) 注册检查函数。调用时,event.name 是工具名称,event.args 是调用参数。它把 Bash 命令文本或写入目标的绝对路径交给规则匹配器,依次检查:

  1. 先匹配 deny:命中就直接拒绝,不进入用户询问。
  2. 再匹配 ask:只有没有命中 deny 时,才查找需要用户批准的规则。
  3. 都未命中则不阻止:Guardian 放行,由调用层继续其余检查和执行。

因此,目标位于工作目录之外,并不会自动触发询问;也没有一组不依赖配置的内置保护目录。要保护刚才的共享配置,可以在 ~/.ink/guardian.json 中写入下面这组规则。这是为本文场景编写的教学配置,不是 Guardian 的默认配置:

{
  "deny": [
    {
      "tool": ["write", "edit"],
      "regex": "^/work/shared/locked(?:/|$)",
      "reason": "受保护的共享配置不能由 Agent 修改"
    }
  ],
  "ask": [
    {
      "tool": ["write", "edit"],
      "regex": "^/work/shared(?:/|$)",
      "reason": "修改共享配置前需要确认"
    }
  ],
  "askTimeoutMs": 600000
}

这组配置让 /work/shared/test-config.json 命中 ask;而 /work/shared/locked/test-config.json 同时符合两类规则,优先命中 deny,不会弹出询问。askTimeoutMs 指定等待回答的时长,下一节会说明它的行为。

规则中的 tool 指定检查哪些工具,未填写时默认针对 bash。regex 匹配命令或路径文本;也可以使用 pattern,其中 * 可以跨越任意字符。pattern 是文本匹配,不是对 Shell 命令或文件系统权限的完整解析。

文件写入进入规则匹配前,会先根据调用所在目录解析路径:

// 文件:examples/extensions/guardian.mjs
// 所在位置:default 导出函数注册的 tool_call 回调
// 节选:已确认工具是 write/edit,text 在外层声明。
const target =
  typeof event.args?.path === "string" ? event.args.path : undefined;
const base = typeof event.cwd === "string" ? event.cwd : api.cwd;
text = target === undefined ? undefined : path.resolve(base, target);

// ……省略审计信息准备、try 包装与工具名称检查;此处 text 已有效。
const hit = match(tool, text);
if (hit === undefined) return undefined;
if (hit.kind === "deny") {
  // ……省略审计记录。
  return {
    block: true,
    reason:
      hit.rule.reason === ""
        ? `blocked by guardian rule: ${hit.rule.source}`
        : hit.rule.reason,
  };
}
// 后续处理 ask:等待确认,再决定是否阻止调用。

event.cwd 让相对路径按本次工具调用的工作目录解释;没有这个字段时,才退回扩展的 api.cwd。match 内部先遍历 deny,再遍历 ask,每类取第一条命中的规则。用户批准 ask 后,return undefined 仍只表示 Guardian 不阻止这次调用,不代表跳过其他扩展的检查。

仓库提供了 Guardian 配置模板,但它不会自动成为生效配置。配置文件不存在时,Guardian 使用零条规则;文件格式错误时,保留当前进程内最后一次成功加载的规则,并显示配置错误。如果此前从未成功加载,仍是零条规则。修改配置后可以用 /guardian reload 重载,用 /guardian status 查看规则数量与加载状态。

3.3 用户的回答怎样变成执行决定?

命中 ask 后,Guardian 通过 api.confirm 将审批问题交给终端交互层。这条正常询问路径提供 sessionKey,因此 Ink 的 TUI 显示三个选项:

  • 允许本次:本次检查通过,不保存批准记录。
  • 本次会话不再询问:本次检查通过,并记住 guardian:session;选项说明会指出,它覆盖本次会话中的全部 Guardian ask 提示。
  • 拒绝:本次检查不通过。取消选择也按拒绝处理。

返回结果现在有三种:true 表示批准,false 表示未批准,字符串 "timeout" 表示询问超时。只有严格等于 true 才能放行。 如果简单写成 if (outcome),非空字符串 "timeout" 也会被误当作批准。

下面节选 Guardian 的文件写入询问路径,text 是已经解析好的绝对路径,hit 是命中的 ask 规则:

// 文件:examples/extensions/guardian.mjs
// 所在位置:tool_call 回调的 ask 分支。
// 节选 write/edit 路径;省略 Bash 预览、审计记录与内部异常的确认分支。
const detail =
  hit.rule.reason === ""
    ? `guardian ask rule: ${hit.rule.source}`
    : hit.rule.reason;
const options = {
  sessionKey: "guardian:session",
  rememberLabel: "all guardian ask prompts this session",
  ...(askTimeoutMs !== undefined ? { timeoutMs: askTimeoutMs } : {}),
};
const outcome = await api.confirm(
  `allow this ${tool}?`,
  `${text}\n${detail}`,
  options
);
// ……省略审计记录,记录 approved、denied 或 timeout。
if (outcome === true) return undefined;
const declined =
  outcome === "timeout"
    ? askTimeoutMs === undefined
      ? "the confirmation timed out — the call was not approved"
      : `the confirmation timed out after ${humanDuration(askTimeoutMs)} — the call was not approved`
    : "the user declined this call";
return {
  block: true,
  reason:
    hit.rule.reason === "" ? declined : `${hit.rule.reason} — ${declined}`,
};

“本次会话不再询问”究竟覆盖什么,由 sessionKey 决定。Guardian 的正常 ask 路径统一使用 guardian:session,因此批准会被后续命中 ask 的 Bash 命令和文件写入复用,不限于某个文件、工作目录或某条规则。deny 仍然优先执行,不会被这份批准绕过。内部异常触发的兜底询问不携带这个键,也不会复用正常 ask 的会话批准。

这份记忆由 TtyConfirm.handler 的 sessionAllowed 集合保存。它只保存在当前进程中,不会写入会话文件,重启 Ink 后不会恢复;界面中的“本次会话”不能理解成对某一个持久化会话文件的授权。

前面的 askTimeoutMs: 600000 表示等待 10 分钟。TUI 从问题实际显示出来时开始计时,问题若排在其他弹窗后面,排队时间不计入。超时返回 "timeout",既不批准操作,也不记录会话批准;拒绝原因会明确说明是等待超时。未配置该字段时没有这条等待期限。当前超时能力只用于支持选择框的 TUI;旧版文本输入界面不执行这个期限,无法交互且没有可复用批准时则直接返回 false。

无论用户拒绝还是询问超时,Guardian 都会返回 { block: true, reason: ... }。Agent 循环把原因作为工具结果写入历史,让模型在下一次调用时知道这次操作没有执行。

3.4 Guardian 的检查和 Ink 的执行机制,还有哪些局限?

Guardian 负责匹配规则、决定是否请求批准,Ink 提供询问用户和阻止工具执行的机制。两者配合能拦截命中规则的调用,但仍有以下局限:

  • Guardian 的检查依赖加载和配置。 没有加载扩展或没有配置规则时,不会提供相应拦截。配置错误时,Guardian 保留上一次有效规则,不会自动切换为全部拒绝。
  • Guardian 的批准记忆范围比较粗。 Guardian 为正常 ask 提示使用同一个 sessionKey,Ink 按这个键记住批准。因此,选择“本次会话不再询问”后,不同命令或路径可能复用同一份批准;Guardian 的 deny 规则仍优先执行。
  • Guardian 的规则只能识别部分风险。 路径经 path.resolve 处理后仍是字符串匹配,Bash 也按命令文本匹配;Guardian 不会完整解析 Shell 行为,也不能保证识别符号链接或脚本内部的访问。
  • Ink 尚未提供操作系统级沙箱。 工具使用当前用户权限运行,Guardian 的执行前检查不会限制代码运行时的实际访问。一个获准运行的脚本,仍可能访问任务之外的文件或网络。

因此,执行前检查只是风险控制的一部分。要限制操作真正造成的影响,还需要约束它运行时能够访问的资源。

4. 生产级 Agent 怎样组织风险控制?

前面的实现展示了怎样接入检查和审批。但面对陌生项目、外部内容和长时间运行的任务,仅检查几个危险命令还不够。先看几种容易被忽略的情况,再看主流产品怎样应对。

4.1 看似正常的工作,风险藏在哪里?

项目文档把危险操作包装成必要步骤。 Agent 读取排障文档,文档声称“必须上传本机凭据才能完成诊断”。如果模型采纳了这条建议,就可能调用工具读取并发送敏感信息。这类通过不可信文字诱导模型改变行为的攻击,叫作提示注入。文档本身没有执行代码,也没有获得授权;需要防范的是模型把它当成了应当执行的要求。

扩展在模型开始工作前就执行了代码。 一个扩展获准加载后,在初始化时读取本地文件并尝试外传。如果它与 Agent 处于同一进程、使用相同权限,这些操作就可能不经过模型的工具调用检查。因此,审查模型准备调用的工具,还覆盖不了扩展加载阶段的行为。

获准运行的测试脚本做了额外的事。 用户同意运行 npm test,测试脚本却启动子进程,读取项目外的文件或连接外部服务器。命令名称看起来正常,也不代表它及其子进程的行为都在任务范围内。需要控制的是实际能访问哪些文件、网络和凭据。

一次批准被用到了其他操作上。 用户只想允许修改一份共享配置,系统却记成“这个会话允许所有项目外写入”。后续修改另一个文件时,也不再询问。这里需要明确批准对应的操作、目标和有效期,避免授权范围被无意扩大。

检查没有完成,操作却继续了。 审批服务超时,或必需的权限组件加载失败,系统把“没有返回拒绝”当作可以执行。对于必须先通过检查的操作,拿不到检查结果时,也应暂停或拒绝执行。

4.2 主流产品采用了哪些方案?

下面选取 Claude Code、Codex 和 Gemini CLI 的公开方案,重点看它们怎样组合不同的保护机制。具体保护范围取决于运行模式、平台与配置;支持某项能力,不代表所有场景下都会自动启用。

这里先解释一个共同概念:沙箱是受限制的执行环境,通过操作系统或容器等机制约束代码能够访问的资源。

在本地 Coding Agent 中,一种常见做法是把工具命令交给受限进程执行:macOS 可以使用 Seatbelt,Linux 可以通过 Bubblewrap 等工具配置操作系统的隔离能力。进程仍可使用本机的程序、依赖和允许访问的真实文件;当它尝试读写文件或连接网络时,操作系统会按沙箱规则允许或拒绝相应操作。具体实现可参阅 Anthropic 的沙箱运行库说明。

Claude Code:把信任检查、工具权限和命令隔离结合起来。

交互会话中,配置文件里的 Hooks 要等工作区信任确认后才会运行;-p 等非交互会话的处理不同,不能假设同样会弹出确认。工具调用则由权限规则决定允许、询问或拒绝,组合命令中的子命令会分别检查,避免批准前半段就放行整条命令。见 Hooks 的信任检查与工具权限规则。

对文档中的恶意指令,它结合上下文分析和权限检查降低提示注入风险;启用 Bash 沙箱后,再限制命令及其子进程的文件和网络访问。两者分别应对“模型被诱导”和“代码执行后越界”。但 Bash 沙箱并不覆盖所有执行路径:命令 Hooks 使用用户权限执行,内置文件工具也有自己的权限处理。见安全机制与沙箱范围。

Codex:将执行环境的限制与审批策略分开。

本地运行时,沙箱规定命令能够访问的资源,审批策略决定哪些操作需要进一步批准。例如,workspace-write 模式限制写入范围,并默认关闭命令的网络访问;即使工作区可写,其中的 .codex 等受保护目录仍保持只读,减少工具改动自身控制配置的机会。见审批、沙箱与受保护路径。

对于需要批准的动作,还可以配置独立的自动审查 Agent,结合具体操作和任务上下文判断数据外传、凭据探查等风险。这一层只处理需要审批的动作,不会替代沙箱,也不意味着所有调用都被额外审查。见 Auto-review。

Gemini CLI:用独立的策略引擎管理工具调用。

策略引擎按工具名称、参数和当前模式匹配规则,产生允许、拒绝或询问用户的决定,并用优先级处理规则冲突。需要询问的操作在非交互模式下按拒绝处理,避免因无人回答就自动放行。见策略引擎。

启用文件夹信任检查时,未受信任的工作区会进入受限模式,不加载项目设置和 .env 等内容;它也提供沙箱隔离。不过不同沙箱配置的能力并不相同,例如 macOS 的 permissive-open 配置限制写入,却允许较广的读取和网络访问。可见“用了沙箱”还不足以判断保护程度。见文件夹信任与沙箱配置。

4.3 这些方案背后的共同思路是什么?

从这些方案中,可以归纳出一个思路:先区分哪些来源可信,再明确允许哪些操作,并在代码执行时限制它能访问的资源。 模型或某一次审批即使判断失误,后续仍应有其他限制,减少可能造成的影响。

从加载到执行,逐层控制风险

加载前:检查资源来源决定启用哪些项目配置和可执行资源

↓ 获准的资源才进入后续流程

执行前:检查操作与授权明确操作、目标及授权范围,决定允许、拒绝或请求审批

↓ 需要审批时,获得有效批准后才继续

执行中:受限环境由系统限制命令及子进程可以访问的文件和网络

↓ 操作结束,或因访问限制而失败

返回结果与原因让 Agent 和用户知道实际发生了什么
设计思路示意,不代表某个产品的完整流程。必要检查失败或未获批准时,停止相关操作; Ink 当前尚未提供图中的操作系统级执行隔离。

这些限制还需要覆盖真正发生操作的地方:模型调用的工具、命令启动的子进程、直接执行的扩展代码,不能因为其中一条路径受到保护,就认为其他路径也受到保护。必要检查不可用时,应停止受它保护的操作;批准、拒绝和实际执行结果也应留下记录,便于核对。

5. 从加载前检查,到运行时限制

Ink 提供了项目信任检查,以及通过扩展接入工具审批的机制;要把这些能力发展成更完整的风险控制,还需要明确授权范围,并补上执行隔离等保护。

Guardian 也展示了扩展怎样参与 Agent 的工作流程。下一篇继续看扩展机制:外部模块怎样增加工具、命令和自定义处理逻辑。