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 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
- Paths are normalised relative to the workspace root so the database is
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
- Category = Literal["logic", "contract", "tests", "types", "ontology"]
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
@@ -11,6 +11,7 @@ _CATEGORY_LABEL = {
11
11
  "tests": "Test Coverage",
12
12
  "types": "Type Safety",
13
13
  "ontology": "Ontology",
14
+ "security": "Security",
14
15
  }
15
16
 
16
17
 
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 target not in self._index.nodes and target not in virtual_nodes:
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
- Frozen by convention (field rebinding prevented); the contained dicts
19
- are never mutated after construction.
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: dict[str, Node]
32
+ nodes: Mapping[str, Node]
24
33
  # name -> list of FQNs
25
- global_symbols: dict[str, list[str]]
34
+ global_symbols: Mapping[str, list[str]]
26
35
  # (file_path, name) -> list of FQNs (list handles conditional redefinitions)
27
- file_global_symbols: dict[tuple[str, str], list[str]]
36
+ file_global_symbols: Mapping[tuple[str, str], list[str]]
28
37
  # class_fqn -> {method_name -> method_fqn}
29
- class_methods: dict[str, dict[str, str]]
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: dict[str, list[str]]
33
- file_variable_symbols: dict[tuple[str, str], list[str]]
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: dict[str, dict[str, str]]
44
+ file_imports: Mapping[str, dict[str, str]]
36
45
  # suffix_fqn -> [full_node_ids] (handles src/ layout prefix mismatch)
37
- suffix_map: dict[str, list[str]]
46
+ suffix_map: Mapping[str, list[str]]
38
47
  # top-level root segments of all internal nodes (for classify)
39
- internal_roots: set[str]
48
+ internal_roots: frozenset[str]
40
49
  # root segments of absolute imports (anything else is UNKNOWN)
41
- external_roots: set[str]
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
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=n2RgUwzqfAZ-cZXzYSvC5CI8rJu8JyfX3v_Ax3NGCYA,22693
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=igJg99ZH0J13wLZtDSiA2GBejsqYetCA3cqADohbZIc,2691
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=w-F5MHyEsIxMHLe6zP05VtSdpzfBsRD9bpLJyUwoVHg,4701
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=zNErmZRpOdAHMux3YWCWz0YbXhorm8BcD7DSKGYJLgQ,6424
69
- cgis/resolver/indices.py,sha256=hof9PyDrOdbFCyE1urDOH3j5uKFHPutvpgetsLvxq8s,7994
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.3.dist-info/METADATA,sha256=Qk6-V9Em57bTdZByMD18KzvWSZ25ecQZvA8NGG1kwS4,13617
75
- codegraph_brain-0.7.3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
76
- codegraph_brain-0.7.3.dist-info/entry_points.txt,sha256=G_ekY569jca0ubm-I3gA9Id9xRj8fc42CDgLgTC-5L4,77
77
- codegraph_brain-0.7.3.dist-info/licenses/LICENSE,sha256=1X36nOG5VocDaEGzAZqIv7uawy_7sWlFoiwECwx5aBI,1065
78
- codegraph_brain-0.7.3.dist-info/RECORD,,
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,,