codegraph-brain 0.7.3__py3-none-any.whl → 0.7.5__py3-none-any.whl
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.
- cgis/api/mcp_server.py +73 -1
- cgis/guardian/findings.py +5 -1
- cgis/guardian/render.py +1 -0
- cgis/resolver/engine.py +1 -1
- cgis/resolver/indices.py +41 -22
- {codegraph_brain-0.7.3.dist-info → codegraph_brain-0.7.5.dist-info}/METADATA +1 -1
- {codegraph_brain-0.7.3.dist-info → codegraph_brain-0.7.5.dist-info}/RECORD +10 -10
- {codegraph_brain-0.7.3.dist-info → codegraph_brain-0.7.5.dist-info}/WHEEL +0 -0
- {codegraph_brain-0.7.3.dist-info → codegraph_brain-0.7.5.dist-info}/entry_points.txt +0 -0
- {codegraph_brain-0.7.3.dist-info → codegraph_brain-0.7.5.dist-info}/licenses/LICENSE +0 -0
cgis/api/mcp_server.py
CHANGED
|
@@ -85,20 +85,92 @@ def _render_subgraph(
|
|
|
85
85
|
return f"❌ Unknown format '{output_format}'. Use 'mermaid' or 'json'."
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
#: Names cgis will create a database under. Everything else is refused, so an
|
|
89
|
+
#: agent cannot be talked into materialising `~/.ssh/authorized_keys` (#312).
|
|
90
|
+
_DB_SUFFIXES = frozenset({".db", ".sqlite", ".sqlite3"})
|
|
91
|
+
|
|
92
|
+
#: First 16 bytes of any SQLite file.
|
|
93
|
+
_SQLITE_MAGIC = b"SQLite format 3\x00"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _reject_db_path(db_path: str) -> str | None:
|
|
97
|
+
"""Return a refusal message for an unusable ``db_path``, or None if it is fine.
|
|
98
|
+
|
|
99
|
+
``cgis_ingest`` is the only MCP tool that creates its database — the other
|
|
100
|
+
twelve refuse a path that does not already exist, which is guard enough for
|
|
101
|
+
them. It needs a different one: it reads untrusted repository content and is
|
|
102
|
+
then told by the same agent where to write, so ``db_path`` is attacker-
|
|
103
|
+
reachable in a way the read-only tools' paths are not.
|
|
104
|
+
|
|
105
|
+
Refuses an unexpected suffix, a missing parent directory (creating a tree is
|
|
106
|
+
never wanted), a directory target, and an existing file that is not a
|
|
107
|
+
database. The last is belt-and-braces — SQLite already declines to open a
|
|
108
|
+
non-database — but it fails with a message that says why.
|
|
109
|
+
|
|
110
|
+
Everything is judged on the **resolved** path. A dangling symlink is the
|
|
111
|
+
bypass this guard exists to stop: ``attack.db`` pointing at a target that
|
|
112
|
+
does not exist yet makes ``is_file()`` return False, every check passes, and
|
|
113
|
+
SQLite creates the target. Resolving first means the suffix rule applies to
|
|
114
|
+
where the write actually lands. (A symlink to an *existing* non-database was
|
|
115
|
+
already refused by the magic-byte check.)
|
|
116
|
+
|
|
117
|
+
The filesystem probes are wrapped: this runs *before* ``cgis_ingest``'s own
|
|
118
|
+
try/except, and ``Path.resolve()``, ``Path.is_dir()`` and friends propagate
|
|
119
|
+
``OSError`` (a ``PermissionError`` on the parent, or a symlink loop), which
|
|
120
|
+
would escape the tool as a crash instead of a message the agent can act on.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
path = Path(db_path).resolve()
|
|
124
|
+
except OSError as exc:
|
|
125
|
+
return f"❌ Refusing db_path '{db_path}': path is inaccessible ({exc})."
|
|
126
|
+
# Case-insensitive: `.DB` carries the same intent, and on a case-insensitive
|
|
127
|
+
# filesystem it is literally the same file.
|
|
128
|
+
if path.suffix.lower() not in _DB_SUFFIXES:
|
|
129
|
+
allowed = ", ".join(sorted(_DB_SUFFIXES))
|
|
130
|
+
return f"❌ Refusing db_path '{db_path}': name must end in one of {allowed}."
|
|
131
|
+
try:
|
|
132
|
+
if path.is_dir():
|
|
133
|
+
return f"❌ Refusing db_path '{db_path}': it is a directory."
|
|
134
|
+
if not path.parent.is_dir():
|
|
135
|
+
return (
|
|
136
|
+
f"❌ Refusing db_path '{db_path}': parent directory does not exist. "
|
|
137
|
+
"cgis will not create one."
|
|
138
|
+
)
|
|
139
|
+
if path.is_file() and path.stat().st_size > 0:
|
|
140
|
+
with path.open("rb") as fh:
|
|
141
|
+
if fh.read(len(_SQLITE_MAGIC)) != _SQLITE_MAGIC:
|
|
142
|
+
return (
|
|
143
|
+
f"❌ Refusing db_path '{db_path}': existing file is not a SQLite database."
|
|
144
|
+
)
|
|
145
|
+
except OSError as exc:
|
|
146
|
+
return f"❌ Refusing db_path '{db_path}': path is inaccessible ({exc})."
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
88
150
|
@mcp.tool()
|
|
89
151
|
def cgis_ingest(project_path: str, db_path: str = _DEFAULT_DB, full_rebuild: bool = False) -> str:
|
|
90
152
|
"""Scan a local directory, extract all symbols, resolve links, and build the graph DB.
|
|
91
153
|
|
|
92
154
|
Use this to initialise or refresh the code knowledge graph for a project.
|
|
93
|
-
|
|
155
|
+
Node FQNs are normalised relative to the workspace root so the graph is
|
|
94
156
|
portable across machines.
|
|
95
157
|
|
|
158
|
+
``db_path`` must name a database — it has to end in ``.db``, ``.sqlite`` or
|
|
159
|
+
``.sqlite3``, live in a directory that already exists, and not point at an
|
|
160
|
+
existing file that is not a SQLite database. cgis will not create parent
|
|
161
|
+
directories.
|
|
162
|
+
|
|
96
163
|
By default the ingest is **incremental**: only changed/new files are
|
|
97
164
|
re-scanned, and the summary reports both what changed this run and the
|
|
98
165
|
whole-graph total. Set ``full_rebuild=True`` to re-scan every file and
|
|
99
166
|
overwrite the database from scratch — use this to drop nodes for files that
|
|
100
167
|
were deleted or renamed, which an incremental run leaves behind.
|
|
101
168
|
"""
|
|
169
|
+
refusal = _reject_db_path(db_path)
|
|
170
|
+
if refusal is not None:
|
|
171
|
+
logger.warning("MCP ingest refused db_path", db=db_path)
|
|
172
|
+
return refusal
|
|
173
|
+
|
|
102
174
|
pipeline = IngestionPipeline(_EXTRACTORS)
|
|
103
175
|
try:
|
|
104
176
|
with SQLiteStore(db_path) as store:
|
cgis/guardian/findings.py
CHANGED
|
@@ -5,7 +5,11 @@ from typing import Literal
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
7
|
Severity = Literal["critical", "major", "minor"]
|
|
8
|
-
|
|
8
|
+
# "security" exists so curated ground truth can name an exploitable defect (#315).
|
|
9
|
+
# The finder prompt does not yet offer it as a choice — teaching the finder to
|
|
10
|
+
# emit it is #258, and needs bench validation. Scoring is unaffected either way:
|
|
11
|
+
# bench matching compares file and line range, never category.
|
|
12
|
+
Category = Literal["logic", "contract", "tests", "types", "ontology", "security"]
|
|
9
13
|
Verdict = Literal["confirmed", "refuted", "uncertain"]
|
|
10
14
|
|
|
11
15
|
|
cgis/guardian/render.py
CHANGED
cgis/resolver/engine.py
CHANGED
|
@@ -126,7 +126,7 @@ class ResolverEngine:
|
|
|
126
126
|
|
|
127
127
|
def _ensure_virtual_node(self, target: str, virtual_nodes: dict[str, Node]) -> None:
|
|
128
128
|
"""Create a virtual boundary node for target if it is not already in the graph."""
|
|
129
|
-
if
|
|
129
|
+
if not self._index.has_node(target) and target not in virtual_nodes:
|
|
130
130
|
virtual_nodes[target] = self._make_virtual_node(
|
|
131
131
|
target, self._index.classify_fqn(target)
|
|
132
132
|
)
|
cgis/resolver/indices.py
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import builtins
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
|
+
from collections.abc import Mapping
|
|
6
7
|
from dataclasses import dataclass
|
|
8
|
+
from types import MappingProxyType
|
|
7
9
|
|
|
8
10
|
from cgis.core.models import SELF_PREFIX, Node, NodeNamespace, NodeType
|
|
9
11
|
|
|
@@ -15,30 +17,37 @@ class SymbolIndex:
|
|
|
15
17
|
"""Immutable lookup indices over the extracted node set.
|
|
16
18
|
|
|
17
19
|
Built by IndexBuilder, consumed by SymbolResolver and ResolverEngine.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
Immutable in substance, not only by convention: the dataclass prevents
|
|
22
|
+
field rebinding, and ``IndexBuilder`` hands over read-only views, so a
|
|
23
|
+
stray write (a cache line added to a shared index, say) raises instead of
|
|
24
|
+
silently corrupting resolution for every later lookup (#183).
|
|
25
|
+
|
|
26
|
+
The views are shallow — the inner ``list``/``dict`` values are still
|
|
27
|
+
mutable. Deep-freezing would mean copying every one of them on a hot path;
|
|
28
|
+
the realistic mistake this guards is adding or replacing a top-level key.
|
|
20
29
|
"""
|
|
21
30
|
|
|
22
31
|
# node id (FQN) -> Node
|
|
23
|
-
nodes:
|
|
32
|
+
nodes: Mapping[str, Node]
|
|
24
33
|
# name -> list of FQNs
|
|
25
|
-
global_symbols:
|
|
34
|
+
global_symbols: Mapping[str, list[str]]
|
|
26
35
|
# (file_path, name) -> list of FQNs (list handles conditional redefinitions)
|
|
27
|
-
file_global_symbols:
|
|
36
|
+
file_global_symbols: Mapping[tuple[str, str], list[str]]
|
|
28
37
|
# class_fqn -> {method_name -> method_fqn}
|
|
29
|
-
class_methods:
|
|
38
|
+
class_methods: Mapping[str, dict[str, str]]
|
|
30
39
|
# DI-alias (VARIABLE) indices for raw_dep: resolution; kept separate
|
|
31
40
|
# from global_symbols so call resolution behavior does not change.
|
|
32
|
-
variable_symbols:
|
|
33
|
-
file_variable_symbols:
|
|
41
|
+
variable_symbols: Mapping[str, list[str]]
|
|
42
|
+
file_variable_symbols: Mapping[tuple[str, str], list[str]]
|
|
34
43
|
# normalized file_path -> {local_alias: target_fqn} (from FILE node import_map)
|
|
35
|
-
file_imports:
|
|
44
|
+
file_imports: Mapping[str, dict[str, str]]
|
|
36
45
|
# suffix_fqn -> [full_node_ids] (handles src/ layout prefix mismatch)
|
|
37
|
-
suffix_map:
|
|
46
|
+
suffix_map: Mapping[str, list[str]]
|
|
38
47
|
# top-level root segments of all internal nodes (for classify)
|
|
39
|
-
internal_roots:
|
|
48
|
+
internal_roots: frozenset[str]
|
|
40
49
|
# root segments of absolute imports (anything else is UNKNOWN)
|
|
41
|
-
external_roots:
|
|
50
|
+
external_roots: frozenset[str]
|
|
42
51
|
|
|
43
52
|
def map_to_node_fqn(self, imported_fqn: str) -> str | None:
|
|
44
53
|
"""Resolve an imported FQN to an actual node in the graph.
|
|
@@ -65,6 +74,14 @@ class SymbolIndex:
|
|
|
65
74
|
return candidate
|
|
66
75
|
return None
|
|
67
76
|
|
|
77
|
+
def has_node(self, fqn: str) -> bool:
|
|
78
|
+
"""True if the graph holds a node with this exact FQN.
|
|
79
|
+
|
|
80
|
+
Saves callers reaching into ``nodes`` for a membership test, which is
|
|
81
|
+
the one place the index's internals leaked (#183).
|
|
82
|
+
"""
|
|
83
|
+
return fqn in self.nodes
|
|
84
|
+
|
|
68
85
|
def classify_fqn(self, fqn: str) -> NodeNamespace:
|
|
69
86
|
"""Classify an FQN as STDLIB, INTERNAL, EXTERNAL, or UNKNOWN.
|
|
70
87
|
|
|
@@ -145,17 +162,19 @@ class IndexBuilder:
|
|
|
145
162
|
# "src.cgis.pipeline.X" → suffix "cgis.pipeline.X" also points to the node
|
|
146
163
|
self._add_node_to_suffix_map(node.id, suffix_map, internal_roots)
|
|
147
164
|
|
|
165
|
+
# Read-only views, not copies: MappingProxyType wraps in O(1), so this
|
|
166
|
+
# buys write protection without touching ingest cost (#183).
|
|
148
167
|
return SymbolIndex(
|
|
149
|
-
nodes=nodes_by_id,
|
|
150
|
-
global_symbols=global_symbols,
|
|
151
|
-
file_global_symbols=file_global_symbols,
|
|
152
|
-
class_methods=class_methods,
|
|
153
|
-
variable_symbols=variable_symbols,
|
|
154
|
-
file_variable_symbols=file_variable_symbols,
|
|
155
|
-
file_imports=file_imports,
|
|
156
|
-
suffix_map=suffix_map,
|
|
157
|
-
internal_roots=internal_roots,
|
|
158
|
-
external_roots=self._build_external_roots(file_imports),
|
|
168
|
+
nodes=MappingProxyType(nodes_by_id),
|
|
169
|
+
global_symbols=MappingProxyType(global_symbols),
|
|
170
|
+
file_global_symbols=MappingProxyType(file_global_symbols),
|
|
171
|
+
class_methods=MappingProxyType(class_methods),
|
|
172
|
+
variable_symbols=MappingProxyType(variable_symbols),
|
|
173
|
+
file_variable_symbols=MappingProxyType(file_variable_symbols),
|
|
174
|
+
file_imports=MappingProxyType(file_imports),
|
|
175
|
+
suffix_map=MappingProxyType(suffix_map),
|
|
176
|
+
internal_roots=frozenset(internal_roots),
|
|
177
|
+
external_roots=frozenset(self._build_external_roots(file_imports)),
|
|
159
178
|
)
|
|
160
179
|
|
|
161
180
|
def _add_node_to_suffix_map(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-brain
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: Semantic code graph for AI agents — deterministic FQN resolution, impact analysis and architectural drift gates, exposed over MCP.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zaebee/codegraph-brain
|
|
6
6
|
Project-URL: Repository, https://github.com/zaebee/codegraph-brain
|
|
@@ -5,7 +5,7 @@ cgis/pipeline.py,sha256=AJfBhlH5ZD4wkDX6QMScuDPbl_Wwr_ZFplNzfoorsqg,10529
|
|
|
5
5
|
cgis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
cgis/api/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
cgis/api/__init__.py,sha256=wvFZK6oZspFGmonBz2rDg1aXxK7DHuE87or_w_cwY28,91
|
|
8
|
-
cgis/api/mcp_server.py,sha256=
|
|
8
|
+
cgis/api/mcp_server.py,sha256=ljg1kzH9WL4VobNiXnCtOFY_K8g5NESKYcXQN7P-NaA,26206
|
|
9
9
|
cgis/core/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
cgis/core/models.py,sha256=VVnldYVFBDLa3cqMCH2GZAUHt3jelIV--lgsGFK103w,3694
|
|
11
11
|
cgis/extractors/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -24,12 +24,12 @@ cgis/guardian/chunker.py,sha256=zm6EF7S8lKNNfeMu1W6z7a6FiBU2qBqRz2UcelnuYac,5705
|
|
|
24
24
|
cgis/guardian/collector.py,sha256=E4g9eW_qOYSw5rr25c_wWFt4xa9luJzAW84_DCo97HM,13505
|
|
25
25
|
cgis/guardian/core.py,sha256=nY8eTcaknfxLUa6u9YwLOdyM4ztb19-A49BfW_eh7uY,4981
|
|
26
26
|
cgis/guardian/diff_index.py,sha256=513WDsnvmcYecqVgoPeeK0ZBLziZIYb0ibRXYEYusv0,6212
|
|
27
|
-
cgis/guardian/findings.py,sha256=
|
|
27
|
+
cgis/guardian/findings.py,sha256=ZhIaQjdconvliyWe7dJs3kJL3N8JYnZRQFAepRuvQ6g,3011
|
|
28
28
|
cgis/guardian/github_poster.py,sha256=Rh1FKUC7ZC6IYWzKd-d7ewHq1L21Y1uUImTsn9J96x0,5658
|
|
29
29
|
cgis/guardian/metrics.py,sha256=ITF2QrHLvNLYlLYYS1qU3baGi7oYETzHXMo7pKfYc70,3641
|
|
30
30
|
cgis/guardian/prompts.py,sha256=xCjlTPx3U8_IX14n31mmYti68BcJkeQTcmU0e2nnv04,11012
|
|
31
31
|
cgis/guardian/recording.py,sha256=LXopddHdnv-042-P5nQrPE4JIx6jQVmGjjWzftgdr40,3375
|
|
32
|
-
cgis/guardian/render.py,sha256=
|
|
32
|
+
cgis/guardian/render.py,sha256=a31wUOWlogY4sY_5EYtGMOZW2m1SPKNfkxhM2yShqkQ,4729
|
|
33
33
|
cgis/guardian/runner.py,sha256=bhgtjp1-qlyc232c2LqycJEODDU68sQ2GwEYSzdCdQs,12148
|
|
34
34
|
cgis/guardian/skeptic.py,sha256=ey_6pzoC297mHXGIqvZlB9fSdTvTwgLI1h2yVJ7VeYY,8848
|
|
35
35
|
cgis/guardian/providers/__init__.py,sha256=wFOW1w9NtI-LBDB9w5ICNPFcuT4Hi5soxzDwpdmgxSc,49
|
|
@@ -65,14 +65,14 @@ cgis/query/render/mermaid.py,sha256=60n26BB7hBhZSTfZq62yq38aeXi3p0gOI0ON2-5N6vM,
|
|
|
65
65
|
cgis/query/render/metrics.py,sha256=tTHIJ5mWh--JchNvGCU7n7_S0pEXBCELzvKiseEiEvo,13929
|
|
66
66
|
cgis/resolver/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
67
|
cgis/resolver/__init__.py,sha256=AYEjjKXi__LvceOV9mEjXXzQS6O03D4RQjuBjpJtGhA,75
|
|
68
|
-
cgis/resolver/engine.py,sha256=
|
|
69
|
-
cgis/resolver/indices.py,sha256=
|
|
68
|
+
cgis/resolver/engine.py,sha256=3NKkvZNxD25vKOBB6NEKA8DobEanv1iMiuWEyrfWbLA,6425
|
|
69
|
+
cgis/resolver/indices.py,sha256=eeVaI-QpqU73gR-UrhlpqARx7j_bQU1LoI-Y7YUy4Zo,9116
|
|
70
70
|
cgis/resolver/symbols.py,sha256=6PNDIgoJ2Mj2xZ4XHpEXwjHe76fi9zvOwF7tAON1GBw,8513
|
|
71
71
|
cgis/resolver/uplift.py,sha256=Hkye1q-kI-gKO6GIBYv3gUZj1AC0-_mkpoIpmwVsG4g,9591
|
|
72
72
|
cgis/storage/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
73
|
cgis/storage/sqlite_store.py,sha256=Dkxkl0WDyTakZFfxWuVpYLNHA_H7CMvRkSUvev3MHew,24778
|
|
74
|
-
codegraph_brain-0.7.
|
|
75
|
-
codegraph_brain-0.7.
|
|
76
|
-
codegraph_brain-0.7.
|
|
77
|
-
codegraph_brain-0.7.
|
|
78
|
-
codegraph_brain-0.7.
|
|
74
|
+
codegraph_brain-0.7.5.dist-info/METADATA,sha256=SP_i4TO1ux803f9AWWre4bgsj015tiYmCI6YXDIsksA,13617
|
|
75
|
+
codegraph_brain-0.7.5.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
76
|
+
codegraph_brain-0.7.5.dist-info/entry_points.txt,sha256=G_ekY569jca0ubm-I3gA9Id9xRj8fc42CDgLgTC-5L4,77
|
|
77
|
+
codegraph_brain-0.7.5.dist-info/licenses/LICENSE,sha256=1X36nOG5VocDaEGzAZqIv7uawy_7sWlFoiwECwx5aBI,1065
|
|
78
|
+
codegraph_brain-0.7.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|