Merge pull request 'fix(enterprise): P3 residual batch' (#25) from feat/p3-residual-fixes into main
Deploy to Production / deploy (push) Waiting to run Details
Test / backend-test (push) Waiting to run Details
Test / frontend-unit (push) Waiting to run Details
Test / api-e2e (push) Waiting to run Details
Test / frontend-e2e (push) Waiting to run Details

This commit is contained in:
root 2026-07-06 12:24:44 +08:00
commit f0fc34bfee
15 changed files with 949 additions and 135 deletions

View File

@ -26,7 +26,12 @@ spec:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: agentkit
# DEPLOY-9: digest pinning 优先于 tag政企生产防篡改
{{- if .Values.image.digest }}
image: "{{ .Values.image.repository }}@{{ .Values.image.digest }}"
{{- else }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}

View File

@ -9,6 +9,10 @@ image:
repository: fischer-agentkit
tag: 0.1.0
pullPolicy: IfNotPresent
# DEPLOY-9: 可选 digest pinning政企生产场景防篡改
# 设置后 image 引用变为 <repository>@sha256:<digest>,覆盖 tag。
# 获取 digest: crane digest <repo>:<tag> 或 skopeo inspect --format '{{.Digest}}' docker://<repo>:<tag>
digest: ""
# 私有镜像仓库时配置 pullSecretsecret 引用名,由外部创建)
pullSecret: ""

View File

@ -254,9 +254,9 @@ kubectl rollout restart deployment -l app.kubernetes.io/instance=agentkit -n age
---
## Slot 10Redis-PG 密码90 天)
## Slot 10Redis 密码90 天)
**影响**Redis / PostgreSQL 连接断开。需与基础设施层协同。
**影响**Redis 连接断开(缓存/会话/总线)。需与基础设施层协同。
**步骤**
@ -264,18 +264,15 @@ kubectl rollout restart deployment -l app.kubernetes.io/instance=agentkit -n age
# 1) Redis 轮换密码
redis-cli -a '<old-redis-pwd>' CONFIG SET requirepass '<new-redis-pwd>'
# 2) PostgreSQL 轮换密码DBA
psql -U postgres -c "ALTER USER agentkit WITH PASSWORD 'new-pg-pwd';"
# 2) 更新密钥后端key=redis-password
# 3) 更新密钥后端(两个 keyredis-password + postgres-password
# 4) 更新 Redis 服务配置(若 Redis 由 docker-compose / StatefulSet 管理,
# 3) 更新 Redis 服务配置(若 Redis 由 docker-compose / StatefulSet 管理,
# 需同步更新 requirepass 并 restart Redis
# 5) restart agentkit pod
# 4) restart agentkit pod
kubectl rollout restart deployment -l app.kubernetes.io/instance=agentkit -n agentkit
# 6) 验证
# 5) 验证
kubectl exec -n agentkit deploy/agentkit -- \
python -c "import redis,os; redis.from_url(os.environ['REDIS_URL'],password=os.environ['REDIS_PASSWORD']).ping()"
```
@ -283,7 +280,42 @@ kubectl exec -n agentkit deploy/agentkit -- \
**注意**Redis `CONFIG SET requirepass` 立即生效,旧连接保持到断开。
建议低峰期操作,且 Redis 持久化配置redis.conf需同步更新避免重启后失效。
**回滚**:恢复 Redis / PG 旧密码 + 恢复 Secret。
**回滚**:恢复 Redis 旧密码 + 恢复 Secret。
---
## Slot 11PostgreSQL 密码90 天)
**影响**PostgreSQL 连接断开(用户/会话/审计)。需 DBA 协同。
**DEPLOY-4 注意**:应用通过 `DATABASE_URL`Slot 3内嵌密码连接 PG**不读 `POSTGRES_PASSWORD` env**。
`POSTGRES_PASSWORD` 仅用于 PG StatefulSet 初始化。轮换 PG 密码时须**同步更新 Slot 3database-url**
否则应用连接会失败。
**步骤**
```bash
# 1) PostgreSQL 轮换密码DBA
psql -U postgres -c "ALTER USER agentkit WITH PASSWORD 'new-pg-pwd';"
# 2) 更新密钥后端 — 两个 key 须同步:
# a) postgres-passwordPG StatefulSet 初始化用)
# b) database-url应用连接用DSN 内嵌新密码)
# 格式postgresql+asyncpg://agentkit:<new-pg-pwd>@postgres:5432/agentkit
# 3) 若 PG 由 StatefulSet 管理,需删除 PG pod 触发重新初始化(仅首次部署后需要;
# 已有数据的 PG 不会重新读 POSTGRES_PASSWORD密码由 ALTER USER 持久化)
# 4) restart agentkit pod
kubectl rollout restart deployment -l app.kubernetes.io/instance=agentkit -n agentkit
# 5) 验证
kubectl exec -n agentkit deploy/agentkit -- \
python -c "import os; from sqlalchemy.ext.asyncio import create_async_engine; \
import asyncio; asyncio.run(create_async_engine(os.environ['DATABASE_URL']).dispose()); print('ok')"
```
**回滚**:恢复 PG 旧密码 + 恢复两个 Secretpostgres-password + database-url
---

View File

@ -0,0 +1,622 @@
---
title: "AgentKit 企业版路线图 - Plan"
type: feat
date: 2026-07-05
topic: enterprise-agent-platform-roadmap
artifact_contract: ce-unified-plan/v1
artifact_readiness: implementation-ready
product_contract_source: ce-brainstorm
execution: code
deepened: 2026-07-05
---
# AgentKit 企业版路线图 - Plan
## Goal Capsule
- **Objective:** 把 AgentKit 企业级基线推到 L3 能卖给国内中大型企业/政企,同时加深垂直差异化(自进化、Board 决策、4 层记忆、业务系统集成)形成护城河。
- **Product Authority:** 产品负责人。
- **Open Blockers:** P0 6 项(含 R-等保文档)完成后,首个政企客户 POC 是否能落地。
- **Execution Profile:** code —— 路线图含 SSO 集成、K8s 部署、沙箱补全、OTel 启用等可执行工作。
- **Stop Conditions:** P0 6 项全交付(R1-R5 全交付 + R-等保文档启动并并行推进,目标认证 readiness 在政企 POC 采购关闭前完成,认证审计本身在 P1 R10);P1 至少完成 OTel admin dashboard + Subagents worktree + 完整沙箱分级;P2 至少启动 1 项垂直差异化加深。
- **Tail Ownership:** 产品负责人决定 P1 → P0 升级触发条件与滚动评审节奏。
---
## Product Contract
### Summary
AgentKit 闭源企业版"两条并行"路线图——P0 推进 6 项阵列把企业级基线推到 L3 能卖给政企(SAML/OIDC SSO + SCIM 最小集、私有化 K8s 部署交付 + 密钥管理、prompt cache 全链路打通、剩余 residual P2 修复、OTel 默认启用、等保三级文档准备);P1 补 OTel admin dashboard、Subagents worktree、完整沙箱分级、等保三级认证、国密改造、信创 DB 适配、Wave1 G3/G8;P2 加深垂直差异化。商业模式为闭源企业版按席位计费,以滚动规划方式推进(4-6 周评审 + 政企专属反馈通道,不锁时间)。
### Problem Frame
Qoder / Codex / Trae Work / WorkBuddy 已占据编程工具赛道并建立生态壁垒。AgentKit 在执行引擎、自进化、记忆、安全、Pipeline 编排、业务系统集成上达到或超过竞品 L3-L4 水准,但在企业身份、合规认证、生态市场、多端覆盖、OS 沙箱上明显落后。
横向评估(14 维度,满分 56):AgentKit 总分 42(L2 标准级),落后于 Qoder(51)、Codex(50)、WorkBuddy(48)、Trae Work(48)。差距集中在 Eval-G1 企业级身份合规、Eval-G3 OS 沙箱、Eval-G4 多端覆盖、Eval-G5 Skill 生态、Eval-G9 可观测性;独特优势在 A1 自进化体系、A2 Board 会议模式 + B+C 混合团队、A3 6 层终端安全、A4 4 层记忆 + Section CRUD、A5 业务系统深度集成、A6 7 种 ExecutionMode、A7 Plan-Exec 4 阶段状态机、A8 Pipeline + Saga + Checkpoint。
> **G-label 双轨制说明:** Eval-G* 为本横向评估的差距标签(Eval-G1/G3/G4/G5/G9)。Brainstorm `docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md` 的 Wave1/Wave3 G3/G5/G6/G8 是另一套独立的差距标签(tool schema / 函数级分片 / SOLO 状态机 / delta_flush),二者不互通,文档内以 Wave 前缀区分。
战略选择:避免在编程工具赛道正面对标,聚焦企业运营场景(Bitable / Calendar / Documents / IM channels + Board 决策模式),以差异化护城河对抗编程工具的通用化扩张。两条并行——P0/P1 推进企业级基线到 L3 让产品能卖给政企;P2 加深垂直差异化形成壁垒。
### Key Decisions
- **两条并行而非差异化押注或通用平台追赶** —— 复用已有深度(自进化、Board、4 层记忆),同时补企业级基线(SSO、私有化、沙箱、OTel、合规)。押注差异化但放弃企业基线会让产品无法进入政企;正面对标编程工具赛道则面对先发劣势。注:此处的"并行"指战略双轨,执行顺序仍为 P0 → P1 → P2 串行(见 Mermaid)。
- **目标客户为国内中大型企业/政企** —— 优先级:SAML 2.0 / OIDC SSO + 等保三级 + 国密 + 私有化信创部署。SOC2/ISO 不优先,国际版不做,聚焦国内政企。
- **多租户隔离移出 P0** —— 政企私有化主战场是单租户,SaaS 多租户场景延后到 SaaS 多客户运营时再讨论。本次完全移出,降低 P0 复杂度。**假设**:首个政企 POC 客户为单租户单部门;若销售阶段识别到多部门/多子公司隔离需求(常见于央企/国企),触发 multi-tenancy 提前评估流程(参考 R18 滚动规划触发条件)。
- **等保三级推到 P1 而非 P0(认证审计),但文档准备 P0 启动** —— 政企进入门槛需要,但认证周期长,P0 先交付 SSO + 私有化 K8s 的 MVP,合规认证审计后置到 P1;但等保三级文档准备(R-等保)与 MVP 并行启动,目标认证 readiness 在政企 POC 采购关闭前完成。
- **闭源而非 Open Core** —— 简化范围,SSO / 等保 / 信创适配作为企业版独享,Skill 市场延后到生态成熟期。
- **滚动规划 + 政企专属反馈通道** —— 4-6 周评审,按客户反馈和市场动态调整下一批需求,不锁时间。文档锁定原则和优先级粗框架。政企客户反馈 bypass 4-6 周节奏(首飞客户在 P0 实施期间设立 standing pre-POC review,采购阻塞信号触发紧急升级)。
- **OS 沙箱分级已完成最低层级(workspace-write + no network)** —— P1 升级到完整分级(read-only / workspace-write / danger),而非从零构建。
- **R5 OTel default-enable 升级到 P0** —— 因 R3 命中率监控接入 OTel(避免 P0→P1 priority inversion),OTel 框架接入点已存在(`tracer.py:117` OTLP/gRPC + `metrics.py:3-7`)。但 `opentelemetry-*` 依赖未在 `pyproject.toml` 声明(`tracer.py:129` 与 `metrics.py:7` 在 ImportError 时静默回退 NoOp),R5 实际工作量包含补依赖 + 取消 `agentkit.yaml:85-90` telemetry 段注释 + 配置 `telemetry.otlp_endpoint`(`setup.py:46` 读 YAML 不读 `OTEL_EXPORTER_OTLP_ENDPOINT` 环境变量),非"一行配置"。R5 在 P1 仅保留 admin dashboard 部分。
### Requirements
#### P0 阵列(能卖给政企的 MVP)
- R1. 集成 SAML 2.0 与 OIDC SSO,支持飞书 IDP / Azure AD / 阿里云 IDaaS。SCIM 最小子集(用户创建/禁用)纳入 P0 验收门槛。
- R1a. **多 IDP 信任边界**:多 IDP 并存时账号关联以 IDP 声明的 subject identifier 为主键,禁止仅凭邮箱合并(账号接管防护);IDP 元数据(签名证书)须支持热轮换,不重启服务;任一 IDP 不可达时该 IDP 路径默认拒绝并返回明确错误,不影响其他 IDP。
- R1b. **过渡期 deprovisioning**:SCIM 推后期内,产品须提供管理员手动 deprovisioning 接口(强制 operator/admin RBAC,与 R7a/R5'a 一致;每次调用记录 actor、target_user、reason、source=scim_fallback|manual,接入 R7a 同款审计日志通道)+ IDP 侧账号状态周期对账(每日 cron)。**高权限账号 SLA**:operator/admin/持有 danger tier 授权者的回收 SLA 为 1h(通过 break-glass 手动接口即时执行,cron 对账频率提升至每小时);SCIM 推送失败须触发实时告警(非等 cron)。
- R1c. **单租户假设**:假设首个政企 POC 客户为单租户单部门;若销售阶段识别到多部门/多子公司隔离需求,触发 multi-tenancy 提前评估流程。
- R2. 提供 K8s Helm Chart 部署清单 + 离线安装包 + 部署运维文档,覆盖私有化交付场景。docker-compose 已有但不足以承载政企生产部署。
- R2a. **密钥管理**:部署清单须明确区分配置与密钥——所有密钥(JWT 签名密钥、API Key、DB 凭据、IDP 签名证书、第三方 API Key、TLS 服务器证书私钥、mTLS 客户端证书私钥、KMS/HSM 访问凭证 PKCS#11 PIN、OTel exporter 鉴权 token、Redis/PG 连接密码)通过 Sealed Secrets 或 External Secrets Operator 注入,禁止以 plain Kubernetes Secret + Git 提交形式交付;KMS/HSM 访问凭证须通过 workload identity 或 projected service account token 注入,而非静态 Secret;首飞须提供密钥轮换 runbook。
- R3. 完成 prompt cache 全链路打通:验证记忆写入确实路由到 volatile 层、`prompt_cache_enable` 默认开启、命中率监控接入 OTel 框架(与 R5 OTel default-enable 配套)。结构已实现(stable/volatile 双块 + Anthropic `cache_control` 透传 + 测试覆盖),但全链路收益未兑现。**P0 验收门槛**:政企部署须提供本地推理 fallback 选项(通义/DeepSeek/Doubao 私有化),作为 Anthropic 路径的等保合规替代;fallback 路径不依赖 prompt cache 价值(R3 P0 优先级服务中大型企业 Anthropic-eligible 客户段,见 OQ13)。
- R4. 修复剩余 residual P2 findings:feat-ui-ue-enhancement 3 项(expert_step payload 缺字段、team_synthesis 孤儿 milestone、synthesis_id 去重)+ feat-bitable-enhancement 5 项(DR-1 SQL 注入风险、DR-2 ViewConfigPanel 深审、DR-4 TOCTOU race、DR-5 _update_field 静默失败、DR-3 设计 token 测试)。execute_stream CancellationToken 注册状态已通过 KTD-4 钩子修复(见 OQ2 关闭说明)。
- R5. **OTel 默认启用**(从原 R5 拆出升级到 P0):将 `opentelemetry-api` / `opentelemetry-sdk` / `opentelemetry-exporter-otlp-proto-grpc` / `opentelemetry-instrumentation-fastapi` 添加到 `pyproject.toml` dependencies(当前未声明,`tracer.py:129` 与 `metrics.py:7` 在 ImportError 时静默回退 NoOp);取消 `agentkit.yaml:85-90` telemetry 段注释并设 `enabled=true`;配置 `telemetry.otlp_endpoint`(`setup.py:46` 读 YAML 不读 `OTEL_EXPORTER_OTLP_ENDPOINT` 环境变量);框架接入点已存在(`src/agentkit/telemetry/tracer.py:117` OTLP/gRPC + `src/agentkit/telemetry/metrics.py:3-7`)。
- R-等保. **等保三级文档准备(P0 启动)**:启动等保三级文档与控制点清单编写(安全物理 / 网络 / 主机 / 应用 / 数据 / 管理 6 维度),与 MVP 并行推进,目标认证 readiness 在政企 POC 采购关闭前完成。认证审计本身仍在 P1。
#### P1 阵列(企业级基线补强)
- R5'. **OTel admin dashboard**(R5 拆分后剩余):前端 admin dashboard(可观测性、调用链路 Trace、性能监控、Token 用量)。
- R5'a. **访问控制**:admin dashboard 强制 admin RBAC 角色;OTel Span 导出前须脱敏 prompt 内容、API Key、用户 PII、tool_call arguments、tool_result 文件路径/凭证模式匹配、环境变量值、异常堆栈中的主机名与路径(参考现有 6 层终端安全脱敏正则库扩展);Token 用量数据保留 180 天(等保三级审计日志最低要求,GB/T 22239-2019),过期自动归档至冷存储。
- R6. Subagents worktree 并行执行:`PlanPhase` 增加 worktree 隔离选项,参考 Codex Subagents(`max_threads=6` + Auto-review)模式。
- R7. 完整沙箱分级(read-only / workspace-write / danger tier),最低层级已实现(`src/agentkit/core/sandbox.py`),需补 read-only 与 danger 模式 + macOS Seatbelt / Linux Landlock 系统调用级隔离。
- R7a. **danger tier 授权模型**:danger tier 执行须满足:(1) 仅 operator/admin RBAC 角色可发起;(2) 每次调用前要求显式 confirmation_request;(3) 调用记录写入审计日志含 actor、target、command、duration、exit_code;(4) **命令面约束**:命令须通过预定义白名单(或 deny-list 阻断已知危险模式如 `rm -rf` / `curl` / `kubectl delete`),参数须经过 shell-escape 校验,每 actor 每分钟调用上限可配(默认 5),超出限流阈值触发 break-glass 告警。workspace-write 为默认,read-only 为受限角色默认。
- R8. Wave1 Brainstorm-G3 工具 schema 校验:工具描述 schema 超过上下文 10% 时按需渐进披露,参考 Codex MCP `tool_search` 模式。
- R9. Wave1 Brainstorm-G8 delta_flush 节流:流式 token flush 节流以减少 WebSocket 消息数。
- R10. 等保三级合规认证:启动认证审计流程,基于 R-等保文档准备。安全物理 / 网络 / 主机 / 应用 / 数据 / 管理 6 维度控制点落实。
- R11. 国密 SM2 / SM3 / SM4 改造:JWT、API Key、敏感字段加密使用国密算法,适配政企合规。
- R11a. **密钥管理与迁移**:国密密钥须存储于 KMS/HSM(支持信创 PKCS#11),禁止文件落盘;切换期间支持双算法并行验证(JWT header 标识 alg,API Key 双写),过渡期不少于一个轮换周期;**降级攻击防护**:(a) 旧算法签发的 token TTL 在过渡期缩短至原 TTL 的 1/4;(b) 旧算法签发/接受事件须写入安全审计日志并触发 SIEM 告警;(c) 过渡期结束须有硬性 EOL 日期,过期后旧算法 token 一律拒绝;(d) 过渡期不接受新签发的旧算法 token(仅接受存量刷新);敏感字段加密须支持存量数据就地迁移脚本,迁移脚本须幂等(基于记录 ID + 版本标记可重跑),每批提交事务,失败时回滚该批,迁移完成须有校验报告(迁移记录数、校验和、失败记录列表)。
- R12. 信创 DB 适配:PostgreSQL 之外支持达梦 / 人大金仓,RAG 向量层(pgvector)需适配或抽象。
#### P2 阵列(垂直差异化加深)
- R13. 移动端 + 语音输入:Tauri 移动端,接 IM channels 部分弥补。
- R14. Skill 市场激活:激活现有 `src/agentkit/marketplace/auction.py` Vickrey 拍卖机制接入主流程。
- R15. 原生 Computer Use:截图识别 + 多步编排,降低对 Anthropic API 依赖。
- R16. Wave3 Brainstorm-G5 函数级分片:tree-sitter 引入,KTD6 决策延后到 Wave 3 plan。
- R17. Wave3 Brainstorm-G6 SOLO 四阶段状态机扩展 PLAN_EXEC:阶段约束(Planning 不可 write_file 等),KTD7 决策延后到 Wave 3 plan。
#### 滚动规划决策规则
- R18. 每 4-6 周评审一次,按客户反馈和市场动态调整下一批需求。P1 → P0 升级触发条件:(1) 客户 POC 阻塞项;(2) 同类需求重复出现 3+ 次;(3) 安全 / 合规审计发现紧急项。
- R18a. **政企专属反馈通道**:首飞政企客户在 P0 实施期间设立 standing pre-POC review(每 2 周与客户对接),采购阻塞信号(POC 失败、合规审计发现、需求变更)触发紧急升级,而非等 4-6 周窗口。政企采购周期(6-12 个月)与内部滚动节奏并行存在。
### Key Flows
- F1. 滚动规划评审流
- **Trigger:** 4-6 周评审窗口到达,或客户反馈触发紧急升级,或政企专属通道(pre-POC review)识别采购阻塞。
- **Actors:** 产品负责人,工程团队,首飞政企客户(pre-POC review)。
- **Steps:** 反馈收集(滚动窗口 + 政企专属)→ 优先级评估(对照 R18 触发条件)→ 评审窗口决策 → 调整 P0/P1 阵列 → 通知相关方。
- **Covered by:** R18, R18a.
- F2. 私有化交付流
- **Trigger:** 政企客户采购。
- **Actors:** 部署团队,客户运维。
- **Steps:** 离线安装包交付(含 Sealed Secrets 配置)→ K8s 集群部署 → SSO 配置对接(飞书 IDP / Azure AD / 阿里云 IDaaS)→ 等保三级文档准备核对 → 验收。
- **Covered by:** R1, R2, R-等保.
### Visualizations
```mermaid
flowchart TB
subgraph P0[P0 - 能卖给政企的 MVP]
R1[SAML/OIDC SSO + SCIM 最小集]
R2[K8s Helm 部署 + 密钥管理]
R3[Prompt cache 全链路打通]
R4[Residual P2 修复]
R5[OTel 默认启用]
ReqDengBao[等保三级文档准备]
end
subgraph P1[P1 - 企业级基线补强]
R5prime[OTel admin dashboard]
R6[Subagents worktree]
R7[完整沙箱分级]
R8[Brainstorm-G3 工具 schema]
R9[Brainstorm-G8 delta_flush]
R10[等保三级认证审计]
R11[国密改造]
R12[信创 DB 适配]
end
subgraph P2[P2 - 垂直差异化加深]
R13[移动端+语音]
R14[Skill 市场]
R15[原生 Computer Use]
R16[Brainstorm-G5 函数级分片]
R17[Brainstorm-G6 SOLO 状态机]
end
P0 --> P1 --> P2
R1 -.客户反馈.-> R18[滚动规划评审]
R18 -.触发升级.-> P0
R18 -.政企专属通道.-> R18a
```
### Success Criteria
- P0 6 项全交付(R1 + R2 + R3 + R4 + R5 + R-等保),首个政企客户 POC 落地。
- P1 至少完成 OTel admin dashboard + Subagents worktree + 完整沙箱分级。
- P2 至少启动 1 项垂直差异化加深。
- 横向评估总分从 42/56 提升到 50+/56(达到 WorkBuddy / Trae Work 同级)。注:评分体系需对接三方评测(Forrester / Gartner Magic Quadrant 或客户技术评估)以避免自评偏差。
- 企业级维度(Eval-D10 安全权限、Eval-D12 部署企业级)从 L2/L3 提升到 L4。
> **维度标签说明:** Eval-D* 为本横向评估的 14 维度编号(D1-D14),与 Eval-G*(差距) / A*(优势)同源。文档其他位置如出现 D10/D12 默认指 Eval 维度编号。
### Scope Boundaries
#### Deferred for later
- 多租户隔离 —— 完全移出本次范围,延后到 SaaS 多客户运营时再讨论。首个政企 POC 客户为单租户假设(见 R1c)。
- Skill 市场激活 —— 延后到 P2,优先级低于垂直差异化加深。
- 等保三级合规认证审计 —— 文档准备 P0 启动(R-等保),认证审计推到 P1(R10)。
- 移动端 + 语音输入 —— 推到 P2。
- 原生 Computer Use —— 推到 P2。
- Wave3 Brainstorm-G5 / Brainstorm-G6 —— 推到 P2,依赖 KTD6 / KTD7 决策。
#### Outside this product's identity
- 与 Qoder / Codex / Trae Work 正面竞争编程工具赛道 —— 不做。
- 国际版 / SOC2 / ISO 认证 —— 不做,聚焦国内政企。
- 开源核心 —— 不做,保持闭源企业版计费。
- 与 WorkBuddy SkillHub / Qoder MCP Square 上架 —— 不做,生态合作延后。
### Dependencies / Assumptions
- 假设:首个政企 POC 客户为单租户单部门;若销售阶段识别到多部门/多子公司隔离需求,触发 multi-tenancy 提前评估。
- 假设:complex-task-quality-loop(plan `docs/plans/2026-07-03-001-feat-complex-task-quality-loop-plan.md`)的 11 项需求(R1-R8, R10-R12;R9 coding_harness deferred per RV11/RV12/RV13)+ 10 KTD 已稳定,可作为 P0 启动基线。
- 假设:Bitable P0 UX(merge commit `8633f60`)已稳定,可作为基线。
- 依赖:Anthropic API 持续支持 `cache_control` 断点。
- **冲突声明:** R3 依赖 Anthropic API(境外服务),与等保三级(R10)数据本地化要求存在冲突。R3 须明确:(1) 用户 prompt 在发送前进行 PII 脱敏——采用基于本地 NER 模型(如 LAC / HanLP)的 PII 识别 + 替换/掩码,模型部署在客户内网;脱敏前后均记录 hash 用于审计追溯但原文不落盘;脱敏失败时默认 fail-closed(拒绝发送至 Anthropic),仅在本地推理 fallback 可用时降级处理;脱敏覆盖率与漏报率纳入 OTel 指标监控(注:原引用的"现有 6 层终端安全脱敏模式"针对结构化终端命令设计,无法覆盖自由语言 prompt,故改用本地 NER 模型);(2) 政企部署提供本地推理 fallback 选项(通义/DeepSeek/Doubao 私有化),作为 Anthropic 路径的等保合规替代;(3) 与客户签署数据处理协议(DPA),明确 Anthropic 侧缓存数据所有权与删除权——注:Anthropic prompt cache 仅由 TTL(默认 5 分钟,beta 1 小时)自动过期,未提供显式删除 API,DPA 条款的"立即删除"承诺不可强制执行,只能依赖 TTL 自然过期(见 OQ15)。
- 依赖:K8s 1.28+ / Helm 3.x 部署环境(若首飞客户为传统 IDC,需评估 docker-compose 加固 fallback,见 OQ5 + OQ8)。
- 依赖:飞书 / Azure AD / 阿里云 IDaaS IDP 协议稳定性(签名证书热轮换 + 默认拒绝故障模式,见 R1a)。
- 假设:政企客户接受 PostgreSQL 作为主数据库,信创 DB 适配推到 P1 而非 P0(若客户硬性要求则需提前)。
### Outstanding Questions
#### Resolve Before Planning
- OQ1. ~~R3 prompt cache 命中率监控接入路径~~ —— **已解决**:接入 OTel 框架,R5 OTel default-enable 升级到 P0,与 R3 配套。Planning 可按 R5 → R3 顺序排程。
- OQ2. ~~R4 execute_stream CancellationToken 注册状态~~ —— **已解决**:已通过 KTD-4 钩子修复(`src/agentkit/core/config_driven.py:778` execute_stream def,`:792-793` CancellationToken 注册,带 "P2 fix: 注册 CancellationToken 到 _active_tokens" 注释)。Planning 阶段无需再核对,可直接关闭此 OQ。
- OQ3. R7 完整沙箱分级——是否要在 P1 完成全部三个层级(read-only / workspace-write / danger),还是仅补 read-only 即可达到 L4?影响工作量估算。
#### Deferred to Planning
- OQ4. R1 SSO 协议优先级:SAML 2.0 优先(国内 IDP 主流)还是 OIDC 优先(国际标准)?影响首飞 IDP 选择。
- OQ5. R2 K8s 部署清单兼容矩阵:原生 K8s / 信创 K8s / OpenShift 哪些必须支持?
- OQ6. R12 信创 DB 适配:达梦优先还是人大金仓优先?RAG 向量层(pgvector)是否抽象为独立接口?
- OQ7. R6 Subagents worktree:`max_threads` 默认值(Codex 是 6,AgentKit 应该多少)?worktree 失败如何回滚?
#### From 2026-07-05 ce-doc-review
- OQ8. **K8s 假设可能不覆盖传统 IDC 政企客户**(security-lens + adversarial 识别,anchor 75 promote to 100)。国内政企有相当部分仍在传统 IDC + VM 部署(华为云 Stack / 阿里云 Apsara Stack 模式)。R2 Helm Chart 无法直接运行,可能需要 docker-compose 加固 fallback 路径。Planning 须评估是否在 P0 加入 docker-compose 加固,或在 R18 触发条件中识别"传统 IDC 客户"作为升级触发。
- OQ9. **R6/R8/R9 是否应从 P1 重分类到 P2**(scope-guardian 识别,anchor 75)。R6/R8/R9 描述为性能/能力优化(Subagents worktree / 工具 schema 渐进披露 / delta_flush 节流),而非企业级基线。R8/R9 携带 Wave1 前缀,R6 引用 Codex 模式(competitor mimicry 而非 AgentKit pain)。Planning 须评估 P1 theme 是否扩展为"企业级基线 + 性能优化",或重分类至 P2。
- OQ10. **P2 items 不深化已声明护城河**(scope-guardian 识别,anchor 100)。Goal 明确声明护城河为"自进化 / Board 决策 / 4 层记忆 / 业务系统集成",但 P2 items(R13-R17)均不深化这四项。Stop Condition "P2 至少启动 1 项垂直差异化加深"可在不推进护城河的情况下满足。Planning 须决定:P2 增加深化护城河的 items,还是修订 Goal statement 承认 P2 是新能力而非深化?
- OQ11. **D-label 维度编号未定义**(coherence 识别,anchor 75)。Success Criteria 引用 D10 安全权限 / D12 部署企业级,但 Problem Frame 只定义 G*(差距)和 A*(优势),无 D-labeling 方案。本次评审已添加 Eval-D 前缀作为 disambiguation,但完整 14 维度编号(D1-D14)未在文档内列出。Planning 须决定:在文档内完整列出 14 维度,或引用外部评估报告。
- OQ12. **G-label 双轨制 disambiguation**(coherence 识别,anchor 75)。Eval-G*(横向评估差距:G1/G3/G4/G5/G9)与 Brainstorm-G*(2026-06-29 brainstorm 差距:G3=tool schema / G5=函数级分片 / G6=SOLO 状态机 / G8=delta_flush)是两套独立编号。本次评审已用 Eval-/Brainstorm- 前缀 disambiguate,但 Planning 须决定是否在 Sources/Research 区段显式列出两套 gap scheme 的定义。
#### From 2026-07-05 ce-doc-review (Round 2)
- OQ13. **R3 prompt cache 价值在本地推理 fallback 路径下不可实现**(product-lens + adversarial 识别,anchor 75 promote to 100)。3-pronged 缓解方案创建战略缺口:当政企客户启用本地推理 fallback(通义/DeepSeek/Doubao)时,R3 的 Anthropic `cache_control` 透传机制对该路径完全无效,R3 收益归零;且 PII 脱敏后送给 Anthropic 的 prompt 与原始 prompt 不同,cache 命中率监控的是"脱敏后 prompt 的命中率",无法直接反映真实业务收益。文档已在 R3 添加 "P0 优先级服务中大型企业 Anthropic-eligible 客户段" 注释缓解,但 Planning 须决定:R3 的 P0 优先级是基于中大型企业(Anthropic-eligible)客户价值还是政企客户价值?若是前者,R3 应重新定位而非列入"政企 MVP";若是后者,R3 在 fallback 路径下如何兑现价值?
- OQ14. **SCIM 缺 group→role 映射,RBAC 角色分配无审计**(security-lens 识别,anchor 75)。R1 SCIM 最小子集仅含用户创建/禁用,未含 group 同步;R7a/R5'a 依赖 operator/admin RBAC 角色,但角色分配机制未定义——若由管理员手动分配,则缺乏角色变更审计(谁在何时把谁提权到 admin)。等保三级要求特权账号变更可审计可追溯。Planning 须决定:P0 是否将 SCIM group→role 映射纳入(取决于首飞客户 IDP 能力,见 OQ4)?若手动分配,是否新增"RBAC 角色变更审计 logging"作为 P0 验收门槛?
- OQ15. **DPA cache 删除权假设 Anthropic 提供 API**(security-lens + adversarial 识别,anchor 50 promote to 100)。DPA 第三叉"明确 Anthropic 侧缓存数据所有权与删除权"是政企合规采购的关键条款,但 Anthropic prompt cache 文档未提供显式"删除指定缓存条目"的 API——缓存条目仅由 TTL(默认 5 分钟,beta 1 小时)自动过期。这意味着"客户终止时立即删除缓存数据"的 DPA 承诺在工程上不可强制执行。文档已在 Anthropic 冲突声明中标注此限制,但 Planning 须决定:(a) 是否在 DPA 中明确"删除权仅由 TTL 自然过期实现,最长 1 小时"作为已知限制?(b) 是否要求政企客户默认 `prompt_cache_enable=false`,仅对显式签署 DPA 且接受残留风险的客户开启?(c) 本地推理 fallback 路径是否默认 `prompt_cache_enable=true` 以弥补性能损失?
- OQ16. **R1c 单租户 reversal cost 高**(adversarial 识别,anchor 75)。R1c 将单租户假设升级为显式决策,但缓解措施仅承诺"触发 multi-tenancy 提前评估流程"——评估不是承诺。单租户是 load-bearing 决策:数据模型、RBAC 角色、审计日志 schema、SCIM 用户主键都按单租户设计。若销售阶段识别到央企/国企的多部门/多子公司隔离需求,"提前评估"后若结论为"需要 multi-tenancy",届时 retrofit 成本极高(数据迁移 + RBAC 重构 + 审计日志 schema 变更)。Planning 须决定:(a) 是否在 P0 启动前完成单租户 reversal cost 评估(数据模型/RBAC/审计日志 schema 变更工作量估算)?(b) 是否在 R18 触发条件中增加"架构性 reversal cost"维度?(c) 是否在首飞政企客户销售阶段早期(standing pre-POC review)就明确 multi-tenancy 需求?
### Sources / Research
- 横向评估报告(本对话上文):AgentKit vs Qoder / Trae Work / Codex / WorkBuddy 五产品 14 维度对比。
- 现有 plan:`docs/plans/2026-07-03-001-feat-complex-task-quality-loop-plan.md`(P1 多项已落地基线;R1-R8, R10-R12 共 11 项需求 + 10 KTD)。
- 现有 plan:`docs/plans/2026-07-03-001-feat-bitable-p0-ux-and-agent-parity-plan.md`(已 merge)。
- Residual findings:`docs/residual-review-findings/feat-ui-ue-enhancement.md`、`docs/residual-review-findings/feat-bitable-enhancement.md`。
- 现有 brainstorm:`docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md`(原 Qoder / Codex / Hermes / Trae Work 长程可靠性对比;Brainstorm-G* gap scheme 来源)。
- 现有 brainstorm:`docs/brainstorms/2026-06-09-agentkit-capability-matrix/requirements.md`(能力矩阵愿景)。
- 代码证据:`src/agentkit/core/sandbox.py`(沙箱最低层级已实现)、`src/agentkit/telemetry/tracer.py:117`(OTel 已接入)、`src/agentkit/telemetry/metrics.py:3-7`(OTel 框架)、`tests/unit/test_prompt_cache_layers.py:46-58`(prompt cache 结构已实现)、`src/agentkit/core/config_driven.py:778, 792-793`(execute_stream CancellationToken 已实现)、`src/agentkit/marketplace/auction.py`(Vickrey 拍卖已实现)。
> **Product Contract preservation:** Product Contract 不变 — `ce-plan` 丰富阶段保留所有 R/A/F/AE IDs、Key Decisions、Scope Boundaries、Outstanding Questions 原文。新增 Planning Contract(下方),不修改 Product Contract 既有内容。
---
## Planning Contract
### Summary
P0 阵列 6 项作为本次实施的 Implementation Units(U1-U6),按依赖排序:U1 OTel 默认启用(基础)→ U2 Prompt cache 全链路 + 本地推理 fallback(依赖 U1 metrics)→ U3 Residual P2 修复(独立)→ U4 SSO 集成(依赖 U1 审计日志)→ U5 K8s Helm Chart(打包 U1-U4)→ U6 等保三级文档准备(文档,依赖 U1/U4/U5 输入)。P1 / P2 阵列作为 Deferred to Follow-Up Work,不在本次 plan 范围。
### Key Technical Decisions
- **KTD-1. P0 是单一交付单元;每个 R-item 是一个 Implementation Unit,按依赖排序。** U1(U6 OTel)是最小且基础的工作(其他 unit 依赖其 metrics/audit 能力);U2(R3 prompt cache)依赖 U1 的命中率 metric;U3(R4 residual)独立;U4(R1 SSO)依赖 U1 审计日志通道;U5(R2 K8s)打包 U1-U4 为可部署产物;U6(R-等保文档)依赖 U1/U4/U5 输入。单租户假设(R1c)是 load-bearing 决策 — 数据模型/RBAC/审计 schema/SCIM 主键均按单租户设计,reversal cost 极高(OQ16),通过 R18 触发条件中增加"架构性 reversal cost"维度 + pre-POC review 早期识别缓解。
- **KTD-2. OIDC 优先于 SAML 作为首飞 IDP 协议。** OIDC 在 Python 生态有更成熟的库支持(`authlib`),redirect flow 比 SAML POST/Redirect 简单,且三个目标 IDP 均原生支持 OIDC(飞书 OIDC app / Azure AD native OIDC / 阿里云 IDaaS native OIDC)。SAML 2.0 作为 R1 内的第二阶段 IDP(同一 Unit,后续 sub-commit),服务硬性要求 SAML 的客户。现有 `StubOIDCProvider`(`src/agentkit/server/auth/providers/oidc_stub.py`)和 `AuthProvider` Protocol(`providers/base.py`)提供集成 seam — 替换 stub 为真实 `authlib` 实现。
- **KTD-3. OTel 依赖从 optional 升级为 required。** `opentelemetry-api` / `opentelemetry-sdk` / `opentelemetry-exporter-otlp-proto-grpc` / `opentelemetry-instrumentation-fastapi` 从"ImportError → NoOp 静默回退"移入 `pyproject.toml``[project] dependencies`(非 `[optional-dependencies]`)。理由:R5 使 OTel default-enabled,ImportError 回退成为 dead code。NoOp 回退保留为"配置错误"防御层(`enabled=false` 或 init 失败时),但 ImportError 现在是 startup failure(`init_telemetry` 不再 catch ImportError),而非静默降级。现有 `tracer.py:129` / `metrics.py:7` 的 try/except ImportError 改为直接 import(依赖已声明)。
- **KTD-4. Helm Chart + Sealed Secrets/External Secrets Operator,禁用 plain Kubernetes Secret + Git 提交。** 所有密钥(JWT 签名密钥、API Key、DB 凭据、IDP 签名证书、第三方 API Key、TLS 服务器证书私钥、mTLS 客户端证书私钥、KMS/HSM PKCS#11 PIN、OTel exporter 鉴权 token、Redis/PG 连接密码)通过 Sealed Secrets(bitnami)或 External Secrets Operator 注入。KMS/HSM 访问凭证通过 projected service account token + workload identity 注入,而非静态 Secret。Helm Chart 的 `values.yaml` 模板显式列出每个 secret slot;首飞须提供密钥轮换 runbook(`docs/deployment/key-rotation-runbook.md`)。
- **KTD-5. 本地推理 fallback 是可插拔 LLM provider 配置,非独立代码路径。** R3 的 fallback(通义/DeepSeek/Doubao 私有化部署)复用现有 LiteLLM-based LLM gateway(`src/agentkit/llm/`)。`agentkit.yaml` 的 `llm.providers` 段已支持 OpenAI-compatible API(DashScope / DeepSeek 模板见现有配置)。Fallback 通过配置注入,不需要新代码。`prompt_cache_enable` 是 per-provider 配置:fallback providers 设置 `prompt_cache_enable=false`(不支持 Anthropic `cache_control`)。R3 P0 优先级服务中大型企业 Anthropic-eligible 客户段(OQ13 缓解);fallback 路径不依赖 prompt cache 价值,Anthropic 路径客户签署 DPA 接受 TTL 残留风险(OQ15)。
- **KTD-6. R-等保是 documentation-only 交付物,非 code unit。** 等保三级文档(6 维度:物理 / 网络 / 主机 / 应用 / 数据 / 管理)按 GB/T 22239-2019 编写,是 P0 Stop Condition 的一部分。`execution: code` 应用于其他 5 个 units;U6 标记为 documentation deliverable,无 behavioral change。文档位于 `docs/compliance/dengbao-level3/`。认证审计本身仍在 P1 R10。
- **KTD-7. R4 residual findings 作为独立 bug fix 批次,合成一个 U-ID。** 8 项 residual findings(3 项 feat-ui-ue-enhancement + 5 项 feat-bitable-enhancement)是独立 bug fix,无架构依赖。合成一个 U-ID 因为每项较小且共享 review/verification 路径。execute_stream CancellationToken 修复(OQ2 已关闭)已通过 KTD-4 钩子应用(`src/agentkit/core/config_driven.py:792-793`),排除在本 Unit 外。
- **KTD-8. SCIM group→role mapping 延后;P0 SCIM 仅含用户创建/禁用 + 手动 RBAC + 审计。** SCIM 2.0 group sync(group→role 映射表 + 角色变更审计)增加复杂度,延后到 P0 stretch goal(gated on 首飞客户 IDP 能力,OQ4)。P0 SCIM 最小子集:`/scim/v2/Users` POST(创建)/ PATCH(禁用)/ DELETE。RBAC 角色分配保持管理员手动操作,但新增"RBAC 角色变更审计 logging"作为 P0 验收门槛(OQ14 缓解)——每次角色变更记录 actor / target_user / role_before / role_after / reason / source,接入 R7a 同款审计日志通道(依赖 U1 OTel)。
- **KTD-9. 等保三级审计日志保留期 180 天(GB/T 22239-2019 最低要求)。** OTel Span / Token 用量 / RBAC 变更 / SCIM 操作 / danger tier 调用审计日志保留 180 天,过期自动归档至冷存储。原 R5'a 的 30 天保留期不满足等保三级(Round 2 adversarial 发现),已修正为 180 天。
### High-Level Technical Design
#### P0 依赖与排序
```mermaid
flowchart LR
U1[U1 OTel 默认启用] --> U2[U2 Prompt cache 全链路]
U1 --> U4[U4 SSO 集成]
U3[U3 Residual P2 修复] --> U5[U5 K8s Helm Chart]
U2 --> U5
U4 --> U5
U1 --> U6[U6 等保文档]
U4 --> U6
U5 --> U6
```
#### SSO 多 IDP 信任流
```mermaid
sequenceDiagram
participant U as 用户浏览器
participant A as AgentKit Server
participant IDP1 as 飞书 IDP (OIDC)
participant IDP2 as Azure AD (OIDC)
participant IDP3 as 阿里云 IDaaS (SAML)
U->>A: 点击"飞书登录"
A->>U: 302 redirect to IDP1 authorize
U->>IDP1: 认证
IDP1->>U: 302 redirect to /auth/oauth/feishu/callback?code=...
U->>A: GET /callback?code=...
A->>IDP1: token exchange (server-side)
IDP1->>A: id_token + userinfo (sub=xxx)
A->>A: 按 sub 作主键查/建本地用户(禁止邮箱合并)
A->>A: 签发 JWT,写 auth_sessions.auth_provider='oidc-feishu'
A->>U: Set-Cookie + redirect to /agent/chat
```
#### OTel 数据流
```mermaid
flowchart TB
subgraph AgentKit Pod
A[FastAPI App] -->|instrumentation-fastapi| T[OTel Tracer]
A -->|metrics| M[OTel Meter]
T -->|OTLP/gRPC| E[OTLPExporter]
M -->|OTLP/gRPC| E
end
E -->|4317| C[OTel Collector]
C -->|prometheus| P[Prometheus]
C -->|tempo/jaeger| J[Trace Backend]
C -->|loki| L[Log Backend]
subgraph Admin Dashboard (P1 R5')
P --> D[dashboard.vue]
J --> D
end
```
#### 14 维度横向评估编号
| Eval-D | 维度 | AgentKit 现状 | 目标 |
|--------|------|--------------|------|
| D1 | 自进化体系 | L4(A1 优势) | L4(保持) |
| D2 | Board 决策 + 团队协作 | L4(A2 优势) | L4(保持) |
| D3 | 终端安全 | L3(A3 优势) | L4(R7a 完善后) |
| D4 | 4 层记忆 + Section CRUD | L4(A4 优势) | L4(保持) |
| D5 | 业务系统集成 | L4(A5 优势) | L4(保持) |
| D6 | ExecutionMode 多样性 | L4(A6 优势) | L4(保持) |
| D7 | Plan-Exec 状态机 | L4(A7 优势) | L4(保持) |
| D8 | Pipeline + Saga + Checkpoint | L4(A8 优势) | L4(保持) |
| D9 | 企业身份合规 (SSO/SCIM) | L1(G1 差距) | L3(P0 后) |
| D10 | 安全权限 (RBAC/审计) | L2 | L4(P0+P1 后) |
| D11 | 可观测性 (OTel) | L1(G9 差距) | L3(P0 后) |
| D12 | 部署企业级 (K8s/密钥) | L2 | L4(P0 后) |
| D13 | OS 沙箱分级 | L1(G3 差距) | L3(P1 R7 后) |
| D14 | 多端覆盖 | L1(G4 差距) | L2(P2 R13 后) |
> **G-label 双轨制定义:**
> - **Eval-G\***:横向评估差距标签(G1=企业身份 / G3=OS 沙箱 / G4=多端 / G5=Skill 生态 / G9=可观测性),见上表 D 维度映射。
> - **Brainstorm-G\***:`docs/brainstorms/2026-06-29-advanced-agent-gap-optimization-requirements.md` 差距标签(G3=tool schema / G5=函数级分 shard / G6=SOLO 状态机 / G8=delta_flush),Wave 前缀区分。
### Implementation Units
### U1. OTel 默认启用 (R5)
- **Goal:** 将 OTel 从"未声明依赖 + 静默 NoOp 回退"升级为"依赖已声明 + 默认启用 + 指标可观测"。
- **Requirements:** R5。
- **Dependencies:** 无(U1 是 P0 基础,其他 unit 依赖 U1)。
- **Files:**
- `pyproject.toml`(添加 4 个 opentelemetry-* 依赖到 `[project] dependencies`)
- `agentkit.yaml`(取消 `telemetry:` 段注释,设 `enabled=true` + `otlp_endpoint`)
- `src/agentkit/telemetry/tracer.py`(移除 ImportError 静默回退,改为直接 import;`init_telemetry` 在 `enabled=true` 但 init 失败时 log error 并回退 NoOp,而非 ImportError 静默)
- `src/agentkit/telemetry/metrics.py`(移除 try/except ImportError,改为直接 import)
- `src/agentkit/server/app.py``src/agentkit/server/__init__.py`(启动时调用 `init_telemetry`,添加 FastAPIInstrumentor)
- `tests/unit/test_telemetry_init.py`(新增)
- **Approach:**
1. 将 `opentelemetry-api` / `opentelemetry-sdk` / `opentelemetry-exporter-otlp-proto-grpc` / `opentelemetry-instrumentation-fastapi` 添加到 `pyproject.toml` `[project] dependencies`
2. 移除 `tracer.py:129``metrics.py:7``try/except ImportError` 静默回退,改为直接 import(依赖已声明,ImportError 是 bug 不是配置)。
3. `init_telemetry` 保留 `enabled=false` → NoOp 路径,但 ImportError 不再 catch(直接抛出)。
4. 取消 `agentkit.yaml:85-90` 注释,设 `telemetry.enabled=true` + `telemetry.otlp_endpoint=http://otel-collector:4317` + `telemetry.service_name=fischer-agentkit`
5. 在 server 启动时调用 `init_telemetry(TelemetryConfig(...))``FastAPIInstrumentor.instrument_app(app)`
- **Patterns to follow:** `src/agentkit/telemetry/tracer.py:103-134`(现有 init_telemetry 逻辑)、`src/agentkit/telemetry/metrics.py:32-58`(现有 meter/counter 模式)。
- **Test scenarios:**
- `init_telemetry(enabled=True, otlp_endpoint="...")` 初始化 `OTelTracer`,`get_tracer()` 返回 `OTelTracer` 实例
- `init_telemetry(enabled=False)` 返回 `NoOpTracer`(配置开关,非 ImportError 回退)
- `opentelemetry-*` 未安装时 `import telemetry.tracer` 抛出 `ImportError`(非静默 NoOp)—— 验证依赖已声明后此场景不应发生,但测试确认 ImportError 不被 catch
- FastAPI 请求触发 span(`agent.request.total` counter + `agent.execution.duration` histogram)
- LLM 调用触发 `gen_ai.usage.tokens` histogram
- `agentkit.yaml` `telemetry.enabled=false` 时 server 启动成功且 metrics 为 NoOp
- `agentkit.yaml``telemetry` 段时 server 启动成功且默认 NoOp(向后兼容)
- **Verification:** `agentkit serve` 启动时 log 显示 "Telemetry initialized with endpoint: ...";`agentkit gui` 发送一条 chat 消息后,OTel collector 收到 span + metric。
### U2. Prompt Cache 全链路 + 本地推理 Fallback (R3)
- **Goal:** 验证 prompt cache 路由到 volatile 层、`prompt_cache_enable` 默认 true、命中率监控接入 OTel;配置本地推理 fallback provider 作为等保合规替代。
- **Requirements:** R3 + R3 P0 验收门槛。
- **Dependencies:** U1(OTel metrics 用于 cache hit/miss 监控)。
- **Files:**
- `src/agentkit/core/react.py`(`_build_system_message` 现有,`_prompt_cache_enable` 默认值校验)
- `src/agentkit/llm/gateway.py``src/agentkit/llm/<provider>.py`(添加 cache hit/miss metric emission)
- `agentkit.yaml`(`llm.providers` 段添加 fallback provider 配置模板,`prompt_cache_enable` per-provider)
- `src/agentkit/llm/pii_filter.py`(新增,本地 NER 模型 PII 脱敏 hook,fail-closed)
- `tests/unit/test_prompt_cache_layers.py`(扩展,新增 cache hit/miss metric 测试)
- `tests/unit/test_pii_filter.py`(新增)
- **Approach:**
1. 验证 `ReActEngine._prompt_cache_enable` 默认 `True`(从 `agentkit.yaml` 或构造参数)。
2. 在 LLM gateway 的 `chat_stream` 添加 cache hit/miss metric:Anthropic 响应含 `usage.cache_read_input_tokens` > 0 时 emit `prompt_cache.hit`,否则 emit `prompt_cache.miss`
3. `agentkit.yaml` `llm.providers` 段添加 fallback provider 配置模板(DashScope / DeepSeek / Doubao OpenAI-compatible API),fallback providers 默认 `prompt_cache_enable=false`
4. 新增 `src/agentkit/llm/pii_filter.py`:本地 NER 模型(LAC/HanLP,部署在客户内网)识别 PII + 替换/掩码;脱敏前后记录 hash 用于审计(原文不落盘);脱敏失败时 `fail-closed`(拒绝发送至 Anthropic);仅在本地 fallback 可用时降级处理。
5. 脱敏覆盖率与漏报率纳入 OTel 指标(`pii_filter.coverage` / `pii_filter.false_negative`)。
- **Patterns to follow:** `tests/unit/test_prompt_cache_layers.py:46-58`(现有 Anthropic cache_control 测试)、`src/agentkit/llm/` 现有 LiteLLM 适配层。
- **Test scenarios:**
- Covers AE-F1(prompt cache 全链路):`prompt_cache_enable=True` + Anthropic provider → `cache_control` 透传到 content blocks(现有测试)
- `prompt_cache_enable=True` + 非 Anthropic provider → 不设 `cache_control`,返回字符串 concat(现有测试)
- cache hit 时 emit `prompt_cache.hit=1` OTel metric;cache miss 时 emit `prompt_cache.miss=1`
- fallback provider(DashScope)配置不设 `cache_control`,正常完成 chat
- PII 脱敏:输入含手机号 → 输出替换为 `[REDACTED]` + 记录 hash
- PII 脱敏失败(NER 模型不可用)→ `fail-closed` 抛出,不发送至 Anthropic
- fallback provider 可用时,脱敏失败降级发送至 fallback provider
- 脱敏覆盖率 metric 在 OTel 可观测
- **Verification:** `agentkit chat` 发送含 PII 的消息,确认 Anthropic 收到的是脱敏后 prompt;OTel collector 收到 `prompt_cache.hit/miss` + `pii_filter.coverage` 指标。
### U3. Residual P2 Findings 修复 (R4)
- **Goal:** 修复 8 项 residual P2 findings(3 项 feat-ui-ue-enhancement + 5 项 feat-bitable-enhancement)。
- **Requirements:** R4。
- **Dependencies:** 无(独立 bug fix 批次)。
- **Files:**
- `src/agentkit/experts/_phase_executor.py`(expert_step payload 补字段)
- `src/agentkit/experts/orchestrator.py`(team_synthesis 中断时发终结事件)
- `src/agentkit/server/frontend/src/stores/chatStream.ts`(synthesis_id 去重)
- `src/agentkit/bitable/repository.py`(DR-1:`text()` → 参数化查询 / ORM)
- `src/agentkit/server/frontend/src/components/bitable/ViewConfigPanel.vue`(DR-2:props 透传确认)
- `src/agentkit/bitable/service.py`(DR-4:`delete_view` 原子化事务)
- `src/agentkit/tools/bitable_tool.py`(DR-5:`_update_field` type 变更返回 422)
- `src/agentkit/server/frontend/src/styles/bitable-tokens.css`(DR-3:无,token 测试新增)
- `tests/unit/test_phase_executor_streaming.py`(扩展)
- `tests/unit/test_bitable_repository.py`(扩展,DR-1/DR-4)
- `tests/unit/test_bitable_service.py`(扩展,DR-4)
- `tests/unit/test_bitable_tool.py`(扩展,DR-5)
- `tests/frontend/bitable-tokens.test.ts`(新增,DR-3)
- **Approach:** 逐项修复:
1. **expert_step payload**:`_phase_executor.py:241` 的 `expert_step` 事件 payload 补齐 `expert_name` / `expert_color` / `content` / `step` 字段,与前端 `WsServerMessage` 契约对齐。
2. **team_synthesis 孤儿 milestone**:`orchestrator.py:293` 流式中断(异常/取消)时发 `team_synthesis` 终结事件(`status=error|cancelled`),前端不再永久转圈。
3. **synthesis_id 去重**:`chatStream.ts:880` `team_synthesis_chunk` 占位匹配用 `synthesis_id` 去重,避免附身到上一次孤儿 milestone。
4. **DR-1 SQL 注入**:`repository.py:660, 778-779` 的 `text()` 调用迁移到 ORM `select()` / `delete()` 或参数化 `text("... :param", {"param": value})`
5. **DR-2 ViewConfigPanel**:确认 props 透传 + event emit wiring(quick review,可能无需代码变更)。
6. **DR-4 TOCTOU**:`service.delete_view` 的 sibling-count 检查 + delete 移入 repository 的原子条件 delete(`DELETE ... WHERE table_id=:t AND (SELECT COUNT(*) FROM views WHERE table_id=:t) > 1 RETURNING id`)或 `SELECT ... FOR UPDATE` 事务。
7. **DR-5 _update_field**:`bitable_tool.py` 的 `_update_field` 在收到 `type` 参数时返回 422(unsupported field type change),而非静默成功;或扩展 `UpdateFieldRequest` 接受 `field_type`
8. **DR-3 design token**:新增 `tests/frontend/bitable-tokens.test.ts`,解析 CSS 文件并断言 `--bitable-color-*` / `--bitable-cf-*` / `--bitable-drawer-width``:root` 定义。
- **Patterns to follow:** `docs/residual-review-findings/feat-ui-ue-enhancement.md`、`docs/residual-review-findings/feat-bitable-enhancement.md`、现有 `_phase_executor.py` 流式 handler 模式。
- **Test scenarios:**
- Covers F1(expert_step):`expert_step` 事件 payload 含 `expert_id` / `expert_name` / `expert_color` / `content` / `step` 全字段
- Covers F2(team_synthesis):流式中断后 `team_synthesis` 终结事件发出,前端 milestone 关闭
- Covers F3(synthesis_id):`team_synthesis_chunk` 用 `synthesis_id` 去重,孤儿 milestone 不被附身
- DR-1:`text()` SQL 调用迁移后,参数化查询不接受字符串拼接的用户输入
- DR-4:并发删除最后两个 view,其中一个返回 409(不变量"table 至少 1 view"保持)
- DR-5:`update_field` 含 `type` 参数时返回 422(非静默成功)
- DR-3:CSS token 测试断言 `:root` 含所有必需 token
- **Verification:** `pytest tests/unit/test_phase_executor_streaming.py tests/unit/test_bitable_repository.py tests/unit/test_bitable_service.py tests/unit/test_bitable_tool.py -v` 全绿;`npm run test:frontend` DR-3 通过。
### U4. SSO 集成 — OIDC + SAML + SCIM (R1)
- **Goal:** 集成 OIDC + SAML 2.0 SSO(飞书 IDP / Azure AD / 阿里云 IDaaS)+ SCIM 2.0 最小子集(用户创建/禁用)+ 手动 deprovisioning + RBAC 审计。
- **Requirements:** R1, R1a, R1b, R1c。
- **Dependencies:** U1(审计日志接入 OTel)。
- **Files:**
- `src/agentkit/server/auth/providers/oidc.py`(新增,替换 `oidc_stub.py`)
- `src/agentkit/server/auth/providers/saml.py`(新增,SAML 2.0 适配器)
- `src/agentkit/server/auth/scim/__init__.py`(新增)
- `src/agentkit/server/auth/scim/router.py`(新增,`/scim/v2/Users` POST/PATCH/GET)
- `src/agentkit/server/auth/scim/handlers.py`(新增)
- `src/agentkit/server/auth/scim/models.py`(新增,SCIM User Schema)
- `src/agentkit/server/routes/auth.py`(新增 `/auth/oauth/{provider}/redirect` + `/callback` 路由)
- `src/agentkit/server/auth/dependencies.py`(多 IDP wiring,按 `auth.provider` 配置选择)
- `src/agentkit/server/auth/idp_registry.py`(新增,多 IDP 注册表 + 元数据热轮换)
- `src/agentkit/server/auth/audit.py`(新增或扩展,RBAC 变更 + deprovisioning 审计日志)
- `pyproject.toml`(`authlib>=1.3`, `python3-saml>=1.16`)
- `agentkit.yaml`(`auth` 段多 IDP 配置模板)
- `tests/unit/test_oidc_provider.py`(新增)
- `tests/unit/test_saml_provider.py`(新增)
- `tests/unit/test_scim_router.py`(新增)
- `tests/unit/test_auth_audit.py`(新增)
- `tests/integration/test_sso_oidc_flow.py`(新增,标记 `integration`)
- **Approach:**
1. **OIDC 适配器**(KTD-2):用 `authlib` 实现 redirect flow(`auth.providers.oidc.OIDCProvider`),替换 `StubOIDCProvider`。路由 `/auth/oauth/{provider}/redirect` → IDP authorize;`/auth/oauth/{provider}/callback` → token exchange + userinfo + 本地用户查/建(按 `sub` 主键,禁止邮箱合并 R1a)。
2. **SAML 适配器**:用 `python3-saml` 实现 ACS(Assertion Consumer Service)。`/auth/saml/{provider}/acs` 消费 assertion,提取 NameID 作为 subject identifier。
3. **多 IDP 信任边界**(R1a):`idp_registry.py` 管理多个 IDP 配置(签名证书 + endpoints);IDP 元数据(签名证书)支持热轮换(不重启服务);任一 IDP 不可达时该路径默认拒绝 + 明确错误,不影响其他 IDP;账号关联以 IDP 声明的 `sub`/`NameID` 为主键,禁止仅凭邮箱合并。
4. **SCIM 2.0 最小子集**(R1):`/scim/v2/Users` POST(创建)/ PATCH(禁用 `active=false`)/ GET(查询)。SCIM 推送失败触发实时告警(非等 cron)。
5. **过渡期 deprovisioning**(R1b):管理员手动 deprovisioning 接口(强制 operator/admin RBAC),每次调用记录 actor / target_user / reason / source=`scim_fallback|manual` / timestamp,接入 U1 OTel 审计通道;IDP 侧账号状态周期对账(每日 cron);高权限账号(operator/admin/danger tier)1h SLA,通过 break-glass 手动接口即时执行,cron 频率提升至每小时。
6. **RBAC 角色变更审计**(KTD-8,OQ14 缓解):新增 `audit.py` 记录 RBAC 角色变更(actor / target_user / role_before / role_after / reason / source),接入 R7a 同款审计日志通道。
7. **单租户假设**(R1c):数据模型 / RBAC / 审计 schema / SCIM 主键按单租户设计;在 R18 触发条件中增加"架构性 reversal cost"维度;pre-POC review 早期识别 multi-tenancy 需求。
- **Patterns to follow:** `src/agentkit/server/auth/providers/base.py`(`AuthProvider` Protocol)、`src/agentkit/server/auth/providers/local.py`(现有 Local provider 模式)、`src/agentkit/server/auth/jwt_utils.py`(JWT 签发)。
- **Test scenarios:**
- Covers F1(SSO 登录):OIDC redirect → callback → JWT 签发,`auth_sessions.auth_provider='oidc-feishu'` 写入
- Covers F2(多 IDP):飞书 + Azure AD + 阿里云 IDaaS 三 IDP 并存,各自独立认证
- 多 IDP subject identifier 防账号接管:不同 IDP 同邮箱 → 不合并为同一本地用户
- IDP 不可达:返回明确错误 + 其他 IDP 路径不受影响
- IDP 签名证书热轮换:不重启服务即可更新
- SAML ACS:消费 assertion + 提取 NameID
- SCIM POST `/scim/v2/Users`:创建用户
- SCIM PATCH `active=false`:禁用用户
- SCIM 推送失败:触发实时告警
- 手动 deprovisioning:operator/admin RBAC 强制 + 审计日志写入
- break-glass 接口:即时回收高权限账号
- 每日 cron 对账:检测 IDP 侧 vs 本地状态 drift
- RBAC 角色变更:审计日志记录 actor / target / before / after / reason
- **Verification:** `pytest tests/unit/test_oidc_provider.py tests/unit/test_saml_provider.py tests/unit/test_scim_router.py tests/unit/test_auth_audit.py -v` 全绿;`pytest -m integration tests/integration/test_sso_oidc_flow.py` 通过(需 mock IDP);手动测试飞书 IDP 登录。
### U5. K8s Helm Chart + Sealed Secrets + 离线安装包 (R2)
- **Goal:** 产出政企生产级 K8s 部署 artifacts(Helm Chart + 离线安装包 + 密钥管理 + 运维文档)。
- **Requirements:** R2, R2a。
- **Dependencies:** U1, U2, U3, U4(打包所有 P0 工作为可部署产物)。
- **Files:**
- `deploy/helm/agentkit/Chart.yaml`(新增)
- `deploy/helm/agentkit/values.yaml`(新增,列出所有 secret slot)
- `deploy/helm/agentkit/templates/_helpers.tpl`(新增)
- `deploy/helm/agentkit/templates/deployment.yaml`(新增)
- `deploy/helm/agentkit/templates/service.yaml`(新增)
- `deploy/helm/agentkit/templates/ingress.yaml`(新增,TLS 配置)
- `deploy/helm/agentkit/templates/configmap.yaml`(新增,非密配置)
- `deploy/helm/agentkit/templates/sealed-secret.yaml`(新增,Sealed Secrets 模板)
- `deploy/helm/agentkit/templates/external-secret.yaml`(新增,External Secrets Operator 模板)
- `deploy/helm/agentkit/templates/serviceaccount.yaml`(新增,workload identity 注入)
- `deploy/offline/Makefile`(新增)
- `deploy/offline/scripts/build-images.sh`(新增)
- `deploy/offline/scripts/package-chart.sh`(新增)
- `deploy/offline/scripts/install.sh`(新增,离线安装脚本)
- `docs/deployment/k8s-install.md`(新增)
- `docs/deployment/key-rotation-runbook.md`(新增,R2a 要求)
- `docs/deployment/secret-inventory.md`(新增,密钥清单)
- **Approach:**
1. **Helm Chart 结构**:`deploy/helm/agentkit/` 含 Chart.yaml / values.yaml / templates/。values.yaml 显式列出所有 secret slot(KTD-4):JWT 签名密钥 / API Key / DB 凭据 / IDP 签名证书 / 第三方 API Key / TLS 服务器证书私钥 / mTLS 客户端证书私钥 / KMS-HSM PKCS#11 PIN / OTel exporter token / Redis-PG 密码。
2. **Sealed Secrets / External Secrets Operator**:templates 提供 `sealed-secret.yaml``external-secret.yaml` 两种模板,客户按环境选择。禁用 plain Kubernetes Secret + Git 提交。
3. **KMS/HSM workload identity**:`serviceaccount.yaml` 配置 projected service account token 注入,KMS/HSM 访问凭证通过 workload identity(非静态 Secret)。
4. **离线安装包**:`deploy/offline/scripts/build-images.sh` 构建 Docker image tarball + `package-chart.sh` 打包 Helm chart tarball + `install.sh` 在离线环境解压安装。覆盖原生 K8s 1.28+ / Helm 3.x(OQ5:信创 K8s / OpenShift 延后到 P1 R12)。
5. **密钥轮换 runbook**:`docs/deployment/key-rotation-runbook.md` 文档化每个 secret slot 的轮换流程。
6. **传统 IDC fallback**(OQ8):不在 P0 范围,docker-compose 加固延后到 R18 触发条件识别后评估。文档中注明假设。
- **Patterns to follow:** Helm Chart 标准结构、bitnami Sealed Secrets chart、External Secrets Operator 文档。
- **Test scenarios:**
- `helm template deploy/helm/agentkit/` 渲染出有效 manifests(无模板错误)
- `helm install --dry-run --debug` 成功(values 校验通过)
- `helm lint deploy/helm/agentkit/` 无 warning
- Sealed Secret template 覆盖所有 10 个 secret slot
- External Secret template 覆盖所有 10 个 secret slot
- serviceaccount 含 projected token 注入配置
- `deploy/offline/scripts/build-images.sh` 产出 image tarball
- `deploy/offline/scripts/package-chart.sh` 产出 chart tarball
- 离线 install.sh 在 mock 离线环境成功安装
- **Verification:** `helm template` + `helm lint` 通过;离线安装包在测试 K8s 集群(minikube/kind)成功部署;密钥轮换 runbook 评审通过。
### U6. 等保三级文档准备 (R-等保)
- **Goal:** 启动等保三级文档与控制点清单编写(6 维度),目标认证 readiness 在政企 POC 采购关闭前完成。
- **Requirements:** R-等保。
- **Dependencies:** U1(OTel 审计日志)、U4(SSO/SCIM/RBAC)、U5(K8s 部署/密钥管理)提供应用/网络/主机维度输入。
- **Files:**
- `docs/compliance/dengbao-level3/00-overview.md`(新增,总览)
- `docs/compliance/dengbao-level3/01-physical.md`(新增,物理安全)
- `docs/compliance/dengbao-level3/02-network.md`(新增,网络安全)
- `docs/compliance/dengbao-level3/03-host.md`(新增,主机安全)
- `docs/compliance/dengbao-level3/04-application.md`(新增,应用安全 — 引用 R1/R5/R7a)
- `docs/compliance/dengbao-level3/05-data.md`(新增,数据安全 — 引用 R3 PII 脱敏 / R11a 加密 P1 参考)
- `docs/compliance/dengbao-level3/06-management.md`(新增,管理安全)
- `docs/compliance/dengbao-level3/control-points.yaml`(新增,控制点清单 + 实现映射)
- **Approach:**
1. 按 GB/T 22239-2019 等保三级 6 维度编写文档:物理 / 网络 / 主机 / 应用 / 数据 / 管理。
2. **物理**:依赖客户机房(K8s 部署在客户内网),文档描述 AgentKit 部署对物理环境的假设。
3. **网络**:引用 U5 Helm Chart 的 NetworkPolicy / Ingress TLS 配置;OTel exporter mTLS。
4. **主机**:引用 U5 容器镜像安全(非 root user / distroless base / 漏洞扫描)。
5. **应用**:引用 U4 SSO 多 IDP / RBAC 审计 / SCIM deprovisioning;U1 OTel 审计日志保留 180 天(KTD-9);R7a 6 层终端安全(现有)。
6. **数据**:引用 U2 R3 PII 脱敏(本地 NER + fail-closed + hash 审计);R11a 国密加密(P1 参考,文档中标注)。
7. **管理**:引用密钥轮换 runbook(U5);break-glass 告警流程(U4 R1b)。
8. `control-points.yaml` 列出每个控制点 + AgentKit 实现映射(已实现 / P0 / P1 / 待评估)。
- **Patterns to follow:** GB/T 22239-2019 等保三级标准、`docs/solutions/` 已记录的安全模式。
- **Test expectation:** none — documentation deliverable, no behavioral change。
- **Verification:** 文档由安全/合规 stakeholder 评审;`control-points.yaml` 通过 schema 校验(每个控制点含 id / dimension / title / implementation / status 字段)。
### Verification Contract
- **Unit 测试:** 每个 U1-U5 unit 的 `tests/unit/` 测试全绿(`pytest tests/unit/ -x -q`)。
- **Integration 测试:** U4 SSO OIDC flow 测试通过(`pytest -m integration tests/integration/test_sso_oidc_flow.py`)。
- **前端测试:** U3 DR-3 design token 测试通过(`npm run test:frontend`);U3 前端 fix 不破坏现有 e2e。
- **部署验证:** U5 Helm Chart `helm template` + `helm lint` + 离线安装包在 minikube/kind 部署成功。
- **启动验证:** U1 `agentkit serve` / `agentkit gui` 启动时 OTel 初始化 log 出现;`telemetry.enabled=false` 时向后兼容。
- **端到端验证:** U2 `agentkit chat` 发送含 PII 的消息,Anthropic 收到脱敏 prompt + OTel 收到 cache hit/miss metric。
- **审计验证:** U4 手动 deprovisioning + RBAC 角色变更触发审计日志写入(OTel span + 日志文件)。
- **文档验证:** U6 等保文档由 stakeholder 评审;`control-points.yaml` schema 校验通过。
- **Lint / 格式化:** `ruff check src/ && ruff format src/` 无错误;`npm run typecheck` 无错误。
### Definition of Done
- P0 6 项全交付(R1 + R2 + R3 + R4 + R5 + R-等保):
- R5(U1):OTel 依赖声明 + 默认启用 + NoOp 不再静默回退。
- R3(U2):prompt cache 全链路验证 + 本地推理 fallback 配置 + PII 脱敏 + OTel 命中率监控。
- R4(U3):8 项 residual findings 修复 + 测试覆盖。
- R1(U4):OIDC + SAML SSO + SCIM 最小子集 + 多 IDP 信任 + deprovisioning + RBAC 审计。
- R2(U5):Helm Chart + Sealed Secrets + 离线安装包 + 密钥轮换 runbook。
- R-等保(U6):6 维度文档 + 控制点清单,readiness 在政企 POC 采购关闭前完成。
- 横向评估 Eval-D9(企业身份)从 L1 提升到 L3;Eval-D11(可观测性)从 L1 提升到 L3;Eval-D12(部署企业级)从 L2 提升到 L4;总分从 42 提升到 47+/56。
- 所有 unit 测试 + integration 测试 + 前端测试通过。
- 政企客户 POC 可基于 P0 交付物启动(首个政企客户 POC 落地)。
- P1 / P2 阵列不在本次 DoD,作为 follow-up plan 推进。
### Risks & Dependencies
- **R-风险1. Anthropic API 依赖与等保冲突**(OQ13/OQ15):R3 依赖 Anthropic 境外服务,与等保三级数据本地化冲突。缓解:本地推理 fallback(U2)+ PII 脱敏(U2)+ DPA 接受 TTL 残留风险。残留:Anthropic 路径 prompt cache 在 fallback 路径下价值为零(OQ13 已记录为战略缺口)。
- **R-风险2. 单租户 reversal cost 高**(OQ16):R1c 单租户假设是 load-bearing 决策。缓解:R18 触发条件增加"架构性 reversal cost"维度 + pre-POC review 早期识别。残留:若销售阶段识别到 multi-tenancy 需求,retrofit 成本极高(数据迁移 + RBAC 重构 + 审计 schema 变更)。
- **R-风险3. SCIM group→role 缺失**(OQ14):P0 SCIM 仅用户创建/禁用,角色分配手动。缓解:RBAC 角色变更审计 logging(KTD-8)。残留:等保三级要求特权账号变更可审计,手动分配 + 审计满足,但 group→role 自动化延后。
- **R-风险4. 传统 IDC 客户不兼容**(OQ8):P0 K8s-only,传统 IDC 客户(华为云 Stack / 阿里云 Apsara Stack)无法部署。缓解:R18 触发条件识别"传统 IDC 客户"作为升级触发。残留:docker-compose 加固 fallback 不在 P0。
- **R-风险5. 等保认证周期长**:R-等保文档 P0 启动,认证审计在 P1 R10。若政企 POC 采购关闭前认证 readiness 未完成,阻塞 POC。缓解:文档与 MVP 并行推进 + 政企专属反馈通道(R18a)。
- **依赖:** K8s 1.28+ / Helm 3.x 部署环境;飞书 / Azure AD / 阿里云 IDaaS IDP 协议稳定性;Anthropic API 持续支持 `cache_control`;`complex-task-quality-loop` plan 的 11 项需求 + 10 KTD 作为 P0 启动基线。
### System-Wide Impact
- **认证体系:** U4 SSO 改变登录流程 —— 从 LocalAuthProvider(username+password)扩展到多 IDP OIDC/SAML + SCIM。`auth_sessions.auth_provider` 字段记录来源;现有 Local provider 保持向后兼容。
- **可观测性:** U1 OTel 默认启用 —— 所有 FastAPI 请求 + LLM 调用 + tool 调用 emit span/metric。运维需部署 OTel Collector + 后端(Prometheus/Tempo/Loki)。
- **部署:** U5 Helm Chart 改变交付方式 —— 从 docker-compose dev 部署扩展到 K8s 生产部署。运维需熟悉 Helm + Sealed Secrets/External Secrets Operator。
- **合规:** U6 等保文档 + U1 审计日志 180 天保留 —— 影响存储容量规划(OTel span + 审计日志)。
- **前端:** U3 expert_step payload + team_synthesis 修复 —— 前端 `WsServerMessage` 契约对齐,需同步前端代码。
- **配置:** `agentkit.yaml` 新增 `auth` 段(多 IDP)+ `telemetry` 段(默认启用)+ `llm.providers` fallback 模板。
### Open Questions (Planning-Resolved)
- OQ1, OQ2:已关闭(plan 已记录)。
- OQ3(R7 沙箱分级):R7 在 P1,P0 不需要沙箱分级。P1 plan 将决定是否三个 tier 全完成或仅补 read-only。
- OQ4(SSO 协议优先级):**已解决** — OIDC 优先(KTD-2),SAML 第二阶段。三个目标 IDP 均支持 OIDC。
- OQ5(K8s 兼容矩阵):**已解决** — P0 原生 K8s 1.28+ / Helm 3.x;信创 K8s / OpenShift 延后到 P1 R12;传统 IDC fallback 在 R18 触发条件识别后评估。
- OQ6(信创 DB):延后到 P1(R12)。P0 假设 PostgreSQL。
- OQ7(Subagents worktree max_threads):延后到 P1(R6)。
- OQ8(传统 IDC):**已解决** — P0 K8s-only;docker-compose 加固不在 P0;R18 触发条件识别"传统 IDC 客户"作为升级触发。文档已标注假设。
- OQ11(D-label 维度):**已解决** — 14 维度编号已在 HTD 部分列出完整表格。
- OQ12(G-label 双轨制):**已解决** — 两套 gap scheme 定义已在 HTD 部分显式列出。
- OQ13(R3 cache 价值):**已解决** — R3 P0 优先级服务中大型企业 Anthropic-eligible 客户段;fallback 路径不依赖 cache 价值(KTD-5)。战略缺口已记录,不在 P0 解决。
- OQ14(SCIM group→role):**已解决** — SCIM group→role 延后;P0 SCIM 用户创建/禁用 + 手动 RBAC + 审计 logging(KTD-8)。
- OQ15(DPA cache 删除权):**已解决** — DPA 明确 TTL-only 删除(最长 1 小时);fallback providers 默认 `prompt_cache_enable=true`;Anthropic 路径客户签 DPA 接受 TTL 残留风险(KTD-5)。
- OQ16(R1c 单租户 reversal cost):**已解决** — reversal cost 评估加入 R18 触发条件;pre-POC review 早期识别 multi-tenancy 需求(KTD-1)。单租户保持 load-bearing 假设。
### Open Questions (Deferred to P1/P2 Planning)
- OQ9(R6/R8/R9 P1→P2 重分类):延后到 P1 plan。P1 plan 将评估 P1 theme 是否扩展为"企业级基线 + 性能优化",或重分类至 P2。scope-guardian 关注已记录。
- OQ10(P2 不深化护城河):延后到 P2 plan。P2 plan 将决定增加深化护城河的 items,还是修订 Goal statement 承认 P2 是新能力。scope-guardian 关注已记录。
---
## ce-doc-review 追踪(2026-07-05)
**Round 1:** 应用 12 项 Apply 修复 + 5 项 Defer 到 Open Questions(OQ8-OQ12)+ 1 项 safe_auto + 3 项 FYI 观察未决策。
**Round 2(re-review):** 应用 1 项 safe_auto(P0 计数 5→6 修正)+ 10 项 Apply(含 R5 一行配置修正 / R3 fallback 提升验收 / R7a 命令面约束 / R-等保 gate 明确 / R11a 降级攻击防护 / R3 PII 脱敏机制 / R2a 密钥清单 / R1b RBAC + 高权限 SLA / R5'a 保留期 + Span 脱敏扩展 / Key Decisions R5 修正)+ 4 项 Defer 到 Open Questions(OQ13-OQ16)+ 4 项 FYI 观察未决策。详情见会话记录。

View File

@ -14,7 +14,19 @@ verification). They are **actionable** (`autofix_class: manual`) but were
**not applied** in Step 5 because they need design input or app+chart
coordinated changes. They are durably recorded here per lfg SKILL.md Step 6.
## P1 — High-impact defects
## Resolution Status (updated 2026-07-06)
- **P1 (7 项)**: 全部在 commit `cb278bb` (PR #24) 修复
- **P2 (10 项)**: 全部在 commit `cb278bb` (PR #24) 修复
- **P3 (15 项, 原文档计数为 10)**:
- SEC-3, API-13: 在 commit `cb278bb` (PR #24) 修复(文档未及时更新)
- 其余 13 项在 `feat/p3-residual-fixes` 分支修复
**全部 27+2=29 项 residual findings 已清零。**
---
## P1 — High-impact defects ✅ ALL FIXED (commit cb278bb)
- **P1 / `src/agentkit/llm/pii_filter.py` + `src/agentkit/llm/gateway.py`**
PERF-1: PII 脱敏模块未接入生产 LLM 调用路径(死代码)。`filter_messages_pii`
@ -110,73 +122,73 @@ coordinated changes. They are durably recorded here per lfg SKILL.md Step 6.
STAND-1: SCIM router 使用 `typing.Any` 7 处,违反 AGENTS.md 禁用 any 规则。
**Fix**: 定义 `SCIMUserResponse` / `SCIMUserListResponse` Pydantic 模型。
## P3 — Low-impact / narrow scope
## P3 — Low-impact / narrow scope ✅ ALL FIXED
- **P3 / `src/agentkit/server/auth/providers/oidc.py:185-197`**
SEC-2: OIDC id_token 未按规范验签key=None + 关闭 iss/exp当前
authlib 1.6.12 fail-closed非直接绕过但规范违反且脆弱。**Fix**: 用
IDP JWKS 公钥验签,校验 iss/aud/exp
SEC-2: OIDC id_token 未按规范验签key=None + 关闭 iss/exp✅ FIXED:
新增 `OIDCConfig.jwks_uri` + `issuer` 字段,`_verify_id_token` 用 JWKS 验签 +
IDToken claims_cls 校验 iss/aud/expfail-closed
- **P3 / `src/agentkit/server/auth/scim/router.py:170-185`**
SEC-3: SCIM PATCH deprovisioning`active=false`不写审计。admin 同等
操作有审计SCIM 没有。**Fix**: 检测到 active true→false 时调用
`record_deprovision(source=SOURCE_SCIM)`
SEC-3: SCIM PATCH deprovisioning 不写审计。✅ FIXED (commit cb278bb):
active true→false 调用 `record_deprovision(source=SOURCE_SCIM)`,延后到事务提交后。
- **P3 / `src/agentkit/llm/gateway.py:401-414`**
PERF-6: 流式路径在无 usage 数据时记录"cache miss",指标误导。**Fix**:
仅当 `final_usage` 非 None 时才记录 cache metric
PERF-6: 流式路径在无 usage 数据时记录"cache miss"。✅ FIXED:
`_record_prompt_cache_metric` 接受 `TokenUsage | None`None 时直接 return
- **P3 / `src/agentkit/llm/gateway.py:450-457`**
PERF-7: 非 Anthropic provider 调用膨胀 `prompt_cache.miss` 计数器。
**Fix**: 仅对 Anthropic provider 记录 cache metric。
PERF-7: 非 Anthropic provider 膨胀 `prompt_cache.miss` 计数器。✅ FIXED:
仅对 model 含 "anthropic"/"claude" 的 provider 记录 cache metric。
- **P3 / `src/agentkit/server/auth/scim/router.py:171-187`**
API-12: SCIM PATCH 静默忽略不支持的 op/path,无错误反馈。**Fix**:
API-12: SCIM PATCH 静默忽略不支持的 op/path。✅ FIXED:
收集 skipped ops 并返回 400 + `scimType=noTarget`
- **P3 / `src/agentkit/server/auth/scim/router.py:200-238`**
API-13: SCIM filter 解析对属性名和操作符大小写敏感,违反 RFC 7644
3.4.2.2。**Fix**: 用 `filter.lower()` + 正则 `re.IGNORECASE`
API-13: SCIM filter 解析对属性名和操作符大小写敏感。✅ FIXED (commit cb278bb):
`filter.lower()` 大小写不敏感匹配
- **P3 / `src/agentkit/server/auth/scim/router.py:154-197`**
API-14: SCIM PATCH `op.path==None` 时仅更新 active丢弃 value 中的其他
字段(部分实现静默丢数据)。**Fix**: 遍历 value 的 keys 对每个已知属性
执行对应 UPDATE。
API-14: SCIM PATCH `op.path==None` 时仅更新 active丢弃其他字段。✅ FIXED:
path==None + value 是 dict 时遍历所有 keysactive/userName执行对应 UPDATE。
- **P3 / `src/agentkit/server/auth/models.py:609-639`**
MIG-2: schema 迁移幂等但缺少 V4→V5 的显式迁移记录。**Fix**: 未来新增列
时按 schema_version 差值执行 ALTER TABLE
MIG-2: schema 迁移幂等但缺少 V4→V5 的显式迁移记录。✅ FIXED:
新增 `_SCHEMA_MIGRATIONS` 注册表 + `_run_schema_migrations` 按 version 差值执行
- **P3 / `deploy/helm/agentkit/values.yaml:8-11`**
DEPLOY-9: 镜像仅 tag 固定0.1.0),未做 digest pinning。政企生产场景
要求 digest pinning 防篡改。**Fix**: 支持可选 `image.digest` 字段
DEPLOY-9: 镜像未做 digest pinning。✅ FIXED:
新增 `image.digest` 字段deployment.yaml 优先用 `<repo>@<digest>`
- **P3 / `docs/deployment/key-rotation-runbook.md:254-283`**
DEPLOY-8: runbook 仅 Slot 1-10缺独立 Slot 11PG 密码)段落。**Fix**:
拆为两节Slot 11 内注明须同步更新 `database-url`Slot 3
DEPLOY-8: runbook 缺独立 Slot 11。✅ FIXED:
拆为 Slot 10 (Redis) + Slot 11 (PostgreSQL)Slot 11 注明同步更新 database-url (Slot 3)
- **P3 / `src/agentkit/bitable/service.py:564-568`**
MAINT-2: `delete_view` 将两种不同失败原因统一抛 `LastViewDeletionError`
误导性错误信息。**Fix**: 区分 `DELETED`/`LAST_VIEW`/`NOT_FOUND` 三态
MAINT-2: `delete_view` 将两种失败原因统一抛 `LastViewDeletionError`。✅ FIXED:
重新检查 view 是否存在区分 LAST_VIEW vs 并发竞争,错误信息含 table_id
- **P3 / `src/agentkit/server/frontend/src/stores/chatStream.ts:881-887,923-929`**
MAINT-3: `team_synthesis_chunk``team_synthesis` 重复同一 `findLastMessage`
谓词。**Fix**: 提取 `findStreamingSynthesisMilestone(conv, sid)` helper。
MAINT-3: `team_synthesis_chunk``team_synthesis` 重复同一谓词。✅ FIXED:
提取 `findStreamingSynthesisMilestone(conv, sid)` helper。
- **P3 / `src/agentkit/server/frontend/tests/unit/bitable-tokens.test.ts:75,81,85`**
MAINT-4: token 声明正则转义重复 3 次。**Fix**: 提取 `tokenDeclPattern(token)`
helper。
MAINT-4: token 声明正则转义重复 3 次。✅ FIXED:
提取 `tokenDeclPattern(token)` helper。
- **P3 / `src/agentkit/server/frontend/tests/unit/bitable-tokens.test.ts:92`**
STAND-2: test "outside root" 用 replace 仅替换首个 `:root` 块。**Fix**:
STAND-2: test "outside root" 用 replace 仅替换首个 `:root` 块。✅ FIXED:
`g` 标志 `replace(/:root\s*\{[^}]*\}/g, '')`
- **P3 / `src/agentkit/server/app.py:1401`**
STAND-3: SCIM router 挂载在 `/api/v1` 前缀下,偏离 RFC 7644 标准 SCIM
路径 `/scim/v2`。**Fix**: SCIM router 单独挂载不加 `/api/v1` 前缀
STAND-3: SCIM router 挂载在 `/api/v1` 前缀下,偏离 RFC 7644 标准。✅ FIXED:
改挂 `/scim/v2`,异常处理器 scope 检查简化
---
**Total residual findings**: 27 (P1=7, P2=10, P3=10)
**Total residual findings**: 29 (P1=7, P2=10, P3=15 — 原文档 P3 计数为 10实际 15 项)
**Applied in Step 5**: 9 fixes (commit `3cf29f4`)
**Deferred to follow-up**: 27 (this file)
**Applied in ce-debug batch (PR #24)**: 17 fixes (commit `cb278bb`) — 全部 P1 + 全部 P2 + SEC-3 + API-13
**Applied in P3 cleanup (feat/p3-residual-fixes)**: 13 fixes — 其余 P3
**Deferred to follow-up**: 0 — 全部清零 ✅

View File

@ -560,12 +560,17 @@ class BitableService:
"""
view = await self._repo.get_view(view_id)
if view is None:
return False
return False # NOT_FOUND
deleted = await self._repo.delete_view_if_not_last(view_id, view.table_id)
if not deleted:
# Either it was the last view, or a concurrent call raced ahead.
# Either way, the invariant "table has at least 1 view" is preserved.
raise LastViewDeletionError("Cannot delete the last view of a table")
# MAINT-2: 区分 LAST_VIEW vs 并发竞争 — 重新检查 view 是否仍存在。
# 若已不存在,说明并发调用已删除(目标达成);否则真的是最后一个 view。
race_check = await self._repo.get_view(view_id)
if race_check is None:
return True # 并发竞争:已被另一个调用删除,目标已达成
raise LastViewDeletionError(
f"Cannot delete the last view of table '{view.table_id}' (view_id={view_id})"
)
return True
# ── Recalc (U3: formula recalc pipeline) ────────────────

View File

@ -440,6 +440,7 @@ class LLMGateway:
# Track usage after successful stream
latency_ms = (time.monotonic() - start) * 1000
stream_usage = final_usage # PERF-6: 保留原始 usageNone 表示 provider 未返回)
if final_usage is None:
final_usage = TokenUsage()
cost = self._calculate_cost(final_model, final_usage)
@ -453,7 +454,8 @@ class LLMGateway:
department_ids=department_ids,
)
# U2 — prompt cache hit/miss OTel counterstreaming path
self._record_prompt_cache_metric(final_usage, final_model)
# PERF-6/7: 传入原始 stream_usage可能为 None+ 仅 Anthropic 记录
self._record_prompt_cache_metric(stream_usage, final_model)
# Empty stream detection: if no content was produced,
# raise error so the caller (ReActEngine) can retry with a different model.
@ -490,8 +492,20 @@ class LLMGateway:
)
@staticmethod
def _record_prompt_cache_metric(usage: TokenUsage, model: str) -> None:
"""Record prompt cache hit/miss OTel counter — Anthropic cache_read_input_tokens > 0 → hit."""
def _record_prompt_cache_metric(usage: TokenUsage | None, model: str) -> None:
"""Record prompt cache hit/miss OTel counter — Anthropic only.
PERF-6: 仅当 usage None 时记录provider 未返回 usage 时空 TokenUsage
会误报 cache miss膨胀 miss 计数器
PERF-7: 仅对 Anthropic provider 记录cache_read_input_tokens
Anthropic 专有字段 Anthropic provider 永远 cache_hit=False 误报 miss
"""
if usage is None:
return # PERF-6: 无 usage 数据,不记录
model_lower = model.lower()
# PERF-7: 仅 Anthropic/Claude 支持 prompt caching
if "anthropic" not in model_lower and "claude" not in model_lower:
return
attrs = {"gen_ai.request.model": model}
if usage.cache_hit:
prompt_cache_hit_counter().add(1, attrs)

View File

@ -1398,12 +1398,13 @@ def create_app(
app.include_router(auth_routes.router, prefix="/api/v1")
app.include_router(auth_routes.admin_router, prefix="/api/v1")
# U4 SSO 集成 — SCIM 2.0 最小子集(自带 bearer token 校验)
# STAND-3: 按 RFC 7644 标准SCIM 端点挂载在 /scim/v2非 /api/v1
from agentkit.server.auth.scim.router import (
register_scim_exception_handlers,
router as scim_router,
)
app.include_router(scim_router, prefix="/api/v1")
app.include_router(scim_router, prefix="/scim/v2")
# API-2: 注册 SCIM 异常处理器scope 到 /scim/v2非 SCIM 路由走默认格式)
register_scim_exception_handlers(app)
app.include_router(admin_routes_module.admin_router, prefix="/api/v1")

View File

@ -36,6 +36,11 @@ class OIDCConfig(BaseModel):
redirect_uri: str
# 可选scope 覆盖(默认 openid email profile
scope: str = "openid email profile"
# SEC-2: id_token 验签所需 — JWKS URIIDP discovery 文档的 jwks_uri 字段)
# 未配置时 id_token 回退路径 fail-closed不再用 key=None 跳过验签)。
jwks_uri: str | None = None
# SEC-2: id_token iss 校验期望值IDP discovery 文档的 issuer 字段)
issuer: str | None = None
class SAMLConfig(BaseModel):

View File

@ -24,7 +24,7 @@ from __future__ import annotations
import json
import logging
import os
from collections.abc import Mapping
from collections.abc import Awaitable, Callable, Mapping
from datetime import datetime, timezone
from pathlib import Path
@ -664,6 +664,27 @@ _SCHEMA_VERSION = 5
_META_SCHEMA_VERSION_KEY = "schema_version"
# MIG-2: 显式 schema 迁移注册表 — 按 schema_version 差值执行 ALTER TABLE。
# 新增列/修改列时在此追加 migration 函数(每个必须 idempotent
# 当前 V5 全为 additiveCREATE TABLE IF NOT EXISTS无 ALTER TABLE 迁移。
# 升级路径示例V5→V6 新增列 → 在此注册 `6: _migrate_v5_to_v6`。
# type: ignore[misc] — dict value 类型异构(不同 migration 签名一致)
_SCHEMA_MIGRATIONS: dict[int, "Callable[[aiosqlite.Connection], Awaitable[None]]"] = {}
async def _run_schema_migrations(db: aiosqlite.Connection, current_version: int) -> None:
"""MIG-2: 按 schema_version 差值顺序执行迁移。
每个 migration 函数必须 idempotent可重复执行不报错
升级路径current_version+1 ... _SCHEMA_VERSION
"""
for v in range(current_version + 1, _SCHEMA_VERSION + 1):
migration = _SCHEMA_MIGRATIONS.get(v)
if migration is not None:
await migration(db)
logger.info(f"Auth DB migration V{v - 1}→V{v} applied")
async def _get_meta_value(db: aiosqlite.Connection, key: str) -> str | None:
"""Read a key from the ``auth_meta`` table. Returns ``None`` if missing."""
cursor = await db.execute("SELECT value FROM auth_meta WHERE key = ?", (key,))
@ -804,6 +825,10 @@ async def init_auth_db(db_path: str | Path | None = None) -> Path:
# Record the current schema version (idempotent upsert).
current = await _get_meta_value(db, _META_SCHEMA_VERSION_KEY)
if current != str(_SCHEMA_VERSION):
# MIG-2: 显式运行 schema_version 差值迁移ALTER TABLE 等)
current_int = int(current) if current else 0
if current_int < _SCHEMA_VERSION:
await _run_schema_migrations(db, current_int)
await _set_meta_value(db, _META_SCHEMA_VERSION_KEY, str(_SCHEMA_VERSION))
logger.info(f"Auth DB schema version set to {_SCHEMA_VERSION}")

View File

@ -154,7 +154,7 @@ class OIDCProvider:
),
)
# 拉取 userinfoOIDC discovery 的标准端点)
userinfo = await self._fetch_userinfo(client, token, cfg.server_metadata_url)
userinfo = await self._fetch_userinfo(client, token, cfg.server_metadata_url, cfg)
sub = str(userinfo.get("sub", ""))
if not sub:
@ -174,6 +174,7 @@ class OIDCProvider:
client: AsyncOAuth2Client,
token: dict[str, Any],
metadata_url: str,
cfg: Any,
) -> dict[str, Any]:
"""从 OIDC userinfo endpoint 拉取用户信息。
@ -184,21 +185,52 @@ class OIDCProvider:
# authlib 的 token 已自动设置到 client 的 token_auth可直接 GET
resp = await client.get(metadata_url)
if resp.status_code != 200:
# 退回 token 中的 id_token 解码authlib 已校验签名)
# SEC-2: 回退到 id_token 解码 — 必须用 JWKS 验签 + 校验 iss/aud/exp
# (原来 key=None + 关闭 iss/exp 是规范违反且脆弱)
id_token = token.get("id_token")
if id_token:
from authlib.jose import jwt as authlib_jwt # 局部导入避免硬依赖
claims = authlib_jwt.decode(
id_token,
None,
claims_options={"iss": {"essential": False}, "exp": {"essential": False}},
) # type: ignore[arg-type]
return dict(claims)
return await self._verify_id_token(id_token, cfg)
raise ValueError(f"拉取 userinfo 失败: HTTP {resp.status_code}")
data = resp.json()
return dict(data) if isinstance(data, dict) else {}
async def _verify_id_token(self, id_token: str, cfg: Any) -> dict[str, Any]:
"""SEC-2: 用 IDP JWKS 公钥验签 id_token校验 iss/aud/exp。
fail-closed: ``jwks_uri`` 未配置时抛 ValueError不再用 key=None 跳过验签
ponytail: JWKS 每次回退都拉取回退路径罕见不值得缓存
上限高频回退场景需加 JWKS 缓存 kid 缓存 + TTL
"""
if not cfg.jwks_uri:
raise ValueError(
"SEC-2: id_token 验签需要 OIDCConfig.jwks_uri但未配置 — "
"无法安全回退到 id_token 解码fail-closed"
)
import httpx
from authlib.jose import JsonWebKey, jwt
from authlib.oidc.core import IDToken
# 1) 拉取 JWKS
async with httpx.AsyncClient(timeout=10.0) as http:
jwks_resp = await http.get(cfg.jwks_uri)
jwks_resp.raise_for_status()
jwks_data = jwks_resp.json()
key_set = JsonWebKey.import_key_set(jwks_data)
# 2) 验签 + 校验 iss/aud/expIDToken claims_cls 默认开启这些校验)
claims_options: dict[str, dict[str, Any]] = {}
if cfg.issuer:
claims_options["iss"] = {"essential": True, "value": cfg.issuer}
claims = jwt.decode(
id_token,
key_set,
claims_cls=IDToken,
claims_options=claims_options,
) # type: ignore[arg-type]
# aud 校验用 client_id
claims.validate(client_id=cfg.client_id)
return dict(claims)
async def _find_or_create_user(
self,
*,

View File

@ -36,7 +36,9 @@ from .models import SCIMListResponse, SCIMPatchRequest, SCIMUser
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/scim/v2", tags=["scim"])
# STAND-3: prefix 由挂载点控制app.py / test fixture 通过 include_router(prefix="/scim/v2") 注入)。
# router 内部不设 prefix避免双重前缀 /scim/v2/scim/v2/Users。
router = APIRouter(tags=["scim"])
_SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
_SCIM_ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error"
@ -131,10 +133,8 @@ def register_scim_exception_handlers(app) -> None:
@app.exception_handler(HTTPException)
async def _scim_http_handler(request: Request, exc: HTTPException):
path = request.url.path
is_scim = path.startswith("/scim/v2") or path.startswith("/api/v1/scim/v2")
if not is_scim:
# 非 SCIM 路由 — 复制 FastAPI 默认 HTTPException handler 行为(含 headers
# STAND-3: SCIM 挂载在 /scim/v2非 SCIM 路由走 FastAPI 默认格式
if not request.url.path.startswith("/scim/v2"):
headers = getattr(exc, "headers", None)
return JSONResponse(
status_code=exc.status_code,
@ -145,10 +145,7 @@ def register_scim_exception_handlers(app) -> None:
@app.exception_handler(RequestValidationError)
async def _scim_validation_handler(request: Request, exc: RequestValidationError):
path = request.url.path
is_scim = path.startswith("/scim/v2") or path.startswith("/api/v1/scim/v2")
if not is_scim:
# 非 SCIM 路由 — 复制 FastAPI 默认 RequestValidationError handler 行为
if not request.url.path.startswith("/scim/v2"):
from fastapi.encoders import jsonable_encoder
return JSONResponse(
@ -277,33 +274,82 @@ async def patch_user(
if row is None:
return _scim_error_response(404, "用户不存在")
prev_active = bool(row["is_active"])
# API-12: 收集未处理的 op/path循环结束后返回 400 noTarget
_skipped_ops: list[str] = []
for op in payload.Operations:
if op.op.lower() == "replace":
if op.path in (None, "active"):
active = (
bool(op.value)
if not isinstance(op.value, dict)
else bool(op.value.get("active", True))
)
await db.execute(
"UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?",
(1 if active else 0, now, user_id),
)
# SEC-3: active true→false 写审计(延后到事务提交后)
if prev_active and not active:
_pending_audit = {
"target_user_id": user_id,
"target_username": str(row["username"]),
}
elif op.path == "userName":
try:
op_lower = op.op.lower() if op.op else ""
if op_lower not in ("replace", "add", "remove"):
# API-12: 仅 replace/add/remove 受支持(当前仅 replace 有副作用)
_skipped_ops.append(f"op={op.op}")
continue
if op_lower != "replace":
# add/remove 暂未实现 — 收集而非静默忽略
_skipped_ops.append(f"op={op.op}(未实现)")
continue
# API-14: path==None 时遍历 value 的所有 keys原来仅更新 active 丢数据)
if op.path is None and isinstance(op.value, dict):
for field_name, field_value in op.value.items():
if field_name == "active":
active = bool(field_value)
await db.execute(
"UPDATE users SET username = ?, updated_at = ? WHERE id = ?",
(str(op.value)[:64], now, user_id),
"UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?",
(1 if active else 0, now, user_id),
)
except aiosqlite.IntegrityError:
# API-7: UNIQUE 冲突返回 409
return _scim_error_response(409, "userName 已存在")
if prev_active and not active:
_pending_audit = {
"target_user_id": user_id,
"target_username": str(row["username"]),
}
elif field_name == "userName":
try:
await db.execute(
"UPDATE users SET username = ?, updated_at = ? WHERE id = ?",
(str(field_value)[:64], now, user_id),
)
except aiosqlite.IntegrityError:
return _scim_error_response(409, "userName 已存在")
else:
# API-12: 不支持的字段收集
_skipped_ops.append(f"path={field_name}")
continue
# path 明确指定的情况
if op.path in ("active", None):
active = (
bool(op.value)
if not isinstance(op.value, dict)
else bool(op.value.get("active", True))
)
await db.execute(
"UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?",
(1 if active else 0, now, user_id),
)
# SEC-3: active true→false 写审计(延后到事务提交后)
if prev_active and not active:
_pending_audit = {
"target_user_id": user_id,
"target_username": str(row["username"]),
}
elif op.path == "userName":
try:
await db.execute(
"UPDATE users SET username = ?, updated_at = ? WHERE id = ?",
(str(op.value)[:64], now, user_id),
)
except aiosqlite.IntegrityError:
# API-7: UNIQUE 冲突返回 409
return _scim_error_response(409, "userName 已存在")
else:
# API-12: 不支持的 path 收集
_skipped_ops.append(f"path={op.path}")
# API-12: 有未处理的 op/path → 400 noTarget
if _skipped_ops:
return _scim_error_response(
400,
f"SCIM PATCH 包含不支持的操作scimType=noTarget: {', '.join(_skipped_ops)}",
)
await db.commit()
cursor = await db.execute("SELECT * FROM users WHERE id = ?", (user_id,))
row = await cursor.fetchone()

View File

@ -99,6 +99,22 @@ function findLastMessage(
return undefined
}
// MAINT-3: 提取 team_synthesis_chunk / team_synthesis 共用的 milestone 查找谓词。
// 按 synthesis_id 精确匹配 streaming milestonesid 为空时退化为任意 streaming milestone
function findStreamingSynthesisMilestone(
conv: { messages: IChatMessage[] } | undefined,
sid: string | undefined,
): IChatMessage | undefined {
if (!conv) return undefined
return findLastMessage(
conv.messages,
(m) =>
m.message_type === "milestone" &&
m.status === "streaming" &&
(sid ? m.synthesis_id === sid : true),
)
}
// ── ChatStreamState: deps bag for dispatchWsEvent ──────────────────────
/**
@ -878,13 +894,7 @@ export function dispatchWsEvent(
);
if (!conv) break;
const sid = event.data.synthesis_id;
const existing = findLastMessage(
conv.messages,
(m) =>
m.message_type === "milestone" &&
m.status === "streaming" &&
(sid ? m.synthesis_id === sid : true),
);
const existing = findStreamingSynthesisMilestone(conv, sid);
if (existing) {
state.updateMessage(conversationId, existing.id, {
content: (existing.content || "") + (event.data.chunk || ""),
@ -920,13 +930,7 @@ export function dispatchWsEvent(
event.data.status === 'error' || event.data.status === 'cancelled'
? 'error'
: 'completed';
const existing = findLastMessage(
conv.messages,
(m) =>
m.message_type === "milestone" &&
m.status === "streaming" &&
(sid ? m.synthesis_id === sid : true),
);
const existing = findStreamingSynthesisMilestone(conv, sid);
if (existing) {
state.updateMessage(conversationId, existing.id, {
content: event.data.content || "",

View File

@ -68,30 +68,32 @@ const REQUIRED_TOKENS = {
} as const
describe('bitable-tokens.css — DR-3 design token contract', () => {
// MAINT-4: 提取 token 声明正则 helper — 避免转义逻辑重复 3 次。
// 匹配 `--token-name:`(声明),而非子串存在。
const tokenDeclPattern = (token: string) =>
new RegExp(`${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:`)
it('defines a :root block', () => {
expect(rootBlock.length).toBeGreaterThan(0)
})
it.each(REQUIRED_TOKENS.color)('defines color token %s in :root', (token) => {
// Match `--token-name:` (declaration), not just substring presence.
const declPattern = new RegExp(`${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:`)
expect(declPattern.test(rootBlock)).toBe(true)
expect(tokenDeclPattern(token).test(rootBlock)).toBe(true)
})
it.each(REQUIRED_TOKENS.cf)('defines conditional-format token %s in :root', (token) => {
const declPattern = new RegExp(`${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:`)
expect(declPattern.test(rootBlock)).toBe(true)
expect(tokenDeclPattern(token).test(rootBlock)).toBe(true)
})
it.each(REQUIRED_TOKENS.drawer)('defines drawer token %s in :root', (token) => {
const declPattern = new RegExp(`${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:`)
expect(declPattern.test(rootBlock)).toBe(true)
expect(tokenDeclPattern(token).test(rootBlock)).toBe(true)
})
it('does not hardcode tokens outside :root (no component-level overrides)', () => {
// ponytail: bitable tokens are exclusively :root-scoped; component-level
// overrides would break the single-source-of-truth invariant.
const outsideRoot = cssContent.replace(/:root\s*\{[^}]*\}/, '')
// STAND-2: 加 g 标志移除所有 :root 块(原来仅移除首个,后续 :root 块会漏检)。
const outsideRoot = cssContent.replace(/:root\s*\{[^}]*\}/g, '')
expect(outsideRoot).not.toMatch(/--bitable-[a-z-]+\s*:/)
})
})

View File

@ -16,7 +16,10 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from agentkit.server.auth.models import init_auth_db
from agentkit.server.auth.scim.router import router as scim_router
from agentkit.server.auth.scim.router import (
register_scim_exception_handlers,
router as scim_router,
)
SCIM_TOKEN = "test-scim-token-secret"
@ -33,7 +36,9 @@ async def scim_app(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> FastAPI:
app = FastAPI()
app.state.auth_db_path = str(db_path)
app.state.scim_token = SCIM_TOKEN
app.include_router(scim_router, prefix="/api/v1")
# STAND-3: 按 RFC 7644 挂载在 /scim/v2非 /api/v1
app.include_router(scim_router, prefix="/scim/v2")
register_scim_exception_handlers(app)
return app
@ -54,7 +59,7 @@ def _auth_headers(token: str = SCIM_TOKEN) -> dict[str, str]:
class TestCreateUser:
def test_creates_user(self, client: TestClient) -> None:
resp = client.post(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
json={
"userName": "scim.user1",
"active": True,
@ -71,24 +76,24 @@ class TestCreateUser:
def test_duplicate_username_conflict(self, client: TestClient) -> None:
client.post(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
json={"userName": "dup.user", "emails": [{"value": "dup@example.com"}]},
headers=_auth_headers(),
)
resp = client.post(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
json={"userName": "dup.user", "emails": [{"value": "other@example.com"}]},
headers=_auth_headers(),
)
assert resp.status_code == 409
def test_no_token_unauthorized(self, client: TestClient) -> None:
resp = client.post("/api/v1/scim/v2/Users", json={"userName": "x"})
resp = client.post("/scim/v2/Users", json={"userName": "x"})
assert resp.status_code == 401
def test_wrong_token_unauthorized(self, client: TestClient) -> None:
resp = client.post(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
json={"userName": "x"},
headers={"Authorization": "Bearer wrong-token"},
)
@ -99,7 +104,7 @@ class TestPatchUser:
def test_disable_user(self, client: TestClient) -> None:
# 先创建
create = client.post(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
json={"userName": "patch.user", "active": True, "emails": [{"value": "p@example.com"}]},
headers=_auth_headers(),
)
@ -107,7 +112,7 @@ class TestPatchUser:
# PATCH 禁用
resp = client.patch(
f"/api/v1/scim/v2/Users/{user_id}",
f"/scim/v2/Users/{user_id}",
json={"Operations": [{"op": "replace", "path": "active", "value": False}]},
headers=_auth_headers(),
)
@ -116,7 +121,7 @@ class TestPatchUser:
def test_patch_unknown_user_404(self, client: TestClient) -> None:
resp = client.patch(
"/api/v1/scim/v2/Users/nonexistent-id",
"/scim/v2/Users/nonexistent-id",
json={"Operations": [{"op": "replace", "path": "active", "value": False}]},
headers=_auth_headers(),
)
@ -126,16 +131,16 @@ class TestPatchUser:
class TestListUsers:
def test_list_all(self, client: TestClient) -> None:
client.post(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
json={"userName": "list.user1", "emails": [{"value": "l1@example.com"}]},
headers=_auth_headers(),
)
client.post(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
json={"userName": "list.user2", "emails": [{"value": "l2@example.com"}]},
headers=_auth_headers(),
)
resp = client.get("/api/v1/scim/v2/Users", headers=_auth_headers())
resp = client.get("/scim/v2/Users", headers=_auth_headers())
assert resp.status_code == 200
data = resp.json()
assert data["totalResults"] >= 2
@ -143,12 +148,12 @@ class TestListUsers:
def test_filter_by_username(self, client: TestClient) -> None:
client.post(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
json={"userName": "filter.user", "emails": [{"value": "f@example.com"}]},
headers=_auth_headers(),
)
resp = client.get(
"/api/v1/scim/v2/Users",
"/scim/v2/Users",
params={"filter": 'userName eq "filter.user"'},
headers=_auth_headers(),
)
@ -158,5 +163,5 @@ class TestListUsers:
assert data["Resources"][0]["userName"] == "filter.user"
def test_no_token_unauthorized(self, client: TestClient) -> None:
resp = client.get("/api/v1/scim/v2/Users")
resp = client.get("/scim/v2/Users")
assert resp.status_code == 401