codecortex 0.3.0__tar.gz → 0.4.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.
- {codecortex-0.3.0/src/codecortex.egg-info → codecortex-0.4.0}/PKG-INFO +23 -1
- {codecortex-0.3.0 → codecortex-0.4.0}/README.md +22 -0
- {codecortex-0.3.0 → codecortex-0.4.0/src/codecortex.egg-info}/PKG-INFO +23 -1
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codecortex.egg-info/SOURCES.txt +3 -0
- codecortex-0.4.0/src/codeintel/__init__.py +1 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/http_server.py +139 -20
- codecortex-0.4.0/src/codeintel/logconfig.py +54 -0
- codecortex-0.4.0/src/codeintel/metrics.py +87 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/server.py +2 -0
- codecortex-0.4.0/tests/test_enterprise.py +145 -0
- codecortex-0.3.0/src/codeintel/__init__.py +0 -1
- {codecortex-0.3.0 → codecortex-0.4.0}/LICENSE +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/pyproject.toml +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/setup.cfg +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/__main__.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/cache.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/config.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/doctor.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/gateway.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/indexer.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/injector.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/installer.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/mapper.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/policy.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/provider.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/reset.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/searcher.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/src/codeintel/term.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_cache.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_config.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_doctor.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_e2e.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_gateway.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_graph_provider.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_graph_real.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_hardening.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_http_auth.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_http_server.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_integration.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_lsp_real.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_mapper.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_never_raise.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_onboarding.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_reindexer.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_reset.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.3.0 → codecortex-0.4.0}/tests/test_term.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -213,6 +213,9 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
213
213
|
| Variable | Effect |
|
|
214
214
|
|---|---|
|
|
215
215
|
| `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
|
|
216
|
+
| `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
|
|
217
|
+
| `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
|
|
218
|
+
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
216
219
|
| `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
220
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
218
221
|
|
|
@@ -273,6 +276,25 @@ if result["result"] is not None:
|
|
|
273
276
|
|
|
274
277
|
The response is always JSON-safe. Check `result["result"] is not None` before use. Never catch an exception from the gateway — it never raises.
|
|
275
278
|
|
|
279
|
+
## Operations & deployment
|
|
280
|
+
|
|
281
|
+
Running codeintel as a shared service? It ships with what ops teams expect:
|
|
282
|
+
|
|
283
|
+
| Endpoint | Auth | Purpose |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `GET /healthz` | none | Liveness — always `200` (for load balancers / `livenessProbe`) |
|
|
286
|
+
| `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
|
|
287
|
+
| `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
|
|
288
|
+
|
|
289
|
+
Plus **bearer-token auth**, **structured JSON logs** (`CODEINTEL_LOG_FORMAT=json`) with optional per-request access logs, **graceful `SIGTERM`** shutdown, a bounded connection pool, and a non-root **Dockerfile** with a healthcheck.
|
|
290
|
+
|
|
291
|
+
**Full guide → [docs/deploy.md](docs/deploy.md)**: systemd, Docker / Compose, Kubernetes (liveness + readiness probes, token from a Secret), reverse-proxy TLS, a Prometheus scrape config, and a security checklist.
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
295
|
+
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
296
|
+
```
|
|
297
|
+
|
|
276
298
|
## Development
|
|
277
299
|
|
|
278
300
|
```bash
|
|
@@ -180,6 +180,9 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
180
180
|
| Variable | Effect |
|
|
181
181
|
|---|---|
|
|
182
182
|
| `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
|
|
183
|
+
| `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
|
|
184
|
+
| `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
|
|
185
|
+
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
183
186
|
| `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
187
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
185
188
|
|
|
@@ -240,6 +243,25 @@ if result["result"] is not None:
|
|
|
240
243
|
|
|
241
244
|
The response is always JSON-safe. Check `result["result"] is not None` before use. Never catch an exception from the gateway — it never raises.
|
|
242
245
|
|
|
246
|
+
## Operations & deployment
|
|
247
|
+
|
|
248
|
+
Running codeintel as a shared service? It ships with what ops teams expect:
|
|
249
|
+
|
|
250
|
+
| Endpoint | Auth | Purpose |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| `GET /healthz` | none | Liveness — always `200` (for load balancers / `livenessProbe`) |
|
|
253
|
+
| `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
|
|
254
|
+
| `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
|
|
255
|
+
|
|
256
|
+
Plus **bearer-token auth**, **structured JSON logs** (`CODEINTEL_LOG_FORMAT=json`) with optional per-request access logs, **graceful `SIGTERM`** shutdown, a bounded connection pool, and a non-root **Dockerfile** with a healthcheck.
|
|
257
|
+
|
|
258
|
+
**Full guide → [docs/deploy.md](docs/deploy.md)**: systemd, Docker / Compose, Kubernetes (liveness + readiness probes, token from a Secret), reverse-proxy TLS, a Prometheus scrape config, and a security checklist.
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
262
|
+
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
263
|
+
```
|
|
264
|
+
|
|
243
265
|
## Development
|
|
244
266
|
|
|
245
267
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -213,6 +213,9 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
213
213
|
| Variable | Effect |
|
|
214
214
|
|---|---|
|
|
215
215
|
| `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
|
|
216
|
+
| `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
|
|
217
|
+
| `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
|
|
218
|
+
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
216
219
|
| `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
220
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
218
221
|
|
|
@@ -273,6 +276,25 @@ if result["result"] is not None:
|
|
|
273
276
|
|
|
274
277
|
The response is always JSON-safe. Check `result["result"] is not None` before use. Never catch an exception from the gateway — it never raises.
|
|
275
278
|
|
|
279
|
+
## Operations & deployment
|
|
280
|
+
|
|
281
|
+
Running codeintel as a shared service? It ships with what ops teams expect:
|
|
282
|
+
|
|
283
|
+
| Endpoint | Auth | Purpose |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `GET /healthz` | none | Liveness — always `200` (for load balancers / `livenessProbe`) |
|
|
286
|
+
| `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
|
|
287
|
+
| `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
|
|
288
|
+
|
|
289
|
+
Plus **bearer-token auth**, **structured JSON logs** (`CODEINTEL_LOG_FORMAT=json`) with optional per-request access logs, **graceful `SIGTERM`** shutdown, a bounded connection pool, and a non-root **Dockerfile** with a healthcheck.
|
|
290
|
+
|
|
291
|
+
**Full guide → [docs/deploy.md](docs/deploy.md)**: systemd, Docker / Compose, Kubernetes (liveness + readiness probes, token from a Secret), reverse-proxy TLS, a Prometheus scrape config, and a security checklist.
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
295
|
+
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
296
|
+
```
|
|
297
|
+
|
|
276
298
|
## Development
|
|
277
299
|
|
|
278
300
|
```bash
|
|
@@ -17,7 +17,9 @@ src/codeintel/http_server.py
|
|
|
17
17
|
src/codeintel/indexer.py
|
|
18
18
|
src/codeintel/injector.py
|
|
19
19
|
src/codeintel/installer.py
|
|
20
|
+
src/codeintel/logconfig.py
|
|
20
21
|
src/codeintel/mapper.py
|
|
22
|
+
src/codeintel/metrics.py
|
|
21
23
|
src/codeintel/onboarding.py
|
|
22
24
|
src/codeintel/policy.py
|
|
23
25
|
src/codeintel/provider.py
|
|
@@ -36,6 +38,7 @@ tests/test_cache.py
|
|
|
36
38
|
tests/test_config.py
|
|
37
39
|
tests/test_doctor.py
|
|
38
40
|
tests/test_e2e.py
|
|
41
|
+
tests/test_enterprise.py
|
|
39
42
|
tests/test_gateway.py
|
|
40
43
|
tests/test_graph_provider.py
|
|
41
44
|
tests/test_graph_real.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -2,17 +2,29 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hmac
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
5
8
|
import sys
|
|
6
9
|
import threading
|
|
10
|
+
import time
|
|
7
11
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
12
|
from urllib.parse import parse_qs, urlparse
|
|
9
13
|
|
|
14
|
+
from codeintel import __version__
|
|
15
|
+
from codeintel.logconfig import configure_logging
|
|
16
|
+
from codeintel.metrics import Metrics
|
|
10
17
|
from codeintel.provider import log_swallowed
|
|
11
18
|
from codeintel.server import code_doctor_handler, code_query_handler, code_status_handler
|
|
12
19
|
|
|
13
20
|
_MAX_BODY_BYTES = 1_048_576 # 1 MiB
|
|
14
21
|
_REQUEST_TIMEOUT_S = 60 # per-request socket read timeout — drops an idle/half-open client
|
|
15
22
|
_MAX_CONCURRENT_REQUESTS = 64 # cap live worker threads so a burst can't exhaust threads/FDs
|
|
23
|
+
_DRAIN_TIMEOUT_S = 15 # on shutdown, wait up to this long for in-flight requests to finish
|
|
24
|
+
_ACCESS_LOG = os.environ.get("CODEINTEL_HTTP_ACCESS_LOG", "").strip().lower() in ("1", "true", "on", "yes")
|
|
25
|
+
_PROM_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
|
|
26
|
+
|
|
27
|
+
_logger = logging.getLogger("codeintel")
|
|
16
28
|
|
|
17
29
|
|
|
18
30
|
class _Handler(BaseHTTPRequestHandler):
|
|
@@ -21,7 +33,35 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
21
33
|
timeout = _REQUEST_TIMEOUT_S
|
|
22
34
|
|
|
23
35
|
def log_message(self, format: str, *args: object) -> None:
|
|
24
|
-
pass # suppress default stderr noise
|
|
36
|
+
pass # suppress default stderr noise; opt-in access logging is handled in _observe
|
|
37
|
+
|
|
38
|
+
def handle_one_request(self) -> None:
|
|
39
|
+
self._t0 = time.monotonic()
|
|
40
|
+
m = getattr(self.server, "metrics", None)
|
|
41
|
+
if m is not None:
|
|
42
|
+
m.inc_in_flight()
|
|
43
|
+
try:
|
|
44
|
+
super().handle_one_request()
|
|
45
|
+
finally:
|
|
46
|
+
if m is not None:
|
|
47
|
+
m.dec_in_flight()
|
|
48
|
+
|
|
49
|
+
def _observe(self, status: int) -> None:
|
|
50
|
+
try:
|
|
51
|
+
dur = time.monotonic() - getattr(self, "_t0", time.monotonic())
|
|
52
|
+
except Exception:
|
|
53
|
+
dur = 0.0
|
|
54
|
+
m = getattr(self.server, "metrics", None)
|
|
55
|
+
if m is not None:
|
|
56
|
+
try:
|
|
57
|
+
m.record(self.command or "?", urlparse(self.path).path, status, dur)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
if _ACCESS_LOG:
|
|
61
|
+
try:
|
|
62
|
+
_logger.info("%s %s -> %s (%.1fms)", self.command, self.path, status, dur * 1000.0)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
25
65
|
|
|
26
66
|
def _send_json(self, status: int, data: dict) -> None:
|
|
27
67
|
body = json.dumps(data).encode()
|
|
@@ -30,6 +70,16 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
30
70
|
self.send_header("Content-Length", str(len(body)))
|
|
31
71
|
self.end_headers()
|
|
32
72
|
self.wfile.write(body)
|
|
73
|
+
self._observe(status)
|
|
74
|
+
|
|
75
|
+
def _send_text(self, status: int, text: str, content_type: str) -> None:
|
|
76
|
+
body = text.encode()
|
|
77
|
+
self.send_response(status)
|
|
78
|
+
self.send_header("Content-Type", content_type)
|
|
79
|
+
self.send_header("Content-Length", str(len(body)))
|
|
80
|
+
self.end_headers()
|
|
81
|
+
self.wfile.write(body)
|
|
82
|
+
self._observe(status)
|
|
33
83
|
|
|
34
84
|
def _authorized(self) -> bool:
|
|
35
85
|
"""When the server was started with a token, require ``Authorization: Bearer <token>``
|
|
@@ -79,17 +129,44 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
79
129
|
self._send_json(200, result)
|
|
80
130
|
|
|
81
131
|
def do_GET(self) -> None:
|
|
132
|
+
parsed = urlparse(self.path)
|
|
133
|
+
path = parsed.path
|
|
134
|
+
|
|
135
|
+
# Liveness / readiness are UNAUTHENTICATED by convention (kubelet/load balancers don't send
|
|
136
|
+
# tokens) and reveal nothing sensitive — only up/ready.
|
|
137
|
+
if path == "/healthz":
|
|
138
|
+
self._send_json(200, {"status": "ok"})
|
|
139
|
+
return
|
|
140
|
+
if path == "/readyz":
|
|
141
|
+
try:
|
|
142
|
+
from codeintel.server import _get_gateway
|
|
143
|
+
_get_gateway() # builds once (cached); readiness = the gateway is constructible
|
|
144
|
+
self._send_json(200, {"status": "ready"})
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
log_swallowed("readyz", exc)
|
|
147
|
+
self._send_json(503, {"status": "not-ready"})
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# Everything else is auth-gated when a token is configured (metrics can reveal usage
|
|
151
|
+
# patterns; status reveals engine/index state).
|
|
82
152
|
if not self._authorized():
|
|
83
153
|
self._send_json(401, {"error": "unauthorized"})
|
|
84
154
|
return
|
|
85
|
-
|
|
86
|
-
if
|
|
87
|
-
self.
|
|
155
|
+
|
|
156
|
+
if path == "/metrics":
|
|
157
|
+
m = getattr(self.server, "metrics", None)
|
|
158
|
+
try:
|
|
159
|
+
text = m.render() if m is not None else ""
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
log_swallowed("metrics.render", exc) # never leave the client with no response
|
|
162
|
+
text = ""
|
|
163
|
+
self._send_text(200, text, _PROM_CONTENT_TYPE)
|
|
88
164
|
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
165
|
+
if path == "/code/status":
|
|
166
|
+
project_root = (parse_qs(parsed.query).get("project_root") or [""])[0]
|
|
167
|
+
self._send_json(200, code_status_handler({"project_root": project_root}))
|
|
168
|
+
return
|
|
169
|
+
self._send_json(404, {"error": "not-found"})
|
|
93
170
|
|
|
94
171
|
|
|
95
172
|
class CodeIntelHTTPServer(ThreadingHTTPServer):
|
|
@@ -102,6 +179,7 @@ class CodeIntelHTTPServer(ThreadingHTTPServer):
|
|
|
102
179
|
|
|
103
180
|
def __init__(self, *args, **kwargs) -> None:
|
|
104
181
|
super().__init__(*args, **kwargs)
|
|
182
|
+
self.metrics = Metrics(version=__version__)
|
|
105
183
|
# Stdlib ThreadingHTTPServer spawns one thread per connection with no ceiling, so many
|
|
106
184
|
# slow/half-open clients could exhaust threads/FDs. Bound live workers; past the cap we
|
|
107
185
|
# refuse fast with 503 rather than spawn an unbounded thread. (For a genuinely hostile
|
|
@@ -110,6 +188,10 @@ class CodeIntelHTTPServer(ThreadingHTTPServer):
|
|
|
110
188
|
|
|
111
189
|
def process_request(self, request, client_address) -> None:
|
|
112
190
|
if not self._slots.acquire(blocking=False):
|
|
191
|
+
try:
|
|
192
|
+
self.metrics.inc_overload() # surface overload in /metrics (rejected_total)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
113
195
|
try:
|
|
114
196
|
request.sendall(
|
|
115
197
|
b"HTTP/1.1 503 Service Unavailable\r\n"
|
|
@@ -152,6 +234,10 @@ def _is_loopback(host: str) -> bool:
|
|
|
152
234
|
return False # a non-IP hostname is never treated as loopback
|
|
153
235
|
|
|
154
236
|
|
|
237
|
+
def _no_auth_override() -> bool:
|
|
238
|
+
return os.environ.get("CODEINTEL_ALLOW_NO_AUTH", "").strip().lower() in ("1", "true", "on", "yes")
|
|
239
|
+
|
|
240
|
+
|
|
155
241
|
def run(
|
|
156
242
|
host: str = "127.0.0.1",
|
|
157
243
|
port: int = 8766,
|
|
@@ -159,17 +245,50 @@ def run(
|
|
|
159
245
|
allow_remote: bool = False,
|
|
160
246
|
token: str | None = None,
|
|
161
247
|
) -> None:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
248
|
+
configure_logging()
|
|
249
|
+
server_token = (token or "").strip() or None
|
|
250
|
+
if not _is_loopback(host):
|
|
251
|
+
if not allow_remote:
|
|
252
|
+
print(f"refusing to bind non-loopback host {host!r} without --allow-remote — this would "
|
|
253
|
+
f"expose a code-intel endpoint (your indexed repo) to the network", file=sys.stderr)
|
|
254
|
+
raise SystemExit(2)
|
|
255
|
+
if not server_token and not _no_auth_override():
|
|
256
|
+
# Fail closed: never serve an UNAUTHENTICATED endpoint on the network by accident (a
|
|
257
|
+
# containerized `serve-http --host 0.0.0.0` with no token stops here unless overridden).
|
|
258
|
+
print(f"refusing to serve on {host}:{port} with NO authentication — set --token or "
|
|
259
|
+
f"CODEINTEL_HTTP_TOKEN (recommended), or set CODEINTEL_ALLOW_NO_AUTH=1 to override "
|
|
260
|
+
f"for a trusted network", file=sys.stderr)
|
|
261
|
+
raise SystemExit(2)
|
|
262
|
+
|
|
167
263
|
server = CodeIntelHTTPServer((host, port), _Handler)
|
|
168
|
-
server.auth_token =
|
|
169
|
-
if not _is_loopback(host) and not
|
|
170
|
-
print(f"WARNING: serving codeintel on {host}:{port} with NO authentication
|
|
171
|
-
f"reach this port can read your
|
|
172
|
-
file=sys.stderr)
|
|
173
|
-
auth_note = " (bearer-token auth required)" if
|
|
264
|
+
server.auth_token = server_token
|
|
265
|
+
if not _is_loopback(host) and not server_token:
|
|
266
|
+
print(f"WARNING: serving codeintel on {host}:{port} with NO authentication "
|
|
267
|
+
f"(CODEINTEL_ALLOW_NO_AUTH set) — anyone who can reach this port can read your "
|
|
268
|
+
f"indexed repo", file=sys.stderr)
|
|
269
|
+
auth_note = " (bearer-token auth required)" if server_token else ""
|
|
174
270
|
print(f"Listening on http://{host}:{port}{auth_note}")
|
|
175
|
-
|
|
271
|
+
|
|
272
|
+
def _graceful(signum, frame) -> None:
|
|
273
|
+
# shutdown() blocks until serve_forever() returns, so it must run OFF the serving thread —
|
|
274
|
+
# spawn a helper thread to avoid deadlocking the signal handler on the main thread.
|
|
275
|
+
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
signal.signal(signal.SIGTERM, _graceful)
|
|
279
|
+
signal.signal(signal.SIGINT, _graceful)
|
|
280
|
+
except (ValueError, OSError):
|
|
281
|
+
pass # not the main thread — signal handlers unavailable; rely on the caller to stop us
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
server.serve_forever()
|
|
285
|
+
finally:
|
|
286
|
+
# Drain: serve_forever() has stopped accepting new connections; wait (bounded) for in-flight
|
|
287
|
+
# requests to finish before closing. Worker threads are daemons, so server_close() will NOT
|
|
288
|
+
# join them — this loop is what makes the shutdown actually graceful (a rolling restart
|
|
289
|
+
# doesn't cut a live response mid-flight).
|
|
290
|
+
deadline = time.monotonic() + _DRAIN_TIMEOUT_S
|
|
291
|
+
while server.metrics.in_flight() > 0 and time.monotonic() < deadline:
|
|
292
|
+
time.sleep(0.05)
|
|
293
|
+
server.server_close()
|
|
294
|
+
print("codeintel http server stopped", file=sys.stderr)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _JsonFormatter(logging.Formatter):
|
|
10
|
+
"""Minimal structured formatter for log aggregators (ELK / Splunk / Datadog). One JSON object
|
|
11
|
+
per line; no external dependency."""
|
|
12
|
+
|
|
13
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
14
|
+
payload = {
|
|
15
|
+
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
|
|
16
|
+
"level": record.levelname,
|
|
17
|
+
"logger": record.name,
|
|
18
|
+
"msg": record.getMessage(),
|
|
19
|
+
}
|
|
20
|
+
if record.exc_info:
|
|
21
|
+
payload["exc"] = self.formatException(record.exc_info)
|
|
22
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_configured = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def configure_logging() -> None:
|
|
29
|
+
"""Configure the ``codeintel`` logger for a long-running server process. Idempotent, and
|
|
30
|
+
scoped to the ``codeintel`` logger only — importing codeintel as a library never hijacks the
|
|
31
|
+
root logger. Honors:
|
|
32
|
+
|
|
33
|
+
* ``CODEINTEL_LOG_LEVEL`` (default ``WARNING``) — DEBUG | INFO | WARNING | ERROR
|
|
34
|
+
* ``CODEINTEL_LOG_FORMAT`` (default plain) — set to ``json`` for structured output
|
|
35
|
+
"""
|
|
36
|
+
global _configured
|
|
37
|
+
if _configured:
|
|
38
|
+
return
|
|
39
|
+
level_name = os.environ.get("CODEINTEL_LOG_LEVEL", "WARNING").strip().upper()
|
|
40
|
+
level = getattr(logging, level_name, logging.WARNING)
|
|
41
|
+
if not isinstance(level, int):
|
|
42
|
+
level = logging.WARNING
|
|
43
|
+
|
|
44
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
45
|
+
if os.environ.get("CODEINTEL_LOG_FORMAT", "").strip().lower() == "json":
|
|
46
|
+
handler.setFormatter(_JsonFormatter())
|
|
47
|
+
else:
|
|
48
|
+
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("codeintel")
|
|
51
|
+
logger.setLevel(level)
|
|
52
|
+
logger.handlers[:] = [handler]
|
|
53
|
+
logger.propagate = False
|
|
54
|
+
_configured = True
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Dict, Tuple
|
|
5
|
+
|
|
6
|
+
# Path labels are restricted to known routes so a caller cannot explode Prometheus label
|
|
7
|
+
# cardinality (a memory-exhaustion vector) by hammering random URLs — everything else folds
|
|
8
|
+
# into path="other". Method and status are already low-cardinality; version is our own string.
|
|
9
|
+
_KNOWN_PATHS = frozenset(
|
|
10
|
+
{"/code/query", "/code/doctor", "/code/status", "/healthz", "/readyz", "/metrics"}
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Metrics:
|
|
15
|
+
"""Thread-safe, dependency-free request metrics rendered in Prometheus text exposition
|
|
16
|
+
format. Shared across the threaded HTTP server's worker threads via one instance on the
|
|
17
|
+
server object; every mutation is lock-guarded."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, version: str = "") -> None:
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
self._version = version
|
|
22
|
+
self._count: Dict[Tuple[str, str, int], int] = {} # (method, path, status) -> n
|
|
23
|
+
self._lat_sum: Dict[str, float] = {} # path -> total seconds
|
|
24
|
+
self._lat_count: Dict[str, int] = {} # path -> n
|
|
25
|
+
self._in_flight = 0
|
|
26
|
+
self._overload = 0 # requests refused at the concurrency cap
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def _norm(path: str) -> str:
|
|
30
|
+
return path if path in _KNOWN_PATHS else "other"
|
|
31
|
+
|
|
32
|
+
def inc_in_flight(self) -> None:
|
|
33
|
+
with self._lock:
|
|
34
|
+
self._in_flight += 1
|
|
35
|
+
|
|
36
|
+
def dec_in_flight(self) -> None:
|
|
37
|
+
with self._lock:
|
|
38
|
+
self._in_flight = max(0, self._in_flight - 1)
|
|
39
|
+
|
|
40
|
+
def in_flight(self) -> int:
|
|
41
|
+
with self._lock:
|
|
42
|
+
return self._in_flight
|
|
43
|
+
|
|
44
|
+
def inc_overload(self) -> None:
|
|
45
|
+
with self._lock:
|
|
46
|
+
self._overload += 1
|
|
47
|
+
|
|
48
|
+
def record(self, method: str, path: str, status: int, duration_s: float) -> None:
|
|
49
|
+
p = self._norm(str(path))
|
|
50
|
+
key = (str(method or "?"), p, int(status))
|
|
51
|
+
with self._lock:
|
|
52
|
+
self._count[key] = self._count.get(key, 0) + 1
|
|
53
|
+
self._lat_sum[p] = self._lat_sum.get(p, 0.0) + max(0.0, float(duration_s))
|
|
54
|
+
self._lat_count[p] = self._lat_count.get(p, 0) + 1
|
|
55
|
+
|
|
56
|
+
def render(self) -> str:
|
|
57
|
+
with self._lock:
|
|
58
|
+
count = dict(self._count)
|
|
59
|
+
lat_sum = dict(self._lat_sum)
|
|
60
|
+
lat_count = dict(self._lat_count)
|
|
61
|
+
in_flight = self._in_flight
|
|
62
|
+
overload = self._overload
|
|
63
|
+
version = self._version
|
|
64
|
+
|
|
65
|
+
out: list[str] = []
|
|
66
|
+
out.append("# HELP codeintel_requests_total Total HTTP requests handled.")
|
|
67
|
+
out.append("# TYPE codeintel_requests_total counter")
|
|
68
|
+
for (method, path, status), n in sorted(count.items()):
|
|
69
|
+
out.append(
|
|
70
|
+
f'codeintel_requests_total{{method="{method}",path="{path}",status="{status}"}} {n}'
|
|
71
|
+
)
|
|
72
|
+
out.append("# HELP codeintel_request_duration_seconds Request handling duration.")
|
|
73
|
+
out.append("# TYPE codeintel_request_duration_seconds summary")
|
|
74
|
+
for path in sorted(lat_count):
|
|
75
|
+
out.append(f'codeintel_request_duration_seconds_sum{{path="{path}"}} {lat_sum[path]:.6f}')
|
|
76
|
+
out.append(f'codeintel_request_duration_seconds_count{{path="{path}"}} {lat_count[path]}')
|
|
77
|
+
out.append("# HELP codeintel_requests_in_flight Requests currently being handled.")
|
|
78
|
+
out.append("# TYPE codeintel_requests_in_flight gauge")
|
|
79
|
+
out.append(f"codeintel_requests_in_flight {in_flight}")
|
|
80
|
+
out.append("# HELP codeintel_requests_rejected_total Requests refused at the concurrency cap.")
|
|
81
|
+
out.append("# TYPE codeintel_requests_rejected_total counter")
|
|
82
|
+
out.append(f"codeintel_requests_rejected_total {overload}")
|
|
83
|
+
if version:
|
|
84
|
+
out.append("# HELP codeintel_build_info Build metadata (constant 1).")
|
|
85
|
+
out.append("# TYPE codeintel_build_info gauge")
|
|
86
|
+
out.append(f'codeintel_build_info{{version="{version}"}} 1')
|
|
87
|
+
return "\n".join(out) + "\n"
|
|
@@ -192,6 +192,8 @@ def code_map_handler(args: dict) -> dict:
|
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
def run() -> None:
|
|
195
|
+
from codeintel.logconfig import configure_logging
|
|
196
|
+
configure_logging() # logs to stderr; stdout is the MCP protocol channel
|
|
195
197
|
mcp = MCPServer(name="codeintel")
|
|
196
198
|
|
|
197
199
|
async def _code_query(
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Enterprise surface (0.4.0): health/readiness probes, Prometheus /metrics, and the metrics
|
|
2
|
+
registry. Real live server on an ephemeral port; no mocks."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import http.client
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
from codeintel.http_server import CodeIntelHTTPServer, _Handler
|
|
10
|
+
from codeintel.metrics import Metrics
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _serve(token=None):
|
|
14
|
+
s = CodeIntelHTTPServer(("127.0.0.1", 0), _Handler)
|
|
15
|
+
s.auth_token = token
|
|
16
|
+
threading.Thread(target=s.serve_forever, daemon=True).start()
|
|
17
|
+
return s, s.server_address[1]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get(port, path, headers=None):
|
|
21
|
+
c = http.client.HTTPConnection("127.0.0.1", port, timeout=5)
|
|
22
|
+
c.request("GET", path, headers=headers or {})
|
|
23
|
+
r = c.getresponse()
|
|
24
|
+
body = r.read().decode()
|
|
25
|
+
c.close()
|
|
26
|
+
return r.status, body
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --------------------------------------------------------------------------- probes
|
|
30
|
+
|
|
31
|
+
def test_healthz_ok_and_unauthenticated_even_with_token():
|
|
32
|
+
s, port = _serve(token="secret") # health must not require the token (kubelet won't send one)
|
|
33
|
+
try:
|
|
34
|
+
status, body = _get(port, "/healthz")
|
|
35
|
+
assert status == 200 and json.loads(body)["status"] == "ok"
|
|
36
|
+
finally:
|
|
37
|
+
s.shutdown()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_readyz_reports_ready():
|
|
41
|
+
s, port = _serve()
|
|
42
|
+
try:
|
|
43
|
+
status, body = _get(port, "/readyz")
|
|
44
|
+
assert status == 200 and json.loads(body)["status"] == "ready"
|
|
45
|
+
finally:
|
|
46
|
+
s.shutdown()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_readyz_unauthenticated_even_with_token():
|
|
50
|
+
s, port = _serve(token="secret")
|
|
51
|
+
try:
|
|
52
|
+
assert _get(port, "/readyz")[0] == 200
|
|
53
|
+
finally:
|
|
54
|
+
s.shutdown()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --------------------------------------------------------------------------- /metrics endpoint
|
|
58
|
+
|
|
59
|
+
def test_metrics_exposition_format():
|
|
60
|
+
s, port = _serve()
|
|
61
|
+
try:
|
|
62
|
+
_get(port, "/healthz") # generate a recorded request first
|
|
63
|
+
status, body = _get(port, "/metrics")
|
|
64
|
+
assert status == 200
|
|
65
|
+
assert "# TYPE codeintel_requests_total counter" in body
|
|
66
|
+
assert "codeintel_build_info" in body
|
|
67
|
+
assert 'path="/healthz"' in body # the earlier request was counted
|
|
68
|
+
finally:
|
|
69
|
+
s.shutdown()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_metrics_is_auth_gated_when_token_set():
|
|
73
|
+
s, port = _serve(token="secret")
|
|
74
|
+
try:
|
|
75
|
+
assert _get(port, "/metrics")[0] == 401
|
|
76
|
+
assert _get(port, "/metrics", {"Authorization": "Bearer secret"})[0] == 200
|
|
77
|
+
finally:
|
|
78
|
+
s.shutdown()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --------------------------------------------------------------------------- metrics registry
|
|
82
|
+
|
|
83
|
+
def test_metrics_registry_counts_and_caps_cardinality():
|
|
84
|
+
m = Metrics(version="9.9.9")
|
|
85
|
+
m.record("POST", "/code/query", 200, 0.05)
|
|
86
|
+
m.record("POST", "/code/query", 200, 0.05)
|
|
87
|
+
m.record("GET", "/some/random/unknown/path", 404, 0.01) # unknown → folded to path="other"
|
|
88
|
+
out = m.render()
|
|
89
|
+
assert 'codeintel_requests_total{method="POST",path="/code/query",status="200"} 2' in out
|
|
90
|
+
assert 'path="other"' in out
|
|
91
|
+
assert "/some/random/unknown/path" not in out # no unbounded label cardinality
|
|
92
|
+
assert 'codeintel_build_info{version="9.9.9"} 1' in out
|
|
93
|
+
assert "codeintel_request_duration_seconds_count" in out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_metrics_in_flight_gauge_never_negative():
|
|
97
|
+
m = Metrics()
|
|
98
|
+
m.dec_in_flight() # underflow guard
|
|
99
|
+
assert "codeintel_requests_in_flight 0" in m.render()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_metrics_exposes_rejected_and_in_flight():
|
|
103
|
+
m = Metrics()
|
|
104
|
+
m.inc_overload()
|
|
105
|
+
m.inc_overload()
|
|
106
|
+
m.inc_in_flight()
|
|
107
|
+
out = m.render()
|
|
108
|
+
assert "codeintel_requests_rejected_total 2" in out # capacity refusals are now visible
|
|
109
|
+
assert "codeintel_requests_in_flight 1" in out
|
|
110
|
+
assert m.in_flight() == 1 # accessor used by the shutdown drain
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --------------------------------------------------------------------------- secure-by-default bind
|
|
114
|
+
|
|
115
|
+
def test_run_refuses_non_loopback_without_auth(monkeypatch):
|
|
116
|
+
import pytest
|
|
117
|
+
|
|
118
|
+
from codeintel.http_server import run
|
|
119
|
+
monkeypatch.delenv("CODEINTEL_ALLOW_NO_AUTH", raising=False)
|
|
120
|
+
# non-loopback + --allow-remote but NO token and no override → must fail closed, not bind.
|
|
121
|
+
with pytest.raises(SystemExit):
|
|
122
|
+
run(host="0.0.0.0", port=0, allow_remote=True, token=None)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_run_refuses_non_loopback_without_allow_remote():
|
|
126
|
+
import pytest
|
|
127
|
+
|
|
128
|
+
from codeintel.http_server import run
|
|
129
|
+
with pytest.raises(SystemExit):
|
|
130
|
+
run(host="0.0.0.0", port=0, allow_remote=False, token="tok")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# --------------------------------------------------------------------------- structured logging
|
|
134
|
+
|
|
135
|
+
def test_json_log_formatter_emits_valid_json():
|
|
136
|
+
import json
|
|
137
|
+
import logging
|
|
138
|
+
|
|
139
|
+
from codeintel.logconfig import _JsonFormatter
|
|
140
|
+
|
|
141
|
+
rec = logging.LogRecord("codeintel", logging.WARNING, __file__, 1, "hello %s", ("world",), None)
|
|
142
|
+
parsed = json.loads(_JsonFormatter().format(rec))
|
|
143
|
+
assert parsed["level"] == "WARNING"
|
|
144
|
+
assert parsed["msg"] == "hello world"
|
|
145
|
+
assert parsed["logger"] == "codeintel"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0"
|
|
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
|
|
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
|