智能体嵌入

工具授权

智能体调用工具前要不要先问用户,以及怎样在你的工具服务端按终端用户鉴权。

智能体能调用你的系统:查订单、改地址、申请退货。这一页讲两件事:哪些调用要先让用户确认,以及真正的权限在哪里卡住。

询问不是安全手段

确认卡只是让终端用户确认「这是我想做的」,防的是智能体理解错了意图。它挡不住有意构造的请求:用户本人就是在和智能体对话,也可以在页面上改任何前端数据。权限必须由你的工具服务端按终端用户鉴权,见下文。

两档策略

每个工具有一个权限策略,只有两档:

策略行为
always_allow智能体直接调用,不打断用户
always_ask调用前暂停,在对话里显示确认卡,用户选择允许或拒绝后才继续

没有第三档。写入 auto 会被拒绝(auto 尚未支持)。

策略写在智能体定义的 tools 里,MCP 工具集可以设整体默认,也可以逐个工具覆盖:

{
  "type": "mcp_toolset",
  "mcp_server_name": "orders",
  "default_config": { "permission_policy": { "type": "always_ask" } },
  "configs": [
    { "name": "list_orders", "permission_policy": { "type": "always_allow" } },
    { "name": "request_return", "permission_policy": { "type": "always_ask" } }
  ]
}

优先级:单个工具的 configs[].permission_policy,然后是 default_config.permission_policy,都没写则使用默认值。

默认值

工具类型没有写策略时
MCP 工具(mcp_toolset)annotations.readOnlyHint === true 时为 always_allow;缺失、false 或非布尔值时为 always_ask
内置工具集(agent_toolset_20260401)工具集的 default_config 没写时为 always_allow
前端工具(type: "custom")没有确认步骤。它由你页面里的代码执行,见前端工具

此缺省规则适用于所有智能体的 MCP 工具。hint 来自 MCP 服务器,是不可信的提示,只影响缺省;单工具和工具集的显式配置始终优先。询问同样不是安全手段。给每个工具设策略时,按「用户自己能不能撤销、做错了代价多大」来决定:查询类放行,退款、删除、发送类保留询问。

确认卡

当某个调用的策略是 always_ask,对话会暂停,Kit 在对话里显示确认卡。终端用户选择允许或拒绝:

  • 允许:调用继续,结果回到智能体。
  • 拒绝:调用不执行,对话继续。

确认卡由 Kit 自带,你不需要写界面。会话预算用完后,会话不再接收新的任务,但仍然接收工具确认和中断。需要在自有界面里处理确认的宿主,可以用 API 发送 user.tool_confirmation 事件,字段为 tool_use_id 和允许 / 拒绝,详见 API 参考。

真正的安全:在工具服务端鉴权

智能体调用你的 MCP 服务器时,平台会带上三个请求头,说明这次调用代表谁:

请求头含义
X-Praxis-External-User-Id签发令牌时传入的 external_user_id,即你系统里的终端用户
X-Praxis-Agent-Id智能体 id
X-Praxis-Session-Id会话 id

这三个头只在会话属于某位终端用户时才带;没有终端用户的会话不带,你的服务要把缺失当作拒绝。它们来自平台对令牌的验证,不是模型可以填写的参数,所以是你唯一应该信任的身份来源。

规则:

  1. 身份只取请求头。不要让工具接收 user_id 之类的参数再相信它:模型可以被诱导填任何值。
  2. 每次查询都按用户限定范围。查询条件里带上用户,而不是查出来再过滤;操作一个具体对象(订单、地址)前先确认它属于这个用户。
  3. 缺少身份就拒绝,返回错误,不要退化成匿名访问。
  4. 服务本身也要认证调用方。MCP 服务器是公网地址,请求头可以被伪造;在智能体定义里为 MCP 服务器配置 headers(例如 Authorization: Bearer …),服务端先校验这个凭据,再信任 X-Praxis-* 头。
  5. 危险动作在服务端再检查一遍业务规则(订单是否已签收、是否超过退货期),不要依赖「用户在确认卡上点过允许」。

示例:一个按终端用户限定范围的订单查询和退货工具(与 MCP 实现无关的部分)。

// 你的 MCP 服务器:每个请求先认证,再按终端用户执行
type Caller = { userId: string };

function authenticate(request: Request): Caller | Response {
  // 1) 认证 Praxis:智能体定义里为这个 MCP 服务器配置的 Authorization 头
  if (request.headers.get("authorization") !== `Bearer ${process.env.PRAXIS_MCP_SECRET}`) {
    return new Response("Unauthorized", { status: 401 });
  }
  // 2) 终端用户身份:只信平台附带的请求头
  const userId = request.headers.get("X-Praxis-External-User-Id");
  if (!userId) return new Response("Missing end-user identity", { status: 403 });
  return { userId };
}

// list_orders:只读,没有任何用户可选的参数
async function listOrders(caller: Caller) {
  return db.orders.findMany({ where: { userId: caller.userId } }); // 范围在查询里
}

// request_return:参数 order_id 来自模型,必须再校验归属和业务规则
async function requestReturn(caller: Caller, input: { order_id: string; reason: string }) {
  const order = await db.orders.findFirst({
    where: { id: input.order_id, userId: caller.userId }, // 不属于该用户就查不到
  });
  if (!order) return { isError: true, text: "订单不存在" }; // 与「不属于你」不作区分
  if (order.status !== "delivered") return { isError: true, text: "只有已签收的订单可以退货" };
  return db.returns.create({ data: { orderId: order.id, reason: input.reason } });
}

db 表示你自己的数据访问层。要点是:userId 永远来自 X-Praxis-External-User-Id,order_id 虽然由模型提供,但在带着 userId 的查询里才有效,别人的订单号查不出东西。

仓库里的「好物集」演示站(apps/kit-demo)的订单 MCP 就是这样做的:list_orders 声明 readOnlyHint: true,request_return 声明 destructiveHint: true,两者都从同一个请求头取用户。

选择策略的建议

工具建议
只读查询,且服务端已按用户限定范围always_allow
会改变数据,用户可以自己撤销视情况,确认卡能减少误操作
不可撤销、涉及金额或对外发送always_ask,同时服务端再校验
前端工具没有确认卡。不要让它执行不可撤销的动作,需要时在你自己的界面里弹确认

下一步:安全 · 传入上下文

导出

本页内容