@furongjun1999/dsh-memory 0.7.3 → 0.7.5
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 +631 -629
- package/codebuddy/CODEBUDDY.md +5 -5
- package/docs/discipline/templates/zcode-user.md.tmpl +4 -1
- package/docs/eval//345/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +108 -0
- package/docs/eval//345/217/221/345/270/20313_/344/270/212/344/270/213/346/226/207/350/207/252/347/256/241/347/220/206/346/234/272/345/210/266_v1.0.md +114 -0
- package/docs/eval//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_P1_/346/212/275/345/217/226/345/231/250v4_/344/270/211/347/216/207/350/257/273/346/225/260_v1.0.md +231 -0
- package/docs/mdcg//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +305 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +45 -43
- package/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 +80 -1
- package/docs/plans//347/201/265/346/236/242/350/272/253/344/275/223/303/227/350/204/221_/344/270/226/347/225/214/346/250/241/345/236/213/345/257/271/346/216/245/350/256/276/350/256/241_v0.1.md +76 -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 +461 -461
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.2.md +121 -0
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.3.md +117 -0
- package/lib/hooks.d.ts +34 -0
- package/lib/hooks.js +205 -18
- package/lib/index.js +22 -1
- package/lib/lib/mdcg_client.d.ts +37 -0
- package/lib/lib/mdcg_client.js +44 -0
- package/lib/lib/session_state.d.ts +56 -0
- package/lib/lib/session_state.js +124 -0
- package/lib/tools.js +12 -1
- package/md_cg/audit.py +7 -1
- package/md_cg/bench_governance.py +4 -1
- package/md_cg/bench_locomo_zh.py +12 -2
- package/md_cg/bench_zh_mad.py +12 -2
- package/md_cg/interop.py +25 -9
- package/md_cg/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +64 -11
- package/md_cg/mdcos.py +78 -9
- package/md_cg/sleep.py +223 -103
- package/md_cg/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- package/md_cg/sustain.py +150 -4
- package/md_cg/test_lifecycle_retire_leak.py +408 -0
- package/md_cg/test_read_face_semantics.py +6 -3
- package/md_cg/test_state_event_op.py +514 -0
- package/md_cg/test_state_slots.py +691 -0
- package/md_cg/test_sustain_bounded.py +624 -0
- package/md_cg/tokens.py +8 -1
- package/md_cg/units.py +4 -1
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +217 -17
- package/src/index.ts +22 -1
- package/src/lib/mdcg_client.ts +48 -0
- package/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
- package/zcode/AGENTS.md +5 -5
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""常驻循环卡死防护与观测守卫(issue #63)。
|
|
3
|
+
|
|
4
|
+
背景(外部 issue #63 三条根因,全部本文件钉死):
|
|
5
|
+
|
|
6
|
+
① `sleep._git()` **无 timeout=**——子进程无界等待(实测停 9.5 小时);
|
|
7
|
+
② 同处**未指定 stdin** ⇒ 子进程继承父进程 stdin(DSH 常驻宿主的
|
|
8
|
+
JSON-RPC 活管道)——活管道下读 stdin 的子进程永不 EOF;
|
|
9
|
+
③ tick 台账在 `run_cycle` **返回后**才写 ⇒ 卡住期间外部无法知道卡在哪档
|
|
10
|
+
(`phases` 空)。
|
|
11
|
+
|
|
12
|
+
修法的可判据面:
|
|
13
|
+
|
|
14
|
+
T1 参数面——`sleep._git` 调 `subprocess.run` 必带
|
|
15
|
+
`timeout=_GIT_TIMEOUT_S` 与 `stdin=subprocess.DEVNULL`(捕获 kwargs 判据);
|
|
16
|
+
旁证(机理复现,等价形态):活管道(PIPE 不写不关)下读 stdin 的子进程
|
|
17
|
+
悬挂;DEVNULL 下 0.02s 级立即 EOF 返回。旁证二:真挂起子进程 + 同形态
|
|
18
|
+
timeout → 真 `TimeoutExpired` 且有界(本机 Python 超时机制在位)。
|
|
19
|
+
T2 真超时——替身按 `subprocess.run(timeout=)` 契约行事(跑满一个 timeout
|
|
20
|
+
后抛 `TimeoutExpired`):`_git_ok` 抛 `SleepError`(`SleepGitTimeout`),
|
|
21
|
+
消息含「超时」「已被 kill」与命令名,耗时 **≈ timeout(有界)**;
|
|
22
|
+
并钉「可区分」:rc≠0 走「失败」文案、与超时文案分开。
|
|
23
|
+
T3 循环续走回归——某档 tick 抛异常 → `_run` 不中断(该档下一轮照常,
|
|
24
|
+
其它档不受影响)——既有能力回归。
|
|
25
|
+
T4 进度面——tick 入口写 `current_tick`(且**进入即刷既有心跳戳**)/ 出口清
|
|
26
|
+
并记 `last_tick_done`;失败档记 `last_tick_error`(容忍≠静默);`status`
|
|
27
|
+
展示三字段;构造过期 `current_tick` → `status` 报 `stale_tick`
|
|
28
|
+
(含档名与已运行时长;负对照:未超不报)。
|
|
29
|
+
T5 审计断言——interop / audit / units / bench×3 / sleep 的 `subprocess.run`
|
|
30
|
+
调用(AST 参数面)带 timeout 与 `stdin=DEVNULL`;whitebox 的 Popen
|
|
31
|
+
保持协议面原形态(stdin=PIPE,**不在加固面**)。
|
|
32
|
+
|
|
33
|
+
运行:python -X utf8 -m md_cg.test_sustain_bounded
|
|
34
|
+
python -X utf8 -m md_cg.test_sustain_bounded --mutate # 变异自证
|
|
35
|
+
python -X utf8 -m md_cg.test_sustain_bounded --mutate --list # 只列变异表
|
|
36
|
+
python -X utf8 -m md_cg.test_sustain_bounded --mutate --null # 空转检出对照
|
|
37
|
+
|
|
38
|
+
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
|
|
39
|
+
2 = ANCHOR-MISS(变异锚点在当前源码里找不到——实现改了却没同步本表)。
|
|
40
|
+
|
|
41
|
+
**红项口径(复核打回修复,2026-10-06)**:每条变异的「自身红项」= 该条运行
|
|
42
|
+
前后 `_FAIL` 长度差——**不得**拿 `len(_FAIL)` 当本轮红项(累计值恒非空,
|
|
43
|
+
会让「判据空转」检测失效、空转变异被静默放行);红项行**全量**打印且与
|
|
44
|
+
自身红项数**账实相符**(不符按失败判)。该缺陷类别由 `--mutate --null`
|
|
45
|
+
(new==old 的无害变异必须被判 MISS)钉住。
|
|
46
|
+
|
|
47
|
+
**基线纪律**:不以 git HEAD 为基线源——「改动前形态」由在当前工作区源码上做
|
|
48
|
+
**定点文本变异**(`_MUTATIONS`)复现,锚点漂移即退出码 2。
|
|
49
|
+
"""
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import ast
|
|
53
|
+
import contextlib
|
|
54
|
+
import io
|
|
55
|
+
import os
|
|
56
|
+
import shutil
|
|
57
|
+
import subprocess
|
|
58
|
+
import sys
|
|
59
|
+
import tempfile
|
|
60
|
+
import time
|
|
61
|
+
import types
|
|
62
|
+
|
|
63
|
+
from . import sleep as SL
|
|
64
|
+
from . import sustain as SUS
|
|
65
|
+
|
|
66
|
+
_PASS = []
|
|
67
|
+
_FAIL = []
|
|
68
|
+
_SRC = {}
|
|
69
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_bounded_")
|
|
70
|
+
_SAVED = {}
|
|
71
|
+
_REAL_SL = SL
|
|
72
|
+
_REAL_SUS = SUS
|
|
73
|
+
|
|
74
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
75
|
+
|
|
76
|
+
#: T5 的审计面:这些文件里的 `subprocess.run` 调用必须带 timeout 与
|
|
77
|
+
#: stdin=DEVNULL(issue #63 同批加固);whitebox 的 Popen 单列(不在加固面)。
|
|
78
|
+
_AUDIT_RUN_FILES = ("md_cg/sleep.py", "md_cg/interop.py", "md_cg/audit.py",
|
|
79
|
+
"md_cg/units.py", "md_cg/bench_locomo_zh.py",
|
|
80
|
+
"md_cg/bench_zh_mad.py", "md_cg/bench_governance.py")
|
|
81
|
+
#: 不在加固面的既有正确形态(免误伤):whitebox 的 Popen 是**长驻对话协议面**
|
|
82
|
+
#: (stdin=PIPE 与后端持续对话,给 DEVNULL 会改变协议语义)。
|
|
83
|
+
_WHITEBOX_REL = "md_cg/whitebox.py"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ok(cond, msg, extra=""):
|
|
87
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
88
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
89
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _rel_text(rel: str) -> str:
|
|
93
|
+
if rel in _SRC:
|
|
94
|
+
return _SRC[rel]
|
|
95
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
96
|
+
return f.read()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _sandbox_env():
|
|
100
|
+
for k in ("MDCG_ROOT", "MDCG_AUX_ROOT", "MDCG_STATE_ROOT",
|
|
101
|
+
"MDCG_SUSTAIN_DIR", "MDCG_SUSTAIN", "MDCG_SLEEP_GITDIR",
|
|
102
|
+
"MDCG_SLEEP_SHADOW"):
|
|
103
|
+
_SAVED[k] = os.environ.get(k)
|
|
104
|
+
os.environ.pop(k, None)
|
|
105
|
+
os.environ["MDCG_SUSTAIN_DIR"] = os.path.join(_TMP, "sustain")
|
|
106
|
+
os.environ["MDCG_ROOT"] = os.path.join(_TMP, "data")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _restore_env():
|
|
110
|
+
for k, v in _SAVED.items():
|
|
111
|
+
if v is None:
|
|
112
|
+
os.environ.pop(k, None)
|
|
113
|
+
else:
|
|
114
|
+
os.environ[k] = v
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class _FakeCG:
|
|
118
|
+
"""最小 cg 替身:SustainLoop 只消费 .root(写戳 / 会话账本读点)。"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, root):
|
|
121
|
+
self.root = root
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _new_loop(**kw):
|
|
125
|
+
root = os.path.join(_TMP, "data")
|
|
126
|
+
os.makedirs(root, exist_ok=True)
|
|
127
|
+
d = os.path.join(_TMP, "sustain")
|
|
128
|
+
os.makedirs(d, exist_ok=True)
|
|
129
|
+
kw.setdefault("name", "md_cg_bounded")
|
|
130
|
+
kw.setdefault("auto_sleep", True)
|
|
131
|
+
kw.setdefault("sleep_merge", "auto")
|
|
132
|
+
kw.setdefault("sleep_window", "")
|
|
133
|
+
kw.setdefault("sleep_scrub_apply", False)
|
|
134
|
+
return SUS.SustainLoop(_FakeCG(root), d=d, **kw)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------- T1
|
|
138
|
+
def t1():
|
|
139
|
+
print("== T1 参数面:_git 必带 timeout=_GIT_TIMEOUT_S 与 stdin=DEVNULL ==")
|
|
140
|
+
S = globals()["SL"]
|
|
141
|
+
real_run = subprocess.run
|
|
142
|
+
calls = []
|
|
143
|
+
|
|
144
|
+
def fake(argv, **kw):
|
|
145
|
+
calls.append((list(argv), dict(kw)))
|
|
146
|
+
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
|
|
147
|
+
|
|
148
|
+
subprocess.run = fake
|
|
149
|
+
try:
|
|
150
|
+
S._git("<G>", "<root>", "status", "--short")
|
|
151
|
+
finally:
|
|
152
|
+
subprocess.run = real_run
|
|
153
|
+
ok(len(calls) == 1, "T1 前置:_git 恰调 subprocess.run 一次", calls)
|
|
154
|
+
if calls:
|
|
155
|
+
argv, kw = calls[0]
|
|
156
|
+
# 回显面**剔除 env**(含本机敏感变量,纪律第 9 条:敏感信息隔离)
|
|
157
|
+
safe = {k: v for k, v in kw.items() if k != "env"}
|
|
158
|
+
ok(argv[:3] == ["git", "--git-dir=<G>", "--work-tree=<root>"],
|
|
159
|
+
"T1a argv 显式 --git-dir / --work-tree(唯一出口形态不变)", argv)
|
|
160
|
+
ok(kw.get("timeout") == S._GIT_TIMEOUT_S
|
|
161
|
+
and isinstance(kw.get("timeout"), (int, float)),
|
|
162
|
+
"T1b **_git 必带 timeout=_GIT_TIMEOUT_S**(%r)——无界等待被闸住"
|
|
163
|
+
% (kw.get("timeout"),), safe)
|
|
164
|
+
ok(kw.get("stdin") is subprocess.DEVNULL,
|
|
165
|
+
"T1c **_git 必带 stdin=subprocess.DEVNULL**——不继承父进程 stdin"
|
|
166
|
+
"(常驻宿主下是 JSON-RPC 活管道)", safe)
|
|
167
|
+
ok(kw.get("shell") is False and kw.get("capture_output") is True,
|
|
168
|
+
"T1d 既有形态保持(shell=False + capture_output=True)", safe)
|
|
169
|
+
ok((kw.get("env") or {}).get("PYTHONUTF8") == "1",
|
|
170
|
+
"T1e env 带 PYTHONUTF8=1(既有形态保持;不回显 env 内容)")
|
|
171
|
+
|
|
172
|
+
# 机理旁证(等价形态):活管道 ⇒ 读 stdin 的子进程悬挂;DEVNULL ⇒ 立即 EOF
|
|
173
|
+
code = "import sys\nsys.stdin.read()\nprint('EOF-OK')\n"
|
|
174
|
+
p = subprocess.Popen([sys.executable, "-c", code], stdin=subprocess.PIPE,
|
|
175
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
176
|
+
text=True, encoding="utf-8", errors="replace")
|
|
177
|
+
try:
|
|
178
|
+
time.sleep(0.6)
|
|
179
|
+
hung = p.poll() is None
|
|
180
|
+
finally:
|
|
181
|
+
p.kill()
|
|
182
|
+
with contextlib.suppress(Exception):
|
|
183
|
+
p.communicate(timeout=5)
|
|
184
|
+
ok(hung, "T1f 机理旁证:**活管道**(PIPE 不写不关)下读 stdin 的子进程"
|
|
185
|
+
"悬挂(poll=None;DSH JSON-RPC 管道的等价形态)")
|
|
186
|
+
t0 = time.time()
|
|
187
|
+
r = subprocess.run([sys.executable, "-c", code], stdin=subprocess.DEVNULL,
|
|
188
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
189
|
+
errors="replace", timeout=15)
|
|
190
|
+
dt = time.time() - t0
|
|
191
|
+
ok(r.returncode == 0 and "EOF-OK" in (r.stdout or ""),
|
|
192
|
+
"T1g 机理旁证:**stdin=DEVNULL** 下同一子进程立即 EOF 返回"
|
|
193
|
+
"(%.2fs)" % dt, (r.returncode, r.stdout, r.stderr))
|
|
194
|
+
t0 = time.time()
|
|
195
|
+
timed_out = False
|
|
196
|
+
try:
|
|
197
|
+
subprocess.run([sys.executable, "-c", "import time; time.sleep(30)"],
|
|
198
|
+
stdin=subprocess.DEVNULL, capture_output=True,
|
|
199
|
+
timeout=0.5)
|
|
200
|
+
except subprocess.TimeoutExpired:
|
|
201
|
+
timed_out = True
|
|
202
|
+
dt = time.time() - t0
|
|
203
|
+
ok(timed_out and dt < 5.0,
|
|
204
|
+
"T1h 机理旁证:真子进程 + timeout → 真 TimeoutExpired 且有界"
|
|
205
|
+
"(%.2fs)——本机 timeout 机制在位" % dt)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------- T2
|
|
209
|
+
def t2():
|
|
210
|
+
print("== T2 真超时:SleepError + 耗时≈timeout(有界)+ 消息含「超时」 ==")
|
|
211
|
+
S = globals()["SL"]
|
|
212
|
+
g = S._git.__globals__
|
|
213
|
+
real_run = subprocess.run
|
|
214
|
+
real_tmo = g["_GIT_TIMEOUT_S"]
|
|
215
|
+
calls = []
|
|
216
|
+
|
|
217
|
+
def fake(argv, **kw):
|
|
218
|
+
"""按 `subprocess.run(timeout=)` 契约行事:跑满一个 timeout 后 kill 抛出。
|
|
219
|
+
|
|
220
|
+
无 timeout(旧形态)⇒ 跑满 2s 才返回(无界等待的等价模拟——T1b/T2a
|
|
221
|
+
会在该形态下转红)。
|
|
222
|
+
"""
|
|
223
|
+
calls.append((list(argv), dict(kw)))
|
|
224
|
+
tmo = kw.get("timeout")
|
|
225
|
+
if tmo is None:
|
|
226
|
+
time.sleep(2.0)
|
|
227
|
+
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
|
|
228
|
+
time.sleep(float(tmo))
|
|
229
|
+
raise subprocess.TimeoutExpired(cmd=argv, timeout=float(tmo))
|
|
230
|
+
|
|
231
|
+
g["_GIT_TIMEOUT_S"] = 0.3 # 测试用短 timeout(模块单点可调)
|
|
232
|
+
subprocess.run = fake
|
|
233
|
+
t0 = time.time()
|
|
234
|
+
raised = None
|
|
235
|
+
try:
|
|
236
|
+
try:
|
|
237
|
+
S._git_ok("<G>", "<root>", "status")
|
|
238
|
+
except S.SleepError as e:
|
|
239
|
+
raised = e
|
|
240
|
+
finally:
|
|
241
|
+
subprocess.run = real_run
|
|
242
|
+
g["_GIT_TIMEOUT_S"] = real_tmo
|
|
243
|
+
dt = time.time() - t0
|
|
244
|
+
ok(raised is not None and isinstance(raised, S.SleepGitTimeout),
|
|
245
|
+
"T2a 超时**不吞**:_git/_git_ok 抛 SleepError(SleepGitTimeout)", raised)
|
|
246
|
+
ok(raised is not None and "超时" in str(raised),
|
|
247
|
+
"T2b 消息含「超时」", str(raised))
|
|
248
|
+
ok(raised is not None and "已被 kill" in str(raised),
|
|
249
|
+
"T2c 消息含「已被 kill」(kill 语义明示)", str(raised))
|
|
250
|
+
ok(raised is not None and "status" in str(raised),
|
|
251
|
+
"T2d 消息含命令名", str(raised))
|
|
252
|
+
ok(raised is not None and "index.lock" in str(raised),
|
|
253
|
+
"T2e 消息含残留锁人工核删指引(不自动删)", str(raised))
|
|
254
|
+
ok(raised is not None and dt <= 0.3 * 3,
|
|
255
|
+
"T2f **耗时有界**(≈timeout:%.2fs ≤ 3×0.3s)——不是无界等待" % dt, dt)
|
|
256
|
+
|
|
257
|
+
# 可区分(issue #63):rc≠0 的失败走「失败」文案,与超时面分开
|
|
258
|
+
def fake_fail(argv, **kw):
|
|
259
|
+
return subprocess.CompletedProcess(argv, 1, stdout="",
|
|
260
|
+
stderr="fatal: boom")
|
|
261
|
+
|
|
262
|
+
subprocess.run = fake_fail
|
|
263
|
+
raised2 = None
|
|
264
|
+
try:
|
|
265
|
+
try:
|
|
266
|
+
S._git_ok("<G>", "<root>", "status")
|
|
267
|
+
except S.SleepError as e2:
|
|
268
|
+
raised2 = e2
|
|
269
|
+
finally:
|
|
270
|
+
subprocess.run = real_run
|
|
271
|
+
ok(raised2 is not None and "失败" in str(raised2)
|
|
272
|
+
and "超时" not in str(raised2),
|
|
273
|
+
"T2g **可区分**:rc≠0 走「失败」文案、不含「超时」(两条失败面分开)",
|
|
274
|
+
str(raised2))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------- T3
|
|
278
|
+
def t3():
|
|
279
|
+
print("== T3 循环续走回归:某档抛异常 → _run 不中断(下一轮照常)==")
|
|
280
|
+
lp = _new_loop(beat_interval=30.0, heal_interval=0.15, scrub_interval=0.3,
|
|
281
|
+
evolve_interval=60.0, tidy_interval=60.0,
|
|
282
|
+
sleep_interval=60.0)
|
|
283
|
+
log = []
|
|
284
|
+
|
|
285
|
+
def bad():
|
|
286
|
+
log.append(("heal", time.time()))
|
|
287
|
+
raise RuntimeError("t3-fail-once")
|
|
288
|
+
|
|
289
|
+
def good():
|
|
290
|
+
log.append(("scrub", time.time()))
|
|
291
|
+
|
|
292
|
+
lp._tick_heal = bad
|
|
293
|
+
lp._tick_scrub = good
|
|
294
|
+
lp._tick_evolve = lambda: log.append(("evolve", time.time()))
|
|
295
|
+
lp._tick_tidy = lambda: log.append(("tidy", time.time()))
|
|
296
|
+
lp._tick_sleep = lambda: log.append(("sleep", time.time()))
|
|
297
|
+
lp.start()
|
|
298
|
+
try:
|
|
299
|
+
time.sleep(1.1)
|
|
300
|
+
finally:
|
|
301
|
+
lp.stop()
|
|
302
|
+
n_heal = sum(1 for t, _ in log if t == "heal")
|
|
303
|
+
n_scrub = sum(1 for t, _ in log if t == "scrub")
|
|
304
|
+
ok(n_heal >= 2,
|
|
305
|
+
"T3a 某档 tick 抛异常后**不中断**:该档下一轮照常调度(heal×%d)"
|
|
306
|
+
% n_heal, log)
|
|
307
|
+
ok(n_scrub >= 1,
|
|
308
|
+
"T3b 其它档不受影响(scrub×%d)" % n_scrub, log)
|
|
309
|
+
ok(lp.last_tick_error is not None
|
|
310
|
+
and lp.last_tick_error.get("name") == "heal"
|
|
311
|
+
and "RuntimeError: t3-fail-once" in lp.last_tick_error.get("error", ""),
|
|
312
|
+
"T3c 异常**不再全静默**:last_tick_error 记 {档名, 类名+摘要}",
|
|
313
|
+
lp.last_tick_error)
|
|
314
|
+
ok(lp.last_tick_done is not None and lp.last_tick_done.get("ok") is False,
|
|
315
|
+
"T3d last_tick_done 记 ok=False(失败档)", lp.last_tick_done)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ---------------------------------------------------------------- T4
|
|
319
|
+
def t4():
|
|
320
|
+
print("== T4 进度面:current_tick / last_tick_done / last_tick_error / "
|
|
321
|
+
"stale_tick ==")
|
|
322
|
+
lp = _new_loop()
|
|
323
|
+
seen = {}
|
|
324
|
+
|
|
325
|
+
def fn():
|
|
326
|
+
seen["mem"] = lp.current_tick
|
|
327
|
+
seen["stamp"] = SUS.read_stamp(lp.name, lp.d)
|
|
328
|
+
|
|
329
|
+
lp._wrap_tick("scrub", fn)()
|
|
330
|
+
ct = seen.get("mem") or {}
|
|
331
|
+
ok(ct.get("name") == "scrub" and "started_at" in ct
|
|
332
|
+
and ct.get("pid") == os.getpid(),
|
|
333
|
+
"T4a tick 入口写 current_tick={name, started_at, pid}", ct)
|
|
334
|
+
ok(((seen.get("stamp") or {}).get("current_tick") or {}).get("name")
|
|
335
|
+
== "scrub",
|
|
336
|
+
"T4a2 **进入档即刷既有状态面**(心跳戳里读得到 current_tick——"
|
|
337
|
+
"卡住场景下也读得到)", seen.get("stamp"))
|
|
338
|
+
ok(lp.current_tick is None,
|
|
339
|
+
"T4b tick 出口清 current_tick", lp.current_tick)
|
|
340
|
+
ok(lp.last_tick_done is not None
|
|
341
|
+
and lp.last_tick_done.get("name") == "scrub"
|
|
342
|
+
and lp.last_tick_done.get("ok") is True
|
|
343
|
+
and "t" in lp.last_tick_done,
|
|
344
|
+
"T4b2 tick 出口记 last_tick_done={name, t, ok}", lp.last_tick_done)
|
|
345
|
+
|
|
346
|
+
lp._wrap_tick("heal", lambda: (_ for _ in ()).throw(ValueError("t4-e")))()
|
|
347
|
+
ok(lp.last_tick_error is not None
|
|
348
|
+
and lp.last_tick_error.get("name") == "heal"
|
|
349
|
+
and "ValueError: t4-e" in lp.last_tick_error.get("error", ""),
|
|
350
|
+
"T4c 失败档记 last_tick_error(类名+摘要)", lp.last_tick_error)
|
|
351
|
+
ok(lp.last_tick_done is not None and lp.last_tick_done.get("ok") is False,
|
|
352
|
+
"T4c2 失败档 last_tick_done.ok=False", lp.last_tick_done)
|
|
353
|
+
|
|
354
|
+
st = lp.status()
|
|
355
|
+
ok("current_tick" in st and "last_tick_done" in st
|
|
356
|
+
and "last_tick_error" in st,
|
|
357
|
+
"T4d status 输出含进度三字段(cg(op=sustain,action=status) 同源)",
|
|
358
|
+
sorted(st))
|
|
359
|
+
ok(st["last_tick_done"]["name"] == "heal"
|
|
360
|
+
and st["last_tick_done"]["ok"] is False,
|
|
361
|
+
"T4d2 status 的 last_tick_done 反映最近一档(失败态可见)", st["last_tick_done"])
|
|
362
|
+
|
|
363
|
+
lp.current_tick = {"name": "sleep", "started_at": time.time() - 9000.0,
|
|
364
|
+
"pid": os.getpid()}
|
|
365
|
+
st2 = lp.status()
|
|
366
|
+
ok(bool(st2["stale_tick"]) and st2["stale_tick"].get("tick") == "sleep",
|
|
367
|
+
"T4e 过期 current_tick → status 报 stale_tick(档名=sleep)",
|
|
368
|
+
st2["stale_tick"])
|
|
369
|
+
ok(bool(st2["stale_tick"])
|
|
370
|
+
and "sleep" in st2["stale_tick"].get("alert", "")
|
|
371
|
+
and "9000" in st2["stale_tick"].get("alert", ""),
|
|
372
|
+
"T4e2 stale_tick.alert 含档名与已运行时长", st2["stale_tick"])
|
|
373
|
+
ok(st2["current_tick"].get("running_s") >= 9000,
|
|
374
|
+
"T4e3 current_tick 附已运行秒数 running_s", st2["current_tick"])
|
|
375
|
+
lp.current_tick = {"name": "sleep", "started_at": time.time() - 10.0,
|
|
376
|
+
"pid": os.getpid()}
|
|
377
|
+
ok(lp.status()["stale_tick"] is None,
|
|
378
|
+
"T4f 负对照:未超阈值不报 stale_tick(不误报)")
|
|
379
|
+
# 跨进程回落:内存清空后仍能从既有状态面读到(写点是戳,不是内存独有)
|
|
380
|
+
lp.last_tick_done = None
|
|
381
|
+
st3 = lp.status()
|
|
382
|
+
ok(st3["last_tick_done"] is not None,
|
|
383
|
+
"T4g 跨进程回落:内存清空后 last_tick_done 仍可从戳读到",
|
|
384
|
+
st3["last_tick_done"])
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ---------------------------------------------------------------- T5
|
|
388
|
+
def t5():
|
|
389
|
+
print("== T5 审计断言:加固面 subprocess.run 带 timeout+DEVNULL;"
|
|
390
|
+
"whitebox Popen 保持协议面 ==")
|
|
391
|
+
|
|
392
|
+
def run_calls(rel):
|
|
393
|
+
tree = ast.parse(_rel_text(rel))
|
|
394
|
+
out = []
|
|
395
|
+
for node in ast.walk(tree):
|
|
396
|
+
if (isinstance(node, ast.Call)
|
|
397
|
+
and isinstance(node.func, ast.Attribute)
|
|
398
|
+
and node.func.attr == "run"
|
|
399
|
+
and isinstance(node.func.value, ast.Name)
|
|
400
|
+
and node.func.value.id == "subprocess"):
|
|
401
|
+
out.append(node)
|
|
402
|
+
return out
|
|
403
|
+
|
|
404
|
+
def has_kw(node, key, devnull=False):
|
|
405
|
+
for kw in node.keywords:
|
|
406
|
+
if kw.arg != key:
|
|
407
|
+
continue
|
|
408
|
+
if not devnull:
|
|
409
|
+
return True
|
|
410
|
+
v = kw.value
|
|
411
|
+
return (isinstance(v, ast.Attribute) and v.attr == "DEVNULL"
|
|
412
|
+
and isinstance(v.value, ast.Name)
|
|
413
|
+
and v.value.id == "subprocess")
|
|
414
|
+
return False
|
|
415
|
+
|
|
416
|
+
for rel in _AUDIT_RUN_FILES:
|
|
417
|
+
nodes = run_calls(rel)
|
|
418
|
+
ok(len(nodes) >= 1, "T5 前置:%s 内有 subprocess.run 调用(%d 处)"
|
|
419
|
+
% (rel, len(nodes)))
|
|
420
|
+
for i, node in enumerate(nodes, 1):
|
|
421
|
+
ok(has_kw(node, "timeout"),
|
|
422
|
+
"T5a %s:%d subprocess.run 带 timeout=" % (rel, node.lineno))
|
|
423
|
+
ok(has_kw(node, "stdin", devnull=True),
|
|
424
|
+
"T5b %s:%d subprocess.run 带 stdin=subprocess.DEVNULL"
|
|
425
|
+
% (rel, node.lineno))
|
|
426
|
+
|
|
427
|
+
# whitebox:长驻对话协议面(stdin=PIPE)——**不在加固面**,保持原形态
|
|
428
|
+
tree = ast.parse(_rel_text(_WHITEBOX_REL))
|
|
429
|
+
popens = [n for n in ast.walk(tree)
|
|
430
|
+
if isinstance(n, ast.Call)
|
|
431
|
+
and isinstance(n.func, ast.Attribute)
|
|
432
|
+
and n.func.attr == "Popen"
|
|
433
|
+
and isinstance(n.func.value, ast.Name)
|
|
434
|
+
and n.func.value.id == "subprocess"]
|
|
435
|
+
ok(len(popens) >= 1, "T5c 前置:whitebox.py 内有 subprocess.Popen(%d 处)"
|
|
436
|
+
% len(popens))
|
|
437
|
+
for node in popens:
|
|
438
|
+
stdin_kw = next((kw for kw in node.keywords if kw.arg == "stdin"), None)
|
|
439
|
+
ok(stdin_kw is not None
|
|
440
|
+
and isinstance(stdin_kw.value, ast.Attribute)
|
|
441
|
+
and stdin_kw.value.attr == "PIPE",
|
|
442
|
+
"T5d whitebox Popen 保持协议面原形态(stdin=PIPE,不加固)"
|
|
443
|
+
"(%s:%d)" % (_WHITEBOX_REL, node.lineno))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ---------------------------------------------------------------- 变异表
|
|
447
|
+
# 锚点 = (名字, rel, old, new)。每次变异必须让套件**转红**:
|
|
448
|
+
# M1 主修删 timeout=(_git 退回无界等待)→ T1b / T2a 红
|
|
449
|
+
# M2 主修删 stdin=DEVNULL(子进程继承活管道 stdin)→ T1c 红
|
|
450
|
+
# M3 进度面删「进入写点」→ T4a / T4a2 红
|
|
451
|
+
# M4 六档 except 删 last_tick_error 记录(退回全静默)→ T3c / T4c 红
|
|
452
|
+
_MUTATIONS = (
|
|
453
|
+
("删 timeout=(_git 退回无界等待)", "md_cg/sleep.py",
|
|
454
|
+
" env=env, shell=False,\n"
|
|
455
|
+
" timeout=_GIT_TIMEOUT_S,\n"
|
|
456
|
+
" stdin=subprocess.DEVNULL)",
|
|
457
|
+
" env=env, shell=False,\n"
|
|
458
|
+
" stdin=subprocess.DEVNULL)"),
|
|
459
|
+
("删 stdin=DEVNULL(继承活管道 stdin)", "md_cg/sleep.py",
|
|
460
|
+
" env=env, shell=False,\n"
|
|
461
|
+
" timeout=_GIT_TIMEOUT_S,\n"
|
|
462
|
+
" stdin=subprocess.DEVNULL)",
|
|
463
|
+
" env=env, shell=False,\n"
|
|
464
|
+
" timeout=_GIT_TIMEOUT_S)"),
|
|
465
|
+
("删进度面进入写点(卡住读不到档)", "md_cg/sustain.py",
|
|
466
|
+
' self.current_tick = {"name": name, "started_at": time.time(),\n'
|
|
467
|
+
' "pid": os.getpid()}\n'
|
|
468
|
+
' self._flush_progress()\n',
|
|
469
|
+
""),
|
|
470
|
+
("删失败档 last_tick_error(退回全静默)", "md_cg/sustain.py",
|
|
471
|
+
" except Exception as e: # noqa: BLE001\n"
|
|
472
|
+
" ok_ = False\n"
|
|
473
|
+
" self.last_tick_error = {\"name\": name, \"t\": time.time(),\n"
|
|
474
|
+
" \"error\": \"%s: %s\" % (type(e).__name__,\n"
|
|
475
|
+
" str(e)[:200])}\n",
|
|
476
|
+
" except Exception: # noqa: BLE001\n"
|
|
477
|
+
" ok_ = False\n"),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
#: 空转检出对照(meta 自证,`--mutate --null`):new==old 的「无害变异」——
|
|
481
|
+
#: 行为零变化、自身红项必须为 0 且被判 MISS。若哪天「本轮红项」判定口径
|
|
482
|
+
#: 退化为累计计数(累计恒非空),本对照立即转 FAIL(钉住的正是复核打回
|
|
483
|
+
#: 实证过的那条假阳性通道)。锚点取 `_git` 调用里一行行为零变化的文本。
|
|
484
|
+
_NULL_MUTATION = ("空转对照(new==old,行为零变化)", "md_cg/sleep.py",
|
|
485
|
+
" env=env, shell=False,\n",
|
|
486
|
+
" env=env, shell=False,\n")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _exec_module(name: str, rel: str, text: str):
|
|
490
|
+
ns = {"__name__": name, "__package__": "md_cg",
|
|
491
|
+
"__file__": os.path.join(_REPO, rel)}
|
|
492
|
+
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
493
|
+
m = types.ModuleType(name)
|
|
494
|
+
m.__dict__.update(ns)
|
|
495
|
+
return m
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _run_groups() -> int:
|
|
499
|
+
for fn in (t1, t2, t3, t4, t5):
|
|
500
|
+
try:
|
|
501
|
+
fn()
|
|
502
|
+
except Exception as e: # noqa: BLE001
|
|
503
|
+
ok(False, "%s 组抛异常:%s: %s" % (fn.__name__, type(e).__name__, e))
|
|
504
|
+
return len(_FAIL)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _mutate_once(name: str, rel: str, old: str, new: str):
|
|
508
|
+
"""跑一条变异并返回**本轮自身**读数 (reds_self, reds_lines, detail)。
|
|
509
|
+
|
|
510
|
+
红项口径(复核打回修复,2026-10-06):`_FAIL` 是**全局累计**列表,
|
|
511
|
+
故「本轮红项」必须取**调用前后长度差**——直接拿 `len(_FAIL)` 当本轮
|
|
512
|
+
红项会让「判据空转」检测失效(累计值恒非空 ⇒ 空转变异也会被记成
|
|
513
|
+
「红」并放行)。本轮自身红项还须与输出里 FAIL 行数**账实相符**。
|
|
514
|
+
"""
|
|
515
|
+
src = _rel_text(rel)
|
|
516
|
+
if old not in src:
|
|
517
|
+
return None, [], "ANCHOR-MISS:锚点在 %s 源码里找不到(实现改了却没同步" \
|
|
518
|
+
"本表)" % rel
|
|
519
|
+
base = len(_FAIL) # 本轮起点:只数本轮自身的红项
|
|
520
|
+
_SRC[rel] = src.replace(old, new, 1)
|
|
521
|
+
try:
|
|
522
|
+
mut = _exec_module("md_cg._bounded_mut", rel, _SRC[rel])
|
|
523
|
+
if rel == "md_cg/sleep.py":
|
|
524
|
+
globals()["SL"] = mut
|
|
525
|
+
else:
|
|
526
|
+
globals()["SUS"] = mut
|
|
527
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
528
|
+
_run_groups()
|
|
529
|
+
detail = buf.getvalue()
|
|
530
|
+
finally:
|
|
531
|
+
globals()["SL"] = _REAL_SL
|
|
532
|
+
globals()["SUS"] = _REAL_SUS
|
|
533
|
+
_SRC.pop(rel, None)
|
|
534
|
+
reds = len(_FAIL) - base # **本轮自身**红项数
|
|
535
|
+
reds_lines = [l for l in detail.splitlines()
|
|
536
|
+
if l.strip().startswith("FAIL ")]
|
|
537
|
+
return reds, reds_lines, detail
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
541
|
+
print("!! 定点变异自证:逐条把机制改回「改动前/错误」形态,套件必须转红"
|
|
542
|
+
"(红项 = 本轮自身,非法用全局累计值)\n")
|
|
543
|
+
if list_only:
|
|
544
|
+
for name, rel, _o, _n in _MUTATIONS:
|
|
545
|
+
print(" %-38s [%s]" % (name, rel))
|
|
546
|
+
return 0
|
|
547
|
+
anchor_miss, bad = [], []
|
|
548
|
+
base0 = len(_FAIL)
|
|
549
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
550
|
+
_run_groups()
|
|
551
|
+
clean = len(_FAIL) - base0
|
|
552
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
553
|
+
if clean:
|
|
554
|
+
bad.append("未变异基线即失败")
|
|
555
|
+
|
|
556
|
+
for name, rel, old, new in _MUTATIONS:
|
|
557
|
+
reds, reds_lines, detail = _mutate_once(name, rel, old, new)
|
|
558
|
+
if reds is None:
|
|
559
|
+
print(" ANCHOR-MISS %s —— %s" % (name, detail))
|
|
560
|
+
anchor_miss.append(name)
|
|
561
|
+
continue
|
|
562
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
563
|
+
print(" %s %-38s 自身红项=%d(本轮;基线=%d) %s"
|
|
564
|
+
% ("OK " if reds else "MISS ", name, reds, clean, verdict))
|
|
565
|
+
for l in reds_lines: # **全量**打印(回报即读数,不截断)
|
|
566
|
+
print(" " + l.strip()[5:])
|
|
567
|
+
if len(reds_lines) != reds:
|
|
568
|
+
# 账实不符 = 计数面自身有缺陷——按失败判(fail-closed)
|
|
569
|
+
print(" !账实不符:FAIL 行数=%d ≠ 自身红项=%d——计数面缺陷"
|
|
570
|
+
% (len(reds_lines), reds))
|
|
571
|
+
bad.append(name + "(账实不符)")
|
|
572
|
+
if not reds:
|
|
573
|
+
bad.append(name)
|
|
574
|
+
|
|
575
|
+
if anchor_miss:
|
|
576
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
577
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
578
|
+
return 2
|
|
579
|
+
print("\n定点变异自证:%s"
|
|
580
|
+
% ("PASS(每处机制都有一条断言把它钉死)" if not bad
|
|
581
|
+
else "FAIL —— " + "、".join(bad)))
|
|
582
|
+
return 0 if not bad else 1
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _null_mode() -> int:
|
|
586
|
+
"""空转检出对照(meta 自证):new==old 的「无害变异」必须被判 MISS。
|
|
587
|
+
|
|
588
|
+
钉住的缺陷类别(复核打回实证):若「本轮红项」误用全局累计值(累计恒
|
|
589
|
+
非空),空转变异会被记成「红」并放行——`--mutate` 的 fail-closed 承诺
|
|
590
|
+
(「变异未按预期转红 = 退出码 1」)随之失效。本对照即钉住该检出能力。
|
|
591
|
+
"""
|
|
592
|
+
print("!! 空转检出对照:一条 new==old 的「无害变异」——必须被判 MISS"
|
|
593
|
+
"(不得凭任何累计值蒙混)\n")
|
|
594
|
+
name, rel, old, new = _NULL_MUTATION
|
|
595
|
+
reds, reds_lines, detail = _mutate_once(name, rel, old, new)
|
|
596
|
+
if reds is None:
|
|
597
|
+
print(" ANCHOR-MISS %s —— %s" % (name, detail))
|
|
598
|
+
return 2
|
|
599
|
+
detected = reds == 0 and not reds_lines
|
|
600
|
+
print(" 变异=%s 自身红项=%d(本轮) FAIL 行数=%d ⇒ %s"
|
|
601
|
+
% (name, reds, len(reds_lines),
|
|
602
|
+
"MISS(如期检出空转,判定有效)" if detected
|
|
603
|
+
else "红(**空转漏检 = 判据失效**,fail-closed 不成立)"))
|
|
604
|
+
print("空转检出对照:%s" % ("PASS" if detected else "FAIL"))
|
|
605
|
+
return 0 if detected else 1
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def main() -> int:
|
|
609
|
+
_sandbox_env()
|
|
610
|
+
try:
|
|
611
|
+
if "--mutate" in sys.argv:
|
|
612
|
+
if "--null" in sys.argv:
|
|
613
|
+
return _null_mode()
|
|
614
|
+
return _mutate_mode("--list" in sys.argv)
|
|
615
|
+
n = _run_groups()
|
|
616
|
+
print("\nSUMMARY: 常驻卡死防护守卫:%d 通过,%d 失败" % (len(_PASS), n))
|
|
617
|
+
return 0 if not n else 1
|
|
618
|
+
finally:
|
|
619
|
+
_restore_env()
|
|
620
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
if __name__ == "__main__":
|
|
624
|
+
sys.exit(main())
|
package/md_cg/tokens.py
CHANGED
|
@@ -97,7 +97,14 @@ ALL_OPS = ("help", "info", "route", "read", "write", "goal", "task", "recent", "
|
|
|
97
97
|
# 缺省不给(fail-closed,新增 op 默认不在任何清单内);
|
|
98
98
|
# 本轮开给:designer(* 自动含)+ verify(证据审计=验证
|
|
99
99
|
# 单元的本职读面,见其 ROLE_SPECS 注释)。
|
|
100
|
-
|
|
100
|
+
# P3 新增(世界模型功能端 P3,2026-10-06):
|
|
101
|
+
# state_event 状态事件记账(追加一条五元事件到 append-only 台账)
|
|
102
|
+
# —— 写口;**角色面零新词**:cg 面作用域闸把它映射到既有
|
|
103
|
+
# "write" 词(见 mcp_server._cg_dispatch),各角色
|
|
104
|
+
# ROLE_SPECS.ops_allow 白名单零改动;登记于本表只为
|
|
105
|
+
# 「op 三处同步」契约(ALL_OPS == cg 工具 schema == 分发分支,
|
|
106
|
+
# 见 test_p27/test_p29/test_p30/test_p31 防漏改守卫)。
|
|
107
|
+
"audit", "state_event")
|
|
101
108
|
|
|
102
109
|
|
|
103
110
|
class TokenError(Exception):
|
package/md_cg/units.py
CHANGED
|
@@ -135,8 +135,11 @@ def _tasklist_row(pid):
|
|
|
135
135
|
# 显式 utf-8 + replace:只消费 ASCII 的 pid 列,但**不依赖 locale**——
|
|
136
136
|
# locale 口径与「后代写 UTF-8」不一致时读线程会崩(见 test_subproc_encoding.py)。
|
|
137
137
|
# P2-16(批次 30):timeout=10——tasklist 挂起曾永久阻塞判活路径。
|
|
138
|
+
# stdin=subprocess.DEVNULL(issue #63 同批加固):不指定 stdin 时子进程
|
|
139
|
+
# 继承父进程 stdin(常驻宿主下是 JSON-RPC 活管道)——判活路径不得因
|
|
140
|
+
# 子进程读 stdin 而悬挂;形态对齐 `md_cg/run_tests.py:105`。
|
|
138
141
|
r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH", "/FO", "CSV"],
|
|
139
|
-
capture_output=True, text=True,
|
|
142
|
+
capture_output=True, text=True, stdin=subprocess.DEVNULL,
|
|
140
143
|
encoding="utf-8", errors="replace", timeout=10)
|
|
141
144
|
except (OSError, subprocess.SubprocessError):
|
|
142
145
|
return None
|
package/package.json
CHANGED
package/skills/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "lingshu-skills",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.5",
|
|
5
5
|
"description": "灵枢(AEIS)自我认知技能包——白箱条件化知识(Agent Skills 兼容导出)。本质是灵枢了解自身的工具:每个技能描述灵枢在什么条件下能做什么、怎么执行、克制什么(KCCS 四要素),由条件路由图精确路由,由灵枢 MCP 工具执行验证(物理基底)。",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "灵枢(AEIS)· CommonTrustProtocol"
|