feat(identity): 请求身份未贯通到运行时——技能来源无法按租户分区、外呼无法携带身份(含插值方案与安全约束)
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
- Ambito
- authentication, backend-api-design
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} 占位,也没有「当前请求」可绑定 —— 函数执行时服务器还没起来。
二、方案建议
目标形态(我的理解)
- 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),
但多租户部署时会成为硬阻塞(进程内无法隔离技能可见性)。
如果你们认为该先做,我可以配合设计评审。
- 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
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di jeffkit/recursive
-
fix(pipeline): preflight kill-stale 在 v2 console/worker 路径把并发兄弟 run 当孤儿杀——同仓并发 run 互杀成链(今日 4 例实证)Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
jeffkit/recursive#148 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 38/100
jeffkit/recursive#147 · 14 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 20/100
jeffkit/recursive#134 · 6 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 30/100
jeffkit/recursive#132 · 3 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di jeffkit/recursive
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
agentic-os-org/ANOLISA#6742 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
api: storage
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
googleapis/google-cloud-rust#7153 ·
I maintainer di solito rispondono entro 1 giorno
-
comp-mysql
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
ClickHouse/ClickHouse#124749 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
oracle/rust-oracledb#43 · 1 commento ·