@furongjun1999/dsh-memory 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -15
- package/docs/eval//345/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +108 -0
- package/docs/eval//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_P1_/346/212/275/345/217/226/345/231/250v4_/344/270/211/347/216/207/350/257/273/346/225/260_v1.0.md +231 -0
- package/docs/mdcg//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +305 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +45 -43
- package/lib/hooks.js +40 -18
- package/lib/lib/session_state.d.ts +56 -0
- package/lib/lib/session_state.js +124 -0
- package/lib/tools.js +12 -1
- package/md_cg/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +64 -11
- package/md_cg/mdcos.py +78 -9
- package/md_cg/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- package/md_cg/test_lifecycle_retire_leak.py +408 -0
- package/md_cg/test_read_face_semantics.py +6 -3
- package/md_cg/test_state_event_op.py +514 -0
- package/md_cg/test_state_slots.py +691 -0
- package/md_cg/tokens.py +8 -1
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +40 -17
- package/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_state_event_op · cg(op=state_event) 记账写口守卫(P3 批:写侧入口)。
|
|
3
|
+
|
|
4
|
+
契约:`docs/plans/语义时空图补全_世界模型功能端_设计_v0.1.md` §2「事件是源、槽位是
|
|
5
|
+
投影、查询走 stg」+ §5②(记账入口形态)。本批把「记账」这一步放到 MCP 面:
|
|
6
|
+
`md_cg/mcp_server.py` 的 cg 基元新增 op=state_event(写口),底层仍是
|
|
7
|
+
`md_cg/state_events.append`(append-only 台账)。
|
|
8
|
+
|
|
9
|
+
断言分组:
|
|
10
|
+
|
|
11
|
+
A 全链——cg(op=state_event) 写入 → `_state_events.jsonl` 得一行(五元
|
|
12
|
+
subject/slot/old/new/kind + seq/evidence/actor 逐字段)→ 投影
|
|
13
|
+
(state_slots.project)与查询面(stg.state_chain,开关开)可见该事件;
|
|
14
|
+
台账追加一条变迁后变迁史条数随之增长;经最外层 `call_tool` 的通路同形。
|
|
15
|
+
B 双闸——① op 映射:state_event 映射到既有 "write" 词(受限令牌的拒绝消息
|
|
16
|
+
点名 op=write 而非 op=state_event;**含 write 不含 state_event** 的白名单
|
|
17
|
+
令牌可记账);② 写闸 require_write:can_write=False / theory_ok=False /
|
|
18
|
+
clearance 不足三态各自被拒(台账直写盘面、绕过库层节点写闸,故须显式补齐)。
|
|
19
|
+
C 防伪造——请求里塞 actor 参数无效:落盘 actor 恒=令牌 actor(无 principal
|
|
20
|
+
时="system")。
|
|
21
|
+
D kind 归一——缺省/空串/纯空白 → 落盘 kind=None(「未分类」是合法记账,而
|
|
22
|
+
append 对空串抛 ValueError);显式合法值(含带空白)strip 后透传;非法值
|
|
23
|
+
ValueError 原样上抛。
|
|
24
|
+
E 参数校验——缺/空 subject、缺/空 slot、old/new 双空 → ValueError 原样上抛
|
|
25
|
+
(fail-closed,不吞、不包装);合法撤回(old 有值、new 空)照常通过。
|
|
26
|
+
F 回归——既有 op(info/read/recent 抽查)行为与权限行改动前一致:designer
|
|
27
|
+
令牌全过、受限令牌该拒的仍拒且消息点名原 op 词;`_ACTION_SIGS` /
|
|
28
|
+
`_ACTION_DEFAULT` 未被本批改动(state_event 无 action 面)。
|
|
29
|
+
|
|
30
|
+
隔离:全部落临时目录(`tempfile.mkdtemp`);env 里的根变量与投影开关全程置空或
|
|
31
|
+
指向临时根,令牌签发写临时令牌库,**绝不动在役库/在役令牌库**(本文件不含任何
|
|
32
|
+
真实数据根路径字面量)。
|
|
33
|
+
|
|
34
|
+
运行:python -X utf8 -m md_cg.test_state_event_op
|
|
35
|
+
python -X utf8 -m md_cg.test_state_event_op --mutate # 定点变异自证
|
|
36
|
+
python -X utf8 -m md_cg.test_state_event_op --mutate --list # 只列变异表
|
|
37
|
+
|
|
38
|
+
退出码(fail-closed):0 = 全绿 / 变异自证 PASS;1 = 有断言失败 / 变异未按预期转红;
|
|
39
|
+
2 = ANCHOR-MISS(变异锚点在当前源码里找不到——实现改了却没同步本表即硬失败)。
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import contextlib
|
|
44
|
+
import io
|
|
45
|
+
import json
|
|
46
|
+
import os
|
|
47
|
+
import shutil
|
|
48
|
+
import sys
|
|
49
|
+
import tempfile
|
|
50
|
+
import types
|
|
51
|
+
|
|
52
|
+
from . import mcp_server
|
|
53
|
+
from . import state_events
|
|
54
|
+
from . import state_slots
|
|
55
|
+
from . import stg
|
|
56
|
+
from . import tokens
|
|
57
|
+
from .mdcos import MdCGOS, MdCGSecure
|
|
58
|
+
from .security import Principal
|
|
59
|
+
|
|
60
|
+
_PASS = []
|
|
61
|
+
_FAIL = []
|
|
62
|
+
_GEN = [0]
|
|
63
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_se_")
|
|
64
|
+
_SAVED = {}
|
|
65
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
66
|
+
_KEK = os.urandom(32)
|
|
67
|
+
#: 被测写口模块的**可变引用**(定点变异把全局换成变异副本,组内一律经 _ms() 取用)。
|
|
68
|
+
MS = mcp_server
|
|
69
|
+
_REAL_MS = mcp_server
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# 生效条件:cond 为真记入 _PASS 并打印 PASS,否则记入 _FAIL 并打印 FAIL(extra 仅在失败时打印,供定位)。
|
|
73
|
+
def ok(cond, msg, extra=""):
|
|
74
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
75
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
76
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# 生效条件:无入参;返回当前被测写口模块(未变异=mcp_server 本体,变异轮=内存副本)。
|
|
80
|
+
def _ms():
|
|
81
|
+
return globals()["MS"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# 生效条件:无入参;把 MDCG_ROOT/MDCG_STATE_ROOT/MDCG_AUX_ROOT/MDCG_DATA_ROOT/MDCG_STG_STATE 的现值存入 _SAVED 并逐个从 os.environ 移除(隔离:任何经 env 取根的路径都不得落到在役库)。
|
|
85
|
+
def _sandbox_env():
|
|
86
|
+
for k in ("MDCG_ROOT", "MDCG_STATE_ROOT", "MDCG_AUX_ROOT", "MDCG_DATA_ROOT",
|
|
87
|
+
"MDCG_STG_STATE"):
|
|
88
|
+
_SAVED[k] = os.environ.get(k)
|
|
89
|
+
os.environ.pop(k, None)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# 生效条件:无入参;把 _SAVED 逐个还原(原值 None 则移除键),恢复进程原有 env 形态。
|
|
93
|
+
def _restore_env():
|
|
94
|
+
for k, v in _SAVED.items():
|
|
95
|
+
if v is None:
|
|
96
|
+
os.environ.pop(k, None)
|
|
97
|
+
else:
|
|
98
|
+
os.environ[k] = v
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# 生效条件:无入参;返回本进程专用的临时令牌库文件路径(签发/校验全落 _TMP,不触在役令牌库)。
|
|
102
|
+
def _tokfile():
|
|
103
|
+
return os.path.join(_TMP, "_tokens.json")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# 生效条件:role 为 tokens.ROLE_SPECS 角色名;签发该角色令牌(actor 缺省 evt-<role>)后经 verify_token 取回 Principal;**签发与校验都只落临时令牌库**;未知角色/参数非法时抛原异常(不吞)。
|
|
107
|
+
def _principal(role, actor=None, **kw):
|
|
108
|
+
r = tokens.issue(role, actor=actor or ("evt-" + role), path=_tokfile(), **kw)
|
|
109
|
+
return tokens.verify_token(r["token"], path=_tokfile())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# 生效条件:无入参;在 _TMP 下新建自增代次目录并返回其上的 MdCGOS 实例(**无 principal**——测「无令牌只读面」路径;每调用一个新根,组间零串味)。
|
|
113
|
+
def _plain_cg():
|
|
114
|
+
_GEN[0] += 1
|
|
115
|
+
return MdCGOS(os.path.join(_TMP, "gen%d" % _GEN[0], "root"))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# 生效条件:principal 为 security.Principal 实例;在 _TMP 下新建自增代次目录并返回其上的 MdCGSecure 实例(principal 参与授权判定;master_key 用进程级随机 kek,密钥库落临时根内)。
|
|
119
|
+
def _secure_cg(principal):
|
|
120
|
+
_GEN[0] += 1
|
|
121
|
+
return MdCGSecure(os.path.join(_TMP, "gen%d" % _GEN[0], "root"),
|
|
122
|
+
principal=principal, master_key=_KEK)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# 生效条件:cg 为沙箱实例、a 为 cg 面参数(不含 op);按外层 _cg_call 调 op=state_event 并返回其返回体(op 显式传入,不走 op 推导)。
|
|
126
|
+
def _evt(cg, **a):
|
|
127
|
+
args = {"op": "state_event"}
|
|
128
|
+
args.update(a)
|
|
129
|
+
return _ms()._cg_call(cg, args)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# 生效条件:fn 为无参可调用对象时——正常返回 ("", "");抛异常返回 (异常类型名, 错误文本)。用于「该拒的拒、且拒的理由是哪个类型/哪句话」的断言。
|
|
133
|
+
def _err(fn):
|
|
134
|
+
try:
|
|
135
|
+
fn()
|
|
136
|
+
return "", ""
|
|
137
|
+
except Exception as exc: # noqa: BLE001
|
|
138
|
+
return type(exc).__name__, str(exc)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# 生效条件:cg 为沙箱实例;读取其台账文件并返回逐行 json.loads 的列表(台账缺失 → [];坏行照常抛——守卫只写合法行)。
|
|
142
|
+
def _ledger(cg):
|
|
143
|
+
p = state_events.ledger_path(cg)
|
|
144
|
+
if not os.path.exists(p):
|
|
145
|
+
return []
|
|
146
|
+
with open(p, encoding="utf-8") as f:
|
|
147
|
+
return [json.loads(l) for l in f if l.strip()]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------- A
|
|
151
|
+
# 生效条件:无入参;在自有沙箱根上(designer 令牌 principal)经 cg(op=state_event) 连写两条事件,断言返回体/落盘行/投影/state_chain 四面对齐、开关关臂不抛、最外层 call_tool 通路同形(落点见组内文案)。
|
|
152
|
+
def gA():
|
|
153
|
+
print("== A 全链:记账 → 台账 → 投影 → state_chain ==")
|
|
154
|
+
cg = _secure_cg(_principal("designer", actor="evt-designer"))
|
|
155
|
+
out = _evt(cg, subject="甲", slot="住所", old=None, new="别墅",
|
|
156
|
+
kind="enablement", seq=1, evidence="u1")
|
|
157
|
+
ev = (out or {}).get("event") or {}
|
|
158
|
+
ok(out.get("ok") is True and isinstance(ev, dict),
|
|
159
|
+
"A1 cg(op=state_event) 返回 {ok:true, event:rec}", out)
|
|
160
|
+
ok(ev.get("subject") == "甲" and ev.get("slot") == "住所"
|
|
161
|
+
and ev.get("old") is None and ev.get("new") == "别墅"
|
|
162
|
+
and ev.get("kind") == "enablement" and ev.get("seq") == 1
|
|
163
|
+
and ev.get("evidence") == "u1" and ev.get("actor") == "evt-designer"
|
|
164
|
+
and isinstance(ev.get("t"), (int, float)),
|
|
165
|
+
"A2 event 五元/seq/evidence/actor/t 逐字段", ev)
|
|
166
|
+
lines = _ledger(cg)
|
|
167
|
+
ok(len(lines) == 1, "A3 台账 _state_events.jsonl 恰得一行", len(lines))
|
|
168
|
+
ok(lines and lines[0] == ev,
|
|
169
|
+
"A4 落盘行与返回 event 逐字段相等(落盘记录=append 返回值)",
|
|
170
|
+
(lines, ev))
|
|
171
|
+
units = state_slots.project(cg, subject="甲", slot="住所")
|
|
172
|
+
ok(len(units) == 1 and units[0]["value"] == "别墅"
|
|
173
|
+
and units[0]["state"] == "active" and units[0]["seq_from"] == 1
|
|
174
|
+
and units[0]["t_from"] == ev.get("t"),
|
|
175
|
+
"A5 投影可见:project 得该槽位单元(value/state/seq_from/t_from 对齐)",
|
|
176
|
+
units)
|
|
177
|
+
os.environ["MDCG_STG_STATE"] = "1"
|
|
178
|
+
try:
|
|
179
|
+
chain = stg.state_chain(cg, subject="甲")
|
|
180
|
+
finally:
|
|
181
|
+
os.environ.pop("MDCG_STG_STATE", None)
|
|
182
|
+
ok(chain.get("count") == 1 and chain.get("kept") == 1
|
|
183
|
+
and (chain.get("items") or []) == state_slots.project(cg, subject="甲"),
|
|
184
|
+
"A6 stg(state_chain)(开关开)可见该事件:items 与 project 逐位一致",
|
|
185
|
+
chain)
|
|
186
|
+
off = stg.state_chain(cg)
|
|
187
|
+
ok(off.get("error") == "disabled",
|
|
188
|
+
"A7 开关关 → disabled 体(「没开」与「没命中」可分辨,不抛)", off)
|
|
189
|
+
out2 = _evt(cg, subject="甲", slot="住所", old="别墅", new="小屋",
|
|
190
|
+
kind="migration", seq=2, evidence="u2")
|
|
191
|
+
ok((out2 or {}).get("ok") is True, "A8 第二条(迁移)写入成功", out2)
|
|
192
|
+
units2 = state_slots.project(cg, subject="甲", slot="住所")
|
|
193
|
+
ok(len(units2) == 1 and units2[0]["value"] == "小屋"
|
|
194
|
+
and units2[0]["event_count"] == 2
|
|
195
|
+
and len(units2[0]["history"]) == 2,
|
|
196
|
+
"A9 变迁史:event_count=2、history 两条(台账追加即投影可见)",
|
|
197
|
+
units2)
|
|
198
|
+
via = _ms().call_tool(cg, "cg",
|
|
199
|
+
{"op": "state_event", "subject": "乙", "slot": "住所",
|
|
200
|
+
"new": "帐篷", "seq": 3, "evidence": "u3"})
|
|
201
|
+
ok((via or {}).get("ok") is True
|
|
202
|
+
and (via.get("event") or {}).get("actor") == "evt-designer",
|
|
203
|
+
"A10 经最外层 call_tool(MCP tools/call 通路)同形:ok=true、actor=令牌 actor",
|
|
204
|
+
via)
|
|
205
|
+
ok(len(_ledger(cg)) == 3, "A11 三条事件皆落盘(append-only 不截断)",
|
|
206
|
+
len(_ledger(cg)))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------- B
|
|
210
|
+
# 生效条件:无入参;在自有沙箱根上按三类令牌(受限 ops_allow 无 write / 含 write 不含 state_event / designer 星号)与三种写闸不足(can_write=False / theory_ok=False / clearance 不足)逐条断言拒绝或放行,并校验拒绝消息点名的 op 词(落点见组内文案)。
|
|
211
|
+
def gB():
|
|
212
|
+
print("== B 双闸:op 映射(write 词)+ require_write(三态) ==")
|
|
213
|
+
# ① op 映射:受限令牌(白名单无 write)→ 拒,且消息点名 op=write(不是 op=state_event)
|
|
214
|
+
rcg = _secure_cg(_principal("recorder", actor="evt-restricted",
|
|
215
|
+
ops_allow=["read"]))
|
|
216
|
+
t, txt = _err(lambda: _evt(rcg, subject="甲", slot="住所", new="值"))
|
|
217
|
+
ok(t == "AccessDenied" and "op=write" in txt and "op=state_event" not in txt,
|
|
218
|
+
"B1 受限令牌(ops_allow 无 write)→ AccessDenied 且消息点名 op=write"
|
|
219
|
+
"(映射生效;不引入 state_event 新 op 词)", (t, txt[:120]))
|
|
220
|
+
ok(_ledger(rcg) == [], "B2 被拒时不落盘(台账零行)", _ledger(rcg))
|
|
221
|
+
# ② 含 write 不含 state_event → 可记账(回退成 require_op(op) 则此处转红)
|
|
222
|
+
wcg = _secure_cg(_principal("recorder", actor="evt-writer",
|
|
223
|
+
ops_allow=["read", "write"]))
|
|
224
|
+
out = _evt(wcg, subject="甲", slot="住所", new="值", seq=1, evidence="b2")
|
|
225
|
+
ok(out.get("ok") is True and len(_ledger(wcg)) == 1,
|
|
226
|
+
"B3 白名单含 write 不含 state_event → 放行并落盘(映射到既有 write 词)",
|
|
227
|
+
out)
|
|
228
|
+
# ③ designer(ops_allow 星号)→ 放行
|
|
229
|
+
dcg = _secure_cg(_principal("designer", actor="evt-designer"))
|
|
230
|
+
out = _evt(dcg, subject="甲", slot="住所", new="值", seq=1)
|
|
231
|
+
ok(out.get("ok") is True, "B4 designer(ops_allow=*)→ 放行", out)
|
|
232
|
+
# ④ guest 令牌(can_write=False,白名单无 write)→ op 闸先拒
|
|
233
|
+
gcg = _secure_cg(_principal("guest", actor="evt-guest"))
|
|
234
|
+
t, txt = _err(lambda: _evt(gcg, subject="甲", slot="住所", new="值"))
|
|
235
|
+
ok(t == "AccessDenied" and "op=write" in txt,
|
|
236
|
+
"B5 guest 令牌 → AccessDenied(只读访客照拒)", (t, txt[:120]))
|
|
237
|
+
# ⑤ 写闸·can_write:直接构造「白名单含 write 但 can_write=False」的 principal
|
|
238
|
+
# (角色规格里无此组合,须显式构造——这条正是 require_write 补齐的判据)
|
|
239
|
+
ncg = _secure_cg(Principal(actor="evt-nocw", role="guest", clearance="internal",
|
|
240
|
+
can_write=False, ops_allow=("*",)))
|
|
241
|
+
t, txt = _err(lambda: _evt(ncg, subject="甲", slot="住所", new="值"))
|
|
242
|
+
ok(t == "AccessDenied" and "无写权限" in txt,
|
|
243
|
+
"B6 can_write=False(op 闸放行)→ 被 require_write 拒(写闸显式补齐)",
|
|
244
|
+
(t, txt[:120]))
|
|
245
|
+
ok(_ledger(ncg) == [], "B7 该拒的不落盘", _ledger(ncg))
|
|
246
|
+
# ⑥ 写闸·theory_ok=False
|
|
247
|
+
tcg = _secure_cg(Principal(actor="evt-notheory", clearance="internal",
|
|
248
|
+
can_write=True, ops_allow=("*",),
|
|
249
|
+
theory_ok=False))
|
|
250
|
+
t, txt = _err(lambda: _evt(tcg, subject="甲", slot="住所", new="值"))
|
|
251
|
+
ok(t == "AccessDenied" and "版本层校验未通过" in txt,
|
|
252
|
+
"B8 theory_ok=False → 被拒(版本层校验维度)", (t, txt[:120]))
|
|
253
|
+
# ⑦ 写闸·密级:clearance=public < internal
|
|
254
|
+
scg = _secure_cg(Principal(actor="evt-lowclear", clearance="public",
|
|
255
|
+
can_write=True, ops_allow=("*",)))
|
|
256
|
+
t, txt = _err(lambda: _evt(scg, subject="甲", slot="住所", new="值"))
|
|
257
|
+
ok(t == "AccessDenied" and "clearance" in txt,
|
|
258
|
+
"B9 clearance 不足 internal → 被拒(密级维度)", (t, txt[:120]))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------- C
|
|
262
|
+
# 生效条件:无入参;在自有沙箱根上断言请求里塞 actor 参数不生效:带令牌时落盘 actor 恒=令牌 actor、无 principal 时恒="system"(落点见组内文案)。
|
|
263
|
+
def gC():
|
|
264
|
+
print("== C 防伪造:actor 恒取令牌/系统(请求参数无效) ==")
|
|
265
|
+
cg = _secure_cg(_principal("recorder", actor="evt-real",
|
|
266
|
+
ops_allow=["read", "write"]))
|
|
267
|
+
out = _evt(cg, subject="甲", slot="住所", new="值", seq=1,
|
|
268
|
+
evidence="c1", actor="forger")
|
|
269
|
+
ok((out.get("event") or {}).get("actor") == "evt-real",
|
|
270
|
+
"C1 请求 actor=forger 无效:落盘 actor=令牌 actor", out)
|
|
271
|
+
lines = _ledger(cg)
|
|
272
|
+
ok(lines and lines[0].get("actor") == "evt-real",
|
|
273
|
+
"C2 台账行 actor 逐字=令牌 actor(不落伪造值)", lines)
|
|
274
|
+
pcg = _plain_cg()
|
|
275
|
+
out = _evt(pcg, subject="甲", slot="住所", new="值", seq=1,
|
|
276
|
+
evidence="c3", actor="forger")
|
|
277
|
+
ok((out.get("event") or {}).get("actor") == "system",
|
|
278
|
+
"C3 无 principal:actor 恒=system(也不取请求参数)", out)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------- D
|
|
282
|
+
# 生效条件:无入参;在自有沙箱根上按 kind 的五态(缺省/空串/纯空白/显式合法含空白/非法)逐条断言落盘归一结果或 ValueError 上抛(落点见组内文案)。
|
|
283
|
+
def gD():
|
|
284
|
+
print("== D kind 归一(空串 → None;非法值 fail-closed) ==")
|
|
285
|
+
cg = _secure_cg(_principal("designer", actor="evt-d"))
|
|
286
|
+
out = _evt(cg, subject="甲", slot="s1", new="v1", seq=1)
|
|
287
|
+
ok((out.get("event") or {}).get("kind") is None,
|
|
288
|
+
"D1 缺省 kind → 落盘 None(未分类是合法记账)", out)
|
|
289
|
+
out = _evt(cg, subject="甲", slot="s2", new="v2", seq=2, kind="")
|
|
290
|
+
ok((out.get("event") or {}).get("kind") is None,
|
|
291
|
+
"D2 kind=空串 → 归一 None(不抛 ValueError)", out)
|
|
292
|
+
out = _evt(cg, subject="甲", slot="s3", new="v3", seq=3, kind=" ")
|
|
293
|
+
ok((out.get("event") or {}).get("kind") is None,
|
|
294
|
+
"D3 kind=纯空白 → 归一 None", out)
|
|
295
|
+
out = _evt(cg, subject="甲", slot="s4", new="v4", seq=4, kind="migration")
|
|
296
|
+
ok((out.get("event") or {}).get("kind") == "migration",
|
|
297
|
+
"D4 显式合法值透传", out)
|
|
298
|
+
out = _evt(cg, subject="甲", slot="s5", new="v5", seq=5, kind=" migration ")
|
|
299
|
+
ok((out.get("event") or {}).get("kind") == "migration",
|
|
300
|
+
"D5 带空白合法值 strip 后透传", out)
|
|
301
|
+
t, txt = _err(lambda: _evt(cg, subject="甲", slot="s6", new="v6",
|
|
302
|
+
kind="bogus"))
|
|
303
|
+
ok(t == "ValueError" and "bogus" in txt,
|
|
304
|
+
"D6 非法 kind → ValueError 原样上抛(fail-closed)", (t, txt[:120]))
|
|
305
|
+
ok(len(_ledger(cg)) == 5, "D7 只有合法五条落盘(非法值不落盘)",
|
|
306
|
+
len(_ledger(cg)))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------- E
|
|
310
|
+
# 生效条件:无入参;在自有沙箱根上按 subject/slot/old-new 组合逐条断言 ValueError 原样上抛(不包装、不吞)与合法撤回照常通过(落点见组内文案)。
|
|
311
|
+
def gE():
|
|
312
|
+
print("== E 参数校验(ValueError 原样上抛) ==")
|
|
313
|
+
cg = _secure_cg(_principal("designer", actor="evt-e"))
|
|
314
|
+
t, txt = _err(lambda: _evt(cg, slot="住所", new="值"))
|
|
315
|
+
ok(t == "ValueError" and "subject" in txt,
|
|
316
|
+
"E1 缺 subject → ValueError(点名 subject)", (t, txt[:120]))
|
|
317
|
+
t, txt = _err(lambda: _evt(cg, subject=" ", slot="住所", new="值"))
|
|
318
|
+
ok(t == "ValueError" and "subject" in txt,
|
|
319
|
+
"E2 subject 纯空白 → ValueError", (t, txt[:120]))
|
|
320
|
+
t, txt = _err(lambda: _evt(cg, subject="甲", new="值"))
|
|
321
|
+
ok(t == "ValueError" and "slot" in txt,
|
|
322
|
+
"E3 缺 slot → ValueError(点名 slot)", (t, txt[:120]))
|
|
323
|
+
t, txt = _err(lambda: _evt(cg, subject="甲", slot=" ", new="值"))
|
|
324
|
+
ok(t == "ValueError" and "slot" in txt,
|
|
325
|
+
"E4 slot 纯空白 → ValueError", (t, txt[:120]))
|
|
326
|
+
t, txt = _err(lambda: _evt(cg, subject="甲", slot="住所"))
|
|
327
|
+
ok(t == "ValueError" and "old" in txt and "new" in txt,
|
|
328
|
+
"E5 old/new 双空(缺省)→ ValueError", (t, txt[:120]))
|
|
329
|
+
t, txt = _err(lambda: _evt(cg, subject="甲", slot="住所",
|
|
330
|
+
old=None, new=None))
|
|
331
|
+
ok(t == "ValueError", "E6 old/new 显式双 None → ValueError", (t, txt[:120]))
|
|
332
|
+
out = _evt(cg, subject="甲", slot="住所", old="旧值", new=None,
|
|
333
|
+
kind="retraction", seq=9, evidence="e7")
|
|
334
|
+
ok(out.get("ok") is True
|
|
335
|
+
and (out.get("event") or {}).get("new") is None
|
|
336
|
+
and (out.get("event") or {}).get("old") == "旧值",
|
|
337
|
+
"E7 合法撤回(old 有值、new 空)照常通过", out)
|
|
338
|
+
ok(len(_ledger(cg)) == 1,
|
|
339
|
+
"E8 校验失败的各次均未落盘(台账仅合法撤回一条)", len(_ledger(cg)))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ---------------------------------------------------------------- F
|
|
343
|
+
# 生效条件:无入参;在自有沙箱根上断言既有 op(info/read/recent/未知 op)与受限令牌拒绝路径行为不变(消息点名原 op 词)、无 principal 只读面照常、_ACTION_SIGS/_ACTION_DEFAULT 未被本批改动(落点见组内文案)。
|
|
344
|
+
def gF():
|
|
345
|
+
print("== F 回归:既有 op 行为与权限行改动前一致 ==")
|
|
346
|
+
dcg = _secure_cg(_principal("designer", actor="evt-reg"))
|
|
347
|
+
r = _err(lambda: _ms()._cg_call(dcg, {"op": "info"}))
|
|
348
|
+
ok(r[0] == "", "F1 designer:op=info 放行(无异常)", r)
|
|
349
|
+
r = _err(lambda: _ms()._cg_call(dcg, {"op": "read", "query": "回归"}))
|
|
350
|
+
ok(r[0] == "", "F2 designer:op=read 放行", r)
|
|
351
|
+
r = _err(lambda: _ms()._cg_call(dcg, {"op": "recent",
|
|
352
|
+
"action": "list"}))
|
|
353
|
+
ok(r[0] == "", "F3 designer:op=recent/list 放行", r)
|
|
354
|
+
t, txt = _err(lambda: _ms()._cg_call(dcg, {"op": "no_such_op"}))
|
|
355
|
+
ok(t == "ValueError",
|
|
356
|
+
"F4 designer:未识别 op 仍走 ValueError 兜底(分发尾部不变)",
|
|
357
|
+
(t, txt[:100]))
|
|
358
|
+
rcg = _secure_cg(_principal("recorder", actor="evt-reg2",
|
|
359
|
+
ops_allow=["read"]))
|
|
360
|
+
r = _err(lambda: _ms()._cg_call(rcg, {"op": "read", "query": "回归"}))
|
|
361
|
+
ok(r[0] == "", "F5 受限令牌:白名单内 op=read 放行", r)
|
|
362
|
+
t, txt = _err(lambda: _ms()._cg_call(rcg, {"op": "whitebox",
|
|
363
|
+
"action": "ping"}))
|
|
364
|
+
ok(t == "AccessDenied" and "op=whitebox" in txt,
|
|
365
|
+
"F6 受限令牌:白名单外 op=whitebox 拒且消息点名 op=whitebox"
|
|
366
|
+
"(非 state_event 的 op 词逐位不变)", (t, txt[:120]))
|
|
367
|
+
t, txt = _err(lambda: _ms()._cg_call(rcg, {"op": "write", "node_id": "x",
|
|
368
|
+
"content": "y"}))
|
|
369
|
+
ok(t == "AccessDenied" and "op=write" in txt,
|
|
370
|
+
"F7 受限令牌:白名单外 op=write 拒(既有写面拒绝行为不变)",
|
|
371
|
+
(t, txt[:120]))
|
|
372
|
+
pcg = _plain_cg()
|
|
373
|
+
r = _err(lambda: _ms()._cg_call(pcg, {"op": "read", "query": "回归"}))
|
|
374
|
+
ok(r[0] == "", "F8 无 principal(MdCGOS):只读面照常(闸门跳过路径不变)", r)
|
|
375
|
+
ok("state_event" not in _ms()._ACTION_SIGS
|
|
376
|
+
and "state_event" not in _ms()._ACTION_DEFAULT,
|
|
377
|
+
"F9 state_event 无 action 面(_ACTION_SIGS/_ACTION_DEFAULT 未动)",
|
|
378
|
+
(sorted(_ms()._ACTION_SIGS), sorted(_ms()._ACTION_DEFAULT)))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
_GROUPS = (gA, gB, gC, gD, gE, gF)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# 生效条件:无入参;逐组执行 _GROUPS(每组自行建沙箱根),组内抛异常记为一条失败并把栈尾打印出来(不中断其余组),返回失败条数。
|
|
385
|
+
def _run_groups() -> int:
|
|
386
|
+
_PASS.clear()
|
|
387
|
+
_FAIL.clear()
|
|
388
|
+
for g in _GROUPS:
|
|
389
|
+
try:
|
|
390
|
+
g()
|
|
391
|
+
except Exception as exc: # noqa: BLE001
|
|
392
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
393
|
+
import traceback
|
|
394
|
+
print(traceback.format_exc()[-900:])
|
|
395
|
+
return len(_FAIL)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ---------------------------------------------------------------- 变异表
|
|
399
|
+
# 锚点 = (名字, rel, ((old, new), …))。每条必须让套件**转红**(红项原文由 --mutate 打印):
|
|
400
|
+
# M1 权限映射回退(require_op(op),state_event 失去 write 词映射)→ B1/B3 红
|
|
401
|
+
# M2 actor 改取请求参数(防伪造失效)→ C1/C2/C3 红
|
|
402
|
+
# M3 kind 不归一(空串直传,append 对空串抛 ValueError)→ D2/D3 红
|
|
403
|
+
# M4 删写闸 require_write(can_write=False 不再拦)→ B6/B7 红
|
|
404
|
+
_MUTATIONS = (
|
|
405
|
+
("权限映射回退(state_event 不再映射到 write 词)", "md_cg/mcp_server.py",
|
|
406
|
+
((' _p.require_op("write" if op == "state_event" else op)',
|
|
407
|
+
' _p.require_op(op)'),)),
|
|
408
|
+
("actor 改取请求参数(防伪造失效)", "md_cg/mcp_server.py",
|
|
409
|
+
((' from . import state_events as _se\n'
|
|
410
|
+
' _actor = _p.actor if _p is not None else "system"',
|
|
411
|
+
' from . import state_events as _se\n'
|
|
412
|
+
' _actor = a.get("actor") or (_p.actor if _p is not None else "system")'),)),
|
|
413
|
+
("kind 不归一(空串直传)", "md_cg/mcp_server.py",
|
|
414
|
+
((' kind=(a.get("kind") or "").strip() or None,',
|
|
415
|
+
' kind=a.get("kind"),'),)),
|
|
416
|
+
("删写闸 require_write(can_write 不再拦)", "md_cg/mcp_server.py",
|
|
417
|
+
((' if _p is not None:\n _p.require_write("internal")\n',
|
|
418
|
+
' if False:\n _p.require_write("internal")\n'),)),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# 生效条件:rel 为仓根相对路径时返回该文件的当前源码文本(每次从盘读,不缓存——变异基线必须是**当前工作区**实现)。
|
|
423
|
+
def _rel_text(rel: str) -> str:
|
|
424
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
425
|
+
return f.read()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# 生效条件:name/rel/text 给定时——把 text 以 __package__="md_cg"、__file__=<仓内 rel 路径> 执行成命名空间并包成新模块对象返回(不注册 sys.modules、不触发 __main__;语法错误原样抛)。
|
|
429
|
+
def _exec_module(name: str, rel: str, text: str):
|
|
430
|
+
ns = {"__name__": name, "__package__": "md_cg",
|
|
431
|
+
"__file__": os.path.join(_REPO, rel)}
|
|
432
|
+
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
433
|
+
m = types.ModuleType(name)
|
|
434
|
+
m.__dict__.update(ns)
|
|
435
|
+
return m
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# 生效条件:list_only 为真时只打印变异表并返回 0;否则先跑未变异基线(必须 0 失败),再逐条在**内存副本**上注入变异(逐锚点 replace,锚点缺失记 ANCHOR-MISS)后重跑整套分组——有红项记 OK、无红项记 MISS(空转即失败);全部锚点命中有红项时返回 0、否则 1;有 ANCHOR-MISS 时返回 2(fail-closed)。
|
|
439
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
440
|
+
print("!! 定点变异自证:逐条把机制改回「错误/缺陷」形态,套件必须转红\n")
|
|
441
|
+
if list_only:
|
|
442
|
+
for name, rel, _pairs in _MUTATIONS:
|
|
443
|
+
print(" %-40s [%s]" % (name, rel))
|
|
444
|
+
return 0
|
|
445
|
+
anchor_miss, bad = [], []
|
|
446
|
+
with contextlib.redirect_stdout(buf0 := io.StringIO()):
|
|
447
|
+
clean = _run_groups()
|
|
448
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
449
|
+
if clean:
|
|
450
|
+
bad.append("未变异基线即失败")
|
|
451
|
+
for l in buf0.getvalue().splitlines(): # 基线红项逐条打印(可定位)
|
|
452
|
+
if l.strip().startswith("FAIL "):
|
|
453
|
+
print(" " + l.strip()[5:])
|
|
454
|
+
for name, rel, pairs in _MUTATIONS:
|
|
455
|
+
mut_src, miss = _rel_text(rel), []
|
|
456
|
+
for old, new in pairs:
|
|
457
|
+
if old not in mut_src:
|
|
458
|
+
miss.append(old.splitlines()[0].strip()[:48])
|
|
459
|
+
continue
|
|
460
|
+
mut_src = mut_src.replace(old, new, 1)
|
|
461
|
+
if miss:
|
|
462
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 源码里找不到(实现改了却没"
|
|
463
|
+
"同步本表):%s" % (name, rel, miss))
|
|
464
|
+
anchor_miss.append(name)
|
|
465
|
+
continue
|
|
466
|
+
try:
|
|
467
|
+
globals()["MS"] = _exec_module("md_cg._ms_mut", rel, mut_src)
|
|
468
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
469
|
+
reds = _run_groups()
|
|
470
|
+
detail = buf.getvalue()
|
|
471
|
+
finally:
|
|
472
|
+
globals()["MS"] = _REAL_MS
|
|
473
|
+
reds_lines = [l for l in detail.splitlines()
|
|
474
|
+
if l.strip().startswith("FAIL ")]
|
|
475
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
476
|
+
print(" %s %-40s 红项=%d %s"
|
|
477
|
+
% ("OK " if reds else "MISS ", name, reds, verdict))
|
|
478
|
+
for l in reds_lines[:6]:
|
|
479
|
+
print(" " + l.strip()[5:])
|
|
480
|
+
if not reds:
|
|
481
|
+
bad.append(name)
|
|
482
|
+
# 恢复证明:全部轮次结束后把真模块放回,重跑一次基线并须全绿。
|
|
483
|
+
with contextlib.redirect_stdout(buf1 := io.StringIO()):
|
|
484
|
+
restored = _run_groups()
|
|
485
|
+
print(" 恢复后基线(真模块):失败=%d(必须为 0)" % restored)
|
|
486
|
+
if restored:
|
|
487
|
+
bad.append("恢复后基线失败")
|
|
488
|
+
if anchor_miss:
|
|
489
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
490
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
491
|
+
return 2
|
|
492
|
+
print("\n定点变异自证:%s"
|
|
493
|
+
% ("PASS(每处机制都有断言把它钉死)" if not bad
|
|
494
|
+
else "FAIL —— " + "、".join(bad)))
|
|
495
|
+
return 0 if not bad else 1
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def main() -> int:
|
|
499
|
+
_sandbox_env()
|
|
500
|
+
try:
|
|
501
|
+
if "--mutate" in sys.argv:
|
|
502
|
+
return _mutate_mode("--list" in sys.argv)
|
|
503
|
+
n = _run_groups()
|
|
504
|
+
print("\n状态事件记账写口守卫:%d 通过 / %d 失败" % (len(_PASS), n))
|
|
505
|
+
print("SUMMARY: md_cg.test_state_event_op 通过=%d 失败=%d"
|
|
506
|
+
% (len(_PASS), n))
|
|
507
|
+
return 0 if not n else 1
|
|
508
|
+
finally:
|
|
509
|
+
_restore_env()
|
|
510
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
if __name__ == "__main__":
|
|
514
|
+
sys.exit(main())
|