From 1325edbffe7e1a0ed7acc28798799d23c8fe9cff Mon Sep 17 00:00:00 2001 From: heeyeon Date: Fri, 17 Apr 2026 12:05:05 +0900 Subject: [PATCH 01/23] feat(vault): make eval_mode configurable via ENVECTOR_EVAL_MODE env var (#59) Hardcoded eval_mode="rmp" in vault_core.py prevented connecting to envector-msa >= 1.4.0 which uses MM (Multi-Multiplication) mode. Read eval mode from ENVECTOR_EVAL_MODE env var (default: rmp for backward compat with envector.io cloud / 1.2.x deployments). Co-Authored-By: Claude Sonnet 4.6 --- vault/.env.example | 8 ++++++++ vault/docker-compose.yml | 1 + vault/vault_core.py | 5 +++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/vault/.env.example b/vault/.env.example index 42ba518..9f06e18 100644 --- a/vault/.env.example +++ b/vault/.env.example @@ -60,3 +60,11 @@ ENVECTOR_API_KEY= VAULT_INDEX_NAME=runecontext # Embedding dimension (must match your embedding model) EMBEDDING_DIM=1024 + +# ── FHE Evaluation Mode ───────────────────────────────────────── +# Controls the homomorphic encryption evaluation strategy. +# rmp — Relinearization Multiplication Protocol (envector-msa <= 1.2.x, envector.io cloud) +# mm — Multi-Multiplication (envector-msa >= 1.4.0-alpha, local/new deployments) +# IMPORTANT: Keys are mode-specific. Changing this requires deleting vault_keys/ +# and restarting Vault so new keys are generated. +ENVECTOR_EVAL_MODE=rmp diff --git a/vault/docker-compose.yml b/vault/docker-compose.yml index 1b5d598..688f36d 100644 --- a/vault/docker-compose.yml +++ b/vault/docker-compose.yml @@ -25,6 +25,7 @@ services: - ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT:-} - ENVECTOR_API_KEY=${ENVECTOR_API_KEY:-} - EMBEDDING_DIM=${EMBEDDING_DIM:-1024} + - ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE:-rmp} - VAULT_AUDIT_LOG=${VAULT_AUDIT_LOG:-file} networks: diff --git a/vault/vault_core.py b/vault/vault_core.py index 3556abc..84b996f 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -37,6 +37,7 @@ ENVECTOR_ENDPOINT = os.getenv("ENVECTOR_ENDPOINT", "").strip() or None ENVECTOR_API_KEY = os.getenv("ENVECTOR_API_KEY", "").strip() or None EMBEDDING_DIM = int(os.getenv("EMBEDDING_DIM", "1024")) +EVAL_MODE = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() # Team index name (set by admin, distributed to all team members via get_public_key) VAULT_INDEX_NAME = os.getenv("VAULT_INDEX_NAME", "").strip() or None @@ -76,7 +77,7 @@ def ensure_vault(): key_path=KEY_DIR, key_id=KEY_ID, dim=EMBEDDING_DIM, - eval_mode="rmp", + eval_mode=EVAL_MODE, auto_key_setup=True, access_token=ENVECTOR_API_KEY, query_encryption="plain", @@ -90,7 +91,7 @@ def ensure_vault(): key_path=KEY_DIR, key_id=KEY_ID, dim=EMBEDDING_DIM, - eval_mode="rmp", + eval_mode=EVAL_MODE, auto_key_setup=False, access_token=ENVECTOR_API_KEY, query_encryption="plain", From 0c0e392e9c6588ca33e2f808a2b0c6dac910fe8f Mon Sep 17 00:00:00 2001 From: heeyeon Date: Mon, 20 Apr 2026 12:43:00 +0900 Subject: [PATCH 02/23] test(vault): verify ENVECTOR_EVAL_MODE env var behavior (#59) Co-Authored-By: Claude Sonnet 4.6 --- tests/unit/test_eval_mode.py | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 tests/unit/test_eval_mode.py diff --git a/tests/unit/test_eval_mode.py b/tests/unit/test_eval_mode.py new file mode 100644 index 0000000..70c47bf --- /dev/null +++ b/tests/unit/test_eval_mode.py @@ -0,0 +1,145 @@ +""" +Unit tests for ENVECTOR_EVAL_MODE configuration (PR #59 test plan). + +Test plan coverage: +- Item 1: EVAL_MODE defaults to "rmp" when env var is not set +- Item 2: ev.init() is called with "mm" when ENVECTOR_EVAL_MODE=mm +- Item 3: invalid EVAL_MODE propagates to SDK (no vault-level validation guard) +""" +import sys +import os +import importlib +from unittest.mock import MagicMock, patch + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../vault")) + +import vault_core + + +class TestEvalModeEnvVar: + """Test plan item 1 & 2: EVAL_MODE env var reading.""" + + def test_eval_mode_reflects_env_at_load_time(self): + """EVAL_MODE at module level matches what os.getenv would return.""" + expected = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() + assert vault_core.EVAL_MODE == expected + + def test_eval_mode_is_lowercased(self, monkeypatch): + """EVAL_MODE applies .lower() so casing doesn't matter.""" + monkeypatch.setattr(vault_core, "EVAL_MODE", "RMP") + # Simulate what the module does: os.getenv(...).lower() + assert vault_core.EVAL_MODE.lower() == "rmp" + + def test_eval_mode_default_is_rmp_when_env_unset(self, monkeypatch): + """When ENVECTOR_EVAL_MODE is not set, the formula produces 'rmp'.""" + monkeypatch.delenv("ENVECTOR_EVAL_MODE", raising=False) + result = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() + assert result == "rmp" + + def test_eval_mode_reads_mm_from_env(self, monkeypatch): + """When ENVECTOR_EVAL_MODE=MM, the formula produces 'mm'.""" + monkeypatch.setenv("ENVECTOR_EVAL_MODE", "MM") + result = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() + assert result == "mm" + + +class TestEvalModePassedToEvInit: + """Test plan item 2 & 3: ev.init() receives EVAL_MODE correctly.""" + + def _setup_offline_patches(self, monkeypatch, tmp_path, *, eval_mode: str): + """Patch vault_core module vars to trigger Phase 2 (cloud init).""" + key_dir = tmp_path / "vault-key" + key_dir.mkdir() + (key_dir / "EncKey.json").write_text("{}") + + monkeypatch.setattr(vault_core, "EVAL_MODE", eval_mode) + monkeypatch.setattr(vault_core, "ENVECTOR_ENDPOINT", "grpc://test:50051") + monkeypatch.setattr(vault_core, "ENVECTOR_API_KEY", "test-key") + monkeypatch.setattr(vault_core, "EMBEDDING_DIM", 1024) + monkeypatch.setattr(vault_core, "KEY_DIR", str(tmp_path)) + monkeypatch.setattr(vault_core, "KEY_SUBDIR", str(key_dir)) + monkeypatch.setattr(vault_core, "KEY_ID", "vault-key") + monkeypatch.setattr(vault_core, "VAULT_INDEX_NAME", None) + + def _patch_pyenvector(self, mock_ev: MagicMock): + """Replace pyenvector in sys.modules with a mock.""" + original = sys.modules.get("pyenvector") + sys.modules["pyenvector"] = mock_ev + return original + + def _restore_pyenvector(self, original): + if original is not None: + sys.modules["pyenvector"] = original + else: + sys.modules.pop("pyenvector", None) + + def test_ensure_vault_passes_eval_mode_mm_to_ev_init(self, monkeypatch, tmp_path): + """ensure_vault() calls ev.init(eval_mode='mm') when EVAL_MODE is 'mm'.""" + self._setup_offline_patches(monkeypatch, tmp_path, eval_mode="mm") + + mock_ev = MagicMock() + original = self._patch_pyenvector(mock_ev) + try: + vault_core.ensure_vault() + finally: + self._restore_pyenvector(original) + + mock_ev.init.assert_called_once_with( + address="grpc://test:50051", + key_path=str(tmp_path), + key_id="vault-key", + dim=1024, + eval_mode="mm", + auto_key_setup=True, + access_token="test-key", + query_encryption="plain", + ) + + def test_ensure_vault_passes_eval_mode_rmp_to_ev_init(self, monkeypatch, tmp_path): + """ensure_vault() calls ev.init(eval_mode='rmp') when EVAL_MODE is 'rmp'.""" + self._setup_offline_patches(monkeypatch, tmp_path, eval_mode="rmp") + + mock_ev = MagicMock() + original = self._patch_pyenvector(mock_ev) + try: + vault_core.ensure_vault() + finally: + self._restore_pyenvector(original) + + mock_ev.init.assert_called_once_with( + address="grpc://test:50051", + key_path=str(tmp_path), + key_id="vault-key", + dim=1024, + eval_mode="rmp", + auto_key_setup=True, + access_token="test-key", + query_encryption="plain", + ) + + def test_invalid_eval_mode_propagates_to_sdk_not_vault(self, monkeypatch, tmp_path): + """Invalid EVAL_MODE passes through to the SDK — vault has no allowlist guard. + + Expected behavior: + - ev.init(auto_key_setup=True) raises (SDK rejects "invalid") + - vault logs warning, retries with auto_key_setup=False + - second ev.init also raises + - ensure_vault() propagates the exception + """ + self._setup_offline_patches(monkeypatch, tmp_path, eval_mode="invalid") + + mock_ev = MagicMock() + mock_ev.init.side_effect = ValueError("unsupported eval_mode: invalid") + original = self._patch_pyenvector(mock_ev) + try: + with pytest.raises(Exception): + vault_core.ensure_vault() + finally: + self._restore_pyenvector(original) + + # Both retry attempts use the same invalid mode → 2 calls total + assert mock_ev.init.call_count == 2 + for call_args in mock_ev.init.call_args_list: + assert call_args.kwargs["eval_mode"] == "invalid" From 83569369a08d974d325bd9dff44fbbd1806fc9ea Mon Sep 17 00:00:00 2001 From: heeyeon Date: Mon, 20 Apr 2026 12:46:00 +0900 Subject: [PATCH 03/23] feat(vault): add allowlist guard for ENVECTOR_EVAL_MODE (#59) Raise ValueError at startup if ENVECTOR_EVAL_MODE is not one of the supported values {"rmp", "mm"}, so misconfiguration fails fast with a clear message instead of propagating an opaque SDK error. Co-Authored-By: Claude Sonnet 4.6 --- tests/unit/test_eval_mode.py | 19 ++++--------------- vault/vault_core.py | 8 ++++++++ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/unit/test_eval_mode.py b/tests/unit/test_eval_mode.py index 70c47bf..8a79196 100644 --- a/tests/unit/test_eval_mode.py +++ b/tests/unit/test_eval_mode.py @@ -119,27 +119,16 @@ def test_ensure_vault_passes_eval_mode_rmp_to_ev_init(self, monkeypatch, tmp_pat query_encryption="plain", ) - def test_invalid_eval_mode_propagates_to_sdk_not_vault(self, monkeypatch, tmp_path): - """Invalid EVAL_MODE passes through to the SDK — vault has no allowlist guard. - - Expected behavior: - - ev.init(auto_key_setup=True) raises (SDK rejects "invalid") - - vault logs warning, retries with auto_key_setup=False - - second ev.init also raises - - ensure_vault() propagates the exception - """ + def test_invalid_eval_mode_rejected_at_vault_level(self, monkeypatch, tmp_path): + """Invalid EVAL_MODE is caught by vault's allowlist guard before reaching the SDK.""" self._setup_offline_patches(monkeypatch, tmp_path, eval_mode="invalid") mock_ev = MagicMock() - mock_ev.init.side_effect = ValueError("unsupported eval_mode: invalid") original = self._patch_pyenvector(mock_ev) try: - with pytest.raises(Exception): + with pytest.raises(ValueError, match="Invalid ENVECTOR_EVAL_MODE"): vault_core.ensure_vault() finally: self._restore_pyenvector(original) - # Both retry attempts use the same invalid mode → 2 calls total - assert mock_ev.init.call_count == 2 - for call_args in mock_ev.init.call_args_list: - assert call_args.kwargs["eval_mode"] == "invalid" + mock_ev.init.assert_not_called() diff --git a/vault/vault_core.py b/vault/vault_core.py index 84b996f..62a6fb9 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -37,6 +37,7 @@ ENVECTOR_ENDPOINT = os.getenv("ENVECTOR_ENDPOINT", "").strip() or None ENVECTOR_API_KEY = os.getenv("ENVECTOR_API_KEY", "").strip() or None EMBEDDING_DIM = int(os.getenv("EMBEDDING_DIM", "1024")) +_VALID_EVAL_MODES = {"rmp", "mm"} EVAL_MODE = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() # Team index name (set by admin, distributed to all team members via get_public_key) @@ -51,6 +52,13 @@ def ensure_vault(): (SDK handles key registration → loading) 3. Create the team index if it doesn't exist """ + if EVAL_MODE not in _VALID_EVAL_MODES: + raise ValueError( + f"Invalid ENVECTOR_EVAL_MODE={EVAL_MODE!r}. " + f"Must be one of: {sorted(_VALID_EVAL_MODES)}. " + "If you changed this value, delete vault_keys/ and restart Vault." + ) + import pyenvector as ev # Phase 1: local key generation From 2142ebb51c7028890129093255766b02bf37a4fc Mon Sep 17 00:00:00 2001 From: heeyeon Date: Mon, 20 Apr 2026 13:33:56 +0900 Subject: [PATCH 04/23] docs(vault): simplify ENVECTOR_EVAL_MODE description in .env.example (#59) Remove cloud/deployment-type framing in favor of version-based guidance, which stays accurate as envector-msa versions evolve. Co-Authored-By: Claude Sonnet 4.6 --- vault/.env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vault/.env.example b/vault/.env.example index 9f06e18..406cd8b 100644 --- a/vault/.env.example +++ b/vault/.env.example @@ -63,8 +63,8 @@ EMBEDDING_DIM=1024 # ── FHE Evaluation Mode ───────────────────────────────────────── # Controls the homomorphic encryption evaluation strategy. -# rmp — Relinearization Multiplication Protocol (envector-msa <= 1.2.x, envector.io cloud) -# mm — Multi-Multiplication (envector-msa >= 1.4.0-alpha, local/new deployments) +# rmp — Relinearization Multiplication Protocol (envector-msa <= 1.2.x) +# mm — Multi-Multiplication (envector-msa >= 1.4.0-alpha) # IMPORTANT: Keys are mode-specific. Changing this requires deleting vault_keys/ # and restarting Vault so new keys are generated. ENVECTOR_EVAL_MODE=rmp From 9fdd2160082ec3045703c353eacef169d6ce1a67 Mon Sep 17 00:00:00 2001 From: heeyeon Date: Tue, 21 Apr 2026 16:01:16 +0900 Subject: [PATCH 05/23] feat(install): prompt for ENVECTOR_EVAL_MODE and write to .env (#59) Co-Authored-By: Claude Sonnet 4.6 --- install.sh | 7 +++++++ scripts/install-dev.sh | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/install.sh b/install.sh index 15c8879..f4a3c77 100755 --- a/install.sh +++ b/install.sh @@ -331,6 +331,12 @@ prompt_envector_config() { prompt ENVECTOR_API_KEY "enVector API key (e.g. aBcDE_12345_xxxxx)" prompt VAULT_INDEX_NAME "Index name" "runecontext" + print_step "enVector eval mode" + local eval_selected + eval_selected=$(select_menu "mm" "rmp") + ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm" || echo "rmp") + print_info "Eval mode: ${ENVECTOR_EVAL_MODE}" + if [ -z "$ENVECTOR_ENDPOINT" ] || [ -z "$ENVECTOR_API_KEY" ]; then print_error "enVector endpoint and API key are required." exit 1 @@ -502,6 +508,7 @@ VAULT_TEAM_SECRET=${VAULT_TEAM_SECRET_VALUE} VAULT_INDEX_NAME=${VAULT_INDEX_NAME} ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT} ENVECTOR_API_KEY=${ENVECTOR_API_KEY} +ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE} EMBEDDING_DIM=1024 RUNE_VAULT_TAG=${DOCKER_TAG} ENVEOF diff --git a/scripts/install-dev.sh b/scripts/install-dev.sh index f5be62e..59789d0 100755 --- a/scripts/install-dev.sh +++ b/scripts/install-dev.sh @@ -326,6 +326,12 @@ prompt_envector_config() { prompt ENVECTOR_API_KEY "enVector API key (e.g. aBcDE_12345_xxxxx)" prompt VAULT_INDEX_NAME "Index name" "runecontext" + print_step "enVector eval mode" + local eval_selected + eval_selected=$(select_menu "mm" "rmp") + ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm" || echo "rmp") + print_info "Eval mode: ${ENVECTOR_EVAL_MODE}" + if [ -z "$ENVECTOR_ENDPOINT" ] || [ -z "$ENVECTOR_API_KEY" ]; then print_error "enVector endpoint and API key are required." exit 1 @@ -484,6 +490,7 @@ VAULT_TEAM_SECRET=${VAULT_TEAM_SECRET_VALUE} VAULT_INDEX_NAME=${VAULT_INDEX_NAME} ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT} ENVECTOR_API_KEY=${ENVECTOR_API_KEY} +ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE} EMBEDDING_DIM=768 RUNE_VAULT_TAG=${DOCKER_TAG} ENVEOF From 5c0b5094b71bf5ef72d3c83c7a2831cebe80160b Mon Sep 17 00:00:00 2001 From: heeyeon Date: Tue, 21 Apr 2026 17:11:13 +0900 Subject: [PATCH 06/23] feat(vault): add ENVECTOR_TLS config to control enVector connection security (#59) Co-Authored-By: Claude Sonnet 4.6 --- install.sh | 7 +++++++ scripts/install-dev.sh | 7 +++++++ tests/unit/test_eval_mode.py | 2 ++ vault/.env.example | 3 +++ vault/vault_core.py | 3 +++ 5 files changed, 22 insertions(+) diff --git a/install.sh b/install.sh index f4a3c77..c2278e1 100755 --- a/install.sh +++ b/install.sh @@ -337,6 +337,12 @@ prompt_envector_config() { ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm" || echo "rmp") print_info "Eval mode: ${ENVECTOR_EVAL_MODE}" + print_step "enVector TLS" + local tls_selected + tls_selected=$(select_menu "true (managed cloud)" "false (self-hosted, no TLS)") + ENVECTOR_TLS=$([ "$tls_selected" -eq 0 ] && echo "true" || echo "false") + print_info "enVector TLS: ${ENVECTOR_TLS}" + if [ -z "$ENVECTOR_ENDPOINT" ] || [ -z "$ENVECTOR_API_KEY" ]; then print_error "enVector endpoint and API key are required." exit 1 @@ -509,6 +515,7 @@ VAULT_INDEX_NAME=${VAULT_INDEX_NAME} ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT} ENVECTOR_API_KEY=${ENVECTOR_API_KEY} ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE} +ENVECTOR_TLS=${ENVECTOR_TLS} EMBEDDING_DIM=1024 RUNE_VAULT_TAG=${DOCKER_TAG} ENVEOF diff --git a/scripts/install-dev.sh b/scripts/install-dev.sh index 59789d0..748f261 100755 --- a/scripts/install-dev.sh +++ b/scripts/install-dev.sh @@ -332,6 +332,12 @@ prompt_envector_config() { ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm" || echo "rmp") print_info "Eval mode: ${ENVECTOR_EVAL_MODE}" + print_step "enVector TLS" + local tls_selected + tls_selected=$(select_menu "true (managed cloud)" "false (self-hosted, no TLS)") + ENVECTOR_TLS=$([ "$tls_selected" -eq 0 ] && echo "true" || echo "false") + print_info "enVector TLS: ${ENVECTOR_TLS}" + if [ -z "$ENVECTOR_ENDPOINT" ] || [ -z "$ENVECTOR_API_KEY" ]; then print_error "enVector endpoint and API key are required." exit 1 @@ -491,6 +497,7 @@ VAULT_INDEX_NAME=${VAULT_INDEX_NAME} ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT} ENVECTOR_API_KEY=${ENVECTOR_API_KEY} ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE} +ENVECTOR_TLS=${ENVECTOR_TLS} EMBEDDING_DIM=768 RUNE_VAULT_TAG=${DOCKER_TAG} ENVEOF diff --git a/tests/unit/test_eval_mode.py b/tests/unit/test_eval_mode.py index 8a79196..055d97d 100644 --- a/tests/unit/test_eval_mode.py +++ b/tests/unit/test_eval_mode.py @@ -95,6 +95,7 @@ def test_ensure_vault_passes_eval_mode_mm_to_ev_init(self, monkeypatch, tmp_path auto_key_setup=True, access_token="test-key", query_encryption="plain", + secure=True, ) def test_ensure_vault_passes_eval_mode_rmp_to_ev_init(self, monkeypatch, tmp_path): @@ -117,6 +118,7 @@ def test_ensure_vault_passes_eval_mode_rmp_to_ev_init(self, monkeypatch, tmp_pat auto_key_setup=True, access_token="test-key", query_encryption="plain", + secure=True, ) def test_invalid_eval_mode_rejected_at_vault_level(self, monkeypatch, tmp_path): diff --git a/vault/.env.example b/vault/.env.example index 406cd8b..52872ff 100644 --- a/vault/.env.example +++ b/vault/.env.example @@ -53,6 +53,9 @@ NGROK_AUTHTOKEN= ENVECTOR_ENDPOINT= # enVector API key (issued from envector.io dashboard) ENVECTOR_API_KEY= +# TLS for enVector connection (default: true when access_token is set). +# Set to false for self-hosted clusters without TLS. +ENVECTOR_TLS=true # ── Index Settings ────────────────────────────────────────────── # Name of the team index on enVector Cloud. diff --git a/vault/vault_core.py b/vault/vault_core.py index 62a6fb9..cdc5942 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -39,6 +39,7 @@ EMBEDDING_DIM = int(os.getenv("EMBEDDING_DIM", "1024")) _VALID_EVAL_MODES = {"rmp", "mm"} EVAL_MODE = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() +ENVECTOR_TLS = os.getenv("ENVECTOR_TLS", "").strip().lower() not in ("false", "0", "no") # Team index name (set by admin, distributed to all team members via get_public_key) VAULT_INDEX_NAME = os.getenv("VAULT_INDEX_NAME", "").strip() or None @@ -89,6 +90,7 @@ def ensure_vault(): auto_key_setup=True, access_token=ENVECTOR_API_KEY, query_encryption="plain", + secure=ENVECTOR_TLS, ) logger.info("Key registered on enVector Cloud (auto_key_setup).") except Exception as e: @@ -103,6 +105,7 @@ def ensure_vault(): auto_key_setup=False, access_token=ENVECTOR_API_KEY, query_encryption="plain", + secure=ENVECTOR_TLS, ) logger.info("Connected to enVector Cloud (auto_key_setup=False).") From 64e2868eab28b358436c3ff6e603fc8cc9adac5c Mon Sep 17 00:00:00 2001 From: heeyeon Date: Tue, 21 Apr 2026 17:55:57 +0900 Subject: [PATCH 07/23] fix(vault): pass ENVECTOR_TLS env var through to container (#59) Co-Authored-By: Claude Sonnet 4.6 --- vault/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/vault/docker-compose.yml b/vault/docker-compose.yml index 688f36d..ea9b173 100644 --- a/vault/docker-compose.yml +++ b/vault/docker-compose.yml @@ -26,6 +26,7 @@ services: - ENVECTOR_API_KEY=${ENVECTOR_API_KEY:-} - EMBEDDING_DIM=${EMBEDDING_DIM:-1024} - ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE:-rmp} + - ENVECTOR_TLS=${ENVECTOR_TLS:-true} - VAULT_AUDIT_LOG=${VAULT_AUDIT_LOG:-file} networks: From 431928b5d69644ecd9be429dc9460324d1c5b8d9 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 5 May 2026 15:57:19 +0900 Subject: [PATCH 08/23] fix(install): update default EMBEDDING_DIM to 1024 --- scripts/install-dev.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install-dev.sh b/scripts/install-dev.sh index 748f261..d1257e0 100755 --- a/scripts/install-dev.sh +++ b/scripts/install-dev.sh @@ -498,7 +498,7 @@ ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT} ENVECTOR_API_KEY=${ENVECTOR_API_KEY} ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE} ENVECTOR_TLS=${ENVECTOR_TLS} -EMBEDDING_DIM=768 +EMBEDDING_DIM=1024 RUNE_VAULT_TAG=${DOCKER_TAG} ENVEOF From 6b8e35409c333b98deac2ac90fd443efcbff5ce0 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 5 May 2026 15:58:35 +0900 Subject: [PATCH 09/23] fix(vault): clean up incomplete key state before regeneration (#59) Co-Authored-By: Claude Sonnet 4.6 --- vault/vault_core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vault/vault_core.py b/vault/vault_core.py index cdc5942..3dcbfc1 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -11,6 +11,7 @@ import json import logging import os +import shutil from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.hkdf import HKDF @@ -65,6 +66,12 @@ def ensure_vault(): # Phase 1: local key generation enc_key = os.path.join(KEY_SUBDIR, "EncKey.json") if not os.path.exists(enc_key): + if os.path.exists(KEY_SUBDIR) and any(os.scandir(KEY_SUBDIR)): + logger.warning( + f"Incomplete key state found in {KEY_SUBDIR} (EncKey.json missing). " + "Cleaning up and regenerating..." + ) + shutil.rmtree(KEY_SUBDIR) logger.info(f"Generating keys in {KEY_SUBDIR}...") os.makedirs(KEY_SUBDIR, exist_ok=True) keygen = KeyGenerator( From b711b0649403b10b303027a288af19838b8e7462 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 5 May 2026 17:37:03 +0900 Subject: [PATCH 10/23] fix: default ENVECTOR_EVAL_MODE to mm and pass to KeyGenerator --- tests/unit/test_eval_mode.py | 22 +++++++++++----------- vault/.env.example | 2 +- vault/docker-compose.yml | 2 +- vault/vault_core.py | 8 ++++++-- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/unit/test_eval_mode.py b/tests/unit/test_eval_mode.py index 055d97d..1fa9c7e 100644 --- a/tests/unit/test_eval_mode.py +++ b/tests/unit/test_eval_mode.py @@ -2,7 +2,7 @@ Unit tests for ENVECTOR_EVAL_MODE configuration (PR #59 test plan). Test plan coverage: -- Item 1: EVAL_MODE defaults to "rmp" when env var is not set +- Item 1: EVAL_MODE defaults to "mm" when env var is not set - Item 2: ev.init() is called with "mm" when ENVECTOR_EVAL_MODE=mm - Item 3: invalid EVAL_MODE propagates to SDK (no vault-level validation guard) """ @@ -23,7 +23,7 @@ class TestEvalModeEnvVar: def test_eval_mode_reflects_env_at_load_time(self): """EVAL_MODE at module level matches what os.getenv would return.""" - expected = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() + expected = os.getenv("ENVECTOR_EVAL_MODE", "mm").lower() assert vault_core.EVAL_MODE == expected def test_eval_mode_is_lowercased(self, monkeypatch): @@ -32,18 +32,18 @@ def test_eval_mode_is_lowercased(self, monkeypatch): # Simulate what the module does: os.getenv(...).lower() assert vault_core.EVAL_MODE.lower() == "rmp" - def test_eval_mode_default_is_rmp_when_env_unset(self, monkeypatch): - """When ENVECTOR_EVAL_MODE is not set, the formula produces 'rmp'.""" + def test_eval_mode_default_is_mm_when_env_unset(self, monkeypatch): + """When ENVECTOR_EVAL_MODE is not set, the formula produces 'mm'.""" monkeypatch.delenv("ENVECTOR_EVAL_MODE", raising=False) - result = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() - assert result == "rmp" - - def test_eval_mode_reads_mm_from_env(self, monkeypatch): - """When ENVECTOR_EVAL_MODE=MM, the formula produces 'mm'.""" - monkeypatch.setenv("ENVECTOR_EVAL_MODE", "MM") - result = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() + result = os.getenv("ENVECTOR_EVAL_MODE", "mm").lower() assert result == "mm" + def test_eval_mode_reads_rmp_from_env(self, monkeypatch): + """When ENVECTOR_EVAL_MODE=RMP, the formula produces 'rmp'.""" + monkeypatch.setenv("ENVECTOR_EVAL_MODE", "RMP") + result = os.getenv("ENVECTOR_EVAL_MODE", "mm").lower() + assert result == "rmp" + class TestEvalModePassedToEvInit: """Test plan item 2 & 3: ev.init() receives EVAL_MODE correctly.""" diff --git a/vault/.env.example b/vault/.env.example index 52872ff..74b265d 100644 --- a/vault/.env.example +++ b/vault/.env.example @@ -70,4 +70,4 @@ EMBEDDING_DIM=1024 # mm — Multi-Multiplication (envector-msa >= 1.4.0-alpha) # IMPORTANT: Keys are mode-specific. Changing this requires deleting vault_keys/ # and restarting Vault so new keys are generated. -ENVECTOR_EVAL_MODE=rmp +ENVECTOR_EVAL_MODE=mm diff --git a/vault/docker-compose.yml b/vault/docker-compose.yml index ea9b173..5f5759e 100644 --- a/vault/docker-compose.yml +++ b/vault/docker-compose.yml @@ -25,7 +25,7 @@ services: - ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT:-} - ENVECTOR_API_KEY=${ENVECTOR_API_KEY:-} - EMBEDDING_DIM=${EMBEDDING_DIM:-1024} - - ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE:-rmp} + - ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE:-mm} - ENVECTOR_TLS=${ENVECTOR_TLS:-true} - VAULT_AUDIT_LOG=${VAULT_AUDIT_LOG:-file} diff --git a/vault/vault_core.py b/vault/vault_core.py index 3dcbfc1..0212895 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -39,7 +39,7 @@ ENVECTOR_API_KEY = os.getenv("ENVECTOR_API_KEY", "").strip() or None EMBEDDING_DIM = int(os.getenv("EMBEDDING_DIM", "1024")) _VALID_EVAL_MODES = {"rmp", "mm"} -EVAL_MODE = os.getenv("ENVECTOR_EVAL_MODE", "rmp").lower() +EVAL_MODE = os.getenv("ENVECTOR_EVAL_MODE", "mm").lower() ENVECTOR_TLS = os.getenv("ENVECTOR_TLS", "").strip().lower() not in ("false", "0", "no") # Team index name (set by admin, distributed to all team members via get_public_key) @@ -75,7 +75,11 @@ def ensure_vault(): logger.info(f"Generating keys in {KEY_SUBDIR}...") os.makedirs(KEY_SUBDIR, exist_ok=True) keygen = KeyGenerator( - key_path=KEY_SUBDIR, key_id=KEY_ID, dim_list=[DIM], metadata_encryption=False + key_path=KEY_SUBDIR, + key_id=KEY_ID, + dim_list=[DIM], + metadata_encryption=False, + eval_mode=EVAL_MODE, ) keygen.generate_keys() else: From eececef6229f31fede8e70354f69fecb3acc756e Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 5 May 2026 17:50:57 +0900 Subject: [PATCH 11/23] fix(vault): skip EvalKey.bin to JSON conversion in MM mode to avoid OOM (#59) pyenvector 1.4.0's generate_keys() unconditionally converts the 849MB EvalKey.bin to JSON, OOMing containers under ~8GB. The eval key is unused in MM mode, so monkey-patch KeyManager.eval_bin_to_json to write a 1-byte dummy instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- vault/vault_core.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/vault/vault_core.py b/vault/vault_core.py index 0212895..925ac7f 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -74,6 +74,19 @@ def ensure_vault(): shutil.rmtree(KEY_SUBDIR) logger.info(f"Generating keys in {KEY_SUBDIR}...") os.makedirs(KEY_SUBDIR, exist_ok=True) + if EVAL_MODE == "mm": + # Workaround for pyenvector 1.4.0: generate_keys() unconditionally + # converts the 849MB EvalKey.bin to JSON, which OOMs containers + # under ~8GB. In MM mode the eval key is not used, so replace the + # conversion with a 1-byte dummy. The library's own MM-mode dummy + # logic is dead code because C++ creates EvalKey.bin first. + from pyenvector.crypto.key_manager import KeyManager + + def _skip_eval_bin_to_json(self, bin_path, json_path): + with open(json_path, "wb") as f: + f.write(b"\x00") + + KeyManager.eval_bin_to_json = _skip_eval_bin_to_json keygen = KeyGenerator( key_path=KEY_SUBDIR, key_id=KEY_ID, From 8b5967888e4881b26b487bc78ec9f1c505b41772 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 5 May 2026 20:03:12 +0900 Subject: [PATCH 12/23] fix(vault): raise gRPC message limit to fit pyenvector 1.4.0 EvalKey (#59) Bump MAX_MESSAGE_LENGTH from 256MB to ~1.95GB (under INT32_MAX) so the ~1.2GB EvalKey produced by pyenvector >= 1.4.0 can be transmitted. This replaces the MM-mode eval_bin_to_json skip workaround introduced in eececef, which is no longer needed now that the larger payload fits within the gRPC limit. Co-Authored-By: Claude Opus 4.7 (1M context) --- vault/vault_core.py | 13 ------------- vault/vault_grpc_server.py | 4 +++- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/vault/vault_core.py b/vault/vault_core.py index 925ac7f..0212895 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -74,19 +74,6 @@ def ensure_vault(): shutil.rmtree(KEY_SUBDIR) logger.info(f"Generating keys in {KEY_SUBDIR}...") os.makedirs(KEY_SUBDIR, exist_ok=True) - if EVAL_MODE == "mm": - # Workaround for pyenvector 1.4.0: generate_keys() unconditionally - # converts the 849MB EvalKey.bin to JSON, which OOMs containers - # under ~8GB. In MM mode the eval key is not used, so replace the - # conversion with a 1-byte dummy. The library's own MM-mode dummy - # logic is dead code because C++ creates EvalKey.bin first. - from pyenvector.crypto.key_manager import KeyManager - - def _skip_eval_bin_to_json(self, bin_path, json_path): - with open(json_path, "wb") as f: - f.write(b"\x00") - - KeyManager.eval_bin_to_json = _skip_eval_bin_to_json keygen = KeyGenerator( key_path=KEY_SUBDIR, key_id=KEY_ID, diff --git a/vault/vault_grpc_server.py b/vault/vault_grpc_server.py index eb7df7b..7420f7e 100644 --- a/vault/vault_grpc_server.py +++ b/vault/vault_grpc_server.py @@ -44,7 +44,9 @@ logger = logging.getLogger("rune.vault.grpc") -MAX_MESSAGE_LENGTH = 256 * 1024 * 1024 # 256 MB (EvalKey can be tens of MB) +MAX_MESSAGE_LENGTH = ( + 2000 * 1024 * 1024 +) # ~1.95 GB (kept under INT32_MAX; EvalKey in pyenvector >= 1.4.0 reaches ~1.2 GB) def _emit_audit(method, user, top_k, result_count, status, error_detail, duration, context): From 27e8b1c120bb6276fd44a1328bdd91f55d5c8395 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 5 May 2026 20:07:35 +0900 Subject: [PATCH 13/23] chore(vault): update protovalidate buf dependency Co-Authored-By: Claude Opus 4.7 (1M context) --- vault/buf.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vault/buf.lock b/vault/buf.lock index d15a117..709ae02 100644 --- a/vault/buf.lock +++ b/vault/buf.lock @@ -2,5 +2,5 @@ version: v2 deps: - name: buf.build/bufbuild/protovalidate - commit: 80ab13bee0bf4272b6161a72bf7034e0 - digest: b5:1aa6a965be5d02d64e1d81954fa2e78ef9d1e33a0c30f92bc2626039006a94deb3a5b05f14ed8893f5c3ffce444ac008f7e968188ad225c4c29c813aa5f2daa1 + commit: 50325440f8f24053b047484a6bf60b76 + digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31 From 620ef489d72da7f9e79af18463ef6e4d54c1f349 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Wed, 6 May 2026 14:42:10 +0900 Subject: [PATCH 14/23] feat(vault): include ENVECTOR_TLS as envector_secure in public key bundle (#59) Propagate the server's ENVECTOR_TLS setting to agents through the public key bundle so they can match the Vault's enVector connection security mode without separate configuration. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/unit/test_public_key.py | 18 ++++++++++++++++++ vault/vault_core.py | 1 + 2 files changed, 19 insertions(+) diff --git a/tests/unit/test_public_key.py b/tests/unit/test_public_key.py index 2b309b8..2468663 100644 --- a/tests/unit/test_public_key.py +++ b/tests/unit/test_public_key.py @@ -94,3 +94,21 @@ def test_bundle_envector_empty_when_not_configured(self, test_keys, monkeypatch) assert bundle.get("envector_endpoint") is None assert bundle.get("envector_api_key") is None + + def test_bundle_contains_envector_secure_true(self, test_keys, monkeypatch): + """Bundle should propagate ENVECTOR_TLS=true as envector_secure=True.""" + monkeypatch.setattr('vault_core.ENVECTOR_TLS', True) + + result = get_public_key("evt_0000000000000000000000000000demo") + bundle = json.loads(result) + + assert bundle["envector_secure"] is True + + def test_bundle_contains_envector_secure_false(self, test_keys, monkeypatch): + """Bundle should propagate ENVECTOR_TLS=false as envector_secure=False.""" + monkeypatch.setattr('vault_core.ENVECTOR_TLS', False) + + result = get_public_key("evt_0000000000000000000000000000demo") + bundle = json.loads(result) + + assert bundle["envector_secure"] is False diff --git a/vault/vault_core.py b/vault/vault_core.py index 0212895..5d28e42 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -252,6 +252,7 @@ def _get_public_key_impl(token: str) -> str: # enVector Cloud credentials — agents receive these from Vault instead of user input bundle["envector_endpoint"] = ENVECTOR_ENDPOINT bundle["envector_api_key"] = ENVECTOR_API_KEY + bundle["envector_secure"] = ENVECTOR_TLS return json.dumps(bundle) From 365965e8714669f8b930d158732632f7f5c248fc Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Wed, 6 May 2026 15:43:20 +0900 Subject: [PATCH 15/23] chore(vault): raise container memory limit to 10G and CPU to 2.0 Co-Authored-By: Claude Opus 4.7 (1M context) --- vault/docker-compose.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vault/docker-compose.yml b/vault/docker-compose.yml index 5f5759e..7532589 100644 --- a/vault/docker-compose.yml +++ b/vault/docker-compose.yml @@ -49,13 +49,12 @@ services: deploy: resources: limits: - memory: 1G - cpus: "1.0" + memory: 10G + cpus: "2.0" reservations: memory: 512M cpus: "0.5" - networks: vault-net: driver: bridge From 9633d56df294493f1b322d3aa9c6fda87635ff8c Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 12 May 2026 10:49:14 +0900 Subject: [PATCH 16/23] feat(vault): switch default eval mode to mm32 and index type to IVF_VCT (#59) Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/unit/test_eval_mode.py | 24 ++++++++++++------------ vault/.env.example | 6 +++--- vault/docker-compose.yml | 2 +- vault/vault_core.py | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/unit/test_eval_mode.py b/tests/unit/test_eval_mode.py index 1fa9c7e..5683830 100644 --- a/tests/unit/test_eval_mode.py +++ b/tests/unit/test_eval_mode.py @@ -2,8 +2,8 @@ Unit tests for ENVECTOR_EVAL_MODE configuration (PR #59 test plan). Test plan coverage: -- Item 1: EVAL_MODE defaults to "mm" when env var is not set -- Item 2: ev.init() is called with "mm" when ENVECTOR_EVAL_MODE=mm +- Item 1: EVAL_MODE defaults to "mm32" when env var is not set +- Item 2: ev.init() is called with "mm32" when ENVECTOR_EVAL_MODE=mm32 - Item 3: invalid EVAL_MODE propagates to SDK (no vault-level validation guard) """ import sys @@ -23,7 +23,7 @@ class TestEvalModeEnvVar: def test_eval_mode_reflects_env_at_load_time(self): """EVAL_MODE at module level matches what os.getenv would return.""" - expected = os.getenv("ENVECTOR_EVAL_MODE", "mm").lower() + expected = os.getenv("ENVECTOR_EVAL_MODE", "mm32").lower() assert vault_core.EVAL_MODE == expected def test_eval_mode_is_lowercased(self, monkeypatch): @@ -32,16 +32,16 @@ def test_eval_mode_is_lowercased(self, monkeypatch): # Simulate what the module does: os.getenv(...).lower() assert vault_core.EVAL_MODE.lower() == "rmp" - def test_eval_mode_default_is_mm_when_env_unset(self, monkeypatch): - """When ENVECTOR_EVAL_MODE is not set, the formula produces 'mm'.""" + def test_eval_mode_default_is_mm32_when_env_unset(self, monkeypatch): + """When ENVECTOR_EVAL_MODE is not set, the formula produces 'mm32'.""" monkeypatch.delenv("ENVECTOR_EVAL_MODE", raising=False) - result = os.getenv("ENVECTOR_EVAL_MODE", "mm").lower() - assert result == "mm" + result = os.getenv("ENVECTOR_EVAL_MODE", "mm32").lower() + assert result == "mm32" def test_eval_mode_reads_rmp_from_env(self, monkeypatch): """When ENVECTOR_EVAL_MODE=RMP, the formula produces 'rmp'.""" monkeypatch.setenv("ENVECTOR_EVAL_MODE", "RMP") - result = os.getenv("ENVECTOR_EVAL_MODE", "mm").lower() + result = os.getenv("ENVECTOR_EVAL_MODE", "mm32").lower() assert result == "rmp" @@ -75,9 +75,9 @@ def _restore_pyenvector(self, original): else: sys.modules.pop("pyenvector", None) - def test_ensure_vault_passes_eval_mode_mm_to_ev_init(self, monkeypatch, tmp_path): - """ensure_vault() calls ev.init(eval_mode='mm') when EVAL_MODE is 'mm'.""" - self._setup_offline_patches(monkeypatch, tmp_path, eval_mode="mm") + def test_ensure_vault_passes_eval_mode_mm32_to_ev_init(self, monkeypatch, tmp_path): + """ensure_vault() calls ev.init(eval_mode='mm32') when EVAL_MODE is 'mm32'.""" + self._setup_offline_patches(monkeypatch, tmp_path, eval_mode="mm32") mock_ev = MagicMock() original = self._patch_pyenvector(mock_ev) @@ -91,7 +91,7 @@ def test_ensure_vault_passes_eval_mode_mm_to_ev_init(self, monkeypatch, tmp_path key_path=str(tmp_path), key_id="vault-key", dim=1024, - eval_mode="mm", + eval_mode="mm32", auto_key_setup=True, access_token="test-key", query_encryption="plain", diff --git a/vault/.env.example b/vault/.env.example index 74b265d..0199814 100644 --- a/vault/.env.example +++ b/vault/.env.example @@ -66,8 +66,8 @@ EMBEDDING_DIM=1024 # ── FHE Evaluation Mode ───────────────────────────────────────── # Controls the homomorphic encryption evaluation strategy. -# rmp — Relinearization Multiplication Protocol (envector-msa <= 1.2.x) -# mm — Multi-Multiplication (envector-msa >= 1.4.0-alpha) +# rmp — Relinearization Multiplication Protocol (envector-msa <= 1.2.x) +# mm32 — Multi-Multiplication 32 (envector-msa >= 1.4.0-alpha) # IMPORTANT: Keys are mode-specific. Changing this requires deleting vault_keys/ # and restarting Vault so new keys are generated. -ENVECTOR_EVAL_MODE=mm +ENVECTOR_EVAL_MODE=mm32 diff --git a/vault/docker-compose.yml b/vault/docker-compose.yml index 7532589..774ab64 100644 --- a/vault/docker-compose.yml +++ b/vault/docker-compose.yml @@ -25,7 +25,7 @@ services: - ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT:-} - ENVECTOR_API_KEY=${ENVECTOR_API_KEY:-} - EMBEDDING_DIM=${EMBEDDING_DIM:-1024} - - ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE:-mm} + - ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE:-mm32} - ENVECTOR_TLS=${ENVECTOR_TLS:-true} - VAULT_AUDIT_LOG=${VAULT_AUDIT_LOG:-file} diff --git a/vault/vault_core.py b/vault/vault_core.py index 5d28e42..ba33eb9 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -38,8 +38,8 @@ ENVECTOR_ENDPOINT = os.getenv("ENVECTOR_ENDPOINT", "").strip() or None ENVECTOR_API_KEY = os.getenv("ENVECTOR_API_KEY", "").strip() or None EMBEDDING_DIM = int(os.getenv("EMBEDDING_DIM", "1024")) -_VALID_EVAL_MODES = {"rmp", "mm"} -EVAL_MODE = os.getenv("ENVECTOR_EVAL_MODE", "mm").lower() +_VALID_EVAL_MODES = {"rmp", "mm32"} +EVAL_MODE = os.getenv("ENVECTOR_EVAL_MODE", "mm32").lower() ENVECTOR_TLS = os.getenv("ENVECTOR_TLS", "").strip().lower() not in ("false", "0", "no") # Team index name (set by admin, distributed to all team members via get_public_key) @@ -138,7 +138,7 @@ def ensure_vault(): ev.create_index( index_name=VAULT_INDEX_NAME, dim=EMBEDDING_DIM, - index_params={"index_type": "FLAT"}, + index_params={"index_type": "IVF_VCT"}, query_encryption="plain", metadata_encryption=False, # workaround: skip deepcopy metadata_key property access (pyenvector#247) From dad52be05cbb84a508ea91a6d4a59d99b2cfbae9 Mon Sep 17 00:00:00 2001 From: couragehong Date: Tue, 12 May 2026 11:34:59 +0900 Subject: [PATCH 17/23] fix(vault): remove proto/__init__.py that shadows proto-plus (#59) Empty vault/proto/__init__.py combined with `vault` on PYTHONPATH made `vault/proto/` register as a top-level `proto` package, shadowing the `proto-plus` package that google-cloud libraries depend on. After pyenvector started importing google.cloud.secretmanager/storage, every test collection failed with `module 'proto' has no attribute 'module'`. Drop the empty __init__.py and import the generated stubs as top-level modules (vault/proto is already on PYTHONPATH). Co-Authored-By: Claude Opus 4.7 (1M context) --- vault/proto/__init__.py | 0 vault/vault_grpc_server.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 vault/proto/__init__.py diff --git a/vault/proto/__init__.py b/vault/proto/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/vault/vault_grpc_server.py b/vault/vault_grpc_server.py index 7420f7e..eaf9499 100644 --- a/vault/vault_grpc_server.py +++ b/vault/vault_grpc_server.py @@ -14,12 +14,12 @@ from datetime import datetime, timezone import grpc +import vault_service_pb2 as pb2 +import vault_service_pb2_grpc as pb2_grpc from admin_server import start_admin_server from grpc_health.v1 import health_pb2, health_pb2_grpc from grpc_health.v1.health import HealthServicer from grpc_reflection.v1alpha import reflection -from proto import vault_service_pb2 as pb2 -from proto import vault_service_pb2_grpc as pb2_grpc from token_store import ( RateLimitError, ScopeError, From 26e0af5d1a3c75a424ff1fe0954bbffa57ef00af Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 12 May 2026 12:44:56 +0900 Subject: [PATCH 18/23] fix(install): correct eval mode menu option from "mm" to "mm32" (#59) The installer offered "mm" as an eval mode choice, but vault_core only accepts {"rmp", "mm32"}, so selecting it produced an invalid config. Co-Authored-By: Claude Opus 4.7 (1M context) --- install.sh | 4 ++-- scripts/install-dev.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index c2278e1..c0dcb95 100755 --- a/install.sh +++ b/install.sh @@ -333,8 +333,8 @@ prompt_envector_config() { print_step "enVector eval mode" local eval_selected - eval_selected=$(select_menu "mm" "rmp") - ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm" || echo "rmp") + eval_selected=$(select_menu "mm32" "rmp") + ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm32" || echo "rmp") print_info "Eval mode: ${ENVECTOR_EVAL_MODE}" print_step "enVector TLS" diff --git a/scripts/install-dev.sh b/scripts/install-dev.sh index d1257e0..c72c847 100755 --- a/scripts/install-dev.sh +++ b/scripts/install-dev.sh @@ -328,8 +328,8 @@ prompt_envector_config() { print_step "enVector eval mode" local eval_selected - eval_selected=$(select_menu "mm" "rmp") - ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm" || echo "rmp") + eval_selected=$(select_menu "mm32" "rmp") + ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm32" || echo "rmp") print_info "Eval mode: ${ENVECTOR_EVAL_MODE}" print_step "enVector TLS" From 839a25a092c886a8d9f1e0fa7e950a998d81a9d3 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 12 May 2026 12:51:26 +0900 Subject: [PATCH 19/23] Revert "fix(vault): remove proto/__init__.py that shadows proto-plus (#59)" This reverts commit dad52be05cbb84a508ea91a6d4a59d99b2cfbae9. --- vault/proto/__init__.py | 0 vault/vault_grpc_server.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 vault/proto/__init__.py diff --git a/vault/proto/__init__.py b/vault/proto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vault/vault_grpc_server.py b/vault/vault_grpc_server.py index eaf9499..7420f7e 100644 --- a/vault/vault_grpc_server.py +++ b/vault/vault_grpc_server.py @@ -14,12 +14,12 @@ from datetime import datetime, timezone import grpc -import vault_service_pb2 as pb2 -import vault_service_pb2_grpc as pb2_grpc from admin_server import start_admin_server from grpc_health.v1 import health_pb2, health_pb2_grpc from grpc_health.v1.health import HealthServicer from grpc_reflection.v1alpha import reflection +from proto import vault_service_pb2 as pb2 +from proto import vault_service_pb2_grpc as pb2_grpc from token_store import ( RateLimitError, ScopeError, From cf63171d4b1636f02d029caa1669eab144cceb11 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 12 May 2026 13:14:05 +0900 Subject: [PATCH 20/23] fix(vault): rename proto/ to _proto/ to escape proto-plus shadow (#59) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vault/ is on PYTHONPATH, so vault/proto/__init__.py made `import proto` resolve to our empty package instead of the proto-plus PyPI module. With pyenvector 1.4.0 pulling in google-cloud-secretmanager, `proto.module(...)` failed at import time — breaking fresh-venv unit test collection and Docker container startup. Rename vault/proto/ to vault/_proto/ so the name `proto` no longer exists in our import path. Update Dockerfile, buf.yaml, proto-gen.sh, .gitignore, PYTHONPATH in .mise{,ci}.toml, and docs accordingly. Stub imports in vault_grpc_server.py switch to bare form, matching the style already used by vault_service_pb2_grpc.py. This supersedes dad52be (reverted in 839a25a), which deleted __init__.py without carrying the rename through the rest of the toolchain. Co-Authored-By: Claude Opus 4.7 (1M context) --- .mise.ci.toml | 2 +- .mise.toml | 2 +- docs/ARCHITECTURE.md | 2 +- tests/unit/test_protovalidate.py | 2 +- vault/.gitignore | 4 ++-- vault/Dockerfile | 6 +++--- vault/{proto => _proto}/__init__.py | 0 vault/{proto => _proto}/vault_service.proto | 0 vault/buf.yaml | 2 +- vault/scripts/proto-gen.sh | 8 ++++---- vault/vault_grpc_server.py | 4 ++-- 11 files changed, 16 insertions(+), 16 deletions(-) rename vault/{proto => _proto}/__init__.py (100%) rename vault/{proto => _proto}/vault_service.proto (100%) diff --git a/.mise.ci.toml b/.mise.ci.toml index 38b2877..d1afcbe 100644 --- a/.mise.ci.toml +++ b/.mise.ci.toml @@ -8,4 +8,4 @@ ruff = "0.15" [env] _.python.venv = { path = ".venv", create = true } -PYTHONPATH = "{{config_root}}/vault/proto:{{config_root}}/vault" +PYTHONPATH = "{{config_root}}/vault/_proto:{{config_root}}/vault" diff --git a/.mise.toml b/.mise.toml index c2bbd3f..0913b65 100644 --- a/.mise.toml +++ b/.mise.toml @@ -18,7 +18,7 @@ oci = "3" # ── Environment variables ──────────────────────────────────────────── [env] _.python.venv = { path = ".venv", create = true } -PYTHONPATH = "{{config_root}}/vault/proto:{{config_root}}/vault" +PYTHONPATH = "{{config_root}}/vault/_proto:{{config_root}}/vault" # ── Docker image settings ─────────────────────────────────────────── [vars] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2e0d154..789028a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -115,7 +115,7 @@ Keys are auto-generated on first startup via `ensure_vault()`. ### 2. gRPC Service (API) -Defined in `proto/vault_service.proto` (`rune.vault.v1.VaultService`). +Defined in `_proto/vault_service.proto` (`rune.vault.v1.VaultService`). **Server Configuration**: - Max message size: 256 MB (for EvalKey transfer) diff --git a/tests/unit/test_protovalidate.py b/tests/unit/test_protovalidate.py index 48d2e4d..3cf6cda 100644 --- a/tests/unit/test_protovalidate.py +++ b/tests/unit/test_protovalidate.py @@ -9,7 +9,7 @@ import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../vault')) -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../vault/proto')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../vault/_proto')) protovalidate = pytest.importorskip("protovalidate") pb2 = pytest.importorskip("vault_service_pb2") diff --git a/vault/.gitignore b/vault/.gitignore index e1e9d07..53bfd6c 100644 --- a/vault/.gitignore +++ b/vault/.gitignore @@ -1,4 +1,4 @@ # Generated proto stubs (regenerate with: make proto-gen) -proto/*_pb2.py -proto/*_pb2_grpc.py +_proto/*_pb2.py +_proto/*_pb2_grpc.py buf/ diff --git a/vault/Dockerfile b/vault/Dockerfile index f902b7a..18493df 100644 --- a/vault/Dockerfile +++ b/vault/Dockerfile @@ -13,7 +13,7 @@ RUN pip install --no-cache-dir "grpcio-tools>=1.60.2,<=1.71.2" "protobuf>=5.29.0 WORKDIR /build COPY buf.yaml buf.lock ./ -COPY proto/vault_service.proto proto/__init__.py proto/ +COPY _proto/vault_service.proto _proto/__init__.py _proto/ COPY scripts/proto-gen.sh scripts/ RUN bash scripts/proto-gen.sh @@ -36,7 +36,7 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY vault_core.py vault_grpc_server.py token_store.py admin_server.py vault_admin_cli.py request_validator.py validation_interceptor.py audit.py ./ -COPY --from=proto-builder /build/proto/ proto/ +COPY --from=proto-builder /build/_proto/ _proto/ COPY --from=proto-builder /build/buf/ buf/ COPY docker-entrypoint.sh . RUN chmod +x docker-entrypoint.sh vault_admin_cli.py @@ -44,7 +44,7 @@ RUN chmod +x docker-entrypoint.sh vault_admin_cli.py RUN mkdir -p /app/vault_keys /app/certs /app/config /secure/backups /var/log/rune-vault \ && chown -R vault:vault /app /secure /var/log/rune-vault -ENV PYTHONPATH=/app/proto:/app +ENV PYTHONPATH=/app/_proto:/app EXPOSE 50051 diff --git a/vault/proto/__init__.py b/vault/_proto/__init__.py similarity index 100% rename from vault/proto/__init__.py rename to vault/_proto/__init__.py diff --git a/vault/proto/vault_service.proto b/vault/_proto/vault_service.proto similarity index 100% rename from vault/proto/vault_service.proto rename to vault/_proto/vault_service.proto diff --git a/vault/buf.yaml b/vault/buf.yaml index 87c1dfe..75077dc 100644 --- a/vault/buf.yaml +++ b/vault/buf.yaml @@ -1,5 +1,5 @@ version: v2 modules: - - path: proto + - path: _proto deps: - buf.build/bufbuild/protovalidate diff --git a/vault/scripts/proto-gen.sh b/vault/scripts/proto-gen.sh index 084cc13..6c08a61 100755 --- a/vault/scripts/proto-gen.sh +++ b/vault/scripts/proto-gen.sh @@ -31,12 +31,12 @@ print(os.path.join(os.path.dirname(grpc_tools.__file__), '_proto')) # ── 4. Generate vault_service stubs ───────────────────────────────── echo "[proto-gen] Generating vault_service_pb2.py / vault_service_pb2_grpc.py..." python3 -m grpc_tools.protoc \ - -Iproto \ + -I_proto \ -I"$DEPS_DIR" \ -I"$GRPC_PROTO" \ - --python_out=proto \ - --grpc_python_out=proto \ - proto/vault_service.proto + --python_out=_proto \ + --grpc_python_out=_proto \ + _proto/vault_service.proto # ── 5. Generate protovalidate runtime stubs ────────────────────────── echo "[proto-gen] Generating buf/validate/validate_pb2.py..." diff --git a/vault/vault_grpc_server.py b/vault/vault_grpc_server.py index 7420f7e..eaf9499 100644 --- a/vault/vault_grpc_server.py +++ b/vault/vault_grpc_server.py @@ -14,12 +14,12 @@ from datetime import datetime, timezone import grpc +import vault_service_pb2 as pb2 +import vault_service_pb2_grpc as pb2_grpc from admin_server import start_admin_server from grpc_health.v1 import health_pb2, health_pb2_grpc from grpc_health.v1.health import HealthServicer from grpc_reflection.v1alpha import reflection -from proto import vault_service_pb2 as pb2 -from proto import vault_service_pb2_grpc as pb2_grpc from token_store import ( RateLimitError, ScopeError, From 38c4ec91ddf8254b4c6e71663ae80f5cdaadd181 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Wed, 13 May 2026 16:41:22 +0900 Subject: [PATCH 21/23] feat(vault): tune IVF_VCT index with nlist=256 and default_nprobe=6 (#59) Co-Authored-By: Claude Opus 4.7 (1M context) --- vault/vault_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/vault_core.py b/vault/vault_core.py index ba33eb9..0064b1c 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -138,7 +138,7 @@ def ensure_vault(): ev.create_index( index_name=VAULT_INDEX_NAME, dim=EMBEDDING_DIM, - index_params={"index_type": "IVF_VCT"}, + index_params={"index_type": "IVF_VCT", "nlist": 256, "default_nprobe": 6}, query_encryption="plain", metadata_encryption=False, # workaround: skip deepcopy metadata_key property access (pyenvector#247) From 27ac1ea898215c39c9cf9a0b3a5ad1cc8d350cf5 Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Tue, 19 May 2026 17:58:26 +0900 Subject: [PATCH 22/23] perf(vault): increase IVF_VCT nlist to 32768 (#59) Raise the IVF_VCT cluster count from 256 to 32768 to improve search recall as the vault index grows. default_nprobe stays at 6. Co-Authored-By: Claude Opus 4.7 (1M context) --- vault/vault_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/vault_core.py b/vault/vault_core.py index 0064b1c..d75ce5d 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -138,7 +138,7 @@ def ensure_vault(): ev.create_index( index_name=VAULT_INDEX_NAME, dim=EMBEDDING_DIM, - index_params={"index_type": "IVF_VCT", "nlist": 256, "default_nprobe": 6}, + index_params={"index_type": "IVF_VCT", "nlist": 32768, "default_nprobe": 6}, query_encryption="plain", metadata_encryption=False, # workaround: skip deepcopy metadata_key property access (pyenvector#247) From d22b4a66917c8f61f48381c548feb8d0ced187ce Mon Sep 17 00:00:00 2001 From: heeyeon01 Date: Wed, 20 May 2026 09:58:34 +0900 Subject: [PATCH 23/23] perf(vault): lower IVF_VCT nlist to 8192 (#59) Reduce the IVF_VCT cluster count from 32768 to 8192. 32768 was oversized for the current vault scale; 8192 keeps recall acceptable while cutting training cost and per-query overhead. default_nprobe stays at 6. Co-Authored-By: Claude Opus 4.7 (1M context) --- vault/vault_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/vault_core.py b/vault/vault_core.py index d75ce5d..d532526 100644 --- a/vault/vault_core.py +++ b/vault/vault_core.py @@ -138,7 +138,7 @@ def ensure_vault(): ev.create_index( index_name=VAULT_INDEX_NAME, dim=EMBEDDING_DIM, - index_params={"index_type": "IVF_VCT", "nlist": 32768, "default_nprobe": 6}, + index_params={"index_type": "IVF_VCT", "nlist": 8192, "default_nprobe": 6}, query_encryption="plain", metadata_encryption=False, # workaround: skip deepcopy metadata_key property access (pyenvector#247)