aps-agent/docs/spec/protocol.md

122 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 客户端与服务端协议(v1)
> 契约本体在 `contracts/schemas/`(JSON Schema),TS 镜像在 `contracts/`,漂移由 `scripts/check-contracts.test.mjs` 卡。本文是给人看的同一份东西。
## 1 范围与所有权
服务端与客户端之间只认这一套接口。引擎协议(Pi 的 JSONL、Codex 的 app-server)只活在 `server/agent` 内部,客户端与 Core 都不感知——协议的所有权比协议的兼容性重要(架构设计 5.2)。
界面只依赖本文的事件模型:换引擎、换模型,事件不变。
## 2 版本与兼容规则
| 规则 | 说明 |
| --- | --- |
| 路径版本 | 前缀 `{PROTOCOL_PREFIX}` = `/v1`;改语义就换前缀,不原地改 |
| 事件版本 | 每个事件带 `v`,当前 `1`;字段只加不改不删 |
| 删除字段 | 先废弃(保留但不再填充)→ 下一个大版本再删 |
| 未知事件 | 客户端必须忽略不认识的 `type`,不能崩 |
| 未知字段 | 客户端必须忽略,不能因多字段报错 |
## 3 认证与身份
| 客户端 | 方式 | 说明 |
| --- | --- | --- |
| 浏览器 | `Cookie: ap_session=<httpOnly>` | 沿用现有 JMS 登录;服务端不把业务系统 token 下发给客户端 |
| 桌面端 | `Authorization: Bearer <desktop-token>` | 授权码 + 设备绑定换来的短期令牌(沿用现有授权方式) |
两者都由 `server/entry` 解析成同一个 Subject(用户 + 租户),再交给应用服务与 Core。首次调用的返回值就是 `MeResponse`。
## 4 端点
| 方法 | 路径 | 请求 | 响应 | 说明 |
| --- | --- | --- | --- | --- |
| GET | `/v1/health` | — | `HealthResponse` | 健康检查 |
| GET | `/v1/me` | — | `MeResponse` | 当前身份与客户端形态 |
| GET | `/v1/projects` | — | `ProjectListResponse` | 可见项目 |
| GET | `/v1/sessions` | `?projectId=` | `SessionListResponse` | 会话列表 |
| POST | `/v1/sessions` | `CreateSessionRequest` | `CreateSessionResponse` | 建会话;写操作必须建在项目会话(ADR-0003) |
| GET | `/v1/sessions/{id}` | `?limit=` | `SessionDetailResponse` | 会话详情与最近消息 |
| GET | `/v1/sessions/{id}/events` | `Last-Event-ID` 或 `?since=` | SSE 事件流 | 回放 + 实时,见第 5 节 |
| POST | `/v1/sessions/{id}/messages` | `SendMessageRequest` | `SendMessageResponse` | 发消息;返回受理,结果走事件流 |
| POST | `/v1/sessions/{id}/cancel` | — | `CancelResponse` | 取消当前运行 |
| GET | `/v1/capabilities` | `?sessionId=` | `CapabilitiesResponse` | 按身份过滤的能力清单 |
| GET | `/v1/executions/{executionId}` | — | `ExecutionReceipt` | 执行状态与回执 |
| GET | `/v1/executions/{executionId}/trace` | — | `ExecutionTraceResponse` | 执行追溯:人、参数、审批与回执(A4.5) |
| POST | `/v1/approvals/{approvalId}` | `ApprovalDecisionRequest` | `ApprovalDecisionResponse` | 确认卡响应 |
| GET | `/v1/bindings` | — | `BindingListResponse` | 我绑定的业务系统账号与登录态 |
| POST | `/v1/bindings/{providerId}/start` | — | `BindingStartResponse` | 发起账号绑定(按 Provider 声明的登录方式) |
| POST | `/v1/bindings/{providerId}/complete` | `BindingCompleteRequest` | `BindingCompleteResponse` | 完成绑定:换取凭据并加密保存 |
| DELETE | `/v1/bindings/{providerId}` | — | `BindingDeleteResponse` | 解绑并删除凭据 |
## 5 事件流(SSE)
| 规则 | 说明 |
| --- | --- |
| 传输 | `text/event-stream`;`event:` 填事件 `type`,`data:` 填整条事件 JSON |
| 序号 | `seq` 在会话内单调递增,从 1 开始;客户端按它去重与排序 |
| 断点续传 | 客户端带 `Last-Event-ID`(或 `?since=`)重连,服务端从该序号之后回放 |
| 回放 | 回放与实时用同一格式,客户端不需要两套解析 |
| 心跳 | 无事件时按固定间隔发 `heartbeat`,用于探活与代理保活 |
| 顺序 | 同一会话的事件严格有序;不同会话之间不保证 |
| 敏感 | 事件里只有参数摘要(`argsDigest`、`paramsDigest`),没有凭据与参数明文 |
## 6 事件清单
| 事件 | 含义 | `data` 关键字段 |
| --- | --- | --- |
| `message.user` | 用户消息(多端与重连回放用) | `messageId`、`text` |
| `message.delta` | 助手增量文本 | `messageId`、`text` |
| `message.completed` | 一条助手消息结束 | `messageId`、`finishReason` |
| `tool.call` | 引擎请求调用能力 | `callId`、`capabilityId`、`argsDigest` |
| `tool.result` | 能力调用的回执 | `callId`、`executionId`、`status` |
| `execution.updated` | 执行状态变化 | `executionId`、`status`、`errorKind` |
| `approval.requested` | 需要用户确认 | `approvalId`、`capabilityId`、`paramsDigest`、`impactSummary`、`revision`、`expiresAt` |
| `approval.resolved` | 确认结果 | `approvalId`、`decision` |
| `run.state` | 当前运行状态 | `state`(`idle`/`running`/`cancelling`/`cancelled`) |
| `session.updated` | 会话标题或状态变化 | `title`、`status` |
| `error` | 错误 | `code`、`message`、`retryable` |
| `heartbeat` | 保活 | 空对象 |
`status` 取值统一为 `accepted` / `pending-confirmation` / `succeeded` / `failed`。
## 7 审批时序
1. 引擎请求写能力 → Core 判定需确认 → 落一条待确认动作(ADR-0003、架构设计 4.2)
2. 服务端推 `approval.requested`,客户端渲染确认卡(影响范围、参数摘要、目标版本、有效期)
3. 用户点批准 → `POST /v1/approvals/{id}`,带 `revision` 与 `idempotencyKey`
4. 服务端校验版本与有效期 → 推 `approval.resolved` → 执行 → 推 `execution.updated`
5. 版本过期或已执行 → 返回 `expired`,客户端提示重新确认;确认人是会话发起人本人
## 7.1 账号绑定(ADR-0012)
1. 设置页用 `GET /v1/bindings` 看每个系统的登录态:`active` / `expired` / `unbound`
2. 点绑定 → `POST /v1/bindings/{providerId}/start`:`oauth` 跳 `authorizationUrl`,`password-exchange` 直接进下一步
3. 提交 → `POST /v1/bindings/{providerId}/complete`:服务端向业务系统换 token,**密码用完即弃,不落盘、不写日志**
4. 凭据以密文存在平台凭据代理,按 execution 签发一次性句柄;业务系统 token 不下发给客户端
5. 业务系统拒绝(401/403)→ 绑定标成 `expired`,后续调用在策略层直接拒(`policy.identity_expired`),界面提示重新绑定
6. 解绑 → `DELETE /v1/bindings/{providerId}`:删凭据与账号映射;审计与执行历史保留
## 8 错误模型
| 位置 | 形状 | 说明 |
| --- | --- | --- |
| HTTP | `ErrorEnvelope` | 请求本身失败(鉴权、参数、找不到) |
| SSE | `error` 事件 | 运行中失败;`retryable` 表示可重试,客户端据此给"重试"按钮 |
## 9 幂等与取消
| 场景 | 规则 |
| --- | --- |
| 发消息 | 带 `idempotencyKey`;同一 key 重试不产生第二个 run |
| 审批 | 带 `idempotencyKey`;同一 key 只生效一次 |
| 取消 | 幂等:重复取消不报错;已结束的运行返回 `idle` |
| 结果未知 | 执行超时返回 `failed` 且 `errorKind = unknown`,由客户端提示查执行记录,不自动重试写操作 |
## 10 变更规则
改协议必须同时改三处,否则 `pnpm test` 会失败:
1. `contracts/schemas/*.json`(规范本体)
2. `contracts/events.ts` / `contracts/protocol.ts`(TS 镜像;第 4 节的端点表与 `ENDPOINT_SPECS` 由测试逐条比对)
3. 本文档(第 4、6 节的表)