Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

feat(identity): 请求身份未贯通到运行时——技能来源无法按租户分区、外呼无法携带身份(含插值方案与安全约束)

Đang mở
#146 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
25/100
Loại issue
Tính năng
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
rust

Hướng nghiên cứu

Start with src/http/auth.rs and the session identity handling, then trace how sessions create TurnContext in src/kernel.rs and how HttpCall endpoints are registered and dispatched. Read the startup path in crates/recursive-cli/src/main.rs to understand why skill sources are currently process-scoped. The issue is ready for implementation only after the open architecture and security questions are decided; completion should include tests for identity propagation and ensure identity values do not appear in agent-visible output.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

背景

我在做 C 端形态(AG-UI 接入 + skill 加载 + 受控业务接口调用)的外部验收,
过程中确认 #74 的服务级技能来源已经打通(HttpSkillSource + RECURSIVE_SKILL_SOURCE_URL),
但往下追「能不能按租户分区下发技能」和「外呼时能不能带上调用者身份」时,
发现身份在 HTTP handler 边界就停住了,没有跨进 agent 运行时。

这条 issue 不是缺陷报告,是一条尚未打通的能力链,附带一个我认为可行的最小方案与它的安全约束。
因为牵动 kernel / session / http 三层,属于架构决策,所以我没有直接进自迭代管线,先提出来讨论。


一、现状:身份存在,但只走到一半

✅ 已经有的

① 入口就解析出身份(src/http/auth.rs:44)

pub struct AuthIdentity {
    pub subject: String,          // JWT `sub` 或 API key 配置的 subject
    pub tenant: Option<String>,   // JWT `tenant` claim
    pub admin: bool,              // 来自服务端配置 RECURSIVE_HTTP_AUTH_ADMINS
}

注释写明:"handed to the handlers through the request extensions. Sessions record
their creator's subject and tenant"
—— 注意它已经考虑到「两个租户可以签出同样的 sub,
所以同 sub 不同租户是不同 owner」。

② 身份用于会话归属:session 记录创建者 subject + tenant,may_access_session 控制读取,admin 可读全部。

③ 身份用于审计(src/http/audit.rs):记录 actor,并按身份过滤可见审计记录。

④ 已知的固定格式(src/http/auth.rs:337):

/// The claims the server reads off a verified token (issue #85).
/// **Everything else in the token is deliberately dropped.**
struct JwtClaims {
    #[serde(default)] sub: Option<String>,
    #[serde(default)] tenant: Option<String>,
}

只读 sub + tenant,其余 claim 显式丢弃(这是 #85 时的有意决策,不是疏漏)。
也没有「用户名」概念 —— sub 是不透明 principal id。

❌ 断掉的三段

④ 身份进不了 agent 运行时(共同前置)

pub struct TurnContext {          // src/kernel.rs:58
    pub messages, pub step_events_tx, pub tool_specs, pub streaming,
    pub permission_hook, pub exploring_plan_mode, pub permission_mode, pub mailbox,
    // ← 无 subject / tenant / identity
}

kernel.rs / runtime.rs / tools/dispatch.rs 里 grep identity|subject|tenant —— 一处都没有。
工具执行时拿不到调用者是谁。

⑤ 技能来源 URL 无占位替换

pub(crate) fn skills_from_http_sources() -> Result<Vec<Skill>, String> {
    let raw = std::env::var("RECURSIVE_SKILL_SOURCE_URL").unwrap_or_default();
    ...
    let urls: Vec<&str> = raw.split(',').map(str::trim).filter(...).collect();

纯环境变量、逗号切分、无任何插值。

⑥ 外呼不带身份:HttpCall 的对外认证是 auth_env(静态环境变量名)/ auth_header /
auth_prefix —— 运维预置的静态凭据,与调用者身份无关。MCP 同理。

⚠️ 一个结构性障碍(这条最关键)

技能来源是启动期、进程级解析的:

// crates/recursive-cli/src/main.rs —— 在服务器启动之前
let mut skills = cli::builder::discover_loaded_skills(&config);
match cli::builder::skills_from_http_sources() { ... }   // ← 此作用域内没有 request
tools = tools.register(Arc::new(LoadSkill::new(skills.clone())));   // ← 目录就此固定

所以即使今天给 URL 加上 {tenant} 占位,也没有「当前请求」可绑定 —— 函数执行时服务器还没起来。


二、方案建议

目标形态(我的理解)

  1. agent 本身不感知业务身份。身份是管道,不是上下文;对模型应当只写不可读。
    (理由:若模型能看见 tenant,它会开始「推理」租户语义 —— 行为不确定 + 多一个提示注入面)
  2. 身份在会话级可见,可作为变量插值到 运维声明的外呼位置(URL / header / query)。
  3. 不把可变维度写死在结构体里,但也不因此放弃类型安全。

建议:两层,信任属性不同

层 内容 谁用 类型
核心(固定) subject、tenant —— 平台自身做授权所需的最小集(会话归属、审计) kernel / session / http 内部 固定、强类型
扩展(开放、opt-in) 凭据里已验证的附加 claim + 服务端声明变量 只在运维声明的外呼位置插值 开放,对 agent 不透明

核心层保持强类型(否则每个新维度都要有人重新实现 authz);
扩展层开放但受下节约束。admin 目前来自服务端列表而非 claim,这个方向我认同 ——
角色是部署/租户相关的,写进 claim 结构体更糟。

最小示例(复用已有插值模式)

HttpCall 的端点注册表已经是运维编写的,且模型只能选端点名(这正是让 SSRF 结构上不可能的设计):

{
  "endpoints": {
    "get_order": {
      "method": "GET",
      "base_url": "https://orders.internal",
      "path": "/api/orders/{order_id}",
      "headers": { "X-Tenant": "{identity.tenant}", "X-Subject": "{identity.subject}" }
    }
  }
}

模型调用仍然只是 HttpCall({"endpoint":"get_order","params":{"order_id":"A-1001"}}) ——
它既不知道有哪些变量,也无法影响插值位置。

同一个设计让身份外带也变得结构上不可能 —— 这是我认为本方案最值得采纳的理由。

技能来源同理:RECURSIVE_SKILL_SOURCE_URL=https://skills.example.com/{identity.tenant}/index.json。

已有先例(说明这是扩展而非新子系统)

  • HttpCall 路径占位 {order_id} —— 运维声明位置 + 模型只提供值 ✓
  • 技能的 ${SKILL_DIR} 替换(substitute_skill_dir)✓

三、开放层必须配的约束(否则会变成漏洞)

  1. 来源必须可信 —— 签名验证过的 JWT claim,或可信认证代理注入的头。
    绝不能来自请求体或任意入站头(否则等于自助式头注入)
  2. 字段白名单 —— 不转发 iss/exp/nbf/jti;尤其不要默认转发 sub
    (跨服务稳定标识符 ⇒ PII 风险)。必须逐字段显式 opt-in
  3. agent 不可读回 —— 值永不进 transcript、不进工具返回值、不进错误信息。只写
  4. 只在运维位置插值 —— 端点注册表的 headers/path/query;绝不在模型提供的字符串里插值
  5. fail closed —— {identity.tenant} 缺失时报错,而不是替换为空。
    多租户里「没有租户」常等价于「默认租户」⇒ 跨租户泄漏
  6. 命名空间体现信任级别 —— {identity.*}(已验证、会话内不可变)与 {session.*}(可变状态)
    分开;混成一个袋子,运维分不清哪个可信

另外建议:插值时记审计事件(哪个端点、用了哪些变量名、不记值),
以便事后回答「租户标识被发到过哪些主机」。这点与 #101
(「工具级审计元数据不构成企业审计日志」)的诉求一致。


四、落地路径(我的建议,供判断)

共同前置:把身份从 session 读出来、放进 TurnContext。

关键便利条件:session 今天就已记录 creator 的 subject + tenant —— 缺的只是「读出来往下传」。
所以最小改动可能是:

  1. TurnContext(或 runtime)增加一个身份/变量表字段,由 session 建立时填充
  2. 一个插值器:给定模板 + 变量表 → 结果;缺失变量 → Err
  3. HttpCall 的端点注册表增加 headers 字段,走插值器
  4. skills_from_http_sources() 从「启动期一次」改为「按会话/按 tenant 解析 + 缓存」,
    并接受变量表参数
  5. (可选)透传开关按端点声明,而非全局

第 4 步是唯一动到架构的(进程级目录 → 会话级目录)。如果想先小步走,
**只做 1+2+3(外呼插值)**就已经能覆盖「按身份调用业务接口」这个 C 端主诉求,
技能按租户分区可以后置。


五、开放问题

  1. 技能目录改为会话级后,缓存键怎么定? 建议 (tenant, source_url),并按 session 复用
  2. 透传的值要不要限长/限字符集? header 注入(CRLF)是经典坑,建议严格白名单
  3. 多来源时,某个 tenant 拉取失败怎么办? 建议 fail closed 还是 log-and-degrade?
    (#74 目前是「一个 URL 失败整批 abort」)
  4. 要不要支持 {identity.*} 出现在响应处理里? 我倾向不支持 —— 保持只写最简单

六、我这边的验收计划

如果这条落地,我可以在现有外部验收工程里加两项真探测(不改产品代码):

  • R5 技能按租户分区:起两个不同 tenant 身份的会话,/skills 应看到不同的技能集合;
    且 workspace 落盘为 0
  • R6 外呼携带身份:mock 业务端点断言收到 X-Tenant/X-Subject,
    且 agent 的 transcript / 工具返回值里不出现这些值

关联

  • #74 —— 服务级 SkillSource(本条是它的「按租户分区」延伸)
  • #85 —— AuthIdentity / JwtClaims 的来源决策(sub+tenant 固定、其余丢弃)
  • #101 —— 审计元数据的完备性(插值审计与它相关)
  • #135 —— 多租户 workspace 注册表

优先级

我倾向 P2:不影响 C 端形态当前可用性(main 已验证 7/7),
但多租户部署时会成为硬阻塞(进程内无法隔离技能可见性)。
如果你们认为该先做,我可以配合设计评审。

Ngôn ngữ chính
Rust
Star
4
Fork
0
Merge trung bình
5 giờ 32 phút
Pull request đã merge (30 ngày)
7

Chuẩn bị môi trường

  • Có Dockerfile hoặc tệp Docker Compose
  • Không có mẫu pull request
  • Không có hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của jeffkit/recursive

Tất cả issue của jeffkit/recursive

Issue tương tự

Thêm issue về Rust

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.