feat(identity): 请求身份未贯通到运行时——技能来源无法按租户分区、外呼无法携带身份(含插值方案与安全约束)
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 25/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- rust
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
背景
我在做 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} 占位,也没有「当前请求」可绑定 —— 函数执行时服务器还没起来。
二、方案建议
目标形态(我的理解)
- agent 本身不感知业务身份。身份是管道,不是上下文;对模型应当只写不可读。
(理由:若模型能看见tenant,它会开始「推理」租户语义 —— 行为不确定 + 多一个提示注入面) - 身份在会话级可见,可作为变量插值到 运维声明的外呼位置(URL / header / query)。
- 不把可变维度写死在结构体里,但也不因此放弃类型安全。
建议:两层,信任属性不同
| 层 | 内容 | 谁用 | 类型 |
|---|---|---|---|
| 核心(固定) | 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)✓
三、开放层必须配的约束(否则会变成漏洞)
- 来源必须可信 —— 签名验证过的 JWT claim,或可信认证代理注入的头。
绝不能来自请求体或任意入站头(否则等于自助式头注入) - 字段白名单 —— 不转发
iss/exp/nbf/jti;尤其不要默认转发sub
(跨服务稳定标识符 ⇒ PII 风险)。必须逐字段显式 opt-in - agent 不可读回 —— 值永不进 transcript、不进工具返回值、不进错误信息。只写
- 只在运维位置插值 —— 端点注册表的
headers/path/query;绝不在模型提供的字符串里插值 - fail closed ——
{identity.tenant}缺失时报错,而不是替换为空。
多租户里「没有租户」常等价于「默认租户」⇒ 跨租户泄漏 - 命名空间体现信任级别 ——
{identity.*}(已验证、会话内不可变)与{session.*}(可变状态)
分开;混成一个袋子,运维分不清哪个可信
另外建议:插值时记审计事件(哪个端点、用了哪些变量名、不记值),
以便事后回答「租户标识被发到过哪些主机」。这点与 #101
(「工具级审计元数据不构成企业审计日志」)的诉求一致。
四、落地路径(我的建议,供判断)
共同前置:把身份从 session 读出来、放进 TurnContext。
关键便利条件:session 今天就已记录 creator 的 subject + tenant —— 缺的只是「读出来往下传」。
所以最小改动可能是:
TurnContext(或 runtime)增加一个身份/变量表字段,由 session 建立时填充- 一个插值器:给定模板 + 变量表 → 结果;缺失变量 →
Err HttpCall的端点注册表增加headers字段,走插值器skills_from_http_sources()从「启动期一次」改为「按会话/按 tenant 解析 + 缓存」,
并接受变量表参数- (可选)透传开关按端点声明,而非全局
第 4 步是唯一动到架构的(进程级目录 → 会话级目录)。如果想先小步走,
**只做 1+2+3(外呼插值)**就已经能覆盖「按身份调用业务接口」这个 C 端主诉求,
技能按租户分区可以后置。
五、开放问题
- 技能目录改为会话级后,缓存键怎么定? 建议
(tenant, source_url),并按 session 复用 - 透传的值要不要限长/限字符集? header 注入(CRLF)是经典坑,建议严格白名单
- 多来源时,某个 tenant 拉取失败怎么办? 建议 fail closed 还是 log-and-degrade?
(#74目前是「一个 URL 失败整批 abort」) - 要不要支持
{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),
但多租户部署时会成为硬阻塞(进程内无法隔离技能可见性)。
如果你们认为该先做,我可以配合设计评审。
- Lenguaje dominante
- Rust
- Estrellas
- 4
- Forks
- 0
- Merge medio
- 5 h 32 min
- PR fusionados (30 d)
- 7
Preparar el entorno
- Incluye un Dockerfile o un archivo de Docker Compose
- Sin plantilla de pull request
- Sin guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de jeffkit/recursive
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
jeffkit/recursive#159 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 15/100
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 15/100
Los mantenedores suelen responder en 1 día
-
tracking: 安全面闭环——已落地机制的绕过(4 项)Abierto
Dificultad 5/5 Más de una semana Aptitud para principiantes 8/100
Los mantenedores suelen responder en 1 día
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 12/100
Los mantenedores suelen responder en 1 día
Todos los issues de jeffkit/recursive
Issues similares
-
good first issue help wanted
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
-
documentation
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
NuSkooler/enigma-bbs#907 ·
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
Los mantenedores suelen responder en 1 día
-
bug pixi-build-r
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
prefix-dev/pixi#7229 ·
Los mantenedores suelen responder en 1 día