feat: sync lijiaoqiao implementation and staging validation artifacts
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
.tools/
|
||||
|
||||
# Local/sensitive env files
|
||||
scripts/supply-gate/.env
|
||||
scripts/supply-gate/.env.*
|
||||
!scripts/supply-gate/.env.example
|
||||
|
||||
# Generated raw artifacts and process files
|
||||
tests/supply/artifacts/
|
||||
reports/gates/*.log
|
||||
reports/gates/*.out.log
|
||||
reports/gates/*.pid
|
||||
|
||||
# Local build output
|
||||
platform-token-runtime/platform-token-runtime
|
||||
@@ -1,7 +1,7 @@
|
||||
# 唯一验收门禁表(Single Source of Truth)
|
||||
|
||||
- 版本:v1.1
|
||||
- 日期:2026-03-24
|
||||
- 版本:v1.2
|
||||
- 日期:2026-03-27
|
||||
- 状态:生效
|
||||
- 用途:统一 S0/S1/S2 的验收阈值、判定逻辑与阻断动作,消除多文档阈值漂移。
|
||||
|
||||
@@ -35,12 +35,19 @@
|
||||
| M-014 | `platform_credential_ingress_coverage_pct` | 使用平台凭证入站请求/总入站请求 | =100% | <100% | 鉴权日志/网关审计 |
|
||||
| M-015 | `direct_supplier_call_by_consumer_events` | 需求方绕过平台直连供应方事件数 | =0 | >0 即 P0 | 出网审计/安全事件中心 |
|
||||
| M-016 | `query_key_external_reject_rate_pct` | 外部 query key 被拒绝数/外部 query key 请求总数 | =100% | <100% | 网关拦截日志 |
|
||||
| M-017 | `dependency_compat_audit_pass_pct` | 依赖版本兼容审计通过项/应审计项 | =100% | <100% | 依赖审计报告 |
|
||||
| M-018 | `stage_quality_gate_pass_pct` | 分阶段质量门禁通过项/应通过项 | =100% | <100% | CI Gate 报告 |
|
||||
| M-019 | `requirement_traceability_coverage_pct` | 已追踪需求数/应追踪需求数 | =100% | <100% | 追踪矩阵 |
|
||||
| M-020 | `escaped_p0_defects_count` | 生产放出后 P0 缺陷数 | =0 | >0 | 生产事故平台 |
|
||||
| M-021 | `token_runtime_readiness_pct` | token 运行态必备能力已实现项/应实现项 | =100% | <100% | TOK 验收报告 |
|
||||
|
||||
口径补充:
|
||||
1. 主路径端点集合固定为 `/v1/chat/completions`、`/v1/messages`、`/v1/responses`、`/v1beta/*`。
|
||||
2. `/responses` 等 alias 入口必须在 Ingress 归一后再统计。
|
||||
3. 国内平台分类来源固定为配置表 `gateway_cn_platforms`,禁止 SQL 硬编码。
|
||||
4. 需求方仅可使用平台签发凭证访问平台入口,禁止获取供应方上游凭证。
|
||||
5. 依赖兼容审计口径固定为“SBOM + 锁文件差异 + 兼容矩阵 + 风险清单”四件套,缺一视为未通过。
|
||||
6. 分阶段质量门禁固定为 G0/G1/G2/G3/G4/G5,禁止跳阶段放行。
|
||||
|
||||
---
|
||||
|
||||
@@ -58,17 +65,19 @@
|
||||
| Gate ID | 场景 | 必达条件 | 不通过动作 | 责任人 |
|
||||
|---|---|---|---|---|
|
||||
| G-S1-1 | 灰度7天上线门禁 | M-001>=99.9%,M-004<=0.1%,30分钟内回滚演练通过 | 不得升至全量;维持灰度并整改 | 平台+SRE |
|
||||
| G-S1-2 | 发布前兼容门禁 | Schema/Behavior/Performance 三重Gate全部通过 | 阻断发布 | 架构+QA |
|
||||
| G-S1-2 | 发布前兼容门禁 | Schema/Behavior/Performance 三重Gate全部通过,且 M-017=100% | 阻断发布 | 架构+QA |
|
||||
| G-S1-3 | 凭证边界门禁 | M-013=0,M-014=100%,M-016=100% | 阻断发布并触发安全复盘 | 安全+平台 |
|
||||
| G-S1-4 | 分阶段质量门禁 | M-018=100%,M-019=100% | 阻断发布并回到失败阶段整改 | 架构+QA+PMO |
|
||||
| G-S1-5 | token 运行态门禁 | M-021=100% | 阻断发布并冻结 SUP 升波 | 架构+安全+平台 |
|
||||
|
||||
### 3.3 S2 阶段门禁(替换核心)
|
||||
|
||||
| Gate ID | 阶段 | 必达条件 | 升波条件 | 阻断条件 | 不通过动作 |
|
||||
|---|---|---|---|---|---|
|
||||
| G-S2-A | 10% | M-001>=99.9%,M-002<=60,M-010>=99%,M-004<=0.1%,M-009>=95%,M-014=100% | 连续2周达标 | 任一红线触发 | 回切 subapi,修复后重试 |
|
||||
| G-S2-B | 30% | M-001>=99.95%,M-003<=100,M-009>=97%,M-014=100% | 连续2周达标 | M-004>0.1% 或 P0事故 | 暂停升波,补救后复核 |
|
||||
| G-S2-C1 | 40%中间检查点 | M-001>=99.95%,M-002<=60,M-003<=100,M-010>=99.5%,M-004<=0.1%,M-009>=97%,M-013=0,M-014=100%,M-015=0 | GO/CONDITIONAL GO | 任一红灯阈值 | 决策会:继续/附条件继续/回滚 |
|
||||
| G-S2-C2 | 60%终验 | M-006>=60%,M-007=100%,M-004<=0.1%,M-005<=0.01%,M-008>=99.9%,M-001>=99.95%,M-013=0,M-014=100%,M-015=0,M-016=100% | 通过后S2完成 | 任一硬门槛不满足 | 延长S2并冻结升波,不降终验目标 |
|
||||
| G-S2-A | 10% | M-001>=99.9%,M-002<=60,M-010>=99%,M-004<=0.1%,M-009>=95%,M-014=100%,M-017=100%,M-018=100%,M-021=100% | 连续2周达标 | 任一红线触发 | 回切 subapi,修复后重试 |
|
||||
| G-S2-B | 30% | M-001>=99.95%,M-003<=100,M-009>=97%,M-014=100%,M-017=100%,M-021=100% | 连续2周达标 | M-004>0.1% 或 P0事故 | 暂停升波,补救后复核 |
|
||||
| G-S2-C1 | 40%中间检查点 | M-001>=99.95%,M-002<=60,M-003<=100,M-010>=99.5%,M-004<=0.1%,M-009>=97%,M-013=0,M-014=100%,M-015=0,M-018=100%,M-019=100%,M-021=100% | GO/CONDITIONAL GO | 任一红灯阈值 | 决策会:继续/附条件继续/回滚 |
|
||||
| G-S2-C2 | 60%终验 | M-006>=60%,M-007=100%,M-004<=0.1%,M-005<=0.01%,M-008>=99.9%,M-001>=99.95%,M-013=0,M-014=100%,M-015=0,M-016=100%,M-017=100%,M-018=100%,M-019=100%,M-020=0,M-021=100% | 通过后S2完成 | 任一硬门槛不满足 | 延长S2并冻结升波,不降终验目标 |
|
||||
|
||||
---
|
||||
|
||||
@@ -84,6 +93,9 @@
|
||||
6. `supplier_credential_exposure_events > 0`。
|
||||
7. `direct_supplier_call_by_consumer_events > 0`。
|
||||
8. `platform_credential_ingress_coverage_pct < 100%` 或 `query_key_external_reject_rate_pct < 100%`。
|
||||
9. `dependency_compat_audit_pass_pct < 100%`。
|
||||
10. `stage_quality_gate_pass_pct < 100%` 或 `requirement_traceability_coverage_pct < 100%`。
|
||||
11. `token_runtime_readiness_pct < 100%`。
|
||||
|
||||
处理动作:
|
||||
1. 立即停止升波。
|
||||
|
||||
23
docs/api_naming_strategy_supply_vs_supplier_v1_2026-03-27.md
Normal file
23
docs/api_naming_strategy_supply_vs_supplier_v1_2026-03-27.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# API 命名策略:`/supply` vs `/supplier`(v1.0)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 决策类型:命名规范与兼容策略
|
||||
- 适用范围:供应侧控制台与平台账务相关 API
|
||||
|
||||
## 1. 决策
|
||||
|
||||
1. 规范主路径统一采用:`/api/v1/supply/*`。
|
||||
2. 历史兼容路径 `/api/v1/supplier/*` 保留为 alias,并标记 `deprecated`。
|
||||
3. 新增接口禁止使用 `/supplier` 前缀。
|
||||
|
||||
## 2. 兼容策略
|
||||
|
||||
1. 别名路径只做兼容,不扩展新字段。
|
||||
2. 响应体增加迁移提示字段(如 `deprecation_notice`)或在文档标注迁移窗口。
|
||||
3. S2 阶段评估 alias 下线时间,提前至少一个版本公告。
|
||||
|
||||
## 3. 验收标准
|
||||
|
||||
1. OpenAPI 同时存在 canonical 路径与 alias 路径声明。
|
||||
2. alias 路径标记 `deprecated: true`。
|
||||
3. 追踪矩阵 `api_alias` 字段可定位所有 alias 使用点。
|
||||
128
docs/database_domain_model_and_governance_v1_2026-03-27.md
Normal file
128
docs/database_domain_model_and_governance_v1_2026-03-27.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# 数据库跨域模型与治理基线(v1.0)
|
||||
|
||||
- 版本:v1.0
|
||||
- 日期:2026-03-27
|
||||
- 状态:生效(数据库设计 SSOT 补丁)
|
||||
- 适用范围:S0-S2 执行与验收
|
||||
- 关联文档:
|
||||
- `docs/llm_gateway_prd_v1_2026-03-25.md`
|
||||
- `docs/supply_technical_design_enhanced_v1_2026-03-25.md`
|
||||
- `docs/technical_architecture_optimized_v2_2026-03-18.md`
|
||||
- `sql/postgresql/supply_schema_v1.sql`
|
||||
- `sql/postgresql/supply_schema_v1_patch_2026-03-27.sql`
|
||||
- `sql/postgresql/platform_core_schema_v1.sql`
|
||||
|
||||
---
|
||||
|
||||
## 1. 本次补齐的缺口
|
||||
|
||||
1. 仅有 `supply_*` 表,缺少 PRD P0/P1 的核心域(租户/项目/鉴权 key/账务总账/审计事件)。
|
||||
2. 供应域缺少统一加密元数据字段,无法审计算法、KMS Key 版本与轮换状态。
|
||||
3. 缺少统一单位字段(quota/cost/amount unit),跨域统计口径不稳定。
|
||||
4. 审计字段不完整(request_id、trace_id、IP、operator、version)。
|
||||
5. 索引以单列为主,未覆盖高频组合查询(租户+状态+时间)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 最小跨域表模型(按 PRD P0/P1)
|
||||
|
||||
| 域 | 表 | 说明 |
|
||||
|---|---|---|
|
||||
| Core | `core_tenants` | 组织/租户主实体 |
|
||||
| Core | `core_projects` | 项目/成本归因单元 |
|
||||
| IAM | `iam_users` | 用户身份与角色 |
|
||||
| Auth | `auth_platform_api_keys` | 平台签发凭证(仅 hash,不存明文) |
|
||||
| Billing | `billing_accounts` | 预算账户与余额 |
|
||||
| Billing | `billing_ledger_entries` | 借贷分录与请求级对账 |
|
||||
| Routing | `routing_policies` | 策略版本、优先级、生效窗口 |
|
||||
| Security | `security_kms_key_registry` | KMS Key 与加密算法版本登记 |
|
||||
| Audit | `audit_events` | 全域审计事件(配置/账务/安全) |
|
||||
|
||||
DDL:`sql/postgresql/platform_core_schema_v1.sql`
|
||||
|
||||
---
|
||||
|
||||
## 3. 供应域字段补齐(在 v1 基础上增量)
|
||||
|
||||
### 3.1 加密字段(必须)
|
||||
|
||||
1. `*_cipher_algo`:默认 `AES-256-GCM`
|
||||
2. `*_kms_key_alias`:KMS key alias(非 key 明文)
|
||||
3. `*_key_version`:key 版本号
|
||||
4. `*_fingerprint`:凭证摘要(不可逆)
|
||||
5. `last_rotation_at`:上次轮换时间
|
||||
|
||||
### 3.2 单位与币种字段(必须)
|
||||
|
||||
1. `quota_unit`:`token/request/credit`
|
||||
2. `price_unit`:`per_1m_tokens` 等
|
||||
3. `amount_unit`:`minor`(分/厘)
|
||||
4. `currency_code`:ISO 4217 三位码
|
||||
|
||||
### 3.3 审计与并发字段(必须)
|
||||
|
||||
1. `request_id`
|
||||
2. `idempotency_key`
|
||||
3. `audit_trace_id`
|
||||
4. `created_ip` / `updated_ip`
|
||||
5. `version`(乐观锁)
|
||||
|
||||
DDL:`sql/postgresql/supply_schema_v1_patch_2026-03-27.sql`
|
||||
|
||||
---
|
||||
|
||||
## 4. 索引策略(高频查询优先)
|
||||
|
||||
### 4.1 组合索引
|
||||
|
||||
1. `supply_accounts(user_id, status, updated_at desc)`
|
||||
2. `supply_packages(user_id, status, updated_at desc)`
|
||||
3. `supply_orders(buyer_user_id, status, created_at desc)`
|
||||
4. `supply_settlements(user_id, status, updated_at desc)`
|
||||
5. `billing_ledger_entries(billing_account_id, occurred_at desc)`
|
||||
|
||||
### 4.2 部分索引
|
||||
|
||||
1. `supply_packages` 的 active 查询(仅 `status=active`)
|
||||
2. `supply_settlements` 的处理中唯一约束(仅 `status=processing`)
|
||||
|
||||
### 4.3 可观测索引
|
||||
|
||||
1. `request_id`
|
||||
2. `trace_id`
|
||||
3. `audit_trace_id`
|
||||
|
||||
说明:所有关键事件必须具备 request 级反查路径,满足“从告警到原始账务分录”单跳可达。
|
||||
|
||||
---
|
||||
|
||||
## 5. 迁移顺序与回滚策略
|
||||
|
||||
1. Phase-A:执行 `platform_core_schema_v1.sql`(新增表,无破坏性)。
|
||||
2. Phase-B:执行 `supply_schema_v1_patch_2026-03-27.sql`(增列+增索引)。
|
||||
3. Phase-C:灰度写入新字段(双写,不读取)。
|
||||
4. Phase-D:回填历史数据(按日批,带校验)。
|
||||
5. Phase-E:切换读路径到新字段并开启质量门禁。
|
||||
|
||||
回滚原则:
|
||||
1. 新字段只增不删,读路径可切回旧字段。
|
||||
2. 新索引可独立回退,不影响主流程事务。
|
||||
3. 任一阶段失败立即冻结下一阶段,不跨阶段带病推进。
|
||||
|
||||
---
|
||||
|
||||
## 6. 质量验收清单(DB)
|
||||
|
||||
1. 结构验收:新增表/列/索引全部存在,且命名符合规范。
|
||||
2. 安全验收:无明文凭证列,hash/指纹字段可用。
|
||||
3. 一致性验收:账务分录借贷平衡,提现处理中单一约束生效。
|
||||
4. 审计验收:关键写接口 100% 带 `request_id + trace_id`。
|
||||
5. 性能验收:高频查询 P95 无劣化(对比 patch 前后)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 约束声明
|
||||
|
||||
1. 本文与两个 SQL 文件共同构成数据库实施 SSOT。
|
||||
2. 任何新增业务功能必须先选择所属域,再定义表/字段/索引,不允许“先代码后补库”。
|
||||
3. 未通过本清单第 6 章,禁止进入发布门禁 `SUP-008` 与全局 `GO` 评审。
|
||||
@@ -0,0 +1,93 @@
|
||||
# 依赖版本兼容性审计基线(v1.0)
|
||||
|
||||
- 版本:v1.0
|
||||
- 日期:2026-03-27
|
||||
- 状态:生效(发布前强制 Gate)
|
||||
- 目标:把“依赖可用”升级为“依赖可审计、可回滚、可阻断”
|
||||
- 关联文档:
|
||||
- `docs/technical_architecture_optimized_v2_2026-03-18.md`
|
||||
- `docs/acceptance_gate_single_source_v1_2026-03-18.md`
|
||||
- `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 审计对象与冻结策略
|
||||
|
||||
| 层 | 对象 | 冻结规则 |
|
||||
|---|---|---|
|
||||
| Runtime | Go / Node / JDK / Python | 仅允许 LTS 或已验证小版本 |
|
||||
| Data | PostgreSQL / Redis | 生产固定主版本,升级必须灰度 |
|
||||
| 服务依赖 | subapi / provider SDK | 固定精确版本(`X.Y.Z`) |
|
||||
| 第三方库 | go mod / npm / maven | 锁文件变更必须触发兼容测试 |
|
||||
| OS 镜像 | 基础镜像 digest | 必须可追溯到 SBOM |
|
||||
|
||||
---
|
||||
|
||||
## 2. 必交付证据
|
||||
|
||||
每次发布候选版本必须提供:
|
||||
|
||||
1. `SBOM`:`reports/dependency/sbom_<date>.spdx.json`
|
||||
2. `锁文件差异`:`reports/dependency/lockfile_diff_<date>.md`
|
||||
3. `兼容矩阵`:`reports/dependency/compat_matrix_<date>.md`
|
||||
4. `风险清单`:`reports/dependency/risk_register_<date>.md`
|
||||
|
||||
无上述四项,发布门禁直接阻断。
|
||||
|
||||
---
|
||||
|
||||
## 3. 兼容性审计流程(分阶段)
|
||||
|
||||
### 3.1 Pre-Merge(开发合并前)
|
||||
|
||||
1. 检查 `go.mod/go.sum`、`package-lock.json/pnpm-lock.yaml`、`pom.xml` 变化。
|
||||
2. 依赖变更自动分类:Patch/Minor/Major。
|
||||
3. Major 变更必须附“兼容影响评估 + 回滚预案”。
|
||||
|
||||
### 3.2 Nightly(每日)
|
||||
|
||||
1. 运行依赖漏洞扫描(CVE/SCA)。
|
||||
2. 运行契约回归(Schema/Behavior)。
|
||||
3. 生成依赖健康趋势(新增高危漏洞数)。
|
||||
|
||||
### 3.3 Pre-Release(发布前)
|
||||
|
||||
1. 运行完整兼容回归(兼容三重 Gate + SUP Gate)。
|
||||
2. 校验运行时与数据层版本匹配矩阵。
|
||||
3. 通过后冻结候选构建包与镜像 digest。
|
||||
|
||||
### 3.4 Post-Release(发布后 24h)
|
||||
|
||||
1. 监控新增依赖告警、崩溃、性能回退。
|
||||
2. 若触发 P0/P1 依赖事故,执行自动回滚到上一稳定版本。
|
||||
|
||||
---
|
||||
|
||||
## 4. 阻断规则(必须)
|
||||
|
||||
1. `dependency_compat_audit_pass_pct < 100%`:阻断发布。
|
||||
2. 新增 Critical CVE 且无缓解:阻断发布。
|
||||
3. Major 依赖变更无回滚演练记录:阻断发布。
|
||||
4. subapi/provider SDK 精确版本未锁定:阻断发布。
|
||||
5. 依赖清单与运行镜像不一致:阻断发布。
|
||||
|
||||
---
|
||||
|
||||
## 5. 推荐版本兼容矩阵(首版)
|
||||
|
||||
| 组件 | 基线版本 | 兼容范围 | 备注 |
|
||||
|---|---|---|---|
|
||||
| Go | 1.21.x | 1.21.x | 不跨主版本 |
|
||||
| PostgreSQL | 15.x | 15.x | SQL 与索引以 PG15 语法为准 |
|
||||
| Redis | 7.x | 7.x | 限流与缓存行为基于 Redis7 验证 |
|
||||
| subapi | 精确 `X.Y.Z` | 同 patch | Minor 升级需完整回归 |
|
||||
| Node(前端) | 20.x LTS | 20.x | 锁文件必须纳入审计 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 与发布门禁对齐
|
||||
|
||||
1. 依赖兼容审计结果接入 `acceptance_gate_single_source` 指标 `M-017`。
|
||||
2. 分阶段测试质量接入指标 `M-018`。
|
||||
3. 任一未达标,不得进入 `GO` 结论。
|
||||
|
||||
@@ -193,18 +193,23 @@
|
||||
1. 供应商接入稳定性
|
||||
2. 计费与账单口径统一
|
||||
3. 告警渠道与通知系统稳定
|
||||
4. 数据库跨域模型完整(Core/IAM/Auth/Billing/Supply/Audit)
|
||||
5. 依赖版本兼容审计机制可执行(SBOM + 锁文件差异 + 兼容矩阵 + 风险清单)
|
||||
|
||||
## 9.2 主要风险
|
||||
|
||||
1. 功能边界过宽导致首发延期
|
||||
2. 缺少真实客户数据导致价值表达不足
|
||||
3. 定价方案与客户价值感知不匹配
|
||||
4. 依赖版本漂移导致线上兼容故障
|
||||
5. 阶段门禁不完整导致实现偏离需求主线
|
||||
|
||||
## 9.3 风险缓解
|
||||
|
||||
1. 坚持 P0 边界,P1/P2 延后
|
||||
2. 以设计合作伙伴反馈驱动迭代
|
||||
3. 发布前完成小规模定价验证访谈
|
||||
4. 将依赖兼容审计和分阶段质量门禁纳入发布前阻断条件
|
||||
|
||||
## 10. 已冻结决策(v1.0 生效)
|
||||
|
||||
|
||||
126
docs/plans/2026-03-30-superpowers-execution-tasklist-v2.md
Normal file
126
docs/plans/2026-03-30-superpowers-execution-tasklist-v2.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# 规划设计闭环执行任务清单(Superpowers v2)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 基线来源:`docs/plans/2026-03-25-superpowers-execution-tasklist-v1.md`
|
||||
- 目标:基于最新真实证据重排执行优先级,持续推进到 staging 可复核、可签署。
|
||||
|
||||
---
|
||||
|
||||
## 1. 实际状态复盘(以证据为准)
|
||||
|
||||
### 1.1 已闭环工作流(开发阶段)
|
||||
|
||||
1. `WG-A/WG-B/WG-C` 已完成:需求冻结、OpenAPI 契约对齐、追踪矩阵一致化。
|
||||
2. `WG-F/WG-G` 已完成:全局 P0 映射、命名策略、跨文档一致性与最终决议草稿链路。
|
||||
3. TOK 链路已完成开发闭环(`TOK-002 ~ TOK-007`):包含 runtime、门禁汇总、复审与候选稿生成。
|
||||
|
||||
关键证据:
|
||||
1. `reports/superpowers_execution_progress_2026-03-27.md`
|
||||
2. `reports/alignment_validation_checkpoint_12_2026-03-30.md` ~ `reports/alignment_validation_checkpoint_27_2026-03-30.md`
|
||||
3. `reports/gates/superpowers_stage_validation_2026-03-30_212426.md`
|
||||
|
||||
### 1.2 未闭环工作流(真实环境)
|
||||
|
||||
1. `WG-D/WG-E` 仍未完成真实 staging 证据闭环,当前仅有 local/mock 与 dry-run 证据。
|
||||
2. 最终签署决议当前为 `NO-GO`,核心阻塞集中在 `F-01/F-02/F-04`(P0)与 `F-03`(P1)。
|
||||
|
||||
关键证据:
|
||||
1. `review/final_decision_2026-03-31.md`
|
||||
2. `reports/supply_gate_review_2026-03-31.md`
|
||||
3. `reports/token_runtime_implementation_gap_review_2026-03-30.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 状态矩阵(v2)
|
||||
|
||||
| 工作流 | 状态 | 说明 | 下一动作 |
|
||||
|---|---|---|---|
|
||||
| WG-A 需求冻结 | DONE | 已完成冻结与决议映射 | 仅维护 |
|
||||
| WG-B 契约对齐 | DONE | OpenAPI 与幂等头已落地 | 仅维护 |
|
||||
| WG-C 测试矩阵 | DONE | 路径一致化与规则文档已落地 | 仅维护 |
|
||||
| WG-D 真实联调 | BLOCKED(外部依赖) | 缺真实 staging 地址与有效短期 token | 优先解锁 F-01/F-02/F-04 |
|
||||
| WG-E 报告签署 | BLOCKED(依赖 WG-D) | 缺真实证据,无法转 GO | 与 WG-D 同步推进 |
|
||||
| WG-F 一致性收尾 | DONE | 命名策略与映射补齐完成 | 仅维护 |
|
||||
| WG-G 全局校验 | DONE(开发口径) | 校验链路可执行,决议一致性脚本已在跑 | 补真实口径复核 |
|
||||
| TOK 运行态链路 | DONE(开发口径) | M-021 开发阶段 100% | 需 staging 实证回填 |
|
||||
|
||||
---
|
||||
|
||||
## 3. P0/P1 阻塞项(从最终决议回填)
|
||||
|
||||
| 编号 | 等级 | 阻塞描述 | Owner | 截止日期 | 退出条件 |
|
||||
|---|---|---|---|---|---|
|
||||
| F-01 | P0 | staging DNS 与 `API_BASE_URL` 可达性修复,重跑 SUP-004~007 | PLAT + QA | 2026-04-01 | `staging_precheck_and_run.sh` 在真实环境 PASS |
|
||||
| F-02 | P0 | 补齐 M-013~M-016 staging 实测值 | SEC + QA | 2026-04-01 | `sec_sup_boundary_report` 回填真实 PASS |
|
||||
| F-04 | P0 | token runtime staging 联调取证 | ARCH + PLAT + SEC | 2026-04-03 | `M-021` 与边界指标 staging 证据齐全 |
|
||||
| F-03 | P1 | M-017/M-018/M-019 连续 7 天趋势证据 | PLAT + PMO | 2026-04-05 | 趋势报告满足 7 天口径 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 批次执行计划(从 2026-03-30 起)
|
||||
|
||||
### Batch-MON-01(当前批次,先做“可持续执行”能力)
|
||||
|
||||
1. `MON-001`:新增 Minimax 7 日趋势脚本(监控链路补齐)。
|
||||
2. `MON-002`:将 Minimax 日快照接入 `superpowers_release_pipeline.sh`(可选、默认关闭、非阻断)。
|
||||
3. `MON-003`:更新命令手册,补齐执行与断言说明。
|
||||
4. `MON-004`:产出对齐验证报告(Checkpoint-28)。
|
||||
|
||||
执行结果(2026-03-30):
|
||||
|
||||
| 任务 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| MON-001 | DONE | `scripts/ci/minimax_upstream_trend_report.sh` + `reports/gates/minimax_upstream_trend_7d_2026-03-30.md` |
|
||||
| MON-002 | DONE | `scripts/ci/superpowers_release_pipeline.sh` + `reports/gates/superpowers_release_pipeline_2026-03-30_235224.md` |
|
||||
| MON-003 | DONE | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
|
||||
| MON-004 | DONE | `reports/alignment_validation_checkpoint_28_2026-03-30.md` |
|
||||
|
||||
### Batch-STG-01(下一批次,真实环境解锁)
|
||||
|
||||
1. `STG-001`:确认真实 staging 网关地址并更新 `scripts/supply-gate/.env`。
|
||||
2. `STG-002`:注入有效 owner/viewer/admin 短期 token(禁用占位值)。
|
||||
3. `STG-003`:执行 `scripts/ci/staging_release_pipeline.sh`(真实环境,不允许 local/mock)。
|
||||
4. `STG-004`:回填 `F-01/F-02/F-04` 证据到最终决议与评审报告。
|
||||
|
||||
当前门禁检查(2026-03-30):
|
||||
1. `scripts/supply-gate/.env` 中 `API_BASE_URL` 仍处于阻塞态(非真实 staging 可发布地址)。
|
||||
2. 因 `STG-001` 未完成,`STG-003` 当前不得触发真实放行判定。
|
||||
|
||||
本机开发测试续跑结果(2026-03-31):
|
||||
1. `local/mock` 口径 `staging_release_pipeline` 已复跑通过:`reports/gates/staging_release_pipeline_2026-03-31_100116.md`。
|
||||
2. STG 本地续跑中识别并修复 `PHASE-10` 阻塞(M-021 smoke 端口冲突与控制流提前退出)。
|
||||
3. 修复后 `superpowers_release_pipeline` 与 `tok007` 复审链路恢复,结论维持 `CONDITIONAL_GO`。
|
||||
4. `STG-001/STG-002`(真实 staging 地址与真 token)仍未完成,真实放行证据仍阻塞。
|
||||
|
||||
本机端口基线固化结果(2026-03-31):
|
||||
1. 已清理蚊子残留进程与冲突端口占用,详见 `reports/gates/local_dev_port_baseline_2026-03-31.md`。
|
||||
2. 清理后再次复测 `staging_release_pipeline`:`reports/gates/staging_release_pipeline_2026-03-31_100942.md`(PASS)。
|
||||
3. 对齐验证补充:`reports/alignment_validation_checkpoint_30_2026-03-31.md`。
|
||||
|
||||
真实 STG 前置自动化补齐(2026-03-31):
|
||||
1. 已新增本地 `.env.staging-real` 一键生成脚本:`scripts/ci/generate_local_staging_env.sh`。
|
||||
2. 已新增真实 STG 就绪度检查脚本:`scripts/ci/staging_real_readiness_check.sh`。
|
||||
3. 当前 `.env.staging-real` 就绪检查结论为 `BLOCKED`:`reports/gates/staging_real_readiness_2026-03-31_110213.md`。
|
||||
4. 阻塞原因聚焦在 `STG-RDY-004/008`(API_BASE_URL 仍为本地地址且无真实外网可达性)。
|
||||
|
||||
完整开发测试续跑结果(2026-03-31 12:31):
|
||||
1. 已重新生成 `.env.staging-real` 且三类 token 均为非占位值:`reports/gates/local_staging_env_generation_2026-03-31_123102.md`。
|
||||
2. `local/mock` 口径 `staging_release_pipeline` 再次通过:`reports/gates/staging_release_pipeline_2026-03-31_123148.md`。
|
||||
3. `superpowers_release_pipeline` 与 `tok007` 复审链路再次通过,机判维持 `CONDITIONAL_GO`:`reports/gates/superpowers_release_pipeline_2026-03-31_123150.md`、`review/outputs/tok007_release_recheck_2026-03-31_123153.md`。
|
||||
4. 真实 STG 就绪度检查仍为 `BLOCKED`:`reports/gates/staging_real_readiness_2026-03-31_123159.md`(`STG-RDY-004/008` 未关闭)。
|
||||
5. Minimax 上游 smoke 继续保持 `PASS`:`reports/gates/minimax_upstream_smoke_2026-03-31_123210.md`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 执行约束
|
||||
|
||||
1. `local/mock` 结果仅可作为开发演练证据,不可替代 staging 放行证据。
|
||||
2. 任何 `P0` 项未关闭,最终结论不得上调为 `GO`。
|
||||
3. 所有阶段结论以脚本返回码 + 报告产物双重校验为准。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 v1 的关系
|
||||
|
||||
1. `v1` 保留原子任务定义(A~G)。
|
||||
2. `v2` 作为执行态总控视图,负责状态、批次与阻塞跟踪。
|
||||
@@ -0,0 +1,450 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Platform Token Runtime API Contract Draft
|
||||
version: 1.0.0-draft
|
||||
description: |
|
||||
TOK-002/TOK-003/TOK-004 对应的 token 运行态接口草案。
|
||||
关键边界:
|
||||
1) 仅平台内部可调用 issue/revoke。
|
||||
2) 不暴露上游供应方凭证信息。
|
||||
servers:
|
||||
- url: https://api.example.com
|
||||
description: Production
|
||||
security:
|
||||
- BearerAuth: []
|
||||
tags:
|
||||
- name: PlatformToken
|
||||
paths:
|
||||
/api/v1/platform/tokens/issue:
|
||||
post:
|
||||
tags: [PlatformToken]
|
||||
summary: 签发平台短期 token
|
||||
operationId: issuePlatformToken
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKeyHeader'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueTokenRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: 签发成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueTokenResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
/api/v1/platform/tokens/{tokenId}/refresh:
|
||||
post:
|
||||
tags: [PlatformToken]
|
||||
summary: 刷新 token 过期时间
|
||||
operationId: refreshPlatformToken
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TokenIdParam'
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKeyHeader'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RefreshTokenRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 刷新成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RefreshTokenResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
/api/v1/platform/tokens/{tokenId}/revoke:
|
||||
post:
|
||||
tags: [PlatformToken]
|
||||
summary: 吊销 token
|
||||
operationId: revokePlatformToken
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/TokenIdParam'
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKeyHeader'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RevokeTokenRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 吊销成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RevokeTokenResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
/api/v1/platform/tokens/introspect:
|
||||
post:
|
||||
tags: [PlatformToken]
|
||||
summary: token 校验与解析
|
||||
operationId: introspectPlatformToken
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntrospectTokenRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 校验成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntrospectTokenResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'422':
|
||||
$ref: '#/components/responses/BusinessError'
|
||||
/api/v1/platform/tokens/audit-events:
|
||||
get:
|
||||
tags: [PlatformToken]
|
||||
summary: 查询 token 审计事件
|
||||
operationId: listPlatformTokenAuditEvents
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- name: request_id
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 128
|
||||
- name: token_id
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 128
|
||||
- name: subject_id
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 128
|
||||
- name: event_name
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 128
|
||||
- name: result_code
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 64
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 500
|
||||
default: 100
|
||||
responses:
|
||||
'200':
|
||||
description: 查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuditEventsResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'501':
|
||||
$ref: '#/components/responses/BusinessError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
parameters:
|
||||
XRequestIdHeader:
|
||||
name: X-Request-Id
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 8
|
||||
maxLength: 128
|
||||
IdempotencyKeyHeader:
|
||||
name: Idempotency-Key
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 8
|
||||
maxLength: 128
|
||||
TokenIdParam:
|
||||
name: tokenId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 8
|
||||
maxLength: 128
|
||||
responses:
|
||||
BadRequest:
|
||||
description: 参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
Unauthorized:
|
||||
description: 认证失败
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
Forbidden:
|
||||
description: 权限不足
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
NotFound:
|
||||
description: 资源不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
Conflict:
|
||||
description: 状态冲突
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
BusinessError:
|
||||
description: 业务校验失败
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
schemas:
|
||||
IssueTokenRequest:
|
||||
type: object
|
||||
required: [subject_id, role, ttl_seconds, scope]
|
||||
properties:
|
||||
subject_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
role:
|
||||
type: string
|
||||
enum: [owner, viewer, admin]
|
||||
ttl_seconds:
|
||||
type: integer
|
||||
minimum: 60
|
||||
maximum: 259200
|
||||
scope:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
IssueTokenResponse:
|
||||
type: object
|
||||
required: [request_id, data]
|
||||
properties:
|
||||
request_id:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
required: [token_id, access_token, issued_at, expires_at, status]
|
||||
properties:
|
||||
token_id:
|
||||
type: string
|
||||
access_token:
|
||||
type: string
|
||||
writeOnly: true
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
status:
|
||||
type: string
|
||||
enum: [active]
|
||||
RefreshTokenRequest:
|
||||
type: object
|
||||
required: [ttl_seconds]
|
||||
properties:
|
||||
ttl_seconds:
|
||||
type: integer
|
||||
minimum: 60
|
||||
maximum: 259200
|
||||
RefreshTokenResponse:
|
||||
type: object
|
||||
required: [request_id, data]
|
||||
properties:
|
||||
request_id:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
required: [token_id, previous_expires_at, expires_at, status]
|
||||
properties:
|
||||
token_id:
|
||||
type: string
|
||||
previous_expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
status:
|
||||
type: string
|
||||
enum: [active]
|
||||
RevokeTokenRequest:
|
||||
type: object
|
||||
required: [reason]
|
||||
properties:
|
||||
reason:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 256
|
||||
RevokeTokenResponse:
|
||||
type: object
|
||||
required: [request_id, data]
|
||||
properties:
|
||||
request_id:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
required: [token_id, status, revoked_at]
|
||||
properties:
|
||||
token_id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [revoked]
|
||||
revoked_at:
|
||||
type: string
|
||||
format: date-time
|
||||
IntrospectTokenRequest:
|
||||
type: object
|
||||
required: [token]
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
minLength: 8
|
||||
IntrospectTokenResponse:
|
||||
type: object
|
||||
required: [request_id, data]
|
||||
properties:
|
||||
request_id:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
required: [token_id, subject_id, role, status, scope, issued_at, expires_at]
|
||||
properties:
|
||||
token_id:
|
||||
type: string
|
||||
subject_id:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
enum: [owner, viewer, admin]
|
||||
status:
|
||||
type: string
|
||||
enum: [active, revoked, expired]
|
||||
scope:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
AuditEventsResponse:
|
||||
type: object
|
||||
required: [request_id, data]
|
||||
properties:
|
||||
request_id:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
required: [total, items]
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
minimum: 0
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AuditEventItem'
|
||||
AuditEventItem:
|
||||
type: object
|
||||
required: [event_id, event_name, request_id, route, result_code, created_at]
|
||||
properties:
|
||||
event_id:
|
||||
type: string
|
||||
event_name:
|
||||
type: string
|
||||
request_id:
|
||||
type: string
|
||||
token_id:
|
||||
type: string
|
||||
subject_id:
|
||||
type: string
|
||||
route:
|
||||
type: string
|
||||
result_code:
|
||||
type: string
|
||||
client_ip:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required: [request_id, error]
|
||||
properties:
|
||||
request_id:
|
||||
type: string
|
||||
error:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
details:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
@@ -0,0 +1,20 @@
|
||||
# 全局 P0 到供应侧/平台侧映射表(v1.0)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 来源:`docs/llm_gateway_prd_v1_2026-03-25.md`(第 11 节及第 4.1 节)
|
||||
- 目标:避免“供应侧完成=全局P0完成”的误判。
|
||||
|
||||
| 全局 P0 ID | 全局能力 | 供应侧入口(按钮/页面) | 平台侧入口(控制面/API) | 当前状态 |
|
||||
|---|---|---|---|---|
|
||||
| PRD-P0-01 | 供应账号挂载与验证 | SUP-PAGE-001:`BTN-ACC-001~006` | 平台鉴权与审计服务 | 已覆盖 |
|
||||
| PRD-P0-02 | 套餐发布与上下架 | SUP-PAGE-002:`BTN-PKG-001~006` | 平台路由与策略中心 | 已覆盖 |
|
||||
| PRD-P0-03 | 收益结算与提现 | SUP-PAGE-003:`BTN-SET-001~005` | 账务与结算服务 | 已覆盖 |
|
||||
| PRD-P0-04 | 凭证边界强制约束 | 三页面全局约束与 SEC-SUP 用例 | 网关鉴权、出网审计、告警中心 | 已覆盖(待 staging 实证) |
|
||||
| PRD-P0-05 | 预算与配额 | 供应侧仅展示配额结果,不做全局预算配置 | 平台预算中心(组织/项目级阈值) | 待平台侧实现证据 |
|
||||
| PRD-P0-06 | 告警与通知 | 供应侧展示告警状态与处理入口 | 平台告警中心(规则、路由、升级) | 待平台侧实现证据 |
|
||||
| PRD-P0-07 | 账单导出 | 供应侧提供结算单导出(供应方视角) | 平台统一账单导出(组织级) | 部分覆盖(需双侧对齐) |
|
||||
|
||||
## 补充说明(F-002)
|
||||
|
||||
1. “预算/告警/统一账单导出”属于平台控制面能力,供应侧仅保留入口与只读结果,不承担策略主配置职责。
|
||||
2. 供应侧“结算单导出”与平台“组织级账单导出”不是同一能力,必须双轨验收。
|
||||
@@ -0,0 +1,17 @@
|
||||
# 供应侧按钮 PRD 待拍板项决议映射(v1.0)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 来源文档:`docs/supply_button_level_prd_v1_2026-03-25.md`
|
||||
- 目标:将“待拍板项”转为“可执行决议”,用于实施与验收对齐。
|
||||
|
||||
| 映射ID | 待拍板项 | 决议 | 责任角色 | 交付动作 | 验收证据 |
|
||||
|---|---|---|---|---|---|
|
||||
| MAP-001 | `POST /api/v1/supply/*` 命名是否冻结 | 冻结 `/api/v1/supply/*` 为主路径;`/api/v1/supplier/billing` 保留兼容 | ARCH + PLAT | 在 OpenAPI 记录主路径/兼容路径声明 | OpenAPI 变更记录 |
|
||||
| MAP-002 | 提现金额阈值与冷却期 | 单笔 `<=50000 CNY`;单日 `<=200000 CNY`;冷却期 `15 min` | FIN + ARCH + QA | 在结算规则与测试断言同步阈值 | 测试用例与执行报告 |
|
||||
| MAP-003 | 下架 vs 暂停财务口径 | `pause` 阻断新购、存量订单继续;`unlist` 阻断新购并触发 T+1 核对 | FIN + 产品 + QA | 更新状态机语义与审计事件说明 | PRD 条目 + 审计字段检查 |
|
||||
| MAP-004 | 批量导入账号是否进入 S0/S1 | 不进入 S0/S1;作为 S2 评审项,白名单灰度 | 产品 + ARCH | 从当前门禁移除,纳入路线图 | 路线图条目与门禁范围声明 |
|
||||
|
||||
## 变更控制
|
||||
|
||||
1. 本映射作为 `A-003` 产物,任何变更需记录变更单号与审批人。
|
||||
2. 未经审批,不得恢复“待拍板项”状态。
|
||||
@@ -1,7 +1,7 @@
|
||||
# Subapi 集成风险控制实施任务单(两周执行版,v1.4)
|
||||
# Subapi 集成风险控制实施任务单(两周执行版,v1.5)
|
||||
|
||||
- 版本:v1.4
|
||||
- 日期:2026-03-25
|
||||
- 版本:v1.5
|
||||
- 日期:2026-03-27
|
||||
- 执行窗口:2026-03-18 至 2026-03-31(两周)
|
||||
- 关联文档:
|
||||
- `subapi_integration_compat_security_reliability_design_v1_2026-03-17.md`
|
||||
@@ -11,13 +11,15 @@
|
||||
- `router_core_s2_acceptance_test_cases_v1_2026-03-17.md`
|
||||
- `acceptance_gate_single_source_v1_2026-03-18.md`(v1.1, 2026-03-24)
|
||||
- `llm_gateway_subapi_evolution_plan_v4_2_2026-03-24.md`
|
||||
- `supply_button_level_prd_v1_2026-03-25.md`
|
||||
- `supply_button_level_prd_v1_2026-03-25.md`(v1.1 冻结,2026-03-27)
|
||||
- `supply_api_contract_openapi_draft_v1_2026-03-25.yaml`
|
||||
- `supply_ui_test_cases_executable_v1_2026-03-25.md`
|
||||
- `supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
- `supply_technical_design_enhanced_v1_2026-03-25.md`
|
||||
- `supply_test_plan_enhanced_v1_2026-03-25.md`
|
||||
- `supply_uiux_design_spec_v1_2026-03-25.md`
|
||||
- `database_domain_model_and_governance_v1_2026-03-27.md`
|
||||
- `dependency_compatibility_audit_baseline_v1_2026-03-27.md`
|
||||
- `tests/supply/ui_design_qa_cases_v1_2026-03-25.md`
|
||||
- `reports/supply_gate_preflight_2026-03-25.md`
|
||||
- `review/multi_expert_planning_review_v1_2026-03-25.md`
|
||||
@@ -31,6 +33,7 @@
|
||||
5. 建立“凭证边界”硬门禁:需求方仅用平台凭证,供应方上游凭证零外发。
|
||||
6. 建立供应侧发布门禁链路(SUP):账号挂载 -> 套餐发布 -> 结算提现全链路可验收。
|
||||
7. 建立四专家整改发布链路(XR):技术/测试/UIUX/业主条款与门禁统一闭环。
|
||||
8. 建立 token 运行态交付链路(TOK):从实现、部署到门禁验收可追踪闭环。
|
||||
|
||||
## 2. 责任角色映射(实名RACI)
|
||||
|
||||
@@ -138,7 +141,7 @@
|
||||
|
||||
| 任务ID | 任务 | Owner | 截止日期 | 依赖 | 验收标准 | 证据产物 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| SUP-001 | 供应侧按钮级 PRD 冻结(3 页面) | `产品` + `ARCH` | 2026-03-26 | 无 | 页面字段、按钮、状态机、错误码冻结 | `docs/supply_button_level_prd_v1_2026-03-25.md` |
|
||||
| SUP-001 | 供应侧按钮级 PRD 冻结(3 页面) | `产品` + `ARCH` | 2026-03-26 | 无 | 页面字段、按钮、状态机、错误码冻结 | `docs/supply_button_level_prd_v1_2026-03-25.md`(v1.1 冻结) |
|
||||
| SUP-002 | 供应侧 OpenAPI 契约冻结(3 页面) | `PLAT` + `ARCH` | 2026-03-26 | SUP-001 | 请求/响应字段、枚举、错误码冻结 | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml` |
|
||||
| SUP-003 | UI-SUP 可执行用例评审通过 | `QA` + `产品` | 2026-03-27 | SUP-001, SUP-002 | `UI-SUP-*` + `UI-DESIGN-QA-*` 全量可执行,覆盖按钮/状态/权限/可访问性 | `docs/supply_ui_test_cases_executable_v1_2026-03-25.md` + `tests/supply/ui_design_qa_cases_v1_2026-03-25.md` |
|
||||
| SUP-004 | 账号挂载链路联调(验证/创建/激活/暂停) | `PLAT` + `QA` | 2026-03-28 | SUP-002, SUP-003 | `UI-SUP-ACC-001~006` 通过率 100% | `scripts/supply-gate/sup004_accounts.sh` + `tests/supply/ui_sup_acc_report_2026-03-28.md` |
|
||||
@@ -152,11 +155,34 @@
|
||||
| 任务ID | 任务 | Owner | 截止日期 | 依赖 | 验收标准 | 证据产物 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| XR-001 | 供应侧技术设计增强落地(幂等/并发/不变量/事务) | `ARCH` + `PLAT` | 2026-03-26 | SUP-002 | 关键写路径均具备双键幂等和冲突语义 | `docs/supply_technical_design_enhanced_v1_2026-03-25.md` |
|
||||
| XR-002 | 供应侧测试方案增强落地(追踪矩阵+并发重放) | `QA` + `ARCH` | 2026-03-27 | XR-001 | Requirement->API->Test->Metric->Gate 全量可追踪 | `docs/supply_test_plan_enhanced_v1_2026-03-25.md` + `reports/supply_traceability_matrix_2026-03-25.csv` + `reports/supply_flaky_budget_2026-03-25.md` |
|
||||
| XR-002 | 供应侧测试方案增强落地(追踪矩阵+并发重放) | `QA` + `ARCH` | 2026-03-27 | XR-001 | Requirement->API->Test->Metric->Gate 全量可追踪,且路径一致性检查通过 | `docs/supply_test_plan_enhanced_v1_2026-03-25.md` + `reports/supply_traceability_matrix_2026-03-25.csv` + `docs/supply_traceability_matrix_generation_rules_v1_2026-03-27.md` + `reports/supply_flaky_budget_2026-03-25.md` |
|
||||
| XR-003 | 供应侧 UI/UX 规范与设计验收清单落地 | `产品` + `UIUX` + `QA` | 2026-03-27 | SUP-003 | DQA P0=0,P1 通过率>=95% | `docs/supply_uiux_design_spec_v1_2026-03-25.md` |
|
||||
| XR-004 | 业主 SLA/申诉/赔付条款并入门禁验收 | `产品` + `CS` + `FIN` | 2026-03-28 | XR-002, XR-003 | 条款可执行可测且签字确认 | `docs/product/owner_sla_dispute_compensation_rules_v1.md` |
|
||||
| XR-005 | 四专家再次对齐复核并形成发布结论 | `ARCH` + `QA` + `产品` + `UIUX` | 2026-03-28 | XR-001~XR-004 | 复核结论明确(GO/CONDITIONAL GO/NO-GO) | `review/multi_expert_alignment_recheck_v1_2026-03-25.md` |
|
||||
|
||||
## 4.9 Workstream I:数据库与依赖质量闭环(新增)
|
||||
|
||||
| 任务ID | 任务 | Owner | 截止日期 | 依赖 | 验收标准 | 证据产物 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| DB-001 | 跨域核心表基线落地(Core/IAM/Auth/Billing/Audit) | `ARCH` + `PLAT` | 2026-03-27 | XR-001 | `platform_core_schema_v1.sql` 可执行且评审通过 | `sql/postgresql/platform_core_schema_v1.sql` |
|
||||
| DB-002 | 供应域加密/单位/审计字段与索引补齐 | `PLAT` + `QA` | 2026-03-28 | DB-001 | patch 可幂等执行,关键查询计划不回退 | `sql/postgresql/supply_schema_v1_patch_2026-03-27.sql` |
|
||||
| DB-003 | 数据模型与迁移策略文档并入 SSOT | `ARCH` | 2026-03-28 | DB-001, DB-002 | 迁移顺序、回滚策略、验收清单完整 | `docs/database_domain_model_and_governance_v1_2026-03-27.md` |
|
||||
| DEP-001 | 依赖兼容审计四件套接入发布流程 | `PLAT` + `SEC` | 2026-03-28 | COMP-005 | SBOM/锁差异/兼容矩阵/风险清单缺一阻断 | `docs/dependency_compatibility_audit_baseline_v1_2026-03-27.md` |
|
||||
| DEP-002 | 分阶段质量门禁(G0-G5)接入 CI | `QA` + `PLAT` | 2026-03-29 | DEP-001, XR-002 | `M-018` 与 `M-019` 自动计算并阻断 | CI 记录 + Gate 汇总 |
|
||||
| DEP-003 | 需求-设计-测试漂移日检机制上线 | `PMO` + `QA` | 2026-03-29 | DEP-002 | 发现漂移 24h 内闭环,周报可追踪 | `reports/design_drift_daily_*.md` |
|
||||
|
||||
## 4.10 Workstream J:token 运行态实现与验收闭环(TOK,新增)
|
||||
|
||||
| 任务ID | 任务 | Owner | 截止日期 | 依赖 | 验收标准 | 证据产物 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| TOK-001 | token 能力最小实现清单冻结(签发/校验/吊销/续期/审计) | `ARCH` + `SEC` + `PLAT` | 2026-03-28 | SUP-002 | 功能边界、接口与状态机冻结,禁止再口头变更 | `docs/token_runtime_minimal_spec_v1.md` |
|
||||
| TOK-002 | 平台鉴权与 token 校验中间件实现(仅平台凭证入站) | `PLAT` + `SEC` | 2026-03-30 | TOK-001 | 外部请求必须通过平台凭证校验,覆盖率=100% | 开发阶段:`docs/token_auth_middleware_design_v1_2026-03-29.md` + `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`;联调阶段:实现代码 + 单测报告 |
|
||||
| TOK-003 | token 生命周期实现(签发/短期TTL/吊销/轮换) | `PLAT` | 2026-03-31 | TOK-001 | 生命周期状态可追踪,吊销生效延迟满足阈值 | 开发阶段:`docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`;联调阶段:实现代码 + 集成测试报告 |
|
||||
| TOK-004 | 安全审计与事件入库(签发/鉴权失败/吊销/越权) | `SEC` + `PLAT` | 2026-03-31 | TOK-002, TOK-003 | 审计事件完整入库,可按租户/角色追踪 | 开发阶段:`docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`;联调阶段:审计表样例 + 查询结果 |
|
||||
| TOK-005 | 凭证边界联调(SUP-007 合并复测) | `SEC` + `QA` | 2026-04-01 | TOK-002~TOK-004 | M-013~M-016 在 staging 实测全部达标 | 开发阶段:`scripts/supply-gate/tok005_boundary_dryrun.sh` + `reports/gates/tok005_dryrun_*.md`;联调阶段:`tests/supply/sec_sup_boundary_report_2026-03-30.md`(staging回填) |
|
||||
| TOK-006 | staging 一键回归(SUP-004~SUP-007 + TOK) | `QA` + `PLAT` | 2026-04-01 | TOK-005 | 全链路通过且无 mock 依赖 | 开发阶段:`scripts/supply-gate/tok006_gate_bundle.sh` + `scripts/ci/superpowers_stage_validate.sh` + `reports/gates/tok006_gate_bundle_*.md` + `reports/gates/superpowers_stage_validation_*.md` + `reports/gates/tok006_release_decision_onepager_template_v1_2026-03-30.md`;联调阶段:`reports/gates/sup_run_all_staging_*.log` + 实测单页判定报告 |
|
||||
| TOK-007 | 发布门禁复审(并入 EXP-006 决议) | `ARCH` + `QA` + `SEC` | 2026-04-03 | TOK-006 | F-04 关闭,生产决议可重新评估 | 开发阶段:`scripts/ci/tok007_release_recheck.sh` + `scripts/ci/final_decision_consistency_check.sh` + `scripts/ci/tok007_generate_final_decision_candidate.sh` + `review/outputs/tok007_release_recheck_*.md` + `review/outputs/final_decision_candidate_from_tok007_*.md` + `reports/gates/final_decision_consistency_*.md`;联调阶段:`review/final_decision_2026-03-31.md`(复审回填) |
|
||||
|
||||
## 5. 验收门禁(每日/每周)
|
||||
|
||||
## 5.1 Daily Gate(每日 18:00)
|
||||
@@ -174,6 +200,10 @@
|
||||
11. 供应侧 UI Gate 是否全绿(`UI-SUP-ACC-* / UI-SUP-PKG-* / UI-SUP-SET-*`)。
|
||||
12. 供应侧凭证边界专项(`SEC-SUP-*`)是否全绿(失败即 P0)。
|
||||
13. 四专家整改链路(XR-001~XR-003)是否全绿(未完成即禁止进入 SUP-008 结论环节)。
|
||||
14. 数据库补丁任务(DB-001~DB-003)是否按阶段达成(未完成即禁止升波)。
|
||||
15. 依赖兼容审计四件套是否完整(缺任一项即阻断发布)。
|
||||
16. 分阶段质量门禁 `M-018/M-019` 是否持续 = 100%(否则回退到失败阶段)。
|
||||
17. token 运行态链路(TOK-002~TOK-006)是否完成(未完成即禁止生产 GO)。
|
||||
|
||||
## 5.2 Weekly Gate(2026-03-24 / 2026-03-31)
|
||||
|
||||
@@ -184,6 +214,10 @@
|
||||
5. 是否完成当周专家评审并关闭必须整改项。
|
||||
6. 供应侧 Gate(SUP-004~SUP-008)是否完成并出具结论。
|
||||
7. 四专家复核链路(XR-001~XR-005)是否完成并形成签署结论。
|
||||
8. DB/依赖质量链路(DB-* / DEP-*)是否全量关闭。
|
||||
9. 依赖兼容审计指标 `M-017` 是否连续 7 天达标。
|
||||
10. 阶段质量与追踪覆盖指标 `M-018/M-019` 是否连续 7 天达标。
|
||||
11. token 运行态审计缺口(`TOK-REAL-001~003`)是否全部关闭。
|
||||
|
||||
## 6. 风险与阻断规则
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ info:
|
||||
安全边界要求:
|
||||
1) 仅接受平台鉴权头(Authorization),不接受 query key 鉴权。
|
||||
2) 任何响应不得返回可复用上游凭证明文片段。
|
||||
变更日志:
|
||||
- 2026-03-27:新增幂等请求头组件与写操作挂载;补充 409/202 幂等语义示例。
|
||||
- 2026-03-27:命名策略调整为 `/supply` 主路径;`/supplier` 保留为兼容 alias。
|
||||
servers:
|
||||
- url: https://api.example.com
|
||||
description: Production
|
||||
@@ -48,6 +51,9 @@ paths:
|
||||
tags: [SupplyAccounts]
|
||||
summary: 创建供应账号
|
||||
operationId: createSupplyAccount
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKeyHeader'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -61,10 +67,14 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateSupplyAccountResponse'
|
||||
'202':
|
||||
$ref: '#/components/responses/AcceptedInProgress'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
'422':
|
||||
$ref: '#/components/responses/BusinessError'
|
||||
/api/v1/supply/accounts/{accountId}/activate:
|
||||
@@ -175,6 +185,8 @@ paths:
|
||||
operationId: publishSupplyPackage
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/PackageIdParam'
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKeyHeader'
|
||||
responses:
|
||||
'200':
|
||||
description: 发布成功
|
||||
@@ -182,6 +194,8 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SupplyPackageStatusResponse'
|
||||
'202':
|
||||
$ref: '#/components/responses/AcceptedInProgress'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
@@ -233,6 +247,9 @@ paths:
|
||||
tags: [SupplyPackages]
|
||||
summary: 批量调价
|
||||
operationId: batchUpdateSupplyPackagePrice
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKeyHeader'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -246,6 +263,8 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BatchUpdateSupplyPackagePriceResponse'
|
||||
'202':
|
||||
$ref: '#/components/responses/AcceptedInProgress'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
@@ -271,11 +290,35 @@ paths:
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
/api/v1/supply/billing:
|
||||
get:
|
||||
tags: [SupplierBilling]
|
||||
summary: 查询供应方账单汇总(canonical)
|
||||
operationId: getSupplyBilling
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/StartDateParam'
|
||||
- $ref: '#/components/parameters/EndDateParam'
|
||||
- $ref: '#/components/parameters/PageParam'
|
||||
- $ref: '#/components/parameters/PageSizeParam'
|
||||
responses:
|
||||
'200':
|
||||
description: 查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SupplierBillingResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
|
||||
/api/v1/supplier/billing:
|
||||
get:
|
||||
tags: [SupplierBilling]
|
||||
summary: 查询供应方账单汇总
|
||||
operationId: getSupplierBilling
|
||||
summary: 查询供应方账单汇总(alias,兼容路径)
|
||||
description: |
|
||||
Deprecated alias of `/api/v1/supply/billing`.
|
||||
仅用于历史客户端兼容,不新增能力字段。
|
||||
deprecated: true
|
||||
operationId: getSupplierBillingAlias
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/StartDateParam'
|
||||
- $ref: '#/components/parameters/EndDateParam'
|
||||
@@ -296,6 +339,9 @@ paths:
|
||||
tags: [SupplySettlements]
|
||||
summary: 发起提现申请
|
||||
operationId: createSupplySettlementWithdraw
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKeyHeader'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -309,6 +355,8 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateWithdrawResponse'
|
||||
'202':
|
||||
$ref: '#/components/responses/AcceptedInProgress'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
@@ -322,6 +370,8 @@ paths:
|
||||
operationId: cancelSupplySettlementWithdraw
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SettlementIdParam'
|
||||
- $ref: '#/components/parameters/XRequestIdHeader'
|
||||
- $ref: '#/components/parameters/IdempotencyKeyHeader'
|
||||
responses:
|
||||
'200':
|
||||
description: 撤销成功
|
||||
@@ -329,6 +379,8 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SupplySettlementStatusResponse'
|
||||
'202':
|
||||
$ref: '#/components/responses/AcceptedInProgress'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
@@ -428,6 +480,24 @@ components:
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 20
|
||||
XRequestIdHeader:
|
||||
name: X-Request-Id
|
||||
in: header
|
||||
required: true
|
||||
description: 客户端请求幂等追踪ID(全链路唯一)
|
||||
schema:
|
||||
type: string
|
||||
minLength: 8
|
||||
maxLength: 128
|
||||
IdempotencyKeyHeader:
|
||||
name: Idempotency-Key
|
||||
in: header
|
||||
required: true
|
||||
description: 写操作幂等键(同资源同动作语义唯一)
|
||||
schema:
|
||||
type: string
|
||||
minLength: 8
|
||||
maxLength: 128
|
||||
responses:
|
||||
BadRequest:
|
||||
description: 参数错误
|
||||
@@ -453,6 +523,34 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
idempotencyPayloadMismatch:
|
||||
summary: 幂等键命中但请求体不一致
|
||||
value:
|
||||
request_id: req_20260327_001
|
||||
error:
|
||||
code: IDEMPOTENCY_PAYLOAD_MISMATCH
|
||||
message: idempotency key replay with different payload
|
||||
details:
|
||||
retryable: false
|
||||
expected_action: reuse_same_payload_or_new_idempotency_key
|
||||
AcceptedInProgress:
|
||||
description: 首次请求仍在处理,请按建议间隔重试
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
idempotencyInProgress:
|
||||
summary: 幂等处理中重放
|
||||
value:
|
||||
request_id: req_20260327_002
|
||||
error:
|
||||
code: IDEMPOTENCY_IN_PROGRESS
|
||||
message: request is processing
|
||||
details:
|
||||
retry_after_ms: 2000
|
||||
retryable: true
|
||||
BusinessError:
|
||||
description: 业务校验失败
|
||||
content:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 供应侧按钮级 PRD 分解(首批 3 个核心页面)
|
||||
|
||||
- 版本:v1.0(草案)
|
||||
- 日期:2026-03-25
|
||||
- 版本:v1.1(冻结)
|
||||
- 日期:2026-03-27
|
||||
- 适用范围:供应侧 S0/S1 首批上线页面
|
||||
- 关联 SSOT:
|
||||
- `llm_gateway_subapi_evolution_plan_v4_2_2026-03-24.md`
|
||||
@@ -233,9 +233,15 @@
|
||||
|
||||
---
|
||||
|
||||
## 9. 待拍板项(进入 PRD v1.0 前必须确认)
|
||||
## 9. 已决议项(2026-03-27)
|
||||
|
||||
1. `POST /api/v1/supply/*` 系列接口是否按本稿命名冻结。
|
||||
2. 提现金额风控阈值(单笔/单日)与冷却期。
|
||||
3. 套餐“下架”与“暂停”的财务影响口径是否一致。
|
||||
4. 供应方是否允许批量导入账号(当前建议 S1 后)。
|
||||
决议依据:
|
||||
1. `docs/product/supply_prd_pending_to_decision_map_v1_2026-03-27.md`
|
||||
2. `review/outputs/supply_prd_decision_meeting_minutes_2026-03-27.md`
|
||||
|
||||
| 决议ID | 原待拍板项 | 决议结论 | 执行动作 |
|
||||
|---|---|---|---|
|
||||
| DEC-001 | `POST /api/v1/supply/*` 系列接口是否按本稿命名冻结 | 冻结 `/api/v1/supply/*` 为供应侧主路径;`/api/v1/supplier/billing` 作为兼容路径保留,待 F 阶段统一命名策略 | 在 OpenAPI 变更日志记录主路径与兼容路径策略 |
|
||||
| DEC-002 | 提现金额风控阈值(单笔/单日)与冷却期 | S1 阶段阈值冻结:单笔 `<= 50,000 CNY`,单日累计 `<= 200,000 CNY`,同账户提现冷却期 `15 分钟` | 在结算风控与测试用例中同步阈值断言 |
|
||||
| DEC-003 | 套餐“下架”与“暂停”的财务影响口径是否一致 | 不一致:`暂停`仅阻断新购,存量订单不变;`下架`阻断新购并触发 T+1 财务核对任务 | 在结算页与审计事件中区分 `pause/unlist` 财务语义 |
|
||||
| DEC-004 | 供应方是否允许批量导入账号 | 不允许进入 S0/S1 主路径;改为 S2 评审项,仅可在受控灰度与白名单下试点 | 移出当前发布门禁范围,纳入后续路线图 |
|
||||
|
||||
@@ -51,6 +51,18 @@ cd "/home/long/project/立交桥"
|
||||
bash "scripts/supply-gate/run_all.sh"
|
||||
```
|
||||
|
||||
真实 staging 推荐使用(含占位值与可达性预检):
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/supply-gate/staging_precheck_and_run.sh" "scripts/supply-gate/.env"
|
||||
```
|
||||
|
||||
说明:
|
||||
1. `staging_precheck_and_run.sh` 默认会先执行 `M-021` 预检(token runtime readiness)。
|
||||
2. `staging_precheck_and_run.sh` 默认会再执行 `TOK-005 dry-run`。
|
||||
3. 如需临时跳过可设置:`ENABLE_M021_PRECHECK=0` 或 `ENABLE_TOK005_DRYRUN=0`。
|
||||
|
||||
---
|
||||
|
||||
## 2. SUP-004 账号挂载链路(UI-SUP-ACC-001~006)
|
||||
@@ -163,3 +175,536 @@ bash "scripts/supply-gate/sup007_boundary.sh"
|
||||
1. 结论(PASS/FAIL/BLOCKED)
|
||||
2. 证据路径(json/screenshot/log)
|
||||
3. 责任人签字
|
||||
|
||||
---
|
||||
|
||||
## 7. 依赖兼容审计命令(M-017)
|
||||
|
||||
执行脚本:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
./scripts/ci/dependency-audit-check.sh 2026-03-27
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 四件套文件存在且非空:
|
||||
1. `reports/dependency/sbom_2026-03-27.spdx.json`
|
||||
2. `reports/dependency/lockfile_diff_2026-03-27.md`
|
||||
3. `reports/dependency/compat_matrix_2026-03-27.md`
|
||||
4. `reports/dependency/risk_register_2026-03-27.md`
|
||||
2. 输出结果为 `PASS`,并生成 `dependency_audit_result_2026-03-27.md`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 分阶段门禁失败回退演练(M-018/M-019)
|
||||
|
||||
执行脚本:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
./scripts/ci/stage-gate-drill.sh G3 2026-03-27
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. G3 失败后必须触发回退到 G2。
|
||||
2. 后续阶段冻结,不允许继续升波。
|
||||
3. 生成原始日志与演练报告:
|
||||
1. `reports/gates/stage_gate_drill_2026-03-27.log`
|
||||
2. `reports/gates/stage_gate_drift_drill_report_2026-03-27.md`
|
||||
|
||||
---
|
||||
|
||||
## 9. 本地 Mock 联调模式(仅演练)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
python3 "scripts/mock/supply_gateway_mock_server.py"
|
||||
```
|
||||
|
||||
另开终端执行:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/supply-gate/run_all.sh" "scripts/supply-gate/.env.local-mock"
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. 本模式仅用于脚本联调与产物验证,不代表 staging/生产可发布。
|
||||
2. 生产放行仍需在真实 staging 地址与真实短期 token 下复跑并验收。
|
||||
|
||||
---
|
||||
|
||||
## 10. TOK-005 凭证边界 Dry-Run(开发阶段)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/supply-gate/tok005_boundary_dryrun.sh" "scripts/supply-gate/.env"
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. `go test ./...` 在 `platform-token-runtime` 通过。
|
||||
2. Query key 外拒规则存在(`key/api_key/token`)。
|
||||
3. 审计脱敏断言存在且通过(禁止敏感值落审计)。
|
||||
4. TOK 用例可执行覆盖完整(`TOK-LIFE-001~008` 与 `TOK-AUD-001~007`)。
|
||||
5. staging 就绪性检查结果可追溯(NO 时需明确阻塞原因)。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `reports/gates/tok005_dryrun_*.md`
|
||||
2. `reports/gates/tok005_dryrun_*.log`
|
||||
3. `tests/supply/artifacts/tok005_dryrun_*/go_test_output.txt`
|
||||
|
||||
说明:
|
||||
|
||||
1. Dry-run 仅用于开发阶段门禁前置验证,不可替代真实 staging 联调结论。
|
||||
2. 真实放行仍以 `staging_precheck_and_run.sh` + `SUP-007/TOK-005` 实测结果为准。
|
||||
|
||||
---
|
||||
|
||||
## 11. TOK-006 统一 Gate 汇总(Dry-Run + SUP-004~007)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/supply-gate/tok006_gate_bundle.sh" "scripts/supply-gate/.env"
|
||||
```
|
||||
|
||||
可选开关:
|
||||
|
||||
```bash
|
||||
# 默认 1:先执行 TOK-005 dry-run
|
||||
ENABLE_TOK005_DRYRUN=1
|
||||
|
||||
# 默认 0:仅汇总现有 SUP 报告,不触发 run_all
|
||||
ENABLE_SUP_RUN=0
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 输出单页 gate 汇总报告(含 TOK-005 + SUP-004~007)。
|
||||
2. 生成明确发布判定:`GO / CONDITIONAL_GO / NO_GO`。
|
||||
3. 若存在 mock 证据或 `staging readiness != YES`,不得输出 GO。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `reports/gates/tok006_gate_bundle_*.md`
|
||||
2. `reports/gates/tok006_gate_bundle_*.log`
|
||||
3. `reports/gates/tok006_release_decision_onepager_template_v1_2026-03-30.md`(模板)
|
||||
|
||||
---
|
||||
|
||||
## 12. Superpowers 严格分阶段验证(代码+脚本+门禁)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/superpowers_stage_validate.sh"
|
||||
```
|
||||
|
||||
阶段定义(当前实现):
|
||||
|
||||
1. PHASE-01:TOK 运行时代码测试(Go 单测)
|
||||
2. PHASE-02:SUP-004~SUP-007 本地 mock 脚本联调
|
||||
3. PHASE-03:TOK-005 凭证边界 dry-run(mock 环境)
|
||||
4. PHASE-04:TOK-006 统一 Gate 汇总
|
||||
5. PHASE-05:依赖兼容审计门禁(M-017)
|
||||
6. PHASE-06:分阶段回退演练门禁(M-018/M-019)
|
||||
7. PHASE-07:真实 staging 预检(无真值时应 DEFERRED)
|
||||
8. PHASE-08:每日指标快照生成(M-017/M-018/M-019)
|
||||
9. PHASE-09:7日趋势报告生成(M-017/M-018/M-019)
|
||||
10. PHASE-10:token 运行态就绪度检查(M-021)
|
||||
|
||||
结果判定:
|
||||
|
||||
1. 任一阶段 FAIL => `NO_GO`
|
||||
2. 无 FAIL 且存在 DEFERRED => `CONDITIONAL_GO`
|
||||
3. 全部 PASS => `GO`
|
||||
|
||||
可选环境变量:
|
||||
|
||||
```bash
|
||||
# PHASE-07 使用的环境文件,默认 scripts/supply-gate/.env
|
||||
STAGING_ENV_FILE="scripts/supply-gate/.env"
|
||||
```
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `reports/gates/superpowers_stage_validation_*.md`
|
||||
2. `reports/gates/superpowers_stage_validation_*.log`
|
||||
3. `tests/supply/artifacts/superpowers_stage_validation_*/phase*.log`
|
||||
|
||||
---
|
||||
|
||||
## 13. TOK-007 发布门禁复审(自动汇总)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/tok007_release_recheck.sh"
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 自动读取最新 `TOK-006` 汇总报告。
|
||||
2. 自动读取最新 `Superpowers` 阶段验证报告。
|
||||
3. 自动读取 `SUP Gate` 汇总评审结论。
|
||||
4. 输出复审结论(`GO / CONDITIONAL GO / NO-GO`)与动作建议。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `review/outputs/tok007_release_recheck_*.md`
|
||||
2. `reports/gates/tok007_release_recheck_*.log`
|
||||
|
||||
---
|
||||
|
||||
## 14. 最终决议一致性校验(Final vs TOK-007)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/final_decision_consistency_check.sh"
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 可解析 `final_decision`、`TOK-007`、`superpowers_stage_validation` 三类结论。
|
||||
2. 若 `final_decision` 与 `TOK-007` 不一致,输出 `WARN`(不自动覆盖签署结论)。
|
||||
3. 若任一来源不可解析,输出 `FAIL` 并阻断自动流程。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `reports/gates/final_decision_consistency_*.md`
|
||||
2. `reports/gates/final_decision_consistency_*.log`
|
||||
|
||||
---
|
||||
|
||||
## 15. 最终决议候选稿生成(不覆盖签署原件)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/tok007_generate_final_decision_candidate.sh"
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 输入源必须包括 `review/final_decision_2026-03-31.md` 与最新 `tok007_release_recheck_*.md`。
|
||||
2. 输出文件位于 `review/outputs/final_decision_candidate_from_tok007_*.md`。
|
||||
3. 仅生成候选稿,不覆盖原签署文件。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `review/outputs/final_decision_candidate_from_tok007_*.md`
|
||||
2. `reports/gates/tok007_generate_candidate_*.log`
|
||||
|
||||
---
|
||||
|
||||
## 16. M-021 Token Runtime 就绪度检查
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/token_runtime_readiness_check.sh" "$(date +%F)"
|
||||
```
|
||||
|
||||
可选开关:
|
||||
|
||||
```bash
|
||||
# 默认 0:跳过本地端口冒烟(适配受限沙箱环境)
|
||||
ENABLE_TOKEN_RUNTIME_SMOKE=0
|
||||
|
||||
# 置 1:执行本地服务启动 + issue + audit-events 冒烟
|
||||
ENABLE_TOKEN_RUNTIME_SMOKE=1
|
||||
|
||||
# 可选:指定冒烟起始端口(默认 18082,若被占用会自动顺延)
|
||||
TOKEN_RUNTIME_SMOKE_PORT=18082
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 输出 `token_runtime_readiness_*.md` 报告并给出百分比结果。
|
||||
2. 运行态代码与契约工件完整(API入口/HTTP处理/OpenAPI/Dockerfile)。
|
||||
3. `platform-token-runtime` 测试与构建均通过。
|
||||
4. 若就绪度 `<100%`,脚本必须返回失败并阻断后续门禁。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `reports/gates/token_runtime_readiness_*.md`
|
||||
2. `reports/gates/token_runtime_readiness_*.log`
|
||||
3. `reports/gates/token_runtime_go_test_*.log`
|
||||
4. `reports/gates/token_runtime_go_build_*.log`
|
||||
|
||||
---
|
||||
|
||||
## 17. Token 审计事件查询(TOK-REAL-002)
|
||||
|
||||
本地服务启动:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥/platform-token-runtime"
|
||||
export PATH="/home/long/project/立交桥/.tools/go-current/bin:$PATH"
|
||||
go run ./cmd/platform-token-runtime
|
||||
```
|
||||
|
||||
审计查询示例:
|
||||
|
||||
```bash
|
||||
curl -sS "http://127.0.0.1:18081/api/v1/platform/tokens/audit-events?limit=20" \
|
||||
-H "X-Request-Id: req-audit-query-demo"
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 返回 `200`,且结构包含 `request_id/data.total/data.items`。
|
||||
2. 返回项包含 `event_id/event_name/request_id/route/result_code/created_at`。
|
||||
3. 响应不包含 `access_token` 或上游敏感凭证明文。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `platform-token-runtime/internal/httpapi/token_api_test.go`(自动化用例)
|
||||
2. `reports/gates/token_runtime_readiness_*.md`(检查项 `TOK-REAL-002-C1/C2`)
|
||||
|
||||
---
|
||||
|
||||
## 18. Staging 证据自动回填草稿
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/staging_evidence_autofill.sh"
|
||||
```
|
||||
|
||||
可选参数(用于锁定本次流水证据,避免误取历史 latest):
|
||||
|
||||
```bash
|
||||
bash "scripts/ci/staging_evidence_autofill.sh" \
|
||||
--staging-run-log "reports/gates/staging_run_2026-03-30_184432.log" \
|
||||
--stage-report "reports/gates/superpowers_stage_validation_2026-03-30_184433.md" \
|
||||
--token-readiness "reports/gates/token_runtime_readiness_2026-03-30_184435.md" \
|
||||
--tok007-report "review/outputs/tok007_release_recheck_2026-03-30_184436.md" \
|
||||
--pipeline-report "reports/gates/superpowers_release_pipeline_2026-03-30_184434.md"
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 自动抽取 `PHASE-07`、`M-013~M-016`、`M-021` 与 TOK-007 机判结论。
|
||||
2. 输出证据路径清单,便于人工补齐与签署。
|
||||
3. 不得自动上调为 GO,仅生成草稿。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `reports/gates/staging_token_go_evidence_autofill_*.md`
|
||||
2. `reports/gates/staging_token_go_evidence_autofill_*.log`
|
||||
|
||||
---
|
||||
|
||||
## 19. 一键 Staging 发布流水
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/staging_release_pipeline.sh" "scripts/supply-gate/.env"
|
||||
```
|
||||
|
||||
local/mock 防误跑(默认拦截):
|
||||
|
||||
```bash
|
||||
# 仅当明确要做 local/mock 演练时启用
|
||||
ALLOW_LOCAL_MOCK_STAGING=1 \
|
||||
bash "scripts/ci/staging_release_pipeline.sh" "scripts/supply-gate/.env.local-mock"
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. STEP-01:`staging_precheck_and_run.sh`(含 M-021/TOK-005/SUP run_all)。
|
||||
2. STEP-02:`superpowers_release_pipeline.sh`(使用 `STAGING_ENV_FILE`)。
|
||||
3. STEP-03:`staging_evidence_autofill.sh` 自动生成回填草稿(显式绑定本次流水证据文件)。
|
||||
4. 检测到 local/mock env 且未设置 `ALLOW_LOCAL_MOCK_STAGING=1` 时,脚本应直接失败,防止误把演练结果当成真实 staging 证据。
|
||||
|
||||
可选监控(默认关闭、非阻断):
|
||||
|
||||
```bash
|
||||
ENABLE_MINIMAX_MONITORING=1 \
|
||||
MINIMAX_ENV_FILE="scripts/supply-gate/.env.minimax-dev" \
|
||||
MINIMAX_RUN_ACTIVE_SMOKE=0 \
|
||||
bash "scripts/ci/superpowers_release_pipeline.sh"
|
||||
```
|
||||
|
||||
说明:
|
||||
1. 开启后会在 `STEP-05` 额外执行 Minimax 每日快照 + 7 日趋势生成。
|
||||
2. 该步骤是监控辅助项,失败仅记 `WARN`,不阻断 SUP 主门禁判定。
|
||||
|
||||
证据输出:
|
||||
|
||||
1. `reports/gates/staging_release_pipeline_*.md`
|
||||
2. `reports/gates/staging_release_pipeline_*.log`
|
||||
|
||||
---
|
||||
|
||||
## 20. Minimax 上游独立 Smoke(不并入 SUP 发布门禁)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/supply-gate/minimax_upstream_smoke.sh" "scripts/supply-gate/.env.minimax-dev"
|
||||
```
|
||||
|
||||
可选环境变量:
|
||||
|
||||
```bash
|
||||
# 默认 /v1/messages
|
||||
MINIMAX_SMOKE_PATH="/v1/messages"
|
||||
|
||||
# 默认 minimax-smoke-model(可替换为实际模型)
|
||||
MINIMAX_SMOKE_MODEL="your-model-id"
|
||||
|
||||
# 默认 20 秒
|
||||
MINIMAX_TIMEOUT_SECONDS=20
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 输出 `reports/gates/minimax_upstream_smoke_*.md` 报告。
|
||||
2. 报告必须包含 base 连通探测与 active 鉴权探测两段结果。
|
||||
3. 分类规则需区分:`PASS / PASS_AUTH_REACHED / FAIL_AUTH / FAIL_PATH / FAIL_NETWORK`。
|
||||
|
||||
说明:
|
||||
|
||||
1. 该脚本仅用于“上游(Minimax)连通与鉴权可达性”验证。
|
||||
2. 该脚本不参与 `SUP-004~SUP-007` 业务契约发布门禁判定。
|
||||
3. 若 Minimax 返回 `404/405`,优先检查 `API_BASE_URL + MINIMAX_SMOKE_PATH` 组合是否正确。
|
||||
|
||||
---
|
||||
|
||||
## 21. Minimax 上游每日快照(CI 汇总)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/minimax_upstream_daily_snapshot.sh" "$(date +%F)" "scripts/supply-gate/.env.minimax-dev"
|
||||
```
|
||||
|
||||
可选环境变量:
|
||||
|
||||
```bash
|
||||
# 默认 0:仅汇总最新 smoke 报告,不触发网络请求
|
||||
RUN_ACTIVE_SMOKE=0
|
||||
|
||||
# 置 1:执行一次实时 smoke 后再汇总
|
||||
RUN_ACTIVE_SMOKE=1
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 生成 `reports/gates/minimax_upstream_daily_snapshot_*.md`。
|
||||
2. 生成/更新 `reports/gates/minimax_upstream_daily_snapshots.csv`。
|
||||
3. 明确标注 `RUN_ACTIVE_SMOKE` 取值,区分“实时探测”与“仅汇总”。
|
||||
4. 默认优先汇总“非 dry-run”最新报告,避免将联调证据误当真实上游证据。
|
||||
|
||||
说明:
|
||||
|
||||
1. 该快照是“上游可达性趋势”证据,不替代 SUP 发布门禁。
|
||||
2. 建议在定时任务中默认 `RUN_ACTIVE_SMOKE=0`,将实时探测作为受控任务执行。
|
||||
3. 若仅存在 `PASS_DRY_RUN` 报告,快照状态应为 `CONDITIONAL_PASS`。
|
||||
|
||||
---
|
||||
|
||||
## 22. Minimax 上游 7 日趋势报告
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/minimax_upstream_trend_report.sh" "$(date +%F)"
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 生成 `reports/gates/minimax_upstream_trend_7d_*.md`。
|
||||
2. 报告包含最近 7 条(不足 7 条按实际)快照的状态统计。
|
||||
3. 趋势状态遵循 `PASS_7D / CONDITIONAL_7D / NOT_READY / INSUFFICIENT_DATA`。
|
||||
|
||||
说明:
|
||||
|
||||
1. 该趋势报告用于 F-03(连续观测证据)收敛,不替代 staging 发布门禁。
|
||||
2. 建议与第 21 节每日快照搭配执行,形成“日报 + 周趋势”组合。
|
||||
|
||||
---
|
||||
|
||||
## 23. 一键生成本地 STG 环境(owner/viewer/admin token)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/generate_local_staging_env.sh" "scripts/supply-gate/.env.staging-real"
|
||||
```
|
||||
|
||||
可选环境变量:
|
||||
|
||||
```bash
|
||||
# 默认 http://127.0.0.1:18080
|
||||
API_BASE_URL_VALUE="http://127.0.0.1:18080"
|
||||
|
||||
# 默认 http://127.0.0.1:18081
|
||||
TOKEN_RUNTIME_URL="http://127.0.0.1:18081"
|
||||
|
||||
# 默认 7200 秒(2小时)
|
||||
TOKEN_TTL_SECONDS=7200
|
||||
|
||||
# 默认 1:若 token runtime 不可用则自动拉起临时实例
|
||||
START_RUNTIME_IF_NEEDED=1
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. 生成 `scripts/supply-gate/.env.staging-real`(权限 `600`)。
|
||||
2. 文件包含 `OWNER_BEARER_TOKEN / VIEWER_BEARER_TOKEN / ADMIN_BEARER_TOKEN` 三类 token。
|
||||
3. 生成摘要报告 `reports/gates/local_staging_env_generation_*.md`(仅 hash,不泄露明文 token)。
|
||||
|
||||
说明:
|
||||
|
||||
1. 该脚本生成的是“本地开发/联调用”平台 token,非外部 LLM 厂商 key。
|
||||
2. 切换真实 staging 时,只需替换 `API_BASE_URL_VALUE` 并重新执行脚本即可刷新 token 与 env。
|
||||
|
||||
---
|
||||
|
||||
## 24. 真实 STG 就绪度检查(地址+token+可达性)
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥"
|
||||
bash "scripts/ci/staging_real_readiness_check.sh" "scripts/supply-gate/.env.staging-real"
|
||||
```
|
||||
|
||||
最低断言:
|
||||
|
||||
1. `API_BASE_URL` 非占位值,且不是 `localhost/127.0.0.1`。
|
||||
2. 三类 token 非空且非占位值。
|
||||
3. `API_BASE_URL` 基础可达性检查通过(`curl -I` 非 `000`)。
|
||||
4. 生成报告 `reports/gates/staging_real_readiness_*.md`。
|
||||
|
||||
说明:
|
||||
|
||||
1. 结果为 `READY` 才建议进入真实 STG 放行口径验证。
|
||||
2. 结果为 `BLOCKED` 时,应先修复地址或 token,再执行 `staging_release_pipeline.sh`。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 供应侧技术设计增强版(XR-001)
|
||||
|
||||
- 版本:v1.0
|
||||
- 日期:2026-03-25
|
||||
- 版本:v1.1
|
||||
- 日期:2026-03-27
|
||||
- 状态:生效(实施基线)
|
||||
- 目标:补齐供应侧关键写路径的幂等、并发、事务、不变量与可靠性闭环
|
||||
- 关联 SSOT:
|
||||
@@ -9,6 +9,7 @@
|
||||
- `acceptance_gate_single_source_v1_2026-03-18.md`
|
||||
- `supply_button_level_prd_v1_2026-03-25.md`
|
||||
- `supply_api_contract_openapi_draft_v1_2026-03-25.yaml`
|
||||
- `database_domain_model_and_governance_v1_2026-03-27.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -38,6 +39,7 @@
|
||||
2. Header 必填:`Idempotency-Key`(长度 16-128)
|
||||
3. 幂等作用域:`tenant_id + operator_id + api_path + idempotency_key`
|
||||
4. 幂等有效期:`24h`(提现类可扩展到 `72h`)
|
||||
5. 契约落地状态:已在 OpenAPI 写操作路径挂载上述 header,并补充 `409/202` 幂等语义示例(2026-03-27)。
|
||||
|
||||
## 2.3 语义规范
|
||||
|
||||
@@ -91,7 +93,7 @@ create table if not exists supply_idempotency_record (
|
||||
|
||||
```sql
|
||||
create unique index if not exists uq_settlement_supplier_processing
|
||||
on supply_settlement(supplier_id)
|
||||
on supply_settlements(user_id)
|
||||
where status = 'processing';
|
||||
```
|
||||
|
||||
@@ -160,12 +162,12 @@ where status = 'processing';
|
||||
|
||||
| 页面按钮 | API | SLI | SLO | Error Budget |
|
||||
|---|---|---|---|---|
|
||||
| BTN-ACC-001 立即验证 | `/accounts/verify` | 可用率 + P95 | 可用率 >= 99.9%,P95 <= 800ms | 月度 0.1% |
|
||||
| BTN-ACC-002 提交挂载 | `/accounts` | 成功率 | 成功率 >= 99.5% | 月度 0.5% |
|
||||
| BTN-PKG-002 发布上架 | `/packages/{id}/publish` | 成功率 + 冲突率 | 成功率 >= 99.5%,冲突率 <= 0.3% | 月度 0.5% |
|
||||
| BTN-PKG-005 批量调价 | `/packages/batch-price` | 局部成功可解释率 | 明细可解释率 = 100% | 0 |
|
||||
| BTN-SET-002 发起提现 | `/settlements/withdraw` | 一致性 + 时延 | `billing_error_rate_pct<=0.1%`,P95<=1200ms | 与 M-004 联动 |
|
||||
| BTN-SET-003 撤销申请 | `/settlements/{id}/cancel` | 成功率 | 成功率 >= 99.9% | 月度 0.1% |
|
||||
| BTN-ACC-001 立即验证 | `/api/v1/supply/accounts/verify` | 可用率 + P95 | 可用率 >= 99.9%,P95 <= 800ms | 月度 0.1% |
|
||||
| BTN-ACC-002 提交挂载 | `/api/v1/supply/accounts` | 成功率 | 成功率 >= 99.5% | 月度 0.5% |
|
||||
| BTN-PKG-002 发布上架 | `/api/v1/supply/packages/{id}/publish` | 成功率 + 冲突率 | 成功率 >= 99.5%,冲突率 <= 0.3% | 月度 0.5% |
|
||||
| BTN-PKG-005 批量调价 | `/api/v1/supply/packages/batch-price` | 局部成功可解释率 | 明细可解释率 = 100% | 0 |
|
||||
| BTN-SET-002 发起提现 | `/api/v1/supply/settlements/withdraw` | 一致性 + 时延 | `billing_error_rate_pct<=0.1%`,P95<=1200ms | 与 M-004 联动 |
|
||||
| BTN-SET-003 撤销申请 | `/api/v1/supply/settlements/{id}/cancel` | 成功率 | 成功率 >= 99.9% | 月度 0.1% |
|
||||
|
||||
---
|
||||
|
||||
@@ -191,3 +193,18 @@ where status = 'processing';
|
||||
6. 证据层:执行日志、指标截图、审计抽样、签署记录齐全。
|
||||
|
||||
达到以上 6 项即视为 XR-001 关闭。
|
||||
|
||||
---
|
||||
|
||||
## 10. 跨域数据库约束(新增)
|
||||
|
||||
1. 供应域不是独立孤岛,必须依赖 Core/IAM/Auth/Billing/Audit 五域主表。
|
||||
2. 供应域关键表必须补齐三类字段:
|
||||
1. 加密字段:`*_cipher_algo`、`*_kms_key_alias`、`*_key_version`、`*_fingerprint`。
|
||||
2. 单位字段:`quota_unit`、`price_unit`、`amount_unit`、`currency_code`。
|
||||
3. 审计字段:`request_id`、`idempotency_key`、`audit_trace_id`、`created_ip`、`updated_ip`、`version`。
|
||||
3. 数据库实施顺序固定:
|
||||
1. `platform_core_schema_v1.sql`
|
||||
2. `supply_schema_v1.sql`
|
||||
3. `supply_schema_v1_patch_2026-03-27.sql`
|
||||
4. 未完成上述顺序与字段补齐,不得判定 XR-001 关闭。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 供应侧测试方案增强版(XR-002)
|
||||
|
||||
- 版本:v1.0
|
||||
- 日期:2026-03-25
|
||||
- 版本:v1.1
|
||||
- 日期:2026-03-27
|
||||
- 状态:生效(测试执行基线)
|
||||
- 目标:形成“需求-接口-测试-指标-门禁”全链路闭环,补齐并发与重放风险覆盖
|
||||
- 关联文档:
|
||||
@@ -35,22 +35,26 @@
|
||||
|
||||
## 2. 测试追踪矩阵(Requirement -> API -> Test -> Metric -> Gate)
|
||||
|
||||
| 需求ID | 需求描述 | API | 测试用例 | 验收指标 | 门禁映射 |
|
||||
|---|---|---|---|---|---|
|
||||
| R-ACC-001 | 账号凭证验证成功可视化 | `POST /accounts/verify` | UI-SUP-ACC-001 | 验证成功率 >=99.5% | SUP-004 |
|
||||
| R-ACC-002 | 挂载需风险确认与审计 | `POST /accounts` | UI-SUP-ACC-002 | 审计覆盖率=100% | SUP-004 |
|
||||
| R-ACC-003 | 账号状态不跳态 | `POST /accounts/{id}/activate/suspend` | UI-SUP-ACC-003/004 + INT-ACC-STATE-001 | 冲突可解释率=100% | SUP-004 |
|
||||
| R-ACC-004 | 活跃账号不可删除 | `DELETE /accounts/{id}` | UI-SUP-ACC-005 | 违规删除成功率=0 | SUP-004 |
|
||||
| R-PKG-001 | 草稿保存可追踪 | `POST /packages/draft` | UI-SUP-PKG-001 | 保存成功率>=99.5% | SUP-005 |
|
||||
| R-PKG-002 | 套餐发布满足保护价与状态约束 | `POST /packages/{id}/publish` | UI-SUP-PKG-002 + INT-PKG-INV-001 | 保护价违规放行率=0 | SUP-005 |
|
||||
| R-PKG-003 | 批量调价部分失败可回执 | `POST /packages/batch-price` | UI-SUP-PKG-005 | 明细完备率=100% | SUP-005 |
|
||||
| R-SET-001 | 提现发起防重复防双扣 | `POST /settlements/withdraw` | UI-SUP-SET-002 + CON-SET-001 | M-004/M-005 达标 | SUP-006 |
|
||||
| R-SET-002 | 处理中/已完成不可撤销 | `POST /settlements/{id}/cancel` | UI-SUP-SET-003 + INT-SET-STATE-001 | 跳态成功率=0 | SUP-006 |
|
||||
| R-SET-003 | 对账单导出不泄露敏感信息 | `GET /settlements/{id}/statement` | UI-SUP-SET-004 + SEC-SUP-001 | M-013=0 | SUP-006/SUP-007 |
|
||||
| R-SEC-001 | 仅平台凭证入站 | 全部北向 API | SEC-SUP-002 | M-014=100% | SUP-007 |
|
||||
| R-SEC-002 | 外部 query key 全拒绝 | 全部北向 API | SEC-SUP-002 | M-016=100% | SUP-007 |
|
||||
| R-SEC-003 | 需求方不可绕平台直连 | 出网策略与告警 | SEC-SUP-002 + SEC-DIRECT-001 | M-015=0 | SUP-007 |
|
||||
| R-UX-001 | 按钮可见性和禁用规则正确 | 三页面全部按钮 | UI-DESIGN-QA-001~020 | 按钮规则通过率=100% | SUP-003/SUP-008 |
|
||||
| 需求ID | 需求描述 | API | api_alias | 测试用例 | 验收指标 | 门禁映射 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| R-ACC-001 | 账号凭证验证成功可视化 | `POST /api/v1/supply/accounts/verify` | - | UI-SUP-ACC-001 | 验证成功率 >=99.5% | SUP-004 |
|
||||
| R-ACC-002 | 挂载需风险确认与审计 | `POST /api/v1/supply/accounts` | - | UI-SUP-ACC-002 | 审计覆盖率=100% | SUP-004 |
|
||||
| R-ACC-003 | 账号状态不跳态 | `POST /api/v1/supply/accounts/{accountId}/activate` / `POST /api/v1/supply/accounts/{accountId}/suspend` | `POST /api/v1/supply/accounts/{id}/activate` / `POST /api/v1/supply/accounts/{id}/suspend` | UI-SUP-ACC-003/004 + INT-ACC-STATE-001 | 冲突可解释率=100% | SUP-004 |
|
||||
| R-ACC-004 | 活跃账号不可删除 | `DELETE /api/v1/supply/accounts/{accountId}` | `DELETE /api/v1/supply/accounts/{id}` | UI-SUP-ACC-005 | 违规删除成功率=0 | SUP-004 |
|
||||
| R-PKG-001 | 草稿保存可追踪 | `POST /api/v1/supply/packages/draft` | - | UI-SUP-PKG-001 | 保存成功率>=99.5% | SUP-005 |
|
||||
| R-PKG-002 | 套餐发布满足保护价与状态约束 | `POST /api/v1/supply/packages/{packageId}/publish` | `POST /api/v1/supply/packages/{id}/publish` | UI-SUP-PKG-002 + INT-PKG-INV-001 | 保护价违规放行率=0 | SUP-005 |
|
||||
| R-PKG-003 | 批量调价部分失败可回执 | `POST /api/v1/supply/packages/batch-price` | - | UI-SUP-PKG-005 | 明细完备率=100% | SUP-005 |
|
||||
| R-SET-001 | 提现发起防重复防双扣 | `POST /api/v1/supply/settlements/withdraw` | - | UI-SUP-SET-002 + CON-SET-001 | M-004/M-005 达标 | SUP-006 |
|
||||
| R-SET-002 | 处理中/已完成不可撤销 | `POST /api/v1/supply/settlements/{settlementId}/cancel` | `POST /api/v1/supply/settlements/{id}/cancel` | UI-SUP-SET-003 + INT-SET-STATE-001 | 跳态成功率=0 | SUP-006 |
|
||||
| R-SET-003 | 对账单导出不泄露敏感信息 | `GET /api/v1/supply/settlements/{settlementId}/statement` | `GET /api/v1/supply/settlements/{id}/statement` | UI-SUP-SET-004 + SEC-SUP-001 | M-013=0 | SUP-006/SUP-007 |
|
||||
| R-SEC-001 | 仅平台凭证入站 | 全部北向 API | - | SEC-SUP-002 | M-014=100% | SUP-007 |
|
||||
| R-SEC-002 | 外部 query key 全拒绝 | 全部北向 API | - | SEC-SUP-002 | M-016=100% | SUP-007 |
|
||||
| R-SEC-003 | 需求方不可绕平台直连 | 出网策略与告警 | - | SEC-SUP-002 + SEC-DIRECT-001 | M-015=0 | SUP-007 |
|
||||
| R-UX-001 | 按钮可见性和禁用规则正确 | 三页面全部按钮 | - | UI-DESIGN-QA-001~020 | 按钮规则通过率=100% | SUP-003/SUP-008 |
|
||||
|
||||
跨域映射补充:
|
||||
1. 全局 P0 中预算/告警/组织级账单导出映射见:`docs/product/global_p0_to_supply_platform_mapping_v1_2026-03-27.md`。
|
||||
2. 对应追踪项已并入:`reports/supply_traceability_matrix_2026-03-25.csv`(`R-PLAT-001~003`)。
|
||||
|
||||
---
|
||||
|
||||
@@ -143,6 +147,16 @@
|
||||
4. `SEC-SUP Gate`:凭证边界与泄露扫描(阻断)。
|
||||
5. `PERF/REL Gate`:每晚定时跑,异常进入发布前强制复核。
|
||||
|
||||
## 7.3 分阶段质量门禁(防偏航)
|
||||
|
||||
1. G0 Requirement Gate:检查 PRD/OpenAPI/按钮清单版本一致,任一漂移阻断开发。
|
||||
2. G1 Design Gate:检查 DDL、状态机、不变量、审计字段齐套,缺一阻断联调。
|
||||
3. G2 Dev Gate:单测与契约测试达标后才允许合并。
|
||||
4. G3 Integration Gate:DB/Redis/Outbox/权限链路通过后才允许提测。
|
||||
5. G4 Release Gate:SUP-004~SUP-007 与安全门禁全绿才允许发布。
|
||||
6. G5 Post Gate:发布后 24h 观察窗口出现 P0/P1 立即冻结后续升波。
|
||||
7. 指标约束:`M-018=100%` 且 `M-019=100%`,否则回退到失败阶段整改。
|
||||
|
||||
## 7.2 失败策略
|
||||
|
||||
1. P0 用例失败:立即阻断发布 + 当日复盘。
|
||||
@@ -158,6 +172,7 @@
|
||||
1. PRD 按钮级规格冻结。
|
||||
2. OpenAPI 字段冻结。
|
||||
3. 技术增强稿(XR-001)已落地。
|
||||
4. 路径一致性检查通过(API 字段与 OpenAPI 主路径一致,alias 映射完整)。
|
||||
|
||||
## 8.2 退出(Exit)
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# 供应侧追踪矩阵生成规则(v1.0)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 适用文件:`reports/supply_traceability_matrix_2026-03-25.csv`
|
||||
- 目标:保证 Requirement -> API -> Test -> Metric -> Gate 的自动化可追踪与口径一致。
|
||||
|
||||
## 1. 字段规范
|
||||
|
||||
1. `requirement_id`:唯一且稳定,不得复用。
|
||||
2. `api`:必须使用 OpenAPI 主路径与精确参数名(如 `{accountId}`、`{packageId}`、`{settlementId}`)。
|
||||
3. `api_alias`:仅记录历史兼容路径;无兼容值填写 `-`。
|
||||
4. `test_case`:使用 `|` 连接多个用例 ID,顺序按主路径优先。
|
||||
5. `metric`:使用 SSOT 中的统一指标名,禁止自造同义词。
|
||||
6. `gate`:映射 SUP/SEC/XR 门禁,多个值用 `|` 分隔。
|
||||
7. `status`:`PLANNED/RUNNING/PASS/FAIL/BLOCKED` 五态。
|
||||
|
||||
## 2. 生成流程
|
||||
|
||||
1. 从按钮级 PRD 抽取需求项并形成 `requirement_id`。
|
||||
2. 从 OpenAPI 提取接口主路径,填入 `api`。
|
||||
3. 对历史路径或迁移路径填入 `api_alias`。
|
||||
4. 绑定测试用例、指标、门禁并指定 owner。
|
||||
5. 由 QA 执行完整性检查后发布 CSV。
|
||||
|
||||
## 3. 校验规则
|
||||
|
||||
1. `api` 必须可在 OpenAPI 中检索命中。
|
||||
2. `api_alias` 不得与 `api` 完全相同。
|
||||
3. `gate` 必须在任务单中存在对应条目。
|
||||
4. 每条记录必须有 `evidence_path`。
|
||||
5. 任一校验失败,`M-019` 计为不通过。
|
||||
|
||||
## 4. 变更治理
|
||||
|
||||
1. 修改 `api` 视为高风险变更,必须同步更新用例与门禁映射。
|
||||
2. 新增 alias 必须附迁移原因和下线计划。
|
||||
3. 每次变更后需执行一次路径一致性检查并留痕。
|
||||
@@ -1,7 +1,7 @@
|
||||
# 优化技术架构设计(最小可运营栈 + 触发式扩容)
|
||||
|
||||
- 版本:v2.0
|
||||
- 日期:2026-03-18
|
||||
- 版本:v2.1
|
||||
- 日期:2026-03-27
|
||||
- 目标:降低 S0/S1 运维复杂度,同时保证 S2 替换目标可达。
|
||||
|
||||
---
|
||||
@@ -110,6 +110,8 @@ Internet
|
||||
- `llm_gateway_subapi_evolution_plan_v4_2_2026-03-24.md`
|
||||
- `acceptance_gate_single_source_v1_2026-03-18.md`
|
||||
- `test_plan_go_aligned_v1_2026-03-18.md`
|
||||
- `dependency_compatibility_audit_baseline_v1_2026-03-27.md`
|
||||
- `database_domain_model_and_governance_v1_2026-03-27.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -120,3 +122,42 @@ Internet
|
||||
3. 发布扩容触发条件评审模板(无触发条件不得引入组件)。
|
||||
4. 将运维看板与门禁阈值绑定到唯一验收门禁表。
|
||||
5. 完成一次“升级 + 灰度 + 自动回滚”全链路演练。
|
||||
|
||||
---
|
||||
|
||||
## 8. 依赖兼容性审计(新增强制门禁)
|
||||
|
||||
1. 发布前必须产出四类证据:SBOM、锁文件差异、兼容矩阵、风险清单。
|
||||
2. 对 `subapi/provider SDK` 执行精确版本锁定(`X.Y.Z`),禁止“仅锁主次版本”。
|
||||
3. 任一依赖发生 major 变更,必须附兼容影响评估与回滚演练记录。
|
||||
4. 依赖审计结果接入门禁指标 `M-017`,要求 `dependency_compat_audit_pass_pct=100%`。
|
||||
5. 运行时、数据层、构建镜像三类版本必须可追溯到同一发布包,禁止“文档版本”和“运行版本”漂移。
|
||||
|
||||
---
|
||||
|
||||
## 9. 分阶段质量检查(防偏离主线)
|
||||
|
||||
### 9.1 阶段门禁定义
|
||||
|
||||
| 阶段 | Gate | 必达条件 | 阻断动作 |
|
||||
|---|---|---|---|
|
||||
| G0 需求冻结 | Requirement Gate | P0/P1 需求、按钮、接口状态全部冻结 | 禁止进入开发 |
|
||||
| G1 设计冻结 | Design Gate | 数据模型、OpenAPI、状态机与审计字段齐套 | 禁止进入联调 |
|
||||
| G2 开发自检 | Dev Gate | 单元/契约测试通过,覆盖率达标 | 禁止合并 |
|
||||
| G3 集成验证 | Integration Gate | DB/缓存/外部依赖集成测试通过 | 禁止预发布 |
|
||||
| G4 发布演练 | Release Gate | 回滚演练、性能门禁、安全门禁通过 | 禁止生产发布 |
|
||||
| G5 发布观察 | Post Gate | 24h 指标稳定,无 P0/P1 回归 | 冻结后续升波 |
|
||||
|
||||
### 9.2 防偏航机制
|
||||
|
||||
1. 需求追踪覆盖率(`M-019`)必须 100%,每条 P0 需求都能映射到 API/测试/指标/Gate。
|
||||
2. 阶段通过率(`M-018`)必须 100%,任一阶段失败禁止“跳阶段推进”。
|
||||
3. 每日执行“需求-设计-测试-门禁”一致性巡检,发现漂移 24h 内关闭。
|
||||
4. 所有变更按 `request_id + trace_id` 留痕,确保故障可逆向定位到需求与提交。
|
||||
|
||||
---
|
||||
|
||||
## 10. 本版补充结论
|
||||
|
||||
1. 架构基线从“最小可运营栈”扩展为“最小可运营栈 + 依赖可审计 + 分阶段质量闭环”。
|
||||
2. 未完成依赖兼容审计或阶段门禁的变更,不得进入 `GO` 决策。
|
||||
|
||||
122
docs/token_auth_middleware_design_v1_2026-03-29.md
Normal file
122
docs/token_auth_middleware_design_v1_2026-03-29.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# 平台鉴权与 Token 校验中间件设计(TOK-002)
|
||||
|
||||
- 版本:v1.0
|
||||
- 日期:2026-03-29
|
||||
- 状态:开发实施设计基线
|
||||
- 依赖:`docs/token_runtime_minimal_spec_v1.md`
|
||||
- 目标:实现“仅平台凭证入站”,并为 M-014/M-016/M-021 提供可验证链路。
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
1. 所有北向请求必须通过平台凭证校验。
|
||||
2. 外部 `query key` 入站一律拒绝并记录审计事件。
|
||||
3. 鉴权结果可追踪到 `request_id + subject_id + token_id`。
|
||||
4. 在不泄露上游凭证的前提下返回标准错误码。
|
||||
|
||||
## 2. 适用范围
|
||||
|
||||
1. 路由范围:`/api/v1/supply/*`、`/api/v1/platform/*`。
|
||||
2. 鉴权头:仅支持 `Authorization: Bearer <token>`。
|
||||
3. 排除范围:健康检查、内部探针、公开静态资源。
|
||||
|
||||
## 3. 中间件链路
|
||||
|
||||
## 3.1 处理顺序
|
||||
|
||||
1. `RequestIdMiddleware`
|
||||
2. `QueryKeyRejectMiddleware`
|
||||
3. `BearerExtractMiddleware`
|
||||
4. `TokenVerifyMiddleware`
|
||||
5. `TokenStatusCheckMiddleware`
|
||||
6. `ScopeRoleAuthzMiddleware`
|
||||
7. `AuditEmitMiddleware`
|
||||
|
||||
## 3.2 关键规则
|
||||
|
||||
1. `QueryKeyRejectMiddleware`
|
||||
- 拒绝任意 `?key=`、`?api_key=`、`?token=` 形式外部参数。
|
||||
- 返回 `401 QUERY_KEY_NOT_ALLOWED`。
|
||||
2. `BearerExtractMiddleware`
|
||||
- 无 `Authorization` 直接 `401 AUTH_MISSING_BEARER`。
|
||||
3. `TokenVerifyMiddleware`
|
||||
- 校验签名、`iss`、`aud`、`exp`、`nbf`、`jti`。
|
||||
- 签名失败返回 `401 AUTH_INVALID_TOKEN`。
|
||||
4. `TokenStatusCheckMiddleware`
|
||||
- 查询 token 状态缓存(`active/revoked/expired`)。
|
||||
- `revoked/expired` 返回 `401 AUTH_TOKEN_INACTIVE`。
|
||||
5. `ScopeRoleAuthzMiddleware`
|
||||
- 按路由匹配 scope;不足返回 `403 AUTH_SCOPE_DENIED`。
|
||||
|
||||
## 4. 数据与缓存策略
|
||||
|
||||
1. 状态源:`platform_token_registry`(运行态主表)。
|
||||
2. 热缓存:`token_status_cache`(TTL 30s)。
|
||||
3. 吊销传播:
|
||||
- 吊销事件写入总线后,1~5 秒内刷新缓存。
|
||||
- 验收阈值:吊销生效延迟 `<= 5s`。
|
||||
|
||||
## 5. 错误语义
|
||||
|
||||
| 场景 | HTTP | error.code | 说明 |
|
||||
|---|---|---|---|
|
||||
| 缺失 Bearer | 401 | AUTH_MISSING_BEARER | 请求头缺失 |
|
||||
| query key 外部入站 | 401 | QUERY_KEY_NOT_ALLOWED | 边界拒绝 |
|
||||
| token 无效/签名失败 | 401 | AUTH_INVALID_TOKEN | 校验失败 |
|
||||
| token 已吊销/过期 | 401 | AUTH_TOKEN_INACTIVE | 状态不可用 |
|
||||
| scope 不足 | 403 | AUTH_SCOPE_DENIED | 权限不足 |
|
||||
|
||||
## 6. 审计事件(TOK-004 依赖)
|
||||
|
||||
1. `token.authn.success`
|
||||
2. `token.authn.fail`
|
||||
3. `token.authz.denied`
|
||||
4. `token.query_key.rejected`
|
||||
|
||||
最小字段:
|
||||
1. `event_id`
|
||||
2. `request_id`
|
||||
3. `token_id`(可空,提取失败时为空)
|
||||
4. `subject_id`(可空)
|
||||
5. `route`
|
||||
6. `result_code`
|
||||
7. `client_ip`
|
||||
8. `created_at`
|
||||
|
||||
## 7. 伪代码(实现参考)
|
||||
|
||||
```text
|
||||
onRequest(req):
|
||||
reqId = ensureRequestId(req)
|
||||
if hasExternalQueryKey(req):
|
||||
emitAudit("token.query_key.rejected", reqId, route, clientIp)
|
||||
return 401 QUERY_KEY_NOT_ALLOWED
|
||||
|
||||
bearer = parseBearer(req.headers.Authorization)
|
||||
if bearer is null:
|
||||
emitAudit("token.authn.fail", reqId, route, "AUTH_MISSING_BEARER")
|
||||
return 401 AUTH_MISSING_BEARER
|
||||
|
||||
claims = verifyToken(bearer)
|
||||
if verify failed:
|
||||
emitAudit("token.authn.fail", reqId, route, "AUTH_INVALID_TOKEN")
|
||||
return 401 AUTH_INVALID_TOKEN
|
||||
|
||||
status = getTokenStatus(claims.jti)
|
||||
if status != active:
|
||||
emitAudit("token.authn.fail", reqId, route, "AUTH_TOKEN_INACTIVE")
|
||||
return 401 AUTH_TOKEN_INACTIVE
|
||||
|
||||
if !checkScopeRole(claims.scope, claims.role, route):
|
||||
emitAudit("token.authz.denied", reqId, route, "AUTH_SCOPE_DENIED")
|
||||
return 403 AUTH_SCOPE_DENIED
|
||||
|
||||
attachPrincipal(req, claims)
|
||||
emitAudit("token.authn.success", reqId, route, "OK")
|
||||
pass
|
||||
```
|
||||
|
||||
## 8. 开发阶段验收(设计级)
|
||||
|
||||
1. 与 `TOK-001` 角色、状态机、审计字段一致。
|
||||
2. 与 `M-014/M-016` 指标定义一致。
|
||||
3. 与 OpenAPI token 契约草案字段一致。
|
||||
76
docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md
Normal file
76
docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# TOK-003/TOK-004 测试断言清单(生命周期 + 审计事件)
|
||||
|
||||
- 版本:v1.0
|
||||
- 日期:2026-03-29
|
||||
- 状态:开发实施测试基线
|
||||
- 适用任务:`TOK-003`、`TOK-004`
|
||||
|
||||
## 1. 测试范围
|
||||
|
||||
1. TOK-003:签发、续期、吊销、过期生命周期。
|
||||
2. TOK-004:签发/校验失败/吊销/越权事件入库与可追踪。
|
||||
|
||||
## 2. 前置数据
|
||||
|
||||
1. 租户:`tenant_id=1001`
|
||||
2. 主体:
|
||||
- `subject_owner=2001`
|
||||
- `subject_viewer=2002`
|
||||
3. 角色策略:
|
||||
- owner: `supply:*`
|
||||
- viewer: `supply:read`
|
||||
4. 观测阈值:
|
||||
- 吊销生效延迟 `<=5s`
|
||||
- 审计事件落库延迟 `<=3s`
|
||||
|
||||
## 3. TOK-003 生命周期断言
|
||||
|
||||
| 用例ID | 场景 | 步骤 | 断言 |
|
||||
|---|---|---|---|
|
||||
| TOK-LIFE-001 | 签发成功 | 1) 调用 `POST /tokens/issue` 2) 记录返回 | 1) `status=active` 2) `expires_at>issued_at` 3) `token_id` 唯一 |
|
||||
| TOK-LIFE-002 | 签发参数非法 | 1) `ttl_seconds` 超上限 2) 调用签发 | 1) 返回 `400` 2) 不落 active token |
|
||||
| TOK-LIFE-003 | 同键幂等签发重放 | 1) 相同 `Idempotency-Key` 重复提交 | 1) 返回同一 `token_id` 2) 无重复写入 |
|
||||
| TOK-LIFE-004 | 续期成功 | 1) 调用 `POST /tokens/{tokenId}/refresh` | 1) `expires_at` 延后 2) `status=active` |
|
||||
| TOK-LIFE-005 | 吊销成功 | 1) 调用 `POST /tokens/{tokenId}/revoke` 2) 立刻 introspect | 1) 最终 `status=revoked` 2) 生效延迟 <=5s |
|
||||
| TOK-LIFE-006 | 吊销后访问受限接口 | 1) 使用被吊销 token 访问受保护路由 | 1) 返回 `401 AUTH_TOKEN_INACTIVE` |
|
||||
| TOK-LIFE-007 | 过期自动失效 | 1) 签发短 TTL token 2) 等待过期 3) introspect | 1) `status=expired` 2) 返回不可用错误 |
|
||||
| TOK-LIFE-008 | viewer 越权写操作 | 1) viewer token 调用写接口 | 1) 返回 `403 AUTH_SCOPE_DENIED` 2) 无写入副作用 |
|
||||
|
||||
## 4. TOK-004 审计事件断言
|
||||
|
||||
| 用例ID | 场景 | 步骤 | 断言 |
|
||||
|---|---|---|---|
|
||||
| TOK-AUD-001 | 签发成功事件 | 执行 TOK-LIFE-001 | 1) 存在 `token.issue.success` 2) 字段齐全 |
|
||||
| TOK-AUD-002 | 签发失败事件 | 执行 TOK-LIFE-002 | 1) 存在 `token.issue.fail` 2) `result_code` 准确 |
|
||||
| TOK-AUD-003 | 鉴权失败事件 | 无效 token 访问受保护路由 | 1) `token.authn.fail` 入库 2) 含 `request_id` |
|
||||
| TOK-AUD-004 | 越权事件 | 执行 TOK-LIFE-008 | 1) `token.authz.denied` 入库 2) 含 `subject_id` |
|
||||
| TOK-AUD-005 | 吊销事件 | 执行 TOK-LIFE-005 | 1) `token.revoke.success` 入库 2) 含 `token_id` |
|
||||
| TOK-AUD-006 | query key 拒绝事件 | 使用 query key 访问接口 | 1) `token.query_key.rejected` 入库 2) 不出现敏感值 |
|
||||
| TOK-AUD-007 | 事件不可篡改 | 重复读取同 `event_id` | 1) 核心字段不可变 2) 时间顺序正确 |
|
||||
|
||||
## 5. 字段级硬断言
|
||||
|
||||
每条审计事件必须包含:
|
||||
1. `event_id`
|
||||
2. `request_id`
|
||||
3. `result_code`
|
||||
4. `route`
|
||||
5. `created_at`
|
||||
|
||||
可选字段规则:
|
||||
1. `token_id`:提取失败场景可空,其余场景必填。
|
||||
2. `subject_id`:匿名失败场景可空,其余场景必填。
|
||||
|
||||
禁止项:
|
||||
1. 不得写入上游供应方凭证明文。
|
||||
2. 不得写入完整 `access_token` 明文(仅允许哈希或指纹)。
|
||||
|
||||
## 6. 结果判定
|
||||
|
||||
1. TOK-003 通过标准:
|
||||
- `TOK-LIFE-*` 全通过
|
||||
- 吊销延迟阈值满足 `<=5s`
|
||||
2. TOK-004 通过标准:
|
||||
- `TOK-AUD-*` 全通过
|
||||
- 审计字段完整率 `=100%`
|
||||
- 敏感数据泄露事件 `=0`
|
||||
92
docs/token_runtime_minimal_spec_v1.md
Normal file
92
docs/token_runtime_minimal_spec_v1.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Token 运行态最小实现规格(TOK-001)
|
||||
|
||||
- 版本:v1.0
|
||||
- 日期:2026-03-27
|
||||
- 状态:开发实施基线
|
||||
- 对应任务:`TOK-001`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在不依赖真实 staging 参数的前提下,定义可落地的 token 运行态最小能力集,为后续 TOK-002~TOK-007 提供统一实施输入。
|
||||
|
||||
## 2. 最小能力范围(MVP)
|
||||
|
||||
1. 平台签发:短期访问 token(owner/viewer/admin)。
|
||||
2. 入站校验:仅平台凭证有效,拒绝 query key 外部入站。
|
||||
3. 生命周期:签发、续期、吊销、过期。
|
||||
4. 边界审计:签发/校验失败/吊销/越权事件全量入审计。
|
||||
5. 指标可观测:可计算 M-013~M-016 与 M-021。
|
||||
|
||||
## 3. 角色与权限
|
||||
|
||||
| 角色 | 能力 | 约束 |
|
||||
|---|---|---|
|
||||
| owner | 管理供应侧账号、套餐、结算 | 不可读取上游凭证明文 |
|
||||
| viewer | 只读查询 | 不可执行写操作 |
|
||||
| admin | 风控与审计管理 | 仅平台内部可用 |
|
||||
|
||||
## 4. Token 数据模型(最小字段)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| token_id | string | 平台内部唯一标识 |
|
||||
| subject_id | string | 用户/服务主体ID |
|
||||
| role | string | owner/viewer/admin |
|
||||
| issued_at | datetime | 签发时间 |
|
||||
| expires_at | datetime | 过期时间 |
|
||||
| status | string | active/revoked/expired |
|
||||
| scope | string[] | 授权范围 |
|
||||
| request_id | string | 请求追踪ID |
|
||||
| revoked_reason | string | 吊销原因(可空) |
|
||||
|
||||
## 5. 生命周期状态机
|
||||
|
||||
`active -> revoked -> expired`
|
||||
|
||||
规则:
|
||||
1. `revoked` 不可恢复为 `active`,需重新签发。
|
||||
2. `expires_at` 到期自动进入 `expired`。
|
||||
3. 续期只能对 `active` token 生效。
|
||||
|
||||
## 6. 核心接口(草案)
|
||||
|
||||
1. `POST /api/v1/platform/tokens/issue`
|
||||
2. `POST /api/v1/platform/tokens/{tokenId}/refresh`
|
||||
3. `POST /api/v1/platform/tokens/{tokenId}/revoke`
|
||||
4. `POST /api/v1/platform/tokens/introspect`
|
||||
5. `GET /api/v1/platform/tokens/audit-events`
|
||||
|
||||
返回要求:
|
||||
1. 不回传任何上游供应方凭证。
|
||||
2. 错误码需区分:无效、过期、越权、吊销。
|
||||
3. 审计查询接口仅返回审计字段,不返回 access token 或任何上游凭证明文。
|
||||
|
||||
## 7. 安全约束
|
||||
|
||||
1. token 存储需采用哈希或加密指纹,禁止明文落库。
|
||||
2. 校验路径必须记录 `request_id` 与调用来源。
|
||||
3. 外部 query key 入站请求必须拒绝并记录事件。
|
||||
4. 任一泄露事件触发 P0。
|
||||
|
||||
## 8. 审计事件最小集
|
||||
|
||||
1. `token.issue.success/fail`
|
||||
2. `token.introspect.success/fail`
|
||||
3. `token.refresh.success/fail`
|
||||
4. `token.revoke.success/fail`
|
||||
5. `token.authz.denied`
|
||||
|
||||
审计字段:
|
||||
1. `event_id`
|
||||
2. `request_id`
|
||||
3. `operator_id`
|
||||
4. `subject_id`
|
||||
5. `token_id`
|
||||
6. `result_code`
|
||||
7. `created_at`
|
||||
|
||||
## 9. 验收标准(TOK-001 关闭条件)
|
||||
|
||||
1. 本规格被 `ARCH + SEC + PLAT` 确认并引用到执行任务单。
|
||||
2. 后续 TOK-002~TOK-004 的实现字段与本规格一致。
|
||||
3. 不得新增“直接向终端用户分发上游 token”的路径。
|
||||
6
platform-token-runtime/.dockerignore
Normal file
6
platform-token-runtime/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.tools
|
||||
reports
|
||||
review
|
||||
tests
|
||||
**/*_test.go
|
||||
13
platform-token-runtime/Dockerfile
Normal file
13
platform-token-runtime/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/platform-token-runtime ./cmd/platform-token-runtime
|
||||
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
WORKDIR /app
|
||||
COPY --from=builder /out/platform-token-runtime /app/platform-token-runtime
|
||||
EXPOSE 18081
|
||||
ENTRYPOINT ["/app/platform-token-runtime"]
|
||||
41
platform-token-runtime/README.md
Normal file
41
platform-token-runtime/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# platform-token-runtime(TOK-002/003/004 开发实现)
|
||||
|
||||
本目录用于承载 token 运行态的开发阶段实现,不依赖真实 staging 参数。
|
||||
|
||||
## 文件说明
|
||||
|
||||
1. `cmd/platform-token-runtime/main.go`:可执行服务入口(HTTP + 健康检查)。
|
||||
2. `internal/httpapi/token_api.go`:`issue/refresh/revoke/introspect` 接口处理。
|
||||
3. `internal/httpapi/token_api_test.go`:HTTP 接口单测。
|
||||
4. `internal/auth/middleware/*`:TOK-002 中间件与单测。
|
||||
2. `internal/auth/service/token_verifier.go`:鉴权依赖接口、错误码、审计事件常量。
|
||||
3. `internal/auth/service/inmemory_runtime.go`:开发阶段最小可运行内存实现(签发/续期/吊销/introspect + 鉴权接口实现)。
|
||||
4. `internal/token/*_template_test.go`:TOK-003/004 测试模板(按 `TOK-LIFE-*`/`TOK-AUD-*` 对齐)。
|
||||
5. `internal/token/*_executable_test.go`:已转可执行用例(`TOK-LIFE-001~008`、`TOK-AUD-001~007`)。
|
||||
6. `Dockerfile`:运行时镜像构建工件。
|
||||
|
||||
## 设计边界
|
||||
|
||||
1. 仅支持 `Authorization: Bearer <token>` 入站。
|
||||
2. 外部 query key (`key/api_key/token`) 一律拒绝。
|
||||
3. 不在任何响应或审计字段中输出上游凭证明文。
|
||||
|
||||
## 本地测试
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥/platform-token-runtime"
|
||||
export PATH="/home/long/project/立交桥/.tools/go-current/bin:$PATH"
|
||||
export GOCACHE="/tmp/go-cache"
|
||||
export GOPATH="/tmp/go"
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## 本地运行
|
||||
|
||||
```bash
|
||||
cd "/home/long/project/立交桥/platform-token-runtime"
|
||||
export PATH="/home/long/project/立交桥/.tools/go-current/bin:$PATH"
|
||||
go run ./cmd/platform-token-runtime
|
||||
```
|
||||
|
||||
服务默认监听 `:18081`,可通过 `TOKEN_RUNTIME_ADDR` 覆盖。
|
||||
63
platform-token-runtime/cmd/platform-token-runtime/main.go
Normal file
63
platform-token-runtime/cmd/platform-token-runtime/main.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
"lijiaoqiao/platform-token-runtime/internal/httpapi"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := envOrDefault("TOKEN_RUNTIME_ADDR", ":18081")
|
||||
|
||||
runtime := service.NewInMemoryTokenRuntime(nil)
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
api := httpapi.NewTokenAPI(runtime, auditor, time.Now)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/actuator/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"status":"UP"}`))
|
||||
})
|
||||
api.Register(mux)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("platform-token-runtime listening on %s", addr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("listen failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Printf("graceful shutdown failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func envOrDefault(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
3
platform-token-runtime/go.mod
Normal file
3
platform-token-runtime/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module lijiaoqiao/platform-token-runtime
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,51 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
)
|
||||
|
||||
var disallowedQueryKeys = []string{"key", "api_key", "token"}
|
||||
|
||||
func QueryKeyRejectMiddleware(next http.Handler, auditor service.AuditEmitter, now func() time.Time) http.Handler {
|
||||
if next == nil {
|
||||
next = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||
}
|
||||
if now == nil {
|
||||
now = defaultNowFunc
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, exists := externalQueryKey(r)
|
||||
if !exists {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
requestID := ensureRequestID(r, now)
|
||||
emitAuditEvent(r.Context(), auditor, service.AuditEvent{
|
||||
EventName: service.EventTokenQueryKeyRejected,
|
||||
RequestID: requestID,
|
||||
Route: r.URL.Path,
|
||||
ResultCode: service.CodeQueryKeyNotAllowed,
|
||||
ClientIP: extractClientIP(r),
|
||||
CreatedAt: now(),
|
||||
})
|
||||
writeError(w, http.StatusUnauthorized, requestID, service.CodeQueryKeyNotAllowed, "query key ingress is not allowed")
|
||||
})
|
||||
}
|
||||
|
||||
func externalQueryKey(r *http.Request) (string, bool) {
|
||||
values := r.URL.Query()
|
||||
for key := range values {
|
||||
lowered := strings.ToLower(key)
|
||||
for _, disallowed := range disallowedQueryKeys {
|
||||
if lowered == disallowed {
|
||||
return key, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/model"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
)
|
||||
|
||||
const requestIDHeader = "X-Request-Id"
|
||||
|
||||
var defaultNowFunc = time.Now
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
requestIDKey contextKey = "request_id"
|
||||
principalKey contextKey = "principal"
|
||||
)
|
||||
|
||||
type AuthMiddlewareConfig struct {
|
||||
Verifier service.TokenVerifier
|
||||
StatusResolver service.TokenStatusResolver
|
||||
Authorizer service.RouteAuthorizer
|
||||
Auditor service.AuditEmitter
|
||||
ProtectedPrefixes []string
|
||||
ExcludedPrefixes []string
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
func BuildTokenAuthChain(cfg AuthMiddlewareConfig, next http.Handler) http.Handler {
|
||||
handler := TokenAuthMiddleware(cfg)(next)
|
||||
handler = QueryKeyRejectMiddleware(handler, cfg.Auditor, cfg.Now)
|
||||
handler = RequestIDMiddleware(handler, cfg.Now)
|
||||
return handler
|
||||
}
|
||||
|
||||
func RequestIDMiddleware(next http.Handler, now func() time.Time) http.Handler {
|
||||
if next == nil {
|
||||
return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||
}
|
||||
if now == nil {
|
||||
now = defaultNowFunc
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestID := ensureRequestID(r, now)
|
||||
w.Header().Set(requestIDHeader, requestID)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func TokenAuthMiddleware(cfg AuthMiddlewareConfig) func(http.Handler) http.Handler {
|
||||
cfg = cfg.withDefaults()
|
||||
return func(next http.Handler) http.Handler {
|
||||
if next == nil {
|
||||
next = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !cfg.shouldProtect(r.URL.Path) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
requestID := ensureRequestID(r, cfg.Now)
|
||||
if cfg.Verifier == nil || cfg.StatusResolver == nil || cfg.Authorizer == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, requestID, service.CodeAuthNotReady, "auth middleware dependencies are not ready")
|
||||
return
|
||||
}
|
||||
|
||||
rawToken, ok := extractBearerToken(r.Header.Get("Authorization"))
|
||||
if !ok {
|
||||
emitAuditEvent(r.Context(), cfg.Auditor, service.AuditEvent{
|
||||
EventName: service.EventTokenAuthnFail,
|
||||
RequestID: requestID,
|
||||
Route: r.URL.Path,
|
||||
ResultCode: service.CodeAuthMissingBearer,
|
||||
ClientIP: extractClientIP(r),
|
||||
CreatedAt: cfg.Now(),
|
||||
})
|
||||
writeError(w, http.StatusUnauthorized, requestID, service.CodeAuthMissingBearer, "missing bearer token")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := cfg.Verifier.Verify(r.Context(), rawToken)
|
||||
if err != nil {
|
||||
emitAuditEvent(r.Context(), cfg.Auditor, service.AuditEvent{
|
||||
EventName: service.EventTokenAuthnFail,
|
||||
RequestID: requestID,
|
||||
Route: r.URL.Path,
|
||||
ResultCode: service.CodeAuthInvalidToken,
|
||||
ClientIP: extractClientIP(r),
|
||||
CreatedAt: cfg.Now(),
|
||||
})
|
||||
writeError(w, http.StatusUnauthorized, requestID, service.CodeAuthInvalidToken, "invalid bearer token")
|
||||
return
|
||||
}
|
||||
|
||||
tokenStatus, err := cfg.StatusResolver.Resolve(r.Context(), claims.TokenID)
|
||||
if err != nil || tokenStatus != service.TokenStatusActive {
|
||||
emitAuditEvent(r.Context(), cfg.Auditor, service.AuditEvent{
|
||||
EventName: service.EventTokenAuthnFail,
|
||||
RequestID: requestID,
|
||||
TokenID: claims.TokenID,
|
||||
SubjectID: claims.SubjectID,
|
||||
Route: r.URL.Path,
|
||||
ResultCode: service.CodeAuthTokenInactive,
|
||||
ClientIP: extractClientIP(r),
|
||||
CreatedAt: cfg.Now(),
|
||||
})
|
||||
writeError(w, http.StatusUnauthorized, requestID, service.CodeAuthTokenInactive, "token is inactive")
|
||||
return
|
||||
}
|
||||
|
||||
if !cfg.Authorizer.Authorize(r.URL.Path, r.Method, claims.Scope, claims.Role) {
|
||||
emitAuditEvent(r.Context(), cfg.Auditor, service.AuditEvent{
|
||||
EventName: service.EventTokenAuthzDenied,
|
||||
RequestID: requestID,
|
||||
TokenID: claims.TokenID,
|
||||
SubjectID: claims.SubjectID,
|
||||
Route: r.URL.Path,
|
||||
ResultCode: service.CodeAuthScopeDenied,
|
||||
ClientIP: extractClientIP(r),
|
||||
CreatedAt: cfg.Now(),
|
||||
})
|
||||
writeError(w, http.StatusForbidden, requestID, service.CodeAuthScopeDenied, "scope denied")
|
||||
return
|
||||
}
|
||||
|
||||
principal := model.Principal{
|
||||
RequestID: requestID,
|
||||
TokenID: claims.TokenID,
|
||||
SubjectID: claims.SubjectID,
|
||||
Role: claims.Role,
|
||||
Scope: append([]string(nil), claims.Scope...),
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), principalKey, principal)
|
||||
ctx = context.WithValue(ctx, requestIDKey, requestID)
|
||||
|
||||
emitAuditEvent(ctx, cfg.Auditor, service.AuditEvent{
|
||||
EventName: service.EventTokenAuthnSuccess,
|
||||
RequestID: requestID,
|
||||
TokenID: claims.TokenID,
|
||||
SubjectID: claims.SubjectID,
|
||||
Route: r.URL.Path,
|
||||
ResultCode: "OK",
|
||||
ClientIP: extractClientIP(r),
|
||||
CreatedAt: cfg.Now(),
|
||||
})
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RequestIDFromContext(ctx context.Context) (string, bool) {
|
||||
if ctx == nil {
|
||||
return "", false
|
||||
}
|
||||
value, ok := ctx.Value(requestIDKey).(string)
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func PrincipalFromContext(ctx context.Context) (model.Principal, bool) {
|
||||
if ctx == nil {
|
||||
return model.Principal{}, false
|
||||
}
|
||||
value, ok := ctx.Value(principalKey).(model.Principal)
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (cfg AuthMiddlewareConfig) withDefaults() AuthMiddlewareConfig {
|
||||
if cfg.Now == nil {
|
||||
cfg.Now = defaultNowFunc
|
||||
}
|
||||
if len(cfg.ProtectedPrefixes) == 0 {
|
||||
cfg.ProtectedPrefixes = []string{"/api/v1/supply", "/api/v1/platform"}
|
||||
}
|
||||
if len(cfg.ExcludedPrefixes) == 0 {
|
||||
cfg.ExcludedPrefixes = []string{"/healthz", "/metrics", "/readyz"}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg AuthMiddlewareConfig) shouldProtect(path string) bool {
|
||||
for _, prefix := range cfg.ExcludedPrefixes {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, prefix := range cfg.ProtectedPrefixes {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureRequestID(r *http.Request, now func() time.Time) string {
|
||||
if now == nil {
|
||||
now = defaultNowFunc
|
||||
}
|
||||
if requestID, ok := RequestIDFromContext(r.Context()); ok && requestID != "" {
|
||||
return requestID
|
||||
}
|
||||
requestID := strings.TrimSpace(r.Header.Get(requestIDHeader))
|
||||
if requestID == "" {
|
||||
requestID = fmt.Sprintf("req-%d", now().UnixNano())
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
|
||||
*r = *r.WithContext(ctx)
|
||||
return requestID
|
||||
}
|
||||
|
||||
func extractBearerToken(authHeader string) (string, bool) {
|
||||
const bearerPrefix = "Bearer "
|
||||
if !strings.HasPrefix(authHeader, bearerPrefix) {
|
||||
return "", false
|
||||
}
|
||||
token := strings.TrimSpace(strings.TrimPrefix(authHeader, bearerPrefix))
|
||||
return token, token != ""
|
||||
}
|
||||
|
||||
func emitAuditEvent(ctx context.Context, auditor service.AuditEmitter, event service.AuditEvent) {
|
||||
if auditor == nil {
|
||||
return
|
||||
}
|
||||
_ = auditor.Emit(ctx, event)
|
||||
}
|
||||
|
||||
type errorResponse struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Error errorPayload `json:"error"`
|
||||
}
|
||||
|
||||
type errorPayload struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]any `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, requestID, code, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
payload := errorResponse{
|
||||
RequestID: requestID,
|
||||
Error: errorPayload{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func extractClientIP(r *http.Request) string {
|
||||
xForwardedFor := strings.TrimSpace(r.Header.Get("X-Forwarded-For"))
|
||||
if xForwardedFor != "" {
|
||||
parts := strings.Split(xForwardedFor, ",")
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err == nil {
|
||||
return host
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/model"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
)
|
||||
|
||||
var fixedNow = func() time.Time {
|
||||
return time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
type fakeVerifier struct {
|
||||
token service.VerifiedToken
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeVerifier) Verify(context.Context, string) (service.VerifiedToken, error) {
|
||||
return f.token, f.err
|
||||
}
|
||||
|
||||
type fakeStatusResolver struct {
|
||||
status service.TokenStatus
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeStatusResolver) Resolve(context.Context, string) (service.TokenStatus, error) {
|
||||
return f.status, f.err
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
allowed bool
|
||||
}
|
||||
|
||||
func (f *fakeAuthorizer) Authorize(string, string, []string, string) bool {
|
||||
return f.allowed
|
||||
}
|
||||
|
||||
type fakeAuditor struct {
|
||||
events []service.AuditEvent
|
||||
}
|
||||
|
||||
func (f *fakeAuditor) Emit(_ context.Context, event service.AuditEvent) error {
|
||||
f.events = append(f.events, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestQueryKeyRejectMiddleware(t *testing.T) {
|
||||
auditor := &fakeAuditor{}
|
||||
nextCalled := false
|
||||
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
nextCalled = true
|
||||
})
|
||||
handler := QueryKeyRejectMiddleware(next, auditor, fixedNow)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts?api_key=secret", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if nextCalled {
|
||||
t.Fatalf("next handler should not be called when query key exists")
|
||||
}
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
|
||||
}
|
||||
if got := decodeErrorCode(t, rec); got != service.CodeQueryKeyNotAllowed {
|
||||
t.Fatalf("unexpected error code: got=%s want=%s", got, service.CodeQueryKeyNotAllowed)
|
||||
}
|
||||
if len(auditor.events) != 1 {
|
||||
t.Fatalf("unexpected audit event count: got=%d want=1", len(auditor.events))
|
||||
}
|
||||
if auditor.events[0].EventName != service.EventTokenQueryKeyRejected {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", auditor.events[0].EventName, service.EventTokenQueryKeyRejected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenAuthMiddleware(t *testing.T) {
|
||||
baseToken := service.VerifiedToken{
|
||||
TokenID: "tok-001",
|
||||
SubjectID: "subject-001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
IssuedAt: fixedNow(),
|
||||
ExpiresAt: fixedNow().Add(time.Hour),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
authHeader string
|
||||
verifierErr error
|
||||
status service.TokenStatus
|
||||
statusErr error
|
||||
allowed bool
|
||||
wantStatus int
|
||||
wantErrorCode string
|
||||
wantEvent string
|
||||
wantNext bool
|
||||
}{
|
||||
{
|
||||
name: "missing bearer",
|
||||
path: "/api/v1/supply/packages",
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantErrorCode: service.CodeAuthMissingBearer,
|
||||
wantEvent: service.EventTokenAuthnFail,
|
||||
},
|
||||
{
|
||||
name: "invalid token",
|
||||
path: "/api/v1/supply/packages",
|
||||
authHeader: "Bearer invalid-token",
|
||||
verifierErr: errors.New("invalid signature"),
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantErrorCode: service.CodeAuthInvalidToken,
|
||||
wantEvent: service.EventTokenAuthnFail,
|
||||
},
|
||||
{
|
||||
name: "inactive token",
|
||||
path: "/api/v1/supply/packages",
|
||||
authHeader: "Bearer active-token",
|
||||
status: service.TokenStatusRevoked,
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantErrorCode: service.CodeAuthTokenInactive,
|
||||
wantEvent: service.EventTokenAuthnFail,
|
||||
},
|
||||
{
|
||||
name: "scope denied",
|
||||
path: "/api/v1/supply/packages",
|
||||
authHeader: "Bearer active-token",
|
||||
status: service.TokenStatusActive,
|
||||
allowed: false,
|
||||
wantStatus: http.StatusForbidden,
|
||||
wantErrorCode: service.CodeAuthScopeDenied,
|
||||
wantEvent: service.EventTokenAuthzDenied,
|
||||
},
|
||||
{
|
||||
name: "authn success",
|
||||
path: "/api/v1/supply/packages",
|
||||
authHeader: "Bearer active-token",
|
||||
status: service.TokenStatusActive,
|
||||
allowed: true,
|
||||
wantStatus: http.StatusNoContent,
|
||||
wantEvent: service.EventTokenAuthnSuccess,
|
||||
wantNext: true,
|
||||
},
|
||||
{
|
||||
name: "excluded path bypasses auth",
|
||||
path: "/healthz",
|
||||
wantStatus: http.StatusNoContent,
|
||||
wantNext: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
auditor := &fakeAuditor{}
|
||||
verifier := &fakeVerifier{
|
||||
token: baseToken,
|
||||
err: tc.verifierErr,
|
||||
}
|
||||
resolver := &fakeStatusResolver{
|
||||
status: tc.status,
|
||||
err: tc.statusErr,
|
||||
}
|
||||
authorizer := &fakeAuthorizer{allowed: tc.allowed}
|
||||
nextCalled := false
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextCalled = true
|
||||
if tc.wantNext && strings.HasPrefix(tc.path, "/api/v1/") {
|
||||
principal, ok := PrincipalFromContext(r.Context())
|
||||
if !ok {
|
||||
t.Fatalf("principal should be attached when auth succeeded")
|
||||
}
|
||||
if principal.TokenID != baseToken.TokenID {
|
||||
t.Fatalf("unexpected principal token id: got=%s want=%s", principal.TokenID, baseToken.TokenID)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
handler := TokenAuthMiddleware(AuthMiddlewareConfig{
|
||||
Verifier: verifier,
|
||||
StatusResolver: resolver,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
ProtectedPrefixes: []string{"/api/v1/supply/", "/api/v1/platform/"},
|
||||
ExcludedPrefixes: []string{"/healthz"},
|
||||
Now: fixedNow,
|
||||
})(next)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
|
||||
if tc.authHeader != "" {
|
||||
req.Header.Set("Authorization", tc.authHeader)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.wantStatus {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, tc.wantStatus)
|
||||
}
|
||||
if tc.wantErrorCode != "" {
|
||||
if got := decodeErrorCode(t, rec); got != tc.wantErrorCode {
|
||||
t.Fatalf("unexpected error code: got=%s want=%s", got, tc.wantErrorCode)
|
||||
}
|
||||
}
|
||||
if nextCalled != tc.wantNext {
|
||||
t.Fatalf("unexpected next call state: got=%v want=%v", nextCalled, tc.wantNext)
|
||||
}
|
||||
if tc.wantEvent == "" {
|
||||
return
|
||||
}
|
||||
if len(auditor.events) == 0 {
|
||||
t.Fatalf("audit event should be emitted")
|
||||
}
|
||||
lastEvent := auditor.events[len(auditor.events)-1]
|
||||
if lastEvent.EventName != tc.wantEvent {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", lastEvent.EventName, tc.wantEvent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type errorEnvelope struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func decodeErrorCode(t *testing.T, rec *httptest.ResponseRecorder) string {
|
||||
t.Helper()
|
||||
var envelope errorEnvelope
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
return envelope.Error.Code
|
||||
}
|
||||
35
platform-token-runtime/internal/auth/model/principal.go
Normal file
35
platform-token-runtime/internal/auth/model/principal.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
RoleOwner = "owner"
|
||||
RoleViewer = "viewer"
|
||||
RoleAdmin = "admin"
|
||||
)
|
||||
|
||||
type Principal struct {
|
||||
RequestID string
|
||||
TokenID string
|
||||
SubjectID string
|
||||
Role string
|
||||
Scope []string
|
||||
}
|
||||
|
||||
func (p Principal) HasScope(required string) bool {
|
||||
if required == "" {
|
||||
return true
|
||||
}
|
||||
for _, scope := range p.Scope {
|
||||
if scope == required {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(scope, ":*") {
|
||||
prefix := strings.TrimSuffix(scope, "*")
|
||||
if strings.HasPrefix(required, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
491
platform-token-runtime/internal/auth/service/inmemory_runtime.go
Normal file
491
platform-token-runtime/internal/auth/service/inmemory_runtime.go
Normal file
@@ -0,0 +1,491 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/model"
|
||||
)
|
||||
|
||||
type TokenRecord struct {
|
||||
TokenID string
|
||||
AccessToken string
|
||||
SubjectID string
|
||||
Role string
|
||||
Scope []string
|
||||
IssuedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
Status TokenStatus
|
||||
RequestID string
|
||||
RevokedReason string
|
||||
}
|
||||
|
||||
type IssueTokenInput struct {
|
||||
SubjectID string
|
||||
Role string
|
||||
Scope []string
|
||||
TTL time.Duration
|
||||
RequestID string
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type InMemoryTokenRuntime struct {
|
||||
mu sync.RWMutex
|
||||
now func() time.Time
|
||||
records map[string]*TokenRecord
|
||||
tokenToID map[string]string
|
||||
idempotencyByKey map[string]idempotencyEntry
|
||||
}
|
||||
|
||||
type idempotencyEntry struct {
|
||||
RequestHash string
|
||||
TokenID string
|
||||
}
|
||||
|
||||
func NewInMemoryTokenRuntime(now func() time.Time) *InMemoryTokenRuntime {
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
return &InMemoryTokenRuntime{
|
||||
now: now,
|
||||
records: make(map[string]*TokenRecord),
|
||||
tokenToID: make(map[string]string),
|
||||
idempotencyByKey: make(map[string]idempotencyEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) Issue(_ context.Context, input IssueTokenInput) (TokenRecord, error) {
|
||||
if strings.TrimSpace(input.SubjectID) == "" {
|
||||
return TokenRecord{}, errors.New("subject_id is required")
|
||||
}
|
||||
if strings.TrimSpace(input.Role) == "" {
|
||||
return TokenRecord{}, errors.New("role is required")
|
||||
}
|
||||
if input.TTL <= 0 {
|
||||
return TokenRecord{}, errors.New("ttl must be positive")
|
||||
}
|
||||
if len(input.Scope) == 0 {
|
||||
return TokenRecord{}, errors.New("scope must not be empty")
|
||||
}
|
||||
idempotencyKey := strings.TrimSpace(input.IdempotencyKey)
|
||||
requestHash := hashIssueInput(input)
|
||||
|
||||
issuedAt := r.now()
|
||||
tokenID, err := generateTokenID()
|
||||
if err != nil {
|
||||
return TokenRecord{}, err
|
||||
}
|
||||
accessToken, err := generateAccessToken()
|
||||
if err != nil {
|
||||
return TokenRecord{}, err
|
||||
}
|
||||
|
||||
record := TokenRecord{
|
||||
TokenID: tokenID,
|
||||
AccessToken: accessToken,
|
||||
SubjectID: input.SubjectID,
|
||||
Role: input.Role,
|
||||
Scope: append([]string(nil), input.Scope...),
|
||||
IssuedAt: issuedAt,
|
||||
ExpiresAt: issuedAt.Add(input.TTL),
|
||||
Status: TokenStatusActive,
|
||||
RequestID: input.RequestID,
|
||||
RevokedReason: "",
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
if idempotencyKey != "" {
|
||||
entry, ok := r.idempotencyByKey[idempotencyKey]
|
||||
if ok {
|
||||
if entry.RequestHash != requestHash {
|
||||
r.mu.Unlock()
|
||||
return TokenRecord{}, errors.New("idempotency key payload mismatch")
|
||||
}
|
||||
existing, exists := r.records[entry.TokenID]
|
||||
if exists {
|
||||
r.mu.Unlock()
|
||||
return cloneRecord(*existing), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
r.records[tokenID] = &record
|
||||
r.tokenToID[accessToken] = tokenID
|
||||
if idempotencyKey != "" {
|
||||
r.idempotencyByKey[idempotencyKey] = idempotencyEntry{
|
||||
RequestHash: requestHash,
|
||||
TokenID: tokenID,
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) Refresh(_ context.Context, tokenID string, ttl time.Duration) (TokenRecord, error) {
|
||||
if ttl <= 0 {
|
||||
return TokenRecord{}, errors.New("ttl must be positive")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
record, ok := r.records[tokenID]
|
||||
if !ok {
|
||||
return TokenRecord{}, errors.New("token not found")
|
||||
}
|
||||
r.applyExpiry(record)
|
||||
if record.Status != TokenStatusActive {
|
||||
return TokenRecord{}, errors.New("token is not active")
|
||||
}
|
||||
|
||||
record.ExpiresAt = r.now().Add(ttl)
|
||||
return cloneRecord(*record), nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) Revoke(_ context.Context, tokenID, reason string) (TokenRecord, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
record, ok := r.records[tokenID]
|
||||
if !ok {
|
||||
return TokenRecord{}, errors.New("token not found")
|
||||
}
|
||||
r.applyExpiry(record)
|
||||
record.Status = TokenStatusRevoked
|
||||
record.RevokedReason = strings.TrimSpace(reason)
|
||||
return cloneRecord(*record), nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) Introspect(_ context.Context, accessToken string) (TokenRecord, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
tokenID, ok := r.tokenToID[accessToken]
|
||||
if !ok {
|
||||
return TokenRecord{}, errors.New("token not found")
|
||||
}
|
||||
record := r.records[tokenID]
|
||||
r.applyExpiry(record)
|
||||
return cloneRecord(*record), nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) Lookup(_ context.Context, tokenID string) (TokenRecord, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
record, ok := r.records[tokenID]
|
||||
if !ok {
|
||||
return TokenRecord{}, errors.New("token not found")
|
||||
}
|
||||
r.applyExpiry(record)
|
||||
return cloneRecord(*record), nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) Verify(_ context.Context, rawToken string) (VerifiedToken, error) {
|
||||
r.mu.RLock()
|
||||
tokenID, ok := r.tokenToID[rawToken]
|
||||
if !ok {
|
||||
r.mu.RUnlock()
|
||||
return VerifiedToken{}, NewAuthError(CodeAuthInvalidToken, errors.New("token not found"))
|
||||
}
|
||||
record, ok := r.records[tokenID]
|
||||
if !ok {
|
||||
r.mu.RUnlock()
|
||||
return VerifiedToken{}, NewAuthError(CodeAuthInvalidToken, errors.New("token record not found"))
|
||||
}
|
||||
claims := VerifiedToken{
|
||||
TokenID: record.TokenID,
|
||||
SubjectID: record.SubjectID,
|
||||
Role: record.Role,
|
||||
Scope: append([]string(nil), record.Scope...),
|
||||
IssuedAt: record.IssuedAt,
|
||||
ExpiresAt: record.ExpiresAt,
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) Resolve(_ context.Context, tokenID string) (TokenStatus, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
record, ok := r.records[tokenID]
|
||||
if !ok {
|
||||
return "", NewAuthError(CodeAuthInvalidToken, errors.New("token not found"))
|
||||
}
|
||||
r.applyExpiry(record)
|
||||
return record.Status, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) TokenCount() int {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.records)
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) IssueAndAudit(ctx context.Context, input IssueTokenInput, auditor AuditEmitter) (TokenRecord, error) {
|
||||
record, err := r.Issue(ctx, input)
|
||||
if err != nil {
|
||||
emitAudit(auditor, AuditEvent{
|
||||
EventName: EventTokenIssueFail,
|
||||
RequestID: input.RequestID,
|
||||
SubjectID: input.SubjectID,
|
||||
Route: "/api/v1/platform/tokens/issue",
|
||||
ResultCode: "ISSUE_FAILED",
|
||||
}, r.now)
|
||||
return TokenRecord{}, err
|
||||
}
|
||||
emitAudit(auditor, AuditEvent{
|
||||
EventName: EventTokenIssueSuccess,
|
||||
RequestID: input.RequestID,
|
||||
TokenID: record.TokenID,
|
||||
SubjectID: record.SubjectID,
|
||||
Route: "/api/v1/platform/tokens/issue",
|
||||
ResultCode: "OK",
|
||||
}, r.now)
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) RevokeAndAudit(ctx context.Context, tokenID, reason, requestID, subjectID string, auditor AuditEmitter) (TokenRecord, error) {
|
||||
record, err := r.Revoke(ctx, tokenID, reason)
|
||||
if err != nil {
|
||||
emitAudit(auditor, AuditEvent{
|
||||
EventName: EventTokenRevokeFail,
|
||||
RequestID: requestID,
|
||||
TokenID: tokenID,
|
||||
SubjectID: subjectID,
|
||||
Route: "/api/v1/platform/tokens/revoke",
|
||||
ResultCode: "REVOKE_FAILED",
|
||||
}, r.now)
|
||||
return TokenRecord{}, err
|
||||
}
|
||||
emitAudit(auditor, AuditEvent{
|
||||
EventName: EventTokenRevokeSuccess,
|
||||
RequestID: requestID,
|
||||
TokenID: record.TokenID,
|
||||
SubjectID: record.SubjectID,
|
||||
Route: "/api/v1/platform/tokens/revoke",
|
||||
ResultCode: "OK",
|
||||
}, r.now)
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryTokenRuntime) applyExpiry(record *TokenRecord) {
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
if record.Status == TokenStatusActive && !record.ExpiresAt.IsZero() && !r.now().Before(record.ExpiresAt) {
|
||||
record.Status = TokenStatusExpired
|
||||
}
|
||||
}
|
||||
|
||||
func cloneRecord(record TokenRecord) TokenRecord {
|
||||
record.Scope = append([]string(nil), record.Scope...)
|
||||
return record
|
||||
}
|
||||
|
||||
func hashIssueInput(input IssueTokenInput) string {
|
||||
scope := append([]string(nil), input.Scope...)
|
||||
sort.Strings(scope)
|
||||
joined := strings.Join(scope, ",")
|
||||
data := strings.TrimSpace(input.SubjectID) + "|" +
|
||||
strings.TrimSpace(input.Role) + "|" +
|
||||
joined + "|" +
|
||||
input.TTL.String()
|
||||
sum := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func generateAccessToken() (string, error) {
|
||||
var entropy [16]byte
|
||||
if _, err := rand.Read(entropy[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "ptk_" + hex.EncodeToString(entropy[:]), nil
|
||||
}
|
||||
|
||||
func generateTokenID() (string, error) {
|
||||
var entropy [8]byte
|
||||
if _, err := rand.Read(entropy[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "tok_" + hex.EncodeToString(entropy[:]), nil
|
||||
}
|
||||
|
||||
type ScopeRoleAuthorizer struct{}
|
||||
|
||||
func NewScopeRoleAuthorizer() *ScopeRoleAuthorizer {
|
||||
return &ScopeRoleAuthorizer{}
|
||||
}
|
||||
|
||||
func (a *ScopeRoleAuthorizer) Authorize(path, method string, scopes []string, role string) bool {
|
||||
if role == model.RoleAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
requiredScope := requiredScopeForRoute(path, method)
|
||||
if requiredScope == "" {
|
||||
return true
|
||||
}
|
||||
return hasScope(scopes, requiredScope)
|
||||
}
|
||||
|
||||
func requiredScopeForRoute(path, method string) string {
|
||||
if path == "/api/v1/supply" || strings.HasPrefix(path, "/api/v1/supply/") {
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||
return "supply:read"
|
||||
default:
|
||||
return "supply:write"
|
||||
}
|
||||
}
|
||||
if path == "/api/v1/platform" || strings.HasPrefix(path, "/api/v1/platform/") {
|
||||
return "platform:admin"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasScope(scopes []string, required string) bool {
|
||||
for _, scope := range scopes {
|
||||
if scope == required {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(scope, ":*") {
|
||||
prefix := strings.TrimSuffix(scope, "*")
|
||||
if strings.HasPrefix(required, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MemoryAuditEmitter struct {
|
||||
mu sync.RWMutex
|
||||
events []AuditEvent
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewMemoryAuditEmitter() *MemoryAuditEmitter {
|
||||
return &MemoryAuditEmitter{now: time.Now}
|
||||
}
|
||||
|
||||
func (e *MemoryAuditEmitter) Emit(_ context.Context, event AuditEvent) error {
|
||||
if event.EventID == "" {
|
||||
eventID, err := generateEventID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event.EventID = eventID
|
||||
}
|
||||
if event.CreatedAt.IsZero() {
|
||||
event.CreatedAt = e.now()
|
||||
}
|
||||
e.mu.Lock()
|
||||
e.events = append(e.events, event)
|
||||
e.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *MemoryAuditEmitter) Events() []AuditEvent {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
copied := make([]AuditEvent, len(e.events))
|
||||
copy(copied, e.events)
|
||||
return copied
|
||||
}
|
||||
|
||||
func (e *MemoryAuditEmitter) QueryEvents(_ context.Context, filter AuditEventFilter) ([]AuditEvent, error) {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
limit := filter.Limit
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
if limit > 500 {
|
||||
limit = 500
|
||||
}
|
||||
|
||||
result := make([]AuditEvent, 0, minInt(limit, len(e.events)))
|
||||
for idx := len(e.events) - 1; idx >= 0; idx-- {
|
||||
ev := e.events[idx]
|
||||
if !matchAuditFilter(ev, filter) {
|
||||
continue
|
||||
}
|
||||
result = append(result, ev)
|
||||
if len(result) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间正序返回,便于前端/审计系统展示时间线。
|
||||
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *MemoryAuditEmitter) LastEvent() (AuditEvent, bool) {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
if len(e.events) == 0 {
|
||||
return AuditEvent{}, false
|
||||
}
|
||||
return e.events[len(e.events)-1], true
|
||||
}
|
||||
|
||||
func emitAudit(emitter AuditEmitter, event AuditEvent, now func() time.Time) {
|
||||
if emitter == nil {
|
||||
return
|
||||
}
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
if event.CreatedAt.IsZero() {
|
||||
event.CreatedAt = now()
|
||||
}
|
||||
_ = emitter.Emit(context.Background(), event)
|
||||
}
|
||||
|
||||
func matchAuditFilter(ev AuditEvent, filter AuditEventFilter) bool {
|
||||
if filter.RequestID != "" && ev.RequestID != filter.RequestID {
|
||||
return false
|
||||
}
|
||||
if filter.TokenID != "" && ev.TokenID != filter.TokenID {
|
||||
return false
|
||||
}
|
||||
if filter.SubjectID != "" && ev.SubjectID != filter.SubjectID {
|
||||
return false
|
||||
}
|
||||
if filter.EventName != "" && ev.EventName != filter.EventName {
|
||||
return false
|
||||
}
|
||||
if filter.ResultCode != "" && ev.ResultCode != filter.ResultCode {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func generateEventID() (string, error) {
|
||||
var entropy [8]byte
|
||||
if _, err := rand.Read(entropy[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "evt_" + hex.EncodeToString(entropy[:]), nil
|
||||
}
|
||||
127
platform-token-runtime/internal/auth/service/token_verifier.go
Normal file
127
platform-token-runtime/internal/auth/service/token_verifier.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CodeAuthMissingBearer = "AUTH_MISSING_BEARER"
|
||||
CodeQueryKeyNotAllowed = "QUERY_KEY_NOT_ALLOWED"
|
||||
CodeAuthInvalidToken = "AUTH_INVALID_TOKEN"
|
||||
CodeAuthTokenInactive = "AUTH_TOKEN_INACTIVE"
|
||||
CodeAuthScopeDenied = "AUTH_SCOPE_DENIED"
|
||||
CodeAuthNotReady = "AUTH_NOT_READY"
|
||||
)
|
||||
|
||||
const (
|
||||
EventTokenAuthnSuccess = "token.authn.success"
|
||||
EventTokenAuthnFail = "token.authn.fail"
|
||||
EventTokenAuthzDenied = "token.authz.denied"
|
||||
EventTokenQueryKeyRejected = "token.query_key.rejected"
|
||||
EventTokenIssueSuccess = "token.issue.success"
|
||||
EventTokenIssueFail = "token.issue.fail"
|
||||
EventTokenIntrospectSuccess = "token.introspect.success"
|
||||
EventTokenIntrospectFail = "token.introspect.fail"
|
||||
EventTokenRefreshSuccess = "token.refresh.success"
|
||||
EventTokenRefreshFail = "token.refresh.fail"
|
||||
EventTokenRevokeSuccess = "token.revoke.success"
|
||||
EventTokenRevokeFail = "token.revoke.fail"
|
||||
)
|
||||
|
||||
type TokenStatus string
|
||||
|
||||
const (
|
||||
TokenStatusActive TokenStatus = "active"
|
||||
TokenStatusRevoked TokenStatus = "revoked"
|
||||
TokenStatusExpired TokenStatus = "expired"
|
||||
)
|
||||
|
||||
type VerifiedToken struct {
|
||||
TokenID string
|
||||
SubjectID string
|
||||
Role string
|
||||
Scope []string
|
||||
IssuedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
NotBefore time.Time
|
||||
Issuer string
|
||||
Audience string
|
||||
}
|
||||
|
||||
type TokenVerifier interface {
|
||||
Verify(ctx context.Context, rawToken string) (VerifiedToken, error)
|
||||
}
|
||||
|
||||
type TokenStatusResolver interface {
|
||||
Resolve(ctx context.Context, tokenID string) (TokenStatus, error)
|
||||
}
|
||||
|
||||
type RouteAuthorizer interface {
|
||||
Authorize(path, method string, scopes []string, role string) bool
|
||||
}
|
||||
|
||||
type AuditEvent struct {
|
||||
EventID string
|
||||
EventName string
|
||||
RequestID string
|
||||
TokenID string
|
||||
SubjectID string
|
||||
Route string
|
||||
ResultCode string
|
||||
ClientIP string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type AuditEmitter interface {
|
||||
Emit(ctx context.Context, event AuditEvent) error
|
||||
}
|
||||
|
||||
type AuditEventFilter struct {
|
||||
RequestID string
|
||||
TokenID string
|
||||
SubjectID string
|
||||
EventName string
|
||||
ResultCode string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type AuditEventQuerier interface {
|
||||
QueryEvents(ctx context.Context, filter AuditEventFilter) ([]AuditEvent, error)
|
||||
}
|
||||
|
||||
type AuthError struct {
|
||||
Code string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *AuthError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Cause == nil {
|
||||
return e.Code
|
||||
}
|
||||
return fmt.Sprintf("%s: %v", e.Code, e.Cause)
|
||||
}
|
||||
|
||||
func (e *AuthError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func NewAuthError(code string, cause error) *AuthError {
|
||||
return &AuthError{Code: code, Cause: cause}
|
||||
}
|
||||
|
||||
func IsAuthCode(err error, code string) bool {
|
||||
var authErr *AuthError
|
||||
if !errors.As(err, &authErr) {
|
||||
return false
|
||||
}
|
||||
return authErr.Code == code
|
||||
}
|
||||
437
platform-token-runtime/internal/httpapi/token_api.go
Normal file
437
platform-token-runtime/internal/httpapi/token_api.go
Normal file
@@ -0,0 +1,437 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/model"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenBasePath = "/api/v1/platform/tokens"
|
||||
)
|
||||
|
||||
type Runtime interface {
|
||||
IssueAndAudit(ctx context.Context, input service.IssueTokenInput, auditor service.AuditEmitter) (service.TokenRecord, error)
|
||||
Refresh(ctx context.Context, tokenID string, ttl time.Duration) (service.TokenRecord, error)
|
||||
RevokeAndAudit(ctx context.Context, tokenID, reason, requestID, subjectID string, auditor service.AuditEmitter) (service.TokenRecord, error)
|
||||
Introspect(ctx context.Context, accessToken string) (service.TokenRecord, error)
|
||||
Lookup(ctx context.Context, tokenID string) (service.TokenRecord, error)
|
||||
}
|
||||
|
||||
type TokenAPI struct {
|
||||
runtime Runtime
|
||||
auditor service.AuditEmitter
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewTokenAPI(runtime Runtime, auditor service.AuditEmitter, now func() time.Time) *TokenAPI {
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
return &TokenAPI{runtime: runtime, auditor: auditor, now: now}
|
||||
}
|
||||
|
||||
func (a *TokenAPI) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc(tokenBasePath+"/issue", a.handleIssue)
|
||||
mux.HandleFunc(tokenBasePath+"/introspect", a.handleIntrospect)
|
||||
mux.HandleFunc(tokenBasePath+"/audit-events", a.handleAuditEvents)
|
||||
mux.HandleFunc(tokenBasePath+"/", a.handleTokenAction)
|
||||
}
|
||||
|
||||
func (a *TokenAPI) handleTokenAction(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.URL.Path, tokenBasePath+"/") {
|
||||
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
|
||||
return
|
||||
}
|
||||
tail := strings.TrimPrefix(r.URL.Path, tokenBasePath+"/")
|
||||
parts := strings.Split(tail, "/")
|
||||
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" {
|
||||
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
|
||||
return
|
||||
}
|
||||
tokenID := strings.TrimSpace(parts[0])
|
||||
action := strings.TrimSpace(parts[1])
|
||||
|
||||
switch action {
|
||||
case "refresh":
|
||||
a.handleRefresh(w, r, tokenID)
|
||||
case "revoke":
|
||||
a.handleRevoke(w, r, tokenID)
|
||||
default:
|
||||
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
|
||||
}
|
||||
}
|
||||
|
||||
type issueRequest struct {
|
||||
SubjectID string `json:"subject_id"`
|
||||
Role string `json:"role"`
|
||||
TTLSeconds int64 `json:"ttl_seconds"`
|
||||
Scope []string `json:"scope"`
|
||||
}
|
||||
|
||||
type refreshRequest struct {
|
||||
TTLSeconds int64 `json:"ttl_seconds"`
|
||||
}
|
||||
|
||||
type revokeRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type introspectRequest struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type errorEnvelope struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (a *TokenAPI) handleIssue(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
|
||||
return
|
||||
}
|
||||
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
|
||||
idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
||||
if requestID == "" || idempotencyKey == "" {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id or Idempotency-Key")
|
||||
return
|
||||
}
|
||||
|
||||
var req issueRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
|
||||
return
|
||||
}
|
||||
if err := validateIssueRequest(req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
record, err := a.runtime.IssueAndAudit(r.Context(), service.IssueTokenInput{
|
||||
SubjectID: req.SubjectID,
|
||||
Role: req.Role,
|
||||
Scope: req.Scope,
|
||||
TTL: time.Duration(req.TTLSeconds) * time.Second,
|
||||
RequestID: requestID,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
}, a.auditor)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "idempotency key payload mismatch") {
|
||||
writeError(w, http.StatusConflict, "IDEMPOTENCY_CONFLICT", "idempotency key payload mismatch")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusUnprocessableEntity, "ISSUE_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]any{
|
||||
"request_id": requestID,
|
||||
"data": map[string]any{
|
||||
"token_id": record.TokenID,
|
||||
"access_token": record.AccessToken,
|
||||
"issued_at": record.IssuedAt,
|
||||
"expires_at": record.ExpiresAt,
|
||||
"status": record.Status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *TokenAPI) handleRefresh(w http.ResponseWriter, r *http.Request, tokenID string) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
|
||||
return
|
||||
}
|
||||
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
|
||||
idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
||||
if requestID == "" || idempotencyKey == "" {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id or Idempotency-Key")
|
||||
return
|
||||
}
|
||||
|
||||
var req refreshRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
|
||||
return
|
||||
}
|
||||
if req.TTLSeconds < 60 {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "ttl_seconds must be >= 60")
|
||||
return
|
||||
}
|
||||
|
||||
before, err := a.runtime.Lookup(r.Context(), tokenID)
|
||||
if err != nil {
|
||||
before = service.TokenRecord{}
|
||||
}
|
||||
|
||||
record, err := a.runtime.Refresh(r.Context(), tokenID, time.Duration(req.TTLSeconds)*time.Second)
|
||||
if err != nil {
|
||||
status, code := mapRuntimeError(err)
|
||||
writeError(w, status, code, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if a.auditor != nil {
|
||||
_ = a.auditor.Emit(r.Context(), service.AuditEvent{
|
||||
EventName: service.EventTokenRefreshSuccess,
|
||||
RequestID: requestID,
|
||||
TokenID: record.TokenID,
|
||||
SubjectID: record.SubjectID,
|
||||
Route: tokenBasePath + "/" + tokenID + "/refresh",
|
||||
ResultCode: "OK",
|
||||
CreatedAt: a.now(),
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"request_id": requestID,
|
||||
"data": map[string]any{
|
||||
"token_id": record.TokenID,
|
||||
"previous_expires_at": before.ExpiresAt,
|
||||
"expires_at": record.ExpiresAt,
|
||||
"status": record.Status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *TokenAPI) handleRevoke(w http.ResponseWriter, r *http.Request, tokenID string) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
|
||||
return
|
||||
}
|
||||
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
|
||||
idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
||||
if requestID == "" || idempotencyKey == "" {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id or Idempotency-Key")
|
||||
return
|
||||
}
|
||||
|
||||
var req revokeRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Reason) == "" {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "reason is required")
|
||||
return
|
||||
}
|
||||
|
||||
introspected, err := a.runtime.Lookup(r.Context(), tokenID)
|
||||
subjectID := ""
|
||||
if err == nil {
|
||||
subjectID = introspected.SubjectID
|
||||
}
|
||||
|
||||
record, err := a.runtime.RevokeAndAudit(r.Context(), tokenID, req.Reason, requestID, subjectID, a.auditor)
|
||||
if err != nil {
|
||||
status, code := mapRuntimeError(err)
|
||||
writeError(w, status, code, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"request_id": requestID,
|
||||
"data": map[string]any{
|
||||
"token_id": record.TokenID,
|
||||
"status": record.Status,
|
||||
"revoked_at": a.now(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *TokenAPI) handleIntrospect(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
|
||||
return
|
||||
}
|
||||
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
|
||||
if requestID == "" {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id")
|
||||
return
|
||||
}
|
||||
|
||||
var req introspectRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Token) == "" {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "token is required")
|
||||
return
|
||||
}
|
||||
|
||||
record, err := a.runtime.Introspect(r.Context(), req.Token)
|
||||
if err != nil {
|
||||
if a.auditor != nil {
|
||||
_ = a.auditor.Emit(r.Context(), service.AuditEvent{
|
||||
EventName: service.EventTokenIntrospectFail,
|
||||
RequestID: requestID,
|
||||
Route: tokenBasePath + "/introspect",
|
||||
ResultCode: "INVALID_TOKEN",
|
||||
CreatedAt: a.now(),
|
||||
})
|
||||
}
|
||||
writeError(w, http.StatusUnprocessableEntity, "TOKEN_INVALID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if a.auditor != nil {
|
||||
_ = a.auditor.Emit(r.Context(), service.AuditEvent{
|
||||
EventName: service.EventTokenIntrospectSuccess,
|
||||
RequestID: requestID,
|
||||
TokenID: record.TokenID,
|
||||
SubjectID: record.SubjectID,
|
||||
Route: tokenBasePath + "/introspect",
|
||||
ResultCode: "OK",
|
||||
CreatedAt: a.now(),
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"request_id": requestID,
|
||||
"data": map[string]any{
|
||||
"token_id": record.TokenID,
|
||||
"subject_id": record.SubjectID,
|
||||
"role": record.Role,
|
||||
"status": record.Status,
|
||||
"scope": record.Scope,
|
||||
"issued_at": record.IssuedAt,
|
||||
"expires_at": record.ExpiresAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *TokenAPI) handleAuditEvents(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
|
||||
return
|
||||
}
|
||||
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
|
||||
if requestID == "" {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id")
|
||||
return
|
||||
}
|
||||
|
||||
querier, ok := a.auditor.(service.AuditEventQuerier)
|
||||
if !ok {
|
||||
writeError(w, http.StatusNotImplemented, "AUDIT_QUERY_NOT_READY", "audit query capability is not available")
|
||||
return
|
||||
}
|
||||
|
||||
limit := parseLimit(r.URL.Query().Get("limit"))
|
||||
filter := service.AuditEventFilter{
|
||||
RequestID: strings.TrimSpace(r.URL.Query().Get("request_id")),
|
||||
TokenID: strings.TrimSpace(r.URL.Query().Get("token_id")),
|
||||
SubjectID: strings.TrimSpace(r.URL.Query().Get("subject_id")),
|
||||
EventName: strings.TrimSpace(r.URL.Query().Get("event_name")),
|
||||
ResultCode: strings.TrimSpace(r.URL.Query().Get("result_code")),
|
||||
Limit: limit,
|
||||
}
|
||||
events, err := querier.QueryEvents(r.Context(), filter)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "AUDIT_QUERY_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]any, 0, len(events))
|
||||
for _, ev := range events {
|
||||
items = append(items, map[string]any{
|
||||
"event_id": ev.EventID,
|
||||
"event_name": ev.EventName,
|
||||
"request_id": ev.RequestID,
|
||||
"token_id": ev.TokenID,
|
||||
"subject_id": ev.SubjectID,
|
||||
"route": ev.Route,
|
||||
"result_code": ev.ResultCode,
|
||||
"client_ip": ev.ClientIP,
|
||||
"created_at": ev.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"request_id": requestID,
|
||||
"data": map[string]any{
|
||||
"total": len(items),
|
||||
"items": items,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func validateIssueRequest(req issueRequest) error {
|
||||
if strings.TrimSpace(req.SubjectID) == "" {
|
||||
return errors.New("subject_id is required")
|
||||
}
|
||||
if req.TTLSeconds < 60 {
|
||||
return errors.New("ttl_seconds must be >= 60")
|
||||
}
|
||||
if len(req.Scope) == 0 {
|
||||
return errors.New("scope is required")
|
||||
}
|
||||
switch req.Role {
|
||||
case model.RoleOwner, model.RoleViewer, model.RoleAdmin:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported role: %s", req.Role)
|
||||
}
|
||||
}
|
||||
|
||||
func mapRuntimeError(err error) (int, string) {
|
||||
msg := err.Error()
|
||||
switch {
|
||||
case strings.Contains(msg, "not found"):
|
||||
return http.StatusNotFound, "TOKEN_NOT_FOUND"
|
||||
case strings.Contains(msg, "not active"):
|
||||
return http.StatusConflict, "TOKEN_NOT_ACTIVE"
|
||||
case strings.Contains(msg, "idempotency key payload mismatch"):
|
||||
return http.StatusConflict, "IDEMPOTENCY_CONFLICT"
|
||||
default:
|
||||
return http.StatusUnprocessableEntity, "BUSINESS_ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
func decodeJSON(r *http.Request, out any) error {
|
||||
defer r.Body.Close()
|
||||
dec := json.NewDecoder(r.Body)
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, code, message string) {
|
||||
var env errorEnvelope
|
||||
env.Error.Code = code
|
||||
env.Error.Message = message
|
||||
writeJSON(w, status, env)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func parseLimit(raw string) int {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return 100
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
if err != nil || n <= 0 {
|
||||
return 100
|
||||
}
|
||||
if n > 500 {
|
||||
return 500
|
||||
}
|
||||
return n
|
||||
}
|
||||
269
platform-token-runtime/internal/httpapi/token_api_test.go
Normal file
269
platform-token-runtime/internal/httpapi/token_api_test.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
)
|
||||
|
||||
func TestTokenAPIIssueAndIntrospect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runtime := service.NewInMemoryTokenRuntime(nil)
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
api := NewTokenAPI(runtime, auditor, func() time.Time {
|
||||
return time.Date(2026, 3, 30, 15, 50, 0, 0, time.UTC)
|
||||
})
|
||||
mux := http.NewServeMux()
|
||||
api.Register(mux)
|
||||
|
||||
issueBody := map[string]any{
|
||||
"subject_id": "2001",
|
||||
"role": "owner",
|
||||
"ttl_seconds": 600,
|
||||
"scope": []string{"supply:*"},
|
||||
}
|
||||
issueReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, issueBody))
|
||||
issueReq.Header.Set("X-Request-Id", "req-api-001")
|
||||
issueReq.Header.Set("Idempotency-Key", "idem-api-001")
|
||||
issueRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(issueRec, issueReq)
|
||||
|
||||
if issueRec.Code != http.StatusCreated {
|
||||
t.Fatalf("unexpected issue status: got=%d want=%d body=%s", issueRec.Code, http.StatusCreated, issueRec.Body.String())
|
||||
}
|
||||
issueResp := decodeMap(t, issueRec.Body.Bytes())
|
||||
data := issueResp["data"].(map[string]any)
|
||||
accessToken := data["access_token"].(string)
|
||||
if accessToken == "" {
|
||||
t.Fatalf("access_token should not be empty")
|
||||
}
|
||||
|
||||
introspectBody := map[string]any{"token": accessToken}
|
||||
introReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/introspect", mustJSON(t, introspectBody))
|
||||
introReq.Header.Set("X-Request-Id", "req-api-002")
|
||||
introRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(introRec, introReq)
|
||||
|
||||
if introRec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected introspect status: got=%d want=%d body=%s", introRec.Code, http.StatusOK, introRec.Body.String())
|
||||
}
|
||||
introResp := decodeMap(t, introRec.Body.Bytes())
|
||||
introData := introResp["data"].(map[string]any)
|
||||
if introData["role"].(string) != "owner" {
|
||||
t.Fatalf("unexpected role: got=%s want=owner", introData["role"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenAPIIssueIdempotencyConflict(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runtime := service.NewInMemoryTokenRuntime(nil)
|
||||
api := NewTokenAPI(runtime, service.NewMemoryAuditEmitter(), time.Now)
|
||||
mux := http.NewServeMux()
|
||||
api.Register(mux)
|
||||
|
||||
firstBody := map[string]any{
|
||||
"subject_id": "2001",
|
||||
"role": "owner",
|
||||
"ttl_seconds": 600,
|
||||
"scope": []string{"supply:*"},
|
||||
}
|
||||
secondBody := map[string]any{
|
||||
"subject_id": "2001",
|
||||
"role": "owner",
|
||||
"ttl_seconds": 600,
|
||||
"scope": []string{"supply:read"},
|
||||
}
|
||||
|
||||
firstReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, firstBody))
|
||||
firstReq.Header.Set("X-Request-Id", "req-api-003-1")
|
||||
firstReq.Header.Set("Idempotency-Key", "idem-api-003")
|
||||
firstRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(firstRec, firstReq)
|
||||
if firstRec.Code != http.StatusCreated {
|
||||
t.Fatalf("first issue should succeed: code=%d body=%s", firstRec.Code, firstRec.Body.String())
|
||||
}
|
||||
|
||||
secondReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, secondBody))
|
||||
secondReq.Header.Set("X-Request-Id", "req-api-003-2")
|
||||
secondReq.Header.Set("Idempotency-Key", "idem-api-003")
|
||||
secondRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(secondRec, secondReq)
|
||||
if secondRec.Code != http.StatusConflict {
|
||||
t.Fatalf("expected idempotency conflict: code=%d body=%s", secondRec.Code, secondRec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenAPIRefreshAndRevoke(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Date(2026, 3, 30, 16, 0, 0, 0, time.UTC)
|
||||
runtime := service.NewInMemoryTokenRuntime(func() time.Time { return now })
|
||||
api := NewTokenAPI(runtime, service.NewMemoryAuditEmitter(), func() time.Time { return now })
|
||||
mux := http.NewServeMux()
|
||||
api.Register(mux)
|
||||
|
||||
issueReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, map[string]any{
|
||||
"subject_id": "2008",
|
||||
"role": "owner",
|
||||
"ttl_seconds": 120,
|
||||
"scope": []string{"supply:*"},
|
||||
}))
|
||||
issueReq.Header.Set("X-Request-Id", "req-api-004-1")
|
||||
issueReq.Header.Set("Idempotency-Key", "idem-api-004")
|
||||
issueRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(issueRec, issueReq)
|
||||
if issueRec.Code != http.StatusCreated {
|
||||
t.Fatalf("issue failed: code=%d body=%s", issueRec.Code, issueRec.Body.String())
|
||||
}
|
||||
issued := decodeMap(t, issueRec.Body.Bytes())
|
||||
issuedData := issued["data"].(map[string]any)
|
||||
tokenID := issuedData["token_id"].(string)
|
||||
|
||||
now = now.Add(10 * time.Second)
|
||||
refreshReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/"+tokenID+"/refresh", mustJSON(t, map[string]any{"ttl_seconds": 300}))
|
||||
refreshReq.Header.Set("X-Request-Id", "req-api-004-2")
|
||||
refreshReq.Header.Set("Idempotency-Key", "idem-api-004-r")
|
||||
refreshRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(refreshRec, refreshReq)
|
||||
if refreshRec.Code != http.StatusOK {
|
||||
t.Fatalf("refresh failed: code=%d body=%s", refreshRec.Code, refreshRec.Body.String())
|
||||
}
|
||||
refreshResp := decodeMap(t, refreshRec.Body.Bytes())
|
||||
refreshData := refreshResp["data"].(map[string]any)
|
||||
if refreshData["previous_expires_at"] == nil {
|
||||
t.Fatalf("previous_expires_at must not be nil")
|
||||
}
|
||||
|
||||
revokeReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/"+tokenID+"/revoke", mustJSON(t, map[string]any{"reason": "operator_request"}))
|
||||
revokeReq.Header.Set("X-Request-Id", "req-api-004-3")
|
||||
revokeReq.Header.Set("Idempotency-Key", "idem-api-004-v")
|
||||
revokeRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(revokeRec, revokeReq)
|
||||
if revokeRec.Code != http.StatusOK {
|
||||
t.Fatalf("revoke failed: code=%d body=%s", revokeRec.Code, revokeRec.Body.String())
|
||||
}
|
||||
revokeResp := decodeMap(t, revokeRec.Body.Bytes())
|
||||
revokeData := revokeResp["data"].(map[string]any)
|
||||
if revokeData["status"].(string) != "revoked" {
|
||||
t.Fatalf("unexpected status after revoke: got=%s", revokeData["status"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenAPIMissingHeaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runtime := service.NewInMemoryTokenRuntime(nil)
|
||||
api := NewTokenAPI(runtime, service.NewMemoryAuditEmitter(), time.Now)
|
||||
mux := http.NewServeMux()
|
||||
api.Register(mux)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, map[string]any{
|
||||
"subject_id": "2001",
|
||||
"role": "owner",
|
||||
"ttl_seconds": 120,
|
||||
"scope": []string{"supply:*"},
|
||||
}))
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("missing headers must be rejected: code=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenAPIAuditEventsQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runtime := service.NewInMemoryTokenRuntime(nil)
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
api := NewTokenAPI(runtime, auditor, time.Now)
|
||||
mux := http.NewServeMux()
|
||||
api.Register(mux)
|
||||
|
||||
issueReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, map[string]any{
|
||||
"subject_id": "2010",
|
||||
"role": "owner",
|
||||
"ttl_seconds": 300,
|
||||
"scope": []string{"supply:*"},
|
||||
}))
|
||||
issueReq.Header.Set("X-Request-Id", "req-audit-query-1")
|
||||
issueReq.Header.Set("Idempotency-Key", "idem-audit-query-1")
|
||||
issueRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(issueRec, issueReq)
|
||||
if issueRec.Code != http.StatusCreated {
|
||||
t.Fatalf("issue failed: code=%d body=%s", issueRec.Code, issueRec.Body.String())
|
||||
}
|
||||
issueResp := decodeMap(t, issueRec.Body.Bytes())
|
||||
tokenID := issueResp["data"].(map[string]any)["token_id"].(string)
|
||||
|
||||
queryReq := httptest.NewRequest(http.MethodGet, "/api/v1/platform/tokens/audit-events?token_id="+tokenID+"&limit=5", nil)
|
||||
queryReq.Header.Set("X-Request-Id", "req-audit-query-2")
|
||||
queryRec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(queryRec, queryReq)
|
||||
if queryRec.Code != http.StatusOK {
|
||||
t.Fatalf("audit query failed: code=%d body=%s", queryRec.Code, queryRec.Body.String())
|
||||
}
|
||||
resp := decodeMap(t, queryRec.Body.Bytes())
|
||||
data := resp["data"].(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
if len(items) == 0 {
|
||||
t.Fatalf("audit query should return at least one event")
|
||||
}
|
||||
first := items[0].(map[string]any)
|
||||
if first["token_id"].(string) != tokenID {
|
||||
t.Fatalf("unexpected token_id in first item: got=%s want=%s", first["token_id"].(string), tokenID)
|
||||
}
|
||||
if strings.Contains(queryRec.Body.String(), "access_token") {
|
||||
t.Fatalf("audit query response must not contain access_token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenAPIAuditEventsNotReady(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runtime := service.NewInMemoryTokenRuntime(nil)
|
||||
api := NewTokenAPI(runtime, noopAuditEmitter{}, time.Now)
|
||||
mux := http.NewServeMux()
|
||||
api.Register(mux)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/platform/tokens/audit-events?limit=3", nil)
|
||||
req.Header.Set("X-Request-Id", "req-audit-query-3")
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusNotImplemented {
|
||||
t.Fatalf("expected not implemented: code=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(t *testing.T, payload any) *bytes.Reader {
|
||||
t.Helper()
|
||||
buf, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal json failed: %v", err)
|
||||
}
|
||||
return bytes.NewReader(buf)
|
||||
}
|
||||
|
||||
func decodeMap(t *testing.T, raw []byte) map[string]any {
|
||||
t.Helper()
|
||||
out := map[string]any{}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
t.Fatalf("decode json failed: %v, raw=%s", err, string(raw))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type noopAuditEmitter struct{}
|
||||
|
||||
func (noopAuditEmitter) Emit(context.Context, service.AuditEvent) error {
|
||||
return nil
|
||||
}
|
||||
295
platform-token-runtime/internal/token/audit_executable_test.go
Normal file
295
platform-token-runtime/internal/token/audit_executable_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/middleware"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/model"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
)
|
||||
|
||||
func TestTOKAud001IssueSuccessEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
|
||||
record, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 10 * time.Minute,
|
||||
RequestID: "req-aud-001",
|
||||
}, auditor)
|
||||
if err != nil {
|
||||
t.Fatalf("issue with audit failed: %v", err)
|
||||
}
|
||||
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected issue success event")
|
||||
}
|
||||
if event.EventName != service.EventTokenIssueSuccess {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenIssueSuccess)
|
||||
}
|
||||
assertAuditRequiredFields(t, event)
|
||||
if event.TokenID != record.TokenID {
|
||||
t.Fatalf("unexpected token_id in event: got=%s want=%s", event.TokenID, record.TokenID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud002IssueFailEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
|
||||
_, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 0,
|
||||
RequestID: "req-aud-002",
|
||||
}, auditor)
|
||||
if err == nil {
|
||||
t.Fatalf("expected issue failure")
|
||||
}
|
||||
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected issue fail event")
|
||||
}
|
||||
if event.EventName != service.EventTokenIssueFail {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenIssueFail)
|
||||
}
|
||||
assertAuditRequiredFields(t, event)
|
||||
if event.ResultCode != "ISSUE_FAILED" {
|
||||
t.Fatalf("unexpected result_code: got=%s want=ISSUE_FAILED", event.ResultCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud003AuthnFailEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts", nil)
|
||||
req.Header.Set("Authorization", "Bearer invalid-token")
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
|
||||
}
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected audit event for authn failure")
|
||||
}
|
||||
if event.EventName != service.EventTokenAuthnFail {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenAuthnFail)
|
||||
}
|
||||
if event.RequestID == "" {
|
||||
t.Fatalf("request_id must not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud004AuthzDeniedEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
|
||||
ctx := context.Background()
|
||||
viewer, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2002",
|
||||
Role: model.RoleViewer,
|
||||
Scope: []string{"supply:read"},
|
||||
TTL: 5 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue viewer token failed: %v", err)
|
||||
}
|
||||
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/supply/packages", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+viewer.AccessToken)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusForbidden)
|
||||
}
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected audit event for authz denial")
|
||||
}
|
||||
if event.EventName != service.EventTokenAuthzDenied {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenAuthzDenied)
|
||||
}
|
||||
if event.SubjectID != viewer.SubjectID {
|
||||
t.Fatalf("unexpected subject_id: got=%s want=%s", event.SubjectID, viewer.SubjectID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud005RevokeSuccessEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
|
||||
record, err := rt.Issue(context.Background(), service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 8 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue token failed: %v", err)
|
||||
}
|
||||
_, err = rt.RevokeAndAudit(context.Background(), record.TokenID, "operator_request", "req-aud-005", record.SubjectID, auditor)
|
||||
if err != nil {
|
||||
t.Fatalf("revoke with audit failed: %v", err)
|
||||
}
|
||||
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected revoke success event")
|
||||
}
|
||||
if event.EventName != service.EventTokenRevokeSuccess {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenRevokeSuccess)
|
||||
}
|
||||
assertAuditRequiredFields(t, event)
|
||||
if event.TokenID != record.TokenID {
|
||||
t.Fatalf("unexpected token_id in event: got=%s want=%s", event.TokenID, record.TokenID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud006QueryKeyRejectedEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts?api_key=raw-secret-value", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
|
||||
}
|
||||
event, ok := auditor.LastEvent()
|
||||
if !ok {
|
||||
t.Fatalf("expected query key rejection audit event")
|
||||
}
|
||||
if event.EventName != service.EventTokenQueryKeyRejected {
|
||||
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenQueryKeyRejected)
|
||||
}
|
||||
|
||||
serialized := strings.Join([]string{
|
||||
event.EventID,
|
||||
event.EventName,
|
||||
event.RequestID,
|
||||
event.TokenID,
|
||||
event.SubjectID,
|
||||
event.Route,
|
||||
event.ResultCode,
|
||||
event.ClientIP,
|
||||
}, "|")
|
||||
if strings.Contains(serialized, "raw-secret-value") {
|
||||
t.Fatalf("audit event must not contain raw query key value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKAud007EventImmutability(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
|
||||
issued, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 20 * time.Minute,
|
||||
RequestID: "req-aud-007-1",
|
||||
}, auditor)
|
||||
if err != nil {
|
||||
t.Fatalf("issue with audit failed: %v", err)
|
||||
}
|
||||
_, err = rt.RevokeAndAudit(context.Background(), issued.TokenID, "test", "req-aud-007-2", issued.SubjectID, auditor)
|
||||
if err != nil {
|
||||
t.Fatalf("revoke with audit failed: %v", err)
|
||||
}
|
||||
|
||||
firstRead := auditor.Events()
|
||||
secondRead := auditor.Events()
|
||||
if len(firstRead) < 2 || len(secondRead) < 2 {
|
||||
t.Fatalf("expected at least two audit events")
|
||||
}
|
||||
for idx := range firstRead {
|
||||
if firstRead[idx].EventID != secondRead[idx].EventID ||
|
||||
firstRead[idx].EventName != secondRead[idx].EventName ||
|
||||
!firstRead[idx].CreatedAt.Equal(secondRead[idx].CreatedAt) {
|
||||
t.Fatalf("event should be immutable across reads at index=%d", idx)
|
||||
}
|
||||
}
|
||||
for idx := 1; idx < len(firstRead); idx++ {
|
||||
if firstRead[idx].CreatedAt.Before(firstRead[idx-1].CreatedAt) {
|
||||
t.Fatalf("event timeline should be ordered by created_at")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertAuditRequiredFields(t *testing.T, event service.AuditEvent) {
|
||||
t.Helper()
|
||||
if event.EventID == "" {
|
||||
t.Fatalf("event_id must not be empty")
|
||||
}
|
||||
if event.RequestID == "" {
|
||||
t.Fatalf("request_id must not be empty")
|
||||
}
|
||||
if event.ResultCode == "" {
|
||||
t.Fatalf("result_code must not be empty")
|
||||
}
|
||||
if event.Route == "" {
|
||||
t.Fatalf("route must not be empty")
|
||||
}
|
||||
if event.CreatedAt.IsZero() {
|
||||
t.Fatalf("created_at must not be zero")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package token_test
|
||||
|
||||
import "testing"
|
||||
|
||||
type auditTemplateCase struct {
|
||||
ID string
|
||||
Name string
|
||||
TriggerCase string
|
||||
Assertions []string
|
||||
}
|
||||
|
||||
func TestTokenAuditTemplateCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []auditTemplateCase{
|
||||
{
|
||||
ID: "TOK-AUD-001",
|
||||
Name: "签发成功事件",
|
||||
TriggerCase: "TOK-LIFE-001",
|
||||
Assertions: []string{
|
||||
"存在 token.issue.success",
|
||||
"event_id/request_id/result_code/route/created_at 齐全",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-AUD-002",
|
||||
Name: "签发失败事件",
|
||||
TriggerCase: "TOK-LIFE-002",
|
||||
Assertions: []string{
|
||||
"存在 token.issue.fail",
|
||||
"result_code 准确",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-AUD-003",
|
||||
Name: "鉴权失败事件",
|
||||
TriggerCase: "无效 token 访问受保护接口",
|
||||
Assertions: []string{
|
||||
"存在 token.authn.fail",
|
||||
"包含 request_id",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-AUD-004",
|
||||
Name: "越权事件",
|
||||
TriggerCase: "TOK-LIFE-008",
|
||||
Assertions: []string{
|
||||
"存在 token.authz.denied",
|
||||
"包含 subject_id",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-AUD-005",
|
||||
Name: "吊销事件",
|
||||
TriggerCase: "TOK-LIFE-005",
|
||||
Assertions: []string{
|
||||
"存在 token.revoke.success",
|
||||
"包含 token_id",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-AUD-006",
|
||||
Name: "query key 拒绝事件",
|
||||
TriggerCase: "query key 访问受保护接口",
|
||||
Assertions: []string{
|
||||
"存在 token.query_key.rejected",
|
||||
"不含敏感值",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-AUD-007",
|
||||
Name: "事件不可篡改",
|
||||
TriggerCase: "重复读取同 event_id",
|
||||
Assertions: []string{
|
||||
"核心字段不可变",
|
||||
"时间顺序正确",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.ID, func(t *testing.T) {
|
||||
t.Skipf("模板用例,待接入实现: %s", tc.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/middleware"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/model"
|
||||
"lijiaoqiao/platform-token-runtime/internal/auth/service"
|
||||
)
|
||||
|
||||
func TestTOKLife001IssueSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
first, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 30 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue token failed: %v", err)
|
||||
}
|
||||
second, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 30 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue second token failed: %v", err)
|
||||
}
|
||||
|
||||
if first.Status != service.TokenStatusActive {
|
||||
t.Fatalf("unexpected status: got=%s want=%s", first.Status, service.TokenStatusActive)
|
||||
}
|
||||
if !first.ExpiresAt.After(first.IssuedAt) {
|
||||
t.Fatalf("expires_at must be greater than issued_at")
|
||||
}
|
||||
if first.TokenID == second.TokenID {
|
||||
t.Fatalf("token_id should be unique")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKLife002IssueInvalidInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
ctx := context.Background()
|
||||
_, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 0,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid ttl_seconds")
|
||||
}
|
||||
if got := rt.TokenCount(); got != 0 {
|
||||
t.Fatalf("unexpected token count after invalid issue: got=%d want=0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKLife003IssueIdempotencyReplay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
first, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 30 * time.Minute,
|
||||
IdempotencyKey: "idem-life-003",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("first issue failed: %v", err)
|
||||
}
|
||||
second, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 30 * time.Minute,
|
||||
IdempotencyKey: "idem-life-003",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("replay issue failed: %v", err)
|
||||
}
|
||||
|
||||
if first.TokenID != second.TokenID {
|
||||
t.Fatalf("replayed issue must return same token_id: first=%s second=%s", first.TokenID, second.TokenID)
|
||||
}
|
||||
if got := rt.TokenCount(); got != 1 {
|
||||
t.Fatalf("idempotent replay must not create duplicate token: got=%d want=1", got)
|
||||
}
|
||||
|
||||
_, err = rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:read"},
|
||||
TTL: 30 * time.Minute,
|
||||
IdempotencyKey: "idem-life-003",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected payload mismatch conflict for same idempotency key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKLife004RefreshSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
issued, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 1 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue token failed: %v", err)
|
||||
}
|
||||
previousExpiresAt := issued.ExpiresAt
|
||||
|
||||
refreshed, err := rt.Refresh(ctx, issued.TokenID, 15*time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("refresh token failed: %v", err)
|
||||
}
|
||||
|
||||
if refreshed.Status != service.TokenStatusActive {
|
||||
t.Fatalf("unexpected status after refresh: got=%s want=%s", refreshed.Status, service.TokenStatusActive)
|
||||
}
|
||||
if !refreshed.ExpiresAt.After(previousExpiresAt) {
|
||||
t.Fatalf("expires_at should be delayed after refresh")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKLife005RevokeSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := time.Now()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
issued, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 10 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue token failed: %v", err)
|
||||
}
|
||||
if _, err := rt.Revoke(ctx, issued.TokenID, "security_event"); err != nil {
|
||||
t.Fatalf("revoke token failed: %v", err)
|
||||
}
|
||||
|
||||
introspected, err := rt.Introspect(ctx, issued.AccessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("introspect failed: %v", err)
|
||||
}
|
||||
if introspected.Status != service.TokenStatusRevoked {
|
||||
t.Fatalf("unexpected status after revoke: got=%s want=%s", introspected.Status, service.TokenStatusRevoked)
|
||||
}
|
||||
if time.Since(start) > 5*time.Second {
|
||||
t.Fatalf("revoke propagation exceeded 5 seconds in in-memory runtime")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKLife006RevokedTokenAccessDenied(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
ctx := context.Background()
|
||||
|
||||
issued, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 5 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue token failed: %v", err)
|
||||
}
|
||||
if _, err := rt.Revoke(ctx, issued.TokenID, "test_revoke"); err != nil {
|
||||
t.Fatalf("revoke failed: %v", err)
|
||||
}
|
||||
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+issued.AccessToken)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
|
||||
}
|
||||
if code := decodeMiddlewareErrorCode(t, rec); code != service.CodeAuthTokenInactive {
|
||||
t.Fatalf("unexpected error code: got=%s want=%s", code, service.CodeAuthTokenInactive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKLife007ExpiredTokenInactive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
current := time.Date(2026, 3, 29, 15, 0, 0, 0, time.UTC)
|
||||
rt := service.NewInMemoryTokenRuntime(func() time.Time { return current })
|
||||
ctx := context.Background()
|
||||
|
||||
issued, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2001",
|
||||
Role: model.RoleOwner,
|
||||
Scope: []string{"supply:*"},
|
||||
TTL: 2 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue token failed: %v", err)
|
||||
}
|
||||
current = current.Add(3 * time.Second)
|
||||
|
||||
introspected, err := rt.Introspect(ctx, issued.AccessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("introspect failed: %v", err)
|
||||
}
|
||||
if introspected.Status != service.TokenStatusExpired {
|
||||
t.Fatalf("unexpected token status: got=%s want=%s", introspected.Status, service.TokenStatusExpired)
|
||||
}
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+issued.AccessToken)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
|
||||
}
|
||||
if code := decodeMiddlewareErrorCode(t, rec); code != service.CodeAuthTokenInactive {
|
||||
t.Fatalf("unexpected error code: got=%s want=%s", code, service.CodeAuthTokenInactive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTOKLife008ViewerWriteDenied(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := service.NewMemoryAuditEmitter()
|
||||
rt := service.NewInMemoryTokenRuntime(nil)
|
||||
authorizer := service.NewScopeRoleAuthorizer()
|
||||
|
||||
ctx := context.Background()
|
||||
viewer, err := rt.Issue(ctx, service.IssueTokenInput{
|
||||
SubjectID: "2002",
|
||||
Role: model.RoleViewer,
|
||||
Scope: []string{"supply:read"},
|
||||
TTL: 10 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("issue viewer token failed: %v", err)
|
||||
}
|
||||
|
||||
nextCalled := false
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextCalled = true
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
|
||||
Verifier: rt,
|
||||
StatusResolver: rt,
|
||||
Authorizer: authorizer,
|
||||
Auditor: auditor,
|
||||
}, next)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/supply/packages", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+viewer.AccessToken)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusForbidden)
|
||||
}
|
||||
if code := decodeMiddlewareErrorCode(t, rec); code != service.CodeAuthScopeDenied {
|
||||
t.Fatalf("unexpected error code: got=%s want=%s", code, service.CodeAuthScopeDenied)
|
||||
}
|
||||
if nextCalled {
|
||||
t.Fatalf("write handler should be blocked for viewer token")
|
||||
}
|
||||
}
|
||||
|
||||
type middlewareErrorEnvelope struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func decodeMiddlewareErrorCode(t *testing.T, rec *httptest.ResponseRecorder) string {
|
||||
t.Helper()
|
||||
var envelope middlewareErrorEnvelope
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("failed to decode middleware error response: %v", err)
|
||||
}
|
||||
return envelope.Error.Code
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package token_test
|
||||
|
||||
import "testing"
|
||||
|
||||
// 说明:
|
||||
// 1. 本文件保留完整 TOK-LIFE 模板清单作为覆盖基线。
|
||||
// 2. 首批可执行用例已在 lifecycle_executable_test.go 落地:
|
||||
// TOK-LIFE-001 / TOK-LIFE-004 / TOK-LIFE-005 / TOK-LIFE-008。
|
||||
|
||||
type lifecycleTemplateCase struct {
|
||||
ID string
|
||||
Name string
|
||||
Preconditions []string
|
||||
Steps []string
|
||||
Assertions []string
|
||||
}
|
||||
|
||||
func TestTokenLifecycleTemplateCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []lifecycleTemplateCase{
|
||||
{
|
||||
ID: "TOK-LIFE-001",
|
||||
Name: "签发成功",
|
||||
Preconditions: []string{
|
||||
"tenant_id=1001",
|
||||
"subject_owner=2001",
|
||||
},
|
||||
Steps: []string{
|
||||
"调用 POST /api/v1/platform/tokens/issue",
|
||||
"记录 token_id/issued_at/expires_at/status",
|
||||
},
|
||||
Assertions: []string{
|
||||
"status=active",
|
||||
"expires_at>issued_at",
|
||||
"token_id 唯一",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-LIFE-002",
|
||||
Name: "签发参数非法",
|
||||
Preconditions: []string{
|
||||
"ttl_seconds 超上限",
|
||||
},
|
||||
Steps: []string{
|
||||
"调用 POST /api/v1/platform/tokens/issue",
|
||||
},
|
||||
Assertions: []string{
|
||||
"返回 400",
|
||||
"不落 active token",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-LIFE-003",
|
||||
Name: "幂等签发重放",
|
||||
Steps: []string{
|
||||
"相同 Idempotency-Key 重复调用签发接口",
|
||||
},
|
||||
Assertions: []string{
|
||||
"返回同一 token_id",
|
||||
"无重复写入",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-LIFE-004",
|
||||
Name: "续期成功",
|
||||
Steps: []string{
|
||||
"调用 POST /api/v1/platform/tokens/{tokenId}/refresh",
|
||||
},
|
||||
Assertions: []string{
|
||||
"expires_at 延后",
|
||||
"status=active",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-LIFE-005",
|
||||
Name: "吊销成功",
|
||||
Steps: []string{
|
||||
"调用 POST /api/v1/platform/tokens/{tokenId}/revoke",
|
||||
"立即调用 introspect 查询状态",
|
||||
},
|
||||
Assertions: []string{
|
||||
"status 最终为 revoked",
|
||||
"吊销生效延迟 <=5s",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-LIFE-006",
|
||||
Name: "吊销后访问受限接口",
|
||||
Steps: []string{
|
||||
"使用已吊销 token 访问受保护接口",
|
||||
},
|
||||
Assertions: []string{
|
||||
"返回 401 AUTH_TOKEN_INACTIVE",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-LIFE-007",
|
||||
Name: "过期自动失效",
|
||||
Steps: []string{
|
||||
"签发短 TTL token",
|
||||
"等待 token 过期",
|
||||
"调用 introspect 查询状态",
|
||||
},
|
||||
Assertions: []string{
|
||||
"status=expired",
|
||||
"返回不可用错误",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "TOK-LIFE-008",
|
||||
Name: "viewer 越权写操作",
|
||||
Preconditions: []string{
|
||||
"viewer scope=supply:read",
|
||||
},
|
||||
Steps: []string{
|
||||
"viewer token 调用写接口",
|
||||
},
|
||||
Assertions: []string{
|
||||
"返回 403 AUTH_SCOPE_DENIED",
|
||||
"无写入副作用",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.ID, func(t *testing.T) {
|
||||
t.Skipf("模板用例,待接入实现: %s", tc.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
43
reports/alignment_validation_checkpoint_01_2026-03-27.md
Normal file
43
reports/alignment_validation_checkpoint_01_2026-03-27.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-01)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 对齐触发条件:已完成 10 个子任务(A-001~A-008, B-001~B-002)
|
||||
- 对齐范围:
|
||||
- `docs/plans/2026-03-25-superpowers-execution-tasklist-v1.md`
|
||||
- `docs/supply_button_level_prd_v1_2026-03-25.md`
|
||||
- `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml`
|
||||
- `review/superpowers_comprehensive_planning_review_v1_2026-03-25.md`
|
||||
- `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
|
||||
- `docs/acceptance_gate_single_source_v1_2026-03-18.md`
|
||||
|
||||
## 1. 对齐结论
|
||||
|
||||
结论:**本检查点总体对齐,允许进入 B-003 后续执行。**
|
||||
|
||||
说明:
|
||||
1. WG-A 目标“需求冻结”已形成可追溯证据链。
|
||||
2. WG-B 当前处于“参数定义完成、路径挂载待完成”的中间态。
|
||||
3. 门禁层(SSOT)未被破坏,凭证边界主线保持一致。
|
||||
|
||||
## 2. 逐项核对
|
||||
|
||||
| 核对项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 按钮 PRD 已从草案改为冻结 | PASS | `docs/supply_button_level_prd_v1_2026-03-25.md:3` |
|
||||
| “待拍板项”已替换为“已决议项” | PASS | `docs/supply_button_level_prd_v1_2026-03-25.md:236` |
|
||||
| 决议映射与会议纪要已形成双证据 | PASS | `docs/product/supply_prd_pending_to_decision_map_v1_2026-03-27.md`、`review/outputs/supply_prd_decision_meeting_minutes_2026-03-27.md` |
|
||||
| 任务单已引用冻结 PRD 版本 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md:14` |
|
||||
| P0-01 已在评审报告关闭 | PASS | `review/superpowers_comprehensive_planning_review_v1_2026-03-25.md:53` |
|
||||
| OpenAPI 已定义幂等头参数组件 | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:431`、`:440` |
|
||||
| OpenAPI 写操作路径已挂载幂等头 | PARTIAL | 下一批次 B-003~B-007 |
|
||||
|
||||
## 3. 风险与约束
|
||||
|
||||
1. `P0-02` 仍未完全关闭:当前仅完成参数定义,尚未完成路径级 required 挂载与示例/校验。
|
||||
2. 本次对齐只覆盖前 10 项,不代表 SUP staging 证据链完成。
|
||||
3. `token` 运行态实现缺口(TOK-REAL)结论保持有效,不因本批次文档修改而变化。
|
||||
|
||||
## 4. 准入建议
|
||||
|
||||
1. 允许进入下一批次(B-003~B-010)。
|
||||
2. 完成 B-010 后必须执行 Checkpoint-02 全面对齐验证。
|
||||
39
reports/alignment_validation_checkpoint_02_2026-03-27.md
Normal file
39
reports/alignment_validation_checkpoint_02_2026-03-27.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-02)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 对齐触发条件:累计完成 20 个子任务(A-001~A-008, B-001~B-012)
|
||||
- 对齐目标:验证 WG-A 与 WG-B 输出是否与 SSOT、技术增强稿、评审结论一致
|
||||
|
||||
## 1. 总体结论
|
||||
|
||||
结论:**A/B 阶段已对齐,可进入 C 阶段执行。**
|
||||
|
||||
说明:
|
||||
1. P0-01(冻结状态冲突)已闭环。
|
||||
2. P0-02(幂等头缺失)已闭环。
|
||||
3. P0-03(执行环境阻塞)仍未关闭,不影响进入 C 阶段文档整改,但阻断最终发布。
|
||||
|
||||
## 2. 对齐矩阵
|
||||
|
||||
| 维度 | 检查项 | 结果 | 证据 |
|
||||
|---|---|---|---|
|
||||
| 需求冻结 | 按钮 PRD 状态为冻结,且不再保留待拍板 | PASS | `docs/supply_button_level_prd_v1_2026-03-25.md:3`、`:236` |
|
||||
| 决议追踪 | 待拍板项有决议映射与会议纪要 | PASS | `docs/product/supply_prd_pending_to_decision_map_v1_2026-03-27.md`、`review/outputs/supply_prd_decision_meeting_minutes_2026-03-27.md` |
|
||||
| 任务链路 | 执行任务单引用冻结 PRD | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md:14` |
|
||||
| 契约定义 | OpenAPI 定义 `X-Request-Id` 与 `Idempotency-Key` 参数组件 | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:456`、`:465` |
|
||||
| 契约挂载 | 5 个关键写接口全部挂载双 header | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:46`、`:178`、`:242`、`:310`、`:339` |
|
||||
| 冲突语义 | 409 payload mismatch 示例存在 | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:500` |
|
||||
| 重放语义 | 202 in-progress 示例存在,含 `retry_after_ms` | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:510` |
|
||||
| 设计对齐 | 技术增强稿已标注契约落地 | PASS | `docs/supply_technical_design_enhanced_v1_2026-03-25.md:42` |
|
||||
| 评审闭环 | P0-02 已在 superpowers 评审报告关闭 | PASS | `review/superpowers_comprehensive_planning_review_v1_2026-03-25.md:66` |
|
||||
| 门禁主线 | M-013~M-016 主线口径未偏移 | PASS | `docs/acceptance_gate_single_source_v1_2026-03-18.md` |
|
||||
|
||||
## 3. 未关闭项(跨阶段)
|
||||
|
||||
1. P0-03:staging 环境与真实 token 证据链缺失。
|
||||
2. TOK-REAL:token 运行态实现缺口仍在(与本次 A/B 文档对齐无冲突)。
|
||||
|
||||
## 4. 下一步准入
|
||||
|
||||
1. 进入 C-001~C-008(测试路径与追踪矩阵一致化)。
|
||||
2. C 阶段完成后执行 Checkpoint-03 对齐验证。
|
||||
29
reports/alignment_validation_checkpoint_03_2026-03-27.md
Normal file
29
reports/alignment_validation_checkpoint_03_2026-03-27.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-03 / WG-C)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 对齐触发条件:独立阶段 WG-C(C-001~C-008)完成
|
||||
- 核心目标:验证“测试追踪矩阵路径口径”与 OpenAPI 主路径是否完全一致
|
||||
|
||||
## 1. 总体结论
|
||||
|
||||
结论:**WG-C 对齐通过,路径一致性缺口已关闭。**
|
||||
|
||||
## 2. 对齐核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 测试矩阵 API 列使用 OpenAPI 精确参数名 | PASS | `docs/supply_test_plan_enhanced_v1_2026-03-25.md:42`、`:45`、`:48` |
|
||||
| 历史路径兼容口径可追踪(`api_alias`) | PASS | `docs/supply_test_plan_enhanced_v1_2026-03-25.md:38` |
|
||||
| CSV 与测试方案字段结构一致 | PASS | `reports/supply_traceability_matrix_2026-03-25.csv:1` |
|
||||
| 生成规则可复跑、可校验 | PASS | `docs/supply_traceability_matrix_generation_rules_v1_2026-03-27.md` |
|
||||
| XR-002 验收项纳入路径一致性检查 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md:158` |
|
||||
|
||||
## 3. 仍未关闭的跨阶段项
|
||||
|
||||
1. D 阶段真实环境证据链(staging 地址与短期 token)仍缺。
|
||||
2. token 运行态实现缺口(TOK-REAL)仍缺实现证据。
|
||||
|
||||
## 4. 准入建议
|
||||
|
||||
1. 允许进入 WG-D(D-001~D-018)。
|
||||
2. 若出现环境阻塞,优先输出阻塞清单与替代执行路径,保持任务推进不中断。
|
||||
31
reports/alignment_validation_checkpoint_04_2026-03-27.md
Normal file
31
reports/alignment_validation_checkpoint_04_2026-03-27.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-04 / WG-D)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 对齐触发条件:独立阶段 WG-D 启动后确认“开发阶段暂缓”
|
||||
- 验证目标:确认暂缓原因与规划设计文档是否一致,避免误判
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**WG-D 暂缓与规划约束一致,不属于执行偏航。**
|
||||
|
||||
## 2. 一致性核对
|
||||
|
||||
| 核对项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| D 阶段要求真实 staging + 短期 token | PASS | `docs/plans/2026-03-25-superpowers-execution-tasklist-v1.md:62` |
|
||||
| 预检脚本会拒绝占位 token/域名 | PASS | `scripts/supply-gate/staging_precheck_and_run.sh` |
|
||||
| 当前 `.env` 仍为占位值 | PASS | `scripts/supply-gate/.env` |
|
||||
| 运行结果确认为预检失败,且当前按阶段暂缓处理 | PASS | `reports/stage_d_blocker_report_2026-03-27.md` |
|
||||
| 当前生产决议仍为 NO-GO | PASS | `review/final_decision_2026-03-31.md` |
|
||||
|
||||
## 3. 风险判定
|
||||
|
||||
1. 若在开发阶段将“暂缓”误判为“已验证通过”,将直接违反 SSOT 与决议门禁。
|
||||
2. 当前最小正确动作是继续推进实现前置,待联调阶段再激活 D-007~D-018。
|
||||
|
||||
## 4. 准入条件
|
||||
|
||||
仅当下列条件全部满足,WG-D 才从暂缓切换为执行:
|
||||
1. `API_BASE_URL` 非占位且可达。
|
||||
2. `OWNER/VIEWER/ADMIN` 三类短期 token 已写入 `.env`。
|
||||
3. `staging_precheck_and_run.sh` 预检通过。
|
||||
20
reports/alignment_validation_checkpoint_05_2026-03-27.md
Normal file
20
reports/alignment_validation_checkpoint_05_2026-03-27.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-05 / WG-E)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 对齐触发条件:独立阶段 WG-E 启动(依赖 D 阶段)后确认暂缓
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**WG-E 暂缓与任务依赖关系一致,不存在执行偏离。**
|
||||
|
||||
## 2. 依赖核对
|
||||
|
||||
| 核对项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| E-001~E-004 依赖 D 阶段产物 | PASS | `docs/plans/2026-03-25-superpowers-execution-tasklist-v1.md` |
|
||||
| D 阶段当前为 DEFERRED | PASS | `reports/stage_d_blocker_report_2026-03-27.md` |
|
||||
| E 阶段当前为 DEFERRED(等待联调窗口) | PASS | `reports/stage_e_blocker_report_2026-03-27.md` |
|
||||
|
||||
## 3. 准入条件
|
||||
|
||||
仅当 D 阶段从暂缓切换为执行并产出 staging 实测证据后,E 阶段才可继续执行。
|
||||
32
reports/alignment_validation_checkpoint_06_2026-03-27.md
Normal file
32
reports/alignment_validation_checkpoint_06_2026-03-27.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-06 / F+G)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 对齐触发条件:完成 10 个子任务(F-001~F-007 + G-001~G-003)
|
||||
|
||||
## 1. 总体结论
|
||||
|
||||
结论:**F/G 阶段对齐通过,治理与决策文档已补齐。**
|
||||
|
||||
## 2. 对齐核查
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 全局 P0 与供应/平台能力边界映射完整 | PASS | `docs/product/global_p0_to_supply_platform_mapping_v1_2026-03-27.md` |
|
||||
| 预算/告警/账单导出映射到入口级 | PASS | 同上 `PRD-P0-05~07` |
|
||||
| 追踪矩阵纳入平台侧 P0(R-PLAT-001~003) | PASS | `reports/supply_traceability_matrix_2026-03-25.csv` |
|
||||
| `/supply` 主路径策略与 `/supplier` alias 规则落地 | PASS | `docs/api_naming_strategy_supply_vs_supplier_v1_2026-03-27.md`、`docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml` |
|
||||
| 复核报告补充 P1/P2 收敛状态 | PASS | `review/prd_tech_planning_recheck_v3_2026-03-27.md:66` |
|
||||
| 链接完整性检查已执行并可追踪 | PASS | `reports/link_integrity_check_2026-03-27.md` |
|
||||
| 门禁指标一致性检查已执行 | PASS | `reports/gate_metrics_consistency_check_2026-03-27.md` |
|
||||
| 已生成新的最终决议稿 | PASS | `review/final_decision_draft_v2_2026-03-27.md` |
|
||||
|
||||
## 3. 未关闭关键暂缓项(不影响本阶段对齐结论)
|
||||
|
||||
1. WG-D:真实 staging/短期 token 缺失(DEFERRED)。
|
||||
2. WG-E:依赖 D 阶段产物,当前 DEFERRED。
|
||||
3. TOK-REAL:token 运行态实现缺口未关闭。
|
||||
|
||||
## 4. 下一步
|
||||
|
||||
1. 仅剩 D/E 真实证据链路暂缓待激活。
|
||||
2. 解锁后按 D-001 -> E-010 顺序继续,不允许跳步。
|
||||
22
reports/alignment_validation_checkpoint_07_2026-03-27.md
Normal file
22
reports/alignment_validation_checkpoint_07_2026-03-27.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-07 / 开发阶段修订)
|
||||
|
||||
- 日期:2026-03-27
|
||||
- 触发条件:用户确认“当前仍在开发实施阶段,真实 URL/token 暂无”
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**执行口径已对齐开发阶段现实约束,主线未偏离。**
|
||||
|
||||
## 2. 对齐项
|
||||
|
||||
| 对齐项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| WG-D 从“执行失败”修订为“阶段暂缓” | PASS | `reports/stage_d_blocker_report_2026-03-27.md` |
|
||||
| WG-E 从“执行失败”修订为“阶段暂缓” | PASS | `reports/stage_e_blocker_report_2026-03-27.md` |
|
||||
| 在无 staging 参数前继续推进实现前置(TOK-001) | PASS | `docs/token_runtime_minimal_spec_v1.md` |
|
||||
| “仅平台分享 token”边界保持不变 | PASS | `docs/token_runtime_minimal_spec_v1.md`、`docs/supply_button_level_prd_v1_2026-03-25.md` |
|
||||
|
||||
## 3. 下一步(开发阶段)
|
||||
|
||||
1. 继续按 TOK-002~TOK-004 推进实现设计与测试前置。
|
||||
2. 待项目进入联调阶段后再激活 D/E 阶段。
|
||||
40
reports/alignment_validation_checkpoint_08_2026-03-29.md
Normal file
40
reports/alignment_validation_checkpoint_08_2026-03-29.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-08 / TOK-002~TOK-004)
|
||||
|
||||
- 日期:2026-03-29
|
||||
- 触发条件:完成 TOK-002 设计与契约细化、TOK-003/TOK-004 测试断言清单
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过,可进入 TOK-002~TOK-004 实现编码阶段。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `docs/token_runtime_minimal_spec_v1.md`(TOK-001)
|
||||
2. `docs/token_auth_middleware_design_v1_2026-03-29.md`(TOK-002)
|
||||
3. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`(TOK-002 契约)
|
||||
4. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`(TOK-003/TOK-004)
|
||||
5. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`(任务链路)
|
||||
6. `docs/acceptance_gate_single_source_v1_2026-03-18.md`(M-021 门禁)
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| TOK-002 设计保持“仅平台凭证入站”边界 | PASS | `docs/token_auth_middleware_design_v1_2026-03-29.md` |
|
||||
| query key 外拒策略在中间件设计中可执行 | PASS | 同上(`QueryKeyRejectMiddleware`) |
|
||||
| TOK-002 接口契约已覆盖 issue/refresh/revoke/introspect | PASS | `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml` |
|
||||
| OpenAPI 草案语法可解析 | PASS | `platform_token_openapi_yaml: PASS` |
|
||||
| TOK-003 生命周期断言可执行 | PASS | `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md` |
|
||||
| TOK-004 审计事件断言可执行 | PASS | 同上(`TOK-AUD-*`) |
|
||||
| 任务单证据口径已区分开发阶段与联调阶段 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md` |
|
||||
| M-021 门禁口径未被破坏 | PASS | `docs/acceptance_gate_single_source_v1_2026-03-18.md` |
|
||||
|
||||
## 4. 风险与限制
|
||||
|
||||
1. 本轮为设计/契约/测试前置对齐,不等于运行态实现已完成。
|
||||
2. D/E 阶段仍处于开发阶段暂缓(待联调窗口激活)。
|
||||
|
||||
## 5. 下一步建议
|
||||
|
||||
1. 进入 TOK-002 实现编码与单测阶段。
|
||||
2. 按本断言清单执行 TOK-003/TOK-004 集成测试准备。
|
||||
45
reports/alignment_validation_checkpoint_09_2026-03-29.md
Normal file
45
reports/alignment_validation_checkpoint_09_2026-03-29.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-09 / TOK-002 代码骨架 + TOK-003/004 测试模板)
|
||||
|
||||
- 日期:2026-03-29
|
||||
- 触发条件:完成 TOK-002 中间件代码骨架与单测骨架、TOK-003/004 测试模板文件
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过,代码骨架与测试模板与 TOK 基线文档一致。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `docs/token_runtime_minimal_spec_v1.md`
|
||||
2. `docs/token_auth_middleware_design_v1_2026-03-29.md`
|
||||
3. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`
|
||||
4. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`
|
||||
5. `docs/acceptance_gate_single_source_v1_2026-03-18.md`(M-021)
|
||||
6. `platform-token-runtime/internal/auth/middleware/token_auth_middleware.go`
|
||||
7. `platform-token-runtime/internal/auth/middleware/query_key_reject_middleware.go`
|
||||
8. `platform-token-runtime/internal/auth/middleware/token_auth_middleware_test.go`
|
||||
9. `platform-token-runtime/internal/token/lifecycle_test_template_test.go`
|
||||
10. `platform-token-runtime/internal/token/audit_test_template_test.go`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 中间件链路包含 request_id -> query key 外拒 -> bearer 校验 -> 状态校验 -> scope 鉴权 -> 审计 | PASS | `platform-token-runtime/internal/auth/middleware/token_auth_middleware.go` |
|
||||
| query key 外拒命中 `key/api_key/token` 且返回 `401 QUERY_KEY_NOT_ALLOWED` | PASS | `platform-token-runtime/internal/auth/middleware/query_key_reject_middleware.go` |
|
||||
| 错误码语义与 TOK-002 设计一致 | PASS | `platform-token-runtime/internal/auth/service/token_verifier.go` |
|
||||
| TOK-002 单测骨架覆盖成功/失败/越权/边界拒绝路径 | PASS | `platform-token-runtime/internal/auth/middleware/token_auth_middleware_test.go` |
|
||||
| TOK-LIFE-001~008 模板已落地 | PASS | `platform-token-runtime/internal/token/lifecycle_test_template_test.go` |
|
||||
| TOK-AUD-001~007 模板已落地 | PASS | `platform-token-runtime/internal/token/audit_test_template_test.go` |
|
||||
| SSOT 边界“仅平台凭证入站,不直发上游 token”未被破坏 | PASS | 上述代码与模板均未暴露上游凭证 |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 当前环境缺少 `go` 工具链,未执行编译/单测命令,仅完成代码骨架与模板落地。
|
||||
2. TOK-003/004 为模板态(`t.Skip`),待生命周期实现后替换为真实断言执行。
|
||||
3. staging 联调(TOK-005~TOK-007)仍需真实环境参数后激活。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 实现 `TokenVerifier/TokenStatusResolver/RouteAuthorizer` 的真实逻辑与缓存策略。
|
||||
2. 将 `TOK-LIFE-*` / `TOK-AUD-*` 模板由 `t.Skip` 切换为真实执行断言。
|
||||
3. 在具备 `go` 环境后补充单测和覆盖率报告,作为 TOK-002 联调阶段证据。
|
||||
41
reports/alignment_validation_checkpoint_10_2026-03-29.md
Normal file
41
reports/alignment_validation_checkpoint_10_2026-03-29.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-10 / TOK 最小实现 + 部分可执行测试)
|
||||
|
||||
- 日期:2026-03-29
|
||||
- 触发条件:完成内存版 token 运行时实现,并将指定模板用例转为可执行测试
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过,TOK-002/003/004 已从“纯骨架”推进至“最小可运行实现 + 部分可执行断言”。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `docs/token_runtime_minimal_spec_v1.md`
|
||||
2. `docs/token_auth_middleware_design_v1_2026-03-29.md`
|
||||
3. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`
|
||||
4. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`
|
||||
5. `platform-token-runtime/internal/auth/service/inmemory_runtime.go`
|
||||
6. `platform-token-runtime/internal/token/lifecycle_executable_test.go`
|
||||
7. `platform-token-runtime/internal/token/audit_executable_test.go`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 实现最小 token 运行时(签发/续期/吊销/introspect) | PASS | `platform-token-runtime/internal/auth/service/inmemory_runtime.go` |
|
||||
| TokenVerifier/StatusResolver 已可被中间件直接调用 | PASS | 同上(`Verify` / `Resolve`) |
|
||||
| RouteAuthorizer 已落实 owner/viewer/admin + scope 语义 | PASS | 同上(`ScopeRoleAuthorizer`) |
|
||||
| TOK-LIFE-001/004/005/008 已转为可执行测试 | PASS | `platform-token-runtime/internal/token/lifecycle_executable_test.go` |
|
||||
| TOK-AUD-003/004/006 已转为可执行测试 | PASS | `platform-token-runtime/internal/token/audit_executable_test.go` |
|
||||
| SSOT 边界“仅平台凭证入站,不直发上游 token”保持一致 | PASS | 中间件链路 + 测试断言均未暴露上游凭证 |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 当前环境无 `go` 工具链,未执行 `go test`;本轮为代码级实现与对齐回填。
|
||||
2. 其余生命周期/审计用例仍保持模板态(`t.Skip`),待后续阶段继续落地。
|
||||
3. 当前实现为内存版,用于开发阶段前置验证;非生产部署实现。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 继续将 `TOK-LIFE-002/003/006/007` 与 `TOK-AUD-001/002/005/007` 转可执行断言。
|
||||
2. 增加幂等键语义(`Idempotency-Key`)与审计不可篡改校验实现。
|
||||
3. 在具备 Go 环境后执行 `go test ./...`,补齐测试报告证据。
|
||||
44
reports/alignment_validation_checkpoint_11_2026-03-29.md
Normal file
44
reports/alignment_validation_checkpoint_11_2026-03-29.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-11 / Go 工具链 + TOK 全量用例可执行化)
|
||||
|
||||
- 日期:2026-03-29
|
||||
- 触发条件:安装 Go 工具链,完成 TOK 生命周期与审计断言全量可执行化,并通过本地测试
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过,TOK-003/TOK-004 已由“部分可执行”推进为“全量可执行”,并已完成本地 `go test` 验证。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `docs/token_runtime_minimal_spec_v1.md`
|
||||
2. `docs/token_auth_middleware_design_v1_2026-03-29.md`
|
||||
3. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`
|
||||
4. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`
|
||||
5. `platform-token-runtime/internal/auth/service/inmemory_runtime.go`
|
||||
6. `platform-token-runtime/internal/token/lifecycle_executable_test.go`
|
||||
7. `platform-token-runtime/internal/token/audit_executable_test.go`
|
||||
8. `platform-token-runtime/internal/token/lifecycle_test_template_test.go`
|
||||
9. `platform-token-runtime/internal/token/audit_test_template_test.go`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| Go 工具链已安装且可执行 | PASS | `/.tools/go-current/bin/go version => go1.26.1` |
|
||||
| TOK-LIFE-001~008 已具备可执行实现 | PASS | `platform-token-runtime/internal/token/lifecycle_executable_test.go` |
|
||||
| TOK-AUD-001~007 已具备可执行实现 | PASS | `platform-token-runtime/internal/token/audit_executable_test.go` |
|
||||
| 幂等重放语义已实现(同键同载荷返回同 token_id,冲突载荷拒绝) | PASS | `inmemory_runtime.go` + `TestTOKLife003IssueIdempotencyReplay` |
|
||||
| 吊销/过期后访问受保护路由返回 `AUTH_TOKEN_INACTIVE` | PASS | `TestTOKLife006RevokedTokenAccessDenied` / `TestTOKLife007ExpiredTokenInactive` |
|
||||
| 审计必填字段与不可泄露约束断言可执行 | PASS | `assertAuditRequiredFields` + `TestTOKAud006QueryKeyRejectedEvent` |
|
||||
| 本地测试执行通过 | PASS | `go test ./...`(全部通过) |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 当前实现为内存版运行时,用于开发阶段验证;未替代生产级持久化/缓存/总线方案。
|
||||
2. 模板文件保留用于需求追踪基线,执行入口已迁移到 `*_executable_test.go`。
|
||||
3. staging 联调(TOK-005~TOK-007)仍需真实环境参数后激活。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 将内存版运行时替换为数据库 + 缓存实现,接入真实 `platform_token_registry/token_status_cache`。
|
||||
2. 接入真实审计落库表并补充查询验证脚本,替换当前内存审计存储。
|
||||
3. 在 `.env` 真值就绪后执行 staging 全链路回归并回填 TOK-005~TOK-007 证据。
|
||||
43
reports/alignment_validation_checkpoint_12_2026-03-30.md
Normal file
43
reports/alignment_validation_checkpoint_12_2026-03-30.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-12 / TOK-005 Dry-Run 门禁并入)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:完成 TOK-005 开发阶段 dry-run 脚本、执行证据与门禁文档并入
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。TOK-005 已形成“可执行脚本 + 可落地证据 + 任务单口径”闭环,可等待真实 staging 参数后切换联调。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
|
||||
2. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
3. `scripts/supply-gate/tok005_boundary_dryrun.sh`
|
||||
4. `scripts/supply-gate/staging_precheck_and_run.sh`
|
||||
5. `reports/gates/tok005_dryrun_2026-03-30_090146.md`
|
||||
6. `tests/supply/artifacts/tok005_dryrun_2026-03-30_090146/go_test_output.txt`
|
||||
7. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`
|
||||
8. `docs/acceptance_gate_single_source_v1_2026-03-18.md`(M-013~M-016, M-021)
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| TOK-005 dry-run 命令已落地且可执行 | PASS | `scripts/supply-gate/tok005_boundary_dryrun.sh` |
|
||||
| staging 预检脚本已接入 TOK-005 dry-run 开关 | PASS | `scripts/supply-gate/staging_precheck_and_run.sh`(`ENABLE_TOK005_DRYRUN`) |
|
||||
| dry-run 输出报告与原始日志可追溯 | PASS | `reports/gates/tok005_dryrun_2026-03-30_090146.md` + `.log` |
|
||||
| TOK 运行态 `go test ./...` 在 dry-run 中通过 | PASS | `tests/supply/artifacts/tok005_dryrun_2026-03-30_090146/go_test_output.txt` |
|
||||
| M-016(query key 外拒)具备脚本化检查 | PASS | dry-run 检查项 `Query Key 外拒检查` |
|
||||
| M-013(审计脱敏)具备脚本化检查 | PASS | dry-run 检查项 `审计脱敏检查` |
|
||||
| staging 准备度口径清晰,不伪造联调结论 | PASS | dry-run 报告 `staging 实测就绪性 = NO(placeholder token)` |
|
||||
| 任务单证据口径已区分开发阶段/联调阶段 | PASS | TOK-005 行已更新为双阶段证据 |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 当前仅完成开发阶段 dry-run,不等价于 staging 联调达标。
|
||||
2. `M-015`(需求方绕过平台直连供应方)仍需真实网络与策略环境实测。
|
||||
3. 生产放行仍受 `TOK-006/TOK-007` 与最终决议约束。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 待 `.env` 真值就绪后,执行:`bash scripts/supply-gate/staging_precheck_and_run.sh scripts/supply-gate/.env`。
|
||||
2. 联调完成后回填:`tests/supply/sec_sup_boundary_report_2026-03-30.md` 与 `review/final_decision_2026-03-31.md`。
|
||||
43
reports/alignment_validation_checkpoint_13_2026-03-30.md
Normal file
43
reports/alignment_validation_checkpoint_13_2026-03-30.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-13 / TOK-006 统一 Gate 汇总链路)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:完成 TOK-006 汇总脚本、单页判定模板、实跑证据与文档并入
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。TOK-006 已形成“统一汇总脚本 + 单页判定模板 + 实跑证据 + 任务口径”闭环。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/supply-gate/tok006_gate_bundle.sh`
|
||||
2. `reports/gates/tok006_gate_bundle_2026-03-30_091849.md`
|
||||
3. `reports/gates/tok006_gate_bundle_2026-03-30_091849.log`
|
||||
4. `reports/gates/tok006_release_decision_onepager_template_v1_2026-03-30.md`
|
||||
5. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
6. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
|
||||
7. `reports/gates/tok005_dryrun_2026-03-30_091849.md`
|
||||
8. `tests/supply/ui_sup_acc_report_2026-03-28.md`
|
||||
9. `tests/supply/ui_sup_pkg_report_2026-03-29.md`
|
||||
10. `tests/supply/ui_sup_set_report_2026-03-29.md`
|
||||
11. `tests/supply/sec_sup_boundary_report_2026-03-30.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| TOK-006 汇总脚本可执行且可生成单页结论 | PASS | `tok006_gate_bundle.sh` |
|
||||
| 汇总范围覆盖 TOK-005 + SUP-004~007 | PASS | `tok006_gate_bundle_2026-03-30_091849.md` Gate 矩阵 |
|
||||
| 发布判定规则满足“有 mock 或 readiness!=YES 不得 GO” | PASS | 同上(输出 `CONDITIONAL_GO`) |
|
||||
| 单页判定模板可复用且字段齐全 | PASS | `tok006_release_decision_onepager_template_v1_2026-03-30.md` |
|
||||
| 命令手册已纳入 TOK-006 执行入口 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
|
||||
| 任务单 TOK-006 证据口径已区分开发/联调阶段 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md` |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 当前汇总判定为 `CONDITIONAL_GO`,原因是现有 SUP 证据为 mock,且 TOK-005 readiness 为 NO(占位 token)。
|
||||
2. 本轮不伪造 staging 结果;真实放行仍依赖 `staging_precheck_and_run.sh` 实测证据。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. `.env` 真值就绪后,执行:`ENABLE_SUP_RUN=1 bash scripts/supply-gate/tok006_gate_bundle.sh scripts/supply-gate/.env`。
|
||||
2. 实测通过后将单页判定切换为 staging 证据版本,并回填 `review/final_decision_2026-03-31.md`。
|
||||
46
reports/alignment_validation_checkpoint_14_2026-03-30.md
Normal file
46
reports/alignment_validation_checkpoint_14_2026-03-30.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-14 / Superpowers 严格分阶段验证)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:新增并执行 `scripts/ci/superpowers_stage_validate.sh`,完成阶段化验证与证据回填
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。已按 superpowers 方式完成“代码测试 + SUP 脚本 + TOK 门禁 + 质量门禁 + staging 预检”的严格阶段验证。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `reports/gates/superpowers_stage_validation_2026-03-30_120619.md`
|
||||
3. `reports/gates/superpowers_stage_validation_2026-03-30_120619.log`
|
||||
4. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase01_go_test.log`
|
||||
5. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase02_sup_run_all_mock.log`
|
||||
6. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase03_tok005_dryrun_mock.log`
|
||||
7. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase04_tok006_bundle.log`
|
||||
8. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase05_dependency_audit.log`
|
||||
9. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase06_stage_gate_drill.log`
|
||||
10. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase07_staging_precheck.log`
|
||||
11. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
12. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 阶段验证脚本可执行且可复跑 | PASS | `scripts/ci/superpowers_stage_validate.sh` |
|
||||
| 代码测试阶段(PHASE-01)通过 | PASS | `phase01_go_test.log` |
|
||||
| SUP 本地联调阶段(PHASE-02)通过 | PASS | `phase02_sup_run_all_mock.log` |
|
||||
| TOK-005/TOK-006 阶段(PHASE-03/04)通过 | PASS | `phase03_tok005_dryrun_mock.log` + `phase04_tok006_bundle.log` |
|
||||
| 依赖/阶段门禁阶段(PHASE-05/06)通过 | PASS | `phase05_dependency_audit.log` + `phase06_stage_gate_drill.log` |
|
||||
| 真实 staging 预检阶段(PHASE-07)按规则 DEFERRED | PASS | `phase07_staging_precheck.log`(placeholder token) |
|
||||
| 总判定逻辑符合门禁规则 | PASS | `superpowers_stage_validation_2026-03-30_120619.md`(CONDITIONAL_GO) |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 本轮 `PHASE-07` 为 DEFERRED,不等价于 staging 联调通过。
|
||||
2. 因缺少真实 token 与真实 API_BASE_URL,当前不能产生生产 GO 结论。
|
||||
3. 其余可执行阶段均已按返回码与证据路径验证通过。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. `.env` 真值就绪后重跑同一脚本,目标将 PHASE-07 从 DEFERRED 收敛为 PASS。
|
||||
2. 重跑后更新 `reports/gates/superpowers_stage_validation_*.md` 并触发 TOK-007 决议复审。
|
||||
39
reports/alignment_validation_checkpoint_15_2026-03-30.md
Normal file
39
reports/alignment_validation_checkpoint_15_2026-03-30.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-15 / TOK-007 复审自动化)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:新增 TOK-007 复审脚本并实跑,完成任务链路与命令手册回填
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。TOK-007 已具备可执行复审入口,可自动汇总 TOK-006/Superpowers/SUP Gate 结果并生成复审报告。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/tok007_release_recheck.sh`
|
||||
2. `review/outputs/tok007_release_recheck_2026-03-30_121727.md`
|
||||
3. `reports/gates/tok007_release_recheck_2026-03-30_121727.log`
|
||||
4. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
5. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
|
||||
6. `reports/gates/tok006_gate_bundle_2026-03-30_120620.md`
|
||||
7. `reports/gates/superpowers_stage_validation_2026-03-30_120619.md`
|
||||
8. `reports/supply_gate_review_2026-03-31.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| TOK-007 脚本可执行并可复跑 | PASS | `scripts/ci/tok007_release_recheck.sh` |
|
||||
| 复审输入源覆盖 TOK-006/Superpowers/SUP Gate | PASS | `tok007_release_recheck_2026-03-30_121727.md` |
|
||||
| 输出结论与当前状态一致(CONDITIONAL GO) | PASS | 同上(机判结论) |
|
||||
| 命令手册已纳入 TOK-007 执行入口 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
|
||||
| 任务单 TOK-007 已区分开发阶段/联调阶段证据 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md` |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 当前复审结论仍为 `CONDITIONAL GO`,因为 staging 真值未就绪,真实联调阶段尚未收敛。
|
||||
2. 自动化复审不替代专家签署,仅用于复审前的结构化证据汇总。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. staging 参数就绪后,重跑 `superpowers_stage_validate.sh` 与 `tok006_gate_bundle.sh`。
|
||||
2. 复跑 `tok007_release_recheck.sh` 后,将输出回填到 `review/final_decision_2026-03-31.md`。
|
||||
40
reports/alignment_validation_checkpoint_16_2026-03-30.md
Normal file
40
reports/alignment_validation_checkpoint_16_2026-03-30.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-16 / 决议一致性校验并入 TOK-007)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:新增并执行 `final_decision_consistency_check.sh`,并将其并入 TOK-007 证据链
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。TOK-007 已具备“自动复审 + 最终决议一致性校验”双重门禁能力。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/final_decision_consistency_check.sh`
|
||||
2. `reports/gates/final_decision_consistency_2026-03-30_*.md`
|
||||
3. `reports/gates/final_decision_consistency_2026-03-30_*.log`
|
||||
4. `scripts/ci/tok007_release_recheck.sh`
|
||||
5. `review/outputs/tok007_release_recheck_2026-03-30_122908.md`
|
||||
6. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
7. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
|
||||
8. `review/final_decision_2026-03-31.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 一致性校验脚本可执行 | PASS | `scripts/ci/final_decision_consistency_check.sh` |
|
||||
| 三源结论可解析(final/tok007/superpowers) | PASS | `final_decision_consistency_2026-03-30_*.md` |
|
||||
| final 与 tok007 不一致时输出 WARN(不自动改签署结论) | PASS | 同上(`RESULT=WARN`) |
|
||||
| 命令手册已纳入一致性校验步骤 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
|
||||
| TOK-007 任务证据口径已扩展为双脚本 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md` |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 当前一致性状态为 `WARN`:`final_decision=NO_GO`,`TOK-007=CONDITIONAL_GO`。
|
||||
2. 该状态说明“决议文档尚未按最新复审自动结论更新”,不代表可直接生产 GO。
|
||||
3. 真实 staging 阶段未收敛前,不建议变更最终签署结论。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. staging 真值就绪后,按顺序重跑:`superpowers_stage_validate` -> `tok007_release_recheck` -> `final_decision_consistency_check`。
|
||||
2. 当 `PHASE-07=PASS` 且一致性为 PASS 时,再提交最终决议签署更新。
|
||||
38
reports/alignment_validation_checkpoint_17_2026-03-30.md
Normal file
38
reports/alignment_validation_checkpoint_17_2026-03-30.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-17 / TOK-007 候选决议稿生成)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:新增并执行 `tok007_generate_final_decision_candidate.sh`
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。TOK-007 已补齐“候选决议稿自动生成”能力,实现不改原件前提下的可审阅回填。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/tok007_generate_final_decision_candidate.sh`
|
||||
2. `review/outputs/final_decision_candidate_from_tok007_2026-03-30_123719.md`
|
||||
3. `reports/gates/tok007_generate_candidate_2026-03-30_123719.log`
|
||||
4. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
5. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
|
||||
6. `review/final_decision_2026-03-31.md`
|
||||
7. `review/outputs/tok007_release_recheck_2026-03-30_122908.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 候选稿生成脚本可执行 | PASS | `scripts/ci/tok007_generate_final_decision_candidate.sh` |
|
||||
| 输入来源正确(final_decision + tok007_recheck) | PASS | `tok007_generate_candidate_2026-03-30_123719.log` |
|
||||
| 输出候选稿不覆盖原签署文件 | PASS | `review/outputs/final_decision_candidate_from_tok007_2026-03-30_123719.md` |
|
||||
| 候选稿结论与 TOK-007 自动复审一致 | PASS | 同上(`CONDITIONAL GO`) |
|
||||
| 命令手册与任务单证据口径已同步 | PASS | 对应文档更新 |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 候选稿仅用于人工审阅,不代表签署生效结论。
|
||||
2. 真实 staging 阶段仍未收敛,最终签署建议保持谨慎。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. staging 真值就绪后重跑所有 TOK-007 链路脚本。
|
||||
2. 人工审阅候选稿后再更新正式签署版 `final_decision_2026-03-31.md`。
|
||||
44
reports/alignment_validation_checkpoint_18_2026-03-30.md
Normal file
44
reports/alignment_validation_checkpoint_18_2026-03-30.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-18 / M-017~M-019 指标修复与复跑)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:修复 `M-018` 统计异常并完成阶段链路复跑
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。指标链路已修复并纳入自动化复跑,阶段验证与TOK-007证据链保持一致。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/metrics_daily_snapshot.sh`
|
||||
2. `scripts/ci/metrics_trend_report.sh`
|
||||
3. `reports/gates/metrics_daily_snapshot_2026-03-30.md`
|
||||
4. `reports/gates/metrics_trend_7d_2026-03-30.md`
|
||||
5. `reports/gates/superpowers_stage_validation_2026-03-30_154103.md`
|
||||
6. `review/outputs/tok007_release_recheck_2026-03-30_154104.md`
|
||||
7. `reports/gates/final_decision_consistency_2026-03-30_154104.md`
|
||||
8. `review/outputs/final_decision_candidate_from_tok007_2026-03-30_154104.md`
|
||||
9. `reports/gates/superpowers_release_pipeline_2026-03-30_154103.md`
|
||||
10. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
11. `reports/superpowers_execution_progress_2026-03-27.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| M-018 计算逻辑恢复正确(不再出现 236.36%) | PASS | `metrics_daily_snapshot_2026-03-30.md`(`pass_steps=8/9`) |
|
||||
| 日快照写入会自动清理 debug 行 | PASS | `scripts/ci/metrics_daily_snapshot.sh` |
|
||||
| 趋势统计仅使用标准日期记录 | PASS | `scripts/ci/metrics_trend_report.sh` + `metrics_trend_7d_2026-03-30.md` |
|
||||
| Superpowers PHASE-08/09 可执行并通过 | PASS | `superpowers_stage_validation_2026-03-30_154103.md` |
|
||||
| TOK-007 复审链复跑后证据一致 | PASS | `tok007_release_recheck_2026-03-30_154104.md` + `final_decision_consistency_2026-03-30_154104.md` |
|
||||
| 总控流水可复跑且步骤全 PASS | PASS | `superpowers_release_pipeline_2026-03-30_154103.md` |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 真实 staging 凭证仍未就绪,PHASE-07 继续按规则保持 DEFERRED。
|
||||
2. 结论维持 `CONDITIONAL_GO/NO_GO` 防线,不得提前判定生产 `GO`。
|
||||
3. 历史 debug 文件可保留用于审计回溯,但不会进入趋势统计口径。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 进入真实 staging 联调窗口后,复跑 `superpowers_release_pipeline.sh` 获取可签署证据。
|
||||
2. 联调完成后更新 `review/final_decision_2026-03-31.md` 与对应签署记录。
|
||||
46
reports/alignment_validation_checkpoint_19_2026-03-30.md
Normal file
46
reports/alignment_validation_checkpoint_19_2026-03-30.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-19 / TOK-REAL 与 M-021 接入)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:新增 token API 服务实现并将 M-021 接入阶段门禁
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。TOK-REAL-001/003 的“无实现/无构建工件”缺口已明显收敛,M-021 已具备自动化计算与门禁接入能力。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `platform-token-runtime/cmd/platform-token-runtime/main.go`
|
||||
2. `platform-token-runtime/internal/httpapi/token_api.go`
|
||||
3. `platform-token-runtime/internal/httpapi/token_api_test.go`
|
||||
4. `platform-token-runtime/internal/auth/service/inmemory_runtime.go`
|
||||
5. `platform-token-runtime/Dockerfile`
|
||||
6. `scripts/ci/token_runtime_readiness_check.sh`
|
||||
7. `scripts/ci/superpowers_stage_validate.sh`
|
||||
8. `scripts/ci/superpowers_release_pipeline.sh`
|
||||
9. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
10. `reports/gates/token_runtime_readiness_2026-03-30_160246.md`
|
||||
11. `reports/gates/superpowers_stage_validation_2026-03-30_160244.md`
|
||||
12. `reports/gates/superpowers_release_pipeline_2026-03-30_160244.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| Token API 服务具备可执行入口 | PASS | `cmd/platform-token-runtime/main.go` |
|
||||
| `issue/refresh/revoke/introspect` 主接口实现存在 | PASS | `internal/httpapi/token_api.go` |
|
||||
| API 级行为具备可执行测试覆盖 | PASS | `internal/httpapi/token_api_test.go` |
|
||||
| runtime 可构建并通过测试 | PASS | `token_runtime_go_build_*.log` + `token_runtime_go_test_*.log` |
|
||||
| M-021 自动化脚本可计算并输出结论 | PASS | `scripts/ci/token_runtime_readiness_check.sh` + readiness 报告 |
|
||||
| Superpowers 阶段门禁已纳入 M-021 | PASS | `superpowers_stage_validation_2026-03-30_160244.md`(PHASE-10 PASS) |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. M-021=100% 仅表示“开发阶段实现就绪”,不代表真实 staging 已验收通过。
|
||||
2. PHASE-07 仍为 DEFERRED(真实 URL 与短期 token 未就绪),因此总门禁结论仍为 `CONDITIONAL_GO`。
|
||||
3. 最终签署结论仍需以真实联调证据替换 mock 证据后更新。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 进入联调窗口后,使用真实 `.env` 执行 `staging_precheck_and_run.sh`。
|
||||
2. 在真实 staging 复跑 `superpowers_release_pipeline.sh`,并更新最终签署稿。
|
||||
3. 若要进一步关闭 TOK-REAL-002,补齐审计事件入库与查询证明链(含租户维度查询样例)。
|
||||
48
reports/alignment_validation_checkpoint_20_2026-03-30.md
Normal file
48
reports/alignment_validation_checkpoint_20_2026-03-30.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-20 / TOK-REAL-002 审计查询与差距复审)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:补齐 token 审计查询能力并更新 TOK-REAL 差距结论
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**开发阶段对齐通过。token 审计查询能力已并入实现与契约,M-021 指标覆盖从 9 项扩展到 12 项且全部通过。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `platform-token-runtime/internal/auth/service/token_verifier.go`
|
||||
2. `platform-token-runtime/internal/auth/service/inmemory_runtime.go`
|
||||
3. `platform-token-runtime/internal/httpapi/token_api.go`
|
||||
4. `platform-token-runtime/internal/httpapi/token_api_test.go`
|
||||
5. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`
|
||||
6. `sql/postgresql/token_runtime_schema_v1.sql`
|
||||
7. `scripts/ci/token_runtime_readiness_check.sh`
|
||||
8. `scripts/ci/superpowers_stage_validate.sh`
|
||||
9. `scripts/ci/superpowers_release_pipeline.sh`
|
||||
10. `reports/gates/token_runtime_readiness_2026-03-30_173728.md`
|
||||
11. `reports/gates/superpowers_stage_validation_2026-03-30_173726.md`
|
||||
12. `reports/gates/superpowers_release_pipeline_2026-03-30_173726.md`
|
||||
13. `reports/token_runtime_implementation_gap_review_2026-03-30.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 审计查询接口已落地(代码) | PASS | `token_api.go`(`/api/v1/platform/tokens/audit-events`) |
|
||||
| 审计查询接口已落地(契约) | PASS | `platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml` |
|
||||
| 审计查询能力具备可执行测试 | PASS | `token_api_test.go` |
|
||||
| token 运行态持久化表结构工件存在 | PASS | `sql/postgresql/token_runtime_schema_v1.sql` |
|
||||
| M-021 检查项扩展后仍 100% | PASS | `token_runtime_readiness_2026-03-30_173728.md`(13/13) |
|
||||
| 阶段门禁与总控流水复跑通过 | PASS | `superpowers_stage_validation_2026-03-30_173726.md` + `superpowers_release_pipeline_2026-03-30_173726.md` |
|
||||
| TOK-REAL 差距结论已更新为“开发收敛+联调待闭环” | PASS | `token_runtime_implementation_gap_review_2026-03-30.md` |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. 真实 staging 凭证仍未就绪,PHASE-07 继续 DEFERRED。
|
||||
2. 因存在真实联调缺口,发布结论仍不得上调为生产 `GO`。
|
||||
3. 本轮只关闭开发阶段能力缺口,不替代真实环境验收。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 进入真实联调窗口后执行 staging 全链路复跑并回填。
|
||||
2. 更新最终签署稿中 M-021 与 TOK-REAL 风险状态。
|
||||
3. 将 token 审计查询结果并入安全看板与取证流程(租户/主体维度)。
|
||||
46
reports/alignment_validation_checkpoint_21_2026-03-30.md
Normal file
46
reports/alignment_validation_checkpoint_21_2026-03-30.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-21 / 联调前收口与决议口径同步)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:完成 staging 预检增强、决议文档口径同步、TOK-007 证据链复跑
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。已将“开发阶段能力收敛”与“真实 staging 待验”明确分离,避免对 M-021 与 token 风险做错误外推。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/supply-gate/staging_precheck_and_run.sh`
|
||||
2. `reports/gates/staging_token_go_evidence_template_v1_2026-03-30.md`
|
||||
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
4. `review/final_decision_2026-03-31.md`
|
||||
5. `reports/supply_gate_review_2026-03-31.md`
|
||||
6. `reports/token_runtime_implementation_gap_review_2026-03-30.md`
|
||||
7. `reports/gates/token_runtime_readiness_2026-03-30_181926.md`
|
||||
8. `reports/gates/superpowers_stage_validation_2026-03-30_181925.md`
|
||||
9. `reports/gates/superpowers_release_pipeline_2026-03-30_181925.md`
|
||||
10. `review/outputs/tok007_release_recheck_2026-03-30_182149.md`
|
||||
11. `reports/gates/final_decision_consistency_2026-03-30_182149.md`
|
||||
12. `review/outputs/final_decision_candidate_from_tok007_2026-03-30_182149.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| staging 预检已纳入 M-021 前置检查 | PASS | `staging_precheck_and_run.sh` |
|
||||
| 联调证据回填模板可直接执行 | PASS | `staging_token_go_evidence_template_v1_2026-03-30.md` |
|
||||
| Final Decision 中 M-021 口径与当前实现一致 | PASS | `review/final_decision_2026-03-31.md` |
|
||||
| SUP 汇总风险描述与 TOK 差距复审一致 | PASS | `reports/supply_gate_review_2026-03-31.md` + `reports/token_runtime_implementation_gap_review_2026-03-30.md` |
|
||||
| TOK-007 复审已显式纳入 M-021 输入 | PASS | `tok007_release_recheck_2026-03-30_181927.md` |
|
||||
| 阶段验证与总控流水可复跑且通过 | PASS | `superpowers_stage_validation_2026-03-30_181925.md` + `superpowers_release_pipeline_2026-03-30_181925.md` |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. PHASE-07 仍为 DEFERRED,说明真实 staging 参数尚未完成闭环。
|
||||
2. 当前结论仍应保持 `CONDITIONAL_GO/NO_GO`,不得提前判定生产 `GO`。
|
||||
3. 本次更新重点是“口径对齐与防误判”,不替代真实联调结果。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 使用模板执行真实 staging 回填,补齐 M-013~M-016 与 M-021 的生产口径证据。
|
||||
2. 回填完成后重跑 `superpowers_release_pipeline.sh` 并更新签署版 `final_decision`。
|
||||
3. 若 PHASE-07 转为 PASS,再触发下一轮专家复审。
|
||||
46
reports/alignment_validation_checkpoint_22_2026-03-30.md
Normal file
46
reports/alignment_validation_checkpoint_22_2026-03-30.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-22 / 联调自动化补齐与双口径决议)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:新增 staging 自动化脚本与 final_decision 双口径指标表
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。已把“联调前准备”从人工流程提升为可执行脚本,并将决议文档升级为开发口径与 staging 口径并行展示,降低误判风险。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/staging_evidence_autofill.sh`
|
||||
2. `scripts/ci/staging_release_pipeline.sh`
|
||||
3. `scripts/ci/superpowers_stage_validate.sh`
|
||||
4. `scripts/supply-gate/staging_precheck_and_run.sh`
|
||||
5. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
6. `review/final_decision_2026-03-31.md`
|
||||
7. `review/outputs/final_decision_candidate_from_tok007_2026-03-30_182830.md`
|
||||
8. `reports/gates/staging_token_go_evidence_autofill_2026-03-30_182910.md`
|
||||
9. `reports/gates/superpowers_release_pipeline_2026-03-30_182827.md`
|
||||
10. `reports/gates/superpowers_stage_validation_2026-03-30_182827.md`
|
||||
11. `reports/gates/token_runtime_readiness_2026-03-30_182829.md`
|
||||
12. `review/outputs/tok007_release_recheck_2026-03-30_182830.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| staging 证据自动回填脚本可执行 | PASS | `staging_evidence_autofill_2026-03-30_182910.md` |
|
||||
| staging 一键流水脚本已落地(可串联3步) | PASS | `scripts/ci/staging_release_pipeline.sh` |
|
||||
| PHASE-07 已支持自定义 env 文件 | PASS | `superpowers_stage_validate.sh`(`STAGING_ENV_FILE`) |
|
||||
| final_decision 指标表已改为双口径 | PASS | `review/final_decision_2026-03-31.md` |
|
||||
| TOK-007 候选稿与双口径保持一致 | PASS | `final_decision_candidate_from_tok007_2026-03-30_182830.md` |
|
||||
| 总控流水可复跑并通过 | PASS | `superpowers_release_pipeline_2026-03-30_182827.md` |
|
||||
|
||||
## 4. 限制与说明
|
||||
|
||||
1. `PHASE-07` 当前仍 `DEFERRED`,说明真实 staging 参数尚未闭环。
|
||||
2. `staging_evidence_autofill.sh` 仅做草稿抽取,不替代人工签署。
|
||||
3. 双口径表的 staging 列仍待真实联调回填,当前不能上调为生产 `GO`。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 使用真实 `.env` 执行 `scripts/ci/staging_release_pipeline.sh`。
|
||||
2. 以真实证据覆盖模板并更新 `final_decision` 签署页。
|
||||
3. 若 PHASE-07 转 PASS,发起下一轮专家复审会。
|
||||
49
reports/alignment_validation_checkpoint_23_2026-03-30.md
Normal file
49
reports/alignment_validation_checkpoint_23_2026-03-30.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-23 / staging防误跑与证据绑定增强)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:补齐 staging 流水防误跑机制与证据输入绑定能力
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。已补齐“local/mock 防误跑确认 + 自动拉起 mock 演练 + 证据文件显式绑定”三项缺口,且验证链路可复跑。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/staging_evidence_autofill.sh`
|
||||
2. `scripts/ci/staging_release_pipeline.sh`
|
||||
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
4. `reports/gates/staging_release_pipeline_2026-03-30_185530.md`
|
||||
5. `reports/gates/staging_token_go_evidence_autofill_2026-03-30_185535.md`
|
||||
6. `reports/gates/staging_token_go_evidence_autofill_manual_bind_2026-03-30_1853.md`
|
||||
7. `reports/gates/superpowers_stage_validation_2026-03-30_185531.md`
|
||||
8. `reports/gates/superpowers_release_pipeline_2026-03-30_185531.md`
|
||||
9. `review/outputs/tok007_release_recheck_2026-03-30_185535.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| `staging_evidence_autofill.sh` 支持显式输入证据文件(非 latest 模式) | PASS | `staging_token_go_evidence_autofill_manual_bind_2026-03-30_1853.md` |
|
||||
| `staging_release_pipeline.sh` 检测 local/mock env 并要求显式确认 | PASS | 无 `ALLOW_LOCAL_MOCK_STAGING` 时命令返回失败(日志已验证) |
|
||||
| local/mock 显式确认后可自动拉起 mock server 并串行完成 3 步流水 | PASS | `staging_release_pipeline_2026-03-30_185530.md` |
|
||||
| STEP-03 回填脚本已绑定本次流水证据路径 | PASS | `staging_token_go_evidence_autofill_2026-03-30_185535.md` |
|
||||
| Superpowers 主链结果保持 `CONDITIONAL_GO` 防线(不误判为 GO) | PASS | `superpowers_stage_validation_2026-03-30_185531.md` |
|
||||
|
||||
## 4. 差异与改进点
|
||||
|
||||
1. 新增 `staging_evidence_autofill.sh` 参数:`--staging-run-log`、`--stage-report`、`--token-readiness`、`--tok007-report`、`--pipeline-report`、`--sec-report`、`--out-file`。
|
||||
2. 新增 `staging_release_pipeline.sh` 防误跑逻辑:检测 local/mock 环境且未确认时立即失败。
|
||||
3. 新增 local/mock 演练可执行保障:`ALLOW_LOCAL_MOCK_STAGING=1` 时,若本地 API 不可达则自动尝试拉起 mock server。
|
||||
4. 文档同步:命令手册补充了防误跑开关与显式证据绑定示例。
|
||||
|
||||
## 5. 限制与说明
|
||||
|
||||
1. 本次通过基于 local/mock 演练,不能替代真实 staging 证据。
|
||||
2. `TOK-007` 最新机判仍为 `CONDITIONAL_GO`,与“真实参数未就绪”状态一致。
|
||||
3. 真实放行仍需:真实 `scripts/supply-gate/.env` + PHASE-07 真机复跑 + Final Decision 签署更新。
|
||||
|
||||
## 6. 下一步
|
||||
|
||||
1. 将真实 API_BASE_URL 与短期 token 写入 `scripts/supply-gate/.env`。
|
||||
2. 执行:`bash scripts/ci/staging_release_pipeline.sh scripts/supply-gate/.env`。
|
||||
3. 使用 `staging_token_go_evidence_autofill_*.md` 草稿回填真实证据并更新 `review/final_decision_2026-03-31.md`。
|
||||
52
reports/alignment_validation_checkpoint_24_2026-03-30.md
Normal file
52
reports/alignment_validation_checkpoint_24_2026-03-30.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-24 / 真实参数验证回归)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:使用真实 `API_BASE_URL + token` 执行 staging 发布流水验证
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐未通过(NO_GO)。根因不是脚本执行框架,而是接口契约不匹配:当前 URL 指向上游提供方接口,不是 SUP-004~SUP-007 预期的平台 API。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/supply-gate/.env`(真实值注入)
|
||||
2. `scripts/ci/staging_release_pipeline.sh`
|
||||
3. `scripts/supply-gate/staging_precheck_and_run.sh`
|
||||
4. `scripts/supply-gate/run_all.sh`
|
||||
5. `scripts/supply-gate/sup004_accounts.sh`
|
||||
6. `reports/gates/staging_release_pipeline_2026-03-30_205035.md`
|
||||
7. `reports/gates/step-01_2026-03-30_205035.out.log`
|
||||
8. `tests/supply/artifacts/sup004/01_verify.json`
|
||||
9. `tests/supply/artifacts/sup004/02_create.json`
|
||||
10. `reports/gates/superpowers_release_pipeline_2026-03-30_205037.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| API_BASE_URL 连通性(HEAD) | PASS | `step-01_2026-03-30_205035.out.log` 中 `reachable` |
|
||||
| TOK-005 dry-run + M-021 readiness | PASS | `step-01_2026-03-30_205035.out.log`(readiness 100%) |
|
||||
| SUP-004 首个业务接口返回契约可解析 JSON | FAIL | `tests/supply/artifacts/sup004/01_verify.json` 为 nginx `404 Not Found` HTML |
|
||||
| staging_release_pipeline 总结果 | FAIL | `staging_release_pipeline_2026-03-30_205035.md` |
|
||||
| superpowers_release_pipeline 总结果 | FAIL | `superpowers_release_pipeline_2026-03-30_205037.md` |
|
||||
|
||||
## 4. 根因分析
|
||||
|
||||
1. `sup004_accounts.sh` 固定访问:`{API_BASE_URL}/api/v1/supply/accounts/verify`。
|
||||
2. 当前提供的 `API_BASE_URL=https://api.minimaxi.com/anthropic`,拼接后为:
|
||||
`https://api.minimaxi.com/anthropic/api/v1/supply/accounts/verify`。
|
||||
3. 该地址返回 HTML 404(非平台契约 JSON),导致 `jq` 解析失败并中断 `run_all`。
|
||||
4. 因此当前失败判定为:**环境地址与 SUP 契约不匹配**,并非单纯 token 占位或脚本逻辑缺陷。
|
||||
|
||||
## 5. 影响评估
|
||||
|
||||
1. 不能据此判定 token 本身有效/无效(未命中正确业务契约)。
|
||||
2. 当前发布门禁链路维持 FAIL/NO_GO 是正确行为,防止误放行。
|
||||
3. 若继续沿用该 URL,SUP-004~007 全链路都会因契约错位失败。
|
||||
|
||||
## 6. 修复建议(下一步)
|
||||
|
||||
1. 提供“平台 SUP API 网关”基地址(应与 `/api/v1/supply/*` 契约匹配)。
|
||||
2. 若目标仅验证 Minimax token,请走独立“上游直连 smoke”脚本,不应复用 SUP 门禁脚本。
|
||||
3. 拿到正确平台地址后,重跑:
|
||||
`bash scripts/ci/staging_release_pipeline.sh scripts/supply-gate/.env`
|
||||
41
reports/alignment_validation_checkpoint_25_2026-03-30.md
Normal file
41
reports/alignment_validation_checkpoint_25_2026-03-30.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-25 / 开发阶段切回本地地址)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:按指示“开发阶段先用本地地址跑通,Minimax URL/token 仅作开发测试参考”
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。默认执行环境已切回本地 mock,开发门禁流水恢复可执行,且仍保持 `CONDITIONAL_GO` 防误判。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/supply-gate/.env`(已切回 local-mock 值)
|
||||
2. `scripts/supply-gate/.env.minimax-dev`(保留此前 Minimax 测试值)
|
||||
3. `scripts/ci/staging_release_pipeline.sh`
|
||||
4. `reports/gates/staging_release_pipeline_2026-03-30_212424.md`
|
||||
5. `reports/gates/superpowers_stage_validation_2026-03-30_212426.md`
|
||||
6. `review/outputs/tok007_release_recheck_2026-03-30_212430.md`
|
||||
7. `reports/gates/staging_token_go_evidence_autofill_2026-03-30_212430.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 默认 env 已可用于本地演练(local mock) | PASS | `staging_release_pipeline_2026-03-30_212424.md` |
|
||||
| staging 三步流水(STEP-01~03) | PASS | 同上报告,3/3 PASS |
|
||||
| superpowers 分阶段验证 | PASS(决策为 `CONDITIONAL_GO`) | `superpowers_stage_validation_2026-03-30_212426.md` |
|
||||
| TOK-007 复审机判 | PASS(机判 `CONDITIONAL_GO`) | `tok007_release_recheck_2026-03-30_212430.md` |
|
||||
| staging 自动回填草稿产出 | PASS | `staging_token_go_evidence_autofill_2026-03-30_212430.md` |
|
||||
|
||||
## 4. 说明
|
||||
|
||||
1. `CONDITIONAL_GO` 是预期:当前为 local/mock 演练证据,不可上调为真实 staging `GO`。
|
||||
2. Minimax URL/token 不能直接替代 SUP 平台契约地址(`/api/v1/supply/*`),此前已在 Checkpoint-24 记录。
|
||||
3. 当前做法是:
|
||||
- 开发门禁与流程联调用 local mock;
|
||||
- 上游 Minimax 能力验证应走独立 smoke(不混入 SUP 发布门禁判定)。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 需要时可新增 `scripts/supply-gate/minimax_upstream_smoke.sh`,单独校验 Minimax token 可用性。
|
||||
2. 当平台 staging API 网关地址可用后,恢复真实 env 并重跑完整门禁链路。
|
||||
37
reports/alignment_validation_checkpoint_26_2026-03-30.md
Normal file
37
reports/alignment_validation_checkpoint_26_2026-03-30.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-26 / Minimax 上游独立 Smoke 落地)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:新增“上游 Minimax 独立验证”能力,避免与 SUP 发布门禁链路耦合
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。已新增独立上游 smoke 脚本并完成实测,Minimax active 探测返回 200;SUP 发布门禁仍保持独立判定边界。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/supply-gate/minimax_upstream_smoke.sh`
|
||||
2. `docs/supply_gate_command_playbook_v1_2026-03-25.md`(新增第 20 节)
|
||||
3. `reports/gates/minimax_upstream_smoke_2026-03-30_231930.md`
|
||||
4. `tests/supply/artifacts/minimax_smoke_2026-03-30_231930/02_active_probe_body.json`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 独立 smoke 脚本可执行(语法 + 运行) | PASS | `minimax_upstream_smoke_2026-03-30_231930.md` |
|
||||
| Base 连通探测可达 | PASS | http_code=404(base 探测) |
|
||||
| Active 鉴权探测到达业务层并成功返回 | PASS | http_code=200,见 active probe body |
|
||||
| 结果分类与失败边界清晰 | PASS | 报告中 `PASS/PASS_AUTH_REACHED/FAIL_*` 规则 |
|
||||
| 与 SUP-004~SUP-007 门禁链路解耦 | PASS | 命令手册第20节说明“不可替代 SUP 门禁” |
|
||||
|
||||
## 4. 关键说明
|
||||
|
||||
1. `API_BASE_URL=https://api.minimaxi.com/anthropic` 在 base 地址上返回 404 属于可预期,不影响 active 路径探测。
|
||||
2. active 路径 `.../v1/messages` 返回 200,说明该 token 在当前 smoke 路径下可用。
|
||||
3. 该结果仅证明“上游可达 + 鉴权可用”,不等价于 SUP 平台业务契约通过。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 继续默认使用 local/mock 跑 SUP 开发门禁。
|
||||
2. 如需持续监控 Minimax 上游可用性,可将 `minimax_upstream_smoke.sh` 挂入定时健康检查。
|
||||
3. 等平台 staging 网关地址就绪后,再执行真实 SUP 门禁闭环。
|
||||
47
reports/alignment_validation_checkpoint_27_2026-03-30.md
Normal file
47
reports/alignment_validation_checkpoint_27_2026-03-30.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-27 / Minimax 监控化增强)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:将 Minimax 上游独立 smoke 进一步纳入“可持续执行”的日常快照链路
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。已完成 Minimax smoke 判定口径修正、dry-run 能力补齐、每日快照脚本落地,满足“开发期可持续执行 + 不误入 SUP 发布门禁”的要求。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/supply-gate/minimax_upstream_smoke.sh`
|
||||
2. `scripts/ci/minimax_upstream_daily_snapshot.sh`
|
||||
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
|
||||
4. `reports/gates/minimax_upstream_smoke_2026-03-30_232510.md`
|
||||
5. `reports/gates/minimax_upstream_daily_snapshot_2026-03-30.md`
|
||||
6. `reports/gates/minimax_upstream_daily_snapshots.csv`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| smoke 脚本支持 `MINIMAX_SMOKE_DRY_RUN=1`(不发网络请求) | PASS | `minimax_upstream_smoke_2026-03-30_232510.md` |
|
||||
| smoke 判定口径修正(base=连通、active=业务状态) | PASS | `scripts/supply-gate/minimax_upstream_smoke.sh` 判定规则段 |
|
||||
| 每日快照脚本可执行并产生日报 | PASS | `minimax_upstream_daily_snapshot_2026-03-30.md` |
|
||||
| 每日快照 CSV 可更新覆盖当日数据 | PASS | `minimax_upstream_daily_snapshots.csv` |
|
||||
| 快照默认优先引用非 dry-run 报告 | PASS | 2026-03-30 快照证据指向 `...231930.md`(active=200) |
|
||||
| 文档已补齐第 21 节命令与断言 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
|
||||
|
||||
## 4. 关键变更
|
||||
|
||||
1. `minimax_upstream_smoke.sh` 新增 dry-run 模式,适配“先联调再开真实请求”的执行策略。
|
||||
2. `minimax_upstream_smoke.sh` 的规则描述与结果口径保持一致,避免 404 base 探测与规则冲突。
|
||||
3. 新增 `scripts/ci/minimax_upstream_daily_snapshot.sh`,支持:
|
||||
- `RUN_ACTIVE_SMOKE=0`:仅汇总(默认);
|
||||
- `RUN_ACTIVE_SMOKE=1`:实时探测后汇总。
|
||||
4. 快照脚本默认优先选取“非 PASS_DRY_RUN”最新报告,降低误判风险。
|
||||
|
||||
## 5. 限制与说明
|
||||
|
||||
1. Minimax 快照仅用于上游可达性趋势,不可替代 SUP-004~SUP-007 门禁结论。
|
||||
2. 当前开发主链仍应使用 local/mock 维持持续迭代;真实 staging 仍待平台网关地址就绪。
|
||||
|
||||
## 6. 下一步
|
||||
|
||||
1. 如你同意,我可继续把 `minimax_upstream_daily_snapshot.sh` 接入 `superpowers_release_pipeline.sh` 的“可选监控步”(默认关闭)。
|
||||
2. 也可新增 7 日趋势脚本(类似 M-017~019)用于上游稳定性周报。
|
||||
39
reports/alignment_validation_checkpoint_28_2026-03-30.md
Normal file
39
reports/alignment_validation_checkpoint_28_2026-03-30.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-28 / Minimax 趋势与流水可选监控接入)
|
||||
|
||||
- 日期:2026-03-30
|
||||
- 触发条件:在不改变 SUP 主门禁判定边界的前提下,补齐 Minimax 上游 7 日趋势能力,并将其接入总控流水(默认关闭、非阻断)。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。Minimax 日快照 + 7 日趋势链路已可执行,且已通过 `superpowers_release_pipeline` 的可选监控步验证。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/minimax_upstream_trend_report.sh`(新增)
|
||||
2. `scripts/ci/superpowers_release_pipeline.sh`(新增 STEP-05 可选监控步)
|
||||
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`(新增第 22 节与可选开关说明)
|
||||
4. `reports/gates/minimax_upstream_trend_7d_2026-03-30.md`
|
||||
5. `reports/gates/superpowers_release_pipeline_2026-03-30_235224.md`
|
||||
6. `reports/gates/step-05_2026-03-30_235224.out.log`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| Minimax 7 日趋势脚本可执行并产出报告 | PASS | `reports/gates/minimax_upstream_trend_7d_2026-03-30.md` |
|
||||
| 趋势脚本在样本不足时给出 `INSUFFICIENT_DATA` 而非误报 PASS_7D | PASS | 同上(采样 1 天) |
|
||||
| 总控流水支持 `ENABLE_MINIMAX_MONITORING=1` 时执行 STEP-05 | PASS | `reports/gates/superpowers_release_pipeline_2026-03-30_235224.md` |
|
||||
| STEP-05 失败不阻断主门禁(非阻断监控定位) | PASS(逻辑校验) | `scripts/ci/superpowers_release_pipeline.sh` |
|
||||
| 新增命令文档与断言说明齐全 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
|
||||
|
||||
## 4. 执行记录说明
|
||||
|
||||
1. 首次在受限沙箱直接执行总控流水时,`STEP-01` 因无法绑定本地 `127.0.0.1:18080`(环境权限限制)失败。
|
||||
2. 在允许非沙箱执行后复跑,同一代码版本下 `STEP-01~STEP-05` 全部 PASS。
|
||||
3. 由此可判定失败原因为执行环境权限,不是本次代码改动引入的功能回归。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 继续按 `docs/plans/2026-03-30-superpowers-execution-tasklist-v2.md` 推进 `Batch-STG-01`(真实 staging 解锁)。
|
||||
2. 按日执行第 21 节快照,累计满 7 天后复跑第 22 节趋势,支撑 `F-03` 连续观测闭环。
|
||||
|
||||
51
reports/alignment_validation_checkpoint_29_2026-03-31.md
Normal file
51
reports/alignment_validation_checkpoint_29_2026-03-31.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-29 / STG 本地演练恢复)
|
||||
|
||||
- 日期:2026-03-31
|
||||
- 触发条件:继续执行 STG 批次(本机开发测试口径),修复 PHASE-10 阻塞后复跑整条流水。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。STG 本地演练流水已恢复 `PASS`,并明确保持 `local/mock` 与真实 staging 放行证据边界。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/token_runtime_readiness_check.sh`
|
||||
2. `reports/gates/staging_release_pipeline_2026-03-31_100116.md`
|
||||
3. `reports/gates/superpowers_release_pipeline_2026-03-31_100120.md`
|
||||
4. `reports/gates/superpowers_stage_validation_2026-03-31_100120.md`
|
||||
5. `review/outputs/tok007_release_recheck_2026-03-31_100127.md`
|
||||
|
||||
## 3. 问题与修复
|
||||
|
||||
### 3.1 发现的问题
|
||||
|
||||
1. `PHASE-10`(M-021)在 `ENABLE_TOKEN_RUNTIME_SMOKE=1` 场景下失败。
|
||||
2. 根因一:默认 smoke 端口 `18082` 被 `supply-api` 占用,冒烟请求命中错误服务(`issue` 返回 404)。
|
||||
3. 根因二:脚本 smoke 分支使用 `exit 1` 直接退出,失败时无法稳定产出完整汇总输出。
|
||||
|
||||
### 3.2 修复动作
|
||||
|
||||
1. 为 M-021 冒烟新增端口自动避让:从基准端口起寻找可用端口(最多 50 次)。
|
||||
2. 将 smoke 执行块改为子 Shell 返回码模型,保留失败但不中断总报告生成流程。
|
||||
|
||||
## 4. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| M-021 脚本修复后可执行 | PASS | `reports/gates/token_runtime_readiness_2026-03-31_100017.md` |
|
||||
| Superpowers 阶段验证恢复通过(PHASE-10 PASS) | PASS | `reports/gates/superpowers_stage_validation_2026-03-31_100120.md` |
|
||||
| Superpowers 发布流水恢复通过 | PASS | `reports/gates/superpowers_release_pipeline_2026-03-31_100120.md` |
|
||||
| STG 本地演练流水 STEP-01~03 全 PASS | PASS | `reports/gates/staging_release_pipeline_2026-03-31_100116.md` |
|
||||
| TOK-007 复审结论保持 `CONDITIONAL_GO`(未误升 GO) | PASS | `review/outputs/tok007_release_recheck_2026-03-31_100127.md` |
|
||||
|
||||
## 5. 结论边界说明
|
||||
|
||||
1. 本次通过仅代表 `local/mock` 演练链路恢复,不等价真实 staging 放行。
|
||||
2. `F-01/F-02/F-04` 的真实 staging 证据要求仍保持不变。
|
||||
|
||||
## 6. 下一步
|
||||
|
||||
1. 进入 STG-001:替换真实 `API_BASE_URL` 并完成可达性验证。
|
||||
2. 进入 STG-002:注入真实短期 token 并复跑 `staging_release_pipeline.sh`(真实环境)。
|
||||
3. 完成 STG-004:将真实证据回填至 `review/final_decision_2026-03-31.md` 与 `reports/supply_gate_review_2026-03-31.md`。
|
||||
|
||||
36
reports/alignment_validation_checkpoint_30_2026-03-31.md
Normal file
36
reports/alignment_validation_checkpoint_30_2026-03-31.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-30 / STG 端口基线固化)
|
||||
|
||||
- 日期:2026-03-31
|
||||
- 触发条件:按“先清理本机冲突进程并固化端口基线”继续执行 STG 批次。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。蚊子残留与关键冲突进程已清理,STG 本地演练在清理后可稳定复现 PASS。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `reports/gates/local_dev_port_baseline_2026-03-31.md`
|
||||
2. `reports/gates/staging_release_pipeline_2026-03-31_100942.md`
|
||||
3. `reports/gates/superpowers_release_pipeline_2026-03-31_100943.md`
|
||||
4. `scripts/ci/token_runtime_readiness_check.sh`(沿用 Checkpoint-29 修复)
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 蚊子残留端口 `5176/5177/8080` 已释放 | PASS | `reports/gates/local_dev_port_baseline_2026-03-31.md` |
|
||||
| M-021 历史冲突端口 `18082` 已释放 | PASS | 同上 |
|
||||
| 清理后 STG 本地流水可通过 | PASS | `reports/gates/staging_release_pipeline_2026-03-31_100942.md` |
|
||||
| 清理后 Superpowers 总控可通过 | PASS | `reports/gates/superpowers_release_pipeline_2026-03-31_100943.md` |
|
||||
| 结论边界保持(未误升为真实 staging GO) | PASS | `LOCAL_MOCK` 标记 + `CONDITIONAL_GO` 链路 |
|
||||
|
||||
## 4. 说明
|
||||
|
||||
1. 端口 `3000` 仍被占用,但不在 STG 本地演练关键端口集内,当前不构成阻塞。
|
||||
2. 本次结果仅覆盖“本机开发测试口径”;真实 staging 放行仍依赖 `STG-001/STG-002`。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 你确认真实 staging 地址后,我直接执行 `STG-001`。
|
||||
2. 你提供短期 token 后,我直接执行真实 `STG-002/003/004` 并回填最终决议证据。
|
||||
|
||||
37
reports/alignment_validation_checkpoint_31_2026-03-31.md
Normal file
37
reports/alignment_validation_checkpoint_31_2026-03-31.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-31 / 真实 STG 就绪度自动化)
|
||||
|
||||
- 日期:2026-03-31
|
||||
- 触发条件:继续执行实施计划,在“外网 STG 暂未申请”条件下补齐真实放行前置检查自动化。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。已新增“真实 STG 就绪度检查”能力,并已在当前本地配置下正确判定为 `BLOCKED`。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/generate_local_staging_env.sh`(一键生成本地 `.env.staging-real`)
|
||||
2. `scripts/ci/staging_real_readiness_check.sh`(真实 STG 前置检查)
|
||||
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`(新增第 23/24 节)
|
||||
4. `reports/gates/local_staging_env_generation_2026-03-31_105620.md`
|
||||
5. `reports/gates/staging_real_readiness_2026-03-31_110213.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 一键脚本可生成 owner/viewer/admin token 并写入 env | PASS | `local_staging_env_generation_2026-03-31_105620.md` |
|
||||
| 生成 env 可直接用于本地 STG 流水 | PASS | `staging_release_pipeline_2026-03-31_105633.md` |
|
||||
| 真实 STG 就绪度脚本可执行并生成报告 | PASS | `staging_real_readiness_2026-03-31_110213.md` |
|
||||
| 当前配置下(本地 URL)被判定 `BLOCKED` | PASS(预期) | `STG-RDY-004/008` 失败项 |
|
||||
| 命令手册完成同步 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
|
||||
|
||||
## 4. 当前阻塞结论
|
||||
|
||||
1. `API_BASE_URL` 仍是本地地址(`127.0.0.1`),不满足真实 STG 放行前提。
|
||||
2. 未申请外网地址前,实施计划只能继续按 local/mock 开发测试口径推进。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 外网 STG 地址可用后,更新 `.env.staging-real` 并重跑 `staging_real_readiness_check.sh`,目标从 `BLOCKED` 转为 `READY`。
|
||||
2. 通过就绪检查后执行真实 `staging_release_pipeline.sh`,并回填 `F-01/F-02/F-04` 证据闭环。
|
||||
|
||||
39
reports/alignment_validation_checkpoint_32_2026-03-31.md
Normal file
39
reports/alignment_validation_checkpoint_32_2026-03-31.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 规划设计对齐验证报告(Checkpoint-32 / 完整开发测试续跑)
|
||||
|
||||
- 日期:2026-03-31
|
||||
- 触发条件:用户确认继续完成项目完整开发测试,执行本地 STG 全链路续跑并复核真实 STG 前置状态。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
结论:**本阶段对齐通过。本地完整开发测试链路稳定 PASS,真实 STG 放行前置仍为 `BLOCKED`,结论边界保持一致。**
|
||||
|
||||
## 2. 对齐范围
|
||||
|
||||
1. `scripts/ci/generate_local_staging_env.sh`
|
||||
2. `scripts/ci/staging_release_pipeline.sh`
|
||||
3. `scripts/ci/staging_real_readiness_check.sh`
|
||||
4. `scripts/supply-gate/minimax_upstream_smoke.sh`
|
||||
5. `docs/plans/2026-03-30-superpowers-execution-tasklist-v2.md`
|
||||
6. `reports/superpowers_execution_progress_2026-03-27.md`
|
||||
|
||||
## 3. 核查结果
|
||||
|
||||
| 核查项 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| `.env.staging-real` 已重新生成并包含 owner/viewer/admin 三类 token | PASS | `reports/gates/local_staging_env_generation_2026-03-31_123102.md` |
|
||||
| 本地 STG 发布流水可重复通过 | PASS | `reports/gates/staging_release_pipeline_2026-03-31_123148.md` |
|
||||
| Superpowers 全链路与 TOK-007 复审可重复通过 | PASS | `reports/gates/superpowers_release_pipeline_2026-03-31_123150.md` + `review/outputs/tok007_release_recheck_2026-03-31_123153.md` |
|
||||
| 真实 STG 就绪检查准确识别当前阻塞 | PASS(预期) | `reports/gates/staging_real_readiness_2026-03-31_123159.md` |
|
||||
| Minimax 上游可达与鉴权调用保持通过 | PASS | `reports/gates/minimax_upstream_smoke_2026-03-31_123210.md` |
|
||||
|
||||
## 4. 阻塞与边界
|
||||
|
||||
1. `STG-RDY-004` 未关闭:`API_BASE_URL` 当前是本地地址 `http://127.0.0.1:18080`。
|
||||
2. `STG-RDY-008` 未关闭:真实 STG 可达性探测仍失败(`http_code=000`)。
|
||||
3. 因 `F-01/F-02/F-04` 仍未关闭,本轮不得上调到真实 `GO`,当前仅可维持 `CONDITIONAL_GO`(开发口径)。
|
||||
|
||||
## 5. 下一步
|
||||
|
||||
1. 将 `.env.staging-real` 的 `API_BASE_URL` 切换到可达的真实 STG 地址(内网或公网均可)。
|
||||
2. 注入真实环境可用的 owner/viewer/admin 平台 token,复跑 `staging_real_readiness_check.sh`,目标 `READY`。
|
||||
3. 就绪后执行真实口径 `staging_release_pipeline.sh`(不带 `ALLOW_LOCAL_MOCK_STAGING=1`),回填 `F-01/F-02/F-04` 证据。
|
||||
82
reports/db/indexes_2026-03-27.txt
Normal file
82
reports/db/indexes_2026-03-27.txt
Normal file
@@ -0,0 +1,82 @@
|
||||
audit_events_pkey
|
||||
auth_platform_api_keys_pkey
|
||||
auth_platform_api_keys_tenant_id_key_prefix_key
|
||||
billing_accounts_pkey
|
||||
billing_accounts_tenant_id_project_id_account_type_key
|
||||
billing_ledger_entries_pkey
|
||||
billing_ledger_entries_tenant_id_request_id_entry_type_key
|
||||
core_projects_pkey
|
||||
core_projects_tenant_id_project_code_key
|
||||
core_tenants_pkey
|
||||
core_tenants_tenant_code_key
|
||||
iam_users_pkey
|
||||
iam_users_tenant_id_email_key
|
||||
idx_audit_events_request_id
|
||||
idx_audit_events_result_code
|
||||
idx_audit_events_tenant_domain_time
|
||||
idx_audit_events_trace_id
|
||||
idx_auth_platform_api_keys_last_used_at
|
||||
idx_auth_platform_api_keys_project_status
|
||||
idx_auth_platform_api_keys_tenant_status
|
||||
idx_billing_accounts_tenant_status
|
||||
idx_billing_ledger_entries_account_time
|
||||
idx_billing_ledger_entries_tenant_time
|
||||
idx_billing_ledger_entries_trace_id
|
||||
idx_core_projects_tenant_status
|
||||
idx_core_tenants_plan_code
|
||||
idx_core_tenants_status
|
||||
idx_iam_users_tenant_role
|
||||
idx_iam_users_tenant_status
|
||||
idx_routing_policies_model_pattern
|
||||
idx_routing_policies_tenant_project_status
|
||||
idx_security_kms_key_registry_status
|
||||
idx_supply_accounts_platform
|
||||
idx_supply_accounts_platform_status_updated
|
||||
idx_supply_accounts_risk_level
|
||||
idx_supply_accounts_status
|
||||
idx_supply_accounts_user_id
|
||||
idx_supply_accounts_user_status_updated
|
||||
idx_supply_earnings_earned_at
|
||||
idx_supply_earnings_source_request_id
|
||||
idx_supply_earnings_status
|
||||
idx_supply_earnings_user_id
|
||||
idx_supply_earnings_user_status_available
|
||||
idx_supply_orders_buyer_status_created
|
||||
idx_supply_orders_buyer_user_id
|
||||
idx_supply_orders_request_id
|
||||
idx_supply_orders_status
|
||||
idx_supply_orders_supplier_status_created
|
||||
idx_supply_orders_supplier_user_id
|
||||
idx_supply_orders_supply_package_id
|
||||
idx_supply_packages_active_lookup
|
||||
idx_supply_packages_platform_model
|
||||
idx_supply_packages_platform_model_status
|
||||
idx_supply_packages_status
|
||||
idx_supply_packages_supply_account_id
|
||||
idx_supply_packages_user_id
|
||||
idx_supply_packages_user_status_updated
|
||||
idx_supply_settlements_period
|
||||
idx_supply_settlements_request_id
|
||||
idx_supply_settlements_status
|
||||
idx_supply_settlements_user_id
|
||||
idx_supply_settlements_user_status_updated
|
||||
idx_supply_usage_records_order_id
|
||||
idx_supply_usage_records_order_started
|
||||
idx_supply_usage_records_platform_model
|
||||
idx_supply_usage_records_request_id
|
||||
idx_supply_usage_records_started_at
|
||||
idx_supply_usage_records_supplier_started
|
||||
idx_supply_usage_records_supply_account_id
|
||||
idx_supply_usage_records_trace_id
|
||||
routing_policies_pkey
|
||||
security_kms_key_registry_key_alias_key
|
||||
security_kms_key_registry_pkey
|
||||
supply_accounts_pkey
|
||||
supply_earnings_pkey
|
||||
supply_orders_order_no_key
|
||||
supply_orders_pkey
|
||||
supply_packages_pkey
|
||||
supply_settlements_pkey
|
||||
supply_settlements_settlement_no_key
|
||||
supply_usage_records_pkey
|
||||
uq_supply_settlements_user_processing
|
||||
37
reports/db/key_columns_2026-03-27.txt
Normal file
37
reports/db/key_columns_2026-03-27.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
audit_events.idempotency_key
|
||||
audit_events.request_id
|
||||
billing_accounts.currency_code
|
||||
billing_ledger_entries.amount_unit
|
||||
billing_ledger_entries.currency_code
|
||||
billing_ledger_entries.idempotency_key
|
||||
billing_ledger_entries.request_id
|
||||
routing_policies.version
|
||||
supply_accounts.audit_trace_id
|
||||
supply_accounts.credential_cipher_algo
|
||||
supply_accounts.credential_fingerprint
|
||||
supply_accounts.credential_key_version
|
||||
supply_accounts.credential_kms_key_alias
|
||||
supply_accounts.currency_code
|
||||
supply_accounts.quota_unit
|
||||
supply_accounts.version
|
||||
supply_earnings.amount_unit
|
||||
supply_earnings.audit_trace_id
|
||||
supply_earnings.version
|
||||
supply_orders.audit_trace_id
|
||||
supply_orders.currency_code
|
||||
supply_orders.idempotency_key
|
||||
supply_orders.quota_unit
|
||||
supply_orders.request_id
|
||||
supply_orders.version
|
||||
supply_packages.audit_trace_id
|
||||
supply_packages.currency_code
|
||||
supply_packages.price_unit
|
||||
supply_packages.quota_unit
|
||||
supply_packages.version
|
||||
supply_settlements.amount_unit
|
||||
supply_settlements.audit_trace_id
|
||||
supply_settlements.currency_code
|
||||
supply_settlements.idempotency_key
|
||||
supply_settlements.request_id
|
||||
supply_settlements.version
|
||||
supply_usage_records.request_id
|
||||
15
reports/db/tables_2026-03-27.txt
Normal file
15
reports/db/tables_2026-03-27.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
audit_events
|
||||
auth_platform_api_keys
|
||||
billing_accounts
|
||||
billing_ledger_entries
|
||||
core_projects
|
||||
core_tenants
|
||||
iam_users
|
||||
routing_policies
|
||||
security_kms_key_registry
|
||||
supply_accounts
|
||||
supply_earnings
|
||||
supply_orders
|
||||
supply_packages
|
||||
supply_settlements
|
||||
supply_usage_records
|
||||
71
reports/db_schema_validation_report_2026-03-27.md
Normal file
71
reports/db_schema_validation_report_2026-03-27.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 数据库基线执行验证报告(2026-03-27)
|
||||
|
||||
- 执行环境:PostgreSQL 15 (`127.0.0.1:34603`)
|
||||
- 执行账号:`mosquito`
|
||||
- 验证库:`lijiaoqiao_design_review_20260327`
|
||||
- 执行人:Codex
|
||||
|
||||
---
|
||||
|
||||
## 1. 执行 SQL 清单
|
||||
|
||||
1. `sql/postgresql/platform_core_schema_v1.sql`
|
||||
2. `sql/postgresql/supply_schema_v1.sql`
|
||||
3. `sql/postgresql/supply_schema_v1_patch_2026-03-27.sql`
|
||||
|
||||
原始日志:
|
||||
1. `reports/db/sql_apply_2026-03-27.log`
|
||||
|
||||
---
|
||||
|
||||
## 2. 执行结果
|
||||
|
||||
1. 三份 SQL 均执行成功(全部到 `COMMIT`)。
|
||||
2. 表总数:`15`
|
||||
3. 索引总数:`82`
|
||||
4. 关键字段命中数:`37`
|
||||
|
||||
结构快照:
|
||||
1. `reports/db/tables_2026-03-27.txt`
|
||||
2. `reports/db/indexes_2026-03-27.txt`
|
||||
3. `reports/db/key_columns_2026-03-27.txt`
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键验收点核对
|
||||
|
||||
1. 跨域核心表(Core/IAM/Auth/Billing/Routing/Security/Audit)已创建。
|
||||
2. 供应域 patch 中加密字段已生效:
|
||||
- `credential_cipher_algo`
|
||||
- `credential_kms_key_alias`
|
||||
- `credential_key_version`
|
||||
- `credential_fingerprint`
|
||||
3. 单位字段已生效:
|
||||
- `quota_unit`
|
||||
- `price_unit`
|
||||
- `amount_unit`
|
||||
- `currency_code`
|
||||
4. 审计与幂等字段已生效:
|
||||
- `request_id`
|
||||
- `idempotency_key`
|
||||
- `audit_trace_id`
|
||||
- `version`
|
||||
5. 关键组合索引与部分索引已创建(含 `uq_supply_settlements_user_processing`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 问题与修复记录
|
||||
|
||||
1. 首次执行失败原因:新增 SQL 文件字符串默认值引号丢失。
|
||||
2. 修复动作:重写 `platform_core_schema_v1.sql` 与 `supply_schema_v1_patch_2026-03-27.sql`,统一字符串字面量语法。
|
||||
3. 修复后复跑结果:全部通过。
|
||||
|
||||
---
|
||||
|
||||
## 5. 结论
|
||||
|
||||
结论:**通过(设计层 SQL 可执行)**。
|
||||
|
||||
后续建议:
|
||||
1. 在目标测试环境执行同样脚本并对比 `EXPLAIN` 计划。
|
||||
2. 将执行日志纳入 `SUP-008` 与 `GO` 决策证据包。
|
||||
16
reports/dependency/compat_matrix_2026-03-27.md
Normal file
16
reports/dependency/compat_matrix_2026-03-27.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Dependency Compatibility Matrix(2026-03-27)
|
||||
|
||||
- Audit-Status: PASS
|
||||
|
||||
| Component | Baseline | Current | Result | Note |
|
||||
|---|---|---|---|---|
|
||||
| Go | 1.21.x | 1.21.x(文档基线) | PASS | 与架构基线一致 |
|
||||
| PostgreSQL | 15.x | 15.x(SQL 语法) | PASS | DDL 在 PG15 实测通过 |
|
||||
| Redis | 7.x | 7.x(文档基线) | PASS | 与架构基线一致 |
|
||||
| subapi | X.Y.Z fixed | 未变更 | PASS | 无依赖升级 |
|
||||
| Frontend Node | 20.x LTS | 未变更 | PASS | 无依赖升级 |
|
||||
|
||||
## Conclusion
|
||||
|
||||
1. 本次无 runtime 依赖变更。
|
||||
2. 兼容性审计结果可放行。
|
||||
10
reports/dependency/dependency_audit_result_2026-03-27.md
Normal file
10
reports/dependency/dependency_audit_result_2026-03-27.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Dependency Audit Check Result (2026-03-27)
|
||||
|
||||
- Result: PASS
|
||||
- M-017 (`dependency_compat_audit_pass_pct`): 100%
|
||||
- Checked files:
|
||||
1. reports/dependency/sbom_2026-03-27.spdx.json
|
||||
2. reports/dependency/lockfile_diff_2026-03-27.md
|
||||
3. reports/dependency/compat_matrix_2026-03-27.md
|
||||
4. reports/dependency/risk_register_2026-03-27.md
|
||||
|
||||
15
reports/dependency/lockfile_diff_2026-03-27.md
Normal file
15
reports/dependency/lockfile_diff_2026-03-27.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Lockfile Diff(2026-03-27)
|
||||
|
||||
- Audit-Status: PASS
|
||||
- Scope: Baseline document-only sync
|
||||
|
||||
## Summary
|
||||
|
||||
1. `go.mod/go.sum`:无本次变更。
|
||||
2. `package-lock.json` / `pnpm-lock.yaml`:无本次变更。
|
||||
3. `pom.xml`:无本次变更。
|
||||
|
||||
## Risk
|
||||
|
||||
1. 本次提交仅含文档与 SQL,不涉及应用依赖升级。
|
||||
2. 依赖风险等级:Low。
|
||||
14
reports/dependency/risk_register_2026-03-27.md
Normal file
14
reports/dependency/risk_register_2026-03-27.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Dependency Risk Register(2026-03-27)
|
||||
|
||||
- Audit-Status: PASS
|
||||
|
||||
| Risk ID | Risk | Severity | Mitigation | Owner | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| DEP-R-001 | 未锁定 subapi 精确版本导致回归 | High | 固定 `X.Y.Z` + 三重Gate | ARCH | Open |
|
||||
| DEP-R-002 | 锁文件漂移未触发审计 | Medium | CI 强制执行 dependency-audit-check | PLAT | Open |
|
||||
| DEP-R-003 | 漏洞库更新导致新 Critical CVE | High | 夜间扫描 + 发布阻断 | SEC | Open |
|
||||
|
||||
## Conclusion
|
||||
|
||||
1. 当前无新增依赖变更触发的阻断项。
|
||||
2. 风险条目已登记并进入持续治理。
|
||||
32
reports/dependency/sbom_2026-03-27.spdx.json
Normal file
32
reports/dependency/sbom_2026-03-27.spdx.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "lijiaoqiao-dependency-sbom-2026-03-27",
|
||||
"documentNamespace": "https://lijiaoqiao.local/sbom/2026-03-27",
|
||||
"creationInfo": {
|
||||
"created": "2026-03-27T12:00:00Z",
|
||||
"creators": [
|
||||
"Tool: codex-manual-baseline"
|
||||
]
|
||||
},
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-Go",
|
||||
"name": "go-runtime",
|
||||
"versionInfo": "1.21.x",
|
||||
"downloadLocation": "NOASSERTION"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-PostgreSQL",
|
||||
"name": "postgresql",
|
||||
"versionInfo": "15.x",
|
||||
"downloadLocation": "NOASSERTION"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-Redis",
|
||||
"name": "redis",
|
||||
"versionInfo": "7.x",
|
||||
"downloadLocation": "NOASSERTION"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
reports/design_drift_daily_2026-03-30-debug.md
Normal file
14
reports/design_drift_daily_2026-03-30-debug.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 需求-设计-测试漂移日检(2026-03-30-debug)
|
||||
|
||||
- 状态:**PASS**
|
||||
- 依据:M-019=100.00%(目标=100%)
|
||||
|
||||
## 检查结论
|
||||
|
||||
1. 若 M-019 < 100%,判定存在追踪漂移风险。
|
||||
2. 当前说明:tracked_rows=15/15
|
||||
|
||||
## 处理动作
|
||||
|
||||
1. 若 FAIL:24h 内补齐缺失追踪项并复跑本脚本。
|
||||
2. 若 PASS:纳入 7 日趋势统计。
|
||||
14
reports/design_drift_daily_2026-03-30.md
Normal file
14
reports/design_drift_daily_2026-03-30.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 需求-设计-测试漂移日检(2026-03-30)
|
||||
|
||||
- 状态:**PASS**
|
||||
- 依据:M-019=100.00%(目标=100%)
|
||||
|
||||
## 检查结论
|
||||
|
||||
1. 若 M-019 < 100%,判定存在追踪漂移风险。
|
||||
2. 当前说明:tracked_rows=15/15
|
||||
|
||||
## 处理动作
|
||||
|
||||
1. 若 FAIL:24h 内补齐缺失追踪项并复跑本脚本。
|
||||
2. 若 PASS:纳入 7 日趋势统计。
|
||||
14
reports/design_drift_daily_2026-03-31.md
Normal file
14
reports/design_drift_daily_2026-03-31.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 需求-设计-测试漂移日检(2026-03-31)
|
||||
|
||||
- 状态:**PASS**
|
||||
- 依据:M-019=100.00%(目标=100%)
|
||||
|
||||
## 检查结论
|
||||
|
||||
1. 若 M-019 < 100%,判定存在追踪漂移风险。
|
||||
2. 当前说明:tracked_rows=15/15
|
||||
|
||||
## 处理动作
|
||||
|
||||
1. 若 FAIL:24h 内补齐缺失追踪项并复跑本脚本。
|
||||
2. 若 PASS:纳入 7 日趋势统计。
|
||||
25
reports/gate_metrics_consistency_check_2026-03-27.md
Normal file
25
reports/gate_metrics_consistency_check_2026-03-27.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 门禁指标与报告一致性检查(2026-03-27)
|
||||
|
||||
- 检查范围:
|
||||
- `docs/acceptance_gate_single_source_v1_2026-03-18.md`
|
||||
- `reports/supply_gate_review_2026-03-31.md`
|
||||
- `review/final_decision_2026-03-31.md`
|
||||
- `review/prd_tech_planning_recheck_v3_2026-03-27.md`
|
||||
|
||||
## 1. 总体结论
|
||||
|
||||
结论:**主要一致,存在 1 项历史引用缺口待清理。**
|
||||
|
||||
## 2. 检查结果
|
||||
|
||||
| 项目 | 结果 | 说明 |
|
||||
|---|---|---|
|
||||
| M-013~M-016 在 SUP 报告与最终决议均有体现 | PASS | 口径一致,均标记为 mock 有条件通过 |
|
||||
| `NO-GO` 决策与 staging 阻塞状态一致 | PASS | 与 D/E 阶段阻塞报告一致 |
|
||||
| M-017~M-019 在复检与最终决议均有体现 | PASS | 口径一致,连续7天证据未齐 |
|
||||
| M-021(token 运行态门禁)是否在决议表中显式核对 | PASS | 已补入最终决议与 SUP 风险项 |
|
||||
| 链接完整性检查是否全绿 | FAIL | 存在历史任务文档引用未落地条目,详见 `reports/link_integrity_check_2026-03-27.md` |
|
||||
|
||||
## 3. 修复建议
|
||||
|
||||
1. 将链接检查中的“未落地引用”拆分为 backlog 并标注 owner。
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_123320
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_122908.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_122907.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_123622
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_122908.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_122907.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_145306
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_145306.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_145305.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_145749
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_145306.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_145305.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_151609
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_145749.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_151555.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_151621
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_151621.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_151555.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_151838
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_151838.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_151821.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_154104
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_154104.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_154103.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_155729
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_155729.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_155727.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_160041
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_160041.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_160039.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_160246
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_160246.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_160244.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_161011
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_161011.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_161009.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_173342
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_173342.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_173339.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_173713
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_173713.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_173339.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_173728
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_173728.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_173726.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_181925
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_181925.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_173726.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
@@ -0,0 +1,26 @@
|
||||
# Final Decision Consistency Check
|
||||
|
||||
- 时间戳:2026-03-30_181927
|
||||
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
|
||||
|
||||
## 1. 输入源
|
||||
|
||||
| 来源 | 路径 | 解析结论 |
|
||||
|---|---|---|
|
||||
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
|
||||
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_181927.md | CONDITIONAL_GO |
|
||||
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_181925.md | CONDITIONAL_GO |
|
||||
|
||||
## 2. 一致性结果
|
||||
|
||||
- 状态:**WARN**
|
||||
- 说明:final signed decision lags latest machine recheck; requires manual review update
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
1. 若状态为 WARN:人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
|
||||
2. 若状态为 FAIL:先修复报告来源或解析格式,再重新执行本检查。
|
||||
3. staging 真值就绪后,按顺序重跑:
|
||||
1. `scripts/ci/superpowers_stage_validate.sh`
|
||||
2. `scripts/ci/tok007_release_recheck.sh`
|
||||
3. `scripts/ci/final_decision_consistency_check.sh`
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user