refutescan 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinay Vobbilichetty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: refutescan
3
+ Version: 0.1.0
4
+ Summary: A two-LLM, refute-first agentic source-code vulnerability scanner — wide-net navigate, skeptical refute, jailed in docker. Bring your own LLM.
5
+ Author-email: Vinay Vobbilichetty <vinayvobbilichetty11@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/vinayvobbili/refutescan
8
+ Project-URL: Repository, https://github.com/vinayvobbili/refutescan
9
+ Project-URL: Issues, https://github.com/vinayvobbili/refutescan/issues
10
+ Keywords: security,sast,vulnerability-scanner,appsec,code-security,agentic,llm,ai-security,static-analysis,secure-code-review,false-positive-reduction,supply-chain,sandbox
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: pydantic>=2
25
+ Requires-Dist: langchain-core>=0.3
26
+ Provides-Extra: openai
27
+ Requires-Dist: langchain-openai>=0.1; extra == "openai"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7; extra == "dev"
30
+ Requires-Dist: ruff>=0.4; extra == "dev"
31
+ Requires-Dist: build>=1.0; extra == "dev"
32
+ Requires-Dist: twine>=5.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # refutescan
36
+
37
+ **A two-LLM, refute-first agentic source-code vulnerability scanner.**
38
+
39
+ Most LLM code scanners have the same problem: they hallucinate. Point a model at
40
+ a repo and it will confidently report SQL injection in a parameterized query and
41
+ SSRF behind an allowlist. The noise buries the real bugs.
42
+
43
+ refutescan splits the job across two models:
44
+
45
+ 1. **Navigate (wide net).** A fast tool-calling model explores the repo with
46
+ read-only tools (`list_dir` / `read_file` / `grep`), traces untrusted input
47
+ toward dangerous sinks, and records *candidate* findings. It's cheap and it
48
+ over-reports on purpose.
49
+ 2. **Refute (skeptic).** A stronger model re-reads the actual code slice around
50
+ each candidate from disk — ground truth, not the navigator's paraphrase — and
51
+ is prompted to **refute** it. Sanitized? Parameterized? Gated by auth? Not
52
+ reachable? It's culled, with the reason kept. Only what survives is reported.
53
+
54
+ You get the recall of an agentic scanner without the false-positive flood.
55
+
56
+ Every scan runs **jailed in an ephemeral docker sandbox** by default: git URLs
57
+ are cloned *inside a container* (no host mounts), then audited in a second
58
+ container with **no network, read-only, no capabilities, non-root**, and only
59
+ the repo mounted. A hostile repo (malicious submodule, hook, symlink) touches
60
+ neither your filesystem nor the network.
61
+
62
+ ## Install
63
+
64
+ ```
65
+ pip install refutescan # core kernel
66
+ pip install 'refutescan[openai]' # + the OpenAI adapter for the CLI
67
+ ```
68
+
69
+ For sandboxed scans (recommended), build the jail image once:
70
+
71
+ ```
72
+ refutescan-build-sandbox
73
+ ```
74
+
75
+ (Requires Docker. Without it, refutescan falls back to a guarded in-process scan.)
76
+
77
+ ## CLI
78
+
79
+ ```
80
+ export OPENAI_API_KEY=sk-...
81
+ refutescan https://github.com/owner/repo
82
+ refutescan ./path/to/local/repo --judge-model gpt-4o --json
83
+ ```
84
+
85
+ Exit code is non-zero when confirmed findings exist, so it drops straight into CI.
86
+
87
+ ## Library
88
+
89
+ ```python
90
+ from refutescan import scan, ScanConfig
91
+ from refutescan.adapters import openai_navigator_factory, openai_judge_factory
92
+
93
+ result = scan(
94
+ "https://github.com/owner/repo",
95
+ navigator_factory=openai_navigator_factory("gpt-4o-mini"),
96
+ judge_factory=openai_judge_factory("gpt-4o"),
97
+ config=ScanConfig(sandbox="docker"), # auto | docker | inprocess
98
+ progress=lambda phase: print(phase),
99
+ )
100
+
101
+ for f in result.findings:
102
+ print(f["severity"], f["title"], f"{f['file']}:{f['line']}")
103
+ print(" ", f["reasoning"])
104
+ print(" fix:", f["recommendation"])
105
+ ```
106
+
107
+ `result.culled` holds the refuted candidates with the reason each was rejected —
108
+ useful for tuning and for trusting the tool.
109
+
110
+ ## Bring your own model
111
+
112
+ refutescan never imports a provider SDK in its core. Pass any LangChain-style
113
+ chat model through two factories:
114
+
115
+ - `navigator_factory() -> chat_model` — supports `.bind_tools(...)` + `.invoke(...)`
116
+ - `judge_factory() -> judge(prompt, schema) -> instance` — one structured-output call
117
+
118
+ So a local model (vLLM/Ollama via the OpenAI shim, `--base-url`), Anthropic,
119
+ Azure OpenAI, or a corporate gateway all work — wire the factory and go. See
120
+ `refutescan/providers.py`.
121
+
122
+ ## What it is and isn't
123
+
124
+ - **Is:** an agentic, read-only, false-positive-resistant first-pass auditor for
125
+ the common web-app vuln classes (injection, authz/IDOR, SSRF, path traversal,
126
+ unsafe deserialization, hardcoded secrets, weak crypto, XSS, auth flaws).
127
+ - **Isn't:** a replacement for a full SAST suite, a proof of exploitability, or a
128
+ patch generator. It finds and explains; a human confirms and fixes.
129
+
130
+ ## License
131
+
132
+ MIT.
@@ -0,0 +1,98 @@
1
+ # refutescan
2
+
3
+ **A two-LLM, refute-first agentic source-code vulnerability scanner.**
4
+
5
+ Most LLM code scanners have the same problem: they hallucinate. Point a model at
6
+ a repo and it will confidently report SQL injection in a parameterized query and
7
+ SSRF behind an allowlist. The noise buries the real bugs.
8
+
9
+ refutescan splits the job across two models:
10
+
11
+ 1. **Navigate (wide net).** A fast tool-calling model explores the repo with
12
+ read-only tools (`list_dir` / `read_file` / `grep`), traces untrusted input
13
+ toward dangerous sinks, and records *candidate* findings. It's cheap and it
14
+ over-reports on purpose.
15
+ 2. **Refute (skeptic).** A stronger model re-reads the actual code slice around
16
+ each candidate from disk — ground truth, not the navigator's paraphrase — and
17
+ is prompted to **refute** it. Sanitized? Parameterized? Gated by auth? Not
18
+ reachable? It's culled, with the reason kept. Only what survives is reported.
19
+
20
+ You get the recall of an agentic scanner without the false-positive flood.
21
+
22
+ Every scan runs **jailed in an ephemeral docker sandbox** by default: git URLs
23
+ are cloned *inside a container* (no host mounts), then audited in a second
24
+ container with **no network, read-only, no capabilities, non-root**, and only
25
+ the repo mounted. A hostile repo (malicious submodule, hook, symlink) touches
26
+ neither your filesystem nor the network.
27
+
28
+ ## Install
29
+
30
+ ```
31
+ pip install refutescan # core kernel
32
+ pip install 'refutescan[openai]' # + the OpenAI adapter for the CLI
33
+ ```
34
+
35
+ For sandboxed scans (recommended), build the jail image once:
36
+
37
+ ```
38
+ refutescan-build-sandbox
39
+ ```
40
+
41
+ (Requires Docker. Without it, refutescan falls back to a guarded in-process scan.)
42
+
43
+ ## CLI
44
+
45
+ ```
46
+ export OPENAI_API_KEY=sk-...
47
+ refutescan https://github.com/owner/repo
48
+ refutescan ./path/to/local/repo --judge-model gpt-4o --json
49
+ ```
50
+
51
+ Exit code is non-zero when confirmed findings exist, so it drops straight into CI.
52
+
53
+ ## Library
54
+
55
+ ```python
56
+ from refutescan import scan, ScanConfig
57
+ from refutescan.adapters import openai_navigator_factory, openai_judge_factory
58
+
59
+ result = scan(
60
+ "https://github.com/owner/repo",
61
+ navigator_factory=openai_navigator_factory("gpt-4o-mini"),
62
+ judge_factory=openai_judge_factory("gpt-4o"),
63
+ config=ScanConfig(sandbox="docker"), # auto | docker | inprocess
64
+ progress=lambda phase: print(phase),
65
+ )
66
+
67
+ for f in result.findings:
68
+ print(f["severity"], f["title"], f"{f['file']}:{f['line']}")
69
+ print(" ", f["reasoning"])
70
+ print(" fix:", f["recommendation"])
71
+ ```
72
+
73
+ `result.culled` holds the refuted candidates with the reason each was rejected —
74
+ useful for tuning and for trusting the tool.
75
+
76
+ ## Bring your own model
77
+
78
+ refutescan never imports a provider SDK in its core. Pass any LangChain-style
79
+ chat model through two factories:
80
+
81
+ - `navigator_factory() -> chat_model` — supports `.bind_tools(...)` + `.invoke(...)`
82
+ - `judge_factory() -> judge(prompt, schema) -> instance` — one structured-output call
83
+
84
+ So a local model (vLLM/Ollama via the OpenAI shim, `--base-url`), Anthropic,
85
+ Azure OpenAI, or a corporate gateway all work — wire the factory and go. See
86
+ `refutescan/providers.py`.
87
+
88
+ ## What it is and isn't
89
+
90
+ - **Is:** an agentic, read-only, false-positive-resistant first-pass auditor for
91
+ the common web-app vuln classes (injection, authz/IDOR, SSRF, path traversal,
92
+ unsafe deserialization, hardcoded secrets, weak crypto, XSS, auth flaws).
93
+ - **Isn't:** a replacement for a full SAST suite, a proof of exploitability, or a
94
+ patch generator. It finds and explains; a human confirms and fixes.
95
+
96
+ ## License
97
+
98
+ MIT.
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "refutescan"
7
+ version = "0.1.0"
8
+ description = "A two-LLM, refute-first agentic source-code vulnerability scanner — wide-net navigate, skeptical refute, jailed in docker. Bring your own LLM."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Vinay Vobbilichetty", email = "vinayvobbilichetty11@gmail.com" }]
13
+ keywords = [
14
+ "security", "sast", "vulnerability-scanner", "appsec", "code-security",
15
+ "agentic", "llm", "ai-security", "static-analysis", "secure-code-review",
16
+ "false-positive-reduction", "supply-chain", "sandbox",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Developers",
21
+ "Intended Audience :: Information Technology",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Security",
28
+ "Topic :: Software Development :: Quality Assurance",
29
+ ]
30
+ # Core: the kernel + the LangChain interface the navigator/judge speak. Provider
31
+ # SDKs are opt-in extras — bring your own model.
32
+ dependencies = [
33
+ "pydantic>=2",
34
+ "langchain-core>=0.3",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ openai = ["langchain-openai>=0.1"]
39
+ dev = [
40
+ "pytest>=7",
41
+ "ruff>=0.4",
42
+ "build>=1.0",
43
+ "twine>=5.0",
44
+ ]
45
+
46
+ [project.scripts]
47
+ refutescan = "refutescan.cli:main"
48
+ refutescan-build-sandbox = "refutescan.cli:build_sandbox"
49
+
50
+ [project.urls]
51
+ Homepage = "https://github.com/vinayvobbili/refutescan"
52
+ Repository = "https://github.com/vinayvobbili/refutescan"
53
+ Issues = "https://github.com/vinayvobbili/refutescan/issues"
54
+
55
+ [tool.setuptools.packages.find]
56
+ where = ["src"]
57
+
58
+ [tool.setuptools.package-data]
59
+ # The sandbox image source ships with the package so `refutescan-build-sandbox`
60
+ # can build it from the installed location.
61
+ "refutescan.sandbox" = ["Dockerfile", "build.sh", "toolrunner.py"]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+ addopts = "-q"
66
+
67
+ [tool.ruff]
68
+ line-length = 100
69
+ target-version = "py310"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """refutescan — a two-LLM, refute-first agentic source-code vulnerability scanner.
2
+
3
+ A fast model NAVIGATES the repo with read-only tools and casts a wide net of
4
+ candidate findings; a stronger model then REFUTES each one against the real code
5
+ slice before it surfaces — so you get the recall of an agentic scanner without
6
+ the false-positive flood. Scans run jailed in an ephemeral docker sandbox
7
+ (clone-in-container, no network, read-only, non-root) by default. Bring your own
8
+ LLMs.
9
+
10
+ from refutescan import scan
11
+ from refutescan.adapters import openai_navigator_factory, openai_judge_factory
12
+
13
+ result = scan(
14
+ "https://github.com/owner/repo",
15
+ navigator_factory=openai_navigator_factory(),
16
+ judge_factory=openai_judge_factory(),
17
+ )
18
+ for f in result.findings:
19
+ print(f["severity"], f["title"], f["file"], f["line"])
20
+ """
21
+
22
+ from .models import ScanConfig, ScanResult, Verdict
23
+ from .scanner import derive_title, looks_like_git_url, scan
24
+ from .vulns import SEVERITIES, VULN_CLASSES
25
+
26
+ __version__ = "0.1.0"
27
+
28
+ __all__ = [
29
+ "scan",
30
+ "ScanConfig",
31
+ "ScanResult",
32
+ "Verdict",
33
+ "VULN_CLASSES",
34
+ "SEVERITIES",
35
+ "looks_like_git_url",
36
+ "derive_title",
37
+ "__version__",
38
+ ]
@@ -0,0 +1,11 @@
1
+ """Ready-made model factories for common providers.
2
+
3
+ These are thin conveniences over LangChain chat models so you don't have to wire
4
+ the injection seam yourself. Import the one you want; each needs its provider
5
+ extra installed (e.g. ``pip install refutescan[openai]``). Bring your own by
6
+ passing any LangChain-style chat model — see ``refutescan.providers``.
7
+ """
8
+
9
+ from .openai_chat import openai_judge_factory, openai_navigator_factory
10
+
11
+ __all__ = ["openai_navigator_factory", "openai_judge_factory"]
@@ -0,0 +1,51 @@
1
+ """OpenAI (or any OpenAI-compatible endpoint) model factories.
2
+
3
+ Requires ``langchain-openai`` (the ``openai`` extra). Set ``OPENAI_API_KEY``, or
4
+ pass ``base_url`` to point at a compatible gateway (vLLM, Ollama's OpenAI shim,
5
+ Azure OpenAI, a local proxy, …).
6
+
7
+ from refutescan import scan
8
+ from refutescan.adapters import openai_navigator_factory, openai_judge_factory
9
+
10
+ result = scan(
11
+ "https://github.com/owner/repo",
12
+ navigator_factory=openai_navigator_factory("gpt-4o-mini"),
13
+ judge_factory=openai_judge_factory("gpt-4o"),
14
+ )
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+ from ..providers import JudgeFactory, NavigatorFactory
22
+
23
+
24
+ def _chat(model: str, **kwargs: Any):
25
+ try:
26
+ from langchain_openai import ChatOpenAI
27
+ except ImportError as e: # pragma: no cover
28
+ raise ImportError(
29
+ "refutescan's OpenAI adapter needs langchain-openai. "
30
+ "Install it with: pip install 'refutescan[openai]'"
31
+ ) from e
32
+ return ChatOpenAI(model=model, temperature=0, **kwargs)
33
+
34
+
35
+ def openai_navigator_factory(model: str = "gpt-4o-mini", **kwargs: Any) -> NavigatorFactory:
36
+ """A navigator factory using a fast OpenAI tool-calling model (the wide-net pass)."""
37
+ def factory():
38
+ return _chat(model, **kwargs)
39
+ return factory
40
+
41
+
42
+ def openai_judge_factory(model: str = "gpt-4o", **kwargs: Any) -> JudgeFactory:
43
+ """A judge factory using a stronger OpenAI model with structured output (refute pass)."""
44
+ def factory():
45
+ llm = _chat(model, **kwargs)
46
+
47
+ def judge(prompt: str, schema: type):
48
+ return llm.with_structured_output(schema).invoke(prompt)
49
+
50
+ return judge
51
+ return factory
@@ -0,0 +1,116 @@
1
+ """Command-line entry points.
2
+
3
+ refutescan <path-or-git-url> [options] — run a scan, print findings
4
+ refutescan-build-sandbox — build the docker jail image
5
+
6
+ The scan CLI uses the OpenAI adapter by default (needs the ``openai`` extra and
7
+ OPENAI_API_KEY, or --base-url for a compatible gateway). For other providers,
8
+ call ``refutescan.scan`` directly with your own factories.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import subprocess
15
+ import sys
16
+
17
+ from . import __version__
18
+ from .models import ScanConfig
19
+
20
+
21
+ def _sev_marker(sev: str) -> str:
22
+ return {"critical": "[CRIT]", "high": "[HIGH]", "medium": "[MED ]",
23
+ "low": "[LOW ]", "info": "[INFO]"}.get(sev, "[????]")
24
+
25
+
26
+ def main(argv=None) -> int:
27
+ ap = argparse.ArgumentParser(
28
+ prog="refutescan",
29
+ description="Two-LLM, refute-first agentic source-code vulnerability scanner.")
30
+ ap.add_argument("source", help="a local directory path or a git URL to clone")
31
+ ap.add_argument("--branch", default="", help="git branch to clone (git sources only)")
32
+ ap.add_argument("--sandbox", default="auto", choices=["auto", "docker", "inprocess"],
33
+ help="isolation backend (default: auto)")
34
+ ap.add_argument("--navigator-model", default="gpt-4o-mini",
35
+ help="navigator (wide-net) model (default: gpt-4o-mini)")
36
+ ap.add_argument("--judge-model", default="gpt-4o",
37
+ help="judge (refute) model (default: gpt-4o)")
38
+ ap.add_argument("--base-url", default=None,
39
+ help="OpenAI-compatible base URL (vLLM, Azure, local proxy, …)")
40
+ ap.add_argument("--json", action="store_true", help="emit the full result as JSON")
41
+ ap.add_argument("--version", action="version", version=f"refutescan {__version__}")
42
+ args = ap.parse_args(argv)
43
+
44
+ try:
45
+ from .adapters import openai_judge_factory, openai_navigator_factory
46
+ except Exception as e: # pragma: no cover
47
+ print(f"error: {e}", file=sys.stderr)
48
+ return 2
49
+
50
+ kw = {"base_url": args.base_url} if args.base_url else {}
51
+ from . import scan
52
+
53
+ def _progress(phase: str) -> None:
54
+ if not args.json:
55
+ print(f" … {phase}", file=sys.stderr)
56
+
57
+ try:
58
+ result = scan(
59
+ args.source,
60
+ navigator_factory=openai_navigator_factory(args.navigator_model, **kw),
61
+ judge_factory=openai_judge_factory(args.judge_model, **kw),
62
+ branch=args.branch,
63
+ config=ScanConfig(sandbox=args.sandbox),
64
+ progress=_progress,
65
+ )
66
+ except Exception as e:
67
+ print(f"scan failed: {type(e).__name__}: {e}", file=sys.stderr)
68
+ return 1
69
+
70
+ if args.json:
71
+ print(result.model_dump_json(indent=2))
72
+ return 0 if not result.findings else 1
73
+
74
+ s = result.summary
75
+ print(f"\nScanned {s.get('files_scanned')} files / {s.get('total_loc')} LOC"
76
+ f" · {'sandboxed' if result.sandboxed else 'in-process'}")
77
+ print(f"{result.candidate_count} candidate(s) → {len(result.findings)} confirmed, "
78
+ f"{len(result.culled)} culled\n")
79
+ if not result.findings:
80
+ print("No confirmed findings.")
81
+ return 0
82
+ for f in result.findings:
83
+ conf = f.get("confidence")
84
+ conf_s = f" (conf {conf:.2f})" if isinstance(conf, (int, float)) else ""
85
+ print(f"{_sev_marker(f.get('severity', ''))} {f.get('title')}{conf_s}")
86
+ print(f" {f.get('file')}:{f.get('line')} [{f.get('vuln_class')}]")
87
+ if f.get("reasoning"):
88
+ print(f" {f['reasoning']}")
89
+ print()
90
+ return 1 # non-zero exit when findings exist (CI-friendly)
91
+
92
+
93
+ def build_sandbox(argv=None) -> int:
94
+ """Build the docker jail image (refutescan-build-sandbox)."""
95
+ from .sandbox.docker import sandbox_dir
96
+ ap = argparse.ArgumentParser(
97
+ prog="refutescan-build-sandbox",
98
+ description="Build the refutescan docker sandbox image.")
99
+ ap.add_argument("--image", default="refutescan-sandbox:current", help="image tag to build")
100
+ args = ap.parse_args(argv)
101
+ d = sandbox_dir()
102
+ print(f"Building {args.image} from {d} ...")
103
+ try:
104
+ subprocess.run(["docker", "build", "-t", args.image, str(d)], check=True)
105
+ except FileNotFoundError:
106
+ print("error: docker not found on PATH", file=sys.stderr)
107
+ return 2
108
+ except subprocess.CalledProcessError as e:
109
+ print(f"docker build failed ({e.returncode})", file=sys.stderr)
110
+ return e.returncode
111
+ print(f"Done. Image: {args.image}")
112
+ return 0
113
+
114
+
115
+ if __name__ == "__main__":
116
+ sys.exit(main())
@@ -0,0 +1,84 @@
1
+ """Deterministic, LLM-free repository map.
2
+
3
+ A single os.walk that classifies files (source / manifest / entrypoint), counts
4
+ LOC, and returns the relative source-file list the navigator will reach. Cheap,
5
+ reproducible, and the same logic that runs inside the sandbox toolrunner.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Tuple
13
+
14
+ # Source file extensions worth scanning. Anything else (assets, lockfiles,
15
+ # binaries, minified bundles) is counted as context, not handed to the LLM.
16
+ SOURCE_EXTS = {
17
+ ".py", ".js", ".jsx", ".ts", ".tsx", ".java", ".go", ".rb", ".php", ".cs",
18
+ ".c", ".cc", ".cpp", ".h", ".hpp", ".rs", ".kt", ".scala", ".swift", ".sh",
19
+ ".bash", ".pl", ".pm", ".sql", ".html", ".vue", ".lua", ".groovy", ".tf",
20
+ }
21
+ # Directories never worth walking — vendored / generated / VCS metadata.
22
+ SKIP_DIRS = {
23
+ ".git", ".hg", ".svn", "node_modules", "vendor", "venv", ".venv", "env",
24
+ "__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build", ".next",
25
+ "site-packages", "target", ".idea", ".vscode", ".gradle", "bin", "obj",
26
+ "coverage", ".tox", ".cache", "bower_components",
27
+ }
28
+ # Dependency manifests we surface in the map (signal for what the app is).
29
+ MANIFESTS = {
30
+ "requirements.txt", "pyproject.toml", "setup.py", "Pipfile", "package.json",
31
+ "go.mod", "pom.xml", "build.gradle", "Gemfile", "composer.json", "Cargo.toml",
32
+ "csproj",
33
+ }
34
+ # Entry-point filename hints (heuristic — where untrusted input often lands).
35
+ ENTRY_HINTS = {
36
+ "app.py", "main.py", "manage.py", "wsgi.py", "asgi.py", "server.py",
37
+ "index.js", "server.js", "app.js", "main.go", "main.rs", "index.php",
38
+ "application.java",
39
+ }
40
+
41
+
42
+ def build_map(root: Path, max_map_files: int = 400,
43
+ max_file_bytes: int = 200_000) -> Tuple[Dict[str, Any], List[str]]:
44
+ """Walk the tree once. Returns (map_dict, relative_source_file_paths)."""
45
+ root = root.resolve()
46
+ langs: Dict[str, int] = {}
47
+ manifests: List[str] = []
48
+ entrypoints: List[str] = []
49
+ source_files: List[str] = []
50
+ total_files = 0
51
+ total_loc = 0
52
+
53
+ for dirpath, dirnames, filenames in os.walk(root):
54
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")]
55
+ for fn in filenames:
56
+ total_files += 1
57
+ ext = os.path.splitext(fn)[1].lower()
58
+ full = Path(dirpath) / fn
59
+ rel = str(full.relative_to(root))
60
+ if fn in MANIFESTS or fn.endswith(".csproj"):
61
+ manifests.append(rel)
62
+ if fn in ENTRY_HINTS:
63
+ entrypoints.append(rel)
64
+ if ext in SOURCE_EXTS and len(source_files) < max_map_files:
65
+ source_files.append(rel)
66
+ langs[ext] = langs.get(ext, 0) + 1
67
+ try:
68
+ if full.stat().st_size <= max_file_bytes:
69
+ with open(full, "r", errors="ignore") as fh:
70
+ total_loc += sum(1 for _ in fh)
71
+ except Exception:
72
+ pass
73
+
74
+ code_map = {
75
+ "root_name": root.name,
76
+ "total_files": total_files,
77
+ "source_files_scanned": len(source_files),
78
+ "truncated": len(source_files) >= max_map_files,
79
+ "total_loc": total_loc,
80
+ "languages": dict(sorted(langs.items(), key=lambda kv: -kv[1])),
81
+ "manifests": manifests[:30],
82
+ "entrypoints": entrypoints[:30],
83
+ }
84
+ return code_map, source_files