Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ ENGRAPHIS_API_TOKEN=
# checkout or the platform user-data directory from an installed wheel.
# ENGRAPHIS_DB_PATH=./engraphis.db

# Code indexing accepts roots under the working, home, or temporary directories by
# default. Set this to replace those defaults with an explicit absolute-path allow-list.
# Separate paths with ";" on Windows or ":" on macOS/Linux.
# ENGRAPHIS_INDEX_ROOTS=/srv/engraphis/repos:/mnt/code
# The dashboard and REST /api/code/index route accept paths only below this one root.
# An explicit root (or the ENGRAPHIS_INDEX_ROOTS fallback entry) must be absolute.
# Defaults to the first ENGRAPHIS_INDEX_ROOTS entry, or the current directory; an
# explicit HTTP root is included in the engine-approved set.
# ENGRAPHIS_HTTP_INDEX_ROOT=/srv/engraphis/repos

# ── Encryption at rest (optional; SQLCipher / AES-256) ──────────────────────
# Transparently encrypt the ENTIRE memory database file. OFF by default. Needs the extra:
# pip install "engraphis[encryption]"
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ jobs:
analyze:
name: Analyze ${{ matrix.language }}
runs-on: ubuntu-latest
# Pull-request analyses otherwise use diff-informed queries, whose SARIF
# intentionally omits findings outside changed lines. The release gate
# must inspect complete raw SARIF on both pull requests and pushes.
env:
CODEQL_ACTION_DIFF_INFORMED_QUERIES: "false"
strategy:
fail-fast: false
matrix:
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ Public 1.0.1 client reliability release.
- API error responses and provider logs no longer expose arbitrary exception or configuration
text; local folder and repository reads resolve and re-check filesystem boundaries.
- Entity extraction and dashboard asset migration avoid adversarial regular-expression
backtracking, and CI now fails when CodeQL emits any SARIF finding.
backtracking. CodeQL now disables pull-request diff-informed analysis and CI fails on every
raw SARIF result, including pre-existing and source-suppressed results.

## [1.0.0] - 2026-07-23

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,8 @@ All via environment (or `.env`):
| `ENGRAPHIS_API_TOKEN` | — | Optional bearer credential for this single-user local customer node; never reuse a hosted credential |
| `ENGRAPHIS_CORS_ORIGINS` | loopback on `ENGRAPHIS_PORT` | Comma-separated REST CORS allow-list; defaults to `127.0.0.1` and `localhost` on the configured port |
| `ENGRAPHIS_WORKSPACES` | — | Optional comma-separated server-side workspace allow-list |
| `ENGRAPHIS_INDEX_ROOTS` | Working, home, and temporary directories | Optional path-separator-delimited absolute-path allow-list that replaces the default roots accepted by local code indexing |
| `ENGRAPHIS_HTTP_INDEX_ROOT` | First `ENGRAPHIS_INDEX_ROOTS` entry, or current directory | Single root for dashboard and REST `POST /api/code/index`; submitted paths resolve beneath it. An explicit root (or fallback entry) must be absolute; an explicit HTTP root is included in the engine-approved set. MCP and CLI indexing continue to use `ENGRAPHIS_INDEX_ROOTS`. |
| `ENGRAPHIS_DB_KEY` | — | Encrypt the database at rest (SQLCipher). Or use `ENGRAPHIS_DB_KEY_FILE` |
| `ENGRAPHIS_EMBED_MODEL` | `sentence-transformers/all-MiniLM-L6-v2` | sentence-transformers model |
| `ENGRAPHIS_EXTRACTOR` | `none` | `none` = verbatim; `chunk` = offline structure-aware chunks; `llm` = free-form LLM facts; `llm_structured` = schema-validated facts + graph metadata |
Expand Down
15 changes: 12 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,18 @@ DOMPurify at all render sites. Verified against payloads with `onerror` handlers
### 5. Code indexing
`engraphis_index_repo` parses source files under a path you give it — same trust boundary as
any other local tool the agent has. Path is attacker-controlled if agent's instructions are.
`max_files`/`max_file_bytes` bound resource use, not access scope. Traversal does not follow
file symlinks outside the root, prunes dependency/build directories during the walk, and honors
the root `.engraphisignore` without allowing negation rules to re-expose hardcoded excludes.
Canonical roots are restricted to the working, home, or temporary directories by default.
Set `ENGRAPHIS_INDEX_ROOTS` to a path-separator-delimited absolute-path operator allow-list to
replace those defaults for nonstandard mounts or a narrower deployment boundary.
Dashboard and REST `POST /api/code/index` use the stricter single-root boundary
`ENGRAPHIS_HTTP_INDEX_ROOT`: submitted paths resolve beneath that root. It defaults to the first
`ENGRAPHIS_INDEX_ROOTS` entry, or the current directory. An explicit HTTP root (or fallback
entry) must be absolute; an explicit HTTP root is included in the engine-approved set. MCP and
CLI indexing retain the `ENGRAPHIS_INDEX_ROOTS` allow-list semantics.
`max_files`/`max_file_bytes` bound resource use, not access within an allowed root. Traversal
does not follow file symlinks outside the root, prunes dependency/build directories during the
walk, and honors the root `.engraphisignore` without allowing negation rules to re-expose
hardcoded excludes.
Anyone who can reach an authenticated local mutation route has the authority of that local
installation, so do not share its bearer token.

Expand Down
5 changes: 2 additions & 3 deletions engraphis/backends/embedder_deterministic.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,12 @@ def dim(self) -> int:
def embed(self, texts: list[str], *, kind: Literal["text", "code"] = "text") -> np.ndarray:
out = np.zeros((len(texts), self._dim), dtype=np.float32)
for i, text in enumerate(texts):
for token in _tokenize(text, kind):
for feature in _tokenize(text, kind):
# SHA-1 is retained only to keep the established offline embedding
# mapping stable across upgrades. This is feature hashing, never a
# password, signature, integrity check, or other security primitive.
# codeql[py/weak-sensitive-data-hashing]
h = hashlib.sha1(
token.encode("utf-8"), usedforsecurity=False
feature.encode("utf-8"), usedforsecurity=False
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
).digest()
idx = int.from_bytes(h[:4], "big") % self._dim
sign = 1.0 if h[4] & 1 else -1.0
Expand Down
71 changes: 66 additions & 5 deletions engraphis/core/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import json
import logging
import math
import os
import re
import tempfile
import threading
import time
from collections import defaultdict, deque
Expand Down Expand Up @@ -78,6 +80,44 @@
CODE_EXPORT_MAX_LIMIT = 20_000


def _approved_local_index_roots() -> tuple[str, ...]:
"""Return canonical roots available to the explicit local indexing capability.

``ENGRAPHIS_INDEX_ROOTS`` is an operator-owned, path-separator-delimited allow-list.
``ENGRAPHIS_HTTP_INDEX_ROOT`` is a separately configured, single HTTP boundary;
include it here too so the checked path handed off by the HTTP route remains usable
by the engine. Configured paths must be absolute: silently resolving a relative
operator setting against the process working directory would make the boundary
deployment-dependent.
When it is unset, the working directory, home directory, and system temporary
directory preserve the local-first defaults used by ordinary agent/project checkouts.
"""
def canonical_configured_root(value: str, setting: str) -> str:
if not os.path.isabs(value):
raise ValueError(f"{setting} must contain only absolute paths")
return os.path.normcase(os.path.realpath(os.path.expanduser(value)))

configured = [
canonical_configured_root(value.strip(), "ENGRAPHIS_INDEX_ROOTS")
for value in os.environ.get("ENGRAPHIS_INDEX_ROOTS", "").split(os.pathsep)
if value.strip()
]
if configured:
roots = configured
else:
roots = [
os.path.normcase(os.path.realpath(os.getcwd())),
os.path.normcase(os.path.realpath(os.path.expanduser("~"))),
os.path.normcase(os.path.realpath(tempfile.gettempdir())),
]

http_root = os.environ.get("ENGRAPHIS_HTTP_INDEX_ROOT", "").strip()
if http_root:
roots.append(canonical_configured_root(http_root, "ENGRAPHIS_HTTP_INDEX_ROOT"))

return tuple(dict.fromkeys(roots))


def _bounded_finite(value, *, default: float, minimum: float, maximum: float) -> float:
try:
number = float(value)
Expand Down Expand Up @@ -1228,11 +1268,32 @@ def index_repo(self, repo_id: str, root_path: str, *, languages: Optional[set] =
)

indexer = get_code_indexer(prefer=prefer)
# index_repo is an explicit local-filesystem capability: callers select the
# repository root to index. Every walked child is resolved and re-contained
# below before it is read.
# codeql[py/path-injection]
root = Path(root_path).expanduser().resolve()
# index_repo is an explicit local-filesystem capability: callers select a
# repository in one of the approved local roots. Canonicalizing then checking
# containment confines the capability to the operator's configured/default
# filesystem boundary. Every walked child is re-contained below before it is read.
# Normalize before the root-prefix check. Keep this check at the filesystem
# call site (rather than hiding it in a helper) so both human review and static
# analysis can establish that every later path is constrained by this boundary.
canonical_root = os.path.normcase(
os.path.realpath(os.path.expanduser(os.fspath(root_path)))
)
# Retain a separator on the checked value. This makes an approved root
# itself and every child use the same normalized-prefix check (and avoids
# accepting a sibling such as ``/work/repo-copy`` for ``/work/repo``).
# It also keeps the checked value, rather than the untrusted input, as
# the only path passed to filesystem APIs below.
canonical_root_with_sep = canonical_root.rstrip(os.sep) + os.sep
safe_root: Optional[str] = None
for approved_root in _approved_local_index_roots():
normalized_approved = os.path.normcase(os.path.realpath(approved_root))
approved_prefix = normalized_approved.rstrip(os.sep) + os.sep
if canonical_root_with_sep.startswith(approved_prefix):
safe_root = canonical_root_with_sep
break
if safe_root is None:
raise ValueError("repo root is outside approved local roots")
root = Path(safe_root)
if not root.exists():
raise ValueError(f"repo root not found: {root_path}")
if not root.is_dir():
Expand Down
8 changes: 5 additions & 3 deletions engraphis/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,8 +715,8 @@ def engraphis_index_repo(
repo: Annotated[str, Field(description="Repo name to index.", min_length=1,
max_length=200)],
root_path: Annotated[str, Field(description="Local filesystem path to the repo root "
"to parse (e.g. '/home/user/projects/myrepo'). Reads files from "
"this path the same way any local tool you have would.",
"to parse (e.g. '/home/user/projects/myrepo'). The path must be "
"inside the local defaults or ENGRAPHIS_INDEX_ROOTS allow-list.",
min_length=1, max_length=4_000)],
languages: Annotated[Optional[List[str]], Field(description="Restrict to these "
"languages (e.g. ['python','csharp']). Names are normalised "
Expand All @@ -739,7 +739,9 @@ def engraphis_index_repo(
engraphis_remember). Re-indexing is safe to call again; each file's symbols are
replaced, not duplicated. Reads files from ``root_path`` on the local filesystem —
the same trust boundary as any other local tool you have, nothing is sent anywhere.
Each completed scan appends a fresh operation receipt, so the MCP call is
Set ``ENGRAPHIS_INDEX_ROOTS`` to a path-separator-delimited absolute-path allow-list when
repositories live outside the working, home, or temporary directories, or to narrow the
defaults. Each completed scan appends a fresh operation receipt, so the MCP call is
non-idempotent even when the code graph itself is unchanged.

Returns:
Expand Down
Loading
Loading