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
18 changes: 16 additions & 2 deletions engraphis/backends/codegraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

import fnmatch
import hashlib
import logging
import os
import re
from dataclasses import dataclass, field
Expand Down Expand Up @@ -662,8 +663,21 @@ def supports(self, lang: str) -> bool:
return self._primary.supports(lang) or self._fallback.supports(lang)

def index_file(self, file_path: str, content: str, lang: str) -> FileIndex:
idx = self._primary if self._primary.supports(lang) else self._fallback
return idx.index_file(file_path, content, lang)
if self._primary.supports(lang):
try:
return self._primary.index_file(file_path, content, lang)
except Exception:
# The AST backend claimed support but failed at runtime — e.g. a
# tree-sitter grammar that lazily downloads its binary and can't
# reach the network. Fall back to the dependency-free regex indexer
# rather than silently producing zero symbols for this file.
if self._fallback.supports(lang):
logging.getLogger(__name__).debug(
"AST indexer failed for %s (%s); falling back to regex",
file_path, lang, exc_info=True)
return self._fallback.index_file(file_path, content, lang)
raise
return self._fallback.index_file(file_path, content, lang)


def get_code_indexer(prefer: str = "auto"):
Expand Down
14 changes: 10 additions & 4 deletions engraphis/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,18 @@ def _env(key: str, default: str = "") -> str:
return os.environ.get(key, default).strip()

def _validate_service_mode(value: str) -> str:
"""Validate service mode against allowed values, defaulting to combined on invalid input."""
"""Validate service mode against allowed values.

An explicitly-set invalid value exits the process rather than silently falling back
to "combined" — a typo'd ENGRAPHIS_SERVICE_MODE silently becoming "combined" would
merge the vendor and customer trust domains on a misconfigured deploy. Unset (the
caller's own default of "combined") is always valid and never reaches this branch."""
normalized = (value or "").strip().lower()
if normalized not in SERVICE_MODES:
print(f"[engraphis] invalid ENGRAPHIS_SERVICE_MODE '{value}', using 'combined'",
file=sys.stderr)
return "combined"
print(f"[engraphis] invalid ENGRAPHIS_SERVICE_MODE '{value}' "
f"(expected one of {', '.join(SERVICE_MODES)}); refusing to start with an "
f"ambiguous trust boundary.", file=sys.stderr)
sys.exit(1)
return normalized


Expand Down
31 changes: 31 additions & 0 deletions engraphis/core/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import hashlib
import json
import os
import re
import sqlite3
import threading
Expand Down Expand Up @@ -447,6 +448,33 @@ def __init__(self, path: str = ":memory:", *,
)
self.init_schema()

def _backup_before_v4_migration(self) -> None:
"""Best-effort snapshot of the database file before running the v4
canonicalization/edge-support backfills below.

Uses SQLite's own online backup API via a second connection to the same file,
which is safe to run against a live WAL-mode database. This must never block or
fail startup — an upgrade that can't snapshot itself should still proceed rather
than refuse to start, so every failure here is swallowed silently."""
try:
if self.path in (":memory:", "") or self.path.startswith("file::memory:"):
return
backup_path = f"{self.path}.pre-migration-v4.bak"
if os.path.exists(backup_path):
return # already have one from a prior attempt; don't overwrite it
src = sqlite3.connect(self.path)
try:
dst = sqlite3.connect(backup_path)
try:
src.backup(dst)
dst.execute("PRAGMA quick_check")
finally:
dst.close()
finally:
src.close()
except Exception:
pass

# ── schema ──────────────────────────────────────────────────────────────
def init_schema(self) -> None:
previous_version = 0
Expand Down Expand Up @@ -503,6 +531,9 @@ def init_schema(self) -> None:
# v4 makes canonical identity and edge evidence explicit and indexed. Run the
# backfills before creating representative-only uniqueness indexes so exact
# normalized aliases can safely converge onto one deterministic canonical id.
# A real upgrade (not a fresh DB) gets a best-effort pre-migration snapshot first.
if 1 <= previous_version < 4:
self._backup_before_v4_migration()
self._backfill_entity_canonicalization()
self.conn.executescript(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_entity_workspace_canonical "
Expand Down
23 changes: 21 additions & 2 deletions engraphis/inspector/license_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,10 @@ async def team_invite(request: Request):
#: How long a magic link stays valid. Long enough to go check an inbox, short enough
#: that an unclicked link isn't a standing liability sitting in the DB.
_TRIAL_TOKEN_TTL_SECONDS = 1800
# Confirmed/claimed trial_claims rows are otherwise kept forever (no natural
# expiry sweep touches them once confirmed_at is set), so the table grows
# unbounded under real traffic. Mirrors the trial_pending retention sweep below.
_TRIAL_CLAIM_RETENTION_SECONDS = 30 * 24 * 3600 # 30 days past expires_at

#: How long an ALREADY-EXPIRED pending row is kept before it is swept (see the sweep in
#: :func:`_reserve_trial`). Deleting expired rows the instant they lapse would bound the
Expand Down Expand Up @@ -1482,6 +1486,13 @@ async def start_team_trial(request: Request):
#: links carry the secret in a URL fragment (never sent in HTTP/access logs), clear it
#: immediately in the browser, and submit it only in a bounded JSON body. A hash-pinned
#: inline script is the sole executable content; no caller value is interpolated here.
_TRIAL_JSON_KEY_HEADERS = {
"Cache-Control": "no-store, no-cache, must-revalidate, private",
"Pragma": "no-cache",
"Referrer-Policy": "no-referrer",
}


_TRIAL_PAGE_HEADERS = {
"Cache-Control": "no-store, no-cache, must-revalidate, private",
"Pragma": "no-cache",
Expand Down Expand Up @@ -1677,6 +1688,12 @@ def _reserve_trial_claim(machine_id: str, email: str, plan: str,
"(deployment_hash=? OR machine_id=? OR email=?)",
(now, deployment_hash, machine_id, email),
)
# Global retention sweep: bound the table's growth regardless of confirmation
# state. Generous window past expiry so a legitimately delayed claim still works.
conn.execute(
"DELETE FROM trial_claims WHERE expires_at < ?",
(now - _TRIAL_CLAIM_RETENTION_SECONDS,),
)
existing = conn.execute(
"SELECT * FROM trial_claims WHERE deployment_hash=? OR machine_id=? "
"OR email=? LIMIT 1", (deployment_hash, machine_id, email)).fetchone()
Expand Down Expand Up @@ -1944,5 +1961,7 @@ async def claim_trial_license(claim_id: str, request: Request):
conn.commit()
finally:
conn.close()
return {"claim_id": claim_id, "status": "ready", "ready": True,
"key": row["license_key"]}
return JSONResponse(
{"claim_id": claim_id, "status": "ready", "ready": True,
"key": row["license_key"]},
headers=_TRIAL_JSON_KEY_HEADERS)
41 changes: 34 additions & 7 deletions engraphis/licensing.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,20 +175,41 @@ def ed25519_verify(public: bytes, message: bytes, signature: bytes) -> bool:
DEFAULT_UPGRADE_URL = "https://buy.polar.sh/polar_cl_n6CR3ERqOus2VUhRrGrsRUqOB8yjDTeEU7p1r3CRrae"
DEFAULT_PRO_UPGRADE_URL = DEFAULT_UPGRADE_URL
DEFAULT_TEAM_UPGRADE_URL = DEFAULT_UPGRADE_URL
#: Informational landing page shown instead of the live checkout until the vendor side
#: (signer rotation, Railway env, Polar/Resend wiring) is fully live. Without this gate a
#: free 1.0.0 launch's "Buy Pro"/"Buy Team" button hits a LIVE Polar checkout and can
#: charge a customer before a license can actually be fulfilled.
DEFAULT_COMING_SOON_URL = "https://engraphis.com/"


def _paid_available() -> bool:
"""Master switch for the free-vs-paid launch split.

False (the default) routes upgrade links to the informational coming-soon page
instead of the live Polar checkout, so enabling real charges is an explicit,
reviewed step (``ENGRAPHIS_PAID_AVAILABLE=1``) rather than an accidental default."""
return os.environ.get("ENGRAPHIS_PAID_AVAILABLE", "").strip().lower() in (
"1", "true", "yes")


def upgrade_url(plan: Optional[str] = None) -> str:
"""The URL a user should visit to buy ``plan`` (defaults to the Pro/general link).

Env-configurable and never empty: ``ENGRAPHIS_TEAM_UPGRADE_URL`` for Team,
``ENGRAPHIS_PRO_UPGRADE_URL`` (or the legacy ``ENGRAPHIS_UPGRADE_URL``) for Pro."""
``ENGRAPHIS_PRO_UPGRADE_URL`` (or the legacy ``ENGRAPHIS_UPGRADE_URL``) for Pro. An
explicit override always wins; otherwise this routes to the coming-soon page unless
``ENGRAPHIS_PAID_AVAILABLE=1`` (see :func:`_paid_available`)."""
if (plan or "").lower() == "team":
return (os.environ.get("ENGRAPHIS_TEAM_UPGRADE_URL", "").strip()
or os.environ.get("ENGRAPHIS_UPGRADE_URL", "").strip()
or DEFAULT_TEAM_UPGRADE_URL)
return (os.environ.get("ENGRAPHIS_PRO_UPGRADE_URL", "").strip()
or os.environ.get("ENGRAPHIS_UPGRADE_URL", "").strip()
or DEFAULT_PRO_UPGRADE_URL)
override = (os.environ.get("ENGRAPHIS_TEAM_UPGRADE_URL", "").strip()
or os.environ.get("ENGRAPHIS_UPGRADE_URL", "").strip())
if override:
return override
return DEFAULT_TEAM_UPGRADE_URL if _paid_available() else DEFAULT_COMING_SOON_URL
override = (os.environ.get("ENGRAPHIS_PRO_UPGRADE_URL", "").strip()
or os.environ.get("ENGRAPHIS_UPGRADE_URL", "").strip())
if override:
return override
return DEFAULT_PRO_UPGRADE_URL if _paid_available() else DEFAULT_COMING_SOON_URL

_KEY_PREFIX = "ENGR1"
# Pinned Ed25519 verifier (32-byte public half). This pre-sale key was generated on a
Expand Down Expand Up @@ -982,6 +1003,12 @@ def production_warnings() -> list:
warns.append(
"upgrade link still points at the GitHub pricing anchor, not a real checkout. "
"Set ENGRAPHIS_UPGRADE_URL to your checkout page URL before charging.")
if _paid_available() and not VENDOR_SIGNER_RELEASE_READY:
warns.append(
"ENGRAPHIS_PAID_AVAILABLE is set but VENDOR_SIGNER_RELEASE_READY is still "
"False — customers could be charged via the live checkout before licenses "
"can be issued. Complete the signer rotation ceremony before enabling paid "
"sales.")
return warns


Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ mcp = [
# fall back to a dependency-free regex indexer without this — see backends/codegraph.py.
code = [
"tree-sitter>=0.23",
"tree-sitter-language-pack>=0.2",
"tree-sitter-language-pack==0.9.0",
]
# Local resource extraction. Text/code/HTML/DOCX remain stdlib-only; this adds PDF
# extraction and image OCR bindings (the Tesseract executable is installed separately).
Expand Down Expand Up @@ -119,7 +119,7 @@ all = [
"pydantic-settings>=2.14.2; python_version >= '3.10'",
"cryptography>=48.0.1; python_version >= '3.10'",
"tree-sitter>=0.23",
"tree-sitter-language-pack>=0.2",
"tree-sitter-language-pack==0.9.0",
"pypdf>=4.0",
"Pillow>=12.3.0; python_version >= '3.10'",
"pytesseract>=0.3.10",
Expand Down Expand Up @@ -148,7 +148,7 @@ test = [
"pydantic-settings>=2.14.2; python_version >= '3.10'",
"cryptography>=48.0.1; python_version >= '3.10'",
"tree-sitter>=0.23",
"tree-sitter-language-pack>=0.2",
"tree-sitter-language-pack==0.9.0",
"pypdf>=4.0",
"Pillow>=12.3.0; python_version >= '3.10'",
"pytesseract>=0.3.10",
Expand Down
8 changes: 6 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
instead of only in code. The default must stay empty so the offline/numpy-only CI path is
unchanged (empty -> None -> IdentityReranker, no torch).
"""
import pytest

from engraphis import config
from engraphis.config import Settings

Expand Down Expand Up @@ -103,9 +105,11 @@ def test_retired_cloud_url_override_is_canonicalized(monkeypatch):
def test_retired_relay_url_override_is_canonicalized():
assert config.canonicalize_relay_url(RETIRED_RELAY_URL) == config.DEFAULT_RELAY_URL

def test_invalid_service_mode_falls_back_to_combined(monkeypatch):
def test_invalid_service_mode_exits_process(monkeypatch):
"""Invalid ENGRAPHIS_SERVICE_MODE must fail-closed (sys.exit), not silently fall back."""
monkeypatch.setenv("ENGRAPHIS_SERVICE_MODE", "bogus")
assert Settings().service_mode == "combined"
with pytest.raises(SystemExit):
Settings()


def test_valid_service_modes_accepted(monkeypatch):
Expand Down
14 changes: 12 additions & 2 deletions tests/test_licensing.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def test_bad_env_key_degrades_to_free_with_reason(monkeypatch):


def test_require_feature_message_is_actionable():
with pytest.raises(LicenseError, match="polar.sh"):
with pytest.raises(LicenseError, match="engraphis.com"):
require_feature("analytics")


Expand Down Expand Up @@ -238,8 +238,18 @@ def test_required_plan_maps_features_to_cheapest_tier():
assert lic.required_plan("unknown-flag") == "team"


def test_upgrade_url_default_is_the_polar_checkout(monkeypatch):
def test_upgrade_url_default_is_coming_soon_until_paid_available(monkeypatch):
"""Without ENGRAPHIS_PAID_AVAILABLE=1, upgrade_url routes to the informational page."""
monkeypatch.delenv("ENGRAPHIS_UPGRADE_URL", raising=False)
monkeypatch.delenv("ENGRAPHIS_PAID_AVAILABLE", raising=False)
assert lic.upgrade_url() == lic.DEFAULT_COMING_SOON_URL
assert "engraphis.com" in lic.upgrade_url()


def test_upgrade_url_routes_to_polar_when_paid_available(monkeypatch):
"""With ENGRAPHIS_PAID_AVAILABLE=1, upgrade_url routes to the live checkout."""
monkeypatch.delenv("ENGRAPHIS_UPGRADE_URL", raising=False)
monkeypatch.setenv("ENGRAPHIS_PAID_AVAILABLE", "1")
assert lic.upgrade_url() == lic.DEFAULT_UPGRADE_URL
assert "polar.sh" in lic.upgrade_url()

Expand Down