From bd130ce50bc1f9a917a5102f7ff3a10d6da704fd Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Tue, 21 Jul 2026 09:00:47 +0000 Subject: [PATCH 1/2] fix: GA-launch hardening pass (tree-sitter pin, service-mode fail-closed, pre-migration DB backup, trial-claims retention+headers, AST-fallback resilience, paid coming-soon gate) Independent fixes, no secrets/infra involved: - pyproject.toml: pin tree-sitter-language-pack==0.9.0. 1.x lazily downloads grammar binaries from a GitHub release at import time, which 403s offline and makes index_repo silently index 0 files. - backends/codegraph.py: CompositeSymbolIndexer.index_file now falls back to the regex indexer if the AST backend raises at runtime (e.g. the above), instead of propagating the failure or silently producing nothing. - config.py: an explicitly-set invalid ENGRAPHIS_SERVICE_MODE now exits the process instead of silently coercing to combined, which would merge the vendor and customer trust domains on a typo. Unset still defaults to combined. - core/store.py: init_schema takes a best-effort pre-migration snapshot (.pre-migration-v4.bak via SQLite's online backup API) before the v4 canonicalization/edge-support backfills, gated to real upgrades only. - inspector/license_cloud.py: claim_trial_license's response (which carries the raw license key) now sends Cache-Control: no-store + Referrer-Policy: no-referrer; added a global retention sweep on trial_claims so confirmed/claimed rows don't grow the table unbounded (previously only unconfirmed-expired rows were swept). - licensing.py: new ENGRAPHIS_PAID_AVAILABLE gate (default off) routes upgrade_url() to an informational coming-soon page instead of the live Polar checkout, so a free launch's Buy Pro/Team button can't charge a customer before the vendor side is wired. Explicit ENGRAPHIS_*_UPGRADE_URL overrides still always win. Also warns in production_warnings() if paid is enabled while VENDOR_SIGNER_RELEASE_READY is still False. Deliberately NOT touched: VENDOR_SIGNER_RELEASE_READY (business go-live decision, not a bug fix), any Railway/Polar/Resend env vars or DNS (require account access I don't have), and the NOTICE/CHANGELOG/SECURITY wording pass (needs verification against actual current bundling status I haven't confirmed). --- engraphis/backends/codegraph.py | 18 ++++++++++-- engraphis/config.py | 14 +++++++--- engraphis/core/store.py | 31 +++++++++++++++++++++ engraphis/inspector/license_cloud.py | 23 ++++++++++++++-- engraphis/licensing.py | 41 +++++++++++++++++++++++----- pyproject.toml | 6 ++-- 6 files changed, 115 insertions(+), 18 deletions(-) diff --git a/engraphis/backends/codegraph.py b/engraphis/backends/codegraph.py index 8265b37..669997a 100644 --- a/engraphis/backends/codegraph.py +++ b/engraphis/backends/codegraph.py @@ -45,6 +45,7 @@ import fnmatch import hashlib +import logging import os import re from dataclasses import dataclass, field @@ -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"): diff --git a/engraphis/config.py b/engraphis/config.py index 5ad0ee0..d96d08c 100644 --- a/engraphis/config.py +++ b/engraphis/config.py @@ -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 diff --git a/engraphis/core/store.py b/engraphis/core/store.py index bde8f94..e9fff6d 100644 --- a/engraphis/core/store.py +++ b/engraphis/core/store.py @@ -12,6 +12,7 @@ import hashlib import json +import os import re import sqlite3 import threading @@ -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 @@ -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 " diff --git a/engraphis/inspector/license_cloud.py b/engraphis/inspector/license_cloud.py index a133170..33f3bdc 100644 --- a/engraphis/inspector/license_cloud.py +++ b/engraphis/inspector/license_cloud.py @@ -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 @@ -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", @@ -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() @@ -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) diff --git a/engraphis/licensing.py b/engraphis/licensing.py index 078b48f..63a34be 100644 --- a/engraphis/licensing.py +++ b/engraphis/licensing.py @@ -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 @@ -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 diff --git a/pyproject.toml b/pyproject.toml index d43a019..6fff6e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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). @@ -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", @@ -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", From 7585b047d34cef7000f1c3b2ca92fc5dc0622a95 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Tue, 21 Jul 2026 05:20:02 -0400 Subject: [PATCH 2/2] fix(tests): align 3 tests with GA-launch hardening behavior changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_config: service_mode now fail-closed (sys.exit) instead of silent fallback to 'combined' — test updated to expect SystemExit - test_licensing: upgrade_url defaults to coming-soon page (engraphis.com) until ENGRAPHIS_PAID_AVAILABLE=1 — split into two tests covering both paths; require_feature message match updated from polar.sh to engraphis.com Fixes CI failures on PR #39 (all 4 jobs: core-floor 3.9 + full-stack 3.10/3.11/3.12) --- tests/test_config.py | 8 ++++++-- tests/test_licensing.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 5b1d364..2185a4f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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 @@ -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): diff --git a/tests/test_licensing.py b/tests/test_licensing.py index 20f0a0f..7b2b5b8 100644 --- a/tests/test_licensing.py +++ b/tests/test_licensing.py @@ -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") @@ -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()