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.
Files changed (76) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/LICENSE +202 -0
  3. package/README.ja.md +320 -0
  4. package/README.md +318 -0
  5. package/README.zh.md +306 -0
  6. package/bin/cli.js +37 -0
  7. package/mcp.json +12 -0
  8. package/package.json +30 -0
  9. package/plugin.json +22 -0
  10. package/server/.agentseed/verification-log.jsonl +2 -0
  11. package/server/__pycache__/guard_cli.cpython-313.pyc +0 -0
  12. package/server/__pycache__/guard_engine.cpython-313.pyc +0 -0
  13. package/server/__pycache__/test_cli.cpython-313-pytest-9.1.1.pyc +0 -0
  14. package/server/__pycache__/test_cli.cpython-313.pyc +0 -0
  15. package/server/__pycache__/test_features.cpython-313-pytest-9.1.1.pyc +0 -0
  16. package/server/__pycache__/test_features.cpython-313.pyc +0 -0
  17. package/server/__pycache__/test_guard.cpython-313-pytest-9.1.1.pyc +0 -0
  18. package/server/__pycache__/test_guard.cpython-313.pyc +0 -0
  19. package/server/__pycache__/test_hook.cpython-313-pytest-9.1.1.pyc +0 -0
  20. package/server/__pycache__/test_hook.cpython-313.pyc +0 -0
  21. package/server/__pycache__/test_manifests.cpython-313-pytest-9.1.1.pyc +0 -0
  22. package/server/__pycache__/test_manifests.cpython-313.pyc +0 -0
  23. package/server/__pycache__/test_server.cpython-313-pytest-9.1.1.pyc +0 -0
  24. package/server/__pycache__/test_server.cpython-313.pyc +0 -0
  25. package/server/engine/__init__.py +64 -0
  26. package/server/engine/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/server/engine/__pycache__/audit.cpython-313.pyc +0 -0
  28. package/server/engine/__pycache__/config.cpython-313.pyc +0 -0
  29. package/server/engine/__pycache__/hallucination.cpython-313.pyc +0 -0
  30. package/server/engine/__pycache__/imports.cpython-313.pyc +0 -0
  31. package/server/engine/__pycache__/plugin.cpython-313.pyc +0 -0
  32. package/server/engine/__pycache__/sandbox.cpython-313.pyc +0 -0
  33. package/server/engine/__pycache__/schema.cpython-313.pyc +0 -0
  34. package/server/engine/__pycache__/symbols.cpython-313.pyc +0 -0
  35. package/server/engine/__pycache__/version.cpython-313.pyc +0 -0
  36. package/server/engine/audit.py +84 -0
  37. package/server/engine/config.py +131 -0
  38. package/server/engine/hallucination.py +254 -0
  39. package/server/engine/imports.py +136 -0
  40. package/server/engine/plugin.py +367 -0
  41. package/server/engine/sandbox.py +287 -0
  42. package/server/engine/schema.py +193 -0
  43. package/server/engine/symbols.py +984 -0
  44. package/server/engine/version.py +17 -0
  45. package/server/guard_cli.py +455 -0
  46. package/server/guard_engine.py +111 -0
  47. package/server/guard_hook.py +404 -0
  48. package/server/guard_server.py +472 -0
  49. package/server/requirements.txt +7 -0
  50. package/server/test_cli.py +132 -0
  51. package/server/test_features.py +426 -0
  52. package/server/test_guard.py +828 -0
  53. package/server/test_hook.py +331 -0
  54. package/server/test_manifests.py +70 -0
  55. package/server/test_server.py +247 -0
  56. package/skills/verify-before-code/SKILL.ja.md +116 -0
  57. package/skills/verify-before-code/SKILL.md +140 -0
  58. package/skills/verify-before-code/SKILL.zh.md +117 -0
  59. package/skills/verify-before-code/references/DEFAULT-NORMS.md +52 -0
  60. package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.ja.md +121 -0
  61. package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.md +166 -0
  62. package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.zh.md +145 -0
  63. package/skills/verify-before-code/references/PROMPT-POOL.ja.md +248 -0
  64. package/skills/verify-before-code/references/PROMPT-POOL.md +282 -0
  65. package/skills/verify-before-code/references/PROMPT-POOL.zh.md +252 -0
  66. package/skills/verify-before-code/references/SDD-CONTRACT.ja.md +61 -0
  67. package/skills/verify-before-code/references/SDD-CONTRACT.md +66 -0
  68. package/skills/verify-before-code/references/SDD-CONTRACT.zh.md +58 -0
  69. package/skills/verify-before-code/references/VENDOR-SOLUTIONS.ja.md +62 -0
  70. package/skills/verify-before-code/references/VENDOR-SOLUTIONS.md +62 -0
  71. package/skills/verify-before-code/references/VENDOR-SOLUTIONS.zh.md +54 -0
  72. package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.ja.md +68 -0
  73. package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.md +73 -0
  74. package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.zh.md +68 -0
  75. package/skills/verify-before-code/scripts/check.ps1 +52 -0
  76. package/skills/verify-before-code/scripts/check.sh +44 -0
@@ -0,0 +1,828 @@
1
+ """AgentSeed guard engine unit tests (stdlib unittest, zero deps)."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import unittest
7
+
8
+ import guard_engine as engine # noqa: E402
9
+
10
+ PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
11
+ # sys.executable works on every platform; the literal "python3" does not
12
+ # exist on many Windows installs (the WindowsApps alias is a Store stub).
13
+ PY = sys.executable
14
+
15
+
16
+ class TestUndefinedSymbols(unittest.TestCase):
17
+ def test_catches_hallucinated_call(self):
18
+ r = engine.detect_undefined_symbols("def f():\n return magic_unknown()\n")
19
+ self.assertIn("magic_unknown", r["suspects"])
20
+
21
+ def test_clean_code_passes(self):
22
+ r = engine.detect_undefined_symbols("import math\nprint(math.sqrt(4))\n")
23
+ self.assertEqual(r["suspects"], [])
24
+
25
+ def test_syntax_error_reported(self):
26
+ r = engine.detect_undefined_symbols("def f(:\n")
27
+ self.assertEqual(r["suspects"], [])
28
+ self.assertIn("syntax", r["note"])
29
+
30
+ def test_ts_catches_hallucinated_call(self):
31
+ src = "import fs from 'fs';\nfunction read(path) { return fs.readFileSync(path); }\nreadFile('../x');\n"
32
+ r = engine.detect_undefined_symbols(src, "typescript")
33
+ self.assertIn("readFile", r["suspects"])
34
+
35
+ def test_ts_clean_imports(self):
36
+ src = "import { join } from 'path';\nimport fs from 'fs';\nconsole.log(join('a', 'b'));\nfs.readFileSync('x');\n"
37
+ r = engine.detect_undefined_symbols(src, "typescript")
38
+ self.assertEqual(r["suspects"], [])
39
+
40
+ def test_ts_keywords_not_flagged(self):
41
+ src = "function f(x: number) {\n if (x > 0) return x;\n return 0;\n}\n"
42
+ r = engine.detect_undefined_symbols(src, "typescript")
43
+ self.assertEqual(r["suspects"], [])
44
+
45
+ def test_ts_call_args_are_not_definitions(self):
46
+ # `wrap(helper)` must not define `helper`; `helper()` must be flagged
47
+ src = "helper();\nwrap(helper);\n"
48
+ r = engine.detect_undefined_symbols(src, "typescript")
49
+ self.assertIn("helper", r["suspects"])
50
+ self.assertIn("wrap", r["suspects"])
51
+
52
+ def test_ts_multi_declaration_collected(self):
53
+ src = "const a = 1, b = 2;\nconsole.log(a + b);\n"
54
+ r = engine.detect_undefined_symbols(src, "typescript")
55
+ self.assertEqual(r["suspects"], [])
56
+
57
+ def test_python_module_dunders_not_flagged(self):
58
+ src = 'if __name__ == "__main__":\n print(__file__, __doc__)\n'
59
+ r = engine.detect_undefined_symbols(src, "python")
60
+ self.assertEqual(r["suspects"], [])
61
+
62
+ def test_python_local_assignment_not_flagged(self):
63
+ src = "def f():\n total = len([1, 2])\n return total\n"
64
+ r = engine.detect_undefined_symbols(src, "python")
65
+ self.assertEqual(r["suspects"], [])
66
+
67
+ def test_python_for_with_except_targets_not_flagged(self):
68
+ src = (
69
+ "def f(items, path):\n"
70
+ " out = []\n"
71
+ " for it in items:\n"
72
+ " out.append(it)\n"
73
+ " try:\n"
74
+ " with open(path) as fh:\n"
75
+ " out.append(fh.read())\n"
76
+ " except OSError as exc:\n"
77
+ " out.append(str(exc))\n"
78
+ " return out\n"
79
+ )
80
+ r = engine.detect_undefined_symbols(src, "python")
81
+ self.assertEqual(r["suspects"], [])
82
+
83
+ def test_python_walrus_and_augassign_not_flagged(self):
84
+ src = "def f(n):\n count = 0\n count += n\n if (big := count * 2) > 10:\n return big\n return count\n"
85
+ r = engine.detect_undefined_symbols(src, "python")
86
+ self.assertEqual(r["suspects"], [])
87
+
88
+ def test_python_comprehension_and_global_not_flagged(self):
89
+ src = (
90
+ "counter = 0\n"
91
+ "def f(vals):\n"
92
+ " global counter\n"
93
+ " counter += 1\n"
94
+ " return [v * 2 for v in vals]\n"
95
+ )
96
+ r = engine.detect_undefined_symbols(src, "python")
97
+ self.assertEqual(r["suspects"], [])
98
+
99
+
100
+ class TestGenericLanguages(unittest.TestCase):
101
+ """Config-driven generic lexical verify_code for the registered languages."""
102
+
103
+ def test_go_catches_hallucinated_call(self):
104
+ src = "package main\n\nfunc main() {\n ghost()\n}\n"
105
+ r = engine.detect_undefined_symbols(src, "go")
106
+ self.assertIn("ghost", r["suspects"])
107
+
108
+ def test_go_clean_defined_and_attribute_calls(self):
109
+ src = (
110
+ "package main\n"
111
+ 'import "fmt"\n'
112
+ "func helper(x int) int { return x * 2 }\n"
113
+ "func main() {\n"
114
+ " fmt.Println(helper(21))\n"
115
+ "}\n"
116
+ )
117
+ r = engine.detect_undefined_symbols(src, "golang")
118
+ self.assertEqual(r["suspects"], [])
119
+
120
+ def test_go_short_declaration_not_flagged(self):
121
+ src = "func main() {\n total := 0\n total += 1\n}\n"
122
+ r = engine.detect_undefined_symbols(src, "go")
123
+ self.assertEqual(r["suspects"], [])
124
+
125
+ def test_rust_catches_hallucinated_call(self):
126
+ src = "fn main() {\n ghost();\n}\n"
127
+ r = engine.detect_undefined_symbols(src, "rust")
128
+ self.assertIn("ghost", r["suspects"])
129
+
130
+ def test_rust_clean_module_with_path_calls(self):
131
+ src = (
132
+ "use std::io;\n"
133
+ "fn helper(x: i32) -> i32 { x + 1 }\n"
134
+ "fn main() {\n"
135
+ " let y = helper(1);\n"
136
+ " println!(\"{}\", y);\n"
137
+ " io::stdout().flush();\n"
138
+ "}\n"
139
+ )
140
+ r = engine.detect_undefined_symbols(src, "rs")
141
+ self.assertEqual(r["suspects"], [])
142
+
143
+ def test_java_catches_hallucinated_call(self):
144
+ src = "class A {\n void run() { ghost(); }\n}\n"
145
+ r = engine.detect_undefined_symbols(src, "java")
146
+ self.assertIn("ghost", r["suspects"])
147
+
148
+ def test_java_clean_methods_fields_and_imports(self):
149
+ src = (
150
+ "import java.util.List;\n"
151
+ "class Calc {\n"
152
+ " private int count = 0;\n"
153
+ " int add(int a, int b) { return a + b; }\n"
154
+ " void run() { System.out.println(add(1, 2)); }\n"
155
+ "}\n"
156
+ )
157
+ r = engine.detect_undefined_symbols(src, "java")
158
+ self.assertEqual(r["suspects"], [])
159
+
160
+ def test_c_catches_hallucinated_call(self):
161
+ src = "int main(void) { ghost(); return 0; }\n"
162
+ r = engine.detect_undefined_symbols(src, "c")
163
+ self.assertIn("ghost", r["suspects"])
164
+
165
+ def test_c_clean_with_libc_and_defs(self):
166
+ src = (
167
+ "#include <stdio.h>\n"
168
+ "int add(int a, int b) { return a + b; }\n"
169
+ "int main(void) { printf(\"%d\", add(1, 2)); return 0; }\n"
170
+ )
171
+ r = engine.detect_undefined_symbols(src, "c")
172
+ self.assertEqual(r["suspects"], [])
173
+
174
+ def test_cpp_clean_class_vars_and_fields(self):
175
+ src = (
176
+ "#include <vector>\n"
177
+ "#include <string>\n"
178
+ "class Box {\n"
179
+ "public:\n"
180
+ " Box(int s) : size(s) {}\n"
181
+ " int get() const { return size; }\n"
182
+ "private:\n"
183
+ " int size;\n"
184
+ "};\n"
185
+ "int main() { Box b(3); std::string s = \"x\"; return b.get(); }\n"
186
+ )
187
+ r = engine.detect_undefined_symbols(src, "cpp")
188
+ self.assertEqual(r["suspects"], [])
189
+
190
+ def test_cpp_catches_hallucinated_call(self):
191
+ src = "int main() { ghost(); return 0; }\n"
192
+ r = engine.detect_undefined_symbols(src, "c++")
193
+ self.assertIn("ghost", r["suspects"])
194
+
195
+ def test_csharp_catches_hallucinated_call(self):
196
+ src = "using System;\nclass P { static void Main() { Ghost(); } }\n"
197
+ r = engine.detect_undefined_symbols(src, "csharp")
198
+ self.assertIn("Ghost", r["suspects"])
199
+
200
+ def test_csharp_clean_console_call(self):
201
+ src = 'using System;\nclass P { static void Main() { Console.WriteLine("hi"); } }\n'
202
+ r = engine.detect_undefined_symbols(src, "c#")
203
+ self.assertEqual(r["suspects"], [])
204
+
205
+ def test_php_catches_hallucinated_call(self):
206
+ src = "<?php\nfunction run() { ghost(); }\nrun();\n"
207
+ r = engine.detect_undefined_symbols(src, "php")
208
+ self.assertIn("ghost", r["suspects"])
209
+
210
+ def test_php_clean_vars_and_functions(self):
211
+ src = (
212
+ "<?php\n"
213
+ "function add($a, $b) { return $a + $b; }\n"
214
+ "$x = 1;\n"
215
+ "$y = add($x, 2);\n"
216
+ "echo $y;\n"
217
+ )
218
+ r = engine.detect_undefined_symbols(src, "php")
219
+ self.assertEqual(r["suspects"], [])
220
+
221
+ def test_ruby_catches_hallucinated_call(self):
222
+ src = "def run\n ghost\nend\nrun\n"
223
+ r = engine.detect_undefined_symbols(src, "ruby")
224
+ self.assertIn("ghost", r["suspects"])
225
+
226
+ def test_ruby_clean(self):
227
+ src = "def helper(x)\n x * 2\nend\nputs helper(21)\n"
228
+ r = engine.detect_undefined_symbols(src, "rb")
229
+ self.assertEqual(r["suspects"], [])
230
+
231
+ def test_kotlin_catches_hallucinated_call(self):
232
+ src = "fun main() {\n ghost()\n}\n"
233
+ r = engine.detect_undefined_symbols(src, "kotlin")
234
+ self.assertIn("ghost", r["suspects"])
235
+
236
+ def test_kotlin_clean_import_and_lambdas(self):
237
+ src = (
238
+ "import kotlin.math.sqrt\n"
239
+ "fun main() {\n"
240
+ " val x = 4\n"
241
+ " println(sqrt(x.toDouble()))\n"
242
+ "}\n"
243
+ )
244
+ r = engine.detect_undefined_symbols(src, "kt")
245
+ self.assertEqual(r["suspects"], [])
246
+
247
+ def test_swift_catches_hallucinated_call(self):
248
+ src = "func run() {\n ghost()\n}\n"
249
+ r = engine.detect_undefined_symbols(src, "swift")
250
+ self.assertIn("ghost", r["suspects"])
251
+
252
+ def test_swift_clean(self):
253
+ src = (
254
+ "import Foundation\n"
255
+ "func helper(_ x: Int) -> Int { x + 1 }\n"
256
+ "let y = helper(1)\n"
257
+ "print(y)\n"
258
+ )
259
+ r = engine.detect_undefined_symbols(src, "swift")
260
+ self.assertEqual(r["suspects"], [])
261
+
262
+ def test_unsupported_language_reports_note(self):
263
+ r = engine.detect_undefined_symbols("print('x')", "brainfuck")
264
+ self.assertEqual(r["suspects"], [])
265
+ self.assertIn("Unsupported", r["note"])
266
+ self.assertIn("go", r["note"])
267
+
268
+
269
+ class TestStreamedSandbox(unittest.TestCase):
270
+ def test_large_output_kept_bounded_tail(self):
271
+ # 100k chars then a marker: memory must stay bounded AND the tail
272
+ # (last 8000 chars) must survive — the marker proves tail semantics.
273
+ src = "import sys\nprint('X' * 100000)\nprint('TAIL_MARKER')\n"
274
+ r = engine.sandbox_run([PY, "-c", src], 15)
275
+ self.assertTrue(r["stdout"].rstrip().endswith("TAIL_MARKER"), repr(r["stdout"][-80:]))
276
+ self.assertLessEqual(len(r["stdout"]), 8000)
277
+
278
+ def test_clean_output_unaffected(self):
279
+ r = engine.sandbox_run([PY, "-c", "print(6*7)"], 10)
280
+ self.assertIn("42", r["stdout"])
281
+
282
+
283
+ class TestCheckContract(unittest.TestCase):
284
+ def test_requires_missing_fails(self):
285
+ r = engine.check_contract(
286
+ "def run():\n return 1\n", '{"requires": ["run", "ghost"]}'
287
+ )
288
+ self.assertFalse(r["contract_ok"])
289
+ self.assertEqual(r["missing"], ["ghost"])
290
+
291
+ def test_requires_satisfied_passes(self):
292
+ r = engine.check_contract(
293
+ "def run():\n return helper()\ndef helper():\n return 1\n",
294
+ '{"requires": ["run", "helper"]}',
295
+ )
296
+ self.assertTrue(r["contract_ok"])
297
+ self.assertEqual(r["missing"], [])
298
+
299
+ def test_prohibits_token_hit(self):
300
+ r = engine.check_contract(
301
+ "def run():\n return 1 # TODO\n", '{"prohibits": ["TODO"]}'
302
+ )
303
+ self.assertFalse(r["contract_ok"])
304
+ self.assertEqual(r["prohibited_hits"], ["TODO"])
305
+
306
+ def test_prohibits_clean(self):
307
+ r = engine.check_contract("def run():\n return 1\n", '{"prohibits": ["TODO"]}')
308
+ self.assertTrue(r["contract_ok"])
309
+
310
+ def test_invalid_contract_json(self):
311
+ r = engine.check_contract("x = 1\n", "not json")
312
+ self.assertFalse(r["contract_ok"])
313
+ self.assertIn("invalid", r["note"])
314
+
315
+ def test_go_contract_uses_registry_definitions(self):
316
+ r = engine.check_contract(
317
+ "package main\nfunc helper(x int) int { return x }\n",
318
+ '{"requires": ["helper"]}',
319
+ "go",
320
+ )
321
+ self.assertTrue(r["contract_ok"])
322
+
323
+ def test_defined_symbols_public_helper(self):
324
+ ds = engine.defined_symbols(
325
+ "package main\nfunc helper(x int) int { return x }\n", "go"
326
+ )
327
+ self.assertIn("helper", ds)
328
+
329
+
330
+ class TestExportPromptPool(unittest.TestCase):
331
+ def test_parse_and_render_deterministic(self):
332
+ sys.path.insert(0, os.path.join(PLUGIN_ROOT, "scripts"))
333
+ import export_prompt_pool as xp
334
+
335
+ pool = os.path.join(
336
+ PLUGIN_ROOT, "skills", "verify-before-code", "references", "PROMPT-POOL.md"
337
+ )
338
+ entries = xp.parse_pool(pool)
339
+ self.assertGreaterEqual(len(entries), 20) # 23 entries today
340
+ for fmt in ("claude", "agents", "cursor"):
341
+ out = xp.render(fmt, entries)
342
+ self.assertIn("## A1", out)
343
+ self.assertIn("## J4", out)
344
+ self.assertEqual(out, xp.render(fmt, entries)) # deterministic
345
+
346
+
347
+ class TestPerformanceBaseline(unittest.TestCase):
348
+ """Regression gate: verify_code + scan_hallucination on a ~0.5 MB
349
+ synthetic corpus must stay under 10 s (real baseline ~0.4 s). Catches a
350
+ pathological regression without flaking on slow CI. Uses the same
351
+ generator as ``scripts/bench.py`` so the measurement is comparable."""
352
+
353
+ def test_half_megabyte_under_ten_seconds(self):
354
+ import time
355
+
356
+ scripts_dir = os.path.join(PLUGIN_ROOT, "scripts")
357
+ sys.path.insert(0, scripts_dir)
358
+ try:
359
+ import bench # noqa: PLC0415
360
+ src = bench.make_source(0.5)
361
+ t0 = time.perf_counter()
362
+ engine.detect_undefined_symbols(src)
363
+ engine.scan_hallucination_words(src)
364
+ elapsed = time.perf_counter() - t0
365
+ finally:
366
+ sys.path.remove(scripts_dir)
367
+ self.assertLess(
368
+ elapsed, 10.0,
369
+ f"hot path took {elapsed:.2f}s on ~0.5MB synthetic source",
370
+ )
371
+
372
+
373
+ class TestCheckImports(unittest.TestCase):
374
+ """Package-hallucination (slopsquatting) guard — USENIX Security 2025."""
375
+
376
+ def test_stdlib_and_common_known_pass(self):
377
+ src = "import os\nimport json\nimport requests\nimport numpy as np\nfrom pathlib import Path\n"
378
+ r = engine.check_imports(src)
379
+ self.assertTrue(r["imports_ok"])
380
+ self.assertEqual(r["suspicious"], [])
381
+
382
+ def test_unknown_package_flagged(self):
383
+ src = "import os\nimport slopsquat_utils\n"
384
+ r = engine.check_imports(src)
385
+ self.assertFalse(r["imports_ok"])
386
+ self.assertEqual([s["package"] for s in r["suspicious"]], ["slopsquat_utils"])
387
+
388
+ def test_known_packages_allowlist(self):
389
+ src = "import os\nimport mycompany_core\n"
390
+ r = engine.check_imports(src, known_packages=["mycompany_core"])
391
+ self.assertTrue(r["imports_ok"])
392
+
393
+ def test_relative_imports_skipped(self):
394
+ # from . import x / from ..pkg import y — package-local, never flagged
395
+ src = "from . import helpers\nfrom ..db import session\nimport os\n"
396
+ r = engine.check_imports(src)
397
+ self.assertTrue(r["imports_ok"])
398
+
399
+ def test_other_language_honest_empty(self):
400
+ r = engine.check_imports("package main\nimport \"fmt\"\n", "go")
401
+ self.assertTrue(r["imports_ok"])
402
+ self.assertIn("python", r["note"])
403
+
404
+ def test_new_research_tokens_flagged(self):
405
+ # tokens added from 2025 research (slopsquatting + overclaim/fabrication)
406
+ r = engine.scan_hallucination_words(
407
+ "This is bulletproof and cannot fail. The data is fictitious and non-existent."
408
+ )
409
+ groups = {h["group"] for h in r["hits"]}
410
+ self.assertIn("oversold", groups)
411
+ self.assertIn("fabricated", groups)
412
+ r2 = engine.scan_hallucination_words("这个包是凭空捏造的,子虚乌有,绝对可靠。")
413
+ self.assertFalse(r2["clean"])
414
+
415
+
416
+ class TestHallucinationScan(unittest.TestCase):
417
+ def test_stub_group(self):
418
+ r = engine.scan_hallucination_words("def run():\n return stub_result # TODO\n")
419
+ self.assertFalse(r["clean"])
420
+ groups = {h["group"] for h in r["hits"]}
421
+ self.assertIn("stub_code", groups)
422
+
423
+ def test_oversold_group(self):
424
+ r = engine.scan_hallucination_words("The feature is production ready, all tests pass.")
425
+ self.assertFalse(r["clean"])
426
+ groups = {h["group"] for h in r["hits"]}
427
+ self.assertIn("oversold", groups)
428
+
429
+ def test_fabricated_group(self):
430
+ r = engine.scan_hallucination_words("this is a simulated example")
431
+ self.assertFalse(r["clean"])
432
+ groups = {h["group"] for h in r["hits"]}
433
+ self.assertIn("fabricated", groups)
434
+
435
+ def test_clean(self):
436
+ r = engine.scan_hallucination_words("import os\nprint(os.getcwd())\n")
437
+ self.assertTrue(r["clean"])
438
+
439
+ def test_unittest_mock_not_flagged(self):
440
+ src = "from unittest.mock import Mock\nm = Mock()\nm.fake_method.return_value = 1\n"
441
+ r = engine.scan_hallucination_words(src)
442
+ self.assertTrue(r["clean"], r["hits"])
443
+
444
+ def test_dotted_path_not_flagged(self):
445
+ r = engine.scan_hallucination_words(
446
+ "import unittest.mock\nresult = unittest.mock.call(x)\n"
447
+ )
448
+ self.assertTrue(r["clean"], r["hits"])
449
+
450
+ def test_real_stub_still_flagged(self):
451
+ src = "def run():\n return stub_result # TODO: replace with real call\n"
452
+ r = engine.scan_hallucination_words(src)
453
+ self.assertFalse(r["clean"])
454
+
455
+ def test_allowlist_override(self):
456
+ src = "m = Mock()\nthis is a fake thing\n"
457
+ strict = engine.scan_hallucination_words(src, allowlist=[])
458
+ self.assertFalse(strict["clean"])
459
+ relaxed = engine.scan_hallucination_words(src)
460
+ self.assertTrue(any(h["word"] == "fake" for h in relaxed["hits"]))
461
+ self.assertTrue(all(h["word"] != "mock" for h in relaxed["hits"]))
462
+
463
+ def test_default_severities(self):
464
+ src = "# TODO: later\nall tests pass, guaranteed\ndefinitely simulated\n"
465
+ r = engine.scan_hallucination_words(src)
466
+ sev_by_group = {h["group"]: h["severity"] for h in r["hits"]}
467
+ self.assertEqual(sev_by_group["stub_code"], "warning")
468
+ self.assertEqual(sev_by_group["oversold"], "error")
469
+ self.assertEqual(sev_by_group["fabricated"], "error")
470
+ self.assertTrue(r["blocking"])
471
+ self.assertFalse(r["clean"])
472
+
473
+ def test_severity_override_downgrades_to_info(self):
474
+ src = "guaranteed to work\n"
475
+ r = engine.scan_hallucination_words(src, severities={"oversold": "info"})
476
+ self.assertEqual(r["hits"][0]["severity"], "info")
477
+ self.assertFalse(r["blocking"])
478
+
479
+ def test_warning_only_does_not_block(self):
480
+ src = "# TODO: finish this section\n"
481
+ r = engine.scan_hallucination_words(src)
482
+ self.assertFalse(r["clean"])
483
+ self.assertFalse(r["blocking"])
484
+
485
+
486
+ class TestConformance(unittest.TestCase):
487
+ def test_self_conformant(self):
488
+ r = engine.check_plugin_conformance(PLUGIN_ROOT)
489
+ self.assertTrue(r["ok"], r)
490
+
491
+ def test_frontmatter_with_dashes_in_body(self):
492
+ import tempfile
493
+
494
+ with tempfile.TemporaryDirectory() as d:
495
+ skill_dir = os.path.join(d, "skills", "demo-skill")
496
+ os.makedirs(skill_dir)
497
+ with open(os.path.join(skill_dir, "SKILL.md"), "w", encoding="utf-8") as fh:
498
+ fh.write("---\nname: demo-skill\ndescription: ok\n---\n\n---\nnot frontmatter\n")
499
+ # body containing a '---' line must not corrupt the parse
500
+ r = engine.check_plugin_conformance(d)
501
+ self.assertEqual([e for e in r["errors"] if "demo-skill" in e], [], r["errors"])
502
+
503
+ def test_rejects_bad_repository_type(self):
504
+ import tempfile
505
+
506
+ with tempfile.TemporaryDirectory() as d:
507
+ with open(os.path.join(d, "plugin.json"), "w", encoding="utf-8") as fh:
508
+ fh.write(
509
+ '{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",'
510
+ '"name":"badplugin","repository":{"type":"git","url":"x"}}'
511
+ )
512
+ r = engine.check_plugin_conformance(d)
513
+ self.assertFalse(r["ok"])
514
+ self.assertTrue(any("repository" in e for e in r["errors"]))
515
+
516
+ @staticmethod
517
+ def _write_plugin(tmp, plugin_json, mcp_json):
518
+ with open(os.path.join(tmp, "plugin.json"), "w", encoding="utf-8") as fh:
519
+ fh.write(plugin_json)
520
+ with open(os.path.join(tmp, "mcp.json"), "w", encoding="utf-8") as fh:
521
+ fh.write(mcp_json)
522
+
523
+ PJ = '{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"t"}'
524
+ MJ = (
525
+ '{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",'
526
+ '"mcpServers":{"s":%s}}'
527
+ )
528
+
529
+ def test_mcp_unknown_field_in_variant(self):
530
+ import tempfile
531
+
532
+ with tempfile.TemporaryDirectory() as d:
533
+ # 'url' belongs to the http variants, not stdio
534
+ self._write_plugin(
535
+ d, self.PJ, self.MJ % '{"type":"stdio","command":"srv","url":"https://x"}'
536
+ )
537
+ r = engine.check_plugin_conformance(d)
538
+ self.assertFalse(r["ok"])
539
+ self.assertTrue(any("unknown field 'url'" in e for e in r["errors"]))
540
+
541
+ def test_mcp_reserved_env_keys(self):
542
+ import tempfile
543
+
544
+ with tempfile.TemporaryDirectory() as d:
545
+ self._write_plugin(
546
+ d, self.PJ, self.MJ % '{"type":"stdio","command":"srv","env":{"PLUGIN_DATA":"/x"}}'
547
+ )
548
+ r = engine.check_plugin_conformance(d)
549
+ self.assertFalse(r["ok"])
550
+ self.assertTrue(any("reserved" in e for e in r["errors"]))
551
+
552
+ def test_mcp_http_non_loopback_rejected(self):
553
+ import tempfile
554
+
555
+ with tempfile.TemporaryDirectory() as d:
556
+ self._write_plugin(
557
+ d, self.PJ, self.MJ % '{"type":"streamable-http","url":"http://example.com/mcp"}'
558
+ )
559
+ r = engine.check_plugin_conformance(d)
560
+ self.assertFalse(r["ok"])
561
+ self.assertTrue(any("HTTPS" in e for e in r["errors"]))
562
+
563
+ def test_mcp_loopback_http_allowed_and_fragment_rejected(self):
564
+ import tempfile
565
+
566
+ with tempfile.TemporaryDirectory() as d:
567
+ self._write_plugin(
568
+ d, self.PJ, self.MJ % '{"type":"sse","url":"http://localhost:3000/sse#frag"}'
569
+ )
570
+ r = engine.check_plugin_conformance(d)
571
+ self.assertFalse(r["ok"])
572
+ self.assertFalse(any("HTTPS" in e for e in r["errors"]))
573
+ self.assertTrue(any("fragment" in e for e in r["errors"]))
574
+
575
+ def test_mcp_valid_remote_entry_passes(self):
576
+ import tempfile
577
+
578
+ with tempfile.TemporaryDirectory() as d:
579
+ self._write_plugin(
580
+ d,
581
+ self.PJ,
582
+ self.MJ % '{"type":"streamable-http","url":"https://api.example.com/mcp",'
583
+ '"headers":{"X-Tenant":"public"}}',
584
+ )
585
+ r = engine.check_plugin_conformance(d)
586
+ self.assertEqual([e for e in r["errors"] if "mcp.json" in e], [], r["errors"])
587
+
588
+
589
+ class TestSandboxRun(unittest.TestCase):
590
+ def test_runs_command(self):
591
+ r = engine.sandbox_run([PY, "-c", "print(6*7)"], 10)
592
+ self.assertEqual(r["exit_code"], 0)
593
+ self.assertIn("42", r["stdout"])
594
+
595
+ def test_timeout_safety(self):
596
+ r = engine.sandbox_run([PY, "-c", "import time; time.sleep(30)"], 1)
597
+ self.assertTrue(r["timed_out"])
598
+
599
+
600
+ class TestSchemaValidate(unittest.TestCase):
601
+ def test_valid(self):
602
+ schema = {
603
+ "type": "object",
604
+ "required": ["name"],
605
+ "properties": {"name": {"type": "string"}},
606
+ }
607
+ r = engine.schema_validate({"name": "agentseed"}, schema)
608
+ self.assertTrue(r["valid"])
609
+
610
+ def test_invalid(self):
611
+ schema = {
612
+ "type": "object",
613
+ "required": ["name"],
614
+ "properties": {"name": {"type": "string"}},
615
+ }
616
+ r = engine.schema_validate({"name": 123}, schema)
617
+ self.assertFalse(r["valid"])
618
+ self.assertTrue(any("type" in e for e in r["errors"]))
619
+
620
+ def test_required_without_properties(self):
621
+ schema = {"type": "object", "required": ["name"]}
622
+ r = engine.schema_validate({}, schema)
623
+ self.assertFalse(r["valid"])
624
+ self.assertTrue(any("name" in e for e in r["errors"]))
625
+
626
+ def test_oversized_pattern_rejected_both_paths(self):
627
+ from engine import schema as schema_mod
628
+
629
+ evil = "a" * 300
630
+ r = schema_mod.schema_validate("x", {"type": "string", "pattern": evil})
631
+ self.assertFalse(r["valid"])
632
+ self.assertIn("ReDoS", r["errors"][0])
633
+ # nested occurrence is caught too (jsonschema path compiles lazily)
634
+ r2 = schema_mod.schema_validate(
635
+ {"s": "x"}, {"type": "object", "properties": {"s": {"pattern": evil}}}
636
+ )
637
+ self.assertFalse(r2["valid"])
638
+ # boundary: 256 chars still validates normally
639
+ ok_pattern = "^" + "a" * 254 + "$"
640
+ r3 = schema_mod.schema_validate("a" * 254, {"type": "string", "pattern": ok_pattern})
641
+ self.assertTrue(r3["valid"], r3)
642
+
643
+ def test_const_null_validated(self):
644
+ r = engine.schema_validate(None, {"const": None})
645
+ self.assertTrue(r["valid"])
646
+ r = engine.schema_validate("x", {"const": None})
647
+ self.assertFalse(r["valid"])
648
+
649
+ def test_enum_bool_not_equal_int(self):
650
+ r = engine.schema_validate(False, {"enum": [0]})
651
+ self.assertFalse(r["valid"])
652
+ r = engine.schema_validate(True, {"enum": [1]})
653
+ self.assertFalse(r["valid"])
654
+ r = engine.schema_validate(0, {"enum": [0]})
655
+ self.assertTrue(r["valid"])
656
+
657
+ def test_type_array(self):
658
+ schema = {"type": ["string", "null"]}
659
+ self.assertTrue(engine.schema_validate(None, schema)["valid"])
660
+ self.assertTrue(engine.schema_validate("s", schema)["valid"])
661
+ r = engine.schema_validate(5, schema)
662
+ self.assertFalse(r["valid"])
663
+ self.assertTrue(any("type" in e for e in r["errors"]))
664
+
665
+
666
+ class TestConfig(unittest.TestCase):
667
+ def test_load_config_missing_returns_empty(self):
668
+ import tempfile
669
+
670
+ with tempfile.TemporaryDirectory() as d:
671
+ old = os.getcwd()
672
+ os.chdir(d)
673
+ try:
674
+ self.assertEqual(engine.load_config(), {})
675
+ finally:
676
+ os.chdir(old)
677
+
678
+ def test_load_config_plugin_data(self):
679
+ import tempfile
680
+
681
+ with tempfile.TemporaryDirectory() as d:
682
+ cfg = os.path.join(d, engine.CONFIG_FILENAME)
683
+ with open(cfg, "w", encoding="utf-8") as fh:
684
+ fh.write('{"severities": {"stub_code": "info"}, "timeout": 15}')
685
+ env = {k: v for k, v in os.environ.items() if k not in ("AGENTSEED_CONFIG",)}
686
+ env["PLUGIN_DATA"] = d
687
+ import subprocess
688
+
689
+ out = subprocess.run(
690
+ [
691
+ sys.executable,
692
+ "-c",
693
+ "import json, sys; sys.path.insert(0, r'%s');"
694
+ "import guard_engine as e; print(json.dumps(e.load_config()))"
695
+ % os.path.dirname(os.path.abspath(__file__)),
696
+ ],
697
+ capture_output=True,
698
+ text=True,
699
+ encoding="utf-8",
700
+ errors="replace",
701
+ env=env,
702
+ check=True,
703
+ )
704
+ self.assertEqual(json.loads(out.stdout)["timeout"], 15)
705
+
706
+ def test_load_config_invalid_json_ignored(self):
707
+ import tempfile
708
+
709
+ with tempfile.TemporaryDirectory() as d:
710
+ bad = os.path.join(d, "bad.json")
711
+ with open(bad, "w", encoding="utf-8") as fh:
712
+ fh.write("{not json")
713
+ self.assertEqual(engine.load_config(bad), {})
714
+
715
+
716
+ class TestSchemaDraft07Tuples(unittest.TestCase):
717
+ """draft-07 tuple 'items' must validate, not crash (regression)."""
718
+
719
+ def test_tuple_items_via_jsonschema_fallback(self):
720
+ from engine import schema as schema_mod
721
+
722
+ r = schema_mod.schema_validate(
723
+ [1, "a"], {"items": [{"type": "integer"}, {"type": "string"}]}
724
+ )
725
+ self.assertTrue(r["valid"], r)
726
+ self.assertNotIn("crashed", "".join(r["errors"]))
727
+
728
+ def test_bad_tuple_types_detected(self):
729
+ from engine import schema as schema_mod
730
+
731
+ r = schema_mod.schema_validate(
732
+ ["x", 1], {"items": [{"type": "integer"}, {"type": "string"}]}
733
+ )
734
+ self.assertFalse(r["valid"])
735
+ self.assertEqual(len(r["errors"]), 2)
736
+
737
+ def test_additional_items_false(self):
738
+ from engine import schema as schema_mod
739
+
740
+ r = schema_mod.schema_validate(
741
+ [1, "a", True],
742
+ {"items": [{"type": "integer"}, {"type": "string"}], "additionalItems": False},
743
+ )
744
+ self.assertFalse(r["valid"])
745
+
746
+
747
+ class TestMatchGuardsForOldPythons(unittest.TestCase):
748
+ """ast.Match* guards must keep 3.9 alive (no AttributeError)."""
749
+
750
+ def test_detection_runs_with_match_nodes_absent(self):
751
+ from engine import symbols as sym
752
+
753
+ saved = (sym._MATCH_AS, sym._MATCH_STAR, sym._MATCH_MAPPING)
754
+ sym._MATCH_AS = sym._MATCH_STAR = sym._MATCH_MAPPING = None
755
+ try:
756
+ r = sym.detect_undefined_symbols("match x:\n case [a]:\n f(a)\n")
757
+ finally:
758
+ sym._MATCH_AS, sym._MATCH_STAR, sym._MATCH_MAPPING = saved
759
+ if sys.version_info >= (3, 10):
760
+ # Nodes exist but were disabled: parse succeeds, guard degrades
761
+ # gracefully and the real suspect is still reported.
762
+ self.assertIn("f", r["suspects"])
763
+ else:
764
+ # Real pre-3.9 parser: match syntax is a SyntaxError; the engine
765
+ # must degrade to an empty (non-crashing) result.
766
+ self.assertEqual(r["suspects"], [])
767
+
768
+
769
+ class TestPyflakesMerge(unittest.TestCase):
770
+ """Del-context undefined names are caught by the stdlib walk itself
771
+ (ast.Delete targets); when pyflakes is installed its findings merge in
772
+ as additional scope-aware coverage."""
773
+
774
+ def test_del_undefined_name_caught_regardless_of_pyflakes(self):
775
+ from engine import symbols as sym
776
+
777
+ r = sym.detect_undefined_symbols("def f():\n del ghost_var\n")
778
+ self.assertIn("ghost_var", r["suspects"], r)
779
+ if sym._HAS_PYFLAKES:
780
+ self.assertIn("pyflakes", r["note"])
781
+
782
+
783
+ class TestPluginVersionFallback(unittest.TestCase):
784
+ """version.py must degrade to '0.0.0' when its root has no/broken
785
+ plugin.json. Executed from a COPY so the __file__-derived root is tmpdir."""
786
+
787
+ @staticmethod
788
+ def _run_copy(source_path: str, fake_file: str) -> str:
789
+ import subprocess
790
+
791
+ script = (
792
+ "ns = {'__file__': %r, '__name__': 'v_under_test'}\n"
793
+ "code = open(%r, encoding='utf-8').read()\n"
794
+ "exec(compile(code, %r, 'exec'), ns)\n"
795
+ "print(ns['plugin_version']())\n" % (fake_file, source_path, source_path)
796
+ )
797
+ out = subprocess.run(
798
+ [sys.executable, "-c", script],
799
+ capture_output=True,
800
+ text=True,
801
+ encoding="utf-8",
802
+ errors="replace",
803
+ timeout=60,
804
+ )
805
+ return out.stdout.strip()
806
+
807
+ def test_missing_and_broken_manifests_fall_back(self):
808
+ import tempfile
809
+
810
+ from engine import version as vmod
811
+
812
+ self.assertTrue(vmod.plugin_version().count(".") >= 1) # sanity: real path works
813
+ source_path = os.path.abspath(vmod.__file__)
814
+
815
+ with tempfile.TemporaryDirectory() as d:
816
+ fake_file = os.path.join(d, "engine", "version.py")
817
+ os.makedirs(os.path.dirname(fake_file))
818
+ # broken plugin.json -> fallback
819
+ with open(os.path.join(d, "plugin.json"), "w", encoding="utf-8") as fh:
820
+ fh.write("{not json")
821
+ self.assertEqual(self._run_copy(source_path, fake_file), "0.0.0")
822
+ # missing plugin.json -> fallback
823
+ os.remove(os.path.join(d, "plugin.json"))
824
+ self.assertEqual(self._run_copy(source_path, fake_file), "0.0.0")
825
+
826
+
827
+ if __name__ == "__main__":
828
+ unittest.main(verbosity=2)