@furongjun1999/dsh-memory 0.6.0 → 0.7.0
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 +618 -572
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/README.md +1 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +52 -2
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -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 +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/lib/bridge.js +24 -2
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.js +16 -1
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +48 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/freshness.py +511 -0
- package/md_cg/fsutil.py +454 -8
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4261 -3818
- package/md_cg/mdcg.py +4695 -3547
- package/md_cg/mdcos.py +4672 -4159
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +713 -576
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1436 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1387 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +4 -3
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +522 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +4 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/hooks.ts +229 -16
- package/src/init.ts +15 -1
- package/src/lib/prompt_safety.ts +88 -1
- package/utf8_boot.py +237 -0
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""默认策略加载守卫(issue #43 问题 1 修复,2026-09-29)。
|
|
3
|
+
|
|
4
|
+
运行:python -X utf8 -m md_cg.test_issue43_default_policy
|
|
5
|
+
python -X utf8 -m md_cg.test_issue43_default_policy --mutation-baseline
|
|
6
|
+
|
|
7
|
+
缺陷(第 4 条取证):默认安装下写入闸门不生效——
|
|
8
|
+
(a) `audit.load_rulebook` 只从 `MDCG_POLICY_FILE` 取路径,未设即返回**空规则库**
|
|
9
|
+
且无任何默认回落;空规则让 `_rule_check` 恒判 `DEFER`;
|
|
10
|
+
(b) npm 包不含 `data/policy.json`、`lingshu-init` 也不生成该 env,而 README 写
|
|
11
|
+
「默认 data/policy.json」——三处不符;
|
|
12
|
+
(c) 后果不是「少一条规则」而是**安全面翻转**:`writepipe._gate_audit` 的
|
|
13
|
+
非 ACCEPT/REJECT 出口是 `cg.propose` ⇒ 正文(含凭据)**明文**落
|
|
14
|
+
`hippocampus/inbox.jsonl`,且脱敏只在 REJECT 分支。
|
|
15
|
+
|
|
16
|
+
使用者 2026-09-29 裁定方向(①+③):
|
|
17
|
+
① 包内随发默认策略(`package.json` 的 files 纳入 `data/policy.json`),
|
|
18
|
+
`load_rulebook` 未设/为空时回落**包内默认路径**(按包根解析、不依赖 cwd);
|
|
19
|
+
② 未设且包内默认也拿不到 → **fail-closed**,且必须发生在提案入队**之前**
|
|
20
|
+
(不落节点、不 propose、inbox.jsonl 字节面零正文),错误结构化
|
|
21
|
+
(错误码 + 可执行 hint);
|
|
22
|
+
③ 显式 `MDCG_POLICY_FILE` 指向不可读/不可解析的文件同样 fail-closed。
|
|
23
|
+
|
|
24
|
+
本套断言分八组:
|
|
25
|
+
G1 包内默认回落生效(未设 / 空串两条路;规则内容与包内文件逐条一致)
|
|
26
|
+
G2 定位不依赖 cwd(chdir 到系统临时目录后包根/策略路径/解析结果三不变)
|
|
27
|
+
G3 未设且不可用 → fail-closed 且**零正文**(合成凭据 + 标记实测 inbox 与全库字节面)
|
|
28
|
+
G4 显式坏路径 fail-closed(不存在 / 不可解析 / 顶层非对象 / 目录不可读 + 端到端)
|
|
29
|
+
G5 默认安装的真实行为(包内默认生效:命中禁表或缺六要素 → REJECT,绝不进审核队列)
|
|
30
|
+
G6 来源可见面(`--show-config` / 启动 stderr 单行 / `cg(op=info).write_policy`)
|
|
31
|
+
G7 单点与接线(防副本回归;含判据函数自身的合成负例判别力自证)
|
|
32
|
+
G8 **键类型闸**(issue #43 补强;复核判 DEFER 的实据面):策略是合法 JSON 对象
|
|
33
|
+
而列表型键取值是**标量**(int / bool / str)时,三面同时失守的旧行为被钉死——
|
|
34
|
+
① `policy_report()` 抛 `TypeError`(int/bool)或把字符串长度当规则数(str);
|
|
35
|
+
② `mcp_server.py` 启动调用不在 try 内 ⇒ server rc=1、诊断面 rc=1 且 stdout 空;
|
|
36
|
+
③ 写入侧 `_rule_check` 同抛被兜底吞成 DEFER ⇒ 正文(含凭据)逐字落
|
|
37
|
+
`hippocampus/inbox.jsonl`。收口口径:**任何列表型键取值非列表 ⇒ 一律视同
|
|
38
|
+
策略不可用**,走与①②③同一条 fail-closed(在提案入队之前拦下),
|
|
39
|
+
`policy_report()` 恒不抛、`--show-config` 恒退出 0(`available=false`)、
|
|
40
|
+
启动 stderr 的来源行不因策略畸形缺失、`op=info` 不被拖崩、字符串**不按
|
|
41
|
+
字符拆成规则**。每形态 7 条(A 诊断面 / B 解析面 / C `--show-config`(同进程) /
|
|
42
|
+
C2 真进程 `--show-config` / D 真进程启动 / E 写入 fail-closed 且字节面零
|
|
43
|
+
正文 / F `op=info` 面)+
|
|
44
|
+
3 条(G 计数非长度 / H 消费面纵深 / I 形状闸不误伤合法默认策略)。
|
|
45
|
+
|
|
46
|
+
**变异自证(--mutation-baseline)**:逐个**定点变异**生产实现(源码锚点替换后
|
|
47
|
+
exec,不碰磁盘文件),套件必须转红且**恰好命中声明条数**(多一条少一条都判
|
|
48
|
+
FAIL——多了说明断言串扰,少了说明该判据空转)。变异锚点写死在 `_MUTATIONS`,
|
|
49
|
+
实现改动致锚点漂移即报 **ANCHOR-MISS 并退出码 2**(fail-closed)。G8-D 走**真
|
|
50
|
+
子进程**(`python -m md_cg.mcp_server`),内存 patch 不跨进程 ⇒ 对 `audit` 模块
|
|
51
|
+
的变异另经 `sitecustomize.py` 注入子进程(N225 同款),注入标记缺失即报
|
|
52
|
+
**INJECT-MISS 并退出码 2**(否则「子进程那半」会静默不设防)。
|
|
53
|
+
|
|
54
|
+
**基线源纪律**:本套**不以 git HEAD 为基线源**(基线绑提交即失效,本仓已有两次
|
|
55
|
+
教训)——判别力由「源码锚点变异 + 逐条计数」现场自证,与提交状态无关。
|
|
56
|
+
实验一律用系统临时目录的合成库;合成凭据是拼接哑值,非真凭据。
|
|
57
|
+
"""
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import contextlib
|
|
61
|
+
import inspect
|
|
62
|
+
import io
|
|
63
|
+
import json
|
|
64
|
+
import os
|
|
65
|
+
import shutil
|
|
66
|
+
import subprocess
|
|
67
|
+
import sys
|
|
68
|
+
import tempfile
|
|
69
|
+
|
|
70
|
+
from . import audit
|
|
71
|
+
from . import mcp_server
|
|
72
|
+
from . import writepipe
|
|
73
|
+
from .mdcos import MdCGSecure
|
|
74
|
+
from .security import Principal
|
|
75
|
+
|
|
76
|
+
_PASS = []
|
|
77
|
+
_FAIL = []
|
|
78
|
+
_RED = [] # [(组名, 断言名)] —— 变异核验只看它
|
|
79
|
+
_GROUP = ["?"]
|
|
80
|
+
|
|
81
|
+
# 合成凭据(拼接哑值,非真凭据;整串在源码里不成形态,发布闸不误报)与正文标记
|
|
82
|
+
SECRET = "sk-" + ("A1b2C3d4" * 3)
|
|
83
|
+
MARK = "I43CANARY"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _group(name):
|
|
87
|
+
_GROUP[0] = name
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def ok(cond, name):
|
|
91
|
+
global _PASS
|
|
92
|
+
if cond:
|
|
93
|
+
_PASS.append(name)
|
|
94
|
+
print(" PASS " + name)
|
|
95
|
+
else:
|
|
96
|
+
_FAIL.append(name)
|
|
97
|
+
_RED.append((_GROUP[0], name))
|
|
98
|
+
print(" FAIL " + name)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------- 实验装置
|
|
102
|
+
|
|
103
|
+
@contextlib.contextmanager
|
|
104
|
+
def _env(**kw):
|
|
105
|
+
"""临时设定/清除环境变量(None = 清除),退出时逐键恢复。"""
|
|
106
|
+
old = {k: os.environ.get(k) for k in kw}
|
|
107
|
+
try:
|
|
108
|
+
for k, v in kw.items():
|
|
109
|
+
if v is None:
|
|
110
|
+
os.environ.pop(k, None)
|
|
111
|
+
else:
|
|
112
|
+
os.environ[k] = v
|
|
113
|
+
yield
|
|
114
|
+
finally:
|
|
115
|
+
for k, v in old.items():
|
|
116
|
+
if v is None:
|
|
117
|
+
os.environ.pop(k, None)
|
|
118
|
+
else:
|
|
119
|
+
os.environ[k] = v
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@contextlib.contextmanager
|
|
123
|
+
def _dfl_policy(path):
|
|
124
|
+
"""临时把包内默认策略路径指向 path(实验注入点;用后必还原)。"""
|
|
125
|
+
old = audit.default_policy_path
|
|
126
|
+
audit.default_policy_path = (lambda: path)
|
|
127
|
+
try:
|
|
128
|
+
yield
|
|
129
|
+
finally:
|
|
130
|
+
audit.default_policy_path = old
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _write_text(tmp, name, text):
|
|
134
|
+
p = os.path.join(tmp, name)
|
|
135
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
136
|
+
f.write(text)
|
|
137
|
+
return p
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _cg(root):
|
|
141
|
+
p = Principal(tenant="default", actor="i43-t", role="designer",
|
|
142
|
+
can_write=True, can_admin=True)
|
|
143
|
+
return MdCGSecure(root, principal=p)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _pipe():
|
|
147
|
+
return writepipe.install_default_gates(writepipe.WritePipeline())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _root_bytes(root):
|
|
151
|
+
"""root 下全部文件的字节面拼合(零正文的**全库**判据面)。"""
|
|
152
|
+
chunks = []
|
|
153
|
+
for dp, _dn, fns in os.walk(root):
|
|
154
|
+
for fn in fns:
|
|
155
|
+
try:
|
|
156
|
+
with open(os.path.join(dp, fn), "rb") as f:
|
|
157
|
+
chunks.append(f.read())
|
|
158
|
+
except OSError:
|
|
159
|
+
pass
|
|
160
|
+
return b"\n".join(chunks)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _inbox_bytes(root):
|
|
164
|
+
p = os.path.join(root, "hippocampus", "inbox.jsonl")
|
|
165
|
+
if not os.path.exists(p):
|
|
166
|
+
return b""
|
|
167
|
+
with open(p, "rb") as f:
|
|
168
|
+
return f.read()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------- G1 包内回落
|
|
172
|
+
|
|
173
|
+
def g1():
|
|
174
|
+
_group("G1")
|
|
175
|
+
with _env(MDCG_POLICY_FILE=None):
|
|
176
|
+
rules, src, err = audit.resolve_rulebook()
|
|
177
|
+
ok(src == "package_default" and err is None,
|
|
178
|
+
"G1a 未设 env → 回落包内默认(source=%s)" % src)
|
|
179
|
+
with open(audit.default_policy_path(), encoding="utf-8") as f:
|
|
180
|
+
dflt = json.load(f)
|
|
181
|
+
ok((rules or {}) == dflt,
|
|
182
|
+
"G1b 规则与包内 data/policy.json 逐条一致(不掺默认值)")
|
|
183
|
+
ok(len((rules or {}).get("forbidden") or []) == 11
|
|
184
|
+
and len((rules or {}).get("required") or []) == 6,
|
|
185
|
+
"G1c 包内默认含 11 条 forbidden + 6 条 required(禁表与六要素未被削弱)")
|
|
186
|
+
ok(audit.load_rulebook() == dflt,
|
|
187
|
+
"G1d load_rulebook 未设 env 时同样回落包内默认")
|
|
188
|
+
rep = audit.policy_report()
|
|
189
|
+
ok(rep.get("source") == "package_default" and rep.get("available") is True
|
|
190
|
+
and rep.get("forbidden") == 11 and rep.get("required") == 6,
|
|
191
|
+
"G1e policy_report 报出来源与计数(source=%s)" % rep.get("source"))
|
|
192
|
+
with _env(MDCG_POLICY_FILE=""):
|
|
193
|
+
rules2, src2, err2 = audit.resolve_rulebook()
|
|
194
|
+
ok(src2 == "package_default" and err2 is None and bool(rules2),
|
|
195
|
+
"G1f env 为空串同样回落包内默认(「未设/为空」两条路)")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------- G2 不依赖 cwd
|
|
199
|
+
|
|
200
|
+
def g2():
|
|
201
|
+
_group("G2")
|
|
202
|
+
before_root = audit.package_root()
|
|
203
|
+
before_path = audit.default_policy_path()
|
|
204
|
+
tmp = tempfile.mkdtemp(prefix="i43_cwd_")
|
|
205
|
+
cwd = os.getcwd()
|
|
206
|
+
try:
|
|
207
|
+
os.chdir(tmp)
|
|
208
|
+
ok(audit.package_root() == before_root
|
|
209
|
+
and os.path.isfile(os.path.join(audit.package_root(), "md_cg", "audit.py")),
|
|
210
|
+
"G2a package_root 不随 cwd 变(且指向含 md_cg/audit.py 的包根)")
|
|
211
|
+
ok(audit.default_policy_path() == before_path
|
|
212
|
+
and os.path.isfile(before_path),
|
|
213
|
+
"G2b default_policy_path 不随 cwd 变且文件存在")
|
|
214
|
+
rules, src, err = audit.resolve_rulebook()
|
|
215
|
+
ok(src == "package_default" and err is None and bool(rules),
|
|
216
|
+
"G2c cwd 在别处时仍解析到包内默认策略(source=%s)" % src)
|
|
217
|
+
finally:
|
|
218
|
+
os.chdir(cwd)
|
|
219
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------- G3 不可用 → fail-closed
|
|
223
|
+
|
|
224
|
+
def g3():
|
|
225
|
+
_group("G3")
|
|
226
|
+
tmp = tempfile.mkdtemp(prefix="i43_unavail_")
|
|
227
|
+
miss = os.path.join(tmp, "no_such_policy.json")
|
|
228
|
+
try:
|
|
229
|
+
with _dfl_policy(miss), _env(MDCG_POLICY_FILE=None):
|
|
230
|
+
root = os.path.join(tmp, "root")
|
|
231
|
+
cg = _cg(root)
|
|
232
|
+
out = _pipe().execute(cg, {
|
|
233
|
+
"node_id": "i43_unavail", "content_kind": "text", "layer": "knowledge",
|
|
234
|
+
"content": "%s 正文 %s\n" % (MARK, SECRET)})
|
|
235
|
+
ok(out.get("moved_to") == "policy_unavailable"
|
|
236
|
+
and out.get("committed") is False and out.get("ok") is False,
|
|
237
|
+
"G3a 未设 env 且包内默认不可得 → fail-closed(moved_to=%s)"
|
|
238
|
+
% out.get("moved_to"))
|
|
239
|
+
err = out.get("error") or {}
|
|
240
|
+
ok(isinstance(err, dict) and err.get("code") == "policy_unavailable"
|
|
241
|
+
and bool(err.get("hint")),
|
|
242
|
+
"G3b 错误结构化:code=policy_unavailable 且有可执行 hint")
|
|
243
|
+
ib = _inbox_bytes(root)
|
|
244
|
+
ok(SECRET.encode() not in ib and MARK.encode() not in ib,
|
|
245
|
+
"G3c inbox.jsonl 字节面零正文(len=%d)" % len(ib))
|
|
246
|
+
ok(not os.path.exists(os.path.join(root, "knowledge", "i43_unavail.md")),
|
|
247
|
+
"G3d 未落节点(knowledge/ 无该 id 文件)")
|
|
248
|
+
rb = _root_bytes(root)
|
|
249
|
+
ok(SECRET.encode() not in rb and MARK.encode() not in rb,
|
|
250
|
+
"G3e 全库字节面零正文(含负记忆/索引,len=%d)" % len(rb))
|
|
251
|
+
ok(SECRET not in json.dumps(out, ensure_ascii=False),
|
|
252
|
+
"G3f 响应体亦不含凭据明文")
|
|
253
|
+
finally:
|
|
254
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------- G4 显式坏路径
|
|
258
|
+
|
|
259
|
+
def g4():
|
|
260
|
+
_group("G4")
|
|
261
|
+
tmp = tempfile.mkdtemp(prefix="i43_envbad_")
|
|
262
|
+
try:
|
|
263
|
+
with _env(MDCG_POLICY_FILE=os.path.join(tmp, "no_such.json")):
|
|
264
|
+
rules, src, err = audit.resolve_rulebook()
|
|
265
|
+
ok(src == "env" and (err or {}).get("code") == "policy_not_found",
|
|
266
|
+
"G4a 显式坏路径 → 不可用(source=env code=policy_not_found)")
|
|
267
|
+
ok(rules is None,
|
|
268
|
+
"G4b 显式坏路径**不回落到包内默认**(rules 为 None,非包内规则)")
|
|
269
|
+
ok(audit.load_rulebook() == {},
|
|
270
|
+
"G4c load_rulebook 对坏路径返回空规则(不抛异常)")
|
|
271
|
+
for name, text, want in (
|
|
272
|
+
("bad_json.json", "{ oops", "policy_invalid_json"),
|
|
273
|
+
("not_object.json", "[1,2,3]", "policy_not_object"),
|
|
274
|
+
("empty_obj.json", "{}", None)):
|
|
275
|
+
with _env(MDCG_POLICY_FILE=_write_text(tmp, name, text)):
|
|
276
|
+
_r, _s, e2 = audit.resolve_rulebook()
|
|
277
|
+
if want is None:
|
|
278
|
+
ok(e2 is None and _r == {},
|
|
279
|
+
"G4d 空 dict 是**可用**策略(内容为空,非可用性故障)")
|
|
280
|
+
else:
|
|
281
|
+
ok((e2 or {}).get("code") == want,
|
|
282
|
+
"G4e 显式坏路径 %s → code=%s" % (name, want))
|
|
283
|
+
with _env(MDCG_POLICY_FILE=tmp): # 目录:exists 为真但不可读
|
|
284
|
+
_r, _s, e3 = audit.resolve_rulebook()
|
|
285
|
+
ok((e3 or {}).get("code") == "policy_unreadable",
|
|
286
|
+
"G4f 显式路径指向目录(不可读)→ code=policy_unreadable")
|
|
287
|
+
# 端到端:显式坏路径 → fail-closed,队列零正文
|
|
288
|
+
with _env(MDCG_POLICY_FILE=os.path.join(tmp, "no_such.json")):
|
|
289
|
+
root = os.path.join(tmp, "root")
|
|
290
|
+
cg = _cg(root)
|
|
291
|
+
out = _pipe().execute(cg, {
|
|
292
|
+
"node_id": "i43_envbad", "content_kind": "text", "layer": "knowledge",
|
|
293
|
+
"content": "%s 正文 %s\n" % (MARK, SECRET)})
|
|
294
|
+
ok(out.get("moved_to") == "policy_unavailable"
|
|
295
|
+
and out.get("committed") is False,
|
|
296
|
+
"G4g 端到端:显式坏路径写入 fail-closed(moved_to=%s)"
|
|
297
|
+
% out.get("moved_to"))
|
|
298
|
+
rb = _root_bytes(root)
|
|
299
|
+
ok(SECRET.encode() not in rb and MARK.encode() not in rb,
|
|
300
|
+
"G4h 端到端:全库字节面零正文(len=%d)" % len(rb))
|
|
301
|
+
finally:
|
|
302
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------- G5 默认安装真实行为
|
|
306
|
+
|
|
307
|
+
def g5():
|
|
308
|
+
_group("G5")
|
|
309
|
+
tmp = tempfile.mkdtemp(prefix="i43_default_")
|
|
310
|
+
try:
|
|
311
|
+
with _env(MDCG_POLICY_FILE=None):
|
|
312
|
+
root = os.path.join(tmp, "root_a")
|
|
313
|
+
cg = _cg(root)
|
|
314
|
+
out = _pipe().execute(cg, {
|
|
315
|
+
"node_id": "i43_cred", "content_kind": "text", "layer": "knowledge",
|
|
316
|
+
"content": "%s 正文 %s\n" % (MARK, SECRET)})
|
|
317
|
+
ok(out.get("moved_to") == "rejected" and out.get("committed") is False
|
|
318
|
+
and "命中禁止规则" in str((out.get("verdict") or {}).get("evidence")),
|
|
319
|
+
"G5a 默认策略生效:命中禁表 → REJECT(moved_to=%s)"
|
|
320
|
+
% out.get("moved_to"))
|
|
321
|
+
ok(SECRET.encode() not in _inbox_bytes(root) and MARK.encode() not in _inbox_bytes(root),
|
|
322
|
+
"G5b inbox 零正文(不进审核队列)")
|
|
323
|
+
rb = _root_bytes(root)
|
|
324
|
+
ok(SECRET.encode() not in rb, "G5c 负记忆已脱敏(凭据不出现)")
|
|
325
|
+
ok("[已过滤:禁表#".encode() in rb, "G5d 负记忆含脱敏占位符(脱敏确实发生)")
|
|
326
|
+
root_b = os.path.join(tmp, "root_b")
|
|
327
|
+
cg_b = _cg(root_b)
|
|
328
|
+
out_b = _pipe().execute(cg_b, {
|
|
329
|
+
"node_id": "i43_six", "content_kind": "text", "layer": "knowledge",
|
|
330
|
+
"content": "%s 只有标记没有六要素\n" % MARK})
|
|
331
|
+
detail = (out_b.get("verdict") or {}).get("detail") or {}
|
|
332
|
+
ok(out_b.get("moved_to") == "rejected" and out_b.get("committed") is False
|
|
333
|
+
and len(detail.get("missing") or []) == 6,
|
|
334
|
+
"G5e 缺六要素 → REJECT 且给全六条缺失清单(missing=%d)"
|
|
335
|
+
% len(detail.get("missing") or []))
|
|
336
|
+
ok(MARK.encode() not in _inbox_bytes(root_b),
|
|
337
|
+
"G5f 缺要素场景 inbox 亦零正文")
|
|
338
|
+
finally:
|
|
339
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ---------------------------------------------------------------- G6 来源可见面
|
|
343
|
+
|
|
344
|
+
def g6():
|
|
345
|
+
_group("G6")
|
|
346
|
+
argv = sys.argv
|
|
347
|
+
with _env(MDCG_POLICY_FILE=None):
|
|
348
|
+
buf = io.StringIO()
|
|
349
|
+
try:
|
|
350
|
+
sys.argv = [argv[0], "--show-config"]
|
|
351
|
+
with contextlib.redirect_stdout(buf):
|
|
352
|
+
rc = mcp_server.main()
|
|
353
|
+
finally:
|
|
354
|
+
sys.argv = argv
|
|
355
|
+
out = buf.getvalue().strip()
|
|
356
|
+
ok(rc == 0 and bool(out),
|
|
357
|
+
"G6a --show-config 只读诊断面:退出码 0 且有输出(rc=%s)" % rc)
|
|
358
|
+
try:
|
|
359
|
+
doc = json.loads(out.splitlines()[-1]) if out else {}
|
|
360
|
+
except (ValueError, IndexError):
|
|
361
|
+
doc = {}
|
|
362
|
+
ok(isinstance(doc.get("policy"), dict)
|
|
363
|
+
and doc["policy"].get("source") == "package_default"
|
|
364
|
+
and doc["policy"].get("available") is True,
|
|
365
|
+
"G6b --show-config 报出来源=包内默认(source=%s)"
|
|
366
|
+
% (doc.get("policy") or {}).get("source"))
|
|
367
|
+
ok(doc.get("policy") == audit.policy_report(),
|
|
368
|
+
"G6c --show-config 与 policy_report 同一判据(无第二套口径)")
|
|
369
|
+
ok(doc.get("server") == mcp_server.SERVER_NAME
|
|
370
|
+
and doc.get("version") == mcp_server.SERVER_VERSION,
|
|
371
|
+
"G6d --show-config 含 server/version 自报")
|
|
372
|
+
with _env(MDCG_POLICY_FILE=os.path.join(tempfile.gettempdir(), "i43_none.json")):
|
|
373
|
+
buf2 = io.StringIO()
|
|
374
|
+
try:
|
|
375
|
+
sys.argv = [argv[0], "--show-config"]
|
|
376
|
+
with contextlib.redirect_stdout(buf2):
|
|
377
|
+
rc2 = mcp_server.main()
|
|
378
|
+
finally:
|
|
379
|
+
sys.argv = argv
|
|
380
|
+
try:
|
|
381
|
+
doc2 = json.loads(buf2.getvalue().strip().splitlines()[-1])
|
|
382
|
+
except (ValueError, IndexError):
|
|
383
|
+
doc2 = {}
|
|
384
|
+
ok(rc2 == 0 and (doc2.get("policy") or {}).get("available") is False
|
|
385
|
+
and bool(((doc2.get("policy") or {}).get("error") or {}).get("code")),
|
|
386
|
+
"G6e 策略不可用时 --show-config 仍退出 0 且 available=false + code")
|
|
387
|
+
with _env(MDCG_POLICY_FILE=None):
|
|
388
|
+
rep = audit.policy_report()
|
|
389
|
+
line = mcp_server._policy_stderr_note(rep)
|
|
390
|
+
ok("写入策略:来源=package_default" in line and rep["path"] in line,
|
|
391
|
+
"G6f 启动 stderr 单行报出「来源=包内默认 + 路径」")
|
|
392
|
+
bad = dict(audit.policy_report(), available=False, source="unavailable",
|
|
393
|
+
error={"code": "policy_unavailable", "reason": "哑值", "hint": "哑值"})
|
|
394
|
+
line2 = mcp_server._policy_stderr_note(bad)
|
|
395
|
+
ok("fail-closed" in line2 and "policy_unavailable" in line2,
|
|
396
|
+
"G6g 不可用时启动 stderr 单行点明 fail-closed 与错误码")
|
|
397
|
+
with _env(MDCG_POLICY_FILE=None):
|
|
398
|
+
tmp = tempfile.mkdtemp(prefix="i43_info_")
|
|
399
|
+
try:
|
|
400
|
+
cg = _cg(os.path.join(tmp, "root"))
|
|
401
|
+
info = mcp_server.call_tool(cg, "cg", {"op": "info"})
|
|
402
|
+
wp = info.get("write_policy") or {}
|
|
403
|
+
ok(wp.get("source") == "package_default" and wp.get("available") is True,
|
|
404
|
+
"G6h cg(op=info).write_policy 报出来源(source=%s)" % wp.get("source"))
|
|
405
|
+
finally:
|
|
406
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------- G7 单点与接线
|
|
410
|
+
|
|
411
|
+
def _def_count(src, name):
|
|
412
|
+
import re
|
|
413
|
+
return len(re.findall(r"^def %s\b" % name, src, re.M))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _files_has_policy(files):
|
|
417
|
+
return "data/policy.json" in (files or [])
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _func_body(src, name):
|
|
421
|
+
i = src.index("def %s(" % name)
|
|
422
|
+
j = src.find("\ndef ", i + 1)
|
|
423
|
+
return src[i:(j if j > 0 else len(src))]
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _code_only(body):
|
|
427
|
+
return "\n".join(l for l in body.splitlines() if not l.strip().startswith("#"))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _no_env_in_load(src):
|
|
431
|
+
"""载入面不得自行读 env(来源判定单点在上游 resolve_rulebook)。"""
|
|
432
|
+
return "os.environ" not in _code_only(_func_body(src, "load_rulebook"))
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def g7():
|
|
436
|
+
_group("G7")
|
|
437
|
+
with open(audit.__file__, encoding="utf-8") as f:
|
|
438
|
+
a_src = f.read()
|
|
439
|
+
ok(_def_count(a_src, "resolve_rulebook") == 1
|
|
440
|
+
and _def_count(a_src, "policy_report") == 1
|
|
441
|
+
and _def_count(a_src, "load_rulebook") == 1,
|
|
442
|
+
"G7a 来源解析/自描述/载入在 audit.py 各仅一处实现(单点)")
|
|
443
|
+
ok(_def_count(a_src + "\n\ndef resolve_rulebook():\n pass\n",
|
|
444
|
+
"resolve_rulebook") == 2,
|
|
445
|
+
"G7b 单点判据有判别力(合成负例:加一份副本即计为 2)")
|
|
446
|
+
ok(_no_env_in_load(a_src) is True,
|
|
447
|
+
"G7c load_rulebook 不再自行读 env(来源判定单点在上游 resolve_rulebook)")
|
|
448
|
+
ok(_no_env_in_load(_func_body(a_src, "load_rulebook")
|
|
449
|
+
+ "\n p = os.environ.get('MDCG_POLICY_FILE')\n") is False,
|
|
450
|
+
"G7c' 判据有判别力(合成负例:载入面塞回 env 读取即 False)")
|
|
451
|
+
with open(writepipe.__file__, encoding="utf-8") as f:
|
|
452
|
+
w_src = f.read()
|
|
453
|
+
body = w_src[w_src.index("def _gate_audit"):w_src.index("def _gate_audit") + 4000]
|
|
454
|
+
code = "\n".join(l for l in body.splitlines() if not l.strip().startswith("#"))
|
|
455
|
+
ok("resolve_rulebook" in code and "policy_unavailable" in code,
|
|
456
|
+
"G7d 写入闸门在提案前有策略可用性前置(可执行行内)")
|
|
457
|
+
with open(mcp_server.__file__, encoding="utf-8") as f:
|
|
458
|
+
m_src = f.read()
|
|
459
|
+
ok("_policy_stderr_note(_audit.policy_report())" in m_src,
|
|
460
|
+
"G7e 启动 stderr 接线未断(main → 内容单点)")
|
|
461
|
+
ok('if "--show-config" in sys.argv:' in m_src,
|
|
462
|
+
"G7f --show-config 分派接线未断")
|
|
463
|
+
with open(os.path.join(audit.package_root(), "package.json"), encoding="utf-8") as f:
|
|
464
|
+
pkg = json.load(f)
|
|
465
|
+
ok(_files_has_policy(pkg.get("files")) is True,
|
|
466
|
+
"G7g package.json files 含 data/policy.json(默认策略随包发布)")
|
|
467
|
+
ok(_files_has_policy(["lib", "md_cg"]) is False,
|
|
468
|
+
"G7h 判据有判别力(合成负例:files 不含该项即 False)")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# ---------------------------------------------------------------- G8 键类型闸
|
|
472
|
+
|
|
473
|
+
# 三种标量形态(契约⑦):**合法 JSON 对象**,但列表型键取标量。
|
|
474
|
+
# str 形态的取值刻意取一个「按字符拆开就一定会命中普通文本」的串:旧行为下
|
|
475
|
+
# `_rule_check` 会以单字符规则判定(count 与判定双失真),是本形态的病态点。
|
|
476
|
+
_STRV = "sk-" + ("Zz9" * 8)
|
|
477
|
+
_SCALARS = (
|
|
478
|
+
("int", {"forbidden": 1, "required": ["^# 功能名"]}),
|
|
479
|
+
("bool", {"forbidden": True, "required": ["^# 功能名"]}),
|
|
480
|
+
("str", {"forbidden": _STRV, "required": ["^# 功能名"]}),
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _policy_file(tmp, name, obj):
|
|
485
|
+
p = os.path.join(tmp, name + ".json")
|
|
486
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
487
|
+
json.dump(obj, f, ensure_ascii=False)
|
|
488
|
+
return p
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# 生效条件:无条件调用 audit.policy_report(),不抛到调用方——返回 (报告 dict 或 None, 异常类型名或 None),供「恒不抛」类断言取证用。
|
|
492
|
+
def _pr_safe():
|
|
493
|
+
try:
|
|
494
|
+
return audit.policy_report(), None
|
|
495
|
+
except Exception as exc: # noqa: BLE001
|
|
496
|
+
return None, type(exc).__name__
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# 生效条件:以 sys.argv=["--show-config"] 在本进程内跑 mcp_server.main(),捕获其 stdout 后还原 argv;返回 (退出码, 解析出的 doc 或 {})。
|
|
500
|
+
def _show_config_doc():
|
|
501
|
+
argv = sys.argv
|
|
502
|
+
buf = io.StringIO()
|
|
503
|
+
try:
|
|
504
|
+
sys.argv = [argv[0], "--show-config"]
|
|
505
|
+
with contextlib.redirect_stdout(buf):
|
|
506
|
+
rc = mcp_server.main()
|
|
507
|
+
finally:
|
|
508
|
+
sys.argv = argv
|
|
509
|
+
try:
|
|
510
|
+
doc = json.loads(buf.getvalue().strip().splitlines()[-1])
|
|
511
|
+
except (ValueError, IndexError):
|
|
512
|
+
doc = {}
|
|
513
|
+
return rc, doc
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# 真子进程启动面的隔离注入面(与 N225 守卫同款):剔净全部 MDCG_* 注入,只留
|
|
517
|
+
# 指向临时目录的覆盖键——子进程绝不读真实 ~/.mdcg、真实令牌库与真实密钥面。
|
|
518
|
+
_SPAWN_DIRTY = (
|
|
519
|
+
"MDCG_TOKEN", "MDCG_TOKEN_FILE", "MDCG_MASTER_KEY", "MDCG_CLEARANCE",
|
|
520
|
+
"MDCG_TENANT", "MDCG_TENANT_REGISTRY",
|
|
521
|
+
"MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_LEGACY_ENV_AUTH",
|
|
522
|
+
"MDCG_LEGACY_ENV_ADMIN", "MDCG_SESSION", "DSH_SESSION_ID",
|
|
523
|
+
"MDCG_HARNESS", "MDCG_UNIT", "MDCG_ROOT", "MDCG_STATE_ROOT",
|
|
524
|
+
"MDCG_DATA_ROOT", "MDCG_AUX_ROOT", "MDCG_SUSTAIN", "MDCG_SUSTAIN_NAME",
|
|
525
|
+
"MDCG_MCP_SURFACE", "MDCG_TOOL_FACE", "MDCG_ACTOR",
|
|
526
|
+
"MDCG_VERIFIER_MODULES", "MDCG_HIVE_JOBS", "MDCG_POLICY_FILE",
|
|
527
|
+
"PYTHONUTF8", "PYTHONIOENCODING", "PYTHONLEGACYWINDOWSSTDIO",
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# 生效条件:tmp 为本次隔离根时,返回剔净 _SPAWN_DIRTY 后的 os.environ 副本再注入指向 tmp 的覆盖键(MDCG_ROOT / TENANT_REGISTRY / STATE_ROOT / DATA_ROOT / AUX_ROOT / TOKEN_FILE / MASTER_KEY / SUSTAIN=0 / LEGACY_ENV_AUTH=1 / ACTOR)——子进程零令牌、不落真实数据根。
|
|
532
|
+
def _spawn_env(tmp):
|
|
533
|
+
env = {k: v for k, v in os.environ.items() if k not in _SPAWN_DIRTY}
|
|
534
|
+
env["MDCG_ROOT"] = os.path.join(tmp, "cgroot")
|
|
535
|
+
env["MDCG_TENANT_REGISTRY"] = os.path.join(tmp, "_tenants_absent.json")
|
|
536
|
+
env["MDCG_STATE_ROOT"] = os.path.join(tmp, "state")
|
|
537
|
+
env["MDCG_DATA_ROOT"] = os.path.join(tmp, "data")
|
|
538
|
+
env["MDCG_AUX_ROOT"] = os.path.join(tmp, "auxroot") # 不可取名 "aux"(保留设备名)
|
|
539
|
+
env["MDCG_TOKEN_FILE"] = os.path.join(tmp, "tokens.json")
|
|
540
|
+
env["MDCG_MASTER_KEY"] = "00" * 32
|
|
541
|
+
env["MDCG_SUSTAIN"] = "0" # 关常驻循环
|
|
542
|
+
env["MDCG_LEGACY_ENV_AUTH"] = "1" # 既有豁免面:零令牌
|
|
543
|
+
env["MDCG_ACTOR"] = "i43-guard"
|
|
544
|
+
return env
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
# 当前生效的跨进程变异(仅 --mutation-baseline 期间非 None):内存 patch 不跨
|
|
548
|
+
# 进程,G8-D 走独立子进程 ⇒ 靠 sitecustomize 把同一处定点变异注入子进程。
|
|
549
|
+
_MUTATION = [None]
|
|
550
|
+
|
|
551
|
+
_SITE_SRC = '''# i43 守卫跨进程变异注入(临时件,随 tmp 目录一并回收)
|
|
552
|
+
import os
|
|
553
|
+
import sys
|
|
554
|
+
import inspect
|
|
555
|
+
_repo = os.environ.get("I43_REPO") or ""
|
|
556
|
+
if _repo and _repo not in sys.path:
|
|
557
|
+
sys.path.insert(0, _repo)
|
|
558
|
+
try:
|
|
559
|
+
import md_cg.audit as _A
|
|
560
|
+
_fname = os.environ["I43_MUT_FUNC"]
|
|
561
|
+
_old = os.environ["I43_MUT_OLD"]
|
|
562
|
+
_new = os.environ["I43_MUT_NEW"]
|
|
563
|
+
_src = inspect.getsource(_A.__dict__[_fname])
|
|
564
|
+
exec(compile(_src.replace(_old, _new), "i43_mut.py", "exec"), vars(_A))
|
|
565
|
+
sys.stderr.write("[i43-mut] injected audit.%s\\n" % _fname)
|
|
566
|
+
except Exception as _e:
|
|
567
|
+
sys.stderr.write("[i43-mut] inject FAILED: %r\\n" % (_e,))
|
|
568
|
+
'''
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
class _InjectFailed(Exception):
|
|
572
|
+
"""跨进程变异注入未生效——子进程那半会静默不设防,故 fail-closed 停手。"""
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# 包根(由 __file__ 反推,不经 audit.package_root()——该函数本身是变异靶子
|
|
576
|
+
# M2,走它会让「子进程 cwd / 注入源」随变异漂移)。
|
|
577
|
+
_PKG_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# 生效条件:policy_path 为子进程要加载的策略文件、tmp 为隔离根、extra 为追加的 argv 尾巴(缺省空 = 常驻 server 模式、EOF 结束 stdin)时,spawn `python -X utf8 -m md_cg.mcp_server <extra>`(cwd=包根、env=_spawn_env(tmp)),返回 (退出码, stdout, stderr);仅当 `_MUTATION[0]` 非 None 时另经 sitecustomize 把该变异注入子进程,注入标记缺失即抛 _InjectFailed。
|
|
581
|
+
def _talk(policy_path, tmp, extra=(), timeout=180):
|
|
582
|
+
env = _spawn_env(tmp)
|
|
583
|
+
env["MDCG_POLICY_FILE"] = policy_path
|
|
584
|
+
mut = _MUTATION[0]
|
|
585
|
+
if mut is not None:
|
|
586
|
+
sc_dir = os.path.join(tmp, "_mutpath")
|
|
587
|
+
os.makedirs(sc_dir, exist_ok=True)
|
|
588
|
+
with open(os.path.join(sc_dir, "sitecustomize.py"), "w",
|
|
589
|
+
encoding="utf-8") as f:
|
|
590
|
+
f.write(_SITE_SRC)
|
|
591
|
+
env["PYTHONPATH"] = sc_dir + os.pathsep + env.get("PYTHONPATH", "")
|
|
592
|
+
env["I43_REPO"] = _PKG_ROOT
|
|
593
|
+
env["I43_MUT_FUNC"] = mut["func"]
|
|
594
|
+
env["I43_MUT_OLD"] = mut["old"]
|
|
595
|
+
env["I43_MUT_NEW"] = mut["new"]
|
|
596
|
+
proc = subprocess.Popen(
|
|
597
|
+
[sys.executable, "-X", "utf8", "-m", "md_cg.mcp_server"] + list(extra),
|
|
598
|
+
cwd=_PKG_ROOT, env=env, stdin=subprocess.PIPE,
|
|
599
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
600
|
+
text=True, encoding="utf-8", errors="replace")
|
|
601
|
+
try:
|
|
602
|
+
out, err = proc.communicate("", timeout=timeout)
|
|
603
|
+
except subprocess.TimeoutExpired:
|
|
604
|
+
proc.kill()
|
|
605
|
+
out, err = proc.communicate()
|
|
606
|
+
out, err = out or "", err or ""
|
|
607
|
+
if mut is not None and "[i43-mut] injected" not in err:
|
|
608
|
+
raise _InjectFailed("%s:sitecustomize 注入未生效" % mut["name"])
|
|
609
|
+
return proc.returncode, out, err
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# 生效条件:无入参;对 int/bool/str 三种标量键形态各跑 A-F 七条(诊断面不抛且结构化 / 解析面判不可用 / --show-config 恒退出 0(同进程)/ 真进程启动 rc=0 且来源行未缺失 / 真进程 --show-config rc=0 且 available=false / 写入侧 fail-closed 且全库与 inbox 字节面零正文 / op=info 不被拖崩且 write_policy 报不可用),另跑 G 计数非长度、H 消费面纵深、I 形状闸不误伤合法默认策略共 3 条;断言数 24。
|
|
613
|
+
def g8():
|
|
614
|
+
_group("G8")
|
|
615
|
+
tmp = tempfile.mkdtemp(prefix="i43_ktype_")
|
|
616
|
+
try:
|
|
617
|
+
for tag, obj in _SCALARS:
|
|
618
|
+
path = _policy_file(tmp, tag, obj)
|
|
619
|
+
with _env(MDCG_POLICY_FILE=path):
|
|
620
|
+
rep, exc = _pr_safe()
|
|
621
|
+
perr = (rep or {}).get("error") or {}
|
|
622
|
+
ok(exc is None and isinstance(rep, dict)
|
|
623
|
+
and rep.get("available") is False
|
|
624
|
+
and perr.get("code") == "policy_bad_shape"
|
|
625
|
+
and bool(perr.get("hint")),
|
|
626
|
+
"G8-%s A policy_report 恒不抛且报不可用+结构化(code=%s)"
|
|
627
|
+
% (tag, perr.get("code")))
|
|
628
|
+
rules, src, rerr = audit.resolve_rulebook()
|
|
629
|
+
ok(rules is None and src == "env"
|
|
630
|
+
and (rerr or {}).get("code") == "policy_bad_shape",
|
|
631
|
+
"G8-%s B resolve_rulebook 判不可用且不当规则用(rules=%r)"
|
|
632
|
+
% (tag, rules))
|
|
633
|
+
rc, doc = _show_config_doc()
|
|
634
|
+
pol = doc.get("policy") or {}
|
|
635
|
+
ok(rc == 0 and pol.get("available") is False
|
|
636
|
+
and ((pol.get("error") or {}).get("code")
|
|
637
|
+
== "policy_bad_shape"),
|
|
638
|
+
"G8-%s C --show-config 恒退出 0 且 available=false+code(rc=%s)"
|
|
639
|
+
% (tag, rc))
|
|
640
|
+
rcp, _out, errtxt = _talk(path, tmp)
|
|
641
|
+
ok(rcp == 0 and "code=policy_bad_shape" in errtxt
|
|
642
|
+
and "fail-closed" in errtxt,
|
|
643
|
+
"G8-%s D server 启动 rc=0 且来源行报出 code+fail-closed(rc=%s)"
|
|
644
|
+
% (tag, rcp))
|
|
645
|
+
# 真进程 `--show-config`:缺陷记录里的 rc=1 + stdout 空 就是这一面
|
|
646
|
+
rcc, sout, _serr = _talk(path, tmp, extra=("--show-config",))
|
|
647
|
+
try:
|
|
648
|
+
d2 = json.loads(sout.strip().splitlines()[-1])
|
|
649
|
+
except (ValueError, IndexError):
|
|
650
|
+
d2 = {}
|
|
651
|
+
p2 = d2.get("policy") or {}
|
|
652
|
+
ok(rcc == 0 and p2.get("available") is False
|
|
653
|
+
and ((p2.get("error") or {}).get("code")
|
|
654
|
+
== "policy_bad_shape"),
|
|
655
|
+
"G8-%s C2 真进程 --show-config rc=0 且 available=false+code"
|
|
656
|
+
"(rc=%s stdout=%d 字节)" % (tag, rcc, len(sout)))
|
|
657
|
+
root = os.path.join(tmp, "root_" + tag)
|
|
658
|
+
cg = _cg(root)
|
|
659
|
+
out = _pipe().execute(cg, {
|
|
660
|
+
"node_id": "i43_kt_" + tag, "content_kind": "text",
|
|
661
|
+
"layer": "knowledge",
|
|
662
|
+
"content": "%s 正文 %s\n" % (MARK, SECRET)})
|
|
663
|
+
ib, rb = _inbox_bytes(root), _root_bytes(root)
|
|
664
|
+
ok(out.get("moved_to") == "policy_unavailable"
|
|
665
|
+
and out.get("committed") is False
|
|
666
|
+
and SECRET.encode() not in ib and MARK.encode() not in ib
|
|
667
|
+
and SECRET.encode() not in rb and MARK.encode() not in rb
|
|
668
|
+
and not os.path.exists(os.path.join(
|
|
669
|
+
root, "knowledge", "i43_kt_%s.md" % tag))
|
|
670
|
+
and SECRET not in json.dumps(out, ensure_ascii=False),
|
|
671
|
+
"G8-%s E 写入 fail-closed 且 inbox/全库字节面零正文"
|
|
672
|
+
"(moved_to=%s inbox=%d)"
|
|
673
|
+
% (tag, out.get("moved_to"), len(ib)))
|
|
674
|
+
# 第三处调用面(op=info):畸形策略不得把 info 拖崩、也不得
|
|
675
|
+
# 让 write_policy 缺项(诊断面三处调用兜底一并钉死)。
|
|
676
|
+
info = mcp_server.call_tool(cg, "cg", {"op": "info"})
|
|
677
|
+
wp = info.get("write_policy") or {}
|
|
678
|
+
ok(wp.get("available") is False
|
|
679
|
+
and ((wp.get("error") or {}).get("code")
|
|
680
|
+
== "policy_bad_shape")
|
|
681
|
+
and bool(info.get("surface")),
|
|
682
|
+
"G8-%s F op=info 仍返回正常载荷且 write_policy 报不可用+code"
|
|
683
|
+
% tag)
|
|
684
|
+
with _env(MDCG_POLICY_FILE=_policy_file(tmp, "str", dict(_SCALARS)["str"])):
|
|
685
|
+
rep_f, _e = _pr_safe()
|
|
686
|
+
ok(rep_f.get("forbidden") == 0 and rep_f.get("forbidden") != len(_STRV)
|
|
687
|
+
and rep_f.get("available") is False,
|
|
688
|
+
"G8-Str G 字符串键未被拆成单字符规则(计数=%s,非长度 %d)"
|
|
689
|
+
% (rep_f.get("forbidden"), len(_STRV)))
|
|
690
|
+
naive = len([r for r in ({"forbidden": _STRV}.get("forbidden") or []) if r])
|
|
691
|
+
st, ev, _det = audit._rule_check("s", {"forbidden": _STRV,
|
|
692
|
+
"required": []}, "text")
|
|
693
|
+
ok(st != audit.REJECT and "命中禁止规则" not in str(ev)
|
|
694
|
+
and naive == len(_STRV),
|
|
695
|
+
"G8-Str H 消费面纵深:直喂标量 rules 不按字符建规则(旧写法同输入 ⇒ "
|
|
696
|
+
"%d 条单字符规则)" % naive)
|
|
697
|
+
with open(audit.default_policy_path(), encoding="utf-8") as f:
|
|
698
|
+
dflt = json.load(f)
|
|
699
|
+
ok(audit._policy_shape_error(dflt) is None
|
|
700
|
+
and audit._policy_shape_error({"forbidden": 1}) is not None
|
|
701
|
+
and audit._policy_shape_error({"required": True}) is not None
|
|
702
|
+
and audit._policy_shape_error({"forbidden": _STRV}) is not None
|
|
703
|
+
and audit._policy_shape_error({"required_labels": {"a": 1}}) is not None,
|
|
704
|
+
"G8-I 形状闸不误伤合法默认策略(_comment/_note 是字符串)且标量/对象取值一律报错")
|
|
705
|
+
finally:
|
|
706
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# ---------------------------------------------------------------- 运行器
|
|
710
|
+
|
|
711
|
+
_GROUPS = (g1, g2, g3, g4, g5, g6, g7, g8)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _run_all_silent():
|
|
715
|
+
"""静默跑全部组,返回红项清单 [(组名, 断言名)](供变异核验复用)。
|
|
716
|
+
|
|
717
|
+
跨进程注入失效(_InjectFailed)**不吞**:子进程那半会静默不设防,必须让
|
|
718
|
+
变异核验 fail-closed 停手(见 _mutation_baseline)。
|
|
719
|
+
"""
|
|
720
|
+
_PASS.clear()
|
|
721
|
+
_FAIL.clear()
|
|
722
|
+
_RED.clear()
|
|
723
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
724
|
+
for g in _GROUPS:
|
|
725
|
+
try:
|
|
726
|
+
g()
|
|
727
|
+
except _InjectFailed:
|
|
728
|
+
raise
|
|
729
|
+
except Exception as exc: # noqa: BLE001
|
|
730
|
+
_RED.append((_GROUP[0], "组异常:%s: %s" % (type(exc).__name__, exc)))
|
|
731
|
+
return list(_RED)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# 定点变异表:逐条变异生产实现的**源码锚点**,套件必须转红且**恰好**命中声明条数。
|
|
735
|
+
# (锚点 = 实现里的字面量;实现改了而本表未同步 → 报 ANCHOR-MISS,退出码 2。)
|
|
736
|
+
_MUTATIONS = (
|
|
737
|
+
("M1 删掉包内默认回落", "audit", "resolve_rulebook",
|
|
738
|
+
" dflt = default_policy_path()",
|
|
739
|
+
' dflt = os.path.join(package_root(), "data", "_mutated_missing.json")',
|
|
740
|
+
13, "未设 env 只剩 fail-closed → G1 全 6 条 + G2c + G5a/d/e + G6b/f/h"
|
|
741
|
+
"(G5b/c/f 反而不红:零正文在 fail-closed 下仍成立,正确)"),
|
|
742
|
+
("M2 定位改为依赖 cwd", "audit", "package_root",
|
|
743
|
+
" return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))",
|
|
744
|
+
" return os.getcwd()",
|
|
745
|
+
3, "包根随 cwd 漂移 → G2 全组"),
|
|
746
|
+
("M3 闸门取消策略前置", "writepipe", "_gate_audit",
|
|
747
|
+
" if perr is not None:",
|
|
748
|
+
" if False:",
|
|
749
|
+
9, "不可用也继续走旧路(DEFER→propose)→ G3a/b/c/e 与 G4g/h + G8-E×3"
|
|
750
|
+
"(键类型面:int/bool 抛 TypeError 被兜底 except 吞成 DEFER→review_queue,"
|
|
751
|
+
"str 按字符 REJECT)(G3d/f 不红:propose 不落节点、响应体不带正文)"),
|
|
752
|
+
("M4 显式坏路径回落包内默认", "audit", "resolve_rulebook",
|
|
753
|
+
' if rules is None:\n return None, "env", {"code": why[0], "path": explicit,\n "reason": why[1], "hint": POLICY_HINT}',
|
|
754
|
+
' if rules is None:\n _dr, _dw = _read_policy_file(default_policy_path())\n if _dr is not None:\n return _dr, "package_default", None\n return None, "env", {"code": why[0], "path": explicit,\n "reason": why[1], "hint": POLICY_HINT}',
|
|
755
|
+
31, "坏路径被包内默认掩蔽 → G4a/b/c/e×2/f/g/h + G6e + G8-A/B/C/E/F×3 + G8-C2×3"
|
|
756
|
+
"(真进程 --show-config) + G8-D×3"
|
|
757
|
+
"(注入子进程后启动面同样变可用)+ G8-G(坏路径面变可用)"
|
|
758
|
+
"(G4d 不红:空 dict 仍走显式分支成功;G8-H/I 不红:直调判据不经来源面)"),
|
|
759
|
+
("M5 策略读取返回空规则", "audit", "_read_policy_file",
|
|
760
|
+
" return rules, None",
|
|
761
|
+
" return {}, None",
|
|
762
|
+
12, "策略文件在但规则被掏空 → G1b/c/d/e/f + G2c + G5 全 6 条"
|
|
763
|
+
"(G1a 不红:来源仍是包内默认,掏空的是内容;G8 不红:形状闸在"
|
|
764
|
+
"该行之前就返回,畸形面走不到这里)"),
|
|
765
|
+
("M6 启动 stderr 行置空", "mcp_server", "_policy_stderr_note",
|
|
766
|
+
" rep = rep or {}\n",
|
|
767
|
+
' rep = rep or {}\n return ""\n',
|
|
768
|
+
2, "来源不可见 → G6f/g"),
|
|
769
|
+
("M7 --show-config 不输出", "mcp_server", "_show_config",
|
|
770
|
+
' sys.stdout.write(json.dumps(doc, ensure_ascii=False) + "\\n")',
|
|
771
|
+
" pass",
|
|
772
|
+
8, "诊断面哑掉 → G6a/b/c/d/e(a 断言有输出、b/d 解析空 doc、c 判据比对失败)"
|
|
773
|
+
"+ G8-C×3(键类型面同样读不到 available/code)"),
|
|
774
|
+
("M8 info 不报策略", "mcp_server", "_cg_dispatch",
|
|
775
|
+
' h["write_policy"] = audit.policy_report()',
|
|
776
|
+
" pass",
|
|
777
|
+
4, "工具面来源不可见 → G6h + G8-F×3(info 面的 write_policy 键被摘掉)"),
|
|
778
|
+
# —— G8 键类型闸两处定点变异(复核 DEFER 的实据面)——
|
|
779
|
+
("M9 形状闸失效(键类型面回退)", "audit", "_policy_shape_error",
|
|
780
|
+
" if key in rules and not isinstance(rules[key], (list, tuple)):",
|
|
781
|
+
" if False:",
|
|
782
|
+
23, "标量键重新被当可用策略 → G8-A/B/C/E/F×3 + G8-C2×3(诊断面报错码/可用性、解析面、"
|
|
783
|
+
"写入面退回 DEFER→review_queue 或按字符 REJECT、info 面码不对)"
|
|
784
|
+
"+ G8-D×3(经 sitecustomize 注入子进程:int/bool 走 policy_report 兜底 ⇒ "
|
|
785
|
+
"来源行 code 变 policy_report_failed、str 直接报可用)+ G8-G + G8-I"
|
|
786
|
+
"(G8-H 不红:直调消费面不经形状闸)"),
|
|
787
|
+
("M10 列表取值不做类型闸", "audit", "_policy_list",
|
|
788
|
+
" return list(v) if isinstance(v, (list, tuple)) else []",
|
|
789
|
+
" return list(v) if isinstance(v, (list, tuple, str)) else []",
|
|
790
|
+
1, "字符串取值被按字符当规则 → G8-H(消费面纵深那条;其余条不红:"
|
|
791
|
+
"形状闸在读取单点已把畸形策略判为不可用,正常路径下消费面碰不到标量)"),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _mutation_baseline() -> int:
|
|
796
|
+
"""逐条定点变异核验:套件必须转红且恰好命中声明条数;锚点漂移 → 退出码 2。
|
|
797
|
+
|
|
798
|
+
锚点漂移报 ANCHOR-MISS(2);子进程注入未生效报 INJECT-MISS(2)——两者都是
|
|
799
|
+
fail-closed 停手:前者说明「判据打不红」,后者说明「子进程那半静默不设防」。
|
|
800
|
+
"""
|
|
801
|
+
print("!! 变异自证模式:逐条定点变异,套件必须按声明条数转红\n")
|
|
802
|
+
clean = _run_all_silent()
|
|
803
|
+
print(" 未变异基线:红项=%d" % len(clean))
|
|
804
|
+
if clean:
|
|
805
|
+
print(" FAIL 未变异即红:", clean)
|
|
806
|
+
return 1
|
|
807
|
+
mods = {"audit": audit, "writepipe": writepipe, "mcp_server": mcp_server}
|
|
808
|
+
bad = []
|
|
809
|
+
for name, modname, fname, old, new, expect, why in _MUTATIONS:
|
|
810
|
+
mod = mods[modname]
|
|
811
|
+
backup = getattr(mod, fname)
|
|
812
|
+
src = inspect.getsource(backup)
|
|
813
|
+
if old not in src:
|
|
814
|
+
print(" ANCHOR-MISS %s —— 变异锚点漂移(实现改了却没同步本表)" % name)
|
|
815
|
+
return 2
|
|
816
|
+
exec(compile(src.replace(old, new), "<mut:%s>" % name, "exec"), vars(mod))
|
|
817
|
+
# 内存 patch 不跨进程:audit 面变异另注入 sitecustomize,G8-D(真子进程
|
|
818
|
+
# 启动)才有判别力;其余模块(mcp_server/writepipe)不在本守卫的子进程
|
|
819
|
+
# 判据面上,故不注入(避免把「注入」本身变成噪声源)。
|
|
820
|
+
_MUTATION[0] = ({"name": name, "func": fname, "old": old, "new": new}
|
|
821
|
+
if modname == "audit" else None)
|
|
822
|
+
try:
|
|
823
|
+
red = _run_all_silent()
|
|
824
|
+
except _InjectFailed as exc:
|
|
825
|
+
print(" INJECT-MISS %s —— %s(子进程那半未设防,fail-closed 停手)"
|
|
826
|
+
% (name, exc))
|
|
827
|
+
return 2
|
|
828
|
+
finally:
|
|
829
|
+
_MUTATION[0] = None
|
|
830
|
+
setattr(mod, fname, backup)
|
|
831
|
+
hit = len(red) == expect
|
|
832
|
+
print(" %s → 红 %d/%d %s(%s)"
|
|
833
|
+
% (name, len(red), expect, "OK" if hit else "** 与声明不符 **", why))
|
|
834
|
+
if not hit:
|
|
835
|
+
for g, n in red:
|
|
836
|
+
print(" 红: [%s] %s" % (g, n))
|
|
837
|
+
bad.append(name)
|
|
838
|
+
print("\n变异自证:%s" % ("PASS(每条变异都恰好命中声明条数)" if not bad
|
|
839
|
+
else "FAIL —— " + "、".join(bad)))
|
|
840
|
+
return 0 if not bad else 1
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def main() -> int:
|
|
844
|
+
if "--mutation-baseline" in sys.argv:
|
|
845
|
+
return _mutation_baseline()
|
|
846
|
+
cwd = os.getcwd()
|
|
847
|
+
tmp = tempfile.mkdtemp(prefix="i43_main_")
|
|
848
|
+
try:
|
|
849
|
+
# 统一在包根下运行:G1/G5 的「包内默认」与 cwd 无关的前提被显式固定,
|
|
850
|
+
# G2 再 chdir 到别处证明「不依赖 cwd」(否则断言随调用方 cwd 飘)。
|
|
851
|
+
os.chdir(audit.package_root())
|
|
852
|
+
for g in _GROUPS:
|
|
853
|
+
try:
|
|
854
|
+
g()
|
|
855
|
+
except Exception as exc: # noqa: BLE001
|
|
856
|
+
ok(False, "%s 组异常:%s: %s" % (g.__name__, type(exc).__name__, exc))
|
|
857
|
+
finally:
|
|
858
|
+
os.chdir(cwd)
|
|
859
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
860
|
+
print("\nissue #43 默认策略守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
|
|
861
|
+
return 0 if not _FAIL else 1
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
if __name__ == "__main__":
|
|
865
|
+
sys.exit(main())
|