codecortex 0.4.0__tar.gz → 0.6.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 (65) hide show
  1. {codecortex-0.4.0/src/codecortex.egg-info → codecortex-0.6.0}/PKG-INFO +5 -3
  2. {codecortex-0.4.0 → codecortex-0.6.0}/README.md +4 -2
  3. {codecortex-0.4.0 → codecortex-0.6.0/src/codecortex.egg-info}/PKG-INFO +5 -3
  4. {codecortex-0.4.0 → codecortex-0.6.0}/src/codecortex.egg-info/SOURCES.txt +3 -0
  5. codecortex-0.6.0/src/codeintel/__init__.py +1 -0
  6. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/__main__.py +8 -0
  7. codecortex-0.6.0/src/codeintel/auth.py +120 -0
  8. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/config.py +2 -0
  9. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/gateway.py +22 -9
  10. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/http_server.py +46 -26
  11. codecortex-0.6.0/src/codeintel/indexer.py +422 -0
  12. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/onboarding.py +1 -0
  13. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/providers/semantic.py +1 -0
  14. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/reindexer.py +1 -0
  15. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/semantic_db.py +47 -2
  16. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/server.py +18 -1
  17. codecortex-0.6.0/tests/test_chunking.py +357 -0
  18. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_config.py +7 -0
  19. codecortex-0.6.0/tests/test_rbac.py +169 -0
  20. codecortex-0.4.0/src/codeintel/__init__.py +0 -1
  21. codecortex-0.4.0/src/codeintel/indexer.py +0 -261
  22. {codecortex-0.4.0 → codecortex-0.6.0}/LICENSE +0 -0
  23. {codecortex-0.4.0 → codecortex-0.6.0}/pyproject.toml +0 -0
  24. {codecortex-0.4.0 → codecortex-0.6.0}/setup.cfg +0 -0
  25. {codecortex-0.4.0 → codecortex-0.6.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
  26. {codecortex-0.4.0 → codecortex-0.6.0}/src/codecortex.egg-info/entry_points.txt +0 -0
  27. {codecortex-0.4.0 → codecortex-0.6.0}/src/codecortex.egg-info/requires.txt +0 -0
  28. {codecortex-0.4.0 → codecortex-0.6.0}/src/codecortex.egg-info/top_level.txt +0 -0
  29. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/cache.py +0 -0
  30. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/doctor.py +0 -0
  31. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/injector.py +0 -0
  32. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/installer.py +0 -0
  33. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/logconfig.py +0 -0
  34. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/mapper.py +0 -0
  35. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/metrics.py +0 -0
  36. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/policy.py +0 -0
  37. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/provider.py +0 -0
  38. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/providers/__init__.py +0 -0
  39. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/providers/graph.py +0 -0
  40. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/providers/lsp.py +0 -0
  41. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/providers/none.py +0 -0
  42. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/reset.py +0 -0
  43. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/searcher.py +0 -0
  44. {codecortex-0.4.0 → codecortex-0.6.0}/src/codeintel/term.py +0 -0
  45. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_cache.py +0 -0
  46. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_doctor.py +0 -0
  47. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_e2e.py +0 -0
  48. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_enterprise.py +0 -0
  49. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_gateway.py +0 -0
  50. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_graph_provider.py +0 -0
  51. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_graph_real.py +0 -0
  52. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_graph_stdin.py +0 -0
  53. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_hardening.py +0 -0
  54. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_http_auth.py +0 -0
  55. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_http_server.py +0 -0
  56. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_integration.py +0 -0
  57. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_lsp_provider.py +0 -0
  58. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_lsp_real.py +0 -0
  59. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_mapper.py +0 -0
  60. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_never_raise.py +0 -0
  61. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_onboarding.py +0 -0
  62. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_reindexer.py +0 -0
  63. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_reset.py +0 -0
  64. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_semantic_provider.py +0 -0
  65. {codecortex-0.4.0 → codecortex-0.6.0}/tests/test_term.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.4.0
3
+ Version: 0.6.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,7 @@ 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 |
216
218
  | `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
217
219
  | `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
218
220
  | `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
@@ -286,9 +288,9 @@ Running codeintel as a shared service? It ships with what ops teams expect:
286
288
  | `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
287
289
  | `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
288
290
 
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.
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.
290
292
 
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.
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.
292
294
 
293
295
  ```bash
294
296
  docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
@@ -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,7 @@ 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 |
183
185
  | `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
184
186
  | `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
185
187
  | `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
@@ -253,9 +255,9 @@ Running codeintel as a shared service? It ships with what ops teams expect:
253
255
  | `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
254
256
  | `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
255
257
 
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.
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.
257
259
 
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.
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.
259
261
 
260
262
  ```bash
261
263
  docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.4.0
3
+ Version: 0.6.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,7 @@ 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 |
216
218
  | `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
217
219
  | `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
218
220
  | `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
@@ -286,9 +288,9 @@ Running codeintel as a shared service? It ships with what ops teams expect:
286
288
  | `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
287
289
  | `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
288
290
 
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.
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.
290
292
 
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.
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.
292
294
 
293
295
  ```bash
294
296
  docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
@@ -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
@@ -35,6 +36,7 @@ src/codeintel/providers/lsp.py
35
36
  src/codeintel/providers/none.py
36
37
  src/codeintel/providers/semantic.py
37
38
  tests/test_cache.py
39
+ tests/test_chunking.py
38
40
  tests/test_config.py
39
41
  tests/test_doctor.py
40
42
  tests/test_e2e.py
@@ -52,6 +54,7 @@ tests/test_lsp_real.py
52
54
  tests/test_mapper.py
53
55
  tests/test_never_raise.py
54
56
  tests/test_onboarding.py
57
+ tests/test_rbac.py
55
58
  tests/test_reindexer.py
56
59
  tests/test_reset.py
57
60
  tests/test_semantic_provider.py
@@ -0,0 +1 @@
1
+ __version__ = "0.6.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
@@ -122,6 +124,7 @@ def main() -> None:
122
124
  stride=int(cfg.get("stride", 10)),
123
125
  max_chunks=int(cfg.get("max_chunks", 500)),
124
126
  max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
127
+ chunk_strategy=str(cfg.get("chunk_strategy", "syntax")),
125
128
  ).index(project_root)
126
129
  if count > 0:
127
130
  print(f"Indexed {count} chunks")
@@ -355,6 +358,11 @@ def main() -> None:
355
358
 
356
359
  sys.exit(0 if any_ok else 1)
357
360
 
361
+ elif args.command == "gen-token":
362
+ import secrets
363
+ print(secrets.token_urlsafe(32))
364
+ sys.exit(0)
365
+
358
366
  else:
359
367
  parser.print_help()
360
368
  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)
@@ -24,6 +24,7 @@ _DEFAULTS: dict = {
24
24
  "max_total_chunks": 100000, # safety ceiling on chunks embedded in one index pass
25
25
  "cosine_floor": 0.25,
26
26
  "model": "BAAI/bge-small-en-v1.5",
27
+ "chunk_strategy": "syntax", # syntax-aware (def/class boundaries) vs fixed line windows
27
28
  }
28
29
 
29
30
  # Values restricted to a fixed set — anything else falls back to the default.
@@ -31,6 +32,7 @@ _ENUMS: dict = {
31
32
  "backend": {"auto", "graph", "lsp", "semantic"},
32
33
  "semantic": {"on", "off"},
33
34
  "reindex": {"on-demand", "never"},
35
+ "chunk_strategy": {"syntax", "lines"},
34
36
  }
35
37
  _POSITIVE_INTS = ("window", "stride", "max_chunks", "max_total_chunks")
36
38
 
@@ -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")
@@ -81,24 +81,30 @@ class _Handler(BaseHTTPRequestHandler):
81
81
  self.wfile.write(body)
82
82
  self._observe(status)
83
83
 
84
- def _authorized(self) -> bool:
85
- """When the server was started with a token, require ``Authorization: Bearer <token>``
86
- (constant-time compare). No token configured → auth is disabled (the loopback default)."""
87
- token = getattr(self.server, "auth_token", None)
88
- if not token:
89
- return True
84
+ def _bearer(self) -> str:
90
85
  header = self.headers.get("Authorization", "")
91
86
  prefix = "Bearer "
92
- if not header.startswith(prefix):
93
- return False
94
- presented = header[len(prefix):].strip()
95
- # Compare as UTF-8 bytes: hmac.compare_digest raises TypeError on a non-ASCII str, so a
96
- # malformed header (e.g. `Bearer résumé`) would otherwise crash the handler thread. Encoding
97
- # is always safe and keeps the comparison constant-time.
98
- return hmac.compare_digest(presented.encode("utf-8"), token.encode("utf-8"))
87
+ return header[len(prefix):].strip() if header.startswith(prefix) else ""
88
+
89
+ def _resolve_role(self) -> tuple[bool, str]:
90
+ """Authenticate the request and return ``(authorized, role)``. The role is
91
+ **server-authoritative** derived from the token, never from the request body. RBAC (a
92
+ token→role auth config) takes precedence; else a single shared token; else no auth (the
93
+ loopback default). Bytes-compare so a non-ASCII header can't crash ``hmac.compare_digest``."""
94
+ auth = getattr(self.server, "token_auth", None)
95
+ if auth is not None and auth.enabled:
96
+ role = auth.role_for(self._bearer())
97
+ return (role is not None, role or "")
98
+ token = getattr(self.server, "auth_token", None)
99
+ if not token:
100
+ return (True, "") # auth disabled → full access, no role
101
+ presented = self._bearer()
102
+ ok = bool(presented) and hmac.compare_digest(presented.encode("utf-8"), token.encode("utf-8"))
103
+ return (ok, "")
99
104
 
100
105
  def do_POST(self) -> None:
101
- if not self._authorized():
106
+ ok, role = self._resolve_role()
107
+ if not ok:
102
108
  self._send_json(401, {"error": "unauthorized"})
103
109
  return
104
110
  if self.path not in ("/code/query", "/code/doctor"):
@@ -122,11 +128,14 @@ class _Handler(BaseHTTPRequestHandler):
122
128
  if not isinstance(parsed, dict):
123
129
  self._send_json(400, {"error": "bad-request"})
124
130
  return
131
+ parsed["role"] = role # server-authoritative — overrides any client-supplied role (no escalation)
125
132
  if self.path == "/code/doctor":
126
133
  result = code_doctor_handler(parsed)
127
134
  else:
128
135
  result = code_query_handler(parsed)
129
- self._send_json(200, result)
136
+ # An RBAC denial (query OR doctor) comes back as a safe-null with this reason — 403 it.
137
+ status = 403 if result.get("reason") == "op-not-allowed-for-role" else 200
138
+ self._send_json(status, result)
130
139
 
131
140
  def do_GET(self) -> None:
132
141
  parsed = urlparse(self.path)
@@ -147,9 +156,10 @@ class _Handler(BaseHTTPRequestHandler):
147
156
  self._send_json(503, {"status": "not-ready"})
148
157
  return
149
158
 
150
- # Everything else is auth-gated when a token is configured (metrics can reveal usage
151
- # patterns; status reveals engine/index state).
152
- if not self._authorized():
159
+ # Everything else is auth-gated when auth is configured (metrics can reveal usage patterns;
160
+ # status reveals engine/index state). Any valid token/role may read these operational views.
161
+ ok, _ = self._resolve_role()
162
+ if not ok:
153
163
  self._send_json(401, {"error": "unauthorized"})
154
164
  return
155
165
 
@@ -175,7 +185,8 @@ class CodeIntelHTTPServer(ThreadingHTTPServer):
175
185
  # lock-guarded (query cache, reindexer, LSP sessions, graph project cache) and the semantic
176
186
  # engine is thread-confined with WAL, so concurrent requests are safe.
177
187
  daemon_threads = True
178
- auth_token: str | None = None # set by run() when a token is provided
188
+ auth_token: str | None = None # set by run() when a single shared token is provided
189
+ token_auth = None # set by run(): a TokenAuth for RBAC (token->role), if configured
179
190
 
180
191
  def __init__(self, *args, **kwargs) -> None:
181
192
  super().__init__(*args, **kwargs)
@@ -246,27 +257,36 @@ def run(
246
257
  token: str | None = None,
247
258
  ) -> None:
248
259
  configure_logging()
260
+ from codeintel.auth import load_auth
261
+ token_auth = load_auth() # RBAC (token→role), if an auth config exists
249
262
  server_token = (token or "").strip() or None
263
+ authenticated = bool(server_token) or token_auth.enabled
250
264
  if not _is_loopback(host):
251
265
  if not allow_remote:
252
266
  print(f"refusing to bind non-loopback host {host!r} without --allow-remote — this would "
253
267
  f"expose a code-intel endpoint (your indexed repo) to the network", file=sys.stderr)
254
268
  raise SystemExit(2)
255
- if not server_token and not _no_auth_override():
269
+ if not authenticated and not _no_auth_override():
256
270
  # 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)
271
+ # containerized `serve-http --host 0.0.0.0` with no auth stops here unless overridden).
272
+ print(f"refusing to serve on {host}:{port} with NO authentication — set --token / "
273
+ f"CODEINTEL_HTTP_TOKEN, configure an RBAC auth.toml, or set CODEINTEL_ALLOW_NO_AUTH=1 "
274
+ f"to override for a trusted network", file=sys.stderr)
261
275
  raise SystemExit(2)
262
276
 
263
277
  server = CodeIntelHTTPServer((host, port), _Handler)
264
278
  server.auth_token = server_token
265
- if not _is_loopback(host) and not server_token:
279
+ server.token_auth = token_auth
280
+ if not _is_loopback(host) and not authenticated:
266
281
  print(f"WARNING: serving codeintel on {host}:{port} with NO authentication "
267
282
  f"(CODEINTEL_ALLOW_NO_AUTH set) — anyone who can reach this port can read your "
268
283
  f"indexed repo", file=sys.stderr)
269
- auth_note = " (bearer-token auth required)" if server_token else ""
284
+ if token_auth.enabled:
285
+ auth_note = " (RBAC: token→role auth)"
286
+ elif server_token:
287
+ auth_note = " (bearer-token auth required)"
288
+ else:
289
+ auth_note = ""
270
290
  print(f"Listening on http://{host}:{port}{auth_note}")
271
291
 
272
292
  def _graceful(signum, frame) -> None: