codecortex 0.3.0__tar.gz → 0.5.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.5.0}/PKG-INFO +25 -1
- {codecortex-0.3.0 → codecortex-0.5.0}/README.md +24 -0
- {codecortex-0.3.0 → codecortex-0.5.0/src/codecortex.egg-info}/PKG-INFO +25 -1
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/SOURCES.txt +5 -0
- codecortex-0.5.0/src/codeintel/__init__.py +1 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/__main__.py +7 -0
- codecortex-0.5.0/src/codeintel/auth.py +120 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/gateway.py +22 -9
- codecortex-0.5.0/src/codeintel/http_server.py +314 -0
- codecortex-0.5.0/src/codeintel/logconfig.py +54 -0
- codecortex-0.5.0/src/codeintel/metrics.py +87 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/server.py +20 -1
- codecortex-0.5.0/tests/test_enterprise.py +145 -0
- codecortex-0.5.0/tests/test_rbac.py +169 -0
- codecortex-0.3.0/src/codeintel/__init__.py +0 -1
- codecortex-0.3.0/src/codeintel/http_server.py +0 -175
- {codecortex-0.3.0 → codecortex-0.5.0}/LICENSE +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/pyproject.toml +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/setup.cfg +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/cache.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/config.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/doctor.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/indexer.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/injector.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/installer.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/mapper.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/policy.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/provider.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/reset.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/searcher.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/term.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_cache.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_config.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_doctor.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_e2e.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_gateway.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_graph_provider.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_graph_real.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_hardening.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_http_auth.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_http_server.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_integration.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_lsp_real.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_mapper.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_never_raise.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_onboarding.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_reindexer.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_reset.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.3.0 → codecortex-0.5.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.5.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
|
|
@@ -189,6 +189,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
189
189
|
| `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
|
|
190
190
|
| `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
|
|
191
191
|
| `codeintel reset [project_root] [--all] [--yes]` | Clear the semantic index (this repo, or `--all`) to recover from a corrupt/stale DB |
|
|
192
|
+
| `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
|
|
192
193
|
|
|
193
194
|
Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
194
195
|
|
|
@@ -213,6 +214,10 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
213
214
|
| Variable | Effect |
|
|
214
215
|
|---|---|
|
|
215
216
|
| `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
|
|
217
|
+
| `CODEINTEL_AUTH_CONFIG` | Path to an RBAC token→role config (default `~/.codeintel/auth.toml`) — per-token roles + op scopes |
|
|
218
|
+
| `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
|
|
219
|
+
| `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
|
|
220
|
+
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
216
221
|
| `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
222
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
218
223
|
|
|
@@ -273,6 +278,25 @@ if result["result"] is not None:
|
|
|
273
278
|
|
|
274
279
|
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
280
|
|
|
281
|
+
## Operations & deployment
|
|
282
|
+
|
|
283
|
+
Running codeintel as a shared service? It ships with what ops teams expect:
|
|
284
|
+
|
|
285
|
+
| Endpoint | Auth | Purpose |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| `GET /healthz` | none | Liveness — always `200` (for load balancers / `livenessProbe`) |
|
|
288
|
+
| `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
|
|
289
|
+
| `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
|
|
290
|
+
|
|
291
|
+
Plus **bearer-token auth** — or **RBAC** (per-token roles + op scopes via `auth.toml`; a disallowed op returns `403`, and the role is server-authoritative so a client can't escalate) — **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.
|
|
292
|
+
|
|
293
|
+
**Full guide → [docs/deploy.md](docs/deploy.md)**: systemd, Docker / Compose, Kubernetes (liveness + readiness probes, token from a Secret), reverse-proxy TLS, **RBAC + SSO-via-auth-proxy**, a Prometheus scrape config, and a security checklist.
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
297
|
+
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
298
|
+
```
|
|
299
|
+
|
|
276
300
|
## Development
|
|
277
301
|
|
|
278
302
|
```bash
|
|
@@ -156,6 +156,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
156
156
|
| `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
|
|
157
157
|
| `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
|
|
158
158
|
| `codeintel reset [project_root] [--all] [--yes]` | Clear the semantic index (this repo, or `--all`) to recover from a corrupt/stale DB |
|
|
159
|
+
| `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
|
|
159
160
|
|
|
160
161
|
Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
161
162
|
|
|
@@ -180,6 +181,10 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
180
181
|
| Variable | Effect |
|
|
181
182
|
|---|---|
|
|
182
183
|
| `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
|
|
184
|
+
| `CODEINTEL_AUTH_CONFIG` | Path to an RBAC token→role config (default `~/.codeintel/auth.toml`) — per-token roles + op scopes |
|
|
185
|
+
| `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
|
|
186
|
+
| `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
|
|
187
|
+
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
183
188
|
| `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
189
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
185
190
|
|
|
@@ -240,6 +245,25 @@ if result["result"] is not None:
|
|
|
240
245
|
|
|
241
246
|
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
247
|
|
|
248
|
+
## Operations & deployment
|
|
249
|
+
|
|
250
|
+
Running codeintel as a shared service? It ships with what ops teams expect:
|
|
251
|
+
|
|
252
|
+
| Endpoint | Auth | Purpose |
|
|
253
|
+
|---|---|---|
|
|
254
|
+
| `GET /healthz` | none | Liveness — always `200` (for load balancers / `livenessProbe`) |
|
|
255
|
+
| `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
|
|
256
|
+
| `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
|
|
257
|
+
|
|
258
|
+
Plus **bearer-token auth** — or **RBAC** (per-token roles + op scopes via `auth.toml`; a disallowed op returns `403`, and the role is server-authoritative so a client can't escalate) — **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.
|
|
259
|
+
|
|
260
|
+
**Full guide → [docs/deploy.md](docs/deploy.md)**: systemd, Docker / Compose, Kubernetes (liveness + readiness probes, token from a Secret), reverse-proxy TLS, **RBAC + SSO-via-auth-proxy**, a Prometheus scrape config, and a security checklist.
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
264
|
+
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
265
|
+
```
|
|
266
|
+
|
|
243
267
|
## Development
|
|
244
268
|
|
|
245
269
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -189,6 +189,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
189
189
|
| `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
|
|
190
190
|
| `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
|
|
191
191
|
| `codeintel reset [project_root] [--all] [--yes]` | Clear the semantic index (this repo, or `--all`) to recover from a corrupt/stale DB |
|
|
192
|
+
| `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
|
|
192
193
|
|
|
193
194
|
Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
194
195
|
|
|
@@ -213,6 +214,10 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
213
214
|
| Variable | Effect |
|
|
214
215
|
|---|---|
|
|
215
216
|
| `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
|
|
217
|
+
| `CODEINTEL_AUTH_CONFIG` | Path to an RBAC token→role config (default `~/.codeintel/auth.toml`) — per-token roles + op scopes |
|
|
218
|
+
| `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
|
|
219
|
+
| `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
|
|
220
|
+
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
216
221
|
| `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
222
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
218
223
|
|
|
@@ -273,6 +278,25 @@ if result["result"] is not None:
|
|
|
273
278
|
|
|
274
279
|
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
280
|
|
|
281
|
+
## Operations & deployment
|
|
282
|
+
|
|
283
|
+
Running codeintel as a shared service? It ships with what ops teams expect:
|
|
284
|
+
|
|
285
|
+
| Endpoint | Auth | Purpose |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| `GET /healthz` | none | Liveness — always `200` (for load balancers / `livenessProbe`) |
|
|
288
|
+
| `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
|
|
289
|
+
| `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
|
|
290
|
+
|
|
291
|
+
Plus **bearer-token auth** — or **RBAC** (per-token roles + op scopes via `auth.toml`; a disallowed op returns `403`, and the role is server-authoritative so a client can't escalate) — **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.
|
|
292
|
+
|
|
293
|
+
**Full guide → [docs/deploy.md](docs/deploy.md)**: systemd, Docker / Compose, Kubernetes (liveness + readiness probes, token from a Secret), reverse-proxy TLS, **RBAC + SSO-via-auth-proxy**, a Prometheus scrape config, and a security checklist.
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
297
|
+
-e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
|
|
298
|
+
```
|
|
299
|
+
|
|
276
300
|
## Development
|
|
277
301
|
|
|
278
302
|
```bash
|
|
@@ -9,6 +9,7 @@ src/codecortex.egg-info/requires.txt
|
|
|
9
9
|
src/codecortex.egg-info/top_level.txt
|
|
10
10
|
src/codeintel/__init__.py
|
|
11
11
|
src/codeintel/__main__.py
|
|
12
|
+
src/codeintel/auth.py
|
|
12
13
|
src/codeintel/cache.py
|
|
13
14
|
src/codeintel/config.py
|
|
14
15
|
src/codeintel/doctor.py
|
|
@@ -17,7 +18,9 @@ src/codeintel/http_server.py
|
|
|
17
18
|
src/codeintel/indexer.py
|
|
18
19
|
src/codeintel/injector.py
|
|
19
20
|
src/codeintel/installer.py
|
|
21
|
+
src/codeintel/logconfig.py
|
|
20
22
|
src/codeintel/mapper.py
|
|
23
|
+
src/codeintel/metrics.py
|
|
21
24
|
src/codeintel/onboarding.py
|
|
22
25
|
src/codeintel/policy.py
|
|
23
26
|
src/codeintel/provider.py
|
|
@@ -36,6 +39,7 @@ tests/test_cache.py
|
|
|
36
39
|
tests/test_config.py
|
|
37
40
|
tests/test_doctor.py
|
|
38
41
|
tests/test_e2e.py
|
|
42
|
+
tests/test_enterprise.py
|
|
39
43
|
tests/test_gateway.py
|
|
40
44
|
tests/test_graph_provider.py
|
|
41
45
|
tests/test_graph_real.py
|
|
@@ -49,6 +53,7 @@ tests/test_lsp_real.py
|
|
|
49
53
|
tests/test_mapper.py
|
|
50
54
|
tests/test_never_raise.py
|
|
51
55
|
tests/test_onboarding.py
|
|
56
|
+
tests/test_rbac.py
|
|
52
57
|
tests/test_reindexer.py
|
|
53
58
|
tests/test_reset.py
|
|
54
59
|
tests/test_semantic_provider.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -90,6 +90,8 @@ def main() -> None:
|
|
|
90
90
|
reset_parser.add_argument("--yes", "-y", action="store_true", help="Skip the confirmation prompt")
|
|
91
91
|
reset_parser.add_argument("--json", action="store_true", help="Emit the structured JSON report")
|
|
92
92
|
|
|
93
|
+
subparsers.add_parser("gen-token", help="Print a secure random bearer token (for serve-http / RBAC auth.toml)")
|
|
94
|
+
|
|
93
95
|
args = parser.parse_args()
|
|
94
96
|
|
|
95
97
|
from codeintel import term
|
|
@@ -355,6 +357,11 @@ def main() -> None:
|
|
|
355
357
|
|
|
356
358
|
sys.exit(0 if any_ok else 1)
|
|
357
359
|
|
|
360
|
+
elif args.command == "gen-token":
|
|
361
|
+
import secrets
|
|
362
|
+
print(secrets.token_urlsafe(32))
|
|
363
|
+
sys.exit(0)
|
|
364
|
+
|
|
358
365
|
else:
|
|
359
366
|
parser.print_help()
|
|
360
367
|
sys.exit(0)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Token→role authentication and RBAC for the HTTP transport.
|
|
2
|
+
|
|
3
|
+
An optional auth config (``~/.codeintel/auth.toml`` or ``$CODEINTEL_AUTH_CONFIG``) maps bearer
|
|
4
|
+
tokens to roles and roles to the operations they may run. The role is derived **server-side from
|
|
5
|
+
the authenticated token** — a client can never escalate by claiming a role in the request body.
|
|
6
|
+
|
|
7
|
+
Example ``auth.toml``::
|
|
8
|
+
|
|
9
|
+
[roles]
|
|
10
|
+
admin = ["*"] # ["*"] (or omitted) = every op
|
|
11
|
+
reader = ["search", "symbol", "overview", "callers", "callees", "impact", "chain", "context"]
|
|
12
|
+
searcher = ["search", "context"]
|
|
13
|
+
|
|
14
|
+
[tokens] # token = role (store this file mode 0600)
|
|
15
|
+
"s3cret-admin-value" = "admin"
|
|
16
|
+
"sha256:<hex-of-a-token>" = "reader" # pre-hashed form, so plaintext need not be at rest
|
|
17
|
+
|
|
18
|
+
SSO: put an OIDC auth proxy (e.g. oauth2-proxy) in front and have it present a per-role bearer
|
|
19
|
+
token to codeintel — codeintel never speaks to the IdP itself (local-first, no per-query network).
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import pathlib
|
|
27
|
+
import tomllib # stdlib on Python 3.11+ (the project's minimum)
|
|
28
|
+
from typing import Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
from codeintel.policy import TieringPolicy
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("codeintel")
|
|
33
|
+
|
|
34
|
+
_ALL = "*"
|
|
35
|
+
_SHA_PREFIX = "sha256:"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TokenAuth:
|
|
39
|
+
"""Resolves a bearer token to a role and builds the matching op policy. Tokens are held as
|
|
40
|
+
sha256 hashes; a presented token is hashed and looked up in O(1). Immutable; thread-safe."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, token_hash_to_role: Dict[str, str], role_ops: Dict[str, List[str]]) -> None:
|
|
43
|
+
self._tokens = token_hash_to_role
|
|
44
|
+
self._role_ops = role_ops
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def enabled(self) -> bool:
|
|
48
|
+
return bool(self._tokens)
|
|
49
|
+
|
|
50
|
+
def role_for(self, presented_token: str) -> Optional[str]:
|
|
51
|
+
"""The role for a presented token, or None if it isn't a known token."""
|
|
52
|
+
if not presented_token:
|
|
53
|
+
return None
|
|
54
|
+
h = hashlib.sha256(presented_token.encode("utf-8")).hexdigest()
|
|
55
|
+
return self._tokens.get(h)
|
|
56
|
+
|
|
57
|
+
def build_policy(self) -> TieringPolicy:
|
|
58
|
+
"""A role with ops ``["*"]`` (or none) is unrestricted → omit it from the rules, since
|
|
59
|
+
TieringPolicy treats a role absent from its rules as full-access. Every other role maps to
|
|
60
|
+
its explicit op allowlist (an empty list = deny all ops, a fail-safe default)."""
|
|
61
|
+
rules: Dict[str, List[str]] = {}
|
|
62
|
+
for role, ops in self._role_ops.items():
|
|
63
|
+
if _ALL in ops:
|
|
64
|
+
continue # unrestricted
|
|
65
|
+
rules[role] = list(ops)
|
|
66
|
+
return TieringPolicy(enabled=bool(rules), rules=rules)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _auth_config_path() -> Optional[pathlib.Path]:
|
|
70
|
+
env = os.environ.get("CODEINTEL_AUTH_CONFIG", "").strip()
|
|
71
|
+
if env:
|
|
72
|
+
p = pathlib.Path(env)
|
|
73
|
+
return p if p.is_file() else None
|
|
74
|
+
default = pathlib.Path.home() / ".codeintel" / "auth.toml"
|
|
75
|
+
return default if default.is_file() else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_auth() -> TokenAuth:
|
|
79
|
+
"""Load the token→role config if present. Never raises — a missing or malformed file yields an
|
|
80
|
+
empty (disabled) TokenAuth, and the caller falls back to single-token / no-auth."""
|
|
81
|
+
path = _auth_config_path()
|
|
82
|
+
if path is None:
|
|
83
|
+
return TokenAuth({}, {})
|
|
84
|
+
try:
|
|
85
|
+
with path.open("rb") as fh:
|
|
86
|
+
data = tomllib.load(fh)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
logger.warning("auth: %s could not be parsed (%s) — RBAC is OFF", path, type(exc).__name__)
|
|
89
|
+
return TokenAuth({}, {})
|
|
90
|
+
|
|
91
|
+
raw_roles = data.get("roles") if isinstance(data.get("roles"), dict) else {}
|
|
92
|
+
role_ops: Dict[str, List[str]] = {}
|
|
93
|
+
for role, ops in raw_roles.items():
|
|
94
|
+
if isinstance(ops, list):
|
|
95
|
+
role_ops[str(role)] = [str(o) for o in ops]
|
|
96
|
+
else:
|
|
97
|
+
# Fail CLOSED: a non-list ops value (the missing-brackets typo `reader = "search"`) must
|
|
98
|
+
# never silently become full access — deny all ops for that role instead.
|
|
99
|
+
logger.warning("auth: role %r ops must be a list (got %r) — denying all ops for it", role, ops)
|
|
100
|
+
role_ops[str(role)] = []
|
|
101
|
+
|
|
102
|
+
raw_tokens = data.get("tokens") if isinstance(data.get("tokens"), dict) else {}
|
|
103
|
+
token_hash_to_role: Dict[str, str] = {}
|
|
104
|
+
for tok, role in raw_tokens.items():
|
|
105
|
+
role = str(role)
|
|
106
|
+
# A token mapped to an undefined role fails safe: define it as deny-all (empty op list).
|
|
107
|
+
if role not in role_ops:
|
|
108
|
+
role_ops[role] = []
|
|
109
|
+
tok = str(tok)
|
|
110
|
+
if tok[:len(_SHA_PREFIX)].lower() == _SHA_PREFIX: # case-insensitive `sha256:` prefix
|
|
111
|
+
h = tok[len(_SHA_PREFIX):].strip().lower()
|
|
112
|
+
else:
|
|
113
|
+
h = hashlib.sha256(tok.encode("utf-8")).hexdigest()
|
|
114
|
+
if h:
|
|
115
|
+
token_hash_to_role[h] = role
|
|
116
|
+
|
|
117
|
+
if not token_hash_to_role:
|
|
118
|
+
logger.warning("auth: %s defines no usable [tokens] — RBAC is OFF (no token→role mapping)", path)
|
|
119
|
+
|
|
120
|
+
return TokenAuth(token_hash_to_role, role_ops)
|
|
@@ -132,6 +132,14 @@ class Gateway:
|
|
|
132
132
|
log_swallowed(f"Gateway._dispatch_single[{engine_str}.{op_str}]", exc)
|
|
133
133
|
return safe_null_result(op_str, target_str, engine=engine_str, reason="provider-error")
|
|
134
134
|
|
|
135
|
+
def allows(self, role: str, op: str) -> bool:
|
|
136
|
+
"""Whether *role* may run *op* under the current policy (True when no policy is configured).
|
|
137
|
+
Lets non-query handlers (e.g. doctor) share the same RBAC gate as query()."""
|
|
138
|
+
try:
|
|
139
|
+
return self._policy is None or self._policy.is_allowed(role, op)
|
|
140
|
+
except Exception:
|
|
141
|
+
return True
|
|
142
|
+
|
|
135
143
|
def query(
|
|
136
144
|
self,
|
|
137
145
|
op=None,
|
|
@@ -142,16 +150,25 @@ class Gateway:
|
|
|
142
150
|
project_root=None,
|
|
143
151
|
) -> Result:
|
|
144
152
|
try:
|
|
145
|
-
try:
|
|
146
|
-
self._reindexer.maybe_reindex(str(project_root or ""))
|
|
147
|
-
except Exception:
|
|
148
|
-
pass
|
|
149
|
-
|
|
150
153
|
op_str = str(op or "")
|
|
151
154
|
target_str = str(target or "")
|
|
152
155
|
engine_str = str(engine or "").strip() or "auto"
|
|
153
156
|
was_auto = engine_str == "auto"
|
|
154
157
|
|
|
158
|
+
# Policy check FIRST — a role denied for this op does NO work (no reindex, no dispatch,
|
|
159
|
+
# no cache lookup). Applies to the modern provider path; the legacy list path has none.
|
|
160
|
+
if (
|
|
161
|
+
self._legacy_providers is None
|
|
162
|
+
and self._policy is not None
|
|
163
|
+
and not self._policy.is_allowed(role, op_str)
|
|
164
|
+
):
|
|
165
|
+
return safe_null_result(op_str, target_str, reason="op-not-allowed-for-role")
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
self._reindexer.maybe_reindex(str(project_root or ""))
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
155
172
|
# Legacy list-based path (backward compat with pre-Phase-2 tests)
|
|
156
173
|
if self._legacy_providers is not None:
|
|
157
174
|
for p in self._legacy_providers:
|
|
@@ -164,10 +181,6 @@ class Gateway:
|
|
|
164
181
|
reason = "engine-unavailable" if engine is not None else "no-result"
|
|
165
182
|
return safe_null_result(op_str, target_str, reason=reason)
|
|
166
183
|
|
|
167
|
-
# Policy check — before cache lookup
|
|
168
|
-
if self._policy is not None and not self._policy.is_allowed(role, op_str):
|
|
169
|
-
return safe_null_result(op_str, target_str, reason="op-not-allowed-for-role")
|
|
170
|
-
|
|
171
184
|
# Unknown engine — reject immediately
|
|
172
185
|
if engine_str not in _KNOWN_ENGINES:
|
|
173
186
|
return safe_null_result(op_str, target_str, reason="unknown-engine")
|