codecortex 0.2.2__tar.gz → 0.3.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.

Potentially problematic release.


This version of codecortex might be problematic. Click here for more details.

Files changed (60) hide show
  1. {codecortex-0.2.2/src/codecortex.egg-info → codecortex-0.3.0}/PKG-INFO +22 -9
  2. {codecortex-0.2.2 → codecortex-0.3.0}/README.md +21 -8
  3. {codecortex-0.2.2 → codecortex-0.3.0/src/codecortex.egg-info}/PKG-INFO +22 -9
  4. {codecortex-0.2.2 → codecortex-0.3.0}/src/codecortex.egg-info/SOURCES.txt +3 -0
  5. codecortex-0.3.0/src/codeintel/__init__.py +1 -0
  6. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/__main__.py +6 -3
  7. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/cache.py +1 -1
  8. codecortex-0.3.0/src/codeintel/config.py +97 -0
  9. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/gateway.py +9 -6
  10. codecortex-0.3.0/src/codeintel/http_server.py +175 -0
  11. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/indexer.py +12 -1
  12. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/onboarding.py +1 -0
  13. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/provider.py +19 -0
  14. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/providers/graph.py +17 -7
  15. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/providers/lsp.py +3 -2
  16. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/providers/semantic.py +23 -10
  17. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/reindexer.py +26 -3
  18. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/server.py +11 -5
  19. codecortex-0.3.0/tests/test_config.py +63 -0
  20. codecortex-0.3.0/tests/test_hardening.py +182 -0
  21. codecortex-0.3.0/tests/test_http_auth.py +121 -0
  22. codecortex-0.2.2/src/codeintel/__init__.py +0 -1
  23. codecortex-0.2.2/src/codeintel/config.py +0 -42
  24. codecortex-0.2.2/src/codeintel/http_server.py +0 -97
  25. {codecortex-0.2.2 → codecortex-0.3.0}/LICENSE +0 -0
  26. {codecortex-0.2.2 → codecortex-0.3.0}/pyproject.toml +0 -0
  27. {codecortex-0.2.2 → codecortex-0.3.0}/setup.cfg +0 -0
  28. {codecortex-0.2.2 → codecortex-0.3.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
  29. {codecortex-0.2.2 → codecortex-0.3.0}/src/codecortex.egg-info/entry_points.txt +0 -0
  30. {codecortex-0.2.2 → codecortex-0.3.0}/src/codecortex.egg-info/requires.txt +0 -0
  31. {codecortex-0.2.2 → codecortex-0.3.0}/src/codecortex.egg-info/top_level.txt +0 -0
  32. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/doctor.py +0 -0
  33. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/injector.py +0 -0
  34. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/installer.py +0 -0
  35. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/mapper.py +0 -0
  36. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/policy.py +0 -0
  37. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/providers/__init__.py +0 -0
  38. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/providers/none.py +0 -0
  39. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/reset.py +0 -0
  40. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/searcher.py +0 -0
  41. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/semantic_db.py +0 -0
  42. {codecortex-0.2.2 → codecortex-0.3.0}/src/codeintel/term.py +0 -0
  43. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_cache.py +0 -0
  44. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_doctor.py +0 -0
  45. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_e2e.py +0 -0
  46. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_gateway.py +0 -0
  47. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_graph_provider.py +0 -0
  48. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_graph_real.py +0 -0
  49. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_graph_stdin.py +0 -0
  50. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_http_server.py +0 -0
  51. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_integration.py +0 -0
  52. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_lsp_provider.py +0 -0
  53. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_lsp_real.py +0 -0
  54. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_mapper.py +0 -0
  55. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_never_raise.py +0 -0
  56. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_onboarding.py +0 -0
  57. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_reindexer.py +0 -0
  58. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_reset.py +0 -0
  59. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_semantic_provider.py +0 -0
  60. {codecortex-0.2.2 → codecortex-0.3.0}/tests/test_term.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -183,7 +183,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
183
183
  | `codeintel setup [project_root] [--index] [--warm] [--install-uv]` | Check backends + optionally index this repo; ends with a health report |
184
184
  | `codeintel index [project_root]` | Index a project for semantic search |
185
185
  | `codeintel serve` | Start the MCP server (stdio transport) |
186
- | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote]` | Start the HTTP transport (loopback-only unless `--allow-remote`) |
186
+ | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
187
187
  | `codeintel query --op OP --target TARGET [--engine auto]` | Run a single query and print the result |
188
188
  | `codeintel status [project_root]` | Show engine availability and index age |
189
189
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
@@ -197,17 +197,28 @@ Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--n
197
197
  Create `.codeintel.toml` at your project root to override defaults:
198
198
 
199
199
  ```toml
200
- backend = "auto" # auto | graph | lsp | semantic
201
- semantic = "on" # on | off
202
- reindex = "on-demand" # on-demand | never
203
- cosine_floor = 0.25 # minimum similarity score for semantic hits
204
- max_chunks = 500 # max chunks to embed per file
205
- model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
200
+ backend = "auto" # auto | graph | lsp | semantic
201
+ semantic = "on" # on | off
202
+ reindex = "on-demand" # on-demand | never
203
+ cosine_floor = 0.25 # minimum similarity score for semantic hits (0–1)
204
+ max_chunks = 500 # max chunks to embed per file
205
+ max_total_chunks = 100000 # safety ceiling on chunks embedded in one index pass
206
+ model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
206
207
  ```
207
208
 
209
+ Config is **validated on load** — an out-of-range number, a misspelled enum, or a wrong type falls back to that key's default (with a logged warning) instead of breaking every query.
210
+
211
+ **Environment variables:**
212
+
213
+ | Variable | Effect |
214
+ |---|---|
215
+ | `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
216
+ | `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
217
+ | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
218
+
208
219
  ## Privacy & dependencies
209
220
 
210
- **codeintel is local-first** — one local process, no cloud service, no API keys, no telemetry, and no per-query network. Its own code makes zero outbound HTTP calls, and the HTTP transport binds to `127.0.0.1` only.
221
+ **codeintel is local-first** — one local process, no cloud service, no API keys, no telemetry, and no per-query network. Its own code makes zero outbound HTTP calls, and the HTTP transport binds to `127.0.0.1` only by default — binding a non-loopback host requires `--allow-remote`, and `--token` (or `CODEINTEL_HTTP_TOKEN`) then gates every request behind a bearer token. The server bounds concurrent connections, but for exposure to a hostile network you should still front it with a reverse proxy (TLS, rate-limiting) — the built-in `http.server` is not hardened for the open internet.
211
222
 
212
223
  **Bundled (installed with the package, run locally):** `mcp` (the tool interface) · `sqlite-vec` (the semantic index, a local DB file) · `fastembed` (the local embedding model).
213
224
 
@@ -240,6 +251,8 @@ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and
240
251
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
241
252
  ```
242
253
 
254
+ For a shared or remote deployment, start it with `--allow-remote --token "$CODEINTEL_HTTP_TOKEN"` and send `Authorization: Bearer <token>` on each request — a missing or wrong token gets a clean `401`. Requests are handled concurrently, so one slow query never blocks another.
255
+
243
256
  ```python
244
257
  import urllib.request, json
245
258
 
@@ -150,7 +150,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
150
150
  | `codeintel setup [project_root] [--index] [--warm] [--install-uv]` | Check backends + optionally index this repo; ends with a health report |
151
151
  | `codeintel index [project_root]` | Index a project for semantic search |
152
152
  | `codeintel serve` | Start the MCP server (stdio transport) |
153
- | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote]` | Start the HTTP transport (loopback-only unless `--allow-remote`) |
153
+ | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
154
154
  | `codeintel query --op OP --target TARGET [--engine auto]` | Run a single query and print the result |
155
155
  | `codeintel status [project_root]` | Show engine availability and index age |
156
156
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
@@ -164,17 +164,28 @@ Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--n
164
164
  Create `.codeintel.toml` at your project root to override defaults:
165
165
 
166
166
  ```toml
167
- backend = "auto" # auto | graph | lsp | semantic
168
- semantic = "on" # on | off
169
- reindex = "on-demand" # on-demand | never
170
- cosine_floor = 0.25 # minimum similarity score for semantic hits
171
- max_chunks = 500 # max chunks to embed per file
172
- model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
167
+ backend = "auto" # auto | graph | lsp | semantic
168
+ semantic = "on" # on | off
169
+ reindex = "on-demand" # on-demand | never
170
+ cosine_floor = 0.25 # minimum similarity score for semantic hits (0–1)
171
+ max_chunks = 500 # max chunks to embed per file
172
+ max_total_chunks = 100000 # safety ceiling on chunks embedded in one index pass
173
+ model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
173
174
  ```
174
175
 
176
+ Config is **validated on load** — an out-of-range number, a misspelled enum, or a wrong type falls back to that key's default (with a logged warning) instead of breaking every query.
177
+
178
+ **Environment variables:**
179
+
180
+ | Variable | Effect |
181
+ |---|---|
182
+ | `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
183
+ | `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
184
+ | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
185
+
175
186
  ## Privacy & dependencies
176
187
 
177
- **codeintel is local-first** — one local process, no cloud service, no API keys, no telemetry, and no per-query network. Its own code makes zero outbound HTTP calls, and the HTTP transport binds to `127.0.0.1` only.
188
+ **codeintel is local-first** — one local process, no cloud service, no API keys, no telemetry, and no per-query network. Its own code makes zero outbound HTTP calls, and the HTTP transport binds to `127.0.0.1` only by default — binding a non-loopback host requires `--allow-remote`, and `--token` (or `CODEINTEL_HTTP_TOKEN`) then gates every request behind a bearer token. The server bounds concurrent connections, but for exposure to a hostile network you should still front it with a reverse proxy (TLS, rate-limiting) — the built-in `http.server` is not hardened for the open internet.
178
189
 
179
190
  **Bundled (installed with the package, run locally):** `mcp` (the tool interface) · `sqlite-vec` (the semantic index, a local DB file) · `fastembed` (the local embedding model).
180
191
 
@@ -207,6 +218,8 @@ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and
207
218
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
208
219
  ```
209
220
 
221
+ For a shared or remote deployment, start it with `--allow-remote --token "$CODEINTEL_HTTP_TOKEN"` and send `Authorization: Bearer <token>` on each request — a missing or wrong token gets a clean `401`. Requests are handled concurrently, so one slow query never blocks another.
222
+
210
223
  ```python
211
224
  import urllib.request, json
212
225
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -183,7 +183,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
183
183
  | `codeintel setup [project_root] [--index] [--warm] [--install-uv]` | Check backends + optionally index this repo; ends with a health report |
184
184
  | `codeintel index [project_root]` | Index a project for semantic search |
185
185
  | `codeintel serve` | Start the MCP server (stdio transport) |
186
- | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote]` | Start the HTTP transport (loopback-only unless `--allow-remote`) |
186
+ | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
187
187
  | `codeintel query --op OP --target TARGET [--engine auto]` | Run a single query and print the result |
188
188
  | `codeintel status [project_root]` | Show engine availability and index age |
189
189
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
@@ -197,17 +197,28 @@ Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--n
197
197
  Create `.codeintel.toml` at your project root to override defaults:
198
198
 
199
199
  ```toml
200
- backend = "auto" # auto | graph | lsp | semantic
201
- semantic = "on" # on | off
202
- reindex = "on-demand" # on-demand | never
203
- cosine_floor = 0.25 # minimum similarity score for semantic hits
204
- max_chunks = 500 # max chunks to embed per file
205
- model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
200
+ backend = "auto" # auto | graph | lsp | semantic
201
+ semantic = "on" # on | off
202
+ reindex = "on-demand" # on-demand | never
203
+ cosine_floor = 0.25 # minimum similarity score for semantic hits (0–1)
204
+ max_chunks = 500 # max chunks to embed per file
205
+ max_total_chunks = 100000 # safety ceiling on chunks embedded in one index pass
206
+ model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
206
207
  ```
207
208
 
209
+ Config is **validated on load** — an out-of-range number, a misspelled enum, or a wrong type falls back to that key's default (with a logged warning) instead of breaking every query.
210
+
211
+ **Environment variables:**
212
+
213
+ | Variable | Effect |
214
+ |---|---|
215
+ | `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
216
+ | `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
217
+ | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
218
+
208
219
  ## Privacy & dependencies
209
220
 
210
- **codeintel is local-first** — one local process, no cloud service, no API keys, no telemetry, and no per-query network. Its own code makes zero outbound HTTP calls, and the HTTP transport binds to `127.0.0.1` only.
221
+ **codeintel is local-first** — one local process, no cloud service, no API keys, no telemetry, and no per-query network. Its own code makes zero outbound HTTP calls, and the HTTP transport binds to `127.0.0.1` only by default — binding a non-loopback host requires `--allow-remote`, and `--token` (or `CODEINTEL_HTTP_TOKEN`) then gates every request behind a bearer token. The server bounds concurrent connections, but for exposure to a hostile network you should still front it with a reverse proxy (TLS, rate-limiting) — the built-in `http.server` is not hardened for the open internet.
211
222
 
212
223
  **Bundled (installed with the package, run locally):** `mcp` (the tool interface) · `sqlite-vec` (the semantic index, a local DB file) · `fastembed` (the local embedding model).
213
224
 
@@ -240,6 +251,8 @@ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and
240
251
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
241
252
  ```
242
253
 
254
+ For a shared or remote deployment, start it with `--allow-remote --token "$CODEINTEL_HTTP_TOKEN"` and send `Authorization: Bearer <token>` on each request — a missing or wrong token gets a clean `401`. Requests are handled concurrently, so one slow query never blocks another.
255
+
243
256
  ```python
244
257
  import urllib.request, json
245
258
 
@@ -33,12 +33,15 @@ src/codeintel/providers/lsp.py
33
33
  src/codeintel/providers/none.py
34
34
  src/codeintel/providers/semantic.py
35
35
  tests/test_cache.py
36
+ tests/test_config.py
36
37
  tests/test_doctor.py
37
38
  tests/test_e2e.py
38
39
  tests/test_gateway.py
39
40
  tests/test_graph_provider.py
40
41
  tests/test_graph_real.py
41
42
  tests/test_graph_stdin.py
43
+ tests/test_hardening.py
44
+ tests/test_http_auth.py
42
45
  tests/test_http_server.py
43
46
  tests/test_integration.py
44
47
  tests/test_lsp_provider.py
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -50,7 +50,8 @@ def main() -> None:
50
50
  http_parser = subparsers.add_parser("serve-http", help="Start the HTTP transport server")
51
51
  http_parser.add_argument("--port", type=int, default=8766, help="Port to listen on (default: 8766)")
52
52
  http_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
53
- http_parser.add_argument("--allow-remote", action="store_true", help="Permit binding a non-loopback host (exposes an UNAUTHENTICATED endpoint)")
53
+ http_parser.add_argument("--allow-remote", action="store_true", help="Permit binding a non-loopback host (use with --token, or the endpoint is UNAUTHENTICATED)")
54
+ http_parser.add_argument("--token", default=None, help="Require this bearer token on every request (or set CODEINTEL_HTTP_TOKEN). Strongly recommended with --allow-remote.")
54
55
 
55
56
  # install subcommand
56
57
  install_parser = subparsers.add_parser("install", help="Register codeintel with AI agents")
@@ -120,6 +121,7 @@ def main() -> None:
120
121
  window=int(cfg.get("window", 20)),
121
122
  stride=int(cfg.get("stride", 10)),
122
123
  max_chunks=int(cfg.get("max_chunks", 500)),
124
+ max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
123
125
  ).index(project_root)
124
126
  if count > 0:
125
127
  print(f"Indexed {count} chunks")
@@ -225,7 +227,7 @@ def main() -> None:
225
227
  from codeintel import server
226
228
 
227
229
  project_root = args.project_root or os.getcwd()
228
- status = server.code_status_handler({})
230
+ status = server.code_status_handler({"project_root": project_root})
229
231
 
230
232
  print("Engine status:")
231
233
  for engine in ["graph", "lsp", "semantic"]:
@@ -318,7 +320,8 @@ def main() -> None:
318
320
  elif args.command == "serve-http":
319
321
  try:
320
322
  from codeintel.http_server import run
321
- run(host=args.host, port=args.port, allow_remote=args.allow_remote)
323
+ token = args.token or os.environ.get("CODEINTEL_HTTP_TOKEN")
324
+ run(host=args.host, port=args.port, allow_remote=args.allow_remote, token=token)
322
325
  except KeyboardInterrupt:
323
326
  pass
324
327
  except Exception as exc:
@@ -13,7 +13,7 @@ def _compute_hash(target: str, project_root: str) -> str:
13
13
  try:
14
14
  root = os.path.realpath(project_root) if project_root else ""
15
15
  path = os.path.realpath(target)
16
- if root and path.startswith(root + os.sep) or path == root:
16
+ if (root and path.startswith(root + os.sep)) or path == root:
17
17
  if os.path.isfile(path):
18
18
  with open(path, "rb") as fh:
19
19
  return hashlib.sha256(fh.read()).hexdigest()
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import pathlib
5
+ import sys
6
+
7
+ if sys.version_info >= (3, 11):
8
+ import tomllib
9
+ else: # pragma: no cover
10
+ try:
11
+ import tomllib # type: ignore[no-redef]
12
+ except ImportError:
13
+ import tomli as tomllib # type: ignore[no-redef]
14
+
15
+ logger = logging.getLogger("codeintel")
16
+
17
+ _DEFAULTS: dict = {
18
+ "backend": "auto",
19
+ "semantic": "on",
20
+ "reindex": "on-demand",
21
+ "window": 20,
22
+ "stride": 10,
23
+ "max_chunks": 500, # per file
24
+ "max_total_chunks": 100000, # safety ceiling on chunks embedded in one index pass
25
+ "cosine_floor": 0.25,
26
+ "model": "BAAI/bge-small-en-v1.5",
27
+ }
28
+
29
+ # Values restricted to a fixed set — anything else falls back to the default.
30
+ _ENUMS: dict = {
31
+ "backend": {"auto", "graph", "lsp", "semantic"},
32
+ "semantic": {"on", "off"},
33
+ "reindex": {"on-demand", "never"},
34
+ }
35
+ _POSITIVE_INTS = ("window", "stride", "max_chunks", "max_total_chunks")
36
+
37
+
38
+ def _read_toml(path: pathlib.Path) -> dict:
39
+ try:
40
+ with path.open("rb") as fh:
41
+ return tomllib.load(fh)
42
+ except (FileNotFoundError, OSError, tomllib.TOMLDecodeError):
43
+ return {}
44
+
45
+
46
+ def _coerce(cfg: dict) -> dict:
47
+ """Coerce/clamp a merged config to safe values, warning on and dropping bad ones. Never raises:
48
+ a malformed ``.codeintel.toml`` (a string where a number belongs, an out-of-range floor, an
49
+ unknown enum) must degrade to the default for that key, not break every query that loads it."""
50
+ out = dict(_DEFAULTS)
51
+ for key, default in _DEFAULTS.items():
52
+ if key not in cfg:
53
+ continue
54
+ val = cfg[key]
55
+ try:
56
+ if key in _ENUMS:
57
+ s = str(val).strip().lower()
58
+ if s in _ENUMS[key]:
59
+ out[key] = s
60
+ else:
61
+ logger.warning("config: %s=%r invalid (expected %s) — using %r",
62
+ key, val, sorted(_ENUMS[key]), default)
63
+ elif key == "model":
64
+ out[key] = str(val).strip() or default
65
+ elif key == "cosine_floor":
66
+ f = float(val)
67
+ if f != f: # NaN (TOML allows `nan`) — reject so it can't silently disable the floor
68
+ raise ValueError("nan")
69
+ out[key] = min(1.0, max(0.0, f))
70
+ elif key in _POSITIVE_INTS:
71
+ n = int(val)
72
+ if n > 0:
73
+ out[key] = n
74
+ else:
75
+ logger.warning("config: %s=%r must be > 0 — using %r", key, val, default)
76
+ except (TypeError, ValueError, OverflowError):
77
+ # int(float('inf')) raises OverflowError (TOML allows `inf`); float("x") → ValueError;
78
+ # int([]) → TypeError. Any non-usable value logs and keeps the default, so the
79
+ # docstring's never-raise promise holds even for the CLI paths that don't wrap this.
80
+ logger.warning("config: %s=%r not usable — using default %r", key, val, default)
81
+ # Keep any extra keys the user set (forward-compat) without validating them.
82
+ for k, v in cfg.items():
83
+ if k not in _DEFAULTS:
84
+ out[k] = v
85
+ return out
86
+
87
+
88
+ def load_config(project_root: str | None = None) -> dict:
89
+ """Return the merged, validated config: defaults < global < project. Values that fail
90
+ validation fall back to their default (logged), so a bad config file never breaks a query."""
91
+ root = pathlib.Path(project_root) if project_root is not None else pathlib.Path.cwd()
92
+
93
+ global_cfg = _read_toml(pathlib.Path.home() / ".codeintel" / "config.toml")
94
+ project_cfg = _read_toml(root / ".codeintel.toml")
95
+
96
+ merged = {**_DEFAULTS, **global_cfg, **project_cfg}
97
+ return _coerce(merged)
@@ -5,7 +5,7 @@ from typing import Any
5
5
 
6
6
  from codeintel.cache import ContentHashCache
7
7
  from codeintel.policy import TieringPolicy
8
- from codeintel.provider import Result, safe_null_result
8
+ from codeintel.provider import Result, log_swallowed, safe_null_result
9
9
  from codeintel.providers.none import NoneProvider
10
10
  from codeintel.reindexer import Reindexer
11
11
 
@@ -128,7 +128,8 @@ class Gateway:
128
128
  if r is not None:
129
129
  return r
130
130
  return safe_null_result(op_str, target_str, engine=engine_str, reason="no-result")
131
- except Exception:
131
+ except Exception as exc:
132
+ log_swallowed(f"Gateway._dispatch_single[{engine_str}.{op_str}]", exc)
132
133
  return safe_null_result(op_str, target_str, engine=engine_str, reason="provider-error")
133
134
 
134
135
  def query(
@@ -206,14 +207,15 @@ class Gateway:
206
207
  provider = self._provider_for(engine_str)
207
208
  result = self._dispatch_single(provider, op_str, target_str, budget, project_root, engine_str)
208
209
 
209
- # overview auto-fallback (F4 Story 2): when auto-routed to graph but graph is
210
- # unavailable, try lsp a file/symbol overview is something lsp can also serve.
210
+ # overview auto-fallback (F4 Story 2): when auto-routed to graph but graph can't serve
211
+ # it — the backend is unavailable, OR this repo simply isn't in the graph try lsp,
212
+ # which can produce a file/symbol overview without the graph index.
211
213
  if (
212
214
  was_auto
213
215
  and op_str == "overview"
214
216
  and engine_str == "graph"
215
217
  and result.get("result") is None
216
- and result.get("reason") == "engine-unavailable"
218
+ and result.get("reason") in ("engine-unavailable", "project-not-indexed")
217
219
  ):
218
220
  lsp_result = self._dispatch_single(
219
221
  self.lsp, op_str, target_str, budget, project_root, "lsp"
@@ -224,5 +226,6 @@ class Gateway:
224
226
  self._cache.put(op_str, target_str, engine_str, root_str, result, freshness)
225
227
  return result
226
228
 
227
- except Exception:
229
+ except Exception as exc:
230
+ log_swallowed("Gateway.query", exc)
228
231
  return safe_null_result(op or "", target or "", reason="gateway-error")
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import hmac
4
+ import json
5
+ import sys
6
+ import threading
7
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
+ from urllib.parse import parse_qs, urlparse
9
+
10
+ from codeintel.provider import log_swallowed
11
+ from codeintel.server import code_doctor_handler, code_query_handler, code_status_handler
12
+
13
+ _MAX_BODY_BYTES = 1_048_576 # 1 MiB
14
+ _REQUEST_TIMEOUT_S = 60 # per-request socket read timeout — drops an idle/half-open client
15
+ _MAX_CONCURRENT_REQUESTS = 64 # cap live worker threads so a burst can't exhaust threads/FDs
16
+
17
+
18
+ class _Handler(BaseHTTPRequestHandler):
19
+ # Applied to the request socket by the base handler's setup(); a slow/half-open client
20
+ # connection is dropped instead of pinning a worker thread forever (basic slowloris guard).
21
+ timeout = _REQUEST_TIMEOUT_S
22
+
23
+ def log_message(self, format: str, *args: object) -> None:
24
+ pass # suppress default stderr noise
25
+
26
+ def _send_json(self, status: int, data: dict) -> None:
27
+ body = json.dumps(data).encode()
28
+ self.send_response(status)
29
+ self.send_header("Content-Type", "application/json")
30
+ self.send_header("Content-Length", str(len(body)))
31
+ self.end_headers()
32
+ self.wfile.write(body)
33
+
34
+ def _authorized(self) -> bool:
35
+ """When the server was started with a token, require ``Authorization: Bearer <token>``
36
+ (constant-time compare). No token configured → auth is disabled (the loopback default)."""
37
+ token = getattr(self.server, "auth_token", None)
38
+ if not token:
39
+ return True
40
+ header = self.headers.get("Authorization", "")
41
+ prefix = "Bearer "
42
+ if not header.startswith(prefix):
43
+ return False
44
+ presented = header[len(prefix):].strip()
45
+ # Compare as UTF-8 bytes: hmac.compare_digest raises TypeError on a non-ASCII str, so a
46
+ # malformed header (e.g. `Bearer résumé`) would otherwise crash the handler thread. Encoding
47
+ # is always safe and keeps the comparison constant-time.
48
+ return hmac.compare_digest(presented.encode("utf-8"), token.encode("utf-8"))
49
+
50
+ def do_POST(self) -> None:
51
+ if not self._authorized():
52
+ self._send_json(401, {"error": "unauthorized"})
53
+ return
54
+ if self.path not in ("/code/query", "/code/doctor"):
55
+ self._send_json(404, {"error": "not-found"})
56
+ return
57
+ try:
58
+ content_length = int(self.headers.get("Content-Length", 0))
59
+ except (TypeError, ValueError):
60
+ self._send_json(400, {"error": "bad-request"})
61
+ return
62
+ if content_length > _MAX_BODY_BYTES:
63
+ self.close_connection = True # do not read the oversized body
64
+ self._send_json(413, {"error": "payload-too-large", "max_bytes": _MAX_BODY_BYTES})
65
+ return
66
+ raw = self.rfile.read(content_length) if content_length > 0 else b""
67
+ try:
68
+ parsed = json.loads(raw)
69
+ except (json.JSONDecodeError, ValueError):
70
+ self._send_json(400, {"error": "bad-request"})
71
+ return
72
+ if not isinstance(parsed, dict):
73
+ self._send_json(400, {"error": "bad-request"})
74
+ return
75
+ if self.path == "/code/doctor":
76
+ result = code_doctor_handler(parsed)
77
+ else:
78
+ result = code_query_handler(parsed)
79
+ self._send_json(200, result)
80
+
81
+ def do_GET(self) -> None:
82
+ if not self._authorized():
83
+ self._send_json(401, {"error": "unauthorized"})
84
+ return
85
+ parsed = urlparse(self.path)
86
+ if parsed.path != "/code/status":
87
+ self._send_json(404, {"error": "not-found"})
88
+ return
89
+ # Optional ?project_root=... scopes the `indexed` flag to that repo.
90
+ project_root = (parse_qs(parsed.query).get("project_root") or [""])[0]
91
+ result = code_status_handler({"project_root": project_root})
92
+ self._send_json(200, result)
93
+
94
+
95
+ class CodeIntelHTTPServer(ThreadingHTTPServer):
96
+ # Threaded so one slow request (an LSP session warming, or a first-time index) can't block
97
+ # every other agent's query. The gateway is a shared singleton, but its mutable state is
98
+ # lock-guarded (query cache, reindexer, LSP sessions, graph project cache) and the semantic
99
+ # engine is thread-confined with WAL, so concurrent requests are safe.
100
+ daemon_threads = True
101
+ auth_token: str | None = None # set by run() when a token is provided
102
+
103
+ def __init__(self, *args, **kwargs) -> None:
104
+ super().__init__(*args, **kwargs)
105
+ # Stdlib ThreadingHTTPServer spawns one thread per connection with no ceiling, so many
106
+ # slow/half-open clients could exhaust threads/FDs. Bound live workers; past the cap we
107
+ # refuse fast with 503 rather than spawn an unbounded thread. (For a genuinely hostile
108
+ # network, still front this with a reverse proxy — http.server is not hardened for that.)
109
+ self._slots = threading.BoundedSemaphore(_MAX_CONCURRENT_REQUESTS)
110
+
111
+ def process_request(self, request, client_address) -> None:
112
+ if not self._slots.acquire(blocking=False):
113
+ try:
114
+ request.sendall(
115
+ b"HTTP/1.1 503 Service Unavailable\r\n"
116
+ b"Content-Type: application/json\r\nContent-Length: 22\r\n"
117
+ b'Connection: close\r\n\r\n{"error":"overloaded"}'
118
+ )
119
+ except Exception:
120
+ pass
121
+ self.shutdown_request(request)
122
+ return
123
+ super().process_request(request, client_address) # spawns the worker thread
124
+
125
+ def process_request_thread(self, request, client_address) -> None:
126
+ try:
127
+ super().process_request_thread(request, client_address)
128
+ finally:
129
+ self._slots.release()
130
+
131
+ def handle_error(self, request, client_address) -> None:
132
+ # A stalled/reset client raises here (socket timeout, broken pipe) — expected noise, not a
133
+ # server bug (handlers are never-raise). Keep it off stderr; surface with CODEINTEL_DEBUG=1.
134
+ log_swallowed("http.handle_error", sys.exc_info()[1] or Exception("unknown"))
135
+
136
+
137
+ _LOOPBACK_NAMES = {"localhost"}
138
+
139
+
140
+ def _is_loopback(host: str) -> bool:
141
+ # Treat as loopback ONLY: the literal name "localhost", or an IP literal in a loopback range
142
+ # (127.0.0.0/8, ::1). A string-prefix test like host.startswith("127.") is unsafe — it would
143
+ # accept an attacker-controlled HOSTNAME such as "127.0.0.1.evil.example", bypassing the guard.
144
+ import ipaddress
145
+
146
+ h = (host or "").strip().lower()
147
+ if h in _LOOPBACK_NAMES:
148
+ return True
149
+ try:
150
+ return ipaddress.ip_address(h).is_loopback
151
+ except ValueError:
152
+ return False # a non-IP hostname is never treated as loopback
153
+
154
+
155
+ def run(
156
+ host: str = "127.0.0.1",
157
+ port: int = 8766,
158
+ *,
159
+ allow_remote: bool = False,
160
+ token: str | None = None,
161
+ ) -> None:
162
+ if not _is_loopback(host) and not allow_remote:
163
+ print(f"refusing to bind non-loopback host {host!r} without --allow-remote — this would "
164
+ f"expose an UNAUTHENTICATED code-intel endpoint (your indexed repo) to the network",
165
+ file=sys.stderr)
166
+ raise SystemExit(2)
167
+ server = CodeIntelHTTPServer((host, port), _Handler)
168
+ server.auth_token = (token or "").strip() or None
169
+ if not _is_loopback(host) and not server.auth_token:
170
+ print(f"WARNING: serving codeintel on {host}:{port} with NO authentication — anyone who can "
171
+ f"reach this port can read your indexed repo (set --token to require a bearer token)",
172
+ file=sys.stderr)
173
+ auth_note = " (bearer-token auth required)" if server.auth_token else ""
174
+ print(f"Listening on http://{host}:{port}{auth_note}")
175
+ server.serve_forever()
@@ -37,12 +37,14 @@ class Indexer:
37
37
  window: int = 20,
38
38
  stride: int = 10,
39
39
  max_chunks: int = 500,
40
+ max_total_chunks: int = 100000,
40
41
  ) -> None:
41
42
  self.db = db
42
43
  self.model_name = model_name
43
44
  self.window = window
44
45
  self.stride = stride
45
- self.max_chunks = max_chunks
46
+ self.max_chunks = max_chunks # per file
47
+ self.max_total_chunks = max_total_chunks # safety ceiling per index pass (memory backstop)
46
48
  self._embedder = None
47
49
 
48
50
  def _get_embedder(self):
@@ -131,6 +133,13 @@ class Indexer:
131
133
  new_chunks: list[tuple[str, str, str, int, str]] = []
132
134
 
133
135
  for filepath in self._walk_files(root):
136
+ if len(new_chunks) >= self.max_total_chunks:
137
+ logger.warning(
138
+ "index: reached max_total_chunks=%d this pass — stopping "
139
+ "(raise it in .codeintel.toml to embed more of a very large repo)",
140
+ self.max_total_chunks,
141
+ )
142
+ break
134
143
  try:
135
144
  with open(filepath, encoding="utf-8", errors="replace") as f:
136
145
  lines = f.readlines()
@@ -145,6 +154,8 @@ class Indexer:
145
154
  chunk_count = 0
146
155
 
147
156
  for chunk_start in range(0, len(lines), self.stride):
157
+ if len(new_chunks) >= self.max_total_chunks:
158
+ break # global ceiling reached mid-file; the file-loop guard logs and stops
148
159
  if chunk_count >= self.max_chunks:
149
160
  logger.debug(
150
161
  "chunk cap hit for %s, truncating at %d",
@@ -72,6 +72,7 @@ def _bounded_index(project_root: str, *, timeout_s: float, out) -> dict:
72
72
  db, model_name=str(cfg.get("model") or "BAAI/bge-small-en-v1.5"),
73
73
  window=int(cfg.get("window", 20)), stride=int(cfg.get("stride", 10)),
74
74
  max_chunks=int(cfg.get("max_chunks", 500)),
75
+ max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
75
76
  ).index(project_root)
76
77
  finally:
77
78
  db.close()
@@ -1,8 +1,27 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
4
+ import os
5
+ import traceback
3
6
  from typing import Any, Optional, Protocol, runtime_checkable
4
7
  from typing_extensions import NotRequired, TypedDict
5
8
 
9
+ _logger = logging.getLogger("codeintel")
10
+ _DEBUG = os.environ.get("CODEINTEL_DEBUG", "").strip().lower() in ("1", "true", "on", "yes")
11
+
12
+
13
+ def log_swallowed(where: str, exc: BaseException) -> None:
14
+ """Record an exception the never-raise contract is about to swallow. Quiet by default so the
15
+ contract stays silent in normal use; set ``CODEINTEL_DEBUG=1`` to surface a full traceback when
16
+ diagnosing why a query came back as a safe-null. Never raises — logging failures are ignored."""
17
+ try:
18
+ if _DEBUG:
19
+ _logger.warning("codeintel swallowed error in %s: %s\n%s", where, exc, traceback.format_exc())
20
+ else:
21
+ _logger.debug("codeintel swallowed error in %s: %s", where, exc)
22
+ except Exception:
23
+ pass
24
+
6
25
 
7
26
  class Result(TypedDict):
8
27
  ok: bool