codespy-ai 0.2.2__tar.gz → 0.2.3__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.
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/PKG-INFO +4 -4
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/README.md +3 -3
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/pyproject.toml +1 -1
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/__init__.py +1 -1
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/modules/code_and_doc_reviewer.py +72 -53
- codespy_ai-0.2.3/src/codespy/agents/reviewer/modules/helpers.py +189 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +101 -44
- codespy_ai-0.2.2/src/codespy/agents/reviewer/modules/helpers.py +0 -65
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/LICENSE +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/cost_tracker.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/dspy_config.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/models.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/modules/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/modules/deduplicator.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/modules/scope_identifier.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/reporters/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/reporters/base.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/reporters/git.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/reporters/stdout.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/reviewer.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/cli.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/config.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/config_dspy.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/config_git.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/config_io.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/config_llm.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/filesystem/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/filesystem/client.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/filesystem/models.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/filesystem/server.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/web/server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codespy-ai
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Code review agent powered by DSPy
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -254,7 +254,7 @@ docker run --rm \
|
|
|
254
254
|
|
|
255
255
|
Add CodeSpy to your repository for automatic PR reviews:
|
|
256
256
|
|
|
257
|
-
**Trigger on
|
|
257
|
+
**Trigger on `/codespy review` comment:**
|
|
258
258
|
|
|
259
259
|
```yaml
|
|
260
260
|
# .github/workflows/codespy-review.yml
|
|
@@ -266,10 +266,10 @@ on:
|
|
|
266
266
|
|
|
267
267
|
jobs:
|
|
268
268
|
review:
|
|
269
|
-
# Only run on PR comments containing '
|
|
269
|
+
# Only run on PR comments containing '/codespy review'
|
|
270
270
|
if: |
|
|
271
271
|
github.event.issue.pull_request &&
|
|
272
|
-
contains(github.event.comment.body, '
|
|
272
|
+
contains(github.event.comment.body, '/codespy review')
|
|
273
273
|
runs-on: ubuntu-latest
|
|
274
274
|
permissions:
|
|
275
275
|
contents: read
|
|
@@ -203,7 +203,7 @@ docker run --rm \
|
|
|
203
203
|
|
|
204
204
|
Add CodeSpy to your repository for automatic PR reviews:
|
|
205
205
|
|
|
206
|
-
**Trigger on
|
|
206
|
+
**Trigger on `/codespy review` comment:**
|
|
207
207
|
|
|
208
208
|
```yaml
|
|
209
209
|
# .github/workflows/codespy-review.yml
|
|
@@ -215,10 +215,10 @@ on:
|
|
|
215
215
|
|
|
216
216
|
jobs:
|
|
217
217
|
review:
|
|
218
|
-
# Only run on PR comments containing '
|
|
218
|
+
# Only run on PR comments containing '/codespy review'
|
|
219
219
|
if: |
|
|
220
220
|
github.event.issue.pull_request &&
|
|
221
|
-
contains(github.event.comment.body, '
|
|
221
|
+
contains(github.event.comment.body, '/codespy review')
|
|
222
222
|
runs-on: ubuntu-latest
|
|
223
223
|
permissions:
|
|
224
224
|
contents: read
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/modules/code_and_doc_reviewer.py
RENAMED
|
@@ -9,7 +9,7 @@ import dspy # type: ignore[import-untyped]
|
|
|
9
9
|
|
|
10
10
|
from codespy.agents import SignatureContext, get_cost_tracker
|
|
11
11
|
from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult
|
|
12
|
-
from codespy.agents.reviewer.modules.helpers import MIN_CONFIDENCE
|
|
12
|
+
from codespy.agents.reviewer.modules.helpers import MIN_CONFIDENCE, make_scope_relative, resolve_scope_root, restore_repo_paths
|
|
13
13
|
from codespy.config import get_settings
|
|
14
14
|
from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server
|
|
15
15
|
|
|
@@ -21,16 +21,16 @@ class CodeAndDocReviewSignature(dspy.Signature):
|
|
|
21
21
|
|
|
22
22
|
You are a busy Principal Engineer with very little time.
|
|
23
23
|
Be extremely terse. Use imperative mood ("Fix X", not "You should fix X").
|
|
24
|
-
You have tools to explore the
|
|
25
|
-
All file paths are relative to the
|
|
24
|
+
You have tools to explore the scope's filesystem, search for text, and analyze code.
|
|
25
|
+
All file paths are relative to the scope root directory (the current tool root).
|
|
26
|
+
Tools are restricted to this scope — you cannot access files outside it.
|
|
26
27
|
|
|
27
28
|
═══════════════════════════════════════════════════════════════════════════════
|
|
28
29
|
PHASE 1 — READ DOCUMENTATION (MANDATORY FIRST STEP)
|
|
29
30
|
═══════════════════════════════════════════════════════════════════════════════
|
|
30
31
|
|
|
31
32
|
Use read_file to read the README at the scope root:
|
|
32
|
-
- Path:
|
|
33
|
-
- If scope.subroot is "peaks/svc/authenticator-v1", read "peaks/svc/authenticator-v1/readme.md"
|
|
33
|
+
- Path: readme.md OR README.md (paths are relative to scope root)
|
|
34
34
|
- This file provides essential context about the scope's purpose, API contracts,
|
|
35
35
|
configuration, and expected behavior. Use it to inform BOTH documentation
|
|
36
36
|
review AND defect detection.
|
|
@@ -126,7 +126,7 @@ class CodeAndDocReviewSignature(dspy.Signature):
|
|
|
126
126
|
OUTPUT RULES
|
|
127
127
|
═══════════════════════════════════════════════════════════════════════════════
|
|
128
128
|
|
|
129
|
-
- Set category to
|
|
129
|
+
- Set category to one of the values provided in the categories input
|
|
130
130
|
- For security issues, include cwe_id where applicable
|
|
131
131
|
- Reference files by name and line number only — never copy source code into issues
|
|
132
132
|
- Do not repeat patch content in reasoning steps. Keep each reasoning step to 1-2 sentences
|
|
@@ -138,12 +138,17 @@ class CodeAndDocReviewSignature(dspy.Signature):
|
|
|
138
138
|
|
|
139
139
|
scope: ScopeResult = dspy.InputField(
|
|
140
140
|
desc="Scope with changed files. Has: subroot, scope_type, "
|
|
141
|
-
"changed_files (filename + patch - analyze patch first), language, package_manifest."
|
|
141
|
+
"changed_files (filename + patch - analyze patch first), language, package_manifest. "
|
|
142
|
+
"File paths in changed_files are relative to the scope root (tool root)."
|
|
143
|
+
)
|
|
144
|
+
categories: list[IssueCategory] = dspy.InputField(
|
|
145
|
+
desc="Allowed issue categories. Use only these values for the 'category' field on each issue."
|
|
142
146
|
)
|
|
143
147
|
|
|
144
148
|
issues: list[Issue] = dspy.OutputField(
|
|
145
|
-
desc="Verified defects and documentation issues.
|
|
146
|
-
"Titles <10 words. Descriptions ≤25 words, imperative. Empty list if none."
|
|
149
|
+
desc="Verified defects and documentation issues. Category must be one of the provided categories. "
|
|
150
|
+
"Titles <10 words. Descriptions ≤25 words, imperative. Empty list if none. "
|
|
151
|
+
"File paths must be relative to scope root."
|
|
147
152
|
)
|
|
148
153
|
|
|
149
154
|
|
|
@@ -152,6 +157,9 @@ class CodeAndDocReviewer(dspy.Module):
|
|
|
152
157
|
|
|
153
158
|
Merges the responsibilities of DefectDetector and DocumentationReviewer
|
|
154
159
|
to share MCP tool overhead and README context across both concerns.
|
|
160
|
+
|
|
161
|
+
MCP tools are scope-restricted: for each scope, tools are rooted at
|
|
162
|
+
repo_path/scope.subroot so the agent cannot access files outside the scope.
|
|
155
163
|
"""
|
|
156
164
|
|
|
157
165
|
def __init__(self) -> None:
|
|
@@ -160,11 +168,11 @@ class CodeAndDocReviewer(dspy.Module):
|
|
|
160
168
|
self._cost_tracker = get_cost_tracker()
|
|
161
169
|
self._settings = get_settings()
|
|
162
170
|
|
|
163
|
-
async def _create_mcp_tools(self,
|
|
164
|
-
"""Create DSPy tools from MCP servers.
|
|
171
|
+
async def _create_mcp_tools(self, scope_root: Path) -> tuple[list[Any], list[Any]]:
|
|
172
|
+
"""Create DSPy tools from MCP servers, rooted at scope directory.
|
|
165
173
|
|
|
166
174
|
Args:
|
|
167
|
-
|
|
175
|
+
scope_root: Path to the scope root directory (repo_path / scope.subroot)
|
|
168
176
|
|
|
169
177
|
Returns:
|
|
170
178
|
Tuple of (tools list, contexts list for cleanup)
|
|
@@ -172,24 +180,24 @@ class CodeAndDocReviewer(dspy.Module):
|
|
|
172
180
|
tools: list[Any] = []
|
|
173
181
|
contexts: list[Any] = []
|
|
174
182
|
tools_dir = Path(__file__).parent.parent.parent.parent / "tools"
|
|
175
|
-
|
|
183
|
+
scope_root_str = str(scope_root)
|
|
176
184
|
caller = "code_and_doc_reviewer"
|
|
177
185
|
# Filesystem tools: read_file, list_directory, get_tree, file_exists, get_file_info
|
|
178
186
|
tools.extend(
|
|
179
187
|
await connect_mcp_server(
|
|
180
|
-
tools_dir / "filesystem" / "server.py", [
|
|
188
|
+
tools_dir / "filesystem" / "server.py", [scope_root_str], contexts, caller
|
|
181
189
|
)
|
|
182
190
|
)
|
|
183
191
|
# Ripgrep tools: search_literal, find_function_usages, find_type_usages, etc.
|
|
184
192
|
tools.extend(
|
|
185
193
|
await connect_mcp_server(
|
|
186
|
-
tools_dir / "parsers" / "ripgrep" / "server.py", [
|
|
194
|
+
tools_dir / "parsers" / "ripgrep" / "server.py", [scope_root_str], contexts, caller
|
|
187
195
|
)
|
|
188
196
|
)
|
|
189
197
|
# Treesitter tools: find_function_definitions, find_function_calls, etc.
|
|
190
198
|
tools.extend(
|
|
191
199
|
await connect_mcp_server(
|
|
192
|
-
tools_dir / "parsers" / "treesitter" / "server.py", [
|
|
200
|
+
tools_dir / "parsers" / "treesitter" / "server.py", [scope_root_str], contexts, caller
|
|
193
201
|
)
|
|
194
202
|
)
|
|
195
203
|
return tools, contexts
|
|
@@ -199,9 +207,13 @@ class CodeAndDocReviewer(dspy.Module):
|
|
|
199
207
|
) -> list[Issue]:
|
|
200
208
|
"""Analyze scopes for code defects and documentation issues.
|
|
201
209
|
|
|
210
|
+
For each scope, MCP tools are created rooted at repo_path/scope.subroot
|
|
211
|
+
so the agent can only access files within the scope boundary. This prevents
|
|
212
|
+
unnecessary out-of-scope tool calls that waste tokens and cost.
|
|
213
|
+
|
|
202
214
|
Args:
|
|
203
215
|
scopes: List of identified scopes with their changed files
|
|
204
|
-
repo_path: Path to the cloned repository
|
|
216
|
+
repo_path: Path to the cloned repository
|
|
205
217
|
|
|
206
218
|
Returns:
|
|
207
219
|
List of issues (bugs, security, documentation) found across all scopes
|
|
@@ -216,46 +228,53 @@ class CodeAndDocReviewer(dspy.Module):
|
|
|
216
228
|
return []
|
|
217
229
|
|
|
218
230
|
all_issues: list[Issue] = []
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
try:
|
|
222
|
-
max_iters = self._settings.get_max_iters("code_and_doc_review")
|
|
223
|
-
agent = dspy.ReAct(
|
|
224
|
-
signature=CodeAndDocReviewSignature,
|
|
225
|
-
tools=tools,
|
|
226
|
-
max_iters=max_iters,
|
|
227
|
-
)
|
|
231
|
+
max_iters = self._settings.get_max_iters("code_and_doc_review")
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
total_files = sum(len(s.changed_files) for s in changed_scopes)
|
|
234
|
+
logger.info(
|
|
235
|
+
f"Reviewing code and docs for {len(changed_scopes)} scopes "
|
|
236
|
+
f"({total_files} changed files)..."
|
|
237
|
+
)
|
|
234
238
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
239
|
+
for scope in changed_scopes:
|
|
240
|
+
# Scope-restrict MCP tools to the scope's subroot directory
|
|
241
|
+
scope_root = resolve_scope_root(repo_path, scope.subroot)
|
|
242
|
+
tools, contexts = await self._create_mcp_tools(scope_root)
|
|
243
|
+
try:
|
|
244
|
+
agent = dspy.ReAct(
|
|
245
|
+
signature=CodeAndDocReviewSignature,
|
|
246
|
+
tools=tools,
|
|
247
|
+
max_iters=max_iters,
|
|
248
|
+
)
|
|
249
|
+
# Create scope-relative copy so file paths match the scoped tool root
|
|
250
|
+
scoped = make_scope_relative(scope)
|
|
251
|
+
logger.info(
|
|
252
|
+
f" Reviewing scope {scope.subroot} "
|
|
253
|
+
f"({len(scope.changed_files)} files)"
|
|
254
|
+
)
|
|
255
|
+
async with SignatureContext("code_and_doc_review", self._cost_tracker):
|
|
256
|
+
result = await agent.acall(
|
|
257
|
+
scope=scoped,
|
|
258
|
+
categories=[IssueCategory.BUG, IssueCategory.SECURITY, IssueCategory.DOCUMENTATION],
|
|
251
259
|
)
|
|
252
|
-
except Exception as e:
|
|
253
|
-
logger.error(f"Review failed for scope {scope.subroot}: {e}")
|
|
254
260
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
261
|
+
issues = [
|
|
262
|
+
issue for issue in (result.issues or [])
|
|
263
|
+
if issue.confidence >= MIN_CONFIDENCE
|
|
264
|
+
]
|
|
265
|
+
# Restore repo-root-relative paths in reported issues
|
|
266
|
+
restore_repo_paths(issues, scope.subroot)
|
|
267
|
+
all_issues.extend(issues)
|
|
268
|
+
logger.debug(
|
|
269
|
+
f" Scope {scope.subroot}: {len(issues)} issues"
|
|
270
|
+
)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.error(f"Review failed for scope {scope.subroot}: {e}")
|
|
273
|
+
finally:
|
|
274
|
+
await cleanup_mcp_contexts(contexts)
|
|
275
|
+
|
|
276
|
+
logger.info(f"Code and doc review found {len(all_issues)} issues")
|
|
277
|
+
return all_issues
|
|
259
278
|
|
|
260
279
|
def forward(
|
|
261
280
|
self, scopes: Sequence[ScopeResult], repo_path: Path
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Helper functions for DSPy review modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from codespy.tools.git.models import ChangedFile
|
|
11
|
+
from codespy.agents.reviewer.models import Issue
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from codespy.agents.reviewer.models import ScopeResult
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Language detection based on file extension
|
|
20
|
+
EXTENSION_TO_LANGUAGE = {
|
|
21
|
+
"py": "Python",
|
|
22
|
+
"js": "JavaScript",
|
|
23
|
+
"ts": "TypeScript",
|
|
24
|
+
"jsx": "JavaScript (React)",
|
|
25
|
+
"tsx": "TypeScript (React)",
|
|
26
|
+
"go": "Go",
|
|
27
|
+
"rs": "Rust",
|
|
28
|
+
"java": "Java",
|
|
29
|
+
"kt": "Kotlin",
|
|
30
|
+
"c": "C",
|
|
31
|
+
"cpp": "C++",
|
|
32
|
+
"h": "C/C++ Header",
|
|
33
|
+
"hpp": "C++ Header",
|
|
34
|
+
"cs": "C#",
|
|
35
|
+
"rb": "Ruby",
|
|
36
|
+
"php": "PHP",
|
|
37
|
+
"swift": "Swift",
|
|
38
|
+
"scala": "Scala",
|
|
39
|
+
"sh": "Shell",
|
|
40
|
+
"bash": "Bash",
|
|
41
|
+
"sql": "SQL",
|
|
42
|
+
"vue": "Vue",
|
|
43
|
+
"svelte": "Svelte",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Minimum confidence threshold
|
|
47
|
+
MIN_CONFIDENCE = 0.5
|
|
48
|
+
|
|
49
|
+
# Markdown file extensions to review
|
|
50
|
+
MARKDOWN_EXTENSIONS = {".md", ".markdown", ".mdx", ".rst", ".txt"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_markdown_file(filename: str) -> bool:
|
|
54
|
+
"""Check if the file is a markdown documentation file."""
|
|
55
|
+
_, ext = os.path.splitext(filename.lower())
|
|
56
|
+
return ext in MARKDOWN_EXTENSIONS
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_language(file: ChangedFile) -> str:
|
|
60
|
+
"""Get the programming language for a file based on extension.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
file: The changed file
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Language name or "Unknown"
|
|
67
|
+
"""
|
|
68
|
+
return EXTENSION_TO_LANGUAGE.get(file.extension, "Unknown")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def strip_prefix(path: str, prefix: str) -> str:
|
|
72
|
+
"""Strip a directory prefix from a file path.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
path: Original file path (e.g., "packages/auth/src/index.ts")
|
|
76
|
+
prefix: Prefix to strip (e.g., "packages/auth")
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Relative path (e.g., "src/index.ts"). Returns path unchanged
|
|
80
|
+
if prefix is "." or path doesn't start with the prefix.
|
|
81
|
+
"""
|
|
82
|
+
if prefix == "." or not prefix:
|
|
83
|
+
return path
|
|
84
|
+
# Normalize: ensure prefix ends with "/" for clean stripping
|
|
85
|
+
normalized = prefix.rstrip("/") + "/"
|
|
86
|
+
if path.startswith(normalized):
|
|
87
|
+
return path[len(normalized):]
|
|
88
|
+
# Exact match (file at scope root)
|
|
89
|
+
if path == prefix:
|
|
90
|
+
return os.path.basename(path)
|
|
91
|
+
return path
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_scope_root(repo_path: Path, subroot: str) -> Path:
|
|
95
|
+
"""Resolve the absolute path for a scope root directory.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
repo_path: Path to the repository root
|
|
99
|
+
subroot: Scope subroot relative to repo root (e.g., "packages/auth" or ".")
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
repo_path unchanged if subroot is ".", otherwise repo_path / subroot
|
|
103
|
+
"""
|
|
104
|
+
return repo_path if subroot == "." else repo_path / subroot
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def make_scope_relative(scope: ScopeResult) -> ScopeResult:
|
|
108
|
+
"""Create a copy of a ScopeResult with file paths relative to scope.subroot.
|
|
109
|
+
|
|
110
|
+
When MCP tools are rooted at repo_path/scope.subroot, the agent needs file
|
|
111
|
+
paths relative to the scope root (not the repo root). This function creates
|
|
112
|
+
a shallow copy of the scope with adjusted filenames.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
scope: Original scope with repo-root-relative file paths
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
New ScopeResult with scope-relative file paths in changed_files.
|
|
119
|
+
The subroot is set to "." since paths are now relative to it.
|
|
120
|
+
"""
|
|
121
|
+
from codespy.agents.reviewer.models import PackageManifest, ScopeResult as SR
|
|
122
|
+
|
|
123
|
+
if scope.subroot == ".":
|
|
124
|
+
return scope # Already at repo root, no transformation needed
|
|
125
|
+
|
|
126
|
+
relative_files = [
|
|
127
|
+
ChangedFile(
|
|
128
|
+
filename=strip_prefix(f.filename, scope.subroot),
|
|
129
|
+
status=f.status,
|
|
130
|
+
additions=f.additions,
|
|
131
|
+
deletions=f.deletions,
|
|
132
|
+
patch=f.patch,
|
|
133
|
+
previous_filename=(
|
|
134
|
+
strip_prefix(f.previous_filename, scope.subroot)
|
|
135
|
+
if f.previous_filename
|
|
136
|
+
else None
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
for f in scope.changed_files
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# Adjust manifest paths too
|
|
143
|
+
manifest = None
|
|
144
|
+
if scope.package_manifest:
|
|
145
|
+
manifest = PackageManifest(
|
|
146
|
+
manifest_path=strip_prefix(scope.package_manifest.manifest_path, scope.subroot),
|
|
147
|
+
lock_file_path=(
|
|
148
|
+
strip_prefix(scope.package_manifest.lock_file_path, scope.subroot)
|
|
149
|
+
if scope.package_manifest.lock_file_path
|
|
150
|
+
else None
|
|
151
|
+
),
|
|
152
|
+
package_manager=scope.package_manifest.package_manager,
|
|
153
|
+
dependencies_changed=scope.package_manifest.dependencies_changed,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return SR(
|
|
157
|
+
subroot=".",
|
|
158
|
+
scope_type=scope.scope_type,
|
|
159
|
+
has_changes=scope.has_changes,
|
|
160
|
+
is_dependency=scope.is_dependency,
|
|
161
|
+
confidence=scope.confidence,
|
|
162
|
+
language=scope.language,
|
|
163
|
+
package_manifest=manifest,
|
|
164
|
+
changed_files=relative_files,
|
|
165
|
+
reason=scope.reason,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def restore_repo_paths(issues: list[Issue], subroot: str) -> None:
|
|
170
|
+
"""Restore repo-root-relative paths in issue filenames (in-place).
|
|
171
|
+
|
|
172
|
+
After the agent reports issues with scope-relative paths, this function
|
|
173
|
+
prepends the scope subroot so filenames are repo-root-relative again.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
issues: List of issues to modify in-place
|
|
177
|
+
subroot: Scope subroot to prepend (e.g., "packages/auth")
|
|
178
|
+
"""
|
|
179
|
+
if subroot == "." or not subroot:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
prefix = subroot.rstrip("/") + "/"
|
|
183
|
+
for issue in issues:
|
|
184
|
+
if issue.filename and not issue.filename.startswith(prefix):
|
|
185
|
+
issue.filename = prefix + issue.filename
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/modules/supply_chain_auditor.py
RENAMED
|
@@ -9,7 +9,7 @@ import dspy # type: ignore[import-untyped]
|
|
|
9
9
|
|
|
10
10
|
from codespy.agents import SignatureContext, get_cost_tracker
|
|
11
11
|
from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult
|
|
12
|
-
from codespy.agents.reviewer.modules.helpers import MIN_CONFIDENCE
|
|
12
|
+
from codespy.agents.reviewer.modules.helpers import MIN_CONFIDENCE, resolve_scope_root, strip_prefix, restore_repo_paths
|
|
13
13
|
from codespy.config import get_settings
|
|
14
14
|
from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server
|
|
15
15
|
|
|
@@ -22,16 +22,19 @@ class SupplyChainSecuritySignature(dspy.Signature):
|
|
|
22
22
|
You are a busy Principal Engineer reviewing supply chain security. Focus on critical risks only.
|
|
23
23
|
Be extremely terse. Use imperative mood.
|
|
24
24
|
You have access to:
|
|
25
|
-
- Filesystem tools to read files and explore the
|
|
25
|
+
- Filesystem tools to read files and explore the scope (rooted at scope directory)
|
|
26
26
|
- OSV (Open Source Vulnerabilities) tools to query real vulnerability data
|
|
27
27
|
|
|
28
|
+
All file paths are relative to the scope root directory (the current tool root).
|
|
29
|
+
Tools are restricted to this scope — you cannot access files outside it.
|
|
30
|
+
|
|
28
31
|
You will analyze TWO types of supply chain security concerns:
|
|
29
32
|
|
|
30
33
|
## 1. DOCKERFILE SECURITY
|
|
31
34
|
|
|
32
35
|
FIRST, search for Dockerfiles in the scope using filesystem tools:
|
|
33
36
|
- Use get_tree or list_directory to find Dockerfile, Dockerfile.*, *.Dockerfile, Containerfile
|
|
34
|
-
- Common locations:
|
|
37
|
+
- Common locations: root (""), docker/, build/, .docker/ directories
|
|
35
38
|
- Once found, use read_file to get the contents
|
|
36
39
|
|
|
37
40
|
For each Dockerfile found, check for:
|
|
@@ -51,11 +54,12 @@ class SupplyChainSecuritySignature(dspy.Signature):
|
|
|
51
54
|
- description: ≤25 words, imperative tone, no filler ("Fix X", "Pin Y").
|
|
52
55
|
- No polite or conversational language ("I suggest", "Please consider", "Great").
|
|
53
56
|
- Do not populate code_snippet—use line numbers instead.
|
|
57
|
+
- File paths in issues must be relative to the scope root.
|
|
54
58
|
|
|
55
59
|
## 2. DEPENDENCY SECURITY (package manifests)
|
|
56
60
|
|
|
57
61
|
If manifest info is provided:
|
|
58
|
-
1. Use read_file to read the manifest file at manifest_path
|
|
62
|
+
1. Use read_file to read the manifest file at manifest_path (relative to scope root)
|
|
59
63
|
2. Extract ALL dependencies with their names and versions
|
|
60
64
|
3. Use OSV tools to scan dependencies for vulnerabilities
|
|
61
65
|
4. Only report vulnerabilities actually found by OSV queries
|
|
@@ -104,14 +108,11 @@ class SupplyChainSecuritySignature(dspy.Signature):
|
|
|
104
108
|
- CWE ID if applicable
|
|
105
109
|
"""
|
|
106
110
|
|
|
107
|
-
scope_subroot: str = dspy.InputField(
|
|
108
|
-
desc="Path to scope root relative to repo root. Search here for Dockerfiles."
|
|
109
|
-
)
|
|
110
111
|
manifest_path: str = dspy.InputField(
|
|
111
|
-
desc="Path to manifest file (e.g., package.json, go.mod). Empty string if none."
|
|
112
|
+
desc="Path to manifest file relative to scope root (e.g., package.json, go.mod). Empty string if none."
|
|
112
113
|
)
|
|
113
114
|
lock_file_path: str = dspy.InputField(
|
|
114
|
-
desc="Path to lock file (e.g., package-lock.json, go.sum). Empty string if none."
|
|
115
|
+
desc="Path to lock file relative to scope root (e.g., package-lock.json, go.sum). Empty string if none."
|
|
115
116
|
)
|
|
116
117
|
package_manager: str = dspy.InputField(
|
|
117
118
|
desc="Package manager name (e.g., npm, pip, go). Empty string if no manifest."
|
|
@@ -121,14 +122,18 @@ class SupplyChainSecuritySignature(dspy.Signature):
|
|
|
121
122
|
)
|
|
122
123
|
|
|
123
124
|
issues: list[Issue] = dspy.OutputField(
|
|
124
|
-
desc="Verified supply chain issues only. Titles <10 words. Descriptions ≤25 words, imperative.
|
|
125
|
+
desc="Verified supply chain issues only. Titles <10 words. Descriptions ≤25 words, imperative. "
|
|
126
|
+
"File paths must be relative to scope root. Empty list if none."
|
|
125
127
|
)
|
|
126
128
|
|
|
127
129
|
|
|
128
130
|
class SupplyChainAuditor(dspy.Module):
|
|
129
|
-
"""Analyzes supply chain security (Dockerfiles and dependencies) using DSPy.
|
|
131
|
+
"""Analyzes supply chain security (Dockerfiles and dependencies) using DSPy.
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
MCP tools are scope-restricted: for each scope, filesystem/parser tools are
|
|
134
|
+
rooted at repo_path/scope.subroot so the agent cannot access files outside
|
|
135
|
+
the scope. OSV tools (no filesystem root) are shared across scopes.
|
|
136
|
+
"""
|
|
132
137
|
|
|
133
138
|
def __init__(self) -> None:
|
|
134
139
|
"""Initialize the security auditor."""
|
|
@@ -142,22 +147,30 @@ class SupplyChainAuditor(dspy.Module):
|
|
|
142
147
|
Returns True if any scope has dependency changes or Dockerfile modifications.
|
|
143
148
|
Avoids spinning up MCP servers when there's nothing to audit.
|
|
144
149
|
"""
|
|
150
|
+
return any(self._scope_needs_analysis(s) for s in scopes)
|
|
151
|
+
|
|
152
|
+
def _scope_needs_analysis(self, scope: ScopeResult) -> bool:
|
|
153
|
+
"""Check if a specific scope has supply-chain-relevant changes.
|
|
154
|
+
|
|
155
|
+
Returns True if the scope has dependency changes or Dockerfile modifications.
|
|
156
|
+
"""
|
|
145
157
|
scan_unchanged = self._settings.get_scan_unchanged("supply_chain")
|
|
146
|
-
|
|
147
|
-
if scope.package_manifest:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return True
|
|
158
|
+
if scope.package_manifest:
|
|
159
|
+
if scan_unchanged or scope.package_manifest.dependencies_changed:
|
|
160
|
+
return True
|
|
161
|
+
for cf in scope.changed_files:
|
|
162
|
+
fname = cf.filename.rsplit("/", 1)[-1].lower()
|
|
163
|
+
if "dockerfile" in fname or fname == "containerfile":
|
|
164
|
+
return True
|
|
154
165
|
return False
|
|
155
166
|
|
|
156
|
-
async def
|
|
157
|
-
|
|
167
|
+
async def _create_scoped_tools(
|
|
168
|
+
self, scope_root: Path
|
|
169
|
+
) -> tuple[list[Any], list[Any]]:
|
|
170
|
+
"""Create scope-restricted DSPy tools from filesystem and parser MCP servers.
|
|
158
171
|
|
|
159
172
|
Args:
|
|
160
|
-
|
|
173
|
+
scope_root: Path to the scope root directory (repo_path / scope.subroot)
|
|
161
174
|
|
|
162
175
|
Returns:
|
|
163
176
|
Tuple of (tools, contexts) for cleanup
|
|
@@ -165,13 +178,13 @@ class SupplyChainAuditor(dspy.Module):
|
|
|
165
178
|
tools: list[Any] = []
|
|
166
179
|
contexts: list[Any] = []
|
|
167
180
|
tools_dir = Path(__file__).parent.parent.parent.parent / "tools"
|
|
168
|
-
|
|
181
|
+
scope_root_str = str(scope_root)
|
|
169
182
|
caller = "supply_chain_auditor"
|
|
170
183
|
|
|
171
184
|
# Add filesystem tools for reading files and exploring structure
|
|
172
185
|
tools.extend(await connect_mcp_server(
|
|
173
186
|
tools_dir / "filesystem" / "server.py",
|
|
174
|
-
[
|
|
187
|
+
[scope_root_str],
|
|
175
188
|
contexts,
|
|
176
189
|
caller,
|
|
177
190
|
))
|
|
@@ -179,7 +192,7 @@ class SupplyChainAuditor(dspy.Module):
|
|
|
179
192
|
# Add tree-sitter tools for parsing code structure
|
|
180
193
|
tools.extend(await connect_mcp_server(
|
|
181
194
|
tools_dir / "parsers" / "treesitter" / "server.py",
|
|
182
|
-
[
|
|
195
|
+
[scope_root_str],
|
|
183
196
|
contexts,
|
|
184
197
|
caller,
|
|
185
198
|
))
|
|
@@ -187,12 +200,24 @@ class SupplyChainAuditor(dspy.Module):
|
|
|
187
200
|
# Add ripgrep tools for searching code patterns
|
|
188
201
|
tools.extend(await connect_mcp_server(
|
|
189
202
|
tools_dir / "parsers" / "ripgrep" / "server.py",
|
|
190
|
-
[
|
|
203
|
+
[scope_root_str],
|
|
191
204
|
contexts,
|
|
192
205
|
caller,
|
|
193
206
|
))
|
|
194
207
|
|
|
195
|
-
|
|
208
|
+
return tools, contexts
|
|
209
|
+
|
|
210
|
+
async def _create_osv_tools(self) -> tuple[list[Any], list[Any]]:
|
|
211
|
+
"""Create OSV tools (no filesystem root, can be shared).
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Tuple of (tools, contexts) for cleanup
|
|
215
|
+
"""
|
|
216
|
+
tools: list[Any] = []
|
|
217
|
+
contexts: list[Any] = []
|
|
218
|
+
tools_dir = Path(__file__).parent.parent.parent.parent / "tools"
|
|
219
|
+
caller = "supply_chain_auditor"
|
|
220
|
+
|
|
196
221
|
tools.extend(await connect_mcp_server(
|
|
197
222
|
tools_dir / "cyber" / "osv" / "server.py",
|
|
198
223
|
[],
|
|
@@ -205,6 +230,11 @@ class SupplyChainAuditor(dspy.Module):
|
|
|
205
230
|
async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list[Issue]:
|
|
206
231
|
"""Analyze scopes for supply chain security vulnerabilities and return issues.
|
|
207
232
|
|
|
233
|
+
For each scope, filesystem/parser tools are created rooted at
|
|
234
|
+
repo_path/scope.subroot so the agent can only access files within the
|
|
235
|
+
scope boundary. OSV tools are shared across scopes since they don't
|
|
236
|
+
access the filesystem.
|
|
237
|
+
|
|
208
238
|
Args:
|
|
209
239
|
scopes: The scopes containing changed files to analyze
|
|
210
240
|
repo_path: Path to the cloned repository for reading manifest files
|
|
@@ -223,30 +253,53 @@ class SupplyChainAuditor(dspy.Module):
|
|
|
223
253
|
return []
|
|
224
254
|
|
|
225
255
|
all_issues: list[Issue] = []
|
|
226
|
-
tools, contexts = await self._create_mcp_tools(repo_path)
|
|
227
|
-
|
|
228
256
|
supply_chain_max_iters = self._settings.get_max_iters("supply_chain")
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
max_iters=supply_chain_max_iters,
|
|
233
|
-
)
|
|
257
|
+
|
|
258
|
+
# Create OSV tools once (shared across scopes, no filesystem root)
|
|
259
|
+
osv_tools, osv_contexts = await self._create_osv_tools()
|
|
234
260
|
|
|
235
261
|
try:
|
|
236
262
|
for scope in scopes:
|
|
263
|
+
# Skip scopes with no supply-chain-relevant changes
|
|
264
|
+
if not self._scope_needs_analysis(scope):
|
|
265
|
+
continue
|
|
266
|
+
|
|
237
267
|
# Check if we should scan unchanged manifests
|
|
238
268
|
scan_unchanged = self._settings.get_scan_unchanged("supply_chain")
|
|
239
269
|
|
|
240
270
|
# Get manifest info (skip if unchanged and scan_unchanged=False)
|
|
241
271
|
manifest = scope.package_manifest
|
|
242
272
|
should_scan_manifest = manifest and (scan_unchanged or manifest.dependencies_changed)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
|
|
274
|
+
# Convert manifest paths to scope-relative
|
|
275
|
+
if should_scan_manifest:
|
|
276
|
+
manifest_path = strip_prefix(manifest.manifest_path, scope.subroot)
|
|
277
|
+
lock_file_path = (
|
|
278
|
+
strip_prefix(manifest.lock_file_path, scope.subroot)
|
|
279
|
+
if manifest.lock_file_path
|
|
280
|
+
else ""
|
|
281
|
+
)
|
|
282
|
+
package_manager = manifest.package_manager
|
|
283
|
+
else:
|
|
284
|
+
manifest_path = ""
|
|
285
|
+
lock_file_path = ""
|
|
286
|
+
package_manager = ""
|
|
287
|
+
if manifest:
|
|
288
|
+
logger.debug(f"Skipping unchanged manifest: {manifest.manifest_path}")
|
|
289
|
+
|
|
290
|
+
# Scope-restrict filesystem/parser tools to the scope's subroot
|
|
291
|
+
scope_root = resolve_scope_root(repo_path, scope.subroot)
|
|
292
|
+
scoped_tools, scoped_contexts = await self._create_scoped_tools(scope_root)
|
|
293
|
+
|
|
249
294
|
try:
|
|
295
|
+
# Combine scoped filesystem tools with shared OSV tools
|
|
296
|
+
all_tools = scoped_tools + osv_tools
|
|
297
|
+
supply_chain_agent = dspy.ReAct(
|
|
298
|
+
signature=SupplyChainSecuritySignature,
|
|
299
|
+
tools=all_tools,
|
|
300
|
+
max_iters=supply_chain_max_iters,
|
|
301
|
+
)
|
|
302
|
+
|
|
250
303
|
logger.debug(
|
|
251
304
|
f"Analyzing supply chain in scope {scope.subroot}: "
|
|
252
305
|
f"manifest={bool(manifest_path)}"
|
|
@@ -254,22 +307,26 @@ class SupplyChainAuditor(dspy.Module):
|
|
|
254
307
|
# Track supply_chain signature costs separately
|
|
255
308
|
async with SignatureContext("supply_chain", self._cost_tracker):
|
|
256
309
|
result = await supply_chain_agent.acall(
|
|
257
|
-
scope_subroot=scope.subroot,
|
|
258
310
|
manifest_path=manifest_path,
|
|
259
311
|
lock_file_path=lock_file_path,
|
|
260
312
|
package_manager=package_manager,
|
|
261
|
-
category=
|
|
313
|
+
category=IssueCategory.SECURITY,
|
|
262
314
|
)
|
|
263
315
|
issues = [
|
|
264
316
|
issue for issue in result.issues
|
|
265
317
|
if issue.confidence >= MIN_CONFIDENCE
|
|
266
318
|
]
|
|
319
|
+
# Restore repo-root-relative paths in reported issues
|
|
320
|
+
restore_repo_paths(issues, scope.subroot)
|
|
267
321
|
all_issues.extend(issues)
|
|
268
322
|
logger.debug(f" Supply chain security in scope {scope.subroot}: {len(issues)} issues")
|
|
269
323
|
except Exception as e:
|
|
270
324
|
logger.error(f"Error analyzing supply chain in scope {scope.subroot}: {e}")
|
|
325
|
+
finally:
|
|
326
|
+
await cleanup_mcp_contexts(scoped_contexts)
|
|
271
327
|
finally:
|
|
272
|
-
await cleanup_mcp_contexts(
|
|
328
|
+
await cleanup_mcp_contexts(osv_contexts)
|
|
329
|
+
|
|
273
330
|
logger.info(f"Security audit found {len(all_issues)} issues")
|
|
274
331
|
return all_issues
|
|
275
332
|
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""Helper functions for DSPy review modules."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
from codespy.tools.git.models import ChangedFile
|
|
7
|
-
from codespy.agents.reviewer.models import Issue
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Language detection based on file extension
|
|
13
|
-
EXTENSION_TO_LANGUAGE = {
|
|
14
|
-
"py": "Python",
|
|
15
|
-
"js": "JavaScript",
|
|
16
|
-
"ts": "TypeScript",
|
|
17
|
-
"jsx": "JavaScript (React)",
|
|
18
|
-
"tsx": "TypeScript (React)",
|
|
19
|
-
"go": "Go",
|
|
20
|
-
"rs": "Rust",
|
|
21
|
-
"java": "Java",
|
|
22
|
-
"kt": "Kotlin",
|
|
23
|
-
"c": "C",
|
|
24
|
-
"cpp": "C++",
|
|
25
|
-
"h": "C/C++ Header",
|
|
26
|
-
"hpp": "C++ Header",
|
|
27
|
-
"cs": "C#",
|
|
28
|
-
"rb": "Ruby",
|
|
29
|
-
"php": "PHP",
|
|
30
|
-
"swift": "Swift",
|
|
31
|
-
"scala": "Scala",
|
|
32
|
-
"sh": "Shell",
|
|
33
|
-
"bash": "Bash",
|
|
34
|
-
"sql": "SQL",
|
|
35
|
-
"vue": "Vue",
|
|
36
|
-
"svelte": "Svelte",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# Minimum confidence threshold
|
|
40
|
-
MIN_CONFIDENCE = 0.5
|
|
41
|
-
|
|
42
|
-
# Markdown file extensions to review
|
|
43
|
-
MARKDOWN_EXTENSIONS = {".md", ".markdown", ".mdx", ".rst", ".txt"}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def is_markdown_file(filename: str) -> bool:
|
|
47
|
-
"""Check if the file is a markdown documentation file."""
|
|
48
|
-
_, ext = os.path.splitext(filename.lower())
|
|
49
|
-
return ext in MARKDOWN_EXTENSIONS
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def get_language(file: ChangedFile) -> str:
|
|
53
|
-
"""Get the programming language for a file based on extension.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
file: The changed file
|
|
57
|
-
|
|
58
|
-
Returns:
|
|
59
|
-
Language name or "Unknown"
|
|
60
|
-
"""
|
|
61
|
-
return EXTENSION_TO_LANGUAGE.get(file.extension, "Unknown")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/agents/reviewer/modules/scope_identifier.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/base_extractor.py
RENAMED
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/java.py
RENAMED
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/javascript.py
RENAMED
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py
RENAMED
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/objc.py
RENAMED
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/python.py
RENAMED
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/rust.py
RENAMED
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/swift.py
RENAMED
|
File without changes
|
{codespy_ai-0.2.2 → codespy_ai-0.2.3}/src/codespy/tools/parsers/treesitter/extractors/terraform.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|