Skip to content

Commit 0590baf

Browse files
authored
Merge pull request #23 from evidentloop/plan/p15-plan-materialization-auth
P1.5-C: plan materialization authorization boundary
2 parents c18f25b + e07ba26 commit 0590baf

12 files changed

Lines changed: 360 additions & 42 deletions

File tree

.sopify-skills/blueprint/tasks.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,44 @@ protocol / validator / runtime 三联动。不只是 runtime 内部统一主体
5757

5858
ADR-017 的直系后续。不要求落地完整 ExecutionAuthorizationReceipt 实现,但要把授权链路操作化为可实现 contract。
5959

60-
- **DECISION_REJECT surface 收口**(P1 语义债):P1 的 validator reject 当前通过 consult route 阻断执行,但对宿主暴露的 surface 仍表现为 consult。P1.5 需扩展 handoff 白名单(`runtime/handoff.py`)和 gate 输出,使 reject 有独立的结构化 surface,而非借 consult 路由机制
61-
- 将 ADR-017 中 ExecutionAuthorizationReceipt 字段规范(plan_id / plan_revision_digest / gate_status / action_proposal_id / authorization_source / fingerprint)从"后续扩展方向"提升为独立里程碑
62-
- 规划 execute_existing_plan 的 authorization context:谁提交、基于哪个 plan revision、经过什么 gate、产生什么 receipt
63-
- 定义 plan revision binding 的失效规则:plan 变更后 receipt 自动失效的判定机制
64-
- 定义 action identity 在 ActionProposal 管线中的唯一性保证
65-
- **Plan materialization authorization boundary**:plan 创建是 side-effecting action,必须走 ActionProposal → Validator 管线。当前 `plan_only → immediate` 硬默认绕过了 Validator 授权,违反核心不变量。P1.5 定义授权缺口、策略选项(`deferred` / `authorized_only` 物化策略)与验收边界。本里程碑不规定具体 runtime 实现位点;凡是使 P2 可落地所必需的最小实现,应在进入 P2 前完成。(此项是 P2 的硬前置——P2 只消费已完成的授权边界,不再背授权缺口)
60+
**执行拆分(4 个方案包,串行依赖):**
61+
62+
```
63+
C: Plan Materialization A: Reject Surface
64+
Auth Boundary 收口
65+
(修现存 bug, (P1 语义债,
66+
P2 硬前置) 独立)
67+
│ │
68+
│ C 建立授权模式 │
69+
▼ │
70+
B: Authorization ◄──────────────┘
71+
Contract Spec A 的 reject surface
72+
(P1.5 核心交付) 是 spec 消费场景之一
73+
74+
│ B 稳定后
75+
76+
D: Verifier Normative
77+
(P1.5→P2 桥接)
78+
```
79+
80+
| 序号 | 方案包 | 蓝图条目 | 前置 | 性质 | 范围 |
81+
|------|--------|---------|------|------|------|
82+
| C | Plan Materialization Auth Boundary | #6 || 修现存 bug + P2 硬前置 | `immediate``authorized_only`;Validator 授权结果传到 planning 流程;router `_ACTION_KEYWORDS` 单字止血 |
83+
| A | DECISION_REJECT Surface 收口 | #1 | 无(独立) | P1 语义债清理 | handoff 白名单扩展 + gate 输出 reject 独立 surface |
84+
| B | Authorization Contract Spec | #2 #3 #4 #5 #7 | C 先做更稳 | P1.5 核心交付 | Receipt 字段规范提升 + auth context + revision binding + action identity + 命名对齐。产出以 spec 为主,最小实现 |
85+
| D | Verifier Minimum Normative Slice | 桥接 | B 稳定 | P1.5→P2 桥接 | protocol.md §6 升格 normative + Verifier 输出消费路径 |
86+
87+
**蓝图条目索引:**
88+
89+
1. **DECISION_REJECT surface 收口**(P1 语义债)→ 方案包 A:P1 的 validator reject 当前通过 consult route 阻断执行,但对宿主暴露的 surface 仍表现为 consult。P1.5 需扩展 handoff 白名单(`runtime/handoff.py`)和 gate 输出,使 reject 有独立的结构化 surface,而非借 consult 路由机制
90+
2. 将 ADR-017 中 ExecutionAuthorizationReceipt 字段规范(plan_id / plan_revision_digest / gate_status / action_proposal_id / authorization_source / fingerprint)从"后续扩展方向"提升为独立里程碑 → 方案包 B
91+
3. 规划 execute_existing_plan 的 authorization context:谁提交、基于哪个 plan revision、经过什么 gate、产生什么 receipt → 方案包 B
92+
4. 定义 plan revision binding 的失效规则:plan 变更后 receipt 自动失效的判定机制 → 方案包 B
93+
5. 定义 action identity 在 ActionProposal 管线中的唯一性保证 → 方案包 B
94+
6. **Plan materialization authorization boundary** → 方案包 C(优先执行):plan 创建是 side-effecting action,必须走 ActionProposal → Validator 管线。当前 `plan_only → immediate` 硬默认绕过了 Validator 授权,违反核心不变量。P1.5 定义授权缺口、策略选项(`deferred` / `authorized_only` 物化策略)与验收边界。本里程碑不规定具体 runtime 实现位点;凡是使 P2 可落地所必需的最小实现,应在进入 P2 前完成。(此项是 P2 的硬前置——P2 只消费已完成的授权边界,不再背授权缺口)
95+
7. **字段命名对齐** → 方案包 B 附属:protocol.md 使用 `revision_digest`(通用 subject identity),ADR-017 使用 `plan_revision_digest`(plan 特化)。P1.5 需明确两者关系:`plan_revision_digest``revision_digest` 在 plan subject 场景的特化命名,实现时不得混用
96+
6697
- 产出:可实现的 authorization contract spec(不一定是完整实现,但足够让后续 P2 的动作层基于此收敛)
67-
- **字段命名对齐**:protocol.md 使用 `revision_digest`(通用 subject identity),ADR-017 使用 `plan_revision_digest`(plan 特化)。P1.5 需明确两者关系:`plan_revision_digest``revision_digest` 在 plan subject 场景的特化命名,实现时不得混用
6898

6999
**可先行切片(presentation-only / protocol 下界验证,不改 machine contract):**
70100

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# 变更提案: P1.5 Plan Materialization Authorization Boundary
2+
3+
## 需求背景
4+
5+
蓝图 tasks.md:65 明确指出:
6+
7+
> plan 创建是 side-effecting action,必须走 ActionProposal → Validator 管线。
8+
> 当前 `plan_only → immediate` 硬默认绕过了 Validator 授权,违反核心不变量。
9+
10+
当前实现存在双重硬默认:
11+
- `router.py:758``_plan_package_policy_for_route` 对 plan_only/workflow/light_iterate 返回 `"immediate"`
12+
- `engine.py:1913-1916``_normalized_plan_package_policy` 兜底再次硬编码 `"immediate"`
13+
14+
同时,router 的 `_is_consultation` 使用 `_ACTION_KEYWORDS` 做 substring match,
15+
单字如"修"会误命中分析类请求(如"批判看下哪些必须修"),
16+
导致咨询请求被误判为 change intent,进入 planning route 后直接生成方案包。
17+
18+
两层叠加的结果:分析/咨询类请求直接落盘生成 plan 包。
19+
20+
## 蓝图依据
21+
22+
- `blueprint/tasks.md:65` — Plan materialization authorization boundary(P1.5 主线未完成项)
23+
- `blueprint/protocol.md:155` — ActionProposal 是 Producer 交给 Sopify 的结构化提案,不由生产器自己决定执行
24+
- `blueprint/protocol.md:164` — Validator 才有授权权
25+
- `action_intent.py:33``write_plan_package` side_effect 已定义
26+
- `action_intent.py:318-325` — Validator 对 side-effecting + evidence 不足的 fail-close 降级已实现
27+
28+
基础设施已就位,只需接线。
29+
30+
## 触发事件
31+
32+
P1.5 先行切片执行过程中,gate 在咨询类请求上误生成了方案包(`20260505_p1-p2-p3/``20260505_consult-plan-actionproposal-validator-determinis/`)。
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# 技术设计: P1.5 Plan Materialization Authorization Boundary
2+
3+
## 核心策略
4+
5+
plan 包物化不再由 route 隐式触发,改为受 ActionProposal → Validator 授权约束。
6+
7+
## 设计决策
8+
9+
**D1: plan_package_policy 默认值从 immediate 改为 authorized_only**
10+
11+
当前 `_plan_package_policy_for_route` 对 plan_only/workflow/light_iterate 返回 `"immediate"`
12+
改为返回 `"authorized_only"`。含义:只有经过 Validator 授权(host 提交的 ActionProposal 包含 `propose_plan` + `write_plan_package` + 足够 evidence)才允许物化。
13+
14+
不恢复 `"confirm"` 策略。`confirm` 在 Wave 3a 已被退化为 `immediate`(engine.py:1909),
15+
复用它会引入语义歧义。`authorized_only` 是新语义,明确表达"需授权"。
16+
17+
**D1-a: `runtime/_models/core.py` 模型闭环**
18+
19+
`PLAN_PACKAGE_POLICIES = ("none", "confirm", "immediate")` 是 RouteDecision 归一化的允许值白名单。
20+
`__post_init__``_normalize_keyword(derived_policy, allowed=PLAN_PACKAGE_POLICIES, default="none")` 会把不在白名单中的值降级为 `"none"`
21+
因此 `authorized_only` 必须先加入白名单,否则从 router 到 engine 的整条链路都无法传递新策略。
22+
23+
改动:`PLAN_PACKAGE_POLICIES` 改为 `("none", "immediate", "authorized_only")``confirm` 直接删除——当前线上无用户,无需兼容旧输入。任何传入 `confirm` 的外部调用会被归一化为 `"none"`(fail-closed)。
24+
25+
**D2: engine `_normalized_plan_package_policy` 去掉兜底 immediate**
26+
27+
当前兜底逻辑(engine.py:1913-1916)会把缺省/空 policy 再硬编码回 `"immediate"`
28+
改为:缺省/空 → `"none"`。不再有任何路径隐式物化 plan。
29+
30+
**D3: `authorized_only` 的语义定义**
31+
32+
`plan_package_policy == "authorized_only"` 时,engine 的 `_advance_planning_route` 行为:
33+
- 如果当前 request 经过了 ActionProposal → Validator,且 Validator 返回 `DECISION_AUTHORIZE`,且 proposal 包含 `side_effect="write_plan_package"` → 允许物化
34+
- 否则 → 降级到 consult surface。不触发 `create_plan_scaffold` 等写盘操作,不创建 plan 目录。原始 guard artifacts(如 `direct_edit_guard_kind`)保留到降级后的 RouteDecision,确保 gate contract 仍能暴露 guard 信息
35+
36+
降级到 consult 是当前 P1.5-C 的最小安全实现:handoff 正确返回 `continue_host_consult`,host 不会收到不存在 plan 的 `review_or_execute_plan` 指令。
37+
38+
**D4: 传递 Validator 授权结果到 planning 流程**
39+
40+
当前 Validator 结果只影响 `proposal_override_route`(engine.py:634-672),不传递到 `_advance_planning_route`
41+
需要把 Validator 的 authorization 状态(authorized / not_authorized / no_proposal)
42+
作为参数传入 `_advance_planning_route`,让它判断是否允许物化。
43+
44+
不新增 action type。已有 `propose_plan` + `write_plan_package` 足够。
45+
46+
**D5: router L1 止血 — 去掉单字 ACTION_KEYWORDS**
47+
48+
`_ACTION_KEYWORDS` 中的单字"修"和"补"做 substring match 误伤率过高。
49+
"修" 会命中"必须修"、"怎么修复"、"先别修改"等分析语句。
50+
"补" 会命中"补充"、"补一句"等非变更表达。
51+
52+
最小改法:直接删除单字"修"和"补",不新增替代 pattern。"修复"已在列表中覆盖合法用法。
53+
不新增 pattern,不堆语义特判。
54+
55+
**D6: `~go plan` 显式命令保持 immediate — 本轮显式兼容例外**
56+
57+
`~go plan` 是用户显式发出的 plan 命令(router.py:435-444),属于本轮保留的 host-side exception / compatibility path。
58+
保留原因:用户通过显式命令表达物化意图,产品层面无歧义。
59+
边界限定:此例外仅适用于 `~go plan` 命令路径,不上升为"显式命令天然免授权"的一般性原则。
60+
后续如需扩展其他命令的 exemption,应单独评审。
61+
62+
实现方式:`_classify_command` 返回的 `plan_only` RouteDecision 仍携带 `plan_package_policy="immediate"`
63+
只有 `_estimate_complexity` / 非命令路径产出的 planning route 才走 `authorized_only`
64+
65+
**D7: `confirm` 直接删除**
66+
67+
`confirm``PLAN_PACKAGE_POLICIES` 中的历史值,在 Wave 3a(engine.py:1909)被退化为 `immediate`
68+
当前线上无用户,无需做兼容迁移。
69+
70+
处理方式:
71+
1. `PLAN_PACKAGE_POLICIES` 删除 `"confirm"`
72+
2. `_normalized_plan_package_policy``confirm → immediate` 退化逻辑直接删除
73+
3. 3 处测试中 `plan_package_policy="confirm"` 改为 `"authorized_only"``"immediate"`(按测试意图选择)
74+
4. 任何外部传入 `"confirm"` 会被 `_normalize_keyword` 归一化为 `"none"`(fail-closed,符合蓝图方向)
75+
76+
---
77+
78+
## In-scope
79+
80+
- `runtime/_models/core.py``PLAN_PACKAGE_POLICIES` 替换:删 `confirm`,增 `authorized_only`
81+
- `_plan_package_policy_for_route` 改默认值
82+
- `_normalized_plan_package_policy` 去掉兜底 immediate + 删除 `confirm` 退化逻辑
83+
- engine 增加 authorization check 控制物化
84+
- router `_ACTION_KEYWORDS` 去掉单字误伤
85+
- 测试:授权边界 + consult 误判回归 + confirm 存量测试更新
86+
87+
## Out-of-scope
88+
89+
- 不改 protocol 主 schema
90+
- 不新增 action type
91+
- 不重做 Validator 架构
92+
- 不处理 DECISION_REJECT surface
93+
- 不扩到 P2 动作层
94+
95+
---
96+
97+
## 影响范围
98+
99+
| 切片 | 文件 | 操作 | 说明 |
100+
|------|------|------|------|
101+
| L2 | runtime/_models/core.py | 编辑 | `PLAN_PACKAGE_POLICIES` 替换:删 `confirm`,增 `authorized_only` |
102+
| L2 | runtime/router.py | 编辑 | `_plan_package_policy_for_route` 默认改为 `authorized_only` |
103+
| L2 | runtime/engine.py | 编辑 | `_normalized_plan_package_policy` 去兜底; `confirm` 直接删除; `_advance_planning_route` 增加 authorization check,unauthorized 降级到 consult |
104+
| L1 | runtime/router.py | 编辑 | `_ACTION_KEYWORDS` 去单字 |
105+
| T | tests/test_action_intent.py | 编辑 | 更新 feature request regression tests 匹配 consult 降级行为 |
106+
| T | tests/test_runtime_engine.py | 编辑 | `plan_package_policy="confirm"` 存量改为 `"authorized_only"`; 增加 `_propose_plan_action()` helper |
107+
| T | tests/test_runtime_router.py | 编辑 | `"immediate"` 断言改为 `"authorized_only"` |
108+
| T | tests/test_runtime_gate.py | 编辑 | archive failure test 增加 `action_proposal_json` |
109+
| T | scripts/check-prompt-runtime-gate-smoke.py | 编辑 | `protected_plan_asset` scenario 匹配 consult 降级行为 |
110+
111+
---
112+
113+
## 验收标准
114+
115+
1. `_plan_package_policy_for_route("plan_only", ...)` 对非命令路径返回 `"authorized_only"`
116+
2. `_normalized_plan_package_policy` 不再有任何路径隐式返回 `"immediate"`(除 `~go plan` 显式命令)
117+
3. 当 host 未提交 ActionProposal 时,planning route 降级到 consult,不创建 plan 目录
118+
4. 当 host 提交 `propose_plan` + `write_plan_package` + high confidence + evidence 时,planning route 正常创建 plan
119+
5. "批判看下哪些必须修,等我确认" 不再被 `_is_consultation` 判为 change intent
120+
6. `~go plan <request>` 仍正常创建 plan(显式命令不受影响)
121+
7. 全量 pytest 通过
122+
123+
## Known Debt
124+
125+
**resume path `plan_materialization_authorized=True` 写死(3 处)**
126+
127+
当前 3 处 checkpoint 恢复路径(engine.py clarification resume / decision resume / run resume)硬编码 `plan_materialization_authorized=True`
128+
这些路径只从已授权或显式 plan flow(`~go plan` / `immediate`)产生的 checkpoint 恢复时才到达,当前上游生成 checkpoint 的方式保证了安全性。
129+
但 contract 本身没有绑定 authorization provenance——它依赖上游恰好安全,而不是自身校验。
130+
更细的 authorization provenance binding(在 checkpoint state 中记录授权来源,resume 时验证)留给 P1.5-B / P2 收敛。
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# 任务清单: P1.5 Plan Materialization Authorization Boundary
2+
3+
目录: `.sopify-skills/plan/20260505_p15_plan_materialization_auth/`
4+
5+
## 1. L2 — Plan Materialization Authorization Boundary
6+
7+
- [x] T1-A: `_models/core.py``PLAN_PACKAGE_POLICIES` 替换为 `("none", "immediate", "authorized_only")`
8+
- 删除 `"confirm"`(当前线上无用户,不做兼容)
9+
- 新增 `"authorized_only"`
10+
- 验收: `RouteDecision(plan_package_policy="authorized_only")` 归一化后仍为 `"authorized_only"``confirm` 输入归一化为 `"none"`
11+
- [x] T1-B: `router.py``_plan_package_policy_for_route` 默认值改为 `authorized_only`
12+
- plan_only/workflow/light_iterate 从 `"immediate"` 改为 `"authorized_only"`
13+
- `~go plan` 命令路径不受影响(`_classify_command` 仍返回 `"immediate"`,D6 本轮显式兼容例外)
14+
- 验收: 非命令路径返回 `"authorized_only"`
15+
- [x] T1-C: `engine.py``_normalized_plan_package_policy` 清理
16+
- 删除 plan_only/workflow/light_iterate 兜底 `"immediate"` 逻辑(line 1913-1916)
17+
- 删除 `confirm``immediate` 退化逻辑(D7: confirm 已从白名单移除,无需迁移)
18+
- 缺省/空 policy 返回 `"none"`
19+
- 验收: 无任何隐式 immediate 路径(除 `~go plan` 显式命令)
20+
- [x] T1-D: `engine.py``_advance_planning_route` 增加 authorization check
21+
- 新增 `plan_materialization_authorized` 参数传递 Validator 授权状态
22+
-`plan_package_policy == "authorized_only"` 且无授权时,降级到 consult surface,不触发 `create_plan_scaffold` 等写盘操作
23+
- 降级后保留原始 guard artifacts(`entry_guard_reason_code``direct_edit_guard_kind` 等)
24+
- 验收: 无授权时不创建 plan 目录,handoff 正确返回 `continue_host_consult`
25+
- [x] T1-E: `engine.py` — Validator 授权结果传递到 planning 流程
26+
- `run_runtime` pre-route interceptor 从 Validator 结果提取 `plan_materialization_authorized`
27+
- 通过 `_advance_planning_route` 参数传递(不走 RouteDecision artifacts)
28+
- 验收: `_advance_planning_route` 能区分"经过授权"和"未经过授权"
29+
30+
## 2. L1 — Router consult 误判止血
31+
32+
- [x] T2-A: `router.py` — 收紧 `_ACTION_KEYWORDS`
33+
- 移除单字"修"和"补"
34+
- "修" 替换为 "修复" / "修改"("修复"已存在,只需删"修")
35+
- "补" 替换为更精确的短语(或直接删除,依赖"修复"/"添加"/"新增"覆盖)
36+
- 验收: "批判看下哪些必须修,等我确认" 不再命中 action keyword
37+
38+
## 3. 测试
39+
40+
- [x] T3-A: plan materialization authorization 测试覆盖
41+
- 无 ActionProposal 时 planning route 降级到 consult,不创建 plan 目录
42+
-`propose_plan` + `write_plan_package` + authorized 时正常创建
43+
- `~go plan` 显式命令不受影响
44+
- feature request regression tests 更新匹配 consult 降级行为
45+
- 验收: 全量测试通过
46+
- [x] T3-B: `confirm` 存量测试更新
47+
- 3 处 `plan_package_policy="confirm"` 测试改为 `"authorized_only"`
48+
- `confirm` 已从白名单删除,无需兼容测试
49+
- 验收: 无 `plan_package_policy="confirm"` 残留
50+
- [x] T3-C: 增加 consult 误判回归测试
51+
- "批判看下哪些必须修,等我确认" → router.classify 返回 consult
52+
- "修复这个 bug" → router.classify 不返回 consult(不回归)
53+
- 验收: 新测试全部通过
54+
- [x] T3-D: 全量 pytest 验证
55+
- 验收: 597 tests passed, 46 subtests passed

runtime/_models/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
DECISION_FIELD_TYPES = ("select", "multi_select", "confirm", "input", "textarea")
1111
DECISION_SUBMISSION_STATUSES = ("empty", "draft", "collecting", "submitted", "confirmed", "cancelled", "timed_out")
1212
DECISION_STATE_STATUSES = ("pending", "collecting", "confirmed", "consumed", "cancelled", "timed_out", "stale")
13-
PLAN_PACKAGE_POLICIES = ("none", "confirm", "immediate")
13+
PLAN_PACKAGE_POLICIES = ("none", "immediate", "authorized_only")
1414

1515

1616
@dataclass(frozen=True)

0 commit comments

Comments
 (0)