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.

Files changed (64) hide show
  1. {codecortex-0.3.0/src/codecortex.egg-info → codecortex-0.5.0}/PKG-INFO +25 -1
  2. {codecortex-0.3.0 → codecortex-0.5.0}/README.md +24 -0
  3. {codecortex-0.3.0 → codecortex-0.5.0/src/codecortex.egg-info}/PKG-INFO +25 -1
  4. {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/SOURCES.txt +5 -0
  5. codecortex-0.5.0/src/codeintel/__init__.py +1 -0
  6. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/__main__.py +7 -0
  7. codecortex-0.5.0/src/codeintel/auth.py +120 -0
  8. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/gateway.py +22 -9
  9. codecortex-0.5.0/src/codeintel/http_server.py +314 -0
  10. codecortex-0.5.0/src/codeintel/logconfig.py +54 -0
  11. codecortex-0.5.0/src/codeintel/metrics.py +87 -0
  12. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/server.py +20 -1
  13. codecortex-0.5.0/tests/test_enterprise.py +145 -0
  14. codecortex-0.5.0/tests/test_rbac.py +169 -0
  15. codecortex-0.3.0/src/codeintel/__init__.py +0 -1
  16. codecortex-0.3.0/src/codeintel/http_server.py +0 -175
  17. {codecortex-0.3.0 → codecortex-0.5.0}/LICENSE +0 -0
  18. {codecortex-0.3.0 → codecortex-0.5.0}/pyproject.toml +0 -0
  19. {codecortex-0.3.0 → codecortex-0.5.0}/setup.cfg +0 -0
  20. {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
  21. {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/entry_points.txt +0 -0
  22. {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/requires.txt +0 -0
  23. {codecortex-0.3.0 → codecortex-0.5.0}/src/codecortex.egg-info/top_level.txt +0 -0
  24. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/cache.py +0 -0
  25. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/config.py +0 -0
  26. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/doctor.py +0 -0
  27. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/indexer.py +0 -0
  28. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/injector.py +0 -0
  29. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/installer.py +0 -0
  30. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/mapper.py +0 -0
  31. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/onboarding.py +0 -0
  32. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/policy.py +0 -0
  33. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/provider.py +0 -0
  34. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/__init__.py +0 -0
  35. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/graph.py +0 -0
  36. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/lsp.py +0 -0
  37. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/none.py +0 -0
  38. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/providers/semantic.py +0 -0
  39. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/reindexer.py +0 -0
  40. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/reset.py +0 -0
  41. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/searcher.py +0 -0
  42. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/semantic_db.py +0 -0
  43. {codecortex-0.3.0 → codecortex-0.5.0}/src/codeintel/term.py +0 -0
  44. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_cache.py +0 -0
  45. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_config.py +0 -0
  46. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_doctor.py +0 -0
  47. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_e2e.py +0 -0
  48. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_gateway.py +0 -0
  49. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_graph_provider.py +0 -0
  50. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_graph_real.py +0 -0
  51. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_graph_stdin.py +0 -0
  52. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_hardening.py +0 -0
  53. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_http_auth.py +0 -0
  54. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_http_server.py +0 -0
  55. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_integration.py +0 -0
  56. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_lsp_provider.py +0 -0
  57. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_lsp_real.py +0 -0
  58. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_mapper.py +0 -0
  59. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_never_raise.py +0 -0
  60. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_onboarding.py +0 -0
  61. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_reindexer.py +0 -0
  62. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_reset.py +0 -0
  63. {codecortex-0.3.0 → codecortex-0.5.0}/tests/test_semantic_provider.py +0 -0
  64. {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.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.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")