@furongjun1999/dsh-memory 0.6.0 → 0.6.1
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 +588 -572
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -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/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//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/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 +20 -0
- 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 +12 -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 +21 -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//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//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_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +1 -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/fsutil.py +454 -8
- 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 +4152 -3818
- package/md_cg/mdcg.py +4208 -3547
- package/md_cg/mdcos.py +4491 -4159
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +639 -575
- 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/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 +1299 -1168
- package/md_cg/tasks.py +469 -470
- 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_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_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1377 -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 +513 -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_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_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- 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 +516 -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_server_version.py +67 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_token_lowercase_form.py +315 -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/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +3 -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/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""令牌形态「只认小写」的守卫(批次79,2026-09-28 使用者裁定)。
|
|
3
|
+
|
|
4
|
+
运行:python -X utf8 -m md_cg.test_token_lowercase_form # 正常跑
|
|
5
|
+
python -X utf8 -m md_cg.test_token_lowercase_form --head-baseline # 判别力自证
|
|
6
|
+
|
|
7
|
+
裁定原文:「不认大写。统一只认小写。」落点=**受理面单点** `tokens.parse_token`
|
|
8
|
+
(`verify_token` 的唯一入口)。改动前该函数返回 `role.lower()`、且对 token_id 不做
|
|
9
|
+
任何形态检查——即受理面比识别面**更宽**:`md_cg` 自己的检测表
|
|
10
|
+
(`data/policy.json` forbidden #9 `\\btk_[0-9a-f]{8,}\\b`、`src/hooks.ts`
|
|
11
|
+
SENSITIVE_PATTERNS 的同形规则)**有意**不含 `A-F`,而受理面却会接受一个含大写的
|
|
12
|
+
token_id——检测面漏过的形态照样能通过校验,两面的松紧就倒挂了。
|
|
13
|
+
|
|
14
|
+
本套断言分七组:
|
|
15
|
+
G1 放行面(真令牌通过;secret 含大写仍通过——base64url 必含大写,不得误伤)
|
|
16
|
+
G2 token_id 含大写 → 拒(`TK_` 前缀、全大写 hex、混合大小写 id 三种)
|
|
17
|
+
G3 role 含大写 → 拒(改动前 role.lower() 会**静默接受**)
|
|
18
|
+
G4 前缀含大写 → 拒(既有判据,钉住它别被后续重构放松)
|
|
19
|
+
G5 端到端(真临时令牌库:合法令牌过闸;同 secret 的大写变形令牌抛 TokenError,
|
|
20
|
+
而不是落到「令牌不存在」这类下游软失败上——形态问题必须在形态层拒掉)
|
|
21
|
+
G6 生成面自证(**推论前提**:`issue` 只产小写 role/token_id ⇒ 大写形态没有合法来源)
|
|
22
|
+
G7 口径面(识别面**仍然**宽:禁表两条原样存在、hooks.ts 的 role 段仍 `[A-Za-z]+`;
|
|
23
|
+
受理面已窄。二者关系=「识别面宽、受理面窄」,不是「两面都收紧」)
|
|
24
|
+
G8 三面口径(2026-09-28 使用者补裁「大写也转小写识别,以后写统一小写,现有的大写
|
|
25
|
+
内容就不动了」):入参面宽容(role_spec/as_unit 的大写照旧归一,收窄只减不增)、
|
|
26
|
+
令牌面从严(同为大写 role 的令牌字符串仍被拒——两面**有意**不同)、
|
|
27
|
+
存量不迁移(test_p46 C2 那条存量断言必须还在)、写面统一小写(issue 产出恒小写)
|
|
28
|
+
|
|
29
|
+
**红基线自证(--head-baseline)**:把 `parse_token` 临时换回改动前的实现
|
|
30
|
+
(`return role.lower(), token_id, secret`),G2/G3/G5/G8d 必须转红(实测 11 红)。若红
|
|
31
|
+
基线不红,说明这几条断言没有判别力(删掉新分支也照样全绿)。
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import contextlib
|
|
36
|
+
import io
|
|
37
|
+
import json
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
import secrets
|
|
41
|
+
import shutil
|
|
42
|
+
import sys
|
|
43
|
+
import tempfile
|
|
44
|
+
|
|
45
|
+
from . import tokens
|
|
46
|
+
from .tokens import ROLE_SPECS, TokenError
|
|
47
|
+
|
|
48
|
+
_PASS = []
|
|
49
|
+
_FAIL = []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def ok(cond, msg, extra=""):
|
|
53
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
54
|
+
print((" PASS " if cond else " FAIL ") + msg + ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _rejects(token: str, want: str = "") -> tuple:
|
|
58
|
+
"""调 parse_token 判拒:返回 (是否被拒, 错误文本)。"""
|
|
59
|
+
try:
|
|
60
|
+
tokens.parse_token(token)
|
|
61
|
+
return False, ""
|
|
62
|
+
except TokenError as e:
|
|
63
|
+
t = str(e)
|
|
64
|
+
return (want in t), t
|
|
65
|
+
except Exception as e: # noqa: BLE001
|
|
66
|
+
return False, "%s: %s" % (type(e).__name__, e)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------- 旧实现(红基线用)
|
|
70
|
+
def _old_parse_token(token: str):
|
|
71
|
+
"""改动前的实现原文——仅用于 --head-baseline 的判别力自证,不参与生产。"""
|
|
72
|
+
parts = (token or "").strip().split(".")
|
|
73
|
+
if len(parts) != 4 or parts[0] != tokens.PREFIX:
|
|
74
|
+
raise TokenError("令牌格式非法(应为 mdcg1.<role>.<token_id>.<secret>)")
|
|
75
|
+
_, role, token_id, secret = parts
|
|
76
|
+
if not role or not token_id or not secret:
|
|
77
|
+
raise TokenError("令牌字段缺失")
|
|
78
|
+
return role.lower(), token_id, secret
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# 合成件:secret 刻意含大写(与 token_urlsafe 的真实产物同分布),用来把「只认小写」
|
|
82
|
+
# 的适用面钉在 role/token_id 两段上,而不是顺手把 secret 也收紧。值里带 `dummy`/`fake`
|
|
83
|
+
# 标记是**发版门禁的既有约定**(scripts/check_publish_artifact.py::FAKE_TOKEN_MARKERS):
|
|
84
|
+
# 测试里的合成凭据必须一眼可辨为假值,否则 R1 凭据面会把本文件判成真实泄露(本批次首跑
|
|
85
|
+
# 实测命中 APIKEY_LITERAL + TOKEN_MDCG 两条,即按该约定改正而成现在这样)。
|
|
86
|
+
DUMMY_SECRET_UC = "Fake-Dummy-aB3xY9zQ7wV1uT5sR2pN8mL4kJ6"
|
|
87
|
+
HEX_ID = "9f3a2b8c7d4e" # token_hex(6) 形态
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _tok(role: str, tid: str, secret: str = None) -> str:
|
|
91
|
+
"""拼一条令牌:经 make_token 而非字面量——测试源码里不留「像真凭据」的整串。"""
|
|
92
|
+
return tokens.make_token(role, tid, secret or DUMMY_SECRET_UC)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def g1_allow():
|
|
96
|
+
print("\n[G1] 放行面:合法令牌与含大写 secret 的令牌都通过")
|
|
97
|
+
good = _tok("designer", "tk_" + HEX_ID)
|
|
98
|
+
role, token_id, secret = tokens.parse_token(good)
|
|
99
|
+
ok(role == "designer" and token_id == "tk_" + HEX_ID and secret == DUMMY_SECRET_UC,
|
|
100
|
+
"G1a 合法令牌原样通过(role/token_id/secret 都不改写)")
|
|
101
|
+
ok(secret == DUMMY_SECRET_UC and any(c.isupper() for c in secret),
|
|
102
|
+
"G1b secret 含大写仍然通过(base64url 必含大写,不得误伤)",
|
|
103
|
+
"样例 secret 大写字母数=%d" % sum(1 for c in secret if c.isupper()))
|
|
104
|
+
ok(tokens.parse_token(_tok("guest", "tk_" + "0123456789ab"))[0] == "guest",
|
|
105
|
+
"G1c 另一角色同样通过(避免只对 designer 生效的伪断言)")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def g2_id_uppercase():
|
|
109
|
+
print("\n[G2] token_id 含大写 → 拒(受理面比识别面宽的那个洞)")
|
|
110
|
+
for label, tid in (("TK_ 前缀大写", "TK_" + HEX_ID),
|
|
111
|
+
("全大写 hex", "tk_" + HEX_ID.upper()),
|
|
112
|
+
("混合大小写", "tk_9f3a2B8c7d4e")):
|
|
113
|
+
hit, err = _rejects(_tok("designer", tid), want="小写")
|
|
114
|
+
ok(hit, "G2 token_id %s → 拒且文案点明「小写」" % label, err)
|
|
115
|
+
ok(tokens.parse_token(_tok("designer", "tk_" + HEX_ID))[1] == "tk_" + HEX_ID,
|
|
116
|
+
"G2 正对照:同一形态换成小写即通过(拒的是大小写,不是 tk_ 前缀)")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def g3_role_uppercase():
|
|
120
|
+
print("\n[G3] role 含大写 → 拒(改动前 role.lower() 静默接受)")
|
|
121
|
+
for label, role in (("全大写", "DESIGNER"), ("首字母大写", "Designer"),
|
|
122
|
+
("别名大写", "VERIFIER")):
|
|
123
|
+
hit, err = _rejects(_tok(role, "tk_" + HEX_ID), want="小写")
|
|
124
|
+
ok(hit, "G3 role %s → 拒且文案点明「小写」" % label, err)
|
|
125
|
+
ok(tokens.parse_token(_tok("designer", "tk_" + HEX_ID))[0] == "designer",
|
|
126
|
+
"G3 正对照:全小写 role 通过且不被改写")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def g4_prefix():
|
|
130
|
+
print("\n[G4] 前缀含大写 → 拒(既有判据,钉住别被后续重构放松)")
|
|
131
|
+
hit, err = _rejects(_tok("designer", "tk_" + HEX_ID).replace("mdcg1", "MDCG1", 1), want="格式非法")
|
|
132
|
+
ok(hit, "G4a 前缀 MDCG1 → 拒", err)
|
|
133
|
+
hit2, err2 = _rejects(_tok("designer", "tk_" + HEX_ID).replace("mdcg1", "Mdcg1", 1), want="格式非法")
|
|
134
|
+
ok(hit2, "G4b 前缀 Mdcg1 → 拒", err2)
|
|
135
|
+
ok(tokens.PREFIX == tokens.PREFIX.lower() and tokens.PREFIX == "mdcg1",
|
|
136
|
+
"G4c PREFIX 常量本身即小写(前缀大小写敏感的前提)", tokens.PREFIX)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def g5_end_to_end():
|
|
140
|
+
print("\n[G5] 端到端:真令牌库上,合法令牌过闸、大写变形令牌在形态层即拒")
|
|
141
|
+
root = tempfile.mkdtemp(prefix="mdcg_toklower_")
|
|
142
|
+
tf = os.path.join(root, "_tokens.json")
|
|
143
|
+
try:
|
|
144
|
+
rec = tokens.issue("designer", path=tf)
|
|
145
|
+
tok = rec["token"]
|
|
146
|
+
p = tokens.verify_token(tok, path=tf)
|
|
147
|
+
ok(p.role == "designer", "G5a 合法令牌 verify_token 通过(role=%s)" % p.role)
|
|
148
|
+
role, tid, sec = tokens.parse_token(tok)
|
|
149
|
+
up_id = tokens.make_token(role, tid.upper(), sec)
|
|
150
|
+
try:
|
|
151
|
+
tokens.verify_token(up_id, path=tf)
|
|
152
|
+
ok(False, "G5b 大写 token_id 的变形令牌必须被拒", "竟然通过了")
|
|
153
|
+
except TokenError as e:
|
|
154
|
+
ok("小写" in str(e), "G5b 大写 token_id 变形 → TokenError 且文案指向形态(非下游软失败)", str(e))
|
|
155
|
+
up_role = tokens.make_token(role.upper(), tid, sec)
|
|
156
|
+
try:
|
|
157
|
+
tokens.verify_token(up_role, path=tf)
|
|
158
|
+
ok(False, "G5c 大写 role 的变形令牌必须被拒", "竟然通过了")
|
|
159
|
+
except TokenError as e:
|
|
160
|
+
ok("小写" in str(e), "G5c 大写 role 变形 → TokenError 同上", str(e))
|
|
161
|
+
msg = _reject_text(up_id)
|
|
162
|
+
ok("小写" in msg and "不存在" not in msg and "不匹配" not in msg,
|
|
163
|
+
"G5d 拒因是形态层(不含「令牌不存在/密钥不匹配」这类下游软失败文案)", msg)
|
|
164
|
+
finally:
|
|
165
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _reject_text(token: str) -> str:
|
|
169
|
+
"""取原始错误文本(不做 must-contain 判据),供 G5d 反向核对。"""
|
|
170
|
+
try:
|
|
171
|
+
tokens.parse_token(token)
|
|
172
|
+
return "(未被拒)"
|
|
173
|
+
except TokenError as e:
|
|
174
|
+
return str(e)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def g6_issuer():
|
|
178
|
+
print("\n[G6] 生成面自证:大写形态没有合法来源(推论的**前提**)")
|
|
179
|
+
hexes = [secrets.token_hex(6) for _ in range(64)]
|
|
180
|
+
ok(all(re.fullmatch(r"[0-9a-f]{12}", h) for h in hexes),
|
|
181
|
+
"G6a secrets.token_hex(6) 只产小写 hex(64 次采样零违例)",
|
|
182
|
+
[h for h in hexes if not re.fullmatch(r"[0-9a-f]{12}", h)][:3])
|
|
183
|
+
ok(all(k == k.lower() for k in ROLE_SPECS),
|
|
184
|
+
"G6b ROLE_SPECS 全部键为小写(签发面 role 取值域即小写)", sorted(ROLE_SPECS))
|
|
185
|
+
# 签发面另有一层归一:issue 经 normalize_role 把小写化做在「我方自己发」这一侧。
|
|
186
|
+
# 这不算「认大写」——产出物恒为小写;受理面收的是外部来的串,没有这层信任。
|
|
187
|
+
# 结构断言(不写整串字面量:测试源码里不留「像真凭据」的串,见 DUMMY_SECRET_UC 处注释)
|
|
188
|
+
ok(tokens.make_token("designer", "tk_" + HEX_ID, DUMMY_SECRET_UC).split(".") ==
|
|
189
|
+
[tokens.PREFIX, "designer", "tk_" + HEX_ID, DUMMY_SECRET_UC],
|
|
190
|
+
"G6c make_token 是纯拼接(四段原样,不在拼接层做大小写变换)")
|
|
191
|
+
root = tempfile.mkdtemp(prefix="mdcg_toklower_iss_")
|
|
192
|
+
try:
|
|
193
|
+
tok = tokens.issue("DESIGNER", path=os.path.join(root, "_tokens.json"))["token"]
|
|
194
|
+
ok(tok.startswith("mdcg1.designer."),
|
|
195
|
+
"G6d 即便调用方传大写 role,issue 产出仍是小写(签发面归一生效处)", tok[:24])
|
|
196
|
+
finally:
|
|
197
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _hooks_rules() -> list:
|
|
201
|
+
"""从 src/hooks.ts 源码里取出现行的 `/…/g` 字面(JS 与 Python 在这些构造上同义)。"""
|
|
202
|
+
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
203
|
+
src = io.open(os.path.join(repo, "src", "hooks.ts"), encoding="utf-8").read().splitlines()
|
|
204
|
+
out = []
|
|
205
|
+
for i, line in enumerate(src, 1):
|
|
206
|
+
m = re.search(r"re:\s*/(.+)/[a-z]*\s*,", line)
|
|
207
|
+
if m:
|
|
208
|
+
out.append((i, m.group(1), line))
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def g7_detection_face():
|
|
213
|
+
print("\n[G7] 口径面:识别面**仍然**宽、受理面已窄(不是两面都收紧)")
|
|
214
|
+
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
215
|
+
pol = json.loads(io.open(os.path.join(repo, "data", "policy.json"), encoding="utf-8").read())
|
|
216
|
+
forb = pol["forbidden"]
|
|
217
|
+
vals = list(forb.values()) if isinstance(forb, dict) else list(forb)
|
|
218
|
+
ok(any(v.strip() == r"\btk_[0-9a-f]{8,}\b" for v in vals),
|
|
219
|
+
"G7a 禁表 id 规则原样保留(未顺手加 A-F / re.I——那会把「只认小写」变成误报源)")
|
|
220
|
+
ok(any("mdcg1" in v for v in vals),
|
|
221
|
+
"G7b 禁表全形态令牌规则原样保留")
|
|
222
|
+
ok(all("(?i)" not in v for v in vals if "tk_" in v),
|
|
223
|
+
"G7c 令牌相关禁用规则不带 (?i)(大小写敏感是刻意的)")
|
|
224
|
+
rules = _hooks_rules()
|
|
225
|
+
full = [(ln, p) for ln, p, _ in rules if "mdcg1" in p]
|
|
226
|
+
bare = [(ln, p) for ln, p, _ in rules if "tk_" in p and "mdcg1" not in p]
|
|
227
|
+
ok(len(full) == 1 and len(bare) == 1,
|
|
228
|
+
"G7d hooks.ts 恰好各有一条全形态/裸 id 令牌规则(行号=%s)"
|
|
229
|
+
% ([ln for ln, _ in full] + [ln for ln, _ in bare]))
|
|
230
|
+
if not full or not bare:
|
|
231
|
+
return
|
|
232
|
+
up_tok = _tok("DESIGNER", ("tk_" + HEX_ID).upper(), DUMMY_SECRET_UC.upper())
|
|
233
|
+
lo_tok = _tok("designer", "tk_" + HEX_ID)
|
|
234
|
+
ok(re.search(full[0][1], up_tok) is not None and re.search(full[0][1], lo_tok) is not None,
|
|
235
|
+
"G7e 插件全形态规则**行为上宽**:含大写的令牌也整条命中(拦得住)",
|
|
236
|
+
full[0][1])
|
|
237
|
+
ok(re.search(bare[0][1], "tk_" + HEX_ID) is not None,
|
|
238
|
+
"G7f 插件裸 id 规则匹配小写 hex id", bare[0][1])
|
|
239
|
+
ok(re.search(bare[0][1], "tk_" + HEX_ID.upper()) is None,
|
|
240
|
+
"G7g 插件裸 id 规则**有意**不匹配大写 hex id(识别面固有缺口=本批次修的另一半)",
|
|
241
|
+
bare[0][1])
|
|
242
|
+
# 缺口闭合:识别面漏掉的大写形态,受理面必须拒 ⇒ 两面组合后无缝隙。
|
|
243
|
+
# 反过来说,若受理面像改动前那样认大写,G7g 的形态就是一条真的凭据通道。
|
|
244
|
+
hit, _ = _rejects(_tok("designer", "tk_" + HEX_ID.upper()))
|
|
245
|
+
ok(hit, "G7h 识别面漏(大写 hex)⇔ 受理面拒:不存在「检测漏 + 受理收」的缝隙")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def g8_three_faces():
|
|
249
|
+
"""三面口径(2026-09-28 使用者补裁):入参面宽容、令牌面从严、写面统一小写。
|
|
250
|
+
|
|
251
|
+
裁定原文:「已有测试把大写归一当预期行为。大写也转小写识别,以后写统一小写,
|
|
252
|
+
现有的大写内容就不动了。」故本组**同时**钉住三件事——G8a/G8b 是「不要顺手收紧入参面」
|
|
253
|
+
(收紧会打断存量语义与 `test_p46_unit_scope.py` 的 C2),G8c 是「令牌面照旧拒」
|
|
254
|
+
(批次79 的裁定不被本组软化),G8e 是「存量断言不许被删」。
|
|
255
|
+
"""
|
|
256
|
+
print("\n[G8] 三面口径:入参面宽容 / 令牌面从严 / 写面统一小写")
|
|
257
|
+
spec = tokens.role_spec("RECORD") # 大写 role 名照常解析
|
|
258
|
+
ok(spec.get("clearance_cap") is not None or isinstance(spec, dict),
|
|
259
|
+
"G8a 入参面宽容:role_spec('RECORD') 照常解析(大写经 strip+lower+别名归一)")
|
|
260
|
+
ok(tokens.normalize_role("RECORD") == "record"
|
|
261
|
+
and tokens.normalize_role(" ReCorder ") == "record",
|
|
262
|
+
"G8b 入参面宽容:'RECORD' / ' ReCorder ' 均归一为 record(别名 + 空白一并归一)")
|
|
263
|
+
from .security import Principal as _P
|
|
264
|
+
owner = _P(role="designer", actor="t", can_write=True, can_admin=True, clearance="secret")
|
|
265
|
+
narrowed = tokens.narrowed_principal(owner, "RECORD")
|
|
266
|
+
ok(narrowed.unit == "record" and narrowed.can_admin is False and owner.can_admin is True,
|
|
267
|
+
"G8c 大写 as_unit 只减不增:unit=record、can_admin 恒 False(无提权面)")
|
|
268
|
+
hit, err = _rejects(_tok("RECORD", "tk_" + HEX_ID), want="小写")
|
|
269
|
+
ok(hit, "G8d 令牌面从严(对照):同为大写 role 的**令牌字符串**被拒——两面有意不同",
|
|
270
|
+
err)
|
|
271
|
+
src = io.open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
272
|
+
"md_cg", "test_p46_unit_scope.py"), encoding="utf-8").read()
|
|
273
|
+
ok("大小写归一(RECORD -> record)" in src,
|
|
274
|
+
"G8e 存量不迁移:test_p46_unit_scope.py 的 C2 存量断言仍在(口径的既有凭据未被改掉)")
|
|
275
|
+
tok = tokens.issue("designer", path=os.path.join(tempfile.mkdtemp(prefix="mdcg_cap_"),
|
|
276
|
+
"_tokens.json"))["token"]
|
|
277
|
+
parts = tok.split(".")
|
|
278
|
+
ok(parts[1] == parts[1].lower() and parts[2] == parts[2].lower(),
|
|
279
|
+
"G8f 写面统一小写:issue 新签发的 role/token_id 段恒小写(以后写一律小写)")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _run_groups() -> int:
|
|
283
|
+
_PASS.clear(); _FAIL.clear()
|
|
284
|
+
for g in (g1_allow, g2_id_uppercase, g3_role_uppercase, g4_prefix, g5_end_to_end,
|
|
285
|
+
g6_issuer, g7_detection_face, g8_three_faces):
|
|
286
|
+
g()
|
|
287
|
+
return len(_FAIL)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def main() -> int:
|
|
291
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower().startswith("cp"):
|
|
292
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
293
|
+
base = "--head-baseline" in sys.argv
|
|
294
|
+
if base:
|
|
295
|
+
print("!! 红基线模式:parse_token 临时替换回改动前实现(role.lower()、不查 token_id 形态)")
|
|
296
|
+
print(" 期望 G2/G3/G5/G8d 转红\n")
|
|
297
|
+
tokens.parse_token = _old_parse_token
|
|
298
|
+
globals()["tokens"] = tokens
|
|
299
|
+
groups = (g1_allow, g2_id_uppercase, g3_role_uppercase, g4_prefix, g5_end_to_end,
|
|
300
|
+
g6_issuer, g7_detection_face, g8_three_faces)
|
|
301
|
+
for g in groups:
|
|
302
|
+
g()
|
|
303
|
+
if base:
|
|
304
|
+
print("\n== 红基线判定:应有失败 ==")
|
|
305
|
+
print("红基线失败数 = %d(>0 才算断言有判别力)" % len(_FAIL))
|
|
306
|
+
for f in _FAIL:
|
|
307
|
+
print(" 红:", f)
|
|
308
|
+
return 0 if _FAIL else 1
|
|
309
|
+
print("\n令牌形态(只认小写)守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
|
|
310
|
+
return 0 if not _FAIL else 1
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
with contextlib.redirect_stdout(sys.stdout):
|
|
315
|
+
sys.exit(main())
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 包版本读取 fail-closed 守卫(v21-R1:非对象 package.json 让服务起不来)
|
|
3
|
+
|
|
4
|
+
缺陷(修复前):`md_cg/mcp_server.py:47-53` 与 `hive/hive_mcp/mcp_server.py:48-54`
|
|
5
|
+
的 `_package_version` 只捉 `(OSError, ValueError)`,随后**无条件** `.get("version")`:
|
|
6
|
+
· `package.json` 是**合法 JSON 但顶层非对象**(`[1,2]` / `"0.1.0"` / `123` /
|
|
7
|
+
`null` / `true`)→ `.get` 抛 AttributeError,**逃出 except**;
|
|
8
|
+
· 而 `SERVER_VERSION = _package_version()`(md_cg:57 / hive:58)是**模块级
|
|
9
|
+
立即求值**——异常发生在 import 期,`python -m md_cg.mcp_server` / `-m
|
|
10
|
+
hive.hive_mcp.mcp_server` 连 initialize 都发不出就退出(服务起不来,
|
|
11
|
+
与「版本号退化」完全不是一个量级);
|
|
12
|
+
· `{"version": 123}` 不抛但把 **int** 当版本号塞进 initialize 的
|
|
13
|
+
serverInfo.version(协议要求字符串),调用方拿到错类型。
|
|
14
|
+
|
|
15
|
+
修复:`_package_version` 只认「读到 dict 且 "version" 为**非空字符串**」,其余
|
|
16
|
+
(不可读/JSON 非法/顶层非对象/version 非字符串/空串)一律回落 "0.1.0";except
|
|
17
|
+
补 TypeError/AttributeError 兜底。同族既有守卫 `md_cg/test_server_version.py`
|
|
18
|
+
只覆盖「好文件 → 版本联动」,本守卫补**坏文件**面。
|
|
19
|
+
|
|
20
|
+
守卫(四组,全部临时包根 + 哑主密钥/哑令牌,绝不写真实 package.json、绝不触
|
|
21
|
+
真实 ~/.mdcg):
|
|
22
|
+
[1] 变体矩阵(16 形态 × 2 模块,真实 import + importlib.reload 重跑模块级
|
|
23
|
+
`SERVER_VERSION = _package_version()`):修前 `[1,2]` 等非对象形态报
|
|
24
|
+
AttributeError、`{"version": 123}` 得 int 123;修后一律回落 "0.1.0",
|
|
25
|
+
合法形态("9.9.9"/"0.6.0")原样;
|
|
26
|
+
[2] hive 握手端到端(真实 stdio 子进程,临时包根内 `package.json=[1,2]`):
|
|
27
|
+
修前进程 import 期即死、无任何响应;修后服务起得来且 initialize 自报
|
|
28
|
+
"0.1.0";换合法 version 时自报该值(证明该腿真有判别力,不是常量);
|
|
29
|
+
[3] md_cg 握手端到端(同上,另加临时 MDCG_ROOT + 哑令牌/哑主密钥);
|
|
30
|
+
[4] 不回归:真实仓库的 md_cg/hive 两模块 SERVER_VERSION 仍等于真实
|
|
31
|
+
package.json 的 version(既有 test_server_version.py 口径)。
|
|
32
|
+
|
|
33
|
+
运行:python -m md_cg.test_v21r1_package_version
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import gc
|
|
38
|
+
import glob
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import shutil
|
|
42
|
+
import subprocess
|
|
43
|
+
import sys
|
|
44
|
+
import tempfile
|
|
45
|
+
import time
|
|
46
|
+
|
|
47
|
+
from . import crypto, tokens
|
|
48
|
+
|
|
49
|
+
PASS = FAIL = 0
|
|
50
|
+
FAILS = []
|
|
51
|
+
|
|
52
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
53
|
+
DUMMY_KEK = "ab" * 32 # 哑主密钥:子进程绝不触真实密钥库
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _clean(root):
|
|
57
|
+
"""尽力清理临时包根:Windows 下句柄未释放会让 rmtree 静默失败,故重试并告警。"""
|
|
58
|
+
for _ in range(4):
|
|
59
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
60
|
+
if not os.path.exists(root):
|
|
61
|
+
return
|
|
62
|
+
gc.collect()
|
|
63
|
+
time.sleep(0.2)
|
|
64
|
+
print(" [warn] 临时目录未能清理(句柄占用?):%s" % os.path.basename(root))
|
|
65
|
+
|
|
66
|
+
# (package.json 正文(None=删文件), 期望版本)
|
|
67
|
+
VARIANTS = [
|
|
68
|
+
("[1, 2]", "0.1.0"), # 顶层数组(AttributeError 逃逸)
|
|
69
|
+
('"0.9.9"', "0.1.0"), # 顶层裸字符串
|
|
70
|
+
("123", "0.1.0"), # 顶层数字
|
|
71
|
+
("null", "0.1.0"), # 顶层 null
|
|
72
|
+
("true", "0.1.0"), # 顶层 bool
|
|
73
|
+
('{"version": 123}', "0.1.0"), # int 版本号曾原样进握手
|
|
74
|
+
('{"version": ["9.9.9"]}', "0.1.0"),
|
|
75
|
+
('{"version": {"a": 1}}', "0.1.0"),
|
|
76
|
+
('{"version": null}', "0.1.0"),
|
|
77
|
+
('{"version": ""}', "0.1.0"),
|
|
78
|
+
('{"version": " "}', "0.1.0"),
|
|
79
|
+
('{"name": "x"}', "0.1.0"), # 无 version 键
|
|
80
|
+
("{", "0.1.0"), # JSON 非法(ValueError 老路)
|
|
81
|
+
(None, "0.1.0"), # 文件缺失
|
|
82
|
+
('{"version": "9.9.9"}', "9.9.9"), # 合法
|
|
83
|
+
('{"version": "0.6.0", "extra": [1]}', "0.6.0"), # 合法(多键)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
DRIVER = '''# -*- coding: utf-8 -*-
|
|
87
|
+
"""测试驱动:逐变体写 package.json 后 reload 两个 server 模块并回报结果。
|
|
88
|
+
|
|
89
|
+
只做两件事:真实 import(触发模块级 `SERVER_VERSION = _package_version()`)
|
|
90
|
+
与 importlib.reload(重跑同一句)——即缺陷的**真实消费点**,不改被测代码。
|
|
91
|
+
"""
|
|
92
|
+
import importlib
|
|
93
|
+
import json
|
|
94
|
+
import os
|
|
95
|
+
import sys
|
|
96
|
+
|
|
97
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
98
|
+
MODS = ("md_cg.mcp_server", "hive.hive_mcp.mcp_server")
|
|
99
|
+
VARIANTS = json.load(open(os.path.join(HERE, "_variants.json"), encoding="utf-8"))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_pkg(body):
|
|
103
|
+
p = os.path.join(HERE, "package.json")
|
|
104
|
+
if body is None:
|
|
105
|
+
if os.path.exists(p):
|
|
106
|
+
os.remove(p)
|
|
107
|
+
return
|
|
108
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
109
|
+
f.write(body)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def probe():
|
|
113
|
+
out = {}
|
|
114
|
+
for name in MODS:
|
|
115
|
+
try:
|
|
116
|
+
m = importlib.import_module(name) # 首次 import
|
|
117
|
+
except Exception as e: # noqa: BLE001
|
|
118
|
+
out[name] = ["IMPORT_ERR", type(e).__name__]
|
|
119
|
+
continue
|
|
120
|
+
try:
|
|
121
|
+
m = importlib.reload(m) # 重跑模块级赋值
|
|
122
|
+
v = m.SERVER_VERSION
|
|
123
|
+
out[name] = ["OK", repr(v), type(v).__name__]
|
|
124
|
+
except Exception as e: # noqa: BLE001
|
|
125
|
+
out[name] = ["RELOAD_ERR", type(e).__name__]
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
results = []
|
|
130
|
+
write_pkg('{"version": "0.0.0"}') # 先保证两模块能 import
|
|
131
|
+
for i, body in enumerate(VARIANTS):
|
|
132
|
+
write_pkg(body)
|
|
133
|
+
results.append({"i": i, "r": probe()})
|
|
134
|
+
print(json.dumps(results))
|
|
135
|
+
'''
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def check(name, cond, detail=""):
|
|
139
|
+
global PASS, FAIL
|
|
140
|
+
if cond:
|
|
141
|
+
PASS += 1
|
|
142
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
143
|
+
else:
|
|
144
|
+
FAIL += 1
|
|
145
|
+
FAILS.append(f"{name} · {detail}" if detail else name)
|
|
146
|
+
print(f" [FAIL] {name}" + (f" · {detail}" if detail else ""))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _mk_pkg_root(tmp, name: str) -> str:
|
|
150
|
+
"""临时包根:md_cg/ 与 hive/hive_mcp/ 的真实源码副本 + 根 package.json 占位。
|
|
151
|
+
|
|
152
|
+
副本是**当前源码**(改动即生效),根下 package.json 决定 `_package_version`
|
|
153
|
+
读到的内容——真实 __file__ 推导,不 mock 任何函数。
|
|
154
|
+
"""
|
|
155
|
+
root = os.path.join(tmp, name)
|
|
156
|
+
os.makedirs(root, exist_ok=True)
|
|
157
|
+
shutil.copytree(os.path.join(REPO, "md_cg"), os.path.join(root, "md_cg"),
|
|
158
|
+
ignore=shutil.ignore_patterns("__pycache__"))
|
|
159
|
+
os.makedirs(os.path.join(root, "hive", "hive_mcp"), exist_ok=True)
|
|
160
|
+
shutil.copy(os.path.join(REPO, "hive", "__init__.py"),
|
|
161
|
+
os.path.join(root, "hive", "__init__.py"))
|
|
162
|
+
for fn in glob.glob(os.path.join(REPO, "hive", "hive_mcp", "*.py")):
|
|
163
|
+
shutil.copy(fn, os.path.join(root, "hive", "hive_mcp", os.path.basename(fn)))
|
|
164
|
+
with open(os.path.join(root, "package.json"), "w", encoding="utf-8") as f:
|
|
165
|
+
f.write('{"version": "0.0.0"}')
|
|
166
|
+
return root
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _dry_run(root, variants) -> list:
|
|
170
|
+
"""跑驱动(真实 import/reload),返回每个变体的逐模块结果。"""
|
|
171
|
+
with open(os.path.join(root, "_variants.json"), "w", encoding="utf-8") as f:
|
|
172
|
+
json.dump([b for b, _e in variants], f, ensure_ascii=False)
|
|
173
|
+
with open(os.path.join(root, "_drv.py"), "w", encoding="utf-8") as f:
|
|
174
|
+
f.write(DRIVER)
|
|
175
|
+
env = dict(os.environ)
|
|
176
|
+
env["PYTHONPATH"] = REPO # 同级仓库根模块(aeis_core 等)
|
|
177
|
+
env["PYTHONUTF8"] = "1"
|
|
178
|
+
p = subprocess.run([sys.executable, "-X", "utf8", "_drv.py"], cwd=root,
|
|
179
|
+
env=env, capture_output=True, text=True,
|
|
180
|
+
encoding="utf-8", timeout=300)
|
|
181
|
+
if p.returncode != 0:
|
|
182
|
+
raise RuntimeError("驱动失败 rc=%s stderr=%s" % (p.returncode,
|
|
183
|
+
(p.stderr or "")[-400:]))
|
|
184
|
+
return json.loads(p.stdout.strip().splitlines()[-1])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _handshake(root, module: str, env: dict):
|
|
188
|
+
"""真实 stdio 子进程发 initialize,返回 (版本, 原始响应, stderr)。"""
|
|
189
|
+
p = subprocess.Popen([sys.executable, "-X", "utf8", "-m", module],
|
|
190
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
191
|
+
stderr=subprocess.PIPE, text=True, encoding="utf-8",
|
|
192
|
+
env=env, cwd=root)
|
|
193
|
+
try:
|
|
194
|
+
p.stdin.write(json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
195
|
+
"params": {"protocolVersion": "2024-11-05",
|
|
196
|
+
"capabilities": {},
|
|
197
|
+
"clientInfo": {"name": "v21r1",
|
|
198
|
+
"version": "1"}}}) + "\n")
|
|
199
|
+
p.stdin.flush()
|
|
200
|
+
line = p.stdout.readline() # 进程死 → EOF,不阻塞
|
|
201
|
+
if not line:
|
|
202
|
+
return None, None, (p.stderr.read() or "")[-300:]
|
|
203
|
+
resp = json.loads(line)
|
|
204
|
+
ver = ((resp.get("result") or {}).get("serverInfo") or {}).get("version")
|
|
205
|
+
return ver, resp, ""
|
|
206
|
+
finally:
|
|
207
|
+
try:
|
|
208
|
+
p.stdin.write(json.dumps({"jsonrpc": "2.0", "id": 2,
|
|
209
|
+
"method": "shutdown"}) + "\n")
|
|
210
|
+
p.stdin.flush()
|
|
211
|
+
p.wait(timeout=10)
|
|
212
|
+
except Exception: # noqa: BLE001
|
|
213
|
+
p.kill()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main():
|
|
217
|
+
print("=" * 68)
|
|
218
|
+
print("包版本读取 fail-closed(v21-R1)")
|
|
219
|
+
print("=" * 68)
|
|
220
|
+
tmp = tempfile.mkdtemp(prefix="v21r1_pkg_")
|
|
221
|
+
try:
|
|
222
|
+
root = _mk_pkg_root(tmp, "pkg")
|
|
223
|
+
mods = ("md_cg.mcp_server", "hive.hive_mcp.mcp_server")
|
|
224
|
+
|
|
225
|
+
# ---------- [1] 变体矩阵(真实 import + reload) ----------
|
|
226
|
+
print("\n[1] 变体矩阵(16 形态 × 2 模块,真实 import/reload)")
|
|
227
|
+
res = _dry_run(root, VARIANTS)
|
|
228
|
+
for rec, (body, want) in zip(res, VARIANTS):
|
|
229
|
+
for m in mods:
|
|
230
|
+
got = rec["r"].get(m) or ["<缺失>"]
|
|
231
|
+
ok = got[0] == "OK" and got[1] == repr(want) and got[2] == "str"
|
|
232
|
+
check(f"[1] {m.split('.')[0]}:{body!r} → {want!r}(必须 str)", ok,
|
|
233
|
+
f"got {got!r}")
|
|
234
|
+
|
|
235
|
+
# ---------- [2] hive 握手端到端 ----------
|
|
236
|
+
print("\n[2] hive 握手端到端(临时包根 package.json=[1, 2])")
|
|
237
|
+
bad_root = _mk_pkg_root(tmp, "pkg_bad")
|
|
238
|
+
with open(os.path.join(bad_root, "package.json"), "w", encoding="utf-8") as f:
|
|
239
|
+
f.write("[1, 2]")
|
|
240
|
+
env = dict(os.environ)
|
|
241
|
+
env["PYTHONPATH"] = REPO
|
|
242
|
+
env["PYTHONUTF8"] = "1"
|
|
243
|
+
ver, resp, err = _handshake(bad_root, "hive.hive_mcp.mcp_server", env)
|
|
244
|
+
check("[2] 绿:服务起得来且握手自报回落版本(修前 import 期 AttributeError,无响应)",
|
|
245
|
+
isinstance(ver, str) and ver == "0.1.0",
|
|
246
|
+
f"ver={ver!r} stderr={err!r}")
|
|
247
|
+
check("[2] 绿:serverInfo.version 为字符串(协议要求)",
|
|
248
|
+
isinstance(ver, str), f"{type(ver).__name__}")
|
|
249
|
+
with open(os.path.join(bad_root, "package.json"), "w", encoding="utf-8") as f:
|
|
250
|
+
f.write('{"version": "9.9.9"}')
|
|
251
|
+
ver2, _r2, err2 = _handshake(bad_root, "hive.hive_mcp.mcp_server", env)
|
|
252
|
+
check("[2] 判别力对照:合法 version 时握手自报 9.9.9", ver2 == "9.9.9",
|
|
253
|
+
f"ver={ver2!r} stderr={err2!r}")
|
|
254
|
+
|
|
255
|
+
# ---------- [3] md_cg 握手端到端 ----------
|
|
256
|
+
print("\n[3] md_cg 握手端到端(临时包根 + 临时 MDCG_ROOT + 哑令牌/哑密钥)")
|
|
257
|
+
cg_root = os.path.join(tmp, "cg_lib")
|
|
258
|
+
tf = os.path.join(tmp, "_tokens.json")
|
|
259
|
+
tok = tokens.issue("designer", actor="v21r1", path=tf)
|
|
260
|
+
aux = os.path.join(tmp, "auxdir") # 注:不能叫 aux(Win32 保留设备名)
|
|
261
|
+
os.makedirs(aux, exist_ok=True)
|
|
262
|
+
env2 = dict(os.environ)
|
|
263
|
+
env2.update({"PYTHONPATH": REPO, "PYTHONUTF8": "1",
|
|
264
|
+
"MDCG_ROOT": cg_root, "MDCG_TOKEN_FILE": tf,
|
|
265
|
+
"MDCG_TOKEN": tok["token"], "MDCG_MASTER_KEY": DUMMY_KEK,
|
|
266
|
+
"MDCG_AUX_ROOT": aux, "MDCG_SUSTAIN": "0",
|
|
267
|
+
"MDCG_SUSTAIN_ENABLED": "0"})
|
|
268
|
+
for k in ("MDCG_LEGACY_ENV_AUTH", "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE",
|
|
269
|
+
"MDCG_CLEARANCE", "MDCG_ACTOR", "MDCG_LEGACY_ENV_ADMIN"):
|
|
270
|
+
env2.pop(k, None)
|
|
271
|
+
check("[3] 隔离声明:临时库 + 哑令牌 + 哑主密钥",
|
|
272
|
+
os.path.abspath(cg_root).startswith(os.path.abspath(tmp))
|
|
273
|
+
and env2["MDCG_MASTER_KEY"] == "ab" * 32
|
|
274
|
+
and env2["MDCG_TOKEN"] == tok["token"])
|
|
275
|
+
root_bad = _mk_pkg_root(tmp, "pkg_bad2")
|
|
276
|
+
with open(os.path.join(root_bad, "package.json"), "w", encoding="utf-8") as f:
|
|
277
|
+
f.write("[1, 2]")
|
|
278
|
+
ver3, _r3, err3 = _handshake(root_bad, "md_cg.mcp_server", env2)
|
|
279
|
+
check("[3] 绿:md_cg 服务起得来且握手自报回落版本", ver3 == "0.1.0",
|
|
280
|
+
f"ver={ver3!r} stderr={err3!r}")
|
|
281
|
+
check("[3] 绿:握手可用(result 结构完整,证明不是半启动)",
|
|
282
|
+
isinstance(_r3, dict) and (_r3.get("result") or {}).get("serverInfo"),
|
|
283
|
+
str(_r3)[:120])
|
|
284
|
+
|
|
285
|
+
# ---------- [4] 不回归:真实包根的版本联动 ----------
|
|
286
|
+
print("\n[4] 不回归:真实 package.json 版本仍被两模块读到")
|
|
287
|
+
from . import mcp_server as mdcg_server
|
|
288
|
+
from hive.hive_mcp import mcp_server as hive_server
|
|
289
|
+
with open(os.path.join(REPO, "package.json"), encoding="utf-8") as f:
|
|
290
|
+
want = json.load(f)["version"]
|
|
291
|
+
check("[4] md_cg SERVER_VERSION == 真实 package.json version",
|
|
292
|
+
mdcg_server.SERVER_VERSION == want,
|
|
293
|
+
f"got {mdcg_server.SERVER_VERSION!r} want {want!r}")
|
|
294
|
+
check("[4] hive SERVER_VERSION == 真实 package.json version",
|
|
295
|
+
hive_server.SERVER_VERSION == want,
|
|
296
|
+
f"got {hive_server.SERVER_VERSION!r} want {want!r}")
|
|
297
|
+
check("[4] 两者均为 str", all(isinstance(x, str) for x in
|
|
298
|
+
(mdcg_server.SERVER_VERSION, hive_server.SERVER_VERSION)))
|
|
299
|
+
finally:
|
|
300
|
+
_clean(tmp)
|
|
301
|
+
|
|
302
|
+
print("\n" + "=" * 68)
|
|
303
|
+
print(f"PASS={PASS} FAIL={FAIL}")
|
|
304
|
+
if FAILS:
|
|
305
|
+
print("失败项:" + ";".join(FAILS))
|
|
306
|
+
return 0 if FAIL == 0 else 1
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
sys.exit(main())
|
package/md_cg/test_writepipe.py
CHANGED
|
@@ -45,8 +45,9 @@ def _mk_cg(tmp, role="designer"):
|
|
|
45
45
|
return MdCGSecure(root, principal=p)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def _policy(tmp, required=("PASSED",), forbidden=("FORBIDDEN_WORD",)
|
|
49
|
-
|
|
48
|
+
def _policy(tmp, required=("PASSED",), forbidden=("FORBIDDEN_WORD",),
|
|
49
|
+
name="policy.json"):
|
|
50
|
+
path = os.path.join(tmp, name)
|
|
50
51
|
with open(path, "w", encoding="utf-8") as f:
|
|
51
52
|
json.dump({"forbidden": list(forbidden),
|
|
52
53
|
"required": list(required)}, f)
|
|
@@ -93,8 +94,13 @@ def _run(tmp):
|
|
|
93
94
|
_check("REJECT负记忆", out.get("moved_to") == "rejected"
|
|
94
95
|
and out.get("committed") is False, repr(out))
|
|
95
96
|
|
|
96
|
-
# DEFER
|
|
97
|
-
|
|
97
|
+
# DEFER(空规则库 → 无法判定 → 入审核队列)+ 幂等去重联动
|
|
98
|
+
# 迁移(issue #43,2026-09-29):修前「未设 env」= 空规则 → DEFER;修后未设
|
|
99
|
+
# 会回落到包内默认 data/policy.json,本用例内容无六要素 → 变 REJECT。此处
|
|
100
|
+
# 考的是 **DEFER 出口 + 幂等去重联动**(与策略来源无关),故显式给一份空
|
|
101
|
+
# 规则库复现原口径——断言一字不改。
|
|
102
|
+
os.environ["MDCG_POLICY_FILE"] = _policy(tmp, required=(), forbidden=(),
|
|
103
|
+
name="policy_empty.json")
|
|
98
104
|
a = {"content_kind": "text", "content": "无规则可判的内容",
|
|
99
105
|
"layer": "knowledge"}
|
|
100
106
|
out1 = pipe.execute(cg, dict(a))
|