feat(v2): T05-c 运行补充说明只能形成草稿(AC-26)

运行时的"本次预审补充说明"只影响本次运行(run_instruction 落在运行对象上,
属 T07 的 ScreeningRun 字段)。要变成持久知识只有一条路:显式提议为草稿,
且走正常审核。本批实现卡片侧的那一半。

新增 knowledge_cards.draft_from_run_instruction(),三条约束:
1. 只产草稿——运行中说过的话绝不直接生效(有测试断言无已发布版本产生)
2. **不得携带工具/权限/引擎字段**:allowed_tools / tools / permissions / engine /
   run_instruction 出现即拒,扩大工具授权必须走独立提议与审核
3. 记录 derived_from_run,便于回溯知识来自哪次运行
另:运行指令不能声明为 history_derived(历史提炼须提供来源事件或证据),
hard 门禁对运行衍生的卡片同样生效。

未完成:运行对象上的 run_instruction 字段与"只影响本次运行"的服务端实现属 T07,
本批只交付卡片侧,不宣称 AC-26 已整体闭环。

测试:test_knowledge_cards.py 增至 25 项(新增 5 项)。
全量 315 项 OK(310 → 315);verify.py 10/10 PASS。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-29 18:51:12 +08:00
co-authored by Claude Code
parent f8c4ba9e1d
commit d18c147076
2 changed files with 79 additions and 0 deletions
+37
View File
@@ -196,3 +196,40 @@ def effective(db, domain: str, project_id: str | None = None, at: str | None = N
continue
result.append(card)
return result
# ---------------------------------------------------------------- AC-26
# 运行时的"本次预审补充说明"只影响**本次运行**(run_instruction 落在运行对象上,
# 见 03 §9.3)。要把它变成持久知识,唯一途径是**显式提议为草稿**,且草稿走正常
# 审核流程;指令本身不得改变工具权限。
RUN_SCOPE_FIELDS = ("run_instruction", "tools", "allowed_tools", "permissions", "engine")
def draft_from_run_instruction(db, project_id: str, run_id: str, body: dict, actor: str):
"""把一次运行的补充说明转成**待审草稿**(AC-26)。
三条约束:
1. 只产草稿,绝不因"运行中说过"而直接生效
2. 指令不得携带工具/权限/引擎字段——扩大工具授权必须走独立提议与审核
3. 保留来源运行,便于回溯"这条知识从哪次运行来"
"""
forbidden = sorted(field for field in RUN_SCOPE_FIELDS if field in body)
if forbidden:
raise ValueError(
"本次预审补充说明不能持久化工具权限或执行引擎;"
f"请移除这些字段并通过独立提议与审核:{forbidden}"
)
if not body.get("name") or not body.get("kind"):
raise ValueError("转为持久知识需要明确的名称与类型")
origin = body.get("origin", "manual")
if origin not in ORIGINS:
raise ValueError(f"来源取值须为 {' 或 '.join(ORIGINS)}")
# 运行指令天然没有历史来源,历史来源必须另行提供证据
if origin == "history_derived":
raise ValueError("运行补充说明不能声明为历史提炼;历史提炼需提供来源事件或证据")
enriched = dict(body, derived_from_run=run_id, proposed_via="run_instruction")
return propose(db, enriched, actor, project_id, origin=origin,
card_scope=body.get("card_scope", "project"),
team_id=body.get("team_id"))