Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1325edb
feat(vault): make eval_mode configurable via ENVECTOR_EVAL_MODE env v…
Apr 17, 2026
0c0e392
test(vault): verify ENVECTOR_EVAL_MODE env var behavior (#59)
Apr 20, 2026
8356936
feat(vault): add allowlist guard for ENVECTOR_EVAL_MODE (#59)
Apr 20, 2026
2142ebb
docs(vault): simplify ENVECTOR_EVAL_MODE description in .env.example …
Apr 20, 2026
9fdd216
feat(install): prompt for ENVECTOR_EVAL_MODE and write to .env (#59)
Apr 21, 2026
5c0b509
feat(vault): add ENVECTOR_TLS config to control enVector connection s…
Apr 21, 2026
64e2868
fix(vault): pass ENVECTOR_TLS env var through to container (#59)
Apr 21, 2026
431928b
fix(install): update default EMBEDDING_DIM to 1024
heeyeon01 May 5, 2026
6b8e354
fix(vault): clean up incomplete key state before regeneration (#59)
heeyeon01 May 5, 2026
b711b06
fix: default ENVECTOR_EVAL_MODE to mm and pass to KeyGenerator
heeyeon01 May 5, 2026
eececef
fix(vault): skip EvalKey.bin to JSON conversion in MM mode to avoid O…
heeyeon01 May 5, 2026
8b59678
fix(vault): raise gRPC message limit to fit pyenvector 1.4.0 EvalKey …
heeyeon01 May 5, 2026
27e8b1c
chore(vault): update protovalidate buf dependency
heeyeon01 May 5, 2026
620ef48
feat(vault): include ENVECTOR_TLS as envector_secure in public key bu…
heeyeon01 May 6, 2026
365965e
chore(vault): raise container memory limit to 10G and CPU to 2.0
heeyeon01 May 6, 2026
9633d56
feat(vault): switch default eval mode to mm32 and index type to IVF_V…
heeyeon01 May 12, 2026
dad52be
fix(vault): remove proto/__init__.py that shadows proto-plus (#59)
couragehong May 12, 2026
26e0af5
fix(install): correct eval mode menu option from "mm" to "mm32" (#59)
heeyeon01 May 12, 2026
0ef7da2
Merge branch 'worktree-issue-59-configurable-eval-mode' of github.com…
heeyeon01 May 12, 2026
839a25a
Revert "fix(vault): remove proto/__init__.py that shadows proto-plus …
heeyeon01 May 12, 2026
cf63171
fix(vault): rename proto/ to _proto/ to escape proto-plus shadow (#59)
heeyeon01 May 12, 2026
38c4ec9
feat(vault): tune IVF_VCT index with nlist=256 and default_nprobe=6 (…
heeyeon01 May 13, 2026
27ac1ea
perf(vault): increase IVF_VCT nlist to 32768 (#59)
heeyeon01 May 19, 2026
d22b4a6
perf(vault): lower IVF_VCT nlist to 8192 (#59)
heeyeon01 May 20, 2026
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
2 changes: 1 addition & 1 deletion .mise.ci.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,18 @@ 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 "mm32" "rmp")
ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm32" || 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
Expand Down Expand Up @@ -502,6 +514,8 @@ 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}
ENVECTOR_TLS=${ENVECTOR_TLS}
EMBEDDING_DIM=1024
RUNE_VAULT_TAG=${DOCKER_TAG}
ENVEOF
Expand Down
16 changes: 15 additions & 1 deletion scripts/install-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,18 @@ 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 "mm32" "rmp")
ENVECTOR_EVAL_MODE=$([ "$eval_selected" -eq 0 ] && echo "mm32" || 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
Expand Down Expand Up @@ -484,7 +496,9 @@ VAULT_TEAM_SECRET=${VAULT_TEAM_SECRET_VALUE}
VAULT_INDEX_NAME=${VAULT_INDEX_NAME}
ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT}
ENVECTOR_API_KEY=${ENVECTOR_API_KEY}
EMBEDDING_DIM=768
ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE}
ENVECTOR_TLS=${ENVECTOR_TLS}
EMBEDDING_DIM=1024
RUNE_VAULT_TAG=${DOCKER_TAG}
ENVEOF

Expand Down
136 changes: 136 additions & 0 deletions tests/unit/test_eval_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""
Unit tests for ENVECTOR_EVAL_MODE configuration (PR #59 test plan).

Test plan coverage:
- 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
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", "mm32").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_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", "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", "mm32").lower()
assert result == "rmp"


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_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)
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="mm32",
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):
"""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",
secure=True,
)

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()
original = self._patch_pyenvector(mock_ev)
try:
with pytest.raises(ValueError, match="Invalid ENVECTOR_EVAL_MODE"):
vault_core.ensure_vault()
finally:
self._restore_pyenvector(original)

mock_ev.init.assert_not_called()
2 changes: 1 addition & 1 deletion tests/unit/test_protovalidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_public_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions vault/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,21 @@ 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.
# Must be alphanumeric, lowercase, no spaces, and less than 20 characters.
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)
# 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=mm32
4 changes: 2 additions & 2 deletions vault/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
6 changes: 3 additions & 3 deletions vault/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -36,15 +36,15 @@ 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

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

Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions vault/buf.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: 80ab13bee0bf4272b6161a72bf7034e0
digest: b5:1aa6a965be5d02d64e1d81954fa2e78ef9d1e33a0c30f92bc2626039006a94deb3a5b05f14ed8893f5c3ffce444ac008f7e968188ad225c4c29c813aa5f2daa1
commit: 50325440f8f24053b047484a6bf60b76
digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31
2 changes: 1 addition & 1 deletion vault/buf.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
version: v2
modules:
- path: proto
- path: _proto
deps:
- buf.build/bufbuild/protovalidate
7 changes: 4 additions & 3 deletions vault/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ services:
- ENVECTOR_ENDPOINT=${ENVECTOR_ENDPOINT:-}
- ENVECTOR_API_KEY=${ENVECTOR_API_KEY:-}
- EMBEDDING_DIM=${EMBEDDING_DIM:-1024}
- ENVECTOR_EVAL_MODE=${ENVECTOR_EVAL_MODE:-mm32}
- ENVECTOR_TLS=${ENVECTOR_TLS:-true}
- VAULT_AUDIT_LOG=${VAULT_AUDIT_LOG:-file}

networks:
Expand All @@ -47,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
Expand Down
8 changes: 4 additions & 4 deletions vault/scripts/proto-gen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down
Loading
Loading