ADR-029:API Edge 受控准入与信任边界
冻结首个公共文本命令、DeveloperCredential 验证、Edge 到 Core 的工作负载身份与非公网实施边界
ADR-029:API Edge 受控准入与信任边界
| 属性 | 内容 |
|---|---|
| 状态 | 已采用,受控准入实现基线已验收,生产公网仍关闭 |
| 决策日期 | 2026-09-03 |
| 适用范围 | oceanway-api-edge、oceanway-core、oceanway-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 的边界。生产公网路由、一般客户流量和模型实际执行继续关闭。
决策摘要
正式采用以下边界:
- 首个且唯一的客户命令为
POST /v1/responses;/v1/chat/completions、流式、Embedding、Rerank 与媒体接口不在本期。 Idempotency-Key是必填 Header,不再使用“建议携带”的弱语义。- 首期只接受 API Key 类型的 DeveloperCredential;V1 固定为 22 字符 base64url
keyId与 43 字符 base64urlsecret,调用时的 Raw Secret 只到 API Edge。 - API Edge 按
keyId从 Core 读取请求级验证快照,在本地计算 SHA-256 并以 constant-time 方式比较;Edge 不持久化任何 Credential、Snapshot 或业务事实。 - API Edge 使用短期 Workload JWT 调用 Core;DeveloperCredential 不作为 Core、Gateway 或 Provider 的 Bearer Token。
- Edge 到 Core 的 Admission 强制携带
apiVersion="v1"。客户只提交稳定publicModelId;Core 负责解析当前apiOffering Revision,查询既有不可变 Price Snapshot 并在同一事务中冻结pricingSnapshotId引用,同时提交 Run Input、Reservation、Run、Manifest、幂等结果和 Outbox。 - 成功准入返回
202 Accepted与reserved,不表示模型已执行、已有输出或已经完成结算。 - 本决策冻结契约但不开放生产公网流量;运行时默认
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是已发布到apiSurface 的稳定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-charow_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。
本期不向客户返回 operationId、correlationId、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-core 和 developer-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:admitJWT 的生命周期、签发和轮换由 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
idempotencyKeyAdmission 对象还在 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 在同一准入事务中:
- 按
publicModelId查询当前 Model Offering; - 验证其发布记录属于
apiSurface,且publication_state为published或deprecated; - 固定不可变 Offering Revision、Execution Target 与
pricingSnapshotId; - 再执行 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
createdAtRun 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-Key 对 POST /v1/responses 强制要求。当前实现将空白裁剪后的 Key 用于以下三层身份:
- Edge 按固定键序序列化
{apiVersion: "v1", organizationId, serviceAccountId, environmentId, operation: "responses.create", idempotencyKey},计算 UTF-8 SHA-256,并分别加operation_/correlation_前缀形成operationId与correlationId;同一业务重放保持稳定,每次 HTTP 接收仍生成新的requestId。 requestFingerprint是以下固定键序 JSON 的 UTF-8 SHA-256:{"apiVersion":"v1","operation":"responses.create","publicModelId":"...","input":{"kind":"text","content":"..."}}。它只描述公共请求语义,不包含客户可伪造的租户字段。- Core 的幂等记录按
scope=execution.run-admission.v1 + organizationId + executionPrincipalId + Idempotency-Key分区;冲突 Fingerprint 只包含apiVersion、operation、environmentId、serviceAccountId与requestFingerprint。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 的每个错误都包含 retryable 与 submissionState;fieldErrors 只在字段校验适用时出现,Contracts 预留的 error.param 当前不产生,内部 errorId 也不进入公共 Envelope。首期冻结以下公共分类:
| HTTP | error.code | 语义 |
|---|---|---|
| 400 | invalid_request | Header、JSON 或字段不符合本期严格契约 |
| 400 | idempotency_key_required | 缺少或提供空的 Idempotency-Key |
| 421 | invalid_request | 请求 Host 不属于 OceanWay Public API |
| 401 | authentication_required | 缺少受支持的 Authorization |
| 401 | invalid_api_key | Key grammar、keyId、Digest 或当前 Credential 状态验证失败 |
| 403 | permission_denied | 已认证 Service Account 无当前 Workspace 或操作授权 |
| 402 | insufficient_credits | 服务端解析的 Billing Account credits 不足 |
| 404 | model_unavailable | 模型不存在、未发布到 api 或已处于 retired 状态 |
| 404 | model_not_found | 保留给未来能够安全区分的资源不存在语义,当前 Admission 不产生 |
| 409 | idempotency_conflict | 同一 Key 被用于不同请求语义 |
| 429 | rate_limit_exceeded | 保留给公开切流前的分布式限流,当前受控基线不产生 |
| 503 | service_unavailable | Core 暂时不可用或提交结果不确定;结合 submissionState 判断 |
| 500 | internal_error | 未归类的 OceanWay 内部错误 |
Contracts 同时保留 rate_limit_exceeded 与 model_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 与真实跨进程联调:
- Contracts
0.2.0对应 Commitce323954,CI33756169615、发布33756329273与Registry/Release 比对33756418040均通过;Releasev0.2.0tgz 的 SHA-256 为731b9a54d846a7fb8714ba651d41d466abfa2b2b98a4f1466eedb0287e0a23c4。 - Core
mainCommitf9e55b6的 CI33777925718已通过冻结安装、格式、类型、33 项单元测试、构建、27 项真实 PostgreSQL 集成测试和生产依赖审计。 - API Edge
mainCommitf97cf5f的 CI33774539995已通过冻结安装、55 项测试、构建、生产依赖审计和 Secret 扫描。 - Infrastructure
mainCommite72e5f3固化门禁;门禁代码来源为9cc3c9c,正式脱敏证据精确绑定该门禁、Coref9e55b6、API Edgef97cf5f与 Contracts Release tgz。
跨服务门禁使用临时 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;
- 旧
/v1Host 切流、生产 DNS 宣告、SDK 发布和一般客户公网访问; - 分布式限流、客户 IP Allowlist、Developer Credential 细粒度 Scope、完整 OpenTelemetry 链路与公网容量/压测。
Core 仍按 ADR-028 在准入事务中写 Outbox 记录;“Outbox Publisher 不在本期”表示这些记录尚不跨边界发布,不能把表中已有事件写成已经可靠投递。
验证门禁
以下条件已由固定 Release、独立 CI 与正式隔离联调证据共同满足:
- 只有
POST /v1/responses进入准入,其他本期未支持协议稳定拒绝。 - 缺少或复用错误的
Idempotency-Key分别被拒绝;并发同 Key 同请求只生成一个 Run Input、一个 Run、一份 Reservation 和两条 Outbox。 - Raw Secret 只存在于 Edge 请求生命周期;Core、数据库、日志、Trace、错误和测试快照均不存在完整 Key。
- Edge 严格接收 22 字符
keyId与 43 字符secret,按keyId读取当前 Snapshot,SHA-256 候选与固定长度 Digest 使用 constant-time 比较;未知 Key 与错误 Secret 的公开响应不可区分。 - Snapshot 查询与 Admission 都拒绝缺失、过期、错误 Audience 或错误 Scope 的 Workload JWT。
- 客户伪造 Organization、Workspace、App、Environment、Service Account、Actor、付款方、Offering Revision 或内部 ID 均不能改变受信上下文。
- Credential ID 作为不可变版本身份;轮换创建新 ID 并撤销旧 ID。Credential、App、Environment、Service Account、Organization、Workspace、Project 或 Billing Account 失效,均在写入前拒绝且无部分事实。
- Core 从
publicModelId解析并冻结apiOffering Revision;模型不存在、未发布到api或已retired时不能准入。模型级 Service Account 策略不属于当前基线。 - Run Input 与 Run/Reservation/Manifest/幂等/Outbox 原子提交,失败回滚不留下输入或预占。
- Admission 强制
apiVersion="v1";Edge 与 Core 对请求 Fingerprint 使用同一 Contracts Canonicalizer,篡改 Fingerprint 在写入前拒绝。 - 合法 W3C
traceparent的 Trace ID 写入 Run/Manifest,无效或缺失值安全回退为新 Trace ID;验收不宣称已经存在完整父子 Span 或 OpenTelemetry Exporter。 - 成功只返回 flat
202/reservedEnvelope;幂等重放返回原 Run 与新的requestId,不声称已有输出或结算。 - 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 验收不自动授权公网切流。公开一般客户流量前至少还要完成:
- 多实例一致的分布式限流、滥用防护与明确的
rate_limit_exceeded策略; - W3C Trace Context、跨 Edge/Core/Gateway 的完整链路追踪、采样与 Secret 脱敏验证;
- 公网鉴权、并发幂等、超时不确定性、容量、压力和故障注入测试;
- DNS/TLS、Ingress、WAF、回滚、告警与值班 Runbook 的独立基础设施验收;
- Outbox/Ops Explorer、Metering/Settlement 和至少一条受控 Gateway 执行链形成可诊断、可对账闭环。
后续顺序
受控准入已经验收,当前工程阶段是 Outbox Publisher 与 Ops Explorer:先可靠发布 run.created 和 wallet.reserved,建立可重建的 Operation/Run/Reservation 投影,再进入 Metering/Settlement 与 Text Gateway canary。不得因为 /v1/responses 已能返回 reserved 而跳过诊断和经济事实闭环。
影响
该决策让 Raw Developer Secret 停留在最窄入口内,同时保持 Credential、模型、Run Input、钱包和 Run 的唯一事实源。代价是首个 /v1/responses 实现只用于验证准入,不产生模型结果;但它避免了在认证、输入和幂等所有权未清晰前,把 API Edge 发展成第二个 Core 或临时任务数据库。