agentseed-mcp 0.3.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/CHANGELOG.md +178 -0
- package/LICENSE +202 -0
- package/README.ja.md +320 -0
- package/README.md +318 -0
- package/README.zh.md +306 -0
- package/bin/cli.js +37 -0
- package/mcp.json +12 -0
- package/package.json +30 -0
- package/plugin.json +22 -0
- package/server/.agentseed/verification-log.jsonl +2 -0
- package/server/__pycache__/guard_cli.cpython-313.pyc +0 -0
- package/server/__pycache__/guard_engine.cpython-313.pyc +0 -0
- package/server/__pycache__/test_cli.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_cli.cpython-313.pyc +0 -0
- package/server/__pycache__/test_features.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_features.cpython-313.pyc +0 -0
- package/server/__pycache__/test_guard.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_guard.cpython-313.pyc +0 -0
- package/server/__pycache__/test_hook.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_hook.cpython-313.pyc +0 -0
- package/server/__pycache__/test_manifests.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_manifests.cpython-313.pyc +0 -0
- package/server/__pycache__/test_server.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_server.cpython-313.pyc +0 -0
- package/server/engine/__init__.py +64 -0
- package/server/engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/audit.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/config.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/hallucination.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/imports.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/plugin.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/schema.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/symbols.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/version.cpython-313.pyc +0 -0
- package/server/engine/audit.py +84 -0
- package/server/engine/config.py +131 -0
- package/server/engine/hallucination.py +254 -0
- package/server/engine/imports.py +136 -0
- package/server/engine/plugin.py +367 -0
- package/server/engine/sandbox.py +287 -0
- package/server/engine/schema.py +193 -0
- package/server/engine/symbols.py +984 -0
- package/server/engine/version.py +17 -0
- package/server/guard_cli.py +455 -0
- package/server/guard_engine.py +111 -0
- package/server/guard_hook.py +404 -0
- package/server/guard_server.py +472 -0
- package/server/requirements.txt +7 -0
- package/server/test_cli.py +132 -0
- package/server/test_features.py +426 -0
- package/server/test_guard.py +828 -0
- package/server/test_hook.py +331 -0
- package/server/test_manifests.py +70 -0
- package/server/test_server.py +247 -0
- package/skills/verify-before-code/SKILL.ja.md +116 -0
- package/skills/verify-before-code/SKILL.md +140 -0
- package/skills/verify-before-code/SKILL.zh.md +117 -0
- package/skills/verify-before-code/references/DEFAULT-NORMS.md +52 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.ja.md +121 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.md +166 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.zh.md +145 -0
- package/skills/verify-before-code/references/PROMPT-POOL.ja.md +248 -0
- package/skills/verify-before-code/references/PROMPT-POOL.md +282 -0
- package/skills/verify-before-code/references/PROMPT-POOL.zh.md +252 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.ja.md +61 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.md +66 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.zh.md +58 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.ja.md +62 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.md +62 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.zh.md +54 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.ja.md +68 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.md +73 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.zh.md +68 -0
- package/skills/verify-before-code/scripts/check.ps1 +52 -0
- package/skills/verify-before-code/scripts/check.sh +44 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""AgentSeed feature tests: line numbers, suppress, CJK tokens,
|
|
2
|
+
sandbox policy, config validation, audit trail, fixtures, perf gate."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
+
|
|
15
|
+
import guard_engine as engine # type: ignore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestSymbolsLineNumbersAndSuppress(unittest.TestCase):
|
|
19
|
+
SOURCE = (
|
|
20
|
+
"import os\n"
|
|
21
|
+
"\n"
|
|
22
|
+
"def a():\n"
|
|
23
|
+
" return ghost_one()\n"
|
|
24
|
+
"\n"
|
|
25
|
+
"def b():\n"
|
|
26
|
+
" x = ghost_two + 1\n"
|
|
27
|
+
" return x\n"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def test_suspects_detail_carries_lines(self):
|
|
31
|
+
r = engine.detect_undefined_symbols(self.SOURCE)
|
|
32
|
+
self.assertEqual(r["suspects"], ["ghost_one", "ghost_two"])
|
|
33
|
+
lines = {d["name"]: d["line"] for d in r["suspects_detail"]}
|
|
34
|
+
self.assertEqual(lines, {"ghost_one": 4, "ghost_two": 7})
|
|
35
|
+
|
|
36
|
+
def test_suppress_filters_and_reports(self):
|
|
37
|
+
r = engine.detect_undefined_symbols(self.SOURCE, suppress=["ghost_two"])
|
|
38
|
+
self.assertEqual(r["suspects"], ["ghost_one"])
|
|
39
|
+
self.assertEqual(r["suppressed"], ["ghost_two"])
|
|
40
|
+
self.assertEqual([d["name"] for d in r["suspects_detail"]], ["ghost_one"])
|
|
41
|
+
|
|
42
|
+
def test_no_suppress_reports_empty_list(self):
|
|
43
|
+
r = engine.detect_undefined_symbols(self.SOURCE)
|
|
44
|
+
self.assertEqual(r["suppressed"], [])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestCjkTokens(unittest.TestCase):
|
|
48
|
+
def test_chinese_stub_tokens_hit_as_substrings(self):
|
|
49
|
+
src = "# 占位实现,稍后补充\n# 待实现\nx = 1\n"
|
|
50
|
+
r = engine.scan_hallucination_words(src)
|
|
51
|
+
words = {h["word"] for h in r["hits"]}
|
|
52
|
+
self.assertIn("占位", words)
|
|
53
|
+
self.assertTrue(any(h["group"] == "stub_code" for h in r["hits"]))
|
|
54
|
+
|
|
55
|
+
def test_chinese_oversold_blocks_by_default(self):
|
|
56
|
+
r = engine.scan_hallucination_words("# 保证通过\n")
|
|
57
|
+
self.assertTrue(r["blocking"])
|
|
58
|
+
self.assertEqual(r["hits"][0]["group"], "oversold")
|
|
59
|
+
|
|
60
|
+
def test_extra_tokens_extend_pool(self):
|
|
61
|
+
base = engine.scan_hallucination_words("x = frobnicate_now()\n")
|
|
62
|
+
self.assertEqual(base["hits"], [])
|
|
63
|
+
ext = engine.scan_hallucination_words(
|
|
64
|
+
"x = frobnicate_now()\n",
|
|
65
|
+
extra_tokens={"fabricated": ["frobnicate_now"]},
|
|
66
|
+
)
|
|
67
|
+
self.assertEqual(len(ext["hits"]), 1)
|
|
68
|
+
self.assertEqual(ext["hits"][0]["group"], "fabricated")
|
|
69
|
+
self.assertTrue(ext["blocking"]) # fabricated defaults to error
|
|
70
|
+
|
|
71
|
+
def test_extra_tokens_invalid_group_ignored(self):
|
|
72
|
+
r = engine.scan_hallucination_words("x = todo()\n", extra_tokens={"nope_group": ["todo"]})
|
|
73
|
+
self.assertEqual(len(r["hits"]), 1) # builtin stub hit only
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestSandboxAllowPolicy(unittest.TestCase):
|
|
77
|
+
def test_unlisted_binary_blocked_without_running(self):
|
|
78
|
+
# Never spawned: the policy gate refuses BEFORE execution (-10).
|
|
79
|
+
r = engine.sandbox_run(
|
|
80
|
+
["some-unlisted-tool", "--flag"],
|
|
81
|
+
allowed_prefixes=["python", "pytest"],
|
|
82
|
+
)
|
|
83
|
+
self.assertEqual(r["exit_code"], -10)
|
|
84
|
+
self.assertIn("sandbox_allowed_prefixes", r["stderr"])
|
|
85
|
+
|
|
86
|
+
def test_listed_basename_allowed(self):
|
|
87
|
+
r = engine.sandbox_run(
|
|
88
|
+
[sys.executable, "-c", "print(7)"],
|
|
89
|
+
allowed_prefixes=[os.path.basename(sys.executable)],
|
|
90
|
+
)
|
|
91
|
+
self.assertEqual(r["exit_code"], 0)
|
|
92
|
+
self.assertIn("7", r["stdout"])
|
|
93
|
+
|
|
94
|
+
def test_none_means_unrestricted(self):
|
|
95
|
+
# Cross-platform: actually runs on Linux/macOS too.
|
|
96
|
+
r = engine.sandbox_run([sys.executable, "-c", "print('ok')"], allowed_prefixes=None)
|
|
97
|
+
self.assertEqual(r["exit_code"], 0)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestSandboxAllowPolicyHardening(unittest.TestCase):
|
|
101
|
+
"""Regressions for the allowlist-bypass fixes: separator-boundary
|
|
102
|
+
prefix matching and PATH-resolved execution (no cwd shadowing)."""
|
|
103
|
+
|
|
104
|
+
def test_prefix_requires_separator_boundary(self):
|
|
105
|
+
from engine import sandbox as sb
|
|
106
|
+
|
|
107
|
+
with tempfile.TemporaryDirectory() as d:
|
|
108
|
+
allowed_dir = os.path.join(d, "safe")
|
|
109
|
+
sibling = os.path.join(d, "safe-x")
|
|
110
|
+
self.assertTrue(
|
|
111
|
+
sb._matches_allowlist(os.path.join(allowed_dir, "tool.exe"), [allowed_dir])
|
|
112
|
+
)
|
|
113
|
+
# Without the boundary fix, prefix "d/safe" would match "d/safe-x/app.exe".
|
|
114
|
+
self.assertFalse(sb._matches_allowlist(os.path.join(sibling, "app.exe"), [allowed_dir]))
|
|
115
|
+
|
|
116
|
+
def test_bare_entry_tolerates_exe_suffix(self):
|
|
117
|
+
from engine import sandbox as sb
|
|
118
|
+
|
|
119
|
+
base = os.path.basename(sys.executable)
|
|
120
|
+
stem = base[:-4] if base.lower().endswith(".exe") else base
|
|
121
|
+
self.assertTrue(sb._matches_allowlist(sys.executable, [stem]))
|
|
122
|
+
|
|
123
|
+
def test_path_qualified_sibling_entry_never_basename_matches(self):
|
|
124
|
+
from engine import sandbox as sb
|
|
125
|
+
|
|
126
|
+
with tempfile.TemporaryDirectory() as d:
|
|
127
|
+
# An entry that LOOKS like a directory must not match via basename.
|
|
128
|
+
entry = os.path.join(d, "tools")
|
|
129
|
+
self.assertFalse(sb._matches_allowlist(os.path.join(d, "tools", "x"), ["tools"]))
|
|
130
|
+
self.assertTrue(sb._matches_allowlist(os.path.join(d, "tools", "x"), [entry]))
|
|
131
|
+
|
|
132
|
+
def test_unresolvable_allowlisted_name_is_refused_not_spawned(self):
|
|
133
|
+
r = engine.sandbox_run(
|
|
134
|
+
["definitely-not-a-real-bin-xyz"],
|
|
135
|
+
5,
|
|
136
|
+
allowed_prefixes=["definitely-not-a-real-bin-xyz"],
|
|
137
|
+
)
|
|
138
|
+
self.assertEqual(r["exit_code"], -10)
|
|
139
|
+
|
|
140
|
+
def test_cwd_planted_executable_cannot_shadow_allowlisted_basename(self):
|
|
141
|
+
exe_name = os.path.basename(sys.executable)
|
|
142
|
+
with tempfile.TemporaryDirectory() as d:
|
|
143
|
+
with open(os.path.join(d, exe_name), "w", encoding="utf-8") as fh:
|
|
144
|
+
fh.write("print('HIJACKED')\n")
|
|
145
|
+
r = engine.sandbox_run(
|
|
146
|
+
[exe_name, "-c", "print('clean')"],
|
|
147
|
+
20,
|
|
148
|
+
cwd=d,
|
|
149
|
+
allowed_prefixes=[exe_name],
|
|
150
|
+
)
|
|
151
|
+
self.assertEqual(r["exit_code"], 0, r)
|
|
152
|
+
self.assertNotIn("HIJACKED", r["stdout"])
|
|
153
|
+
self.assertIn("clean", r["stdout"])
|
|
154
|
+
|
|
155
|
+
def test_relative_command_resolves_against_cwd_not_server(self):
|
|
156
|
+
# Policy-checked path must BE the executed path: a relative command
|
|
157
|
+
# resolves against the run cwd, so a planted binary in the caller
|
|
158
|
+
# cwd is judged where it actually lives, not where the server sits.
|
|
159
|
+
from engine import sandbox as sb
|
|
160
|
+
|
|
161
|
+
with tempfile.TemporaryDirectory() as d:
|
|
162
|
+
real = os.path.join(d, "real")
|
|
163
|
+
planted = os.path.join(d, "planted")
|
|
164
|
+
os.makedirs(real)
|
|
165
|
+
os.makedirs(planted)
|
|
166
|
+
r = sb.sandbox_run(["./prog.exe"], 5, cwd=planted, allowed_prefixes=[real])
|
|
167
|
+
self.assertEqual(r["exit_code"], -10, r) # outside the allowed dir
|
|
168
|
+
r2 = sb.sandbox_run(["./prog.exe"], 5, cwd=real, allowed_prefixes=[real])
|
|
169
|
+
self.assertNotEqual(r2["exit_code"], -10, r2) # policy passed (file absent -> -2)
|
|
170
|
+
|
|
171
|
+
def test_env_scrub_drops_credential_like_vars(self):
|
|
172
|
+
marker = "AGENTSEED_TEST_FAKE_API_TOKEN"
|
|
173
|
+
os.environ[marker] = "leak-me"
|
|
174
|
+
try:
|
|
175
|
+
code = (
|
|
176
|
+
"import os, sys; sys.stdout.write("
|
|
177
|
+
f"'TOKEN-SEEN' if os.environ.get({marker!r}) else 'SCRUBBED')"
|
|
178
|
+
)
|
|
179
|
+
scrubbed = engine.sandbox_run([sys.executable, "-c", code], 20, env_mode="scrub")
|
|
180
|
+
inherited = engine.sandbox_run([sys.executable, "-c", code], 20, env_mode="inherit")
|
|
181
|
+
finally:
|
|
182
|
+
os.environ.pop(marker, None)
|
|
183
|
+
self.assertEqual(scrubbed["exit_code"], 0, scrubbed)
|
|
184
|
+
self.assertIn("SCRUBBED", scrubbed["stdout"], scrubbed)
|
|
185
|
+
self.assertIn("TOKEN-SEEN", inherited["stdout"])
|
|
186
|
+
|
|
187
|
+
def test_timeout_reaps_grandchild(self):
|
|
188
|
+
import subprocess as sp
|
|
189
|
+
import time as _time
|
|
190
|
+
|
|
191
|
+
inner = (
|
|
192
|
+
"import subprocess\n"
|
|
193
|
+
f"child = subprocess.Popen([{sys.executable!r}, '-c', 'import time; time.sleep(60)'])\n"
|
|
194
|
+
"print(child.pid, flush=True)\n"
|
|
195
|
+
"child.wait()\n"
|
|
196
|
+
)
|
|
197
|
+
r = engine.sandbox_run([sys.executable, "-c", inner], 2)
|
|
198
|
+
self.assertTrue(r["timed_out"], r)
|
|
199
|
+
gc_pid = int(r["stdout"].strip())
|
|
200
|
+
|
|
201
|
+
if os.name == "nt":
|
|
202
|
+
listing = sp.run(
|
|
203
|
+
["tasklist", "/FI", f"PID eq {gc_pid}"],
|
|
204
|
+
capture_output=True,
|
|
205
|
+
text=True,
|
|
206
|
+
encoding="utf-8",
|
|
207
|
+
errors="replace",
|
|
208
|
+
).stdout.lower()
|
|
209
|
+
self.assertNotIn(str(gc_pid), listing.replace(",", ""), listing)
|
|
210
|
+
elif os.path.isdir("/proc"):
|
|
211
|
+
# A SIGKILLed orphan can linger as a zombie until reaped; signal-0
|
|
212
|
+
# probes succeed on zombies, so inspect the kernel state instead.
|
|
213
|
+
def _alive(pid: int) -> bool:
|
|
214
|
+
try:
|
|
215
|
+
with open(f"/proc/{pid}/stat", encoding="utf-8") as fh:
|
|
216
|
+
return fh.read().rsplit(")", 1)[1].split()[0] != "Z"
|
|
217
|
+
except (FileNotFoundError, ProcessLookupError):
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
deadline = _time.monotonic() + 5.0
|
|
221
|
+
while _alive(gc_pid) and _time.monotonic() < deadline:
|
|
222
|
+
_time.sleep(0.2)
|
|
223
|
+
self.assertFalse(_alive(gc_pid), f"grandchild {gc_pid} survived tree-kill")
|
|
224
|
+
# other platforms: timed_out assertion above is the contract
|
|
225
|
+
|
|
226
|
+
def test_cli_sandbox_policy_block_exits_nonzero(self):
|
|
227
|
+
import subprocess
|
|
228
|
+
|
|
229
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
230
|
+
cli = os.path.join(here, "guard_cli.py")
|
|
231
|
+
with tempfile.TemporaryDirectory() as d:
|
|
232
|
+
cfg = os.path.join(d, "agentseed.config.json")
|
|
233
|
+
with open(cfg, "w", encoding="utf-8") as fh:
|
|
234
|
+
json.dump({"sandbox_allowed_prefixes": ["definitely-not-a-bin"]}, fh)
|
|
235
|
+
env = dict(os.environ, AGENTSEED_CONFIG=cfg)
|
|
236
|
+
proc = subprocess.run(
|
|
237
|
+
[sys.executable, cli, "sandbox", "--", "other-unlisted-tool", "--flag"],
|
|
238
|
+
capture_output=True,
|
|
239
|
+
text=True,
|
|
240
|
+
encoding="utf-8",
|
|
241
|
+
errors="replace",
|
|
242
|
+
env=env,
|
|
243
|
+
timeout=60,
|
|
244
|
+
cwd=here,
|
|
245
|
+
)
|
|
246
|
+
self.assertEqual(proc.returncode, 1)
|
|
247
|
+
self.assertIn("-10", proc.stdout)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class TestConfigUnknownKeysAndExtras(unittest.TestCase):
|
|
251
|
+
def test_unknown_keys_listed(self):
|
|
252
|
+
self.assertEqual(
|
|
253
|
+
engine.unknown_config_keys({"allowlist": [], "alowlist": [], "wht": 1}),
|
|
254
|
+
["alowlist", "wht"],
|
|
255
|
+
)
|
|
256
|
+
self.assertEqual(engine.unknown_config_keys({}), [])
|
|
257
|
+
|
|
258
|
+
def test_extra_tokens_validator(self):
|
|
259
|
+
out = engine.config_extra_tokens({"extra_tokens": {"stub_code": ["待办"], "bogus": ["x"]}})
|
|
260
|
+
self.assertEqual(out, {"stub_code": ["待办"]})
|
|
261
|
+
self.assertIsNone(engine.config_extra_tokens({"extra_tokens": "x"}))
|
|
262
|
+
self.assertIsNone(engine.config_extra_tokens({}))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class TestAuditTrail(unittest.TestCase):
|
|
266
|
+
def test_record_appends_jsonl(self):
|
|
267
|
+
with tempfile.TemporaryDirectory() as d:
|
|
268
|
+
r1 = engine.record_verification(
|
|
269
|
+
"task A", [{"tool": "verify_code", "status": "pass"}], data_dir=d
|
|
270
|
+
)
|
|
271
|
+
self.assertTrue(r1["ok"])
|
|
272
|
+
r2 = engine.record_verification(
|
|
273
|
+
"task B",
|
|
274
|
+
[
|
|
275
|
+
{"tool": "sandbox_run", "status": "fail", "summary": "boom"},
|
|
276
|
+
{"tool": "bogus", "status": "invalid-status"},
|
|
277
|
+
],
|
|
278
|
+
data_dir=d,
|
|
279
|
+
)
|
|
280
|
+
self.assertEqual(r2["entries"], 2)
|
|
281
|
+
with open(r2["path"], encoding="utf-8") as fh:
|
|
282
|
+
lines = [json.loads(ln) for ln in fh if ln.strip()]
|
|
283
|
+
self.assertEqual(len(lines), 2)
|
|
284
|
+
self.assertEqual(lines[0]["checks"][0]["tool"], "verify_code")
|
|
285
|
+
self.assertEqual(
|
|
286
|
+
lines[1]["checks"], [{"tool": "sandbox_run", "status": "fail", "summary": "boom"}]
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def test_record_rejects_blank_task(self):
|
|
290
|
+
self.assertFalse(engine.record_verification(" ", [])["ok"])
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class TestExampleFixtures(unittest.TestCase):
|
|
294
|
+
EXAMPLES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "examples", "plugins")
|
|
295
|
+
|
|
296
|
+
def test_good_plugin_conforms(self):
|
|
297
|
+
r = engine.check_plugin_conformance(
|
|
298
|
+
os.path.abspath(os.path.join(self.EXAMPLES, "good-plugin"))
|
|
299
|
+
)
|
|
300
|
+
self.assertTrue(r["ok"], r["errors"])
|
|
301
|
+
|
|
302
|
+
def test_broken_plugin_flagged(self):
|
|
303
|
+
r = engine.check_plugin_conformance(
|
|
304
|
+
os.path.abspath(os.path.join(self.EXAMPLES, "broken-plugin"))
|
|
305
|
+
)
|
|
306
|
+
self.assertFalse(r["ok"])
|
|
307
|
+
joined = " ".join(r["errors"])
|
|
308
|
+
self.assertIn("Broken-Demo", joined)
|
|
309
|
+
self.assertIn("privateExtra", joined)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestPerfBaseline(unittest.TestCase):
|
|
313
|
+
"""Loose gate: a pathological slowdown must not ship silently."""
|
|
314
|
+
|
|
315
|
+
def test_1mb_source_under_30s(self):
|
|
316
|
+
sys.path.insert(
|
|
317
|
+
0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts")
|
|
318
|
+
)
|
|
319
|
+
import bench # noqa: PLC0415
|
|
320
|
+
|
|
321
|
+
src = bench.make_source(1.0)
|
|
322
|
+
t0 = time.perf_counter()
|
|
323
|
+
engine.detect_undefined_symbols(src)
|
|
324
|
+
engine.scan_hallucination_words(src)
|
|
325
|
+
elapsed = time.perf_counter() - t0
|
|
326
|
+
self.assertLess(elapsed, 30.0, f"1MB baseline regressed: {elapsed:.1f}s")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TestCliRecordAndAsyncPolicy(unittest.TestCase):
|
|
330
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
331
|
+
|
|
332
|
+
def _run(self, *args: str, env_extra=None):
|
|
333
|
+
import subprocess
|
|
334
|
+
|
|
335
|
+
env = dict(os.environ)
|
|
336
|
+
if env_extra:
|
|
337
|
+
env.update(env_extra)
|
|
338
|
+
return subprocess.run(
|
|
339
|
+
[sys.executable, os.path.join(self.HERE, "guard_cli.py"), *args],
|
|
340
|
+
capture_output=True,
|
|
341
|
+
text=True,
|
|
342
|
+
encoding="utf-8",
|
|
343
|
+
errors="replace",
|
|
344
|
+
timeout=60,
|
|
345
|
+
cwd=self.HERE,
|
|
346
|
+
env=env,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def test_cli_record_writes_log(self):
|
|
350
|
+
with tempfile.TemporaryDirectory() as d:
|
|
351
|
+
r = self._run(
|
|
352
|
+
"record",
|
|
353
|
+
"fix #7",
|
|
354
|
+
"--check",
|
|
355
|
+
"verify_code=pass",
|
|
356
|
+
"--check",
|
|
357
|
+
"scan=fail",
|
|
358
|
+
"--data-dir",
|
|
359
|
+
d,
|
|
360
|
+
)
|
|
361
|
+
self.assertEqual(r.returncode, 0, r.stderr)
|
|
362
|
+
log = os.path.join(d, "verification-log.jsonl")
|
|
363
|
+
with open(log, encoding="utf-8") as fh:
|
|
364
|
+
entries = [json.loads(ln) for ln in fh if ln.strip()]
|
|
365
|
+
self.assertEqual(len(entries), 1)
|
|
366
|
+
self.assertEqual(entries[0]["task"], "fix #7")
|
|
367
|
+
statuses = [c["status"] for c in entries[0]["checks"]]
|
|
368
|
+
self.assertEqual(statuses, ["pass", "fail"])
|
|
369
|
+
|
|
370
|
+
def test_async_sandbox_policy_blocks_via_server(self):
|
|
371
|
+
import subprocess
|
|
372
|
+
|
|
373
|
+
with tempfile.TemporaryDirectory() as d:
|
|
374
|
+
cfg = os.path.join(d, "cfg.json")
|
|
375
|
+
with open(cfg, "w", encoding="utf-8") as fh:
|
|
376
|
+
json.dump({"sandbox_allowed_prefixes": ["only-this-bin"]}, fh)
|
|
377
|
+
proc = subprocess.Popen(
|
|
378
|
+
[sys.executable, "-u", os.path.join(self.HERE, "guard_server.py")],
|
|
379
|
+
stdin=subprocess.PIPE,
|
|
380
|
+
stdout=subprocess.PIPE,
|
|
381
|
+
stderr=subprocess.DEVNULL,
|
|
382
|
+
env=dict(os.environ, AGENTSEED_CONFIG=cfg),
|
|
383
|
+
)
|
|
384
|
+
try:
|
|
385
|
+
req = {
|
|
386
|
+
"jsonrpc": "2.0",
|
|
387
|
+
"id": 1,
|
|
388
|
+
"method": "tools/call",
|
|
389
|
+
"params": {
|
|
390
|
+
"name": "sandbox_run",
|
|
391
|
+
"arguments": {"command": ["another-unlisted-bin", "x"]},
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
proc.stdin.write((json.dumps(req) + "\n").encode())
|
|
395
|
+
proc.stdin.flush()
|
|
396
|
+
frame = json.loads(proc.stdout.readline().decode())
|
|
397
|
+
text = json.loads(frame["result"]["content"][0]["text"])
|
|
398
|
+
self.assertEqual(text["exit_code"], -10)
|
|
399
|
+
finally:
|
|
400
|
+
# kill alone leaves the process un-reaped (ResourceWarning) and
|
|
401
|
+
# the stdin/stdout pipe buffers unclosed — wait() reaps it and
|
|
402
|
+
# closing the pipes silences the unclosed-file warnings.
|
|
403
|
+
proc.kill()
|
|
404
|
+
proc.wait()
|
|
405
|
+
for _pipe in (proc.stdin, proc.stdout):
|
|
406
|
+
if _pipe is not None and not _pipe.closed:
|
|
407
|
+
_pipe.close()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class TestDetectionBenchmark(unittest.TestCase):
|
|
411
|
+
"""Regression lock: every injected defect class must stay caught with
|
|
412
|
+
zero false positives on the clean set (seeded synthetic corpus)."""
|
|
413
|
+
|
|
414
|
+
def test_corpus_precision_and_recall_are_perfect(self):
|
|
415
|
+
sys.path.insert(
|
|
416
|
+
0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts")
|
|
417
|
+
)
|
|
418
|
+
import bench_detection # noqa: PLC0415
|
|
419
|
+
|
|
420
|
+
report = bench_detection.evaluate(bench_detection.build_corpus(4, 8, seed=7))
|
|
421
|
+
self.assertEqual(report["totals"]["fn"], 0, report)
|
|
422
|
+
self.assertEqual(report["totals"]["fp"], 0, report)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if __name__ == "__main__":
|
|
426
|
+
unittest.main(verbosity=2)
|