处理审批与用户输入

使用 allowedTools/disallowedTools 规则和 canUseTool 回调来控制代理可以使用哪些工具以及如何处理权限请求。

默认情况下,SDK 会强制执行工具权限,不会绕过它们。canUseTool 回调使您可以对尚未通过 allowedTools/disallowedTools 规则或显式权限模式(如 bypassPermissions)解决的权限请求进行精细控制。

默认情况下,SDK 不会提供交互式权限提示。如果一个需要权限检查的请求到达了 SDK 且未提供 canUseTool,则该请求失败。

工作原理

当您提供 canUseTool 回调时,SDK 会在每次执行需要权限检查的工具之前调用它。您的回调决定后续行为:

  1. 代理决定使用一个需要权限检查的工具。

  2. SDK 使用工具名称、请求输入和权限上下文调用您的 canUseTool 回调。

  3. 您的回调返回 allowdeny

  4. 工具执行相应地继续进行或被阻止。

对于许多常规的工具权限检查,回调输入包含诸如 { action, resource } 之类的字段。SDK 路由的伪工具(如 AskUserQuestionExitPlanMode)包含特定于工具的字段。

如果未提供 canUseTool 且需要权限检查的请求到达了 SDK,则 SDK 会返回一个错误,而不是以交互方式提示。

基本示例

import { createCortexCodeSession } from "cortex-code-agent-sdk";

const session = await createCortexCodeSession({
  cwd: process.cwd(),
  canUseTool: async (toolName, input, context) => {
    console.log(`Tool requested: ${toolName}`, input);

    // Allow read-only tools, deny destructive ones
    if (["Read", "Glob", "Grep"].includes(toolName)) {
      return { behavior: "allow" };
    }

    return { behavior: "deny", message: "Only read-only tools are allowed" };
  },
});

await session.send("What files are in this directory?");
for await (const event of session.stream()) {
  if (event.type === "assistant") {
    for (const b of event.content) {
      if (b.type === "text") process.stdout.write(b.text);
    }
  }
  if (event.type === "result") break;
}
await session.close();

响应模式

允许工具调用

返回 { behavior: "allow" } 以允许工具使用其原始输入执行:

canUseTool: async (toolName, input, context) => {
  return { behavior: "allow" };
}

拒绝工具调用

返回 { behavior: "deny" } 并附带可选消息。代理会看到拒绝消息,并可以调整其方法:

canUseTool: async (toolName, input, context) => {
  if (toolName === "Bash") {
    return { behavior: "deny", message: "Bash commands are not allowed in this session" };
  }
  return { behavior: "allow" };
}

AskUserQuestion 工具

Cortex Code 有一个内置的 AskUserQuestion 工具,代理在需要向用户澄清问题时使用该工具。当代理调用 AskUserQuestion 时,SDK 会将其通过您的 canUseTool 回调进行路由,并将 toolName 设置为 "AskUserQuestion"。输入中包含代理的问题,以结构化的多选选项形式呈现。

处理问题

在您的 canUseTool 回调中检查 toolName === "AskUserQuestion"input 包含一个 questions 数组,其中每个问题包含以下内容:

字段

描述

question

要显示的完整问题文本

header

问题的短标签

options

选项数组,每个选项包含 labeldescription``freeForm``(布尔值)和 ``isCancel``(布尔值)

multiSelect

如果为 true,用户可以选择多个选项

返回 allow 并附带一个 updatedInput,其中包含原始的 questions 和一个 answers 映射。每个键是问题文本,每个值是被选中的选项标签。对于多选问题,使用 ", " 连接标签。

const session = await createCortexCodeSession({
  cwd: process.cwd(),
  canUseTool: async (toolName, input, context) => {
    if (toolName === "AskUserQuestion") {
      const answers: Record<string, string> = {};
      for (const q of input.questions) {
        // Present q.question and q.options to the user, collect their choice
        const selected = await promptUser(q.question, q.options);
        answers[q.question] = selected;
      }
      return {
        behavior: "allow",
        updatedInput: { questions: input.questions, answers },
      };
    }
    return { behavior: "allow" };
  },
});

要拒绝一个问题(取消交互),请返回 deny

return { behavior: "deny", message: "User cancelled" };

小技巧

AskUserQuestion 通过 canUseTool 进行路由。如果没有回调,交互将无法完成,并且请求会报错,而不是静默继续。

ExitPlanMode 工具

当会话处于 plan 模式时,Cortex Code 会保持在计划状态,直到计划获得批准。批准请求会通过您的 canUseTool 回调进行路由,并将 toolName/tool_name 设置为 "ExitPlanMode"

输入包含:

字段

描述

plan

代理想要执行的建议计划文本

question

代理提供的可选附加批准提示

批准或拒绝计划

返回 allow 以批准计划。您可以选择性地包含 updatedInput.message,以便在代理离开计划模式之前将审核上下文传递回代理。

返回 deny 以拒绝计划。当您希望代理根据具体反馈修改计划时,请使用 message。拒绝 ExitPlanMode 会使当前轮次保持在计划模式,以便代理更新计划并再次询问。

批准 ExitPlanMode 将结束当前轮次的计划模式。后续轮次将恢复会话的正常非计划权限行为,因此后续的工具调用将按照与计划模式外相同的方式进行评估。

审核循环示例

最简单的审核循环是:先用具体的反馈拒绝一个不完善的计划一次,然后批准修改后的计划:

let rejectedOnce = false;

const session = await createCortexCodeSession({
  cwd: process.cwd(),
  permissionMode: "plan",
  canUseTool: async (toolName, input) => {
    if (toolName === "ExitPlanMode") {
      const plan = String(input.plan ?? "");
      if (!rejectedOnce) {
        rejectedOnce = true;
        return {
          behavior: "deny",
          message: "Add a verification step and say which file you will edit.",
        };
      }
      return {
        behavior: "allow",
        updatedInput: {
          message: `Approved plan: ${plan}`,
        },
      };
    }
    return { behavior: "allow" };
  },
});

基于规则的权限

对于常见的权限模式,您可以使用基于规则的配置,而不是编写回调逻辑。allowedToolsdisallowedTools 选项让您可以定义自动批准或阻止的工具列表:

const session = await createCortexCodeSession({
  cwd: process.cwd(),
  allowedTools: ["Read", "Glob", "Grep", "Bash(npm test:*)"],
  disallowedTools: ["Write"],
});

allowedTools 条目可以包含模式。例如,Bash(npm test:*) 会自动批准任何匹配该模式的 Bash 命令。disallowedTools 条目会完全阻止特定的工具。

这些规则由 CLI 在 canUseTool 回调之前进行评估。如果某个工具匹配允许或不允许的规则,则不会为该工具调用回调。

与权限模式结合使用

canUseTool 回调与权限模式一起使用。当两者都设置时,CLI 的内置权限模式过滤器会首先运行,然后您的回调会处理任何剩余的需要批准的工具调用。

可用的权限模式包括:

模式

描述

default

使用标准权限检查。在 SDK 会话中,使用 allowedToolsdisallowedToolscanUseTool 控制需要权限检查的工具。

autoAcceptPlans

自动批准计划请求及计划退出确认。它不会绕过常规工具权限。

plan

代理会计划更改,但未经批准不会执行这些更改。使用 canUseTool 时,计划模式下的批准会通过 ExitPlanMode 进行路由。拒绝该请求会使计划保持活跃状态;批准该请求则会退出计划模式,后续轮次恢复普通权限。

bypassPermissions

跳过所有权限检查。需要 allowDangerouslySkipPermissions: true (TypeScript) 或 allow_dangerously_skip_permissions=True (Python) 作为安全标志。

在会话期间更改权限模式

您可以在会话启动后更改权限模式。TypeScript 在 QueryCortexCodeSession 上都公开了 setPermissionMode()。Python 在 CortexCodeSDKClient 上公开了 set_permission_mode()

更新后的模式适用于控制请求处理完成后的后续轮次。它不会追溯更改已经在运行的工具调用。

const session = await createCortexCodeSession({
  cwd: process.cwd(),
});

await session.setPermissionMode("plan");
await session.send("Review the repo and propose a plan before editing.");

await session.setPermissionMode("default");
await session.send("Now implement the approved change.");

Hook

Hook 允许您在不同的生命周期阶段拦截工具执行。与仅控制工具是否运行的 canUseTool 不同,Hook 可以检查工具结果、注入上下文以及控制代理流程。

import { createCortexCodeSession } from "cortex-code-agent-sdk";

const session = await createCortexCodeSession({
  cwd: process.cwd(),
  hooks: {
    PreToolUse: [
      {
        matcher: "Bash",
        hooks: [
          async (input, toolUseId, context) => {
            console.log(`About to run Bash: ${input.tool_input?.command}`);
            return {};  // Allow execution to proceed
          },
        ],
      },
    ],
    PostToolUse: [
      {
        hooks: [
          async (input, toolUseId, context) => {
            console.log(`Tool ${input.tool_name} completed`);
            return {};
          },
        ],
      },
    ],
  },
});

Hook 事件

事件

触发时机

PreToolUse

在工具执行之前。可以阻止执行或修改输入。

PostToolUse

工具成功完成后。

UserPromptSubmit

当用户提示被提交时。

Stop

当代理停止时。

SubagentStop

当子代理停止时。

Notification

当代理发出通知时。

PermissionRequest

当发生工具权限请求时。

PreCompact

上下文压缩之前。

Hook 条目上的 matcher 字段是可选的。如果提供了该字段,它会按事件的匹配值进行过滤:PreToolUsePostToolUsePermissionRequest 匹配工具名称;Notification 匹配通知类型;PreCompact 匹配触发器。如果省略,则该 Hook 会针对该事件的所有值触发。

有关完整的 Hook 输入和输出类型定义,请参阅 TypeScript SDK 参考 以及 Python SDK 参考

选择方法

方法

何时使用

带有安全标志的 permissionMode: "bypassPermissions"

适用于沙盒环境、CI 管道或完全可信且无需审核工具调用的场景。需要 allowDangerouslySkipPermissions: true (TypeScript) 或 allow_dangerously_skip_permissions=True (Python)。

allowedTools / disallowedTools

当您可以将权限策略表示为允许或阻止工具的静态列表时

canUseTool 回调

需要在执行前对工具调用进行审计、筛选或修改的生产系统。

仅权限模式(无回调)

autoAcceptPlans 足够且您不需要自定义回调处理时

Hook

当您需要观察工具结果并做出响应、注入上下文或控制允许/拒绝决策之外的代理流程时

备注

默认情况下,SDK 不会绕过权限,也不会以交互方式提示。如果一个需要权限检查的请求到达了 SDK 且未提供 canUseTool,则该请求失败。要绕过权限,您必须使用适当的安全标志显式设置 permissionMode: "bypassPermissions"