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,131 @@
1
+ """AgentSeed config loading — zero dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+
8
+ CONFIG_FILENAME = "agentseed.config.json"
9
+ _VALID_SEVERITIES = {"error", "warning", "info"}
10
+ VALID_GROUPS = {"stub_code", "oversold", "fabricated"}
11
+
12
+ # Every key load_config() understands; anything else is a likely typo and
13
+ # callers should surface a warning (silently ignoring typos = silent no-op).
14
+ KNOWN_CONFIG_KEYS = {
15
+ "allowlist",
16
+ "severities",
17
+ "timeout",
18
+ "extra_tokens",
19
+ "suppress_symbols",
20
+ "sandbox_allowed_prefixes",
21
+ "sandbox_env",
22
+ "known_packages",
23
+ }
24
+
25
+ SANDBOX_ENV_MODES = ("inherit", "scrub")
26
+
27
+
28
+ def sandbox_env_mode(config: dict, default: str = "inherit") -> str:
29
+ """Validated sandbox_env mode from config ("inherit" | "scrub")."""
30
+ value = config.get("sandbox_env", default)
31
+ return value if value in SANDBOX_ENV_MODES else default
32
+
33
+
34
+ def load_config(explicit_path: str | None = None) -> dict:
35
+ """Load the effective AgentSeed config (zero dependencies).
36
+
37
+ Search order (first hit wins):
38
+ 1. ``explicit_path`` argument
39
+ 2. ``AGENTSEED_CONFIG`` environment variable
40
+ 3. ``${PLUGIN_DATA}/agentseed.config.json``
41
+ 4. ``./agentseed.config.json`` in the current working directory.
42
+
43
+ Recognized keys (all optional):
44
+ allowlist : list[str] - scan exclusions (replaces DEFAULT_ALLOWLIST)
45
+ severities : dict[str, str] - group -> error|warning|info
46
+ timeout : int - default sandbox_run timeout in seconds
47
+ extra_tokens : dict[group, list[str]] - extra hallucination words
48
+ suppress_symbols : list[str] - names verify_code never flags
49
+ known_packages : list[str] - packages check_imports treats as known
50
+ (project-local / trusted third-party), beyond stdlib
51
+ sandbox_allowed_prefixes : list[str] - executable allowlist for sandbox_run
52
+ (absent/empty = unrestricted)
53
+
54
+ Returns {} when no config file exists or it cannot be parsed.
55
+ """
56
+ candidates: list[str] = []
57
+ if explicit_path:
58
+ candidates.append(explicit_path)
59
+ env_path = os.environ.get("AGENTSEED_CONFIG")
60
+ if env_path:
61
+ candidates.append(env_path)
62
+ plugin_data = os.environ.get("PLUGIN_DATA")
63
+ if plugin_data:
64
+ candidates.append(os.path.join(plugin_data, CONFIG_FILENAME))
65
+ candidates.append(CONFIG_FILENAME)
66
+
67
+ for path in candidates:
68
+ if path and os.path.isfile(path):
69
+ try:
70
+ with open(path, encoding="utf-8") as fh:
71
+ data = json.load(fh)
72
+ if isinstance(data, dict):
73
+ return data
74
+ except (OSError, ValueError):
75
+ continue
76
+ return {}
77
+
78
+
79
+ def config_str_list(config: dict, key: str) -> list[str] | None:
80
+ """Extract a validated string-list value from config, or None."""
81
+ value = config.get(key)
82
+ if isinstance(value, list) and all(isinstance(v, str) for v in value):
83
+ return value
84
+ return None
85
+
86
+
87
+ def config_severities(config: dict) -> dict[str, str] | None:
88
+ """Extract a validated severities map from config, or None."""
89
+ value = config.get("severities")
90
+ if isinstance(value, dict) and all(
91
+ isinstance(k, str) and isinstance(v, str) and v in _VALID_SEVERITIES
92
+ for k, v in value.items()
93
+ ):
94
+ return value
95
+ return None
96
+
97
+
98
+ def parse_timeout(config: dict, default: int = 30) -> int:
99
+ """Extract and validate timeout from config dict."""
100
+ try:
101
+ return int(config.get("timeout", default))
102
+ except (TypeError, ValueError):
103
+ return default
104
+
105
+
106
+ def unknown_config_keys(config: dict) -> list[str]:
107
+ """Keys present in ``config`` that load_config() does not understand.
108
+
109
+ A typo'd key is silently ignored by every consumer — surfacing it turns
110
+ a silent no-op into an actionable warning.
111
+ """
112
+ if not isinstance(config, dict):
113
+ return []
114
+ return sorted(k for k in config if k not in KNOWN_CONFIG_KEYS)
115
+
116
+
117
+ def config_extra_tokens(config: dict) -> dict[str, list[str]] | None:
118
+ """Validate the extra_tokens mapping {group: [words]}, or None."""
119
+ value = config.get("extra_tokens")
120
+ if not isinstance(value, dict):
121
+ return None
122
+ out: dict[str, list[str]] = {}
123
+ for group, words in value.items():
124
+ if (
125
+ group in VALID_GROUPS
126
+ and isinstance(words, list)
127
+ and all(isinstance(w, str) and w for w in words)
128
+ and words
129
+ ):
130
+ out[group] = words
131
+ return out or None
@@ -0,0 +1,254 @@
1
+ """AgentSeed hallucination word scanning.
2
+
3
+ Flags tokens across three signal groups:
4
+ - stub_code: stub/mock/fake/placeholder/dummy/todo/...
5
+ - oversold: guaranteed/"all tests pass"/"production ready"/...
6
+ - fabricated: simulated/invented/fabricated/...
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+
13
+ from .config import _VALID_SEVERITIES
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Hallucination token pools (grouped by signal type).
17
+ # ---------------------------------------------------------------------------
18
+
19
+ STUB_TOKENS = [
20
+ "stub",
21
+ "mock",
22
+ "fake",
23
+ "placeholder",
24
+ "dummy",
25
+ "todo",
26
+ "fixme",
27
+ "xxx",
28
+ "tbd",
29
+ "tba",
30
+ "wip",
31
+ "not implemented",
32
+ "coming soon",
33
+ "to be implemented",
34
+ "not implemented yet",
35
+ "pending implementation",
36
+ ]
37
+
38
+ # CJK tokens: \b word boundaries are meaningless between CJK chars, so these
39
+ # are matched as substrings (they are specific enough to stay low-noise).
40
+ STUB_TOKENS_ZH = [
41
+ "占位",
42
+ "待实现",
43
+ "未实现",
44
+ "待补充",
45
+ "稍后补",
46
+ "假数据",
47
+ "模拟数据",
48
+ "临时方案",
49
+ "先这样",
50
+ "待完成",
51
+ "尚未实现",
52
+ ]
53
+
54
+ OVERSOLD_TOKENS = [
55
+ "guaranteed",
56
+ "definitely works",
57
+ "all tests pass",
58
+ "everything works",
59
+ "fully tested",
60
+ "production ready",
61
+ "no bugs",
62
+ "works perfectly",
63
+ "should work",
64
+ "trust me",
65
+ "works on my machine",
66
+ "100% correct",
67
+ "bug free",
68
+ "zero errors",
69
+ "foolproof",
70
+ "bulletproof",
71
+ "cannot fail",
72
+ "guaranteed to pass",
73
+ "impossible to break",
74
+ ]
75
+
76
+ OVERSOLD_TOKENS_ZH = [
77
+ "保证通过",
78
+ "绝对没问题",
79
+ "肯定能跑",
80
+ "万无一失",
81
+ "完美运行",
82
+ "零缺陷",
83
+ "无需测试",
84
+ "包过",
85
+ "绝无问题",
86
+ "不可能失败",
87
+ "绝对可靠",
88
+ "稳过",
89
+ ]
90
+
91
+ FABRICATED_TOKENS = [
92
+ "simulated",
93
+ "invented",
94
+ "fabricated",
95
+ "fictional",
96
+ "pretend",
97
+ "made up",
98
+ "fictitious",
99
+ "nonexistent",
100
+ "non-existent",
101
+ "mythical",
102
+ ]
103
+
104
+ FABRICATED_TOKENS_ZH = [
105
+ "虚构",
106
+ "编造",
107
+ "凭空捏造",
108
+ "子虚乌有",
109
+ ]
110
+
111
+ # Full pool: token -> group (kept for backward compatibility).
112
+ HALLUCINATION_WORDS: dict[str, str] = {}
113
+ for _tokens, _group in [
114
+ (STUB_TOKENS + STUB_TOKENS_ZH, "stub_code"),
115
+ (OVERSOLD_TOKENS + OVERSOLD_TOKENS_ZH, "oversold"),
116
+ (FABRICATED_TOKENS + FABRICATED_TOKENS_ZH, "fabricated"),
117
+ ]:
118
+ for _t in _tokens:
119
+ HALLUCINATION_WORDS[_t] = _group
120
+
121
+ _GROUP_LABELS = {
122
+ "stub_code": "placeholder / not-really-done code",
123
+ "oversold": "unverified confidence claim",
124
+ "fabricated": "fabricated / invented content",
125
+ }
126
+
127
+ # Tokens that are legitimate in common testing/idiomatic contexts.
128
+ DEFAULT_ALLOWLIST = [
129
+ "unittest.mock",
130
+ "Mock(",
131
+ "MagicMock(",
132
+ "AsyncMock(",
133
+ "PropertyMock(",
134
+ "patch(",
135
+ "monkeypatch",
136
+ "mocker",
137
+ ]
138
+
139
+ _IMPORT_LINE_RE = re.compile(r"^\s*(?:from\s+[\w.]+\s+import\b|import\s+\w)", re.IGNORECASE)
140
+
141
+ # Default severity per signal group.
142
+ DEFAULT_SEVERITIES: dict[str, str] = {
143
+ "stub_code": "warning",
144
+ "oversold": "error",
145
+ "fabricated": "error",
146
+ }
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Precompiled regex patterns (one per group, compiled once at import time).
150
+ # ASCII tokens use \b word boundaries; CJK tokens are substring matches
151
+ # (\b never fires between two CJK chars, so boundaries would miss 占位符 etc).
152
+ # ---------------------------------------------------------------------------
153
+
154
+ _CJK_RE = re.compile(r"[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]")
155
+
156
+
157
+ def _compile_group(tokens: list[str]) -> re.Pattern:
158
+ ascii_alts = [re.escape(t).replace(r"\ ", r"\s+") for t in tokens if not _CJK_RE.search(t)]
159
+ cjk_alts = [re.escape(t) for t in tokens if _CJK_RE.search(t)]
160
+ parts = []
161
+ if ascii_alts:
162
+ parts.append(rf"\b(?:{'|'.join(ascii_alts)})\b")
163
+ if cjk_alts:
164
+ parts.append("|".join(cjk_alts))
165
+ return re.compile("(?:" + "|".join(parts) + ")", re.IGNORECASE)
166
+
167
+
168
+ _HALLUCINATION_PATTERNS: list[tuple[re.Pattern, str]] = [
169
+ (_compile_group(STUB_TOKENS + STUB_TOKENS_ZH), "stub_code"),
170
+ (_compile_group(OVERSOLD_TOKENS + OVERSOLD_TOKENS_ZH), "oversold"),
171
+ (_compile_group(FABRICATED_TOKENS + FABRICATED_TOKENS_ZH), "fabricated"),
172
+ ]
173
+
174
+
175
+ def scan_hallucination_words(
176
+ source: str,
177
+ allowlist: list[str] | None = None,
178
+ severities: dict[str, str] | None = None,
179
+ extra_tokens: dict[str, list[str]] | None = None,
180
+ ) -> dict:
181
+ """Scan source for tokens in the grouped hallucination pool.
182
+
183
+ To avoid flagging legitimate code, matches are skipped when:
184
+ - the line is an import statement;
185
+ - the match is part of a dotted path (``unittest.mock``, ``os.path``);
186
+ - the matched text starts with an entry of the effective allowlist.
187
+
188
+ ``extra_tokens`` extends the pool at runtime (config: ``extra_tokens``
189
+ mapping group -> [words]); unknown groups are ignored.
190
+
191
+ Each hit carries a severity (``error`` | ``warning`` | ``info``) taken
192
+ from ``severities`` (group -> severity), falling back to
193
+ DEFAULT_SEVERITIES.
194
+
195
+ Returns:
196
+ {
197
+ "hits": [{"word": "stub", "group": "stub_code", "line": 12,
198
+ "severity": "warning"}, ...],
199
+ "clean": bool,
200
+ "blocking": bool,
201
+ "groups": {"stub_code": 2, "oversold": 1, "fabricated": 0},
202
+ "severities": {"error": 1, "warning": 2, "info": 0}
203
+ }
204
+ """
205
+ if allowlist is None:
206
+ allowlist = DEFAULT_ALLOWLIST
207
+ elif isinstance(allowlist, str):
208
+ # MCP clients sometimes send a bare string instead of a list; a raw
209
+ # string would iterate characters and silently suppress every match.
210
+ allowlist = [allowlist]
211
+ elif not isinstance(allowlist, list):
212
+ allowlist = (
213
+ [a for a in allowlist if isinstance(a, str)] if hasattr(allowlist, "__iter__") else []
214
+ )
215
+ allowlist = [a for a in allowlist if isinstance(a, str) and a]
216
+ sev = dict(DEFAULT_SEVERITIES)
217
+ if severities:
218
+ for g, s in severities.items():
219
+ if g in _GROUP_LABELS and s in _VALID_SEVERITIES:
220
+ sev[g] = s
221
+ patterns = list(_HALLUCINATION_PATTERNS)
222
+ if extra_tokens:
223
+ for g, words in extra_tokens.items():
224
+ if g in _GROUP_LABELS and isinstance(words, list):
225
+ words = [w for w in words if isinstance(w, str) and w]
226
+ if words:
227
+ patterns.append((_compile_group(words), g))
228
+ hits: list[dict] = []
229
+ group_counts: dict[str, int] = {g: 0 for g in _GROUP_LABELS}
230
+ severity_counts: dict[str, int] = {"error": 0, "warning": 0, "info": 0}
231
+ for i, line in enumerate(source.splitlines(), start=1):
232
+ if _IMPORT_LINE_RE.match(line):
233
+ continue
234
+ for pattern, group in patterns:
235
+ for m in pattern.finditer(line):
236
+ before = line[max(0, m.start() - 1) : m.start()]
237
+ after = line[m.end() : m.end() + 1]
238
+ if before == "." or after == ".":
239
+ continue # part of a dotted path (module/attribute)
240
+ rest = line[m.start() :]
241
+ if any(rest.lower().startswith(a.lower()) for a in allowlist):
242
+ continue
243
+ word = m.group(0).lower()
244
+ severity = sev.get(group, "warning")
245
+ hits.append({"word": word, "group": group, "line": i, "severity": severity})
246
+ group_counts[group] += 1
247
+ severity_counts[severity] += 1
248
+ return {
249
+ "hits": hits,
250
+ "clean": len(hits) == 0,
251
+ "blocking": severity_counts["error"] > 0,
252
+ "groups": group_counts,
253
+ "severities": severity_counts,
254
+ }
@@ -0,0 +1,136 @@
1
+ """AgentSeed import verification — package-hallucination (slopsquatting) guard.
2
+
3
+ Motivated by "We Have a Package for You!" (USENIX Security 2025,
4
+ arXiv:2406.10279): across 576k generated samples, LLMs invented non-existent
5
+ package names in ~5.2% (commercial) to ~21.7% (open-source) of outputs, and
6
+ ~58% of those names recurred across runs — predictable enough that attackers
7
+ pre-register the exact hallucinated names with malicious payloads
8
+ ("slopsquatting").
9
+
10
+ ``check_imports`` flags top-level imports that are neither Python stdlib nor
11
+ in the project's ``known_packages`` allowlist, so a model-suggested phantom
12
+ package cannot reach a lockfile silently. It is a REPORT, not a hard gate:
13
+ a legit long-tail package will also be flagged for the human to confirm —
14
+ that is the intended cost. Python (AST) only; other languages return an
15
+ honest empty result.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+ import sys
22
+
23
+ # Fallback for Python < 3.10 (``sys.stdlib_module_names`` is 3.10+): a curated
24
+ # list of the most commonly imported stdlib modules. On 3.10+ the exact set is
25
+ # used and this list is never consulted.
26
+ _STDLIB_FALLBACK = frozenset(
27
+ {
28
+ "abc", "argparse", "array", "ast", "asyncio", "atexit", "base64",
29
+ "bisect", "builtins", "collections", "concurrent", "configparser",
30
+ "contextlib", "copy", "csv", "ctypes", "dataclasses", "datetime",
31
+ "decimal", "difflib", "enum", "errno", "functools", "gc", "getpass",
32
+ "glob", "gzip", "hashlib", "heapq", "hmac", "html", "http", "importlib",
33
+ "inspect", "io", "ipaddress", "itertools", "json", "logging", "math",
34
+ "multiprocessing", "os", "pathlib", "pickle", "platform", "queue",
35
+ "random", "re", "readline", "secrets", "shlex", "shutil", "signal",
36
+ "site", "socket", "sqlite3", "ssl", "stat", "statistics", "string",
37
+ "struct", "subprocess", "sys", "tempfile", "textwrap", "threading",
38
+ "time", "timeit", "token", "tokenize", "traceback", "types", "typing",
39
+ "unicodedata", "unittest", "urllib", "uuid", "venv", "warnings",
40
+ "weakref", "xml", "zipfile", "zoneinfo",
41
+ }
42
+ )
43
+
44
+ # Common third-party packages treated as known by default (beyond stdlib).
45
+ # The full resolution for anything not listed is: user config ``known_packages``.
46
+ _DEFAULT_COMMON = frozenset(
47
+ {
48
+ "numpy", "pandas", "scipy", "sklearn", "matplotlib", "seaborn", "plotly",
49
+ "requests", "httpx", "aiohttp", "urllib3", "beautifulsoup4", "bs4",
50
+ "scrapy", "selenium", "playwright", "flask", "fastapi", "django",
51
+ "starlette", "uvicorn", "gunicorn", "jinja2", "sqlalchemy", "pymysql",
52
+ "psycopg2", "redis", "pymongo", "elasticsearch", "celery", "kafka",
53
+ "pydantic", "pydantic_settings", "click", "typer", "rich", "tqdm",
54
+ "pytest", "coverage", "black", "ruff", "mypy", "flake8", "isort",
55
+ "tox", "nox", "hypothesis", "unittest", "jupyter", "notebook",
56
+ "ipykernel", "ipython", "nbformat", "nbconvert", "tensorflow", "torch",
57
+ "keras", "transformers", "datasets", "tokenizers", "accelerate",
58
+ "sentence_transformers", "openai", "anthropic", "google", "googleapiclient",
59
+ "boto3", "botocore", "azure", "awscli", "paramiko", "cryptography",
60
+ "pycryptodome", "bcrypt", "jwt", "pyjwt", "passlib", "yaml", "pyyaml",
61
+ "tomllib", "tomli", "tomlkit", "jsonschema", "setuptools", "wheel",
62
+ "pip", "pipenv", "poetry", "uv", "pre_commit", "arrow", "pendulum",
63
+ "dateutil", "python_dateutil", "pytz", "tzlocal", "zoneinfo", "natsort",
64
+ "more_itertools", "toolz", "pydash", "tenacity", "structlog", "loguru",
65
+ "colorama", "clickhouse_driver", "duckdb", "polars", "dask", "ray",
66
+ }
67
+ )
68
+
69
+
70
+ def _stdlib_modules() -> frozenset[str]:
71
+ names = getattr(sys, "stdlib_module_names", None)
72
+ return frozenset(names) if names is not None else _STDLIB_FALLBACK
73
+
74
+
75
+ def _imported_top_level(source: str) -> list[tuple[str, int]]:
76
+ """(top-level module name, lineno) for every import / from-import."""
77
+ try:
78
+ tree = ast.parse(source)
79
+ except SyntaxError:
80
+ return []
81
+ out: list[tuple[str, int]] = []
82
+ for node in ast.walk(tree):
83
+ if isinstance(node, ast.Import):
84
+ for alias in node.names:
85
+ out.append((alias.name.split(".")[0], node.lineno))
86
+ elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
87
+ out.append((node.module.split(".")[0], node.lineno))
88
+ return out
89
+
90
+
91
+ def check_imports(
92
+ source: str,
93
+ language: str = "python",
94
+ known_packages: list[str] | None = None,
95
+ ) -> dict:
96
+ """Flag top-level imports neither in stdlib nor in the known-package set.
97
+
98
+ ``known_packages`` (config key of the same name) extends the default
99
+ common third-party allowlist with project-local packages. Python only
100
+ (AST); other languages return an honest empty result.
101
+
102
+ Returns:
103
+ {"language", "imports_ok", "suspicious": [{"package", "line"}, ...],
104
+ "note"}
105
+ """
106
+ lang = (language or "python").strip().lower()
107
+ if lang != "python":
108
+ return {
109
+ "language": lang,
110
+ "imports_ok": True,
111
+ "suspicious": [],
112
+ "note": "Import verification is implemented for python (AST); "
113
+ "other languages are not covered yet.",
114
+ }
115
+ known = set(_DEFAULT_COMMON)
116
+ for pkg in known_packages or []:
117
+ if isinstance(pkg, str) and pkg.strip():
118
+ known.add(pkg.strip())
119
+ stdlib = _stdlib_modules()
120
+ suspicious = [
121
+ {"package": pkg, "line": line}
122
+ for pkg, line in _imported_top_level(source)
123
+ if pkg not in stdlib and pkg not in known
124
+ ]
125
+ note = (
126
+ "Top-level import is neither Python stdlib nor in the known-package "
127
+ "set (stdlib + common third-party + config `known_packages`) — a "
128
+ "possible hallucinated package (slopsquatting). Verify the name exists "
129
+ "in the registry before installing. This is a report, not a gate."
130
+ )
131
+ return {
132
+ "language": "python",
133
+ "imports_ok": not suspicious,
134
+ "suspicious": suspicious,
135
+ "note": note,
136
+ }