Issue #41:可重启控制面与 Admin API¶
本页是 Issue #41 的交付契约。它承接 PostgreSQL 控制面实现(Issue #37 / PR #38),记录真实启动、readiness、管理 API 和重启恢复证据。
当前页面同步记录已完成的 bootstrap、readiness、Admin API、持久化 Session 和重启恢复验收。
目标与边界¶
Issue #37 已提供六类控制面对象的 PostgreSQL migration、受控写函数、领域 Repository 和基础 bootstrap:
PostgreSQL
-> SQL Repository
-> PlanResolver / RunnerRegistry / Dispatcher
-> HTTP Gateway
本 Issue 要把这条链路变成可操作的服务:
- 进程从显式配置读取数据库和运行时依赖;
- 新进程可以从同一个 PostgreSQL 数据库恢复 Tenant、App、Profile 和 Binding;
- /readyz 只在真实依赖完整可用时返回成功;
- Admin API 通过受认证、受租户约束的 HTTP 操作管理控制面;
- 所有写入继续使用现有 Repository 的乐观锁、状态迁移、发布和 secret-free 约束。
Session/Event/Memory/Summary 持久化、Redis/S3 runtime、Secret Resolver、Outbox 消费队列和 Channel runtime 由对应模块接入 bootstrap;本页固定服务级装配、认证和恢复边界。
已完成能力与交付证据¶
| 能力 | 当前基线 | Issue #41 交付 |
|---|---|---|
| PostgreSQL migration | 已实现并合入 | 复用,不重做 schema |
| SQL Tenant/App/Model/Backend/Binding Repository | 已实现并合入 | 复用既有契约 |
| 显式 bootstrap graph | bootstrap.New / NewFromEnvironment 已实现 | 服务级启动、readiness 和双进程重启恢复验收 |
| HTTP Gateway / readiness | 已有真实 Dispatcher、Registry 和数据库 ping gate | 保持 503/200 和摘流语义 |
| Admin API | 已实现 | 受认证、受租户约束的控制面 HTTP API |
| Session | 已实现 | 按显式配置装配 InMemory/PostgreSQL/Redis capability |
| 重启恢复 E2E | 已实现 | 独立 Bootstrap 读取同一 PostgreSQL 数据并恢复运行时对象 |
Bootstrap 契约¶
配置来源¶
启动配置必须来自显式的进程配置、环境变量解析结果或测试构造器。请求 body、header、IM 消息和未验证的 route 不能决定 tenant_id、app_id、Profile 或 Binding。
生产入口当前使用 bootstrap.NewFromEnvironment。最小配置如下:
| 环境变量 | 用途 | 是否必需 |
|---|---|---|
| TRPC_POSTGRES_DSN | PostgreSQL 控制面连接 | 是 |
| TRPC_API_TOKEN | 最小 API principal 凭证 | 是 |
| TRPC_TENANT_ID | API token 固定的租户 | 是 |
| TRPC_APP_ID | API token 固定的 Agent App | 是 |
| TRPC_ADMIN_TOKEN | 独立 Admin principal 凭证;不能复用 TRPC_API_TOKEN | 是 |
| TRPC_ADMIN_TENANTS | Admin principal 的租户范围,逗号分隔;显式 * 表示平台管理员全部租户权限 |
是 |
| TRPC_MODEL_API_KEY | 仅在运行时交给 ModelFactory | 是 |
| TRPC_MODEL_PROVIDER | 当前支持 openai | 否 |
| TRPC_MODEL_NAMES | 受信 Model Catalog | 否 |
| TRPC_MODEL_ENDPOINT_HOSTS | HTTPS endpoint host 白名单 | 否 |
| TRPC_MODEL_SECRET_REF | 控制面中的 secret reference | 否 |
TRPC_API_TOKEN 只用于普通对话 API,固定映射到一个已存在的 Tenant/App;它不能访问 /admin/v1/。Admin API 必须使用独立的 TRPC_ADMIN_TOKEN,并由 Admin principal 携带 subject、role=admin 和 tenant scope。TRPC_ADMIN_TENANTS= 表示显式配置的平台管理员,可列出、读取和管理所有 Tenant 及其资源;首个 Tenant 创建仍通过受控的初始化门槛,普通 API token 永远不能提权。生产环境应按最小权限配置具体租户 ID。所有 token 和模型 key 只能在进程启动配置或 SecretResolver 输入边界中出现,不能写入数据库、ExecutionPlan、Factory cache key、日志、trace 或错误响应。Issue #41 的 Admin API 只接收 secret_ref,不接收明文凭据。
装配顺序¶
Bootstrap 必须保持以下依赖顺序。配置解析、Admin token 或 migration 执行失败时,生产入口必须在绑定业务 HTTP 端口前以稳定类别失败;已经构造出的 Runtime 对数据库、Repository、Resolver、Registry、Dispatcher 或 Handler 失效统一通过 /readyz=503 摘流:
validate config
-> open/ping PostgreSQL
-> migrations.Apply (ordered files, advisory lock, schema history)
-> construct five control-plane Repository + Channel Candidate source
-> construct Provider Catalogs
-> construct SecretResolver / ModelFactory / Session capability
-> construct PlanResolver
-> construct RunnerRegistry
-> construct Dispatcher
-> construct HTTP Gateway + Admin API
-> expose readiness
Channel Binding 不是 PlanResolver 的直接输入。它必须由 Channel Candidate Resolver 独立查询和验证,形成 RoutingTarget / trusted principal 后,才能进入 Gateway Dispatch。
资源所有权¶
Bootstrap graph 必须记录 DB、Session、RunnerRegistry、Dispatcher、HTTP Handler 的所有权;进程 supervisor 另外拥有 net/http.Server,并负责把 Runtime 的 draining gate 与 Server.Shutdown 串起来:
- 先将 Handler 和 Runtime 标记为 draining;
- 由 cmd/trpc-service 调用 http.Server.Shutdown,停止新请求并等待在途请求;
- bounded close RunnerRegistry,等待或取消剩余 Runner lease;
- 关闭 Bootstrap 自己拥有的 DB/pool 和依赖;
- 不关闭由调用方借用的 Repository、Session、Factory 或 HTTP client。
重复 Close 必须安全;取消和关闭不能遗留 Runner、event consumer、goroutine 或数据库连接。
Readiness 契约¶
/healthz 只表示进程存活,不检查业务依赖。/readyz 是唯一流量接入闸门:
| 条件 | /readyz |
|---|---|
| 已运行 Runtime 的依赖失效或 readiness gate 为 false | 503 |
| DB ping 失败或 migration 未准备好 | 503 |
| Repository/Catalog/Secret/Model/Session capability 缺失 | 503 |
| Resolver、Registry、Dispatcher 或 Handler 未就绪 | 503 |
| 所有依赖可用 | 200 |
| BeginShutdown 已调用 | 503 |
数据库 readiness 检查必须设置短超时,不能因数据库阻塞 HTTP handler。错误响应只能返回稳定类别,例如 not ready 或 configuration unavailable,不能泄露 DSN、provider endpoint、secret reference 的值或 SQL 错误细节。
Admin API 资源与权限¶
第一版 Admin API 只提供控制面所需的最小操作,所有路径都必须从认证 principal 得到可信租户范围。请求体中的 tenant_id 只能作为一致性校验,不能覆盖 principal。
| 资源 | 最小操作 | 写入约束 |
|---|---|---|
| Tenant | create/get/update/transition | tenant identity 不可变;配置更新带 expected_version;状态迁移必须有 actor/reason/correlation |
| Agent App | create/get/update metadata | tenant_id + app_id 同租户;乐观锁 |
| Agent Revision | create/update draft/publish/rollback | 已发布 Revision 不可变;发布和 current pointer 原子提交 |
| Model Profile | create/get/update/transition | Catalog 校验;只保存 secret_ref;版本冲突拒绝覆盖 |
| Backend Profile | create/get/update/transition | binding 能力和租户引用校验;状态迁移受控 |
| Channel Binding | create/get/update/transition | route/account 唯一性、同租户 App 引用、候选路由约束 |
Admin API 的第一版线协议固定如下;所有 JSON 响应都包含 request_id,错误响应为 {"error":"
| 方法 | 路径 | 请求体/成功响应 |
|---|---|---|
| POST | /admin/v1/tenants | tenant.CreateInput;201 + Tenant |
| GET | /admin/v1/tenants/{tenant_id} | 无;200 + Tenant |
| PATCH | /admin/v1/tenants/{tenant_id} | UpdateConfigurationInput;200 + Tenant |
| POST | /admin/v1/tenants/{tenant_id}/status | expected_version,next_status,reason,correlation_id;200 + Tenant + event |
| POST | /admin/v1/tenants/{tenant_id}/apps | agent.CreateInput;201 + App |
| GET | /admin/v1/tenants/{tenant_id}/apps/{app_id} | 无;200 + App |
| PATCH | /admin/v1/tenants/{tenant_id}/apps/{app_id} | UpdateMetadataInput;200 + App |
| POST | /admin/v1/tenants/{tenant_id}/apps/{app_id}/revisions | CreateDraftInput;201 + Revision |
| PATCH | /admin/v1/tenants/{tenant_id}/apps/{app_id}/revisions/{revision} | UpdateDraftInput;200 + Revision |
| POST | /admin/v1/tenants/{tenant_id}/apps/{app_id}/revisions/{revision}/publish | expected_app_version,expected_draft_version,reason,correlation_id;200 + App,Revision,event |
| POST | /admin/v1/tenants/{tenant_id}/apps/{app_id}/rollback | target_revision,expected_app_version,reason,correlation_id;200 + App,event |
| POST | /admin/v1/tenants/{tenant_id}/apps/{app_id}/status | expected_version,next_status,reason,correlation_id;200 + App,event |
| POST/PATCH/GET | /admin/v1/tenants/{tenant_id}/models/{profile_id} | model.CreateInput / UpdateConfigurationInput;201/200 + Profile,event |
| POST/PATCH/GET | /admin/v1/tenants/{tenant_id}/backends/{profile_id} | backend.CreateInput / UpdateConfigurationInput;201/200 + Profile,event |
| POST/PATCH/GET | /admin/v1/tenants/{tenant_id}/bindings/{binding_id} | channels.CreateInput / UpdateConfigurationInput;201/200 + Binding,event |
所有资源的状态动作统一使用 /status,next_status 只能使用领域允许的迁移。读取不存在或跨租户对象统一为 404;认证失败为 401;租户范围不足为 403;字段/状态校验失败为 400;expected version 冲突为 409;存储/依赖故障为 503。
实现必须遵循上述路径和字段;不得以“实现 PR 可调整路由”替换契约。资源路径与动作分离的示例:
POST /admin/v1/tenants
GET /admin/v1/tenants/{tenant_id}
PATCH /admin/v1/tenants/{tenant_id}
POST /admin/v1/tenants/{tenant_id}/status
POST /admin/v1/tenants/{tenant_id}/apps/{app_id}/revisions/{revision}/publish
POST /admin/v1/tenants/{tenant_id}/apps/{app_id}/rollback
示例路径与上表一致;实现不能绕过领域 Repository 直接拼 SQL 或更新发布表。
认证与租户隔离¶
- Admin API 使用独立的管理员认证结果(subject、role、tenant scope),不能把普通对话 API token 自动提升为跨租户管理员;
- principal 的 tenant scope、resource path 的 tenant ID、Repository 的 tenant 条件三者必须一致;
- 无权限、跨租户不存在和越权访问统一映射为稳定的 unauthorized / not found 类别;
- 所有写操作写入审计或 Outbox 事件,记录 actor、reason、correlation ID、旧/新版本和 trace ID;
- expected version 不匹配返回可识别的 conflict,绝不能静默覆盖更新。
重启恢复时序¶
完整验收验证“同一 PostgreSQL 数据 + 两次独立 Bootstrap”,而非仅测试 Repository。生产
Bootstrap 是 migration 唯一 owner:先取得 advisory lock,再由 migrations.Apply 按文件名
顺序执行从 0001_control_plane.up.sql 至 0017_agent_chain.up.sql 的嵌入式序列,并在
schema_migrations 写入版本;重复启动验证已应用版本,版本缺失、超前或内容 digest 不一致均失败。
迁移事务失败时不构造可接流量的 Runtime。
Process A
-> migrate / bootstrap
-> Admin API creates Tenant, App, Revision, Model, Backend, Binding
-> publish active Revision
-> Resolver builds ExecutionPlan
-> bounded shutdown (HTTP -> Registry -> DB)
Process B
-> bootstrap from same DSN and explicit config
-> readiness becomes 200
-> PlanResolver reads persisted Tenant/App/Model/Backend
-> Channel Candidate Resolver reads persisted Binding independently
-> trusted principal routes to the same tenant/app
-> stale expected_version is rejected
测试不得 seed fake roots、绕过 Admin/Repository boundary,或从请求体推导租户来使流程通过。
测试矩阵¶
Bootstrap / readiness¶
- 缺少每个必需环境变量时启动失败且不监听端口;
- PostgreSQL ping/migration 失败时启动失败或 readiness 为 503;
- fake SecretResolver、ModelFactory 和 InMemory Session 可以构造测试 graph;
- readiness 从 503 变为 200,再在 BeginShutdown 后回到 503;
- 数据库和依赖关闭只发生一次,取消不泄漏 goroutine。
Admin API¶
- 管理员认证成功、普通 API principal 被拒绝;
- 同租户 CRUD 成功,跨租户读取/写入/引用失败;
- expected version 冲突不会覆盖已有更新;
- 发布、回滚、状态迁移产生原子变更和事件;
- 明文 token、API key、DSN 不会出现在持久化记录、响应、日志或错误中。
Restart E2E¶
- PostgreSQL migration 只执行一次且可重复验证;
- Process A 写入的数据由 Process B 的新 Resolver/Channel Candidate Resolver 读取;
- Process A 关闭后连接池、Runner 和 Session owner 的生命周期符合声明;
- Process B 不能读取其他租户对象,也不能使用旧版本或未发布 Revision。
干净 PostgreSQL 启动示例¶
createdb trpc_control_plane
export TRPC_POSTGRES_DSN='postgres://postgres:postgres@127.0.0.1:5432/trpc_control_plane?sslmode=disable'
export TRPC_API_TOKEN='chat-token'
export TRPC_TENANT_ID='t_01ARZ3NDEKTSV4RRFFQ69G5FAV'
export TRPC_APP_ID='app_01ARZ3NDEKTSV4RRFFQ69G5FAV'
export TRPC_ADMIN_TOKEN='admin-token'
export TRPC_ADMIN_TENANTS='*'
export TRPC_MODEL_API_KEY='provided-outside-control-plane'
go run ./cmd/trpc-service
启动会由 bootstrap 执行有序 migration;migration 或配置失败时进程不绑定业务端口;已运行进程的依赖失效由 /readyz 返回 503。
验收命令¶
实现阶段至少运行:
go test ./... -count=1
go test -race ./... -count=1
go vet ./...
go build ./...
python -m mkdocs build --strict -f docs/mkdocs.yml
PostgreSQL 集成测试使用 CI 提供的 disposable PostgreSQL;本地快速开始应提供等价 DSN 和 migration 步骤,但不把真实模型供应商或真实 IM 作为本 Issue 的必要依赖。