源码仓库 · 有用请 Star

第 5 章 租户隔离停在网关的哪一层

alice 看不见 aurora-main,这是 AgentScope 已经做完的事。bob 的客服如果拿着海岸的虚拟密钥,网关仍会放行,因为网关不认识 alice。这一章划清两层隔离各管什么,并把「在 metadata 里写上 namespace」从隔离手段里删掉。

flowchart LR A["请求进入
agentscope.example.com"] --> B["namespace
seashore-main"] B --> C["选出海岸的
虚拟密钥"] C --> D["llm.example.com
核对 models"] D --> E{"别名在名单里?"} E -->|"seashore-cs"| F["放行并记
海岸团队的账"] E -->|"aurora-cs"| G["403
key_model_access_denied"]

本章导览图

Teams 页上的两家商家

网关里的商家,12 日的界面上就是两行。

Teams / Your Teams:aurora-main,id 5d1e432d-fbb1-4496-8a51-bea176084366,Organization 为横线,Members 1、Models 1、Keys 1,Team spend 不到 $0.01 / $30.00,My Spend $0.00 / Unlimited,创建于 Nov 12, 2026。seashore-main,id fbc26c00-fd5e-4e75-af6b-43c91ed76712,Members 1、Models 2、Keys 1,Team spend 不到 $0.01 / $50.00。模型列是数量,不列出别名

列 aurora-main seashore-main
这次库里的 id 5d1e432d-fbb1-4496-8a51-bea176084366 fbc26c00-fd5e-4e75-af6b-43c91ed76712
Organization - -
Members / Models / Keys 1 / 1 / 1 1 / 2 / 1
Team spend 不到 $0.01,上限 $30.00 不到 $0.01,上限 $50.00
My Spend $0.00 / Unlimited $0.00 / Unlimited

Models 是 1 和 2,不是 aurora-cs 这三个字。海岸的 2 对应 seashore-cs 与 seashore-cs-fast,极光的 1 对应 aurora-cs,这个对应在配置和钥匙页上,不在这一列的文字里。Keys 各 1,就是 bob 和 alice。Members 各 1,是签发时带上的代理默认用户,不是店主 alice,也不是店主 bob。第 3 章说过:members_with_roles 里的 default_user_id 不要写进商家成员表。

Organization 为空。本书没有建组织。My Spend 的 Unlimited 是当前登录管理员自己的花费上限,和团队的 $30、$50 不是同一个桶。Team spend 用 < $0.01 表示两家都已经有花费、但还没到一美分。钥匙页上的 $0.0001 和日志上的 $0.000062 是更细的两位读数,团队列把它们收进了「不到一美分」。

Available Teams、Default Team Settings 这两个标签这次没有打开。Your Teams 上的两行就是种入的全部。

xbd-seashore-cs-team 仍然是 AgentScope 里处理 OD-2049 工单的人。它不出现在这张表上。表上的 seashore-main 是预算桶。

两套名字指的不是同一层

名字 属于 问的问题
tenant=default AgentScope 部署 这是不是同一套平台部署
seashore-main / aurora-main AgentScope namespace 这个人能不能看见这家商家的 Agent、会话、工单
team_alias 同名的 LiteLLM 团队 网关 这把钥匙的花费和模型名单归哪一桶
metadata.namespace 一把钥匙上的注释 人读日志时能不能看懂

demo Step 02 已经证明 alice 看不见 aurora-main,bob 反过来也看不见。Step 05 用 404 避免资源枚举。那些断言继续有效。它们不检查 Authorization: Bearer 后面那把钥匙是不是这家商家的。

Dataplane 若全平台共用一个 XBD_MODEL_API_KEY,namespace 隔离仍然成立,模型账单却只有一桶。海岸的重试会吃掉极光的供应商额度。网关这层要补的就是这桶。

名单执行了,注释没有

海岸钥匙的 metadata 是:

{"namespace": "seashore-main", "agent": "xbd-seashore-cs-agent", "order_anchor": "OD-2049"}

把 metadata 改成 aurora-main 而不改 models 和 team_id,网关不会因此开始拒绝 seashore-cs。本机没有做这个改写实验。不做的原因是:文档没有把 metadata 定义成访问控制,返回体也只是原样存取。用一次实验去「证明注释无效」意义有限,只要不在代码里读取 metadata 来做放行。

真正执行过的是 models:

团队的 models 和钥匙的 models 要同时写。只写团队、钥匙留空时,继承规则依赖有没有 team_id、是不是服务账号、主人是谁。第 3 章的钥匙是管理员签发的、user_id 为空、带 team_id。文档说这种钥匙在模型面上不继承主人,团队钥匙的模型以钥匙和团队为准。我们显式写了两边,避免去赌空数组的含义。空数组在不同版本里可能是「全部」或「没有」。不要留空。

平台怎么选钥匙

小帆的托管形态按 namespace 选钥匙,而不是按用户句选模型。

调用方 namespace 注入的环境变量 请求的 model
xbd-seashore-cs-agent seashore-main SEASHORE_LITELLM_KEY seashore-cs
xbd-seashore-logistics-agent seashore-main 可以是另一把同团队的钥匙 seashore-cs
xbd-seashore-product-scout seashore-main 同上,预算分开 seashore-cs
xbd-aurora-cs-agent aurora-main AURORA_LITELLM_KEY aurora-cs
平台公共助手 default 公共区 不持有商家钥匙 不调用商家模型

物流和客服若共用一把钥匙,OD-2049 的工单拆成两次调用时,rpm 和预算也混在一起。第 4 章的限流是按钥匙计数的。拆钥匙才能拆限流。团队预算仍把两把钥匙加总,海岸这家商家的日上限还在。

公共助手的系统提示写明不涉及任何单一商家的私有数据。它不应该持有 SEASHORE_LITELLM_KEY。否则公共区的一次问答会记在海岸的账上,也会让公共助手具备调用 seashore-cs 的能力。能力比账单更要紧。

选品 Agent 如果以后要另一个模型别名,在网关上新加 model_name,并只放进选品那把钥匙的 models。不要加进客服钥匙「图方便」。

网关隔离不了的东西

403 只看模型名。下面这些它看不见,也不该假装看得见。

提示词正文。客户说「帮我查极光数码的订单」,海岸的客服仍用 seashore-cs 调用,网关返回 200。订单数据不在网关。小帆的工具层必须按 namespace 查订单。查不到就说查不到,这是 AgentScope 书里 OD-2049 那次回复已经体现过的态度:没有物流记录就不要编。

日志。网关默认会看到消息正文才能计价和转发。海岸的对话出现在网关日志里,等于运维人员能读到海岸的客户原话。这是另一条访问路径,模型名单不管它。生产上缩小谁能读 Proxy 日志,并决定要不要关掉消息正文日志。litellm_settings 有 turn_off_message_logging,文档说它让回调里不再带消息和响应,元数据仍在。本机没开。开之前确认 Langfuse 里你还要不要看正文(第 9 章)。

主密钥。持有主密钥的人可以给自己签一把 models 含全部别名的钥匙。网关隔离假定主密钥不在商家进程里。这个假定要靠部署,不靠 metadata。

降级目标。seashore-cs 失败后改走 seashore-cs-fast。文档默认 enforce_fallback_model_access 为 true:降级目标也要在钥匙、团队、项目的允许名单里,否则跳过。本机海岸钥匙的名单包含 seashore-cs-fast,降级才有机会发生。若客服钥匙只有 seashore-cs,主模型挂了不会悄悄改用极光的 aurora-cs,这是对的。名单漏写降级目标时,故障会表现为「主模型的原始错误」,响应里没有 x-litellm-attempted-fallbacks。第 7 章的另一次跳过是预算,不是名单。两种跳过都要在告警里能看出来。

组织和项目这次不用

返回的 organization_id 和钥匙上的 project_id 都是空。企业功能列表把多租户的组织、项目、标签预算放在许可证一节。开源的团队和钥匙已经够把海岸和极光拆开。

不要为了「文档里有 organization 这个词」就建一层和 namespace 同构的组织。两层主数据会漂移:商家在 AgentScope 里改名,网关组织没改,账单仍旧按旧 id 走,报表按新名字走。星帆以 namespace 为商家主键,网关 team_alias 抄同一字符串,team_id 存回平台数据库。组织留空。

access_group_ids 本机是空数组。模型访问组适合「一组别名被多个团队引用」。现在只有三个别名、两个团队,直接写在团队和钥匙上更短。访问组是第三张表,没有运维的人就不要加。

和 Step 05 的 404 并列,而不是替代

AgentScope 对越权读资源返回 404,避免枚举。网关对越权模型返回 403,消息里点出了模型名。这是网关的选择,不是平台的漏洞。攻击者已经知道自己请求的 model 字符串,403 没有多泄露一个存在的资源 id。不要为了风格统一去改网关的状态码,除非你准备维护一个分叉。

业务日志里则不要把 403 的完整 JSON 回给终端客户。客户应看到小帆的一句「暂时无法回答」,工单里留 request id 和 type。OD-2049 的客户不需要知道 aurora-cs 这个别名存在。

一次跨商家调用的时间线

海岸的进程拿错了环境变量,注入了极光的钥匙,请求却仍写 model=seashore-cs。预期是 403,因为极光钥匙的名单里没有 seashore-cs。本机测的是相反方向:海岸钥匙请求 aurora-cs,同样 403。两个方向是同一条规则。平台告警按 key_model_access_denied 聚合,不按「谁的进程」聚合。进程身份要靠部署系统的环境变量审计,网关只看钥匙。

若两边都错了:极光钥匙加上 model=aurora-cs,调用会 200,记在极光的账上,而客户会话仍属于海岸的 namespace。这是最坏的组合,网关认为完全合法。挡住它的办法在平台:密钥和模型名从同一行租户配置取出,禁止两个字段来自不同的配置文件。代码评审看的是「这两个字符串是不是同一个结构体的两个字段」。

公共区

default namespace 的公共助手给所有商家用。demo Step 03 写明它不接触商家私有数据。因此:

工单和会话仍归 AgentScope

网关的团队不是工单的参与者。xbd-seashore-cs-team 是 AgentScope 里的客服团队,处理「售后升级:订单 OD-2049 客户投诉物流延迟」。LiteLLM 的 team_alias=seashore-main 是商家级的预算桶。两个「团队」不要合并成一张表。客服组的人离职,不自动作废商家的虚拟密钥;商家解约,要作废钥匙,但不等于删除历史工单。

bob 不能调用 alice 的 Agent,这是 Step 05。bob 的进程也不能持有 alice 的虚拟密钥,这是本章。两句话都要出现在上线检查里,缺一句就会在另一次评审里被当成已经覆盖。

每个会说话的 Agent 各拿哪把钥匙

demo Step 03 的矩阵和网关钥匙的对应关系就这些,不新增角色。

Agent namespace 网关团队 模型别名 钥匙
xbd-seashore-product-scout seashore-main seashore-main seashore-cs 单独一把,和客服分开 rpm
xbd-seashore-cs-agent seashore-main seashore-main seashore-cs key_alias seashore-cs-agent
xbd-seashore-logistics-agent seashore-main seashore-main seashore-cs 单独一把。OD-2049 的物流核实不要占用客服的 rpm
xbd-aurora-cs-agent aurora-main aurora-main aurora-cs key_alias aurora-cs-agent
平台公共助手 default 公共区 不建,直到有不含商家数据的模型 无 无
小帆本地示例 xingfan-support 开发者笔记本 探测团队,不是商家团队 seashore-cs 仅在连到实验网关时 短期钥匙,不进仓库

xbd-seashore-cs-team 是工单团队,不是上表的网关团队。物流专员认领「核实订单 OD-2049 物流实际状态」时,人在 AgentScope 的团队里,模型请求仍用物流 Agent 那把虚拟密钥。人的权限和钥匙的权限是两行配置。

选品专员若以后要更便宜的别名,新别名只放进选品钥匙的 models。客服钥匙不加。否则客服的降级名单和选品的便宜模型会缠在一起。

平台选钥匙的伪代码

这不是已编译的类,是为了把「同一个结构体」说清楚。复核时看真实代码是否分成两个来源。

TenantRoute route = routes.get(namespace); // seashore-main 或 aurora-main
String apiKey = System.getenv(route.virtualKeyEnv); // SEASHORE_LITELLM_KEY
String model = route.gatewayModel; // seashore-cs 或 aurora-cs
OpenAIChatModel chat = OpenAIChatModel.builder()
        .apiKey(apiKey)
        .baseUrl("https://llm.example.com/v1")
        .modelName(model)
        .build();

routes 里没有 default 公共区的条目。公共助手走不到这个 builder。若有人给公共区加一条「先用海岸的」,代码评审拒绝。

环境变量缺失时进程应启动失败,而不是退回上游密钥或主密钥。退回是第 1 章要堵的路。

metadata 可以怎么用才不出事

可以:日志检索、和订单号对齐、在 Langfuse 里过滤。order_anchor: OD-2049 是实验标记。生产用真实订单号时,确认 Langfuse 项目的访问范围不大于这个商家。

不可以:作为 if 条件决定是否转发;作为「信任客户端」的依据。客户端能改自己的请求体。metadata 若从请求体覆盖钥匙上的 metadata,客户端就能把 namespace 改成别人的。本机没有测试请求体 metadata 能否覆盖钥匙 metadata。在没看清之前,业务隔离不读取请求里的 namespace 字段。namespace 以平台鉴权结果为准,钥匙以部署注入为准。

离开这一章之前

接着读