runcycles/cycles-server-admin
GitHub: runcycles/cycles-server-admin
面向 AI Agent 的多租户预算治理管理 API,通过预留制预算模型、细粒度权限、策略引擎和审计日志为企业级 AI 平台提供全链路成本管控与合规治理能力。
Stars: 1 | Forks: 2
[](https://github.com/runcycles/cycles-server-admin/actions)
[](LICENSE)
[](https://github.com/runcycles/cycles-server-admin/actions)
# Cycles Admin Server — AI Agent 治理的多租户管理
**用于管理 Cycles 部署中的租户、预算、API Key 和策略的管理 API。** 配置 [Cycles Server](https://github.com/runcycles/cycles-server) 在运行时应用的 AI Agent 预算和操作执行策略。
默认支持多租户,具有四个集成层面:租户生命周期和预算账本、API Key 认证和权限执行、运行时预留控制,以及用于可观测性的事件/Webhook 投递。与 [Cycles Protocol v0.1.25.33](https://github.com/runcycles/cycles-protocol/blob/main/cycles-governance-admin-v0.1.25.yaml) 保持一致。
## 文档
- **[`CHANGELOG.md`](CHANGELOG.md)** — 面向拉取 Docker image / JAR 的下游消费者的发布说明。
- **[`OPERATIONS.md`](OPERATIONS.md)** — 面向运维人员的操作手册:指标清单、告警方案、配置调优、事件应急响应。
- **[`AUDIT.md`](AUDIT.md)** — 内部工程历史(根因分析、被否决的备选方案、测试策略决策)。
## 概述
本服务实现了一个建立在四个集成支柱上的预算治理系统:
| 支柱 | 层面 | 目的 |
|--------|-------|---------|
| **租户与预算管理** | 配置 | 租户生命周期、预算账本、策略配置 |
| **认证与授权** | 身份 | API Key 验证、权限执行、审计日志 |
| **运行时执行** | 预留 | 预算预留、提交、余额查询(Cycles Protocol v0.1.24) |
| **事件与 Webhooks** | 可观测性 | 事件发送、Webhook 订阅、带 HMAC 签名的投递 |
## 架构
```
cycles-admin-service/
├── cycles-admin-service-model # Shared domain models, DTOs, enums
├── cycles-admin-service-data # Redis repositories, key service
└── cycles-admin-service-api # REST controllers, auth interceptor, Spring Boot app
```
- **语言:** Java 21
- **框架:** Spring Boot 3.5.11
- **数据存储:** Redis(通过 Jedis 5.2.0)
- **API 文档:** SpringDoc OpenAPI (Swagger UI)
- **测试:** JUnit 5 + TestContainers (Redis)
## 使用 Docker 快速开始
运行 admin server 最快的方式——无需 Java 或 Maven:
```
# 使用来自 GHCR 的预构建镜像
docker compose -f docker-compose.prod.yml up -d
```
服务器在 `http://localhost:7979` 启动。Swagger UI:http://localhost:7979/swagger-ui.html
运行完整技术栈(Admin + Runtime + Events + Redis):
```
# 为 webhook 签名 secrets 生成加密密钥 (在所有 services 之间共享)
export WEBHOOK_SECRET_ENCRYPTION_KEY=$(openssl rand -base64 32)
# 开发环境 (从源代码构建)
docker compose -f docker-compose.full-stack.yml up
# 生产环境 (预构建镜像)
docker compose -f docker-compose.full-stack.prod.yml up -d
```
| 服务 | 端口 | 目的 |
|---------|------|---------|
| Redis | 6379 | 共享状态存储 |
| Admin (`cycles-server-admin`) | 7979 | 租户/预算/Webhook CRUD,事件持久化 |
| Runtime (`cycles-server`) | 7878 | 预留/提交/释放,亚 10ms 级执行 |
| Events (`cycles-server-events`) | 7980 | 带 HMAC 签名的异步 Webhook 投递 |
事件服务是可选的——如果未部署,admin 和 runtime 将继续正常运行。事件和投递会在 Redis 中累积(带有 TTL),直到事件服务启动。
## 前置条件(用于从源码构建)
- Java 21+
- Maven 3.9+
- Redis 7+(或使用 Docker 运行 TestContainers)
## 从源码构建
### 构建
```
cd cycles-admin-service
mvn clean install
```
### 运行
```
cd cycles-admin-service/cycles-admin-service-api
mvn spring-boot:run
```
服务器在 `http://localhost:7979` 启动。Swagger UI 可在 `/swagger-ui.html` 访问。
### 运行集成测试
集成测试使用 TestContainers 自动启动一个 Redis 实例。Docker 必须处于运行状态。
```
cd cycles-admin-service
mvn verify -P integration-tests
```
## 认证
API 使用两种认证方案:
| 方案 | Header | 用途 |
|--------|--------|-----|
| **AdminKeyAuth** | `X-Admin-API-Key` | 系统管理(租户/密钥管理、审计、仪表盘) |
| **ApiKeyAuth** | `X-Cycles-API-Key` | 租户范围内的操作(预算、预留、余额) |
AdminKeyAuth 也可在明确的按操作白名单中作为 ApiKeyAuth 的替代方案被接受——权威列表是在[治理规范](https://github.com/runcycles/cycles-protocol/blob/main/cycles-governance-admin-v0.1.25.yaml)中声明了 `AdminKeyAuth` 的 `security:` 块的操作的并集。该白名单在多个版本中不断扩充(v0.1.25.5 读取操作;v0.1.25.6 注资;v0.1.25.13 createBudget / createPolicy / updatePolicy;v0.1.25.14 六项租户范围内的 Webhook 操作)——查阅按操作的安全块可避免本文档描述与实际代码之间产生偏差。在列表和注资端点上,管理员调用者需要提供租户范围参数(`tenant_id` 或 `tenant`,取决于具体操作——详见规范);如果缺失则返回 400 `INVALID_REQUEST`。通过非租户键唯一标识资源的查找类端点(例如 `GET /v1/admin/budgets/lookup`,其中 `(scope, unit)` 是唯一的)不需要租户参数。
API Key 使用格式 `cyc_live_{random}`(生产)或 `cyc_test_{random}`(测试),其中随机部分为 32 个加密随机字符。密钥以 bcrypt 哈希的形式存储;完整的密钥仅在创建时返回一次。建议过期时间:90 天。
### API Key 权限(共 27 项)
| 类别 | 权限 | 说明 |
|---|---|---|
| **运行时(10 项默认)** | `reservations:create/commit/release/extend/list`, `balances:read`, `budgets:read/write`, `policies:read/write` | 未指定权限时默认分配 |
| **Webhooks(3 项,v0.1.25)** | `webhooks:write`, `webhooks:read`, `events:read` | 用于 `/v1/webhooks` 和 `/v1/events` 的租户自助服务 |
| **Admin 通配符(2 项)** | `admin:read`, `admin:write` | 通配符:`admin:write` 满足任何 `*:write`,`admin:read` 满足任何 `*:read` |
| **Admin 细粒度(12 项,v0.1.25)** | `admin:tenants:read/write`, `admin:budgets:read/write`, `admin:policies:read/write`, `admin:apikeys:read/write`, `admin:webhooks:read/write`, `admin:events:read`, `admin:audit:read` | 通配符的更细粒度替代方案 |
## 环境变量
| 变量 | 必需 | 默认值 | 描述 |
|----------|----------|---------|-------------|
| `REDIS_HOST` | 是 | — | Redis 主机名 |
| `REDIS_PORT` | 是 | — | Redis 端口 |
| `REDIS_PASSWORD` | 是 | — | Redis 密码(留空表示无认证) |
| `ADMIN_API_KEY` | 是 | — | 用于 `X-Admin-API-Key` header 的主管理员 API Key |
| `WEBHOOK_SECRET_ENCRYPTION_KEY` | 否 | (空) | 用于静态 Webhook 签名密钥的 AES-256-GCM 加密密钥。Base64 编码的 32 字节。如果为空,密钥将以明文存储(开发模式)。 |
| `LOG_LEVEL` | 否 | `INFO` | 应用日志级别(`DEBUG`, `INFO`, `WARN`, `ERROR`) |
| `SWAGGER_ENABLED` | 否 | `false` | 在 `/swagger-ui.html` 启用 Swagger UI |
| `EVENT_TTL_DAYS` | 否 | `90` | Redis 中的事件保留期(天) |
| `DELIVERY_TTL_DAYS` | 否 | `14` | Redis 中的 Webhook 投递保留期(天) |
| `DASHBOARD_CORS_ORIGIN` | 否 | `http://localhost:5173` | 允许从浏览器调用 `/v1/**` 的来源的逗号分隔列表。**在生产环境中必须设置**为您的仪表盘 URL(例如 `https://dash.example.com`)。默认值是 Vite 开发服务器,不适用于生产环境的仪表盘部署。 |
### Webhook 密钥加密
Webhook 签名密钥在 Redis 中使用 AES-256-GCM 进行静态加密。`cycles-server-admin`(写入)和 `cycles-server-events`(读取)必须共享相同的加密密钥。
**生成密钥:**
```
openssl rand -base64 32
```
**在 docker-compose 或环境变量中配置:**
```
export WEBHOOK_SECRET_ENCRYPTION_KEY=$(openssl rand -base64 32)
```
**工作原理:**
1. Admin 在将签名密钥存入 Redis 之前对其进行加密:`webhook:secret:{id}` = `enc:`
2. Events 服务在读取时解密,然后再计算 HMAC-SHA256 签名
3. 向后兼容:现有的明文密钥(无 `enc:` 前缀)将按原样返回
4. 如果未设置密钥,两个服务都将以直通模式运行(无加密)
**密钥管理:**
- 将密钥存储在 secrets manager(Vault, AWS Secrets Manager 等)中——而不是 git 中
- 轮换密钥需要重新加密所有现有密钥
- 两个服务必须使用新密钥同时重启
### Webhook 安全(SSRF 防护)
Webhook URL 在创建和更新时会进行验证,以防止 SSRF 攻击:
| 检查项 | 默认值 | 描述 |
|-------|---------|-------------|
| 要求 HTTPS | `allow_http: false` | 仅接受 HTTPS URL。在本地开发中设置为 `true`。 |
| 阻止私有 IP | 阻止 RFC 1918 范围 | 根据 `blocked_cidr_ranges` 检查解析后的 IP |
| URL 模式 | (无) | 通过 `allowed_url_patterns` 设置的可选白名单 |
**默认阻止的 CIDRs:** `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `169.254.0.0/16`, `::1/128`, `fc00::/7`
**通过 API 管理:**
```
# 查看当前 config
curl http://localhost:7979/v1/admin/config/webhook-security \
-H "X-Admin-API-Key: $ADMIN_API_KEY"
# 更新 config
curl -X PUT http://localhost:7979/v1/admin/config/webhook-security \
-H "X-Admin-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"allow_http": true, "blocked_cidr_ranges": []}'
```
**使用 Docker 进行本地开发:** 通过 `docker-compose` 运行完整技术栈时,默认会阻止指向 `localhost` 或 Docker 网关 IP 的 Webhook 接收端。要在本地测试 Webhook:
1. 启用 HTTP 并清除 CIDR 阻止列表:
curl -X PUT http://localhost:7979/v1/admin/config/webhook-security \
-H "X-Admin-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"allow_http": true, "blocked_cidr_ranges": []}'
2. 使用 `host.docker.internal` (macOS/Windows) 或 Docker 桥接 IP 作为 Webhook URL
3. 或者将您的 Webhook 接收器作为容器在同一 Docker 网络上运行
## API 端点
### 支柱 1:租户与预算管理
| 方法 | 路径 | 操作 | 认证 |
|--------|------|-----------|------|
| `POST` | `/v1/admin/tenants` | 创建租户 | Admin |
| `GET` | `/v1/admin/tenants` | 列出租户 | Admin |
| `GET` | `/v1/admin/tenants/{tenant_id}` | 获取租户 | Admin |
| `PATCH` | `/v1/admin/tenants/{tenant_id}` | 更新租户 | Admin |
| `POST` | `/v1/admin/budgets` | 创建预算账本 | ApiKey |
| `GET` | `/v1/admin/budgets` | 列出预算账本 | ApiKey / Admin |
| `GET` | `/v1/admin/budgets/lookup?scope={scope}&unit={unit}` | 精确预算查找 | ApiKey / Admin |
| `PATCH` | `/v1/admin/budgets?scope={scope}&unit={unit}` | 更新预算 | Admin |
| `POST` | `/v1/admin/budgets/fund?scope={scope}&unit={unit}` | 注资/调整预算 | ApiKey / Admin |
| `POST` | `/v1/admin/policies` | 创建策略 | ApiKey |
| `GET` | `/v1/admin/policies` | 列出策略 | ApiKey / Admin |
| `PATCH` | `/v1/admin/policies/{policy_id}` | 更新策略 | ApiKey |
### 支柱 2:认证与授权
| 方法 | 路径 | 操作 | 认证 |
|--------|-----------------|------|
| `POST` | `/v1/admin/api-keys` | 创建 API Key | Admin |
| `GET` | `/v1/admin/api-keys` | 列出 API Keys | Admin |
| `PATCH` | `/v1/admin/api-keys/{key_id}` | 更新密钥属性 | Admin |
| `DELETE` | `/v1/admin/api-keys/{key_id}` | 吊销 API Key | Admin |
| `POST` | `/v1/auth/validate` | 验证密钥并解析租户 | Admin |
| `GET` | `/v1/auth/introspect` | 认证与能力自省 | Admin |
| `GET` | `/v1/admin/audit/logs` | 查询审计日志 | Admin |
### 支柱 3:运行时执行(Cycles Protocol v0.1.24)
| 方法 | 路径 | 操作 | 认证 |
|--------|------|-----------|------|
| `POST` | `/v1/reservations` | 创建预算预留 | ApiKey |
| `POST` | `/v1/reservations/{reservation_id}/commit` | 提交实际消耗 | ApiKey |
| `GET` | `/v1/balances` | 查询预算余额 | ApiKey |
### 支柱 4:事件与 Webhooks (v0.1.25)
**管理员 Webhook 管理** (`X-Admin-API-Key`):
| 方法 | 路径 | 操作 | 认证 |
|--------|------|-----------|------|
| `POST` | `/v1/admin/webhooks` | 创建 Webhook 订阅 | Admin |
| `GET` | `/v1/admin/webhooks` | 列出订阅 | Admin |
| `GET` | `/v1/admin/webhooks/{id}` | 获取订阅 | Admin |
| `PATCH` | `/v1/admin/webhooks/{id}` | 更新订阅 | Admin |
| `DELETE` | `/v1/admin/webhooks/{id}` | 删除订阅 | Admin |
| `POST` | `/v1/admin/webhooks/{id}/test` | 测试 Webhook | Admin |
| `GET` | `/v1/admin/webhooks/{id}/deliveries` | 列出投递 | Admin |
| `POST` | `/v1/admin/webhooks/{id}/replay` | 重放事件(进行中返回 202,冲突返回 409) | Admin |
| `GET` | `/v1/admin/events` | 查询事件 | Admin |
| `GET` | `/v1/admin/events/{id}` | 获取事件 | Admin |
| `GET` | `/v1/admin/config/webhook-security` | 获取 URL 策略 | Admin |
| `PUT` | `/v1/admin/config/webhook-security` | 更新 URL 策略 | Admin |
**租户自助服务** (`X-Cycles-API-Key`,需要 `webhooks:read/write` 或 `events:read`):
| 方法 | 路径 | 操作 | 认证 |
|--------|------|-----------|------|
| `POST` | `/v1/webhooks` | 创建租户 Webhook | ApiKey |
| `GET` | `/v1/webhooks` | 列出租户 Webhooks | ApiKey |
| `GET` | `/v1/webhooks/{id}` | 获取租户 Webhook | ApiKey |
| `PATCH` | `/v1/webhooks/{id}` | 更新租户 Webhook | ApiKey |
| `DELETE` | `/v1/webhooks/{id}` | 删除租户 Webhook | ApiKey |
| `POST` | `/v1/webhooks/{id}/test` | 测试租户 Webhook | ApiKey |
| `GET` | `/v1/webhooks/{id}/deliveries` | 列出租户投递 | ApiKey |
| `GET` | `/v1/events` | 查询租户事件 | ApiKey |
租户可以订阅 `budget.*`、`reservation.*`、`tenant.*`(41 种事件类型中的 27 种)。仅限管理员:`api_key.*`、`policy.*`、`system.*`。
### 预算操作(v0.1.25.6)
| 方法 | 路径 | 操作 | 认证 |
|--------|------|-----------|------|
| `POST` | `/v1/admin/budgets/freeze` | 冻结预算(ACTIVE → FROZEN) | Admin |
| `POST` | `/v1/admin/budgets/unfreeze` | 解冻预算(FROZEN → ACTIVE) | Admin |
`POST /v1/admin/budgets/fund` 也接受 AdminKeyAuth(双重认证,需要 `tenant_id`)。
### 仪表盘(v0.1.25.5)
| 方法 | 路径 | 操作 | 认证 |
|--------|------|-----------|------|
| `GET` | `/v1/admin/overview` | 运行状况概览 | Admin |
| `GET` | `/v1/auth/introspect` | 认证与能力自省 | ApiKey / Admin |
概览端点返回包含租户/预算/Webhook 计数、最高风险数组(超限、负债、失败 Webhook)以及近期事件摘要的单次请求聚合数据。自省端点返回用于仪表盘 UI 门控的有效能力——自 v0.1.25.19(规范 v0.1.25.15)起支持双重认证:管理员密钥返回管理员形态(`auth_type=admin`,所有 15 项能力为 true);租户 API 密钥返回租户形态(`auth_type=tenant`,`tenant_id`,具体权限,派生出的各项能力标志,管理员层面的能力强制设为 false)。
跨 6 个类别的 **41 种事件类型**:budget (16)、reservation (5)、tenant (6)、api_key (6)、policy (3)、system (5)。
**Webhook 特性:** HMAC-SHA256 签名、至少一次投递、指数退避重试、连续失败自动禁用、带有分布式锁的事件重放(防止重复重放)、SSRF 防护(默认阻止私有 IP)。
## 核心概念
### 租户
租户是顶层的隔离边界。所有的预算、密钥和预留都限定在租户范围内。
- **ID 格式:** kebab-case,`^[a-z0-9-]+$`,3-64 个字符(例如,`acme-corp`,`demo-tenant`)
- **状态生命周期:** `ACTIVE` → `SUSPENDED` ↔ `ACTIVE`,或 `* → CLOSED`(不可逆)
- `SUSPENDED` 阻止新的预留;现有的活跃预留仍可提交/释放
- 支持通过 `parent_tenant_id` 实现层级租户(支持预算委派和合并账单)
- **租户创建是幂等的**——使用相同的 `tenant_id` 重试将返回现有租户(200),而不是失败
**租户级别的预留配置:**
| 属性 | 默认值 | 范围 | 描述 |
|----------|---------|-------|-------------|
| `default_commit_overage_policy` | `ALLOW_IF_AVAILABLE` | — | 所有范围的默认超额策略 |
| `default_reservation_ttl_ms` | `60000` (60s) | 1s – 24h | 未指定时的预留默认 TTL |
| `max_reservation_ttl_ms` | `3600000` (1h) | 1s – 24h | 允许的最大 TTL;超出此值的请求将被限制 |
| `max_reservation_extensions` | `10` | 0+ | 每次预留的最大 TTL 延长次数(防止僵尸预留) |
| `reservation_expiry_policy` | `AUTO_RELEASE` | — | 过期预留的处理方式 |
**预留过期策略:**
| 策略 | 行为 |
|--------|----------|
| `AUTO_RELEASE` | 过期预留将在宽限期后自动释放 |
| `MANUAL_CLEANUP` | 需要显式释放或清理作业 |
| `GRACE_ONLY` | 在宽限期内允许提交,随后标记为 `EXPIRED` |
### 预算账本
预算账本跟踪特定 `(scope, unit)` 对的财务状况。每个账本维护:
| 字段 | 描述 |
|-------|-------------|
| `allocated` | 总预算上限 |
| `remaining` | 可用于新预留的余额(透支时可为负数) |
| `reserved` | 被活跃预留锁定的金额 |
| `spent` | 成功提交的消耗量 |
| `debt` | `ALLOW_WITH_OVERDRAFT` 提交产生的透支金额 |
| `overdraft_limit` | 允许的最大负债(0 = 无透支) |
| `is_over_limit` | 当 `debt > overdraft_limit` 时为 `true` |
| `commit_overage_policy` | 按账本覆盖的超额策略;如果未设置则从租户继承 |
**支持的单位:** `USD_MICROCENTS`、`TOKENS`、`CREDITS`、`RISK_POINTS`
**金额值**为 `int64` 整数(非浮点数)。所有 `Amount` 对象都带有明确的 `unit` 字段以防止不匹配。
**账本状态:** `ACTIVE`(正常操作) | `FROZEN`(禁止新建预留、提交或注资;仍允许属性更新) | `CLOSED`(已归档)
**预算周期:** 账本支持可选的 `period_start` / `period_end` 及结转策略:
| 结转策略 | 行为 |
|-----------------|----------|
| `NONE` | 不结转(默认) |
| `CARRY_FORWARD` | 未使用的预算结转到下一周期 |
| `CAP_AT_ALLOCATED` | 剩余额度上限为分配的金额 |
#### 范围
范围使用层级路径:`tenant:acme-corp/workspace:eng/agent:summarizer`
**范围层级语义:**
- 每个范围是**独立的**——父子节点之间没有自动传播
- 父范围不会自动聚合子范围的费用
- 层级验证:如果 `tenant:acme/workspace:eng` 有预算,则 `tenant:acme` 和 `tenant:acme/workspace:eng` 必须同时存在
- 对某个范围的操作不会影响父/子范围,除非通过多范围预留显式指定
**创建预算时的初始状态:** `remaining = allocated`,`reserved = spent = debt = 0`。在针对某个范围进行任何预留之前,必须先存在该范围的预算账本。
### 注资操作
使用 `POST /v1/admin/budgets/fund?scope={scope}&unit={unit}` 并附带以下操作之一:
| 操作 | 效果 |
|-----------|--------|
| `CREDIT` | `allocated += amount`,`remaining += amount`。保留 spent / reserved / debt。 |
| `DEBIT` | `allocated -= amount`,`remaining -= amount`(如果 remaining 将变为负数则失败)。保留 spent / reserved / debt。 |
| `RESET` | `allocated = amount`,`remaining = amount - reserved - spent - debt`。保留 spent / reserved / debt。用于**调整上限**(套餐变更、额度调整)。不适用于计费周期边界——请使用 `RESET_SPENT`。 |
| `RESET_SPENT` (v0.1.25.18+) | `allocated = amount`,`spent = request.spent`(默认为 0),`remaining = allocated - spent - reserved - debt`。保留 reserved(活跃预留跨越周期)和 debt(使用 `REPAY_DEBT` 清除)。用于**开启新的计费周期**、迁移、按比例注册和消耗纠正。可选的 `spent` 字段(Amount,必须 ≥ 0,单位必须匹配)允许操作员设置特定的起始消耗量——发出带有 `spent_override_provided` 标志的 `budget.reset_spent` 事件以供审计。 |
| `REPAY_DEBT` | `debt -= amount`(如果 debt < amount 则使用 remaining)。 |
所有注资操作都支持 `idempotency_key`(防止重复注资;重播的请求返回原始响应)和可选的 `reason` 字段以记录审计轨迹。
### 提交超额策略
控制实际消耗超过预留金额时的行为:
| 策略 | 行为 |
|--------|----------|
| `REJECT` | 如果 actual > reserved 则失败 |
| `ALLOW_IF_AVAILABLE` | 如果有可用余额,从剩余预算中扣除差额 |
| `ALLOW_WITH_OVERDRAFT` | 如果预算耗尽,在 `overdraft_limit` 范围内产生负债 |
策略解析顺序(最高优先级优先):**预留** > **策略** > **预算账本** > **租户默认值**
### 策略
策略定义由范围模式匹配的上限、速率限制和行为规则:
- `tenant:acme-corp` —— 精确匹配
- `tenant:acme-corp/*` —— 所有后代范围
- `agent:*` —— 所有租户中的所有 agent
- `*/agent:summarizer` —— 所有租户中的特定 agent
策略状态:** `ACTIVE` | `DISABLED`
策略支持:
- **优先级排序**——首先评估高优先级策略;冲突时以最高优先级为准
- **生效日期窗口**—— `effective_from` / `effective_until` 用于限时策略(试用、临时限制)
- **速率限制**—— `max_reservations_per_minute`,`max_commits_per_minute`
- **TTL 覆盖**—— `default_ttl_ms`,`max_ttl_ms`,每个匹配范围的 `max_extensions`
- **提交超额策略覆盖**——覆盖租户和预算账本的默认值
### Caps(软着陆约束)
来自 Cycles Protocol v0.1.24:
```
{
"max_tokens": 4096,
"max_steps_remaining": 10,
"tool_allowlist": ["read_file", "search"],
"tool_denylist": ["execute_command"],
"cooldown_ms": 5000
}
```
### 权限
API Key 携带细粒度权限:
| 权限 | 描述 |
|------------|-------------|
| `reservations:create` | 创建预算预留 |
| `reservations:commit` | 提交实际消耗 |
| `reservations:release` | 释放未使用的预留 |
| `reservations:extend` | 延长预留 TTL |
| `reservations:list` | 列出预留 |
| `balances:read` | 查询预算余额 |
| `admin:read` | 读取管理员资源 |
| `admin:write` | 修改管理员资源 |
**默认值:** 租户密钥获得上表中列出的 10 个默认权限。`admin:read` 和 `admin:write` 作为通配符提供向后兼容性(见表)。
可以通过 `scope_filter`(例如,`["workspace:eng", "agent:*"]`)将密钥进一步限制在特定范围。
**密钥更新** (`PATCH /v1/admin/api-keys/{key_id}`) 允许更改权限、scope_filter、名称、描述和元数据,而无需轮换密钥。
**密钥吊销** (`DELETE /v1/admin/api-keys/{key_id}`) 是永久性的且无法撤销。已吊销的密钥将保留在数据库中以记录审计轨迹。使用已吊销密钥创建的活跃预留仍可提交/释放,但不允许进行任何新操作。
### API Key 验证
`POST /v1/auth/validate` 按顺序执行以下检查:
1. 密钥存在于数据库中
2. 密钥哈希匹配
3. 状态为 `ACTIVE`(非 `REVOKED` 或 `EXPIRED`)
4. 当前时间 < `expires_at`
5. 租户为 `ACTIVE`(非 `SUSPENDED` 或 `CLOSED`)
成功时,返回 `tenant_id`、`permissions` 和 `scope_filter`。失败时,返回 `valid: false` 及 `reason`(例如,`"REVOKED"`,`"EXPIRED"`)。结果应以较短的 TTL(约 60 秒)进行缓存,并在密钥吊销时失效。
### 预留治理流程
当调用 `POST /v1/reservations` 时,治理层会执行:
1. 通过 `/v1/auth/validate` 验证 `X-Cycles-API-Key`
2. 从密钥派生有效租户
3. 验证 `Subject.tenant` 是否与有效租户匹配(不匹配则返回 403)
4. 检查租户状态(如果是 `SUSPENDED` 或 `CLOSED` 则阻止)
5. 检查范围对应的预算账本是否存在
6. 应用匹配的策略(上限、速率限制)
7. 执行 Cycles 预留逻辑
8. 将操作记录到审计轨迹中
**提交治理** (`POST /v1/reservations/{id}/commit`):如上所述验证密钥和租户,应用 `ALLOW_WITH_OVERDRAFT` 策略(在 `overdraft_limit` 范围内产生负债),并记录到审计轨迹。
**余额治理** (`GET /v1/balances`):返回完整的账本状态,包括 `debt`、`overdraft_limit` 和 `is_over_limit`;限定在有效租户范围内;支持通过查询参数进行范围过滤。
### 审计日志
`GET /v1/admin/audit/logs` 提供符合合规要求的审计轨迹(SOC2,GDPR)。
**查询过滤器:** `tenant_id`、`key_id`、`operation`、`status`(HTTP 代码)、`resource_type`、`resource_id`、`from`/`to`(日期时间范围)
**保留建议:** 热存储 90 天,冷存储 1 年。
### 分页
所有列表端点均使用**基于游标的分页**:
| 参数 | 描述 |
|-----------|-------------|
| `cursor` | 来自上一次响应 `next_cursor` 的不透明游标 |
| `limit` | 页面大小(默认:50,最大:100) |
响应包含 `next_cursor` 和 `has_more` 字段。
## 错误处理
所有错误均返回标准的 `ErrorResponse`:
```
{
"error": "BUDGET_EXCEEDED",
"message": "Remaining budget insufficient for reservation",
"request_id": "req_abc123",
"details": {}
}
```
**Cycles v0.1.24 错误码:**
`INVALID_REQUEST`、`UNAUTHORIZED`、`FORBIDDEN`、`NOT_FOUND`、`BUDGET_EXCEEDED`、`RESERVATION_EXPIRED`、`RESERVATION_FINALIZED`、`IDEMPOTENCY_MISMATCH`、`UNIT_MISMATCH`、`OVERDRAFT_LIMIT_EXCEEDED`、`DEBT_OUTSTANDING`、`INTERNAL_ERROR`
**治理错误码:**
`TENANT_NOT_FOUND`、`TENANT_SUSPENDED`、`TENANT_CLOSED`、`BUDGET_NOT_FOUND`、`BUDGET_FROZEN`、`BUDGET_CLOSED`、`POLICY_VIOLATION`、`INSUFFICIENT_PERMISSIONS`、`KEY_REVOKED`、`KEY_EXPIRED`、`DUPLICATE_RESOURCE`、`WEBHOOK_NOT_FOUND`、`WEBHOOK_URL_INVALID`、`EVENT_NOT_FOUND`、`REPLAY_IN_PROGRESS` (409 —— 同一订阅上的并发重放)
## 部署模型
| 模型 | 描述 |
|-------|-------------|
| **单一服务** | 所有四个支柱在一个部署中 |
| **分离层面** | Admin + 事件与运行时执行分离 |
| **完整技术栈** | Admin (7979) + Runtime (7878) + Events (7980) + Redis |
## 可观测性
**健康检查端点** (Spring Boot Actuator):
| 端点 | 用途 |
|----------|-----|
| `GET /actuator/health` | 聚合健康状况(用于调试) |
| `GET /actuator/health/liveness` | K8s liveness 探针 —— JVM 是否存活? |
| `GET /actuator/health/readiness` | K8s readiness 探针 —— 应用是否准备好处理流量? |
| `GET /actuator/info` | 构建信息(版本、git commit) |
| `GET /actuator/prometheus` | Prometheus 抓取端点 |
Docker 健康检查会访问 `/actuator/health/liveness`(而非聚合端点),因此降级的就绪状态不会导致容器重启。
**指标:**
Spring Boot 自动发布 `http_server_requests_seconds_*`(按 URI + 方法 + 状态统计的延迟/计数/错误)、JVM、Jedis 连接池和 logback 计数器。Admin 服务为域操作添加了两个自定义计数器:
| 指标 | 标签 | 描述 |
|--------|------|-------------|
| `cycles_admin_events_emitted_total` | `type`,`result` | 发出的事件(`result` = `success` | `failure`) |
| `cycles_admin_webhook_dispatched_total` | `result` | Webhook 投递入队尝试(`result` = `queued` | `failure`) |
所有指标都标有 `application=cycles-admin-service`,用于多服务的 Prometheus/Grafana 仪表盘。
**CORS:**
浏览器客户端(仪表盘)通过 CORS 调用 `/v1/**`。服务器白名单包括:
| | 值 |
|---|---|
| 方法 | `GET`、`POST`、`PATCH`、`DELETE`、`OPTIONS` |
| 请求头 | `X-Admin-API-Key`、`X-Cycles-API-Key`、`X-Request-Id`、`Content-Type` |
| 暴露的响应头 | `X-Request-Id`(用于关联) |
| 来源 | `DASHBOARD_CORS_ORIGIN` 环境变量(逗号分隔),默认 `http://localhost:5173` |
支持多来源,适用于在同一服务器后的预发布和生产部署:`DASHBOARD_CORS_ORIGIN=https://dash.example.com,https://staging.example.com`。
## 协议规范
完整的 OpenAPI 3.1.0 规范是 [cycles-protocol](https://github.com/runcycles/cycles-protocol) 仓库中的 [`cycles-governance-admin-v0.1.25.yaml`](https://github.com/runcycles/cycles-protocol/blob/main/cycles-governance-admin-v0.1.25.yaml)。
v0.1.25 增加了支柱 4(事件与 Webhooks):41 种事件类型、20 个 Webhook 端点、HMAC-SHA256 签名、至少一次投递以及静态 Webhook 密钥加密。
v0.1.25.4 强制执行严格的规范合规性:对所有请求和响应模型设置 `additionalProperties: false`,根据规范对所有字段进行范围/大小约束,分布式重放锁(409 `REPLAY_IN_PROGRESS`),以及嵌套对象上的级联 `@Valid`。
v0.1.25.5 增加了管理员仪表盘支持:双重认证白名单(AdminKeyAuth 用于预算/策略读取)、精确预算查找、服务器聚合概览端点、带能力的认证自省,以及严格的仪表盘响应模式。
v0.1.25.6 增加了预算冻结/解冻操作端点、注资上的双重认证以及细粒度租户权限(`budgets:read/write`、`policies:read/write`)。
v0.1.25.7 增加了向后兼容的通配符回退(`admin:write` 满足任何 `*:write`,`admin:read` 满足任何 `*:read`),用于更新密钥权限/元数据而无需轮换密钥的 `PATCH /v1/admin/api-keys/{key_id}`,可重用的 `Permission` 枚举模式,全部 45 个端点上的完整 401 覆盖,集中的 FROZEN 语义,详细的 Webhook 测试错误消息,以及在所有修改端点上带有 `resource_type`、`resource_id` 和上下文 `metadata` 的丰富审计条目。
v0.1.25.8 增加了针对 v0.1.26 就绪的仪表盘和可观测性强化:`EventDataReservationDenied` 可扩展性(`policy_id`、`deny_detail`、开放字符串 `reason_code`),`AdminOverviewResponse` 增强(在 v0.1.25.x 上自动填充的 `recent_denials_by_reason`,加上为 v0.1.26 扩展保留的 `quota_health`、`access_control_stats`、`tenant_counts.in_observe_mode`),并在 `listTenants`(`observe_mode`)和 `listPolicies`(`has_action_quotas`、`references_action_kind`)上接受并忽略查询参数。所有新增内容均向后兼容——当新字段为空时,`@JsonInclude(NON_NULL)` 保持响应不变。
v0.1.25.9 是一个仅包含运维强化的补丁版本——**无 API 接口变更**,规范停留在 v0.1.25.8。新增:在 `/actuator/prometheus` 的 Micrometer/Prometheus 指标,带有自定义计数器(`cycles_admin_events_emitted_total`、`cycles_admin_webhook_dispatched_total`);在 `/actuator/health/liveness` 和 `/actuator/health/readiness` 的 Kubernetes liveness/readiness 探针(docker-compose 健康检查切换为 liveness);CORS 修复——`X-Cycles-API-Key` 和 `X-Request-Id` 现已加入白名单(之前在预检时均被阻止,破坏了使用租户认证的浏览器仪表盘),并通过逗号分隔的 `DASHBOARD_CORS_ORIGIN` 环境变量支持多来源 CORS。
v0.1.25.10 是一个针对 `cycles-governance-admin-v0.1.25` 强化规范合规性的补丁版本(无 API 接口变更)。(1) **`Permission` 枚举现已建模**——27 个规范权限值是一个使用 Jackson `@JsonValue`/`@JsonCreator` 的类型化 Java 枚举;入站 `POST /v1/admin/api-keys` 和 `PATCH /v1/admin/api-keys/{key_id}` 现在在反序列化时会拒绝未知的权限字符串(例如像 `"budgets:wirte"` 这样的拼写错误)并返回 400,而不是静默存储它们。传输格式未变。(2) **`AuthIntrospectResponse.capabilities` 结构化类型化**——用一个专用的 `Capabilities` 类替换了 `Map`,根据规范的“所有字段存在”契约暴露八个必需的布尔值(`view_overview`、`view_budgets`、`view_events`、`view_webhooks`、`view_audit`、`view_tenants`、`view_api_keys`、`view_policies`)。JSON 形态一致。
v0.1.25.11 默认开启**硬性失败契约测试**——无 API 接口变更,仅在构建时生效。来自 13 个管理控制器的每个 2xx 响应现在都会在每次构建时针对从 `cycles-protocol@main` 获取的权威 `cycles-governance-admin-v0.1.25.yaml` 规范进行验证。未来服务器与规范之间的任何偏差——缺少必填字段、违反 `additionalProperties: false` 的额外字段、类型/枚举/min-max 不匹配——都将导致构建失败。依赖于 v0.1.25.10(服务器端 `Permission`/`Capabilities` 修复)、cycles-protocol 规范 v0.1.25.10(规范端的 `SignedAmount`、`BalanceListResponse`、严格 PATCH 请求体)以及测试夹具重命名(tenant_id `minLength:3`)。这三项均已合并;切换仅是一行默认值更改。在离线开发中使用 `-Dcontract.validation.enabled=false` 或 `CONTRACT_VALIDATION_ENABLED=false` 禁用。有关操作手册,请参见 [docs/contract-testing.md](docs/contract-testing.md)。
v0.1.25.12 是一个规范合规性 + 可观测性版本。**一个传输变更:** 包含枚举字段的出站 Webhook 事件负载现在发出符合规范的值——`EventDataBudgetLifecycle.operation` 之前发出小写的 `"create"` / `"update"`,而规范要求大写的 `CREATE` / `UPDATE` / `STATUS_CHANGE`;服务器现在发出正确的值。之前进行区分大小写匹配小写的消费者需要更新(这些小写值从未在规范中记录过)。**新增 Prometheus 指标:** `/actuator/prometheus` 处的 `cycles_admin_events_payload_invalid_total{type, expected_class}`——计算每个无法通过其规范分配的 `EventData*` 类进行完整往返的 `data` 负载的事件发送。非零值表示生产者回归;可设置告警。**仅限构建时(无运行时影响):** 在每次 CI 运行时对 SpringDoc `/v3/api-docs` 与固定规范进行完整结构化差异比较(捕获缺失/额外端点);4xx/5xx 错误响应现在根据 `ErrorResponse` 模式进行验证;每个 `EventData*` 负载使用 `@JsonIgnoreProperties(ignoreUnknown=false)` 进行类型化,以便格式错误的负载在序列化时快速失败;规范覆盖率断言在任何具有零测试的规范端点上导致构建失败;`BudgetOperation` / `ThresholdDirection` / `RateSpikeMetric` 枚举替换了 EventData 类上的原始 `String` 字段;运行时 `EventService.emit` 在负载形态与规范预期不匹配时记录 WARN。与 `cycles-protocol@main` 保持一致,后者在所有 43 个操作上增加了 400 响应文档(cycles-protocol v0.1.25.11)。总计:在合规性推进期间发现并修复了跨规范 + 服务器 + 夹具的 13 个真实偏差——参见 AUDIT.md。
v0.1.25.13 修复了 `PUT /v1/admin/config/webhook-security` 上的 CORS 预检回归([仪表盘 #30](https://github.com/runcycles/cycles-dashboard/issues/30))。`WebConfig.addCorsMappings` 的 `allowedMethods` 中缺少 `PUT`,因此任何跨域仪表盘(例如直接访问管理端的 Vite 开发服务器)都会在 `AuthInterceptor` 运行之前看到来自 Spring CorsFilter 的 `403 Forbidden`——没有应用日志,静默拒绝。这是管理端 API 中唯一规范定义的 `PUT` 端点;所有其他修改都使用 POST/PATCH/DELETE。增加了锁定预期方法列表的回归测试。同源部署(标准的 nginx 代理生产技术栈)不受影响。无规范变更。
v0.1.25.14 实现了**管理员代持双重认证的第一阶段**(规范方面:[cycles-protocol#36](https://github.com/runcycles/cycles-protocol/pull/36),v0.1.25.13)。填补了一个长期存在的仪表盘功能缺口:管理员操作员以前无法创建预算或策略,因为这些端点只接受 `ApiKeyAuth`(X-Cycles-API-Key),而仪表盘仅使用 `X-Admin-API-Key` 进行认证。**新增双重认证**至 `POST /v1/admin/budgets`(createBudget)、`POST /v1/admin/policies`(createPolicy)和 `PATCH /v1/admin/policies/{policy_id}`(updatePolicy)。`BudgetCreateRequest` 和 `PolicyCreateRequest` 增加了一个可选的 `tenant_id` 字段;管理员调用者必须发送该字段,租户调用者绝不能发送(严格双向验证——防止租户伪造创建)。对于管理员驱动的调用,审计记录标记为 `actor_type=admin_on_behalf_of`(新的 `ActorType` 枚举值,带有 `@JsonValue = "admin_on_behalf_of"`),而对于租户自助服务则标记为 `api_key`。还包括 `AuthInterceptor.preHandle` 中的纵深防御路径遍历防护(使用 400 短路拦截 `/../`、`/./`、末尾的 `/..`),并将双重认证路径匹配器切换为 `getServletPath()` 以实现与 Spring 调度器的规范化一致性。
v0.1.25.15 增加了**规范化范围验证**。用户报告使用 `scope=tenant:acme/agentic:codex` 创建预算成功了("agentic" 是规范化种类 `agent` 的拼写错误);探测表明服务器基本上没有进行范围验证。根据 `cycles-protocol-v0.yaml` 的规范性排序(`tenant → workspace → app → workflow → agent → toolset`),非规范范围会静默破坏下游执行(预留无法匹配它们)并污染审计轨迹。**新增 `ScopeValidator`** 强制执行:第一段必须是 `tenant:`;仅接受规范化种类;种类必须按规范顺序出现且不能重复;每个 id 非空、≤128 个字符、匹配 `[A-Za-z0-9._-]+`;范围的租户必须与请求的 `tenant_id` 匹配。从 `BudgetController.create` 和 `PolicyController.create`/`update` 调用。策略的 `scope_pattern` 允许终端通配符 `*` / id 通配符 `agent:*`(按规范);预算范围保持具体。拒绝请求将返回 400 `INVALID_REQUEST`,并附带一条指出哪一段违反了哪条规则的具体消息。规范合规性未变——这加强了规范已描述为规范化内容的执行。
v0.1.25.16 是**管理员代持双重认证的第 3 阶段(最终阶段)**,用于租户范围的 Webhook 操作(规范:[cycles-protocol#40](https://github.com/runcycles/cycles-protocol/pull/40),v0.1.25.14)。运维用例:租户的 Webhook 端点在生产环境中不断闪烁,运维人员收到告警,需要暂停/强制删除/检查,而无需等待租户轮换密钥。**新增双重认证**至 `WebhookTenantController` 下的 6 个端点:`GET /v1/webhooks`(管理员调用下新增必填的 `tenant` 查询参数;在 ApiKey 下绝不能设置),`GET /v1/webhooks/{id}`,`PATCH /v1/webhooks/{id}`,`DELETE /v1/webhooks/{id}`,`POST /v1/webhooks/{id}/test`,`GET /v1/webhooks/{id}/deliveries`。**故意不支持双重认证:** `POST /v1/webhooks`(创建)——URL、签名密钥和事件类型选择是租户策略;以及 `POST /v1/webhooks/{id}/replay`,该端点在 `/v1/admin/webhooks/{id}/replay` 路径下已经是仅限管理员的。管理员驱动的修改/测试的审计条目标记为 `actor_type=admin_on_behalf_of`;审计主体是订阅的拥有租户(而不是管理员调用者)。`ADMIN_ALLOWED_PREFIXES` 增加了 4 个条目;`GET:/v1/webhooks` 被添加到 `ADMIN_ALLOWED_ENDPOINTS` 精确白名单中。
v0.1.25.25 在六个管理员列表端点(`listTenants`、`listBudgets`、`listApiKeys`、`listAuditLogs`、`listWebhookSubscriptions`、`listEvents`)上增加了**自由文本 `search`**——完成了治理规范 v0.1.25.21 的第一个要点。可选的 `search` 查询参数,不区分大小写的子字符串匹配,`maxLength: 128`,与其他每个过滤器进行 AND 组合。每个端点的匹配字段(在端点内进行 OR 组合):`listTenants`(`tenant_id`、`name`),`listBudgets`(`tenant_id`、`scope`),`listApiKeys`(`key_id`、`name`),`listAuditLogs`(`resource_id`、`log_id`),`listWebhookSubscriptions`(`subscription_id`、`url`),`listEvents`(`correlation_id`、`scope`)。新增的共享 `SearchSpec` 验证器锁定了 `trim → 空值检查 → 长度检查` 的顺序,因此末尾的空格无法绕过上限。游标稳定性不变量(搜索谓词在游标提交**之前**应用)由 `EventRepositoryTest` 和 `AuditRepositoryTest` 中显式的 page1→page2 游标遍历测试断言。
v0.1.25.26 增加了**过滤器驱动的批量生命周期操作**——`POST /v1/admin/tenants/bulk-action`(`SUSPEND`/`REACTIVATE`/`CLOSE`)和 `POST /v1/admin/webhooks/bulk-action`(`PAUSE`/`RESUME`/`DELETE`),完成了治理规范 v0.1.25.21 的剩余要点。请求格式:`{filter, action, expected_count?, idempotency_key}`。响应格式:`{action, total_matched, succeeded[], failed[], skipped[], idempotency_key}`。**四个安全门控:** 空过滤器 → 400 `INVALID_REQUEST`;幂等重放返回缓存信封(15 分钟 TTL);匹配数 >500 → 400 `LIMIT_EXCEEDED`;`expected_count` 不匹配 → 409 `COUNT_MISMATCH`(预览→提交防误操作)。新增的共享 `IdempotencyStore` 原语专用于批量操作(有意不迁移 Lua 原子的 `BudgetController.fund` 幂等性——外部化会破坏与余额修改的原子性)。与 cycles-protocol 规范 v0.1.25.23 配对,后者将 `COUNT_MISMATCH` 和 `LIMIT_EXCEEDED` 添加到 `ErrorCode` 枚举中,因此响应验证器不会拒绝符合规范的服务器。
v0.1.25.27 发布了**审计日志过滤器 DSL 升级**(在 `listAuditLogs` 上)(规范 v0.1.25.24)。四个新的查询参数——`error_code`(IN 列表)、`error_code_exclude`(NOT-IN 列表)、`status_min`、`status_max`(包含范围,与精确 `status` 互斥)——并将 `operation` / `resource_type` 从标量提升为 `array`(`explode=false`)。IN 列表语义:区分大小写,maxItems 25,未知代码不匹配任何内容(向前兼容)。NULL 语义的不对称性是明确的:NULL `entry.error_code` 永远不会匹配 `error_code`(询问“显示代码 X”的审计员永远不希望看到成功行),但总是通过 `error_code_exclude`(隐藏嘈杂代码绝不能静默隐藏成功)。`search` 匹配集从 `{resource_id, log_id}` 扩展到 `{resource_id, log_id, error_code, operation}`——填补了 `?search=BUDGET` 遗漏 `BUDGET_EXCEEDED` 的自由文本空白。所有变更在传输层均是新增的;单个标量 `?operation=createBudget` 仍被解析为单元素列表。游标稳定性不变量(v0.1.25.25)扩展到了每个新谓词。
v0.1.25.28 拆分了**审计 `tenant_id` 标记**(规范 v0.1.25.25),使得管理员层面的合规信号不再按匿名 401 计划老化。之前的 `""` 标记混淆了两个截然不同的群体——认证前失败(可被用于 DDoS 放大的噪音)和平台管理员认证的请求(高信号治理操作)。现在:`__admin__` 乘用认证层的 TTL(默认 400 天)且从不被采样;`__unauth__` 乘用未认证 TTL(默认 30 天)且仍受 `audit.sample.unauthenticated` 约束。两者都是 URL 安全的(租户语法排除了下划线,不需要百分号编码),因此运维仪表盘可以直接过滤 `?tenant_id=__admin__`,而不会出现 `%3C…%3E` 的丑陋字符。**向后兼容:** 带有字面量 `` 的历史行仍路由到未认证层的 TTL——没有行会静默翻转为长保留期。硬编码为旧标记的仪表盘工具应迁移至 `?tenant_id=__unauth__`(用于认证前失败切片)并新增 `?tenant_id=__admin__`(用于新的管理员层面切片)。
v0.1.25.28.1 是一个**仅限测试的点发布**,修复了夜间审计浸泡不变量 `AS4` 中的覆盖率缺口。在 v0.1.25.28 将认证前标记拆分为 `__unauth__` + `__admin__` 之后,浸泡测试的按层级等式求和仍然只包含 `__unauth__` + tenant-soak。管理员层面的 4xx 层(`__admin__`)在首次发布后的运行中正好持有短缺的数量(5077 行 = 400 响应的数量)。生产环境的审计写入路径始终是正确的——所有 14000 次写入都落地到了全局索引并增加了计数器。更新了一个测试文件;无服务器/规范/数据变更。
v0.1.25.29 增加了**预算批量操作端点**——`POST /v1/admin/budgets/bulk-action`——解决了 [cycles-server-admin#99](https://github.com/runcycles/cycles-server-admin/issues/99)(“在租户或父范围级别进行批量预算重置”)。仅限 AdminKeyAuth。五个操作复用了 `FundingOperation`:`CREDIT`、`DEBIT`、`RESET`、`REPAY_DEBT`、`RESET_SPENT`(均需要 `amount`;`spent` 仅在 `RESET_SPENT` 时被处理)。过滤器与 `listBudgets` 一一对应(`tenant_id` 必填——根据规范,跨租户批量操作明确超出范围;`scope_prefix`、`unit`、`status`、`over_limit`、`has_debt`、`utilization_min/max`、`search` 可选),并使用相同的 `BudgetListFilters` 匹配器,因此通过列表预览和批量应用的匹配集在字节级别上完全一致。**安全门控**与租户/Webhook 批量操作(v0.1.25.26)相同:500 行上限(`LIMIT_EXCEEDED` 400)、`expected_count` 门控(`COUNT_MISMATCH` 409)、15 分钟幂等重放,即使存在逐行失败也返回 HTTP 200。**逐行分类:** 包含 `BUDGET_EXCEEDED`(DEBIT 溢出)、`INVALID_TRANSITION`(单位不匹配 / FROZEN / CLOSED)、`NOT_FOUND`(在匹配和执行之间被删除)或 `INTERNAL_ERROR` 的 `failed[]`;包含 `reason=ALREADY_IN_TARGET_STATE`(目前仅限 `debt==0` 时的 `REPAY_DEBT`)的 `skipped[]`。**双重应用防护:** 每行的幂等键 `{bulkKey}:{scope}:{unit}` 贯穿到 `BudgetRepository.fund` 中,让现有的 Lua fund-idempotency 缓存能够短路任何在其上次运行已实际落地的行,因此在更严格的过滤器上重试失败集不会导致双重应用。操作员工作流:`listBudgets`(预览)→ `bulk-action` → 检查 `failed[]` → 缩小过滤范围 → 使用**新的** `idempotency_key` 重新运行。
v0.1.25.30 是针对所有三个批量操作端点(`bulkActionTenants`、`bulkActionWebhooks`、`bulkActionBudgets`)的**分类富化发布**——无规范升级。在 v0.1.25.30 之前,每次批量操作发出的单一 `AuditLogEntry` 在其 `metadata` 中仅包含桶计数 + `idempotency_key`;事件后的分类需要操作员自己保留一份响应信封的副本或重新运行操作(这对于 DELETE / DEBIT 等破坏性操作是不可接受的)。现在,仅凭审计条目就足够了。**五个新的 `metadata` 键**(新增):`succeeded_ids`(成功操作的逐行 ID)、`failed_rows`(每个失败的完整 `id + error_code + message`——取代了“重新运行以查看中断原因”的工作流)、`skipped_rows`(每个跳过的完整 `id + reason`——区分 `ALREADY_IN_TARGET_STATE` 与 `ALREADY_DELETED`)、`filter`(按原样回显的规范化过滤器——重建操作员意图)、`duration_ms`(从处理程序入口到审计发出的挂钟时间,用于 SLO 分类)。合并到一个单一的 `BulkActionAuditMetadataBuilder` 助手中,因此未来的批量操作端点不会在键集或顺序上产生偏差。最坏情况下的元数据大小约为 40 KB(500 行上限 × 每个结果约 80 B),远在 Redis value 大小的舒适范围内。无需规范升级——`AuditLogEntry.metadata` 已被类型化为带有 `additionalProperties: true` 的 `object`。有关操作手册,请参见 [OPERATIONS.md §Audit coverage → Bulk-action triage](OPERATIONS.md#bulk-action-audit-triage)。
v0.1.25.31 是**跨层面关联的服务器实现**(规范 v0.1.25.28,cycles-protocol PRs #56 + #58)——在现有的 `request_id` / `correlation_id` 之上增加了第三个关联层。`trace_id`(W3C Trace Context,`^[0-9a-f]{32}$`)从入站的 `traceparent` 或 `X-Cycles-Trace-Id` headers 中捕获(或在缺失时由服务器生成),传播到每个 `ErrorResponse`、`AuditLogEntry`、`Event` 和出站的 `WebhookDelivery` 上(v0.1.25.28 补丁了 `WebhookDelivery` 模式以声明 `trace_id` / `trace_flags` / `traceparent_inbound_valid`,因此该服务器的负载能干净地符合规范),并在每个响应中作为 `X-Cycles-Trace-Id` 响应头回显。入站优先级:有效的 `traceparent` → 有效的 `X-Cycles-Trace-Id` → 生成(16 个随机字节 → 32 位十六进制,根据 W3C §3.2.2.3 重新滚动全零)。格式错误的入站关联 headers 会被容忍(回退到下一个规则);服务器绝不会因为错误的关联 header 而拒绝请求。有效的入站 trace-flags 被保留用于出站 Webhook 投递,因此选择退出采样的上游(`00`)会被尊重,而不是被静默重新启用。`GET /v1/admin/audit/logs` 和 `GET /v1/admin/events` 上新增的精确匹配查询参数 `trace_id` + `request_id` 使得仪表盘的 JOIN 操作变成每个列表一次查询。传输层完全新增——没有 `trace_id` 的历史行继续通过严格的 Jackson 往返传输。
v0.1.25.32 是一个针对仅管理端的**读取端反序列化容忍度调整**——无传输契约变更。`Event` 和 `WebhookDelivery` 现在带有类级别的 `@JsonIgnoreProperties(ignoreUnknown = true)`,因为运行时(`cycles-server`)是 `event:*` 和 `delivery:*` Redis 记录的权威写入者,而管理端只读取它们。在 .32 之前,如果运行时在补丁版本中发布了一个新增字段,管理端的 `listEvents` / `listWebhookDeliveries` 将抛出 `UnrecognizedPropertyException`,直到管理端同步更新 POJO——这违反了管理/运行时分离所建立的“新增字段是安全的”不变量。现在运行时可以在任何补丁中发布新增字段,而无需强制发布管理端版本。**在管理端拥有的模式上保留严格模式**(`WebhookSubscription`、`Tenant`、`Budget`、`Policy`、`ApiKey`,每个 `EventData*` 子类型,每个 `Bulk*Request`/`Filter`,每个 `*CreateRequest`/`UpdateRequest`)——管理端写入这些内容,因此那里的拼写错误属于管理端内部错误,仍然必须大声失败。两个新的测试用例固定了该不变量,因此未来的回归(有人重新添加了 `ignoreUnknown = false`)将导致构建失败。同时删除了 `ErrorResponse` 上在运行时永远无法到达的死代码 `@JsonIgnoreProperties(ignoreUnknown = false)`。
v0.1.25.33 是一个**安全补丁**:Spring Boot 3.5.11 → 3.5.13,并带有 `10.1.54 ` 属性覆盖,关闭了针对 `tomcat-embed-core 10.1.52` 的 4 个 HIGH/CRITICAL 级别的 CVE(CVE-2026-29145 CRITICAL,CVE-2026-29129 HIGH,CVE-2026-34483 HIGH,CVE-2026-34487 HIGH)。SB 3.5.13 传递性地带来了 10.1.53;属性覆盖解决了剩余的两个。同一 3.5.x 行内的补丁级别升级——无 API 接口变更。
v0.1.25.34 是 v0.1.25.33 Spring Boot 升级的**安全后续措施**:Trivy 标记了 SB 3.5.13 的 BOM 未涵盖的一个剩余 HIGH 发现(CVE-2025-48924),通过在现有 Tomcat 覆盖旁边添加 `3.18.0 ` 解决。当 SB 发布包含 3.18.0+ 管理的版本时可移除。无 API 接口变更。
v0.1.25.35 是**租户关闭级联的服务器实现**(规范 v0.1.25.29 规则 1 + 规则 2)。当租户转换为 `CLOSED` 时,其拥有的对象现在在同一个请求中原子性地转换为它们的终止状态:`BudgetLedger` → `CLOSED`(标记 `closed_at`,释放任何未完成的 `reserved` 金额),`WebhookSubscription` → `DISABLED`,`ApiKey` → `REVOKED`(标记 `revoked_at`,原因为 `tenant_closed`)。在关闭时任何 `reserved > 0` 的预算还会发出一个聚合的 `reservation.released_via_tenant_cascade` 事件。所有受影响的行共享发起请求的 `request_id` + `trace_id` 以及一个专用的 `correlation_id = tenant_close_cascade::`,以便操作员可以通过三者中的任何一个进行 JOIN。级联从带有 `status=CLOSED` 的 `PATCH /admin/tenants/{id}` 和批量操作的 `CLOSE` 路径触发;当租户已经关闭时是幂等的。伴随的**规则 2 `TENANT_CLOSED` 修改守卫**(409)在控制器层短路了对已关闭租户所拥有对象的修改,最初涵盖了预算的 create/update/fund/freeze/unfreeze 和 Webhook 的 create/update。增加了四种级联事件类型(`BUDGET_CLOSED_VIA_TENANT_CASCADE`、`RESERVATION_RELEASED_VIA_TENANT_CASCADE`、`WEBHOOK_DISABLED_VIA_TENANT_CASCADE`、`API_KEY_REVOKED_VIA_TENANT_CASCADE`)以及一个新的 `WEBHOOK` 事件类别。租户关闭的审计 + 事件负载现在带有一个 `cascade_summary` 映射(`budgets_closed`、`webhooks_disabled`、`api_keys_revoked`、`reservations_released`)。仅为新增的传输接口。
v0.1.25.36 弥补了 v0.1.25.35 的 AUDIT 条目中指出的**规则 2 修改守卫覆盖范围缺口**。现在,对拥有租户处于 `CLOSED` 状态的对象的任何修改,都会从每个管理员修改端点返回 `409 TENANT_CLOSED`,根据规范 v0.1.25.29 的 MUST 要求。新增的守卫调用点:策略的 create/update;API 密钥的 create/update/delete;Webhook 的 create/update/delete/test/replay;租户 Webhook 的 delete/test;`POST /v1/admin/budgets/bulk-action` 和 `POST /v1/admin/webhooks/bulk-action` 中的逐行守卫(拥有者已关闭的行落入 `failed[]`,带有 `error_code: "TENANT_CLOSED"`;同级行继续进行)。无传输契约变更——`TENANT_CLOSED` 已在 v0.1.25.35 的共享 `ErrorCode` 枚举中发布;.36 仅增加了调用点和行级分类器分支。
v0.1.25.37 是一个**有界收敛修复(规范 v0.1.25.31 规则 1(c))。以前的版本在 PATCH 路径中通过 `isFreshClose` 以及在批量操作中通过 `ALREADY_IN_TARGET_STATE` 来门控级联,因此在级联中途发生崩溃会导致租户处于 CLOSED 状态,但任何遗留的子项仍处于非终止状态,并且重新发出关闭命令会静默地成为无操作。`OPERATIONS.md` 中记录的恢复路径(重新发出关闭命令;级联是幂等的并会拾取遗留项)现在与代码相匹配。带有 `status=CLOSED` 的 `PATCH /v1/admin/tenants/{id}` 在每次请求时都会重新调用级联,而不管先前的状态;父事件在重试时降级为 `tenant.updated`(而不是 `tenant.closed`),因此消费者不会看到重复的关闭事件。带有 `action=CLOSE` 的 `POST /v1/admin/tenants/bulk-action` 会跳过对已关闭行的冗余 `repo.update`,但仍然运行级联:如果有任何子项发生状态转换,该行将被归入 `succeeded`;完全无操作则归入 `skipped`,原因为 `reason=ALREADY_IN_TARGET_STATE`。级联服务语义未变——每个子项已经是幂等的(存储库查询按非终止状态过滤);仅移除了控制器级别的重新进入门控。无传输契约变更。
v0.1.25.39 增加了**Webhook 生命周期事件**(规范 v0.1.25.33),填补了 v0.1.25.38 明确推迟的运营商可观测性盲点。单次操作的 `createWebhookSubscription` / `updateWebhookSubscription` / `deleteWebhookSubscription` 端点以及 `POST /v1/admin/webhooks/bulk-action`(PAUSE/RESUME/DELETE)路径现在发出类型化的事件:`webhook.created`、`webhook.updated`、`webhook.paused`、`webhook.resumed`、`webhook.deleted`(外加为 `cycles-server-events` 中的调度器自动禁用保留的 `webhook.disabled`)。新的 `EventDataWebhookLifecycle` 负载模式包含 `subscription_id`、`tenant_id`、`previous_status`、`new_status`、`changed_fields`、`disable_reason`。更新端点根据实际转换对发出类型进行分类(`ACTIVE → PAUSED` → `webhook.paused`,`PAUSED → ACTIVE` → `webhook.resumed`,仅字段 PATCH → `webhook.updated`)。批量 correlation_id:`webhook_bulk_action::`(每次调用一个,在行间共享);单次操作的 correlation_id:`webhook_create:`、`webhook_update::`、`webhook_delete:`。跳过/失败的行不发出事件。新增枚举 + 模式——无传输中断。
v0.1.25.38 在批量操作端点上增加了**逐行事件**(规范 v0.1.25.32)。在此版本之前,`POST /v1/admin/budgets/bulk-action` 和 `POST /v1/admin/tenants/bulk-action` 每次调用只写入一条聚合的 `AuditLogEntry`,而匹配的单次操作端点则发出每次操作的生命周期事件——每当相同的逻辑操作走批量路径时,监控 `listEvents` 的操作员就会存在盲点。`bulkActionBudgets` 现在为每个成功修改的行发出一个事件,按操作类型化(`budget.funded` / `budget.debited` / `budget.reset` / `budget.reset_spent` / `budget.debt_repaid`),带有 `correlation_id = budget_bulk_action::`。`bulkActionTenants` 为每个成功修改的行发出一个父事件(`tenant.suspended` / `tenant.reactivated` / `tenant.closed`),带有 `correlation_id = tenant_bulk_action::`;对于 action=CLOSE,这附加于现有的 `tenant_close_cascade::` 级联扇出事件(互补的 correlation_id——跟踪批量调用的操作员查询批量 ID;跟踪特定租户关闭的操作员查询级联 ID)。跳过的行(`ALREADY_IN_TARGET_STATE`)和失败的行不发出事件——匹配单次操作的原则并避免错误信号。无传输 / OpenAPI / DTO 契约变更;`EventType` 枚举未变(重用现有类型)。每次调用的聚合 `AuditLogEntry` 不变;关闭级联的语义不变。
## 文档
- [Cycles 文档](https://runcycles.io) —— 完整文档站点
- [部署完整技术栈](https://runcycles.io/quickstart/deploying-the-full-cycles-stack) —— 包含 Admin Server 设置的部署指南
- [租户管理](https://runcycles.io/how-to/tenant-creation-and-management-in-cycles) —— 租户创建和生命周期
- [预算分配与管理](https://runcycles.io/how-to/budget-allocation-and-management-in-cycles) —— 预算配置
- [API Key 管理](https://runcycles.io/how-to/api-key-management-in-cycles) —— API Key 生命周期
## 许可证
[Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
标签:AI代理, API密钥, API集成, Docker, SaaS, Streamlit, Webhook, 事件监控, 人工智能安全, 力导向图, 可观测性, 合规性, 后端开发, 安全策略, 安全防御评估, 审计, 授权, 提示词模板, 提示词设计, 搜索引擎查询, 权限执行, 治理, 租户生命周期, 管理API, 自定义请求头, 访问控制, 请求拦截, 账本, 运行时控制, 预算管理