# Agent Sidecar Secret Store 与 API Proxy 技术设计

创建日期：2026-09-03

状态：Draft

关联设计：

- `docs/design/2026-09-02-agent-codex-workspace-sidecar-prd.md`
- `docs/design/2026-09-02-agent-sidecar-phase-1-technical-design.md`
- `docs/design/2026-09-02-agent-sidecar-phase-2-agent-gateway-technical-design.md`

## 1. 摘要

本文设计在现有 `agent-sidecar` 中增加两个独立 Service：

- `Secret Store`：负责凭证的获取、解析、内存缓存、刷新、过期和状态管理；
- `API Proxy`：向 Agent 暴露经过路由和操作白名单约束的 HTTP API 能力，并在转发请求时从
  Secret Store 解析和注入凭证。

目标是在不向 `agent-codex` 容器注入内部 API Token 的前提下，使 Agent 能访问 DevOps、
Clinic、Jira、GitHub、O11y 等明确授权的内部或第三方服务。Agent 获得的是一组受限 API
能力，而不是底层凭证。

内部 API 凭证不得通过以下路径进入 Agent 容器：

- Sidecar 配置文件；
- Agent 环境变量；
- Agent 可见的 Kubernetes Secret Volume；
- Project、CODEX_HOME 或其他共享持久卷；
- API Proxy 响应、管理接口、日志或错误信息。

Secret Store 首期以 AWS Secrets Manager 为权威来源，通过只对 Sidecar 容器可见的
bootstrap identity 获取 Secret。目标 API 凭证只保存在 Sidecar 进程内存中。首选方式是
关闭 Pod 默认 ServiceAccount Token 自动挂载，再把 IRSA 所需的短期 Web Identity Token
显式投射并仅挂载给 Sidecar。该 Token 不是目标 API 凭证，但仍属于敏感的引导凭证，Agent
容器不得读取。只有平台能够证明凭证端点和环境变量按容器隔离时，才可改用 Pod Identity。

本文同时评估现有 `services/datasource-proxy`。结论是：其固定上游、认证注入、操作白名单、
动态主机校验和流式转发可以作为行为原型与迁移输入，但文件型 Secret、硬编码模型、全局
权限模式和凭证返回接口不能直接迁移。`agent-sidecar` 不直接导入另一个 Service 的代码，
也不为短期双跑提前抽取共享 package。

## 2. 目标与非目标

### 2.1 目标

1. Agent 无需持有内部 API Token 即可调用明确允许的 API；
2. Secret 的来源、刷新和使用方解耦，API Proxy 不直接访问 AWS Secrets Manager；
3. 每条 Route 独立声明上游、认证方式和可执行操作，默认拒绝；
4. 代理目标不能由 Agent 任意指定，避免成为 Forward Proxy 或 SSRF 跳板；
5. Secret 加载或单条 Route 故障不影响 Sidecar 中现有 Project Service；
6. 提供不包含 Secret value 的状态管理能力，便于 Agent Gateway 和运维诊断；
7. 支持流式 HTTP 响应、调用方取消传播和连接复用；
8. 支持从现有 `datasource-proxy` 平滑迁移并最终下线独立代理。

### 2.2 非目标

- 不实现任意目标地址的 HTTP Forward Proxy；
- 不实现完整 API Gateway、开放平台、计费、租户认证或业务级授权系统；
- 不向 Agent 下发临时 Token、AWS STS Credential 或其他可直接使用的凭证；
- 不支持需要向 Agent 暴露凭证的 S3 CLI、数据库驱动等非 HTTP 访问；
- 不提供任意 HTTP Token Exchange 配置模板；
- 首期不提供运行时 Secret、Secret Source、Listener 或 Route CRUD；
- 不在 API Proxy 中解析或理解通用业务响应正文；
- 不承诺对 Go Runtime 堆内数据进行可证明的原地清零；
- 不以本方案替代 Pod、节点、Kubernetes 控制面或 AWS IAM 的安全边界；
- 不单独解决 Agent 已获授权业务数据的外传问题，数据防泄漏仍需网络出口和业务权限共同约束。

## 3. 命名与核心语义

### 3.1 正式命名

第二个模块正式命名为 `API Proxy`：

| 层级 | 名称 |
| --- | --- |
| Sidecar 配置 | `services.apiProxy` |
| Go package | `internal/services/apiproxy` |
| Secret 配置 | `services.secretStore` |
| Secret Go package | `internal/services/secretstore` |
| Unix Socket | `/run/agent-sidecar/api-proxy.sock` |

本文中的 API Proxy 特指“凭证注入、路由白名单驱动的受控 API 反向代理”。它不是通用
Forward Proxy，也不是完整 API Gateway。

`Credential Proxy` 不作为正式名称，因为该名称容易暗示代理负责向调用者分发凭证，而本
设计要求任何接口都不得返回 Secret value。`Capability Proxy` 更能表达安全模型，但不是
团队当前选择的模块名；本文仍使用 capability 描述授权语义：一条 Route 及其 Operation
Policy 共同代表 Agent 获得的一项受限能力。

### 3.2 术语

| 术语 | 含义 |
| --- | --- |
| Secret | 一个逻辑凭证集合，可包含一个 opaque value 或多个命名字段 |
| Secret Source | 绑定来源配置、依赖与获取行为，并生成 Secret Snapshot 的实现 |
| Secret Runtime | Sidecar 中一个逻辑 Secret 的缓存、刷新和状态运行时 |
| Secret Snapshot | 某次成功加载得到的不可变凭证快照 |
| Secret Ref | API Proxy 对逻辑 Secret 及字段的引用，不包含值 |
| Resolved Value | Secret Store 为一次请求解析出的单个字段值及其版本和有效期 |
| Listener | 具名本地 Unix/TCP HTTP 端点，承载通过 listenerId 绑定的 Route |
| Route | Agent 入口前缀到固定上游和 Auth Binding 的映射 |
| Operation Policy | Route 内允许的方法和规范化上游路径集合 |
| Auth Binding | 如何从 Secret Ref 生成上游认证头 |
| Capability | Route、Operation Policy 和 Auth Binding 共同授予的调用能力 |

## 4. 现有 datasource-proxy 评估

### 4.1 可复用的行为

当前 `services/datasource-proxy/main.go` 已验证以下思路：

- 以 URL prefix 选择固定 upstream，并在转发前剥离 prefix；
- 支持 Bearer、固定 Header、Basic 和 GitHub PAT Basic 认证注入；
- 在转发前删除调用者提供的部分认证头；
- 通过受约束的 path segment 填充 O11y 和 Bedrock 动态主机参数；
- 对 GET、HEAD 以及少量语义只读 POST 做白名单控制；
- 对 Agent callback 提供窄化的 PATCH 白名单；
- 禁止 HTTP redirect，避免凭证跟随 3xx 被发送到其他 Host；
- 对 SSE 和 AWS Event Stream 等响应按 chunk flush；
- 使用 GitHub PAT 交换 O11y 短期 Token 和 AWS STS Credential。

这些行为说明“Agent 调本地入口、代理注入凭证、上游固定、操作受限”的基本模型可行。

### 4.2 不能直接迁移的部分

#### Secret 存储

当前实现从 `secrets.json` 读取固定结构，并通过文件 mtime 轮询刷新。这与目标不符：

- Secret 必须先落到本地文件；
- 字段与具体数据源绑定，新增凭证必须修改结构和 switch；
- 文件路径、加载结果和 Secret key 会进入日志或错误；
- 刷新线程没有受统一 Service 生命周期管理；
- 没有版本、过期、last-known-good、退避或可观测状态。

#### Route 与授权

当前 Route、POST allowlist 和 Agent PATCH allowlist 是不同的全局变量。`isPostAllowed`
只根据上游路径判断，不能证明某个操作属于匹配到的 Route。通用化后可能出现“Route A
声明的 POST 路径意外授权给 Route B”的跨 Route 能力泄漏。

`readonly | agent | full` 是进程级权限模式，其中 `full` 会允许任意方法携带注入后的凭证，
不能存在于 Agent 可访问的部署中。新模型必须把授权收敛到逐 Route 的 Operation Policy，
并始终默认拒绝。

#### 凭证返回

`GET /o11y/credentials` 会返回 O11y Token 以及 AWS Access Key、Secret Key 和 Session
Token。即使默认关闭，这仍与“Agent 永远不获得凭证”的核心边界冲突。Sidecar 版本不迁移
该接口，也不提供等价替代。需要 S3 的流程必须改造成 Sidecar 内部操作服务，或留在受信任
后端执行。

#### HTTP 与可观测性

当前实现还存在以下通用化缺口：

- 每次请求创建 `http.Client`，不能充分复用连接池；
- 未统一限制请求体、响应体和并发数；
- 转发多数调用者请求头，并复制全部上游响应头；
- 日志包含完整 Request URI 和 upstream URL，query 可能包含敏感数据；
- 部分错误把底层网络错误或 Secret key 返回给调用者；
- 根路径会枚举当前可用 Route；
- 缺少逐 Secret、逐 Route 的稳定状态码；
- O11y 刷新逻辑是数据源特例，没有 Secret Source 抽象。

### 4.3 结论

现有 `datasource-proxy` 适合作为行为原型、Route 目录和兼容性测试输入，不适合作为共享库
直接导入或原样移动到 `agent-sidecar`。新实现应位于 Sidecar 自己的 Service 边界内，以新
配置和新接口重建；迁移稳定后删除 standalone Service。

## 5. 总体架构

### 5.1 组件关系

```mermaid
flowchart LR
    Agent[Agent Codex] -->|HTTP over Unix Socket| Proxy[API Proxy]
    Proxy -->|Resolve SecretRef| Store[Secret Store]
    Store -->|in-memory Resolved Value| Proxy
    Proxy -->|fixed upstream + injected auth| API[Internal or Third-party API]
    Store -->|workload identity| ASM[AWS Secrets Manager]
    Store -->|fixed protocol| O11y[O11y Token Exchange]
    Gateway[Agent Gateway or Operator] -->|authenticated management gRPC| Admin[Sidecar Admin Handlers]
    Admin -->|status or refresh trigger only| Store
    Admin -->|status only| Proxy
```

依赖方向固定为：

```text
API Proxy -> Secret Store API -> Secret Source
Admin Handler -> Secret Store status API
Admin Handler -> API Proxy status API
Project Service                         # 与以上模块相互独立
```

Secret Store 不依赖 API Proxy。Secret Source 不知道 Route。API Proxy 不知道 AWS Secret ID、
O11y bootstrap PAT 或 Source 响应结构，只持有逻辑 Secret Ref。

### 5.2 Agent 请求时序

```mermaid
sequenceDiagram
    participant A as Agent
    participant P as API Proxy
    participant S as Secret Store
    participant U as Fixed Upstream

    A->>P: HTTP request over Unix Socket
    P->>P: normalize path and match Route
    P->>P: authorize method and operation path
    P->>S: Resolve(SecretRef)
    alt Secret ready or usable stale snapshot
        S-->>P: in-memory Resolved Value
        P->>U: sanitized request + injected auth
        U-->>P: response or stream
        P-->>A: filtered response or stream
    else Secret unavailable or expired
        S-->>P: error
        P-->>A: 503 Service Unavailable
    end
```

API Proxy 在 Operation Policy 判定通过后才解析 Secret。被拒绝的请求不访问 Secret Store，
也不触发上游连接。

### 5.3 Sidecar 代码布局

建议实现布局：

```text
services/agent-sidecar/internal/
  services/
    secretstore/
      api/
      sources/awssecretsmanager/
      sources/o11yexchange/
      sources/larktenanttoken/
      service.go
      snapshot.go
      status.go
    apiproxy/
      api/
      service.go
      listener/
        listener.go
        handler.go
        logging.go
      route/
        route.go
        headers.go
        middleware/
          operation_policy.go
          credential_auth.go
          lark_auth_proxy.go
  app/
    handlers/proxyadmin/
```

两个模块继续遵循现有 `app + deps + services` 和 `go.uber.org/dig` 约定，并通过
`service.BaseService`、`service.Manager.RunServices()` 和 `StopServices()` 管理生命周期。
Secret Store 是需要运行刷新循环的 `RunnableService`；API Proxy 是协调多个 Listener 组件的
`RunnableService`。两者不能依赖未定义的启动顺序，因此 API Proxy 启动后必须能处理 Secret
仍处于 `LOADING` 的状态，而不是假设 Secret Store 已完成初次加载。

## 6. 信任模型与安全边界

### 6.1 受信任组件

- `agent-sidecar` 进程、镜像及其配置；
- Kubernetes 控制面、节点内核与容器运行时；
- AWS IAM、仅 Sidecar 可见的 bootstrap identity 和 AWS Secrets Manager；
- 配置发布与 Agent Gateway 管理面的身份认证链路；
- Route 所指向的固定上游。

### 6.2 不受信任组件

- Agent 模型输出；
- Agent 运行的 shell、脚本、Skill、MCP 或第三方依赖；
- Agent 可控的 HTTP path、query、header 和 body；
- 上游返回的业务数据，除非 Route 评审明确允许转交给 Agent。

### 6.3 Pod 内隔离

Agent 和 Sidecar 位于同一 Pod，但必须运行在不同容器内。以下约束是安全模型的一部分：

- 不开启 `shareProcessNamespace`；
- 两个容器均以 non-root 运行，并删除全部不需要的 Linux capabilities；
- Agent 不具备 `CAP_SYS_PTRACE`，不能访问 Sidecar `/proc/<pid>/mem`；
- 禁止 core dump，Sidecar 使用只读 root filesystem；
- 目标 API Secret 不挂载到任何 volume；
- Pod 关闭默认 ServiceAccount Token 自动挂载；
- Sidecar 使用的 Web Identity Token 通过显式 projected volume 单独挂载；
- Web Identity 相关环境变量只配置在 Sidecar 容器；
- Agent 不能访问 EKS Pod Identity Credential Endpoint 或节点 IMDS；
- API Proxy Socket 使用共享 `emptyDir`，Socket 本身不是 Secret；
- Sidecar 管理 gRPC 必须认证，不能把“不同端口”视为 Pod 内隔离手段。

Kubernetes ServiceAccount 属于 Pod，而不是某个容器。仅仅给 Pod 绑定 IRSA 或 Pod Identity
并不能证明 Agent 无法使用该身份。如果平台组件把 Token volume、Credential Endpoint 或
对应环境变量同时暴露给 Agent，Agent 就可能绕过 Secret Store 直接调用 AWS Secrets
Manager。因此“bootstrap identity 对 Agent 不可见”是本方案的硬前置条件，而不是部署优化。

同一 Pod 的容器共享网络命名空间。Agent 可以连接 Sidecar 监听的 TCP 端口，因此新增
Secret/API Proxy 管理方法不能仅依靠 `localhost` 或 Kubernetes Service 不暴露来保护。
在 Gateway-to-Sidecar 身份认证落地前，部署中不得启用 `RefreshSecret` 等管理 RPC；只读
状态方法也应最小化信息，并且永远不返回 Secret value、AWS Secret ID 或 Source 原始错误。

### 6.4 能力边界

本方案保护的是凭证机密性和调用能力的最小授权，不保证经授权返回给 Agent 的业务数据不会
被继续外传。生产部署仍需限制 Agent 的直接网络出口，使其不能绕过 Sidecar 访问未授权内部
服务，并根据数据敏感度决定 Agent 是否应获得某条 Route。

即使一条 Route 不直接返回凭证，如果它允许调用“创建 Access Token”“配置 Webhook”或
“让上游访问任意 URL”等业务接口，也可能间接扩大权限。所有 Operation Policy 必须按业务
语义评审，不能只依据 HTTP method 判断安全性。

## 7. 配置模型

### 7.1 示例

Sidecar 配置仅保存 Secret Source、Secret 引用和 Route 策略，不保存 Secret value：

```yaml
services:
  project:
    projectsRootDir: /data/projects
    codexHomeDir: /home/node/.codex

  secretStore:
    runtimes:
      devops-prod:
        source:
          type: awsSecretsManager
          awsSecretsManager:
            region: us-west-2
            secretId: dev-agent-codex/devops-prod
            format: json
        refresh:
          interval: 5m
          maxStaleness: 1h

      github-pat:
        source:
          type: awsSecretsManager
          awsSecretsManager:
            region: us-west-2
            secretId: dev-agent-codex/github
            format: json
        refresh:
          interval: 5m
          maxStaleness: 1h

      o11y-session:
        source:
          type: o11yExchange
          o11yExchange:
            environment: prod
            bootstrap:
              secret: github-pat
              field: token
            cloudProvider: aws
        refresh:
          refreshBeforeExpiry: 1h
          maxStaleness: 0s

  apiProxy:
    listeners:
      internal:
        network: unix
        address: /run/agent-sidecar/api-proxy.sock
        mode: "0660"

    routes:
      devops-prod:
        listenerId: internal
        prefix: /devops/prod
        upstream:
          baseUrl: https://prod.devops.pingcap.com
        middlewares:
          - type: operation-policy
            config:
              allow:
                - method: GET
                  pathPrefix: /api/v1/
                - method: PATCH
                  pathTemplate: /api/v1/alertstore/alerts/{alertId}/agent-runs/{runId}
          - type: credential-auth
            config:
              candidates:
                - type: header
                  headerName: X-API-TOKEN
                  valueFrom:
                    secret: devops-prod
                    field: apiToken
                - type: header
                  headerName: X-API-KEY
                  valueFrom:
                    secret: devops-prod
                    field: apiKey
      o11y-ds:
        listenerId: internal
        prefix: /o11y/ds
        upstream:
          scheme: https
          hostTemplate: www.ds.{region}.{vendor}.observability.tidbcloud.com
          hostParameters:
            region:
              source: pathSegment
              allowedValues: [us-west-2, us-east-1]
            vendor:
              source: pathSegment
              allowedValues: [aws]
        middlewares:
          - type: operation-policy
            config:
              allow:
                - method: GET
                  pathPrefix: /
          - type: credential-auth
            config:
              candidates:
                - type: bearer
                  valueFrom:
                    secret: o11y-session
                    field: auth0Token
```

Secret、Listener 和 Route 使用 map，名称稳定并便于 GitOps diff。Route 的 `listenerId`
必须引用存在的 Listener；Route 名称只用于引用和管理状态，不能被写入 Agent 错误响应。
旧的 `apiProxy.listener` 配置被拒绝，需迁移为 `listeners` 并为每条 Route 增加绑定。

`middlewares` 按声明顺序构建。首期每条 Route 必须且只能声明一个 `operation-policy` 和一个
`credential-auth`，并保证授权先于凭证解析。Header 清洗、redirect 拒绝及 Transport 生命周期
是 Route 固定安全链，不允许通过配置删除或改序。

### 7.2 启动时配置校验

以下错误使 Sidecar 启动失败：

- 名称重复、引用不存在或 Secret Source 类型未知；
- Secret 依赖图存在环；
- Listener 未配置、没有 Route，或 Route 缺少 `operation-policy`、允许的 Operation 或
  `credential-auth` candidate；
- 同一 Listener 内 Route 存在重复或互相覆盖的 prefix（不同 Listener 可以复用）；
- TCP Listener 使用非 loopback IP、无效端口或 Unix 专用 mode；
- upstream 使用非 `https`，且不在代码维护的显式例外清单；
- upstream 包含 userinfo、fragment、未声明模板或调用者可控 URL；
- host parameter 没有非空 `allowedValues`；
- Operation path 不是以 `/` 开始的规范化绝对路径；
- Auth header 属于禁止配置的 hop-by-hop 或代理头；
- Unix Socket 不是绝对路径或不位于允许的 runtime directory。

配置只决定允许能力，不能通过配置解除以下硬编码边界：禁止 redirect、禁止凭证返回、禁止
任意 upstream、禁止受保护 Header 透传、禁止 `full` 模式。

## 8. Secret Store 设计

### 8.1 Contract 与内部接口

`SecretRef` 是不包含 value 的逻辑引用，和 Secret 状态、格式一起定义在
`packages/contracts/agent-sidecar`。它可以被 Sidecar 内部接口复用，但不得出现在面向 Agent
的响应或管理面状态响应中。Secret Source 类型和来源定位信息仅属于 Sidecar 内部配置。

Secret Store 的值接口以及包含 Secret ID、value 或 Source 内部数据的结构只存在于
`internal/services/secretstore/api`，不能生成 Proto 或注册为 gRPC：

```go
type ResolvedValue struct {
    Value     []byte
    Version   string
    ExpiresAt time.Time
}

type Store interface {
    Resolve(ctx context.Context, ref *secretstorev1.SecretRef) (ResolvedValue, error)
    ListStatuses(ctx context.Context) []*secretstorev1.SecretStatus
    Refresh(ctx context.Context, name string) error
}

type SecretSource interface {
    Fetch(ctx context.Context) (*SecretSnapshot, error)
}
```

`SecretSource` 是绑定完成的可执行对象；AWS Secret ID、格式、O11y bootstrap Secret Ref 等
来源配置由各独立 Source package 的实现保存，`Runtime` 不再分别保存 Provider 与被动参数。
`ResolvedValue.Value` 只允许 API Proxy 的 Auth Binding 在构造上游请求时使用。`SecretSource`、`ResolvedValue`、
`SecretSnapshot` 和 Source 原始响应不得实现 JSON/YAML/Text Marshaler，也不得作为结构化
日志字段。调用完成后释放引用；不承诺 Go GC 已复制的数据能够可靠清零。

`internal/services/secretstore/runtime` 封装一个逻辑 Secret 的 Snapshot、刷新并发控制和状态
计算和刷新结果日志。Secret Store `Service` 只管理 Runtime 集合与依赖调度，不直接访问 Runtime
内部的 Snapshot 或刷新状态。

### 8.2 Snapshot 与状态

`SecretSnapshot` 包含：

- `fields map[string][]byte`；
- `version`：ASM Version ID 或派生 Source 的非敏感版本标识；
- `fetchedAt`；
- `expiresAt`：无自然过期时间时为空；
- `sourceMetadata`：仅供内部刷新使用，不能进入管理 API。

刷新成功后构造完整不可变 Snapshot，再通过一次原子指针替换发布。读取路径不观察部分更新，
也不在调用者持有 Resolved Value 时修改底层 byte slice。

对外状态只包含：

```text
LOADING      尚无成功快照
READY        当前快照可用且未进入 stale 窗口
STALE        刷新失败，但 last-known-good 仍未超过 maxStaleness
UNAVAILABLE  没有快照，或快照已过期/超过 maxStaleness
```

`SecretStatus` 包含逻辑名称、状态、非敏感 version、最近成功刷新时间和过期时间。不得包含
Secret value、AWS Secret ID、Source 响应正文、堆栈或底层错误。

### 8.3 AWS Secrets Manager Source

首期复用 `packages/sdk-go/infra/secret` 中 AWS SDK 初始化思路，但不能直接把现有
`SecretSource.GetSecretValue() string` 当作完整 Store：现有接口不返回 Version ID、过期时间，
也不负责缓存和刷新。

Source 行为：

- 使用 AWS SDK Default Credential Chain 和配置的 Region；
- `format: opaque` 把 SecretString 保存为保留字段 `value`；
- `format: json` 解析顶层 JSON object，每个字符串字段成为可引用 field；
- 拒绝二进制 Secret、嵌套对象、数组和非字符串字段；
- 保存 ASM Version ID 作为非敏感 version；
- 日志只记录逻辑 Secret 名称和刷新结果，不记录 Secret ID 或 AWS 原始错误正文。

Secret selector 在 `Resolve` 时必须精确命中字段。不存在的字段使依赖 Route
`UNAVAILABLE`，不回退到整个 JSON 正文。

### 8.4 O11y Exchange Source

O11y Exchange 是代码注册的专用 Secret Source：

- endpoint 由 `environment` 映射到代码内允许的固定地址；
- 首期只注册 `prod`，其他 environment 在配置校验阶段失败；
- bootstrap PAT 通过 Secret Ref 从 Store 解析；
- 请求 method、path、body schema 和响应 schema 固定；
- 禁止 redirect；
- 响应只提取当前 Route 需要的 `auth0Token`；
- 当前 `/client/auth` 响应不提供过期时间，`expiresAt` 使用代码定义的 12 小时 TTL；若后续协议
  增加可信过期字段，再优先使用响应值；
- O11y Data Export Token 和 AWS STS 字段首期直接丢弃，不写入 Snapshot；
- 若未来专用 Operation Service 需要其他字段，应由其独立设计内部接口，仍不得通过管理 API
  或 Agent 响应返回。

首期不提供通用 `httpExchange` Source。新增另一种 Exchange 必须增加代码、测试和明确的
固定上游评审，不能只提交配置。

### 8.5 刷新与失败语义

- Service 启动后立即并发加载无依赖的 Secret；
- 派生 Source 在依赖 Secret 首次 READY 后加载；
- 定时刷新基于 `interval` 或 `expiresAt - refreshBeforeExpiry`；
- 同一 Secret 同时最多有一个刷新任务；
- 失败使用有上限的指数退避并加入 jitter；
- 手工 Refresh 与后台刷新合并，不并行请求 Source；
- 刷新失败保留最后成功 Snapshot；
- 有自然过期时间的凭证在 `expiresAt` 后立即不可用，`maxStaleness` 不能越过真实过期时间；
- 无自然过期时间的凭证可在刷新失败后使用至 `lastSuccess + maxStaleness`；
- 初次加载失败和最终过期只影响依赖 Route，不导致整个 Sidecar 退出。

## 9. API Proxy 设计

### 9.1 Listener

API Proxy 使用 `apiProxy.listeners` 定义具名 Unix/TCP HTTP 端点。Unix Socket 继续位于
`/run/agent-sidecar`，权限为 `0600` 或 `0660`；TCP 只绑定 loopback IP，例如
Lark CLI 的 `127.0.0.1:16384`。同 Pod 容器共享 loopback 网络。

`apiproxy/listener.Listener` 负责地址绑定、HTTP server、所属 Route 的匹配、路径校验和
访问日志。顶层 Service 负责组装 Listener 与 Route，先绑定全部端点，再启动服务；
部分绑定失败或任一 server 异常退出时清理全部端点。退出只清理 Unix socket 文件。

每条 Route 必须配置 `listenerId`，只挂载到一个 Listener。Header allowlist 按 Listener
隔离。`/healthz` 返回固定正文，不枚举能力；`/` 可以配置为保留完整路径的 Route，
但不能与同 Listener 的其他 Route 共存。

Unix runtime directory 通过共享内存型 `emptyDir` 暴露给 Agent，真实凭证与 AWS
Workload Identity 仍只供 Sidecar 使用。部署网络策略与 Route 授权独立配置；
本次代码修改不改变线上部署或网络出口。

### 9.2 Route 匹配和 URL 构造

请求处理顺序为：

1. 验证 path，拒绝 dot segment、反斜线、非法转义和编码斜线，保留原始请求供验签；
2. 在当前 Listener 的 Route 集合中按 segment boundary 匹配 prefix；
3. 配置了 `lark-auth-proxy` 时，先对原始请求执行协议校验与验签；
4. 剥离非根 prefix 和 host parameter；根 Route 保留完整路径及其转义；
5. 固定 Header 过滤后运行 `operation-policy`，再运行 `credential-auth`；
6. 构造配置中的 HTTPS upstream URL，使用调用方 context 转发并过滤响应。

调用者不能提供 scheme、authority、port、userinfo 或 fragment。query 可以在请求中携带并原样
转发，但不进入日志；Route 不能通过 query 改写上游地址。

Route prefix 不允许重叠，从而避免依赖注册顺序或最长 prefix 规则决定授权结果。

### 9.3 Operation Policy

每条 Operation 由 HTTP method 和一种 path matcher 构成：

- `pathExact`：与完整规范化路径相等；
- `pathPrefix`：以 path segment 边界匹配；
- `pathTemplate`：`{name}` 匹配一个非空 path segment，`{name...}` 匹配一个或多个
  path segment；多段参数可用于 Git repository path 和 Bedrock inference-profile ARN。

一条 Operation 只能配置一种 matcher，且最多包含一个多段参数。首期不接受用户提供的正则
表达式，不支持 `*`、`**` 或任意方法。GET 和 HEAD 也必须显式配置；不存在隐式“只读方法
全部放行”。

判定只使用剥离 Route prefix 和 host parameter 后的 upstream path。Operation Policy 保存在
Route 内，绝不跨 Route 共享。

### 9.4 Auth Binding

Route 的 `credential-auth.config.candidates` 是有序候选列表。API Proxy 选择第一个能成功
Resolve 的候选；这用于兼容 DevOps `X-API-TOKEN` 到 `X-API-KEY` 的迁移。除该兼容场景外，
Route 应只有一个 candidate。

首期支持：

| type | 行为 |
| --- | --- |
| `bearer` | `Authorization: Bearer <value>` |
| `basic` | 对 Secret value 做 Base64 后写入 Basic Authorization |
| `header` | 写入允许的固定 Header 名 |
| `githubPATBasic` | 生成 `x-access-token:<value>` 的 Git Smart HTTP Basic 认证 |

Auth Binding 在请求发送前最后一步注入。调用者传入的同名 Header 无条件删除。每次请求只
使用一个 candidate，不合并多个凭证。

### 9.5 Header 与响应处理

始终删除以下请求 Header：

- `Authorization`、`Cookie`、`Proxy-Authorization`；
- `Connection` 指定的 Header 及所有标准 hop-by-hop Header；
- `Forwarded`、`X-Forwarded-*`；
- 当前 Route 所有 Auth candidate 可能写入的 Header；
- Kubernetes、AWS 或内部约定的受保护身份 Header。

其他 Header 仅在 Listener 默认 allowlist 或 Route 扩展 allowlist 中时转发。受保护 Header
不能通过配置重新加入 allowlist。

响应侧同样删除 hop-by-hop Header、`Set-Cookie` 和已知凭证 Header，只转发 allowlist 中的
缓存、内容类型、长度、范围、ETag 和协议所需流式 Header。API Proxy 不通用扫描响应正文，
因此“该 Operation 是否会在 body 中返回凭证”必须在 Route 评审和集成测试中确认。

### 9.6 Transport、流式和取消传播

- 按 upstream/transport 配置复用长期 `http.Transport` 和连接池；
- 禁止自动 redirect；
- 首期不自动 retry，避免重复执行非幂等操作；
- 请求和响应 body 直接流式转发，不在 Route 层限制大小；
- Route 层不限制并发，也不配置独立的响应头或请求整体 timeout；
- Agent 取消 context 时立即取消上游请求；
- 不信任调用者提供的 Host；
- 调用方负责设置符合自身操作的 deadline；若调用方不取消且连接保持，Sidecar 不主动终止请求。

该行为让普通查询和长时间推理使用同一转发机制，避免 Proxy 配置重复表达调用方已经掌握的
操作时限。Listener 仍保留请求头读取、Header 大小和空闲连接保护。

### 9.7 Agent 错误响应

API Proxy 使用 HTTP status 表达错误类型，并返回固定 JSON 结构：

```json
{
  "error": {
    "message": "request is not allowed",
    "requestId": "..."
  }
}
```

主要错误状态包括：

| HTTP | 场景 |
| --- | --- |
| 400 | 请求 path 或 Lark 协议格式非法 |
| 401 | Lark 签名、正文摘要或时间窗口校验失败 |
| 403 | Lark 目标、身份或认证 Header 不允许 |
| 404 | Route 不存在 |
| 405 | Operation 不允许 |
| 502 | 上游不可用或响应非法 |
| 503 | 凭证不可用 |

错误响应不包含 Route 列表、Secret 名称、upstream URL、query、响应正文、Source 原始错误或
堆栈。上游正常返回的 4xx/5xx 状态码和经过过滤的正文可以透传；若该上游错误正文可能包含
敏感信息，Route 必须配置为丢弃正文并返回标准错误。

### 9.8 Lark Auth Proxy（bot）

Lark 使用独立 TCP Listener 和 `/` Route，配置顺序为
`lark-auth-proxy → operation-policy → credential-auth`。
`lark-auth-proxy.config.keyFrom` 只包含签名密钥的 Secret/Field 引用；Route 必须使用
固定 `https://open.feishu.cn` 或 `https://open.larksuite.com`，只配置一个 Bearer candidate。

按 larksuite/cli v1 协议校验版本、原始方法/转义路径/查询参数、目标 Host、正文摘要、
时间戳（允许前后 60 秒）、bot 身份和 Authorization 注入目标。HMAC-SHA256 使用
密钥原始字符串字节，不对十六进制字符串解码。此协议的时间窗口不提供单次请求去重；
不据此自动重放写请求。

先验签，再完整读取正文核对 SHA256，验证通过后恢复原始字节，最后进入操作策略和
TAT 注入。正文只缓存在内存，不落盘；响应保持流式。校验后清除所有 Lark 代理头和
调用方认证头，目标 Header 不能改变配置中的上游。

`larkTenantToken` Secret Source 配置 appId、App Secret 引用、brand 和 requestTimeout，
调用官方自建应用 TAT endpoint。Snapshot 仅保存 `tenantAccessToken` 和真实过期时间；
默认提前 5 分钟刷新，提前量不超过有效期的一半。App Secret 每次交换重新解析，
刷新失败时旧 Token 仅在未过期期间可用。业务请求不自动重放。

Agent 使用镜像中固定 v1.0.88、启用 `authsidecar` 的 CLI。运行环境向 CLI 注入代理签名
密钥（可随机生成 64 个十六进制字符）；这是代理调用凭据，不是真实飞书凭证，Agent
不得打印、复制或持久化。App Secret、TAT、AWS 身份继续留在 Sidecar。

示例和全部环境变量见服务的 `config.example.yaml` 及 Agent Codex project-seeds README。
初始只开放 `GET /open-apis/bot/v3/info`；业务接口逐项加入操作策略。
user、MCP、预签名 URL 直连下载不在首期能力内，skills 不替代网络隔离。

## 10. 管理面

### 10.1 Contract

Secret Store 与 API Proxy 的安全领域实体放在 `packages/contracts/agent-sidecar`，包括
`SecretRef`、状态、Route Capability 和 Operation matcher；不包含 Secret value、Secret ID、
upstream 地址或 Auth Binding 详情。

后续管理面 Contract 建议拆为两个 Service，保持模块边界：

```text
SecretStoreAdminService
  ListSecretStatuses
  RefreshSecret

APIProxyAdminService
  ListProxyRoutes
  GetProxyRouteStatus
  ListProxyCapabilities
```

管理 API 返回逻辑名称、启用状态、非敏感版本和时间戳。RPC 失败使用标准 gRPC status code，
不为 Secret Store 定义独立错误模型。禁止定义任何 `GetSecret`、`ResolveSecret`、
`ExportCredential` 或包含 value 的字段。

`ListProxyCapabilities` 返回 Route ID、入口 prefix 和允许操作的规范化描述，用于管理端展示
和审计；不返回 upstream 完整地址、Secret Ref 或 Auth Header。

### 10.2 认证与可用性

这些 RPC 是 Agent Gateway/运维管理面，不是 Agent Runtime API。由于当前 Sidecar gRPC
端口在同 Pod 内也可被 Agent 访问，启用前必须具备 Gateway-to-Sidecar 身份认证与授权。

在认证能力上线前：

- 可以实现并测试 Contract 和 Handler；
- 部署配置不得注册 `RefreshSecret`；
- 若注册只读状态接口，其响应必须按 Agent 可见信息设计；
- 不得依赖 NetworkPolicy 隔离同一 Pod 内的容器。

`RefreshSecret` 还需按逻辑 Secret 限流、合并并发刷新，并记录操作者、request ID 和结果；
审计日志仍不记录 Secret ID 或 value。

### 10.3 GitOps 管理

首期 Secret Source、Secret、多个具名 Listener 和 Route 由 Sidecar YAML 配置声明，经代码评审和部署发布
生效。管理 RPC 不提供 CRUD。这样可以保证每次能力变更都有 Git diff、配置校验和回滚点。

如果未来需要动态配置，应由 Agent Gateway 鉴权并向 Sidecar 下发带版本、签名和完整快照的
配置；Sidecar 在内存中校验并原子切换。该能力不在本文首期实现范围。

## 11. 健康状态与日志

### 11.1 健康语义

- Sidecar 进程健康：gRPC Server 和 Listener 正常运行；
- Secret Store 健康：刷新循环正常运行，不要求所有 Secret READY；
- Route 状态：`READY | CREDENTIAL_UNAVAILABLE | CONFIG_DISABLED`；
- 单条 Secret/Route 不可用不改变 Project Service 的健康状态；
- 配置结构非法、Socket 无法创建或 Service 运行循环退出才导致 Sidecar 启动/运行失败。

### 11.2 日志

允许记录：

- request ID、Route ID、method；
- Operation path template，而不是实际 path；
- 上游状态码、总耗时、发送/接收字节数；
- Secret 逻辑名称、状态迁移和非敏感 version；
- API Proxy 的 HTTP 状态码；
- 管理操作的认证主体和结果。

禁止记录：

- Secret value、AWS Secret ID、Secret Ref field；
- Authorization、Cookie、请求/响应 Header 全量；
- 完整 path、query、request/response body；
- 完整 upstream URL；
- AWS/O11y 原始错误正文和堆栈。

### 11.3 Metrics

首期不引入业务 Metrics，后续根据实际观测需求再设计和添加。

## 12. 现有 Route 迁移

迁移以当前 `datasource-proxy` Route 为输入，但每条 Route 单独建立 Operation Policy：

| Route | 认证 | 首期能力 |
| --- | --- | --- |
| DevOps prod/staging | `X-API-TOKEN`，必要时回退 `X-API-KEY` | 显式 GET 路径及 Agent Run callback PATCH |
| Clinic prod/staging | Bearer | Data Proxy 查询 GET/POST |
| Jira | Basic | GET 与 `/rest/api/3/search/jql` POST |
| GitHub | PAT Basic | clone/fetch 与 `/git-upload-pack`，禁止 push |
| O11y DS/GS | O11y Exchange 的 Auth0 Token | 查询 GET，禁止 Credential 和 Data Export |
| DashScope | Bearer | 显式推理路径 |
| Bedrock | Bearer | 显式 invoke 路径 |

迁移时不得把当前全局 POST allowlist 直接复制到所有 Route。DashScope 若承担 Codex Responses
调用，必须在对应 Route 中显式加入 `/compatible-mode/v1/responses`；当前
standalone allowlist 未包含该路径。

O11y 数据导出和 S3 不迁移到 API Proxy，因为它们需要写操作或非 HTTP Credential。若后续
确有需求，应设计专用的 `O11yExportService` 或 `ArtifactService`，让 Sidecar 持有凭证并执行
完整操作，而不是把 Token/STS 返回给 Agent。

## 13. 部署设计

Agent Codex Pod 增加内存型 runtime volume：

```yaml
volumes:
  - name: agent-sidecar-runtime
    emptyDir:
      medium: Memory

containers:
  - name: agent-codex
    volumeMounts:
      - name: agent-sidecar-runtime
        mountPath: /run/agent-sidecar

  - name: agent-sidecar
    volumeMounts:
      - name: agent-sidecar-runtime
        mountPath: /run/agent-sidecar
```

实际部署还需满足：

- `automountServiceAccountToken: false`，不向所有容器自动挂载默认 Token；
- 为 IRSA 创建显式 `serviceAccountToken` projected volume，设置 AWS STS audience，并只挂载
  到 Sidecar 容器；
- `AWS_ROLE_ARN` 和 `AWS_WEB_IDENTITY_TOKEN_FILE` 等引导环境变量只注入 Sidecar；
- 只给该 Web Identity 对应的 IAM Role 提供读取指定 ASM Secret 的权限；
- IAM Policy 按部署实例和 Secret ARN 最小化；
- Agent 容器不挂载目标 Secret、AWS Web Identity Token 或其他 AWS Credential；
- Agent 容器不能访问 EKS Pod Identity Credential Endpoint 或节点 IMDS；若当前平台无法
  保证这一点，首期不得使用 EKS Pod Identity；
- API Proxy 不新增 Service、Ingress 或 Pod TCP Port；
- 对 Agent 的直连网络出口应用 allowlist，至少禁止目标内部 API 的直接访问；
- 配置、日志采集、debug endpoint 和 crash dump 路径不得序列化内存 Secret；
- Sidecar 资源限制要容纳连接池和并发流式响应，避免 OOM 影响 Project Service。

Socket 只能证明调用者具有 volume 访问权，不能区分同一 Agent 容器内的不同进程。若未来
需要隔离多个本地信任域，应使用独立 Proxy 实例和不同 volume mount；不能让调用者在 Header
中自报身份。

## 14. 测试与验收

### 14.1 Secret Store 单元测试

- ASM opaque 和 JSON 字段加载；
- JSON 字段不存在、类型错误和 binary Secret 拒绝；
- Version、过期时间和状态转换；
- 初次失败、刷新成功、刷新失败、last-known-good 和最终过期；
- 指数退避、jitter、定时刷新和手工刷新合并；
- 并发 `Resolve` 与刷新期间只观察完整 Snapshot；
- O11y Exchange 固定 endpoint、禁止 redirect 和响应字段校验；
- 日志、status 和 error 不包含 Secret value、Secret ID 或原始 Source 错误。

### 14.2 API Proxy 单元测试

- 每种 Auth Binding 正确注入且调用者认证头被覆盖；
- primary/fallback candidate 选择；
- Route prefix segment boundary 和 host parameter enum；
- dot segment、编码斜线、非法转义、Host 覆盖和 SSRF 输入拒绝；
- exact、prefix、template Operation 匹配；
- Route A 的 Operation 不能授权 Route B；
- 所有未声明 GET、POST、PATCH、DELETE 默认拒绝；
- hop-by-hop、Cookie、Forwarded 和受保护 Header 清理；
- redirect 不跟随；
- Agent cancel 传播与流式 chunk flush；
- 错误响应不枚举 Route、Secret 或 upstream。

### 14.3 集成与安全测试

- 使用 `httptest` upstream 验证实际收到的 Host、path、query、Header 和 body；
- 通过真实 Unix Socket 从 Agent 容器用户访问 API Proxy；
- 验证 Agent 用户不能读取 Sidecar Workload Identity 或 `/proc` 内存；
- Sidecar 重启后 Socket 恢复且 Secret 重新加载；
- ASM 暂时不可用时 READY Route 使用 last-known-good，过期 Route 返回标准化 503；
- 一个 Secret/Route 故障时 Project Service 继续提供服务；
- 日志采集结果执行敏感信息扫描；
- 与 standalone proxy 对允许请求、拒绝请求和流式行为做兼容对比；
- 验证 `/o11y/credentials` 和任何 Secret value API 不存在。

测试使用标准 `testing`、`testify` 和 `go.uber.org/mock`，可并行用例调用 `t.Parallel()`。
需要本地 TCP/Unix Listener 的测试在允许创建 Socket 的环境运行。

### 14.4 验收标准

1. Agent 容器中不存在目标内部 API Token 的环境变量或可读文件；
2. 允许请求经 UDS 成功到达固定 upstream，且 upstream 收到正确认证；
3. 所有未声明 Route、method 和 path 均 fail closed；
4. 无法通过 path、query、Host、redirect 或 Header 把凭证发送到其他目标；
5. API、日志、管理 RPC 和错误响应均不泄漏凭证；
6. Secret 刷新、过期和故障隔离符合本文状态模型；
7. Project Service 行为和可用性不受单条 Secret/Route 故障影响；
8. 迁移后 Agent 不再依赖 `secrets.json` 或 standalone `/o11y/credentials`。

## 15. 迁移与发布

### 15.1 阶段一：实现基础模块

- 在 Sidecar 增加 Secret Store、ASM Source、API Proxy 和 Unix Listener；
- 先迁移一条低风险只读 Route；
- 完成配置校验、状态和负向安全测试；
- standalone `datasource-proxy` 保持不变，Sidecar 不导入其代码。

### 15.2 阶段二：Route 迁移和双跑验证

- 按 Route 单独迁移 DevOps、Clinic、Jira、GitHub 和 O11y；
- 使用相同只读请求比较新旧代理结果；
- 写操作只在测试环境使用明确测试对象验证，不对生产做影子双写；
- 将 Skill 和工具脚本的访问地址切换为 UDS；
- 模型 Route 通过独立 Operation Policy 单独上线。

### 15.3 阶段三：管理面与生产化

- 完成 Gateway-to-Sidecar 身份认证后启用管理 gRPC；
- 增加 dashboard、告警和刷新审计；
- 验证 IRSA/Pod Identity 最小权限和 Agent 网络出口限制；
- 分别在 dev、staging、prod 灰度。

### 15.4 阶段四：下线 standalone proxy

- 确认所有调用方已切换，且不再使用 `/o11y/credentials`；
- 删除 standalone Deployment、镜像发布和 `secrets.json` 运维流程；
- 最后删除 `services/datasource-proxy`；
- 若迁移期间发现另一个长期部署方也需要相同引擎，再单独评估抽取中立 package，不在首期预设。

## 16. 已确定决策

| 主题 | 决策 |
| --- | --- |
| 模块名称 | `Secret Store` 与 `API Proxy` |
| Secret 权威来源 | 首期 AWS Secrets Manager |
| Agent 凭证可见性 | 永不返回、挂载或注入目标 API 凭证 |
| Agent 接口 | HTTP over Unix Domain Socket |
| 管理接口 | 认证后的 Sidecar gRPC，仅状态与刷新触发 |
| 配置方式 | GitOps 静态 YAML，首期无运行时 CRUD |
| 路由授权 | 每 Route 显式 Operation Policy，默认拒绝 |
| 上游选择 | 固定地址；动态 Host 参数仅允许枚举值 |
| Redirect | 始终禁止 |
| Secret 失败 | Route 局部降级，不拖垮 Project Service |
| 非 HTTP 能力 | 不下发凭证，另建专用操作 Service |
| 旧代理复用 | 作为迁移输入，不跨 Service import |
| 最终状态 | Sidecar 稳定后下线 standalone proxy |

## 17. 风险与后续设计

- Sidecar 同时承载 Project、Secret Store 和 API Proxy，进程级漏洞可能跨能力边界；需要保持
  package 隔离、输入校验、依赖更新和容器加固；
- API Proxy 无法通用判断响应正文是否包含业务敏感数据，Route 上线必须经过数据权限评审；
- 长时间流式请求可能争抢 Sidecar 资源，且未设置 deadline 的调用方可能长期占用连接；需要
  调用方 timeout、Sidecar 资源监控和 Pod 资源限制共同约束；
- 管理 gRPC 在同 Pod 网络中对 Agent 可达，身份认证是启用管理写操作的前置条件；
- O11y Data Export、S3、数据库等非 HTTP 场景需要专用 Operation Service 设计；
- 动态 Route、配置签名、多租户 Listener 和 Gateway 下发配置留待后续设计。

## 18. 参考资料

- `services/datasource-proxy/main.go`
- `services/datasource-proxy/README.md`
- `services/agent-sidecar/config/root_config.go`
- `services/agent-sidecar/internal/deps/deps.go`
- `packages/sdk-go/infra/secret/source_aws_asm.go`
- [OpenAI Docs: Agent approvals and security](https://learn.chatgpt.com/docs/agent-approvals-security)
