Skip to content

Proposal:出站 LLM 请求的 HTTP 头注入协议(headers.dsh/v1alpha1) #12

Description

@Mepuru

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 负责把规则应用到产品出站请求。这同时解决三个问题:

  1. 可移植组件获得标准表达方式,不必 patch 产品内部;
  2. 适配层获得 owner、生命周期与安全边界(见下文);
  3. 多组件注入同名头等冲突可被 composition 显式报告,而不是静默覆盖。

协议坐标

命名惯例

依据 packages/core/src/protocol.tsparseApiVersion/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 模式:规则是静态声明(可离线校验、可审计、可离线投影),注入器是运行时协商入口。
  • 候选 Bheader-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/v1alpha1OutboundHeaderInjector 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 字段)保持边界,不向这些协议注入传输层语义。

安全考虑

  • 本能力可改写任意出站请求头,包括 Authorization 等凭据头。因此注入必须(MUST)仅在组件显式声明并获 permission grant 后才生效;没有 grant 的规则不得(MUST NOT)被应用;
  • 注入规则及注入后的头值不得(MUST NOT)泄漏到连接对端、日志、审计记录或 session history(对齐 presentation 对短期值的处理);
  • 注入发生在本地出站边界,不经 connection 传播给远程 participant;远程/connection 场景见开放问题(呼应 Remote Host integration: CommandRuntime, Session/Message mapping, Presentation, and wire profile` #4 Remote Host integration);
  • 规则声明中的头值若含敏感信息,应当(SHOULD)由 adapter 按受保护存储/短期值处理,不写入持久化配置之外的位置。

兼容性

  • 新增命名空间 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)。

开放问题

  1. 通用出站请求钩子 vs 收窄为头注入:是否应定义更宽的 outbound request hook(可观察/可拦截),头注入只是其一?还是首版收窄为头注入、保留扩展空间?
  2. 配置热切换:规则热重载由本协议自包含(resource 版本化 + reconcile),还是依赖未来配置协议(settings 命名空间)?首版是否只支持 manifest 静态声明?
  3. 匹配维度扩展:v1alpha1 已固定 host 子串匹配、大小写不敏感;后续版本是否扩展 host 精确匹配、路径、方法?是否支持否定匹配(except)与优先级(order)?
  4. 覆盖/合并策略:同名头默认覆盖已有值,还是支持 merge(如逗号拼接)?多规则命中同一请求时如何合成?
  5. 注入点:由 adapter 决定(undici dispatcher / provider 层),还是协议规定注入点?协议是否应把「adapter 之外的组件不得 patch 产品目标」成文化?
  6. 远程/connection 传播:注入规则是否可能通过 connection 传播到远程 host(呼应 Remote Host integration: CommandRuntime, Session/Message mapping, Presentation, and wire profile` #4)?首版建议不传播。
  7. 只读先行:是否先定义「观测出站请求头」的只读面(对齐 events 的 observe/intercept 区分),再放开注入?

相关链接

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions