Proposal:出站 LLM 请求的 HTTP 头注入协议(headers.dsh/v1alpha1)
Summary
本提案在 headers.dsh/v1alpha1 命名空间下定义两个 kind:
OutboundHeaderRule(resource):静态声明「当出站 LLM 请求的 host 匹配某条件时,向该请求注入指定的请求头」;
OutboundHeaderInjector(capability):运行时协商入口。adapter 实现它,把已声明的规则应用到产品实际的出站 HTTP 请求上。
规则由可移植组件在 manifest facet 的 extensions 中声明(激活时经 context.extensions.publish(ref, name, handler) 发布);注入器由 adapter(如 @dsh-std/adapter-dsh)实现,位于产品的出站 HTTP 边界。协议本身不引入 undici 或其他传输层依赖,可移植协议包不得 import adapter 或产品运行时(对齐 AGENTS.md 的可移植性要求)。本提案按当前 main 分支的 manifest/lifecycle 词汇(facet 级 extensions、publication barrier)撰写,先于 v0.15 schema 的字段细节。
Motivation
场景:网关客户端指纹检测
部分 LLM 网关(如 AgentRouter)会校验请求的客户端身份 :只接受官方客户端(Codex CLI、Claude Code 等),否则返回:
401 {"message":"unauthorized client detected, contact support for assistance at ..."}
社区实测结论:注入 Originator: codex_cli_rs 请求头即可通过——问题不在 API key,而在网关对客户端指纹的检测。这类网关通常还校验 User-Agent 等头。
现状:可移植组件没有标准路径
当前这类能力只能通过包装宿主运行时实现:在 undici 全局 dispatcher 层包一层 Proxy,拦截 dispatch 并按 host 注入请求头。社区插件即通过这种方式为匹配的 LLM 网关注入身份头(见 相关链接 ),但按 docs/proposals/adapter-dsh.zh.md §Hooks and product API drift(L239-252)的边界规则:
Hook 只能位于 adapter 的产品映射或一次 bootstrap 边界。某个 component 若需要 adapter 尚未提供的 DSH 能力,应增加领域协议或 DSH 专属 extension/activation contract;不能在自身 activation 中重新 patch 同一个产品目标。
因此:
可移植的标准组件不得自行 patch 产品目标(如 undici 全局 dispatcher);
adapter 目前没有任何出站 HTTP/请求头注入映射——@dsh-std/adapter-dsh@0.1.1-rc.1 的 lib/ 中不存在 undici/dispatcher/fetch 相关代码;
于是「出站 LLM 请求头注入」这个需求没有标准协议路径:组件无法表达它需要该能力,adapter 也没有协商坐标去实现它。
为什么需要领域协议
用一个领域协议把实现收进 adapter:组件声明规则,adapter 负责把规则应用到产品出站请求。这同时解决三个问题:
可移植组件获得标准表达方式,不必 patch 产品内部;
适配层获得 owner、生命周期与安全边界(见下文);
多组件注入同名头等冲突可被 composition 显式报告,而不是静默覆盖。
协议坐标
命名惯例
依据 packages/core/src/protocol.ts 的 parseApiVersion/validateApiReference:
group:小写 [a-z][a-z0-9.-]*,可含 . 与 -;
版本:v1 + 可选 alphaN/betaN 稳定后缀;
kind:PascalCase ^[A-Z][A-Za-z0-9]*$。
已有坐标先例:commands.dsh/v1alpha1(Command + CommandRuntime)、models.dsh/v1alpha1(ModelProvider + ModelCatalog)、tools.dsh/v1alpha1(Tool + ToolOverride)、lifecycle.dsh/v1alpha1(FacetModule)、browser.ui.dsh/v1alpha1(SettingsSection 等,「子域.域.dsh」先例)、adapter.dsh/v1alpha1(CordisEntrypoint)。
候选坐标
候选 A(推荐) :headers.dsh/v1alpha1 + OutboundHeaderRule(resource)+ OutboundHeaderInjector(capability)。镜像 Command+CommandRuntime 的「静态 resource + 运行时 capability」双 kind 模式:规则是静态声明(可离线校验、可审计、可离线投影),注入器是运行时协商入口。
候选 B :header-injection.dsh/v1alpha1 + OutboundHeaderInjector(单一 capability,规则并入 spec)。语义最直白,但 group 较长,且把静态规则与运行时能力混在一个 kind 里。
本文案按候选 A 展开;若维护者倾向候选 B,数据模型与生命周期语义可平移,仅坐标与 kind 划分不同。
数据模型
OutboundHeaderRule(resource)
{
apiVersion : 'headers.dsh/v1alpha1' ,
kind : 'OutboundHeaderRule' ,
metadata : {
name : 'agent-router-fingerprint' , // 必须符合 manifest/API reference 命名语法
} ,
spec : {
// host 匹配:v1alpha1 默认子串匹配、大小写不敏感(对齐社区实测行为与适配现状);精确匹配/路径/方法为扩展项
hostMatch : 'gateway.example.com' ,
// 注入的请求头;同名头默认覆盖已有值(覆盖/合并策略见「开放问题」)
headers : {
'Originator' : 'codex_cli_rs' ,
'User-Agent' : 'codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464' ,
} ,
// 可选:是否启用;缺省为 true
enabled ?: boolean ,
// 可选:规则顺序;多规则合成语义见「开放问题」
order ?: number ,
} ,
}
示例值(Originator/User-Agent 等)仅示意,非协议要求。
约束:
metadata.name 必须符合 manifest/API reference 命名语法;
spec.headers 是字符串键值映射,键必须是合法 HTTP 头名(非法头名产生校验错误,见「错误语义」);
规则只作用于出站 LLM 请求:不拦截响应、不改写请求体(边界见「范围与边界」)。
OutboundHeaderInjector(capability)
// 注入器为纯函数 handler:apply(origin, headers) => headers,无副作用
interface OutboundHeaderInjector {
apply ( input : {
origin : { url : string , method : string } ,
headers : HeadersInit ,
} ) : HeadersInit
}
Handler 是纯函数:给定出站请求的 origin 与当前请求头,返回注入后的请求头。它不发起网络请求、不读取外部可变状态、不产生日志副作用。多规则的合并、顺序与冲突由实现方(adapter)依据规则的 order 等优先级合成;协议只定义最小行为与约束(见「协商与生命周期」)。
协商与生命周期
声明与协商
组件在 manifest facet 的 extensions 中声明 OutboundHeaderRule resource(激活时经 context.extensions.publish(ref, name, handler) 发布);在 requires.contracts 中声明需要 headers.dsh/v1alpha1 的 OutboundHeaderInjector contract;
协商经 core 的 capability negotiation 进行:adapter 实现 injector 并发布 support 后,才与规则组件形成 agreement。没有注入器时,仅贡献规则的组件不应当(SHOULD NOT)因规则不生效而激活失败;若组件把注入作为硬依赖,则必须(MUST)在 requires.contracts 中显式声明,让协商阶段报告缺失。
owner 化副作用与 publication barrier
adapter 实现注入器时,若产品尚无正式出站 HTTP 扩展点(如 dsh 现状),需要包装 undici 全局 dispatcher。按 adapter-dsh.zh.md 的规则,此类 hook/wrapper 实现必须(MUST):
绑定明确 owner 与 lifecycle cleanup scope;
验证目标签名和适用版本;
在目标不存在的 profile 中不注册等待不到的依赖;
卸载时恢复原状态;
把独占目标和版本范围报告给 composition/provenance;
对 API 漂移给出结构化不兼容结果。
生命周期与 lifecycle.dsh/v1alpha1 FacetModule 一致:activate(context) 中经 context.protocols.implement(support, impl) 登记 injector,经 context.extensions.publish(ref, name, handler) 发布规则;publication barrier 保证 implement() 仅登记,activate 成功且协商通过后才 live。
停用 / 卸载 / 热重载
停用或卸载必须(MUST)恢复基线 dispatcher:移除包装、还原原始全局 dispatcher,且必须(MUST)在 cleanup scope 内完成,不留残留;
热重载(配置变化)时,新规则集必须(MUST)原子切换后生效:先构建新状态、再切换、后释放旧状态;不允许中间态同时持有两个包装或出现部分规则生效;
多组件注入同名头必须(MUST)经 composition 报告冲突(显式选择或报告歧义),不得由后注册者静默覆盖前者(对齐 adapter-dsh.zh.md 对同一标准协议多个 mapping 的处理)。
错误语义
校验错误:非法头名、非法 metadata.name、非法 host 匹配表达式,在规则发布/协商阶段即失败并给出结构化错误;不得以运行时静默忽略的方式处理;
协商失败:requires.contracts 中声明的 injector contract 无实现时,协商必须(MUST)报告失败原因(缺实现或缺版本兼容),由组件决定降级或失败;
注入错误:应用规则时若头值非法(如包含 CR/LF),实现必须(MUST)拒绝该规则而非注入畸形头。
与现状差距
范围与边界
本协议只处理:
方向 :出站(outbound)——产品发往模型 endpoint/网关的 HTTP 请求;
维度 :请求头——注入、覆盖与合并;
匹配 :host——v1alpha1 默认子串匹配、大小写不敏感(路径/方法等扩展项见开放问题)。
本协议不处理:响应头/响应体改写、请求体改写、TLS/证书、代理发现、通用出站请求钩子(见开放问题)。与 events(领域事件拦截)、connection(carrier 级)、model(provider 流内无 headers 字段)保持边界,不向这些协议注入传输层语义。
安全考虑
兼容性
新增命名空间 headers.dsh/v1alpha1,不改变任何现有协议坐标、manifest 字段或 wire profile;
可移植协议包不得 import adapter 或产品运行时(对齐 AGENTS.md:portable protocol packages must not import an adapter or a product runtime);
若后续出现更宽泛的「出站请求钩子」协议,本协议应保持可叠加而非被吸收。按 AGENTS.md Protocol changes 要求,公开协议须有对应 docs/proposals/ 文档;方向确认后本协议应据此固化(建议命名 headers.zh.md)。
开放问题
通用出站请求钩子 vs 收窄为头注入 :是否应定义更宽的 outbound request hook(可观察/可拦截),头注入只是其一?还是首版收窄为头注入、保留扩展空间?
配置热切换 :规则热重载由本协议自包含(resource 版本化 + reconcile),还是依赖未来配置协议(settings 命名空间)?首版是否只支持 manifest 静态声明?
匹配维度扩展 :v1alpha1 已固定 host 子串匹配、大小写不敏感;后续版本是否扩展 host 精确匹配、路径、方法?是否支持否定匹配(except)与优先级(order)?
覆盖/合并策略 :同名头默认覆盖已有值,还是支持 merge(如逗号拼接)?多规则命中同一请求时如何合成?
注入点 :由 adapter 决定(undici dispatcher / provider 层),还是协议规定注入点?协议是否应把「adapter 之外的组件不得 patch 产品目标」成文化?
远程/connection 传播 :注入规则是否可能通过 connection 传播到远程 host(呼应 Remote Host integration: CommandRuntime, Session/Message mapping, Presentation, and wire profile` #4 )?首版建议不传播。
只读先行 :是否先定义「观测出站请求头」的只读面(对齐 events 的 observe/intercept 区分),再放开注入?
相关链接
Proposal:出站 LLM 请求的 HTTP 头注入协议(headers.dsh/v1alpha1)
Summary
本提案在
headers.dsh/v1alpha1命名空间下定义两个 kind:OutboundHeaderRule(resource):静态声明「当出站 LLM 请求的 host 匹配某条件时,向该请求注入指定的请求头」;OutboundHeaderInjector(capability):运行时协商入口。adapter 实现它,把已声明的规则应用到产品实际的出站 HTTP 请求上。规则由可移植组件在 manifest facet 的
extensions中声明(激活时经context.extensions.publish(ref, name, handler)发布);注入器由 adapter(如@dsh-std/adapter-dsh)实现,位于产品的出站 HTTP 边界。协议本身不引入 undici 或其他传输层依赖,可移植协议包不得 import adapter 或产品运行时(对齐 AGENTS.md 的可移植性要求)。本提案按当前 main 分支的 manifest/lifecycle 词汇(facet 级extensions、publication barrier)撰写,先于 v0.15 schema 的字段细节。Motivation
场景:网关客户端指纹检测
部分 LLM 网关(如 AgentRouter)会校验请求的客户端身份:只接受官方客户端(Codex CLI、Claude Code 等),否则返回:
社区实测结论:注入
Originator: codex_cli_rs请求头即可通过——问题不在 API key,而在网关对客户端指纹的检测。这类网关通常还校验User-Agent等头。现状:可移植组件没有标准路径
当前这类能力只能通过包装宿主运行时实现:在 undici 全局 dispatcher 层包一层 Proxy,拦截
dispatch并按 host 注入请求头。社区插件即通过这种方式为匹配的 LLM 网关注入身份头(见 相关链接),但按docs/proposals/adapter-dsh.zh.md§Hooks and product API drift(L239-252)的边界规则:因此:
@dsh-std/adapter-dsh@0.1.1-rc.1的 lib/ 中不存在 undici/dispatcher/fetch 相关代码;为什么需要领域协议
用一个领域协议把实现收进 adapter:组件声明规则,adapter 负责把规则应用到产品出站请求。这同时解决三个问题:
协议坐标
命名惯例
依据
packages/core/src/protocol.ts的parseApiVersion/validateApiReference:[a-z][a-z0-9.-]*,可含.与-;v1+ 可选alphaN/betaN稳定后缀;^[A-Z][A-Za-z0-9]*$。已有坐标先例:
commands.dsh/v1alpha1(Command + CommandRuntime)、models.dsh/v1alpha1(ModelProvider + ModelCatalog)、tools.dsh/v1alpha1(Tool + ToolOverride)、lifecycle.dsh/v1alpha1(FacetModule)、browser.ui.dsh/v1alpha1(SettingsSection 等,「子域.域.dsh」先例)、adapter.dsh/v1alpha1(CordisEntrypoint)。候选坐标
headers.dsh/v1alpha1+OutboundHeaderRule(resource)+OutboundHeaderInjector(capability)。镜像 Command+CommandRuntime 的「静态 resource + 运行时 capability」双 kind 模式:规则是静态声明(可离线校验、可审计、可离线投影),注入器是运行时协商入口。header-injection.dsh/v1alpha1+OutboundHeaderInjector(单一 capability,规则并入 spec)。语义最直白,但 group 较长,且把静态规则与运行时能力混在一个 kind 里。本文案按候选 A 展开;若维护者倾向候选 B,数据模型与生命周期语义可平移,仅坐标与 kind 划分不同。
数据模型
OutboundHeaderRule(resource)
示例值(
Originator/User-Agent等)仅示意,非协议要求。约束:
metadata.name必须符合 manifest/API reference 命名语法;spec.headers是字符串键值映射,键必须是合法 HTTP 头名(非法头名产生校验错误,见「错误语义」);OutboundHeaderInjector(capability)
Handler 是纯函数:给定出站请求的 origin 与当前请求头,返回注入后的请求头。它不发起网络请求、不读取外部可变状态、不产生日志副作用。多规则的合并、顺序与冲突由实现方(adapter)依据规则的
order等优先级合成;协议只定义最小行为与约束(见「协商与生命周期」)。协商与生命周期
声明与协商
extensions中声明OutboundHeaderRuleresource(激活时经context.extensions.publish(ref, name, handler)发布);在requires.contracts中声明需要headers.dsh/v1alpha1的OutboundHeaderInjectorcontract;requires.contracts中显式声明,让协商阶段报告缺失。owner 化副作用与 publication barrier
adapter 实现注入器时,若产品尚无正式出站 HTTP 扩展点(如 dsh 现状),需要包装 undici 全局 dispatcher。按 adapter-dsh.zh.md 的规则,此类 hook/wrapper 实现必须(MUST):
生命周期与
lifecycle.dsh/v1alpha1FacetModule 一致:activate(context)中经context.protocols.implement(support, impl)登记 injector,经context.extensions.publish(ref, name, handler)发布规则;publication barrier 保证implement()仅登记,activate 成功且协商通过后才 live。停用 / 卸载 / 热重载
错误语义
metadata.name、非法 host 匹配表达式,在规则发布/协商阶段即失败并给出结构化错误;不得以运行时静默忽略的方式处理;requires.contracts中声明的 injector contract 无实现时,协商必须(MUST)报告失败原因(缺实现或缺版本兼容),由组件决定降级或失败;与现状差距
@dsh-std/adapter-dsh@0.1.1-rc.1(npm 预发布版本)的 lib/ 中无 undici/dispatcher/fetch 相关代码,无任何出站 HTTP 层映射;现有实现映射为 Command/Model/Tool/Session/Storage/Workspace/Presentation/UI/Connection(另有 messages、ui-browser 相关);范围与边界
本协议只处理:
本协议不处理:响应头/响应体改写、请求体改写、TLS/证书、代理发现、通用出站请求钩子(见开放问题)。与 events(领域事件拦截)、connection(carrier 级)、model(provider 流内无 headers 字段)保持边界,不向这些协议注入传输层语义。
安全考虑
Authorization等凭据头。因此注入必须(MUST)仅在组件显式声明并获 permission grant 后才生效;没有 grant 的规则不得(MUST NOT)被应用;兼容性
headers.dsh/v1alpha1,不改变任何现有协议坐标、manifest 字段或 wire profile;headers.zh.md)。开放问题
相关链接
@dsh-std/adapter-dsh:https://www.npmjs.com/package/@dsh-std/adapter-dsh