Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

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

Aperta
#146 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
25/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
rust

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

背景

我在做 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),
但多租户部署时会成为硬阻塞(进程内无法隔离技能可见性)。
如果你们认为该先做,我可以配合设计评审。

Lingua principale
Rust
Stelle
4
Fork
0
Merge medio
5h 32m
PR unite (30g)
7

Preparare l'ambiente

  • Include un Dockerfile o un file Docker Compose
  • Nessun modello di pull request
  • Nessuna guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di jeffkit/recursive

Tutte le issue di jeffkit/recursive

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.