OceanWayOceanWay
架构决策记录

ADR-029:API Edge 受控准入与信任边界

冻结首个公共文本命令、DeveloperCredential 验证、Edge 到 Core 的工作负载身份与非公网实施边界

ADR-029:API Edge 受控准入与信任边界

属性内容
状态已采用,受控准入实现基线已验收,生产公网仍关闭
决策日期2026-09-03
适用范围oceanway-api-edgeoceanway-coreoceanway-contracts 与后续 api.oceanway.tech/v1 入口
前置决策OW-ADR-019、022、023、026–028

背景

Core 已具备 API Surface 的 Run Admission 基线,但它只接受 API Edge 已认证的受信 Request Context。下一步需要验证真正的机器入口、Credential 终止、上下文构造和跨仓调用,同时避免在 Text Gateway、完整结算和 Outbox Publisher 尚未接通时,把“能够创建 reserved Run”误写成“文本模型已经可用”。

本阶段因此只建立受控准入链路:冻结一个最小公共 HTTP 命令,在隔离环境使用受控 Credential 验证 API Edge 到 Core 的边界。生产公网路由、一般客户流量和模型实际执行继续关闭。

决策摘要

正式采用以下边界:

  1. 首个且唯一的客户命令为 POST /v1/responses/v1/chat/completions、流式、Embedding、Rerank 与媒体接口不在本期。
  2. Idempotency-Key 是必填 Header,不再使用“建议携带”的弱语义。
  3. 首期只接受 API Key 类型的 DeveloperCredential;V1 固定为 22 字符 base64url keyId 与 43 字符 base64url secret,调用时的 Raw Secret 只到 API Edge。
  4. API Edge 按 keyId 从 Core 读取请求级验证快照,在本地计算 SHA-256 并以 constant-time 方式比较;Edge 不持久化任何 Credential、Snapshot 或业务事实。
  5. API Edge 使用短期 Workload JWT 调用 Core;DeveloperCredential 不作为 Core、Gateway 或 Provider 的 Bearer Token。
  6. Edge 到 Core 的 Admission 强制携带 apiVersion="v1"。客户只提交稳定 publicModelId;Core 负责解析当前 api Offering Revision,查询既有不可变 Price Snapshot 并在同一事务中冻结 pricingSnapshotId 引用,同时提交 Run Input、Reservation、Run、Manifest、幂等结果和 Outbox。
  7. 成功准入返回 202 Acceptedreserved,不表示模型已执行、已有输出或已经完成结算。
  8. 本决策冻结契约但不开放生产公网流量;运行时默认 PUBLIC_ADMISSION_MODE=disabled 且监听回环地址,只有隔离联调显式设为 controlled。后续完成真实执行与经济闭环并通过公开切流门禁后,才能另行授权公网入口。

公共 HTTP 契约

请求

POST /v1/responses HTTP/1.1
Host: api.oceanway.tech
Authorization: Bearer ow_sk_<keyId>.<secret>
Idempotency-Key: <client-operation-key>
Content-Type: application/json
traceparent: <optional-w3c-trace-context>

{
  "model": "<publicModelId>",
  "input": "<non-empty-text>"
}

本期请求对象使用严格字段集合:

  • model 是已发布到 api Surface 的稳定 publicModelId,不是 Offering Revision、Provider Model ID 或 Gateway 路由名;
  • input 只接受非空文本;图片、音频、视频、Asset、Tool、Agent、MCP 和多模态内容不进入本契约;
  • stream、Provider 参数和内部执行参数不被忽略,而是作为不受支持字段拒绝;
  • Organization、Workspace、付款方、App、Environment、Service Account、Credential、Actor 与 Execution Principal 均不得由客户 Body 或 Header 声明;
  • 本期由 Environment Execution Binding 唯一解析 Workspace、可选 projectId 与 Billing Account;客户不能在 Body 或 Header 中声明、覆盖或切换这些上下文。若未来开放客户选择,需要新的兼容契约和独立授权校验。

API Edge 为每次 HTTP 接收生成新的 requestId,并按幂等身份建立 Correlation 与 Operation。客户端不能覆盖这些受信 ID。当前受控基线只校验 W3C Version 00 traceparent:合法值的非零 128-bit Trace ID 会写入 Core 的 Run/Manifest;Header 缺失、重复或格式无效时生成新的 128-bit Trace ID。它不保留远端 Parent Span、Trace Flags 或 Sampling State,也没有创建/传播完整父子 Span 或接入 OpenTelemetry Exporter;这些属于公网切流与后续运维能力。

本期入口不读取 Cookie 或 Customer Session,也不返回 HTML 登录跳转。Host、方法与 Authorization 在业务 Body 解析前检查;缺少受支持的 DeveloperCredential 时直接拒绝,不为失败请求读取或记录 Prompt。

DeveloperCredential Grammar

首期 Authorization Scheme 固定为:

Authorization: Bearer ow_sk_<keyId>.<secret>

base64url-char = ALPHA / DIGIT / "_" / "-"
keyId          = 22base64url-char
secret         = 43base64url-char
  • ow_sk_ 表示 OceanWay Secret Key;
  • keyId 是签发器用密码学安全随机源生成的 22 字符不透明值,用于定位 Credential,不是认证秘密;
  • secret 是签发器用密码学安全随机源生成的 43 字符认证秘密;
  • V1 的解析器按上述固定长度和字符集严格校验;未来改变长度必须发布新的 Credential Issuance 与入口兼容契约,不能静默放宽;
  • 值中不得出现空白、第二个句点、查询参数或其他编码形式;
  • Authorization Header、完整 Key 和 Raw Secret 不得进入日志、Trace、Metrics、错误、Audit、Run Input、Execution Manifest 或 Outbox。

“Raw Secret 只到 Edge”描述调用链:Credential 签发时的一次性展示仍由 Developer Access Domain 的独立签发契约管理,不改变它作为唯一 Credential Owner 的边界。

成功响应

Core 完成准入事务后,API Edge 返回:

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "requestId": "request_...",
  "runId": "run_...",
  "status": "reserved",
  "createdAt": "2026-09-03T00:00:00Z"
}

202 只表示 OceanWay 已原子创建 Run 并完成 credits 预占。它不是生成成功终态,也不能触发结算。幂等重放返回原 runId、原 status 与原 createdAt,但本次 HTTP 接收使用新的 requestId

本期不向客户返回 operationIdcorrelationId、Offering Revision、Execution Target、Reservation ID、Credential ID 或任何 Gateway/Provider 标识。

Credential 验证快照

Core 是唯一事实源

Developer Access Domain 继续拥有 Credential Digest、状态和完整父子链。API Edge 只使用 keyId 读取请求级、只读验证快照;当前 Contracts 固定返回:

keyId
verifier.algorithm = sha256 / verifier.digest
authentication.kind = developer_credential
authentication.developerCredentialId
authentication.developerAppId / environmentId / serviceAccountId
actorPrincipalId / executionPrincipalId(均为 Service Account Principal)
organizationId / workspaceId / billingAccountId
projectId?

verifier.digest 是 Raw Secret 的 SHA-256,也是受限验证材料,不是可公开摘要。Core 只向具有 audience=oceanway-coredeveloper-credential:resolve Scope 的 API Edge Workload Principal 返回;双方均不得记录、回显或转发它。Snapshot 不是新的持久聚合,不进入 Edge 数据库,也不能成为绕过 Developer Access Domain 的长期授权 Token。

Snapshot 查询只在 Credential、App、Environment、Service Account、Organization、Workspace、可选 Project 与 Billing Account 当前有效且绑定一致时返回。当前 Contracts 不引入独立的 credentialRevision 数字字段developerCredentialId 本身就是不可变 Credential Version 身份,keyId、Digest、Service Account 归属与生效时间不能原地改写。轮换必须创建新的 Credential ID、Key ID 与 Secret,并撤销旧 Credential ID;旧 ID 不能重新激活或替换验证材料。

本期不在 Edge 建立持久缓存。Core Run Admission 按 Snapshot 携带的确切 developerCredentialId 再次查询 Credential、App、Environment、Service Account、Organization/Workspace/Project 绑定和 Billing Account,并重新解析当前 api Model Offering。若 Snapshot 返回后旧 Credential 被撤销或任一父资源停用,Admission 在写入前拒绝;不依赖一个会与 Credential ID 重复表达版本含义的 Revision 字段。

本地验证

API Edge 只对解析出的 secret 执行:

candidate = SHA-256(UTF-8(secret))
expected  = snapshot.verifier.digest
valid     = constant_time_equal(candidate, expected)

比较前将双方解码为固定 32-byte Digest;长度或编码无效统一作为认证失败。未知 keyId、Digest 不匹配、Credential 不可用或父资源停用,对客户都返回稳定且不暴露存在性的认证错误;内部 Error Occurrence 可以保留脱敏原因。客户 IP Allowlist 与 Developer Credential 细粒度 Scope 尚未进入本期 Snapshot 契约,不能写成已实施控制。

Raw Secret 在验证完成后不进入应用对象、异步任务或重试载荷。Credential 验证失败发生在业务 Body 解析和 Core Run Admission 之前,不创建 Run、Reservation、Manifest 或 Outbox。

API Edge 到 Core 的身份

API Edge 的两个内部调用——读取 Credential 验证快照、提交 Run Admission——都必须使用短期 Workload JWT。JWT 至少绑定:

issuer = 部署配置固定的受信签发者
audience = oceanway-core
subject = API Edge Workload Principal
expiresAt
scope = developer-credential:resolve / run:admit

JWT 的生命周期、签发和轮换由 Infrastructure 与安全策略管理,不在代码中写无依据的固定时长。API Edge 每次内部调用读取当前 Workload Credential;Core 通过受信 JWKS 验证签名、Issuer、Audience、Subject、有效期与目标操作 Scope。Customer Session、DeveloperCredential、Playground Grant 和 Workforce Session 都不能替代该 JWT。

API Edge 构造的 Request Context 固定包含:

requestId / traceId / correlationId / operationId
actorPrincipalId = serviceAccountPrincipalId
executionPrincipalId = serviceAccountPrincipalId
authentication.kind = developer_credential
developerAppId / environmentId / serviceAccountId / developerCredentialId
organizationId / workspaceId / projectId?
billingAccountId
idempotencyKey

Admission 对象还在 Context 外强制包含 apiVersion="v1"operation="responses.create"publicModelId、文本 Run Input 与 requestFingerprint。除 Idempotency-Key 和请求语义外,以上字段均由 Edge 的受信运行时和 Core Snapshot 产生;callerWorkloadPrincipalId 不由 Edge Body 声明,而由 Core 从已验证 Workload JWT 注入。Core 不因为请求来自受信 Edge 就跳过租户、确切 Credential ID、父链、授权、Offering 与 Billing 校验。

Core 准入与 Run Input

publicModelId 解析

Public API 不接收内部 Revision ID。Core 在同一准入事务中:

  1. publicModelId 查询当前 Model Offering;
  2. 验证其发布记录属于 api Surface,且 publication_statepublisheddeprecated
  3. 固定不可变 Offering Revision、Execution Target 与 pricingSnapshotId
  4. 再执行 credits Quote/Reservation 与 Run Admission。

模型不存在、未发布到 api Surface 或已处于 retired 状态时统一返回 model_unavailable。当前基线的授权粒度是 Workspace 级 run:create;模型级 Service Account Policy/Entitlement 不属于当前实现。API Edge 不保存 Public Model 到 Revision 的映射,也不从 Gateway 目录推断模型。

Run Input 所有权

Execution Domain 负责持久化本次公共命令的不可变 Run Input。它至少固定:

runId
public API schema version = v1
operation = responses.create
publicModelId
input text
input SHA-256
requestFingerprint
createdAt

Run Input、Reservation、Run、Execution Manifest、幂等结果和 run.created / wallet.reserved Outbox 记录在同一 PostgreSQL 事务中提交。Data Classification 当前固定在不可变 Execution Manifest,而不是 run_inputs 表字段。任何一步失败都不得留下孤立 Input、Run 或预占。

Run Input 按租户权限读取并采用平台数据保护策略;Raw Input 不进入普通日志、Metrics、Trace 或 Outbox。API Edge 只在同步请求生命周期中持有解析后的输入,不建立输入数据库、任务表或重放存储。

幂等与失败语义

Idempotency-KeyPOST /v1/responses 强制要求。当前实现将空白裁剪后的 Key 用于以下三层身份:

  1. Edge 按固定键序序列化 {apiVersion: "v1", organizationId, serviceAccountId, environmentId, operation: "responses.create", idempotencyKey},计算 UTF-8 SHA-256,并分别加 operation_ / correlation_ 前缀形成 operationIdcorrelationId;同一业务重放保持稳定,每次 HTTP 接收仍生成新的 requestId
  2. requestFingerprint 是以下固定键序 JSON 的 UTF-8 SHA-256:{"apiVersion":"v1","operation":"responses.create","publicModelId":"...","input":{"kind":"text","content":"..."}}。它只描述公共请求语义,不包含客户可伪造的租户字段。
  3. Core 的幂等记录按 scope=execution.run-admission.v1 + organizationId + executionPrincipalId + Idempotency-Key 分区;冲突 Fingerprint 只包含 apiVersionoperationenvironmentIdserviceAccountIdrequestFingerprint。Credential Version、Trace、Operation/Correlation,以及 Workspace、Project、Billing Account 等可变执行绑定均不进入 Fingerprint,以保持 Credential 轮换和执行绑定变化后的业务重放稳定。首期 Service Account Principal 同时是 Execution Principal,因此不会让同一 Key 跨 Service Account 合并。
  • 同 Key、同指纹返回同一个 Run,不重复写 Run Input、不重复预占;
  • 同 Key、不同指纹返回 idempotency_conflict
  • 每一次 HTTP 接收仍产生新的 requestId;首次成功准入的 requestId 写入 Run,幂等重放尝试与准入前失败当前只保留最小脱敏日志。Request Attempt、Error Occurrence 及其与 Run/Operation 的持久关联由下一阶段 Ops Explorer 建立;
  • Edge 到 Core 超时后,客户端只能携带原 Key 重试,不能由 Edge 换 Key 创建第二个 Operation;
  • 已知请求未到 Core 时使用 submissionState=not_submitted;无法确认 Core 是否提交时使用 submissionState=unknown,不得伪装成确定失败。

稳定错误契约

所有错误保持统一 Envelope:

{
  "error": {
    "type": "oceanway_api_error",
    "code": "invalid_request",
    "message": "The request body is invalid.",
    "retryable": false,
    "submissionState": "not_submitted",
    "fieldErrors": []
  },
  "requestId": "request_..."
}

HTTP Status 位于响应状态行,不在 JSON 中重复。当前 Edge 的每个错误都包含 retryablesubmissionStatefieldErrors 只在字段校验适用时出现,Contracts 预留的 error.param 当前不产生,内部 errorId 也不进入公共 Envelope。首期冻结以下公共分类:

HTTPerror.code语义
400invalid_requestHeader、JSON 或字段不符合本期严格契约
400idempotency_key_required缺少或提供空的 Idempotency-Key
421invalid_request请求 Host 不属于 OceanWay Public API
401authentication_required缺少受支持的 Authorization
401invalid_api_keyKey grammar、keyId、Digest 或当前 Credential 状态验证失败
403permission_denied已认证 Service Account 无当前 Workspace 或操作授权
402insufficient_credits服务端解析的 Billing Account credits 不足
404model_unavailable模型不存在、未发布到 api 或已处于 retired 状态
404model_not_found保留给未来能够安全区分的资源不存在语义,当前 Admission 不产生
409idempotency_conflict同一 Key 被用于不同请求语义
429rate_limit_exceeded保留给公开切流前的分布式限流,当前受控基线不产生
503service_unavailableCore 暂时不可用或提交结果不确定;结合 submissionState 判断
500internal_error未归类的 OceanWay 内部错误

Contracts 同时保留 rate_limit_exceededmodel_not_found:前者供公网分布式限流使用,后者保留给未来可明确区分的资源不存在语义;当前 Admission 把不存在、未发布到 api 和已 retired 统一收敛为 model_unavailable,也不声称已具备模型级 Service Account 策略或分布式限流产生链路。错误正文不能区分“keyId 不存在”和“Secret 不匹配”,也不能包含 Credential/Snapshot、内部 URL、堆栈、SQL、Offering Revision、Provider、Gateway 或其他租户信息。fieldErrors 只用于非敏感字段定位;是否可重试由错误事实决定,不能把所有 5xx 自动标成可安全重放。

Contracts 与兼容性

@oceanway-ai/contracts@0.2.0 已作为固定 Release 发布,覆盖 Public Response Request/Accepted Response、Credential Verification Snapshot、带 apiVersion="v1" 的 Edge-to-Core Admission、Run Input 与上述 Error Code Catalog。不得直接给既有 strict 对象增加字段并把它误写成兼容变更。

API Edge 与 Core 只能依赖固定 Contracts Release。本次已按“Contracts → Core 兼容实现 → API Edge 消费 → 隔离环境联调”完成门禁;后续版本仍遵循该顺序,不通过本地路径、Git 分支或复制 TypeScript 文件联调。

实现与验收证据

截至 2026-09-04,本 ADR 的受控实现基线已经完成固定制品、独立 CI 与真实跨进程联调:

跨服务门禁使用临时 HTTPS JWKS 与真实 RS256 Workload JWT,通过回环 Socket 启动 Edge/Core,写入一次性 _test PostgreSQL,验证默认禁用、受控准入、客户端并发重放收敛、稳定 Envelope、认证/Scope/Audience/Expiry 拒绝、Trace 与事实关联及日志脱敏,结束后确认无服务、测试库或临时目录残留。该证据验证成功终态;事务故障回滚由 Core 独立 PostgreSQL 集成测试覆盖,不把跨服务门禁描述成事务中途故障注入。

数据与进程边界

API Edge 可以拥有运行配置、短期进程内认证对象与最小关联日志,但不得建立下列业务持久化:

  • DeveloperCredential、Digest、App、Environment、Service Account 或 Access Snapshot 表;
  • Model Offering、Revision、Execution Target 或 Provider 路由表;
  • Run、Run Input、Reservation、Ledger、Usage、Asset 或 Outbox 表;
  • 为恢复请求而保存 Raw Secret、Prompt 或完整公共请求的本地队列。

Edge 不能导入 Core Repository、读取 Core 数据库或直接调用 Text/Media Gateway。所有跨仓对象来自固定版本 Contracts,所有领域写入只由 Core 完成。

本阶段明确不包含

  • Text Gateway/UUMI/new-api、Provider 调用与真实文本生成;
  • 流式事件、断流恢复、/v1/chat/completions、Embedding、Rerank 和 GET /v1/models
  • Media Gateway、图片、音频、视频、Asset、Agent 或 MCP;
  • Playground Execution Grant、Console BFF 或 Customer Session 接入;
  • Run 查询、Output、Cancel 和客户 Webhook;
  • Outbox Publisher、消费回执、Ops Explorer 和 Admin UI;
  • Meter Usage、Provider Cost、Settlement、Release、Refund 与 Reconciliation;
  • /v1 Host 切流、生产 DNS 宣告、SDK 发布和一般客户公网访问;
  • 分布式限流、客户 IP Allowlist、Developer Credential 细粒度 Scope、完整 OpenTelemetry 链路与公网容量/压测。

Core 仍按 ADR-028 在准入事务中写 Outbox 记录;“Outbox Publisher 不在本期”表示这些记录尚不跨边界发布,不能把表中已有事件写成已经可靠投递。

验证门禁

以下条件已由固定 Release、独立 CI 与正式隔离联调证据共同满足:

  1. 只有 POST /v1/responses 进入准入,其他本期未支持协议稳定拒绝。
  2. 缺少或复用错误的 Idempotency-Key 分别被拒绝;并发同 Key 同请求只生成一个 Run Input、一个 Run、一份 Reservation 和两条 Outbox。
  3. Raw Secret 只存在于 Edge 请求生命周期;Core、数据库、日志、Trace、错误和测试快照均不存在完整 Key。
  4. Edge 严格接收 22 字符 keyId 与 43 字符 secret,按 keyId 读取当前 Snapshot,SHA-256 候选与固定长度 Digest 使用 constant-time 比较;未知 Key 与错误 Secret 的公开响应不可区分。
  5. Snapshot 查询与 Admission 都拒绝缺失、过期、错误 Audience 或错误 Scope 的 Workload JWT。
  6. 客户伪造 Organization、Workspace、App、Environment、Service Account、Actor、付款方、Offering Revision 或内部 ID 均不能改变受信上下文。
  7. Credential ID 作为不可变版本身份;轮换创建新 ID 并撤销旧 ID。Credential、App、Environment、Service Account、Organization、Workspace、Project 或 Billing Account 失效,均在写入前拒绝且无部分事实。
  8. Core 从 publicModelId 解析并冻结 api Offering Revision;模型不存在、未发布到 api 或已 retired 时不能准入。模型级 Service Account 策略不属于当前基线。
  9. Run Input 与 Run/Reservation/Manifest/幂等/Outbox 原子提交,失败回滚不留下输入或预占。
  10. Admission 强制 apiVersion="v1";Edge 与 Core 对请求 Fingerprint 使用同一 Contracts Canonicalizer,篡改 Fingerprint 在写入前拒绝。
  11. 合法 W3C traceparent 的 Trace ID 写入 Run/Manifest,无效或缺失值安全回退为新 Trace ID;验收不宣称已经存在完整父子 Span 或 OpenTelemetry Exporter。
  12. 成功只返回 flat 202/reserved Envelope;幂等重放返回原 Run 与新的 requestId,不声称已有输出或结算。
  13. API Edge 没有业务数据库、Core/Gateway 数据库访问或 Gateway 调用;全部公开错误符合 nested Error Envelope 和脱敏规则。首次成功准入的 requestId 写入 Run;重放尝试与准入前失败当前只保留最小脱敏日志,不能宣称所有公开错误和重放 Request ID 均可持久还原到 Run。

上述门禁已经由固定 Contracts Release、Core/API Edge 独立 CI 和 Infrastructure 隔离联调共同通过,因此本阶段标记为“受控实现基线已验收”。它仍不得写成生产上线或完整执行链路完成:API Edge 默认使用 PUBLIC_ADMISSION_MODE=disabled 并只监听回环地址;Infrastructure 不得据此创建公网写入口。

公开切流前置门禁

受控 Admission 验收不自动授权公网切流。公开一般客户流量前至少还要完成:

  1. 多实例一致的分布式限流、滥用防护与明确的 rate_limit_exceeded 策略;
  2. W3C Trace Context、跨 Edge/Core/Gateway 的完整链路追踪、采样与 Secret 脱敏验证;
  3. 公网鉴权、并发幂等、超时不确定性、容量、压力和故障注入测试;
  4. DNS/TLS、Ingress、WAF、回滚、告警与值班 Runbook 的独立基础设施验收;
  5. Outbox/Ops Explorer、Metering/Settlement 和至少一条受控 Gateway 执行链形成可诊断、可对账闭环。

后续顺序

受控准入已经验收,当前工程阶段是 Outbox Publisher 与 Ops Explorer:先可靠发布 run.createdwallet.reserved,建立可重建的 Operation/Run/Reservation 投影,再进入 Metering/Settlement 与 Text Gateway canary。不得因为 /v1/responses 已能返回 reserved 而跳过诊断和经济事实闭环。

影响

该决策让 Raw Developer Secret 停留在最窄入口内,同时保持 Credential、模型、Run Input、钱包和 Run 的唯一事实源。代价是首个 /v1/responses 实现只用于验证准入,不产生模型结果;但它避免了在认证、输入和幂等所有权未清晰前,把 API Edge 发展成第二个 Core 或临时任务数据库。

On this page