Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b538ec9
ci: preserve Strix failure evidence in OpenCode reviews
seonghobae Jun 12, 2026
f91487f
fix: address Strix review gate feedback
seonghobae Jun 12, 2026
3c315a1
fix: report Strix provider blockers concretely
seonghobae Jun 12, 2026
9179a46
fix: explain pending checks when OpenCode models fail
seonghobae Jun 12, 2026
51c4854
fix: explain cancelled Strix runs without logs
seonghobae Jun 12, 2026
45686f8
fix: anchor Strix provider blockers to config line
seonghobae Jun 12, 2026
31c5582
fix: scope failed-check evidence to Strix runs
seonghobae Jun 12, 2026
fb1580f
fix: cap OpenCode prompt evidence
seonghobae Jun 12, 2026
4b29cc8
fix: align OpenCode evidence self-test
seonghobae Jun 12, 2026
add611c
fix: add contextual workspace aria labels
seonghobae Jun 12, 2026
75bd5d7
fix: debounce network graph resize fitting
seonghobae Jun 12, 2026
ee48163
fix: reduce email grouping lookup overhead
seonghobae Jun 12, 2026
474b4e6
fix: include manual Strix evidence reruns
seonghobae Jun 13, 2026
9d48b7f
fix: validate configured CORS origins
seonghobae Jun 13, 2026
6f621d1
fix: expand checkbox label hit targets
seonghobae Jun 13, 2026
5a6c6ca
fix: harden session auth checks
seonghobae Jun 13, 2026
01a576b
fix: address review gate follow-ups
seonghobae Jun 13, 2026
341cb4b
fix: forward-port http-only browser sessions
seonghobae Jun 13, 2026
217b00c
fix: address strix session and scan findings
seonghobae Jun 13, 2026
6f15aaf
fix: fail closed on session claim fetch
seonghobae Jun 13, 2026
d0efcba
fix: require organization scope for browser session claims
seonghobae Jun 13, 2026
21c58d8
fix: reject non-source-backed opencode findings
seonghobae Jun 13, 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
57 changes: 46 additions & 11 deletions .github/workflows/opencode-review.yml

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ on:
strix_llm:
description: Optional Strix model override for manual evidence runs
required: false
default: openai/openai/gpt-5
default: openai/gpt-5
type: string

concurrency:
Expand Down Expand Up @@ -117,7 +117,7 @@ jobs:
- name: Gate Strix secrets
id: gate
env:
STRIX_MODEL: ${{ github.event.inputs.strix_llm || 'openai/openai/gpt-5' }}
STRIX_MODEL: ${{ github.event.inputs.strix_llm || 'openai/gpt-5' }}
STRIX_OPENAI_API_KEY: ${{ secrets.STRIX_OPENAI_API_KEY }}
STRIX_VERTEX_CREDENTIALS: ${{ secrets.GCP_SA_KEY }}
STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }}
Expand All @@ -132,7 +132,8 @@ jobs:
;;
openai/gpt-5* | openai/gpt-[6-9]* | openai/gpt-[1-9][0-9]* | \
openai/openai/gpt-5* | openai/openai/gpt-[6-9]* | openai/openai/gpt-[1-9][0-9]* | \
github_models/*)
github_models/openai/gpt-5* | github_models/openai/gpt-[6-9]* | github_models/openai/gpt-[1-9][0-9]* | \
github_models/deepseek/deepseek-r1-0528 | github_models/deepseek/deepseek-v3-0324)
echo 'enabled=true' >> "$GITHUB_OUTPUT"
echo 'provider_mode=github_models' >> "$GITHUB_OUTPUT"
sanitized_github_models_token="$(printf '%s' "$STRIX_GITHUB_MODELS_TOKEN" | tr -d '\r\n')"
Expand Down Expand Up @@ -252,7 +253,7 @@ jobs:
- name: Prepare Strix model input file
if: steps.gate.outputs.enabled == 'true'
env:
STRIX_MODEL: ${{ github.event.inputs.strix_llm || 'openai/openai/gpt-5' }}
STRIX_MODEL: ${{ github.event.inputs.strix_llm || 'openai/gpt-5' }}
run: |
umask 077
strix_llm_file="$RUNNER_TEMP/strix_llm.txt"
Expand All @@ -266,7 +267,8 @@ jobs:
;;
openai/gpt-5* | openai/gpt-[6-9]* | openai/gpt-[1-9][0-9]* | \
openai/openai/gpt-5* | openai/openai/gpt-[6-9]* | openai/openai/gpt-[1-9][0-9]* | \
github_models/*)
github_models/openai/gpt-5* | github_models/openai/gpt-[6-9]* | github_models/openai/gpt-[1-9][0-9]* | \
github_models/deepseek/deepseek-r1-0528 | github_models/deepseek/deepseek-v3-0324)
printf '%s' "${strix_model#github_models/}" > "$strix_llm_file"
;;
openai/*)
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile.ollama
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ RUN set -eux; \
sleep 1; \
done; \
if [ "${server_ready}" != "true" ]; then cat /tmp/ollama-build.log; exit 1; fi; \
ollama pull gemma4; \
ollama list | grep -E '^gemma4(:|[[:space:]])'; \
ollama pull gemma4:e2b-it-qat; \
ollama list | grep -E '^gemma4:e2b-it-qat[[:space:]]'; \
kill "$server_pid"; \
wait "$server_pid" 2>/dev/null || true; \
chown -R ollama:ollama /usr/share/ollama/.ollama
Expand Down
15 changes: 7 additions & 8 deletions backend/api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
from sqlalchemy import select, func, union_all
from db.session import get_db
from db.models import Email, Attachment
from services.embedding import generate_embeddings
from services.embedding import STORAGE_EMBEDDING_DIMENSION, fit_embedding_vector, generate_embeddings
from api.auth import AuthContext, get_auth_context
from services.llm_provider_selection import resolve_runtime_llm_provider

router = APIRouter(prefix="/api")
logger = logging.getLogger(__name__)
SEARCH_VECTOR_DIMENSIONS = 1536
SEARCH_VECTOR_DIMENSIONS = STORAGE_EMBEDDING_DIMENSION


class SearchRequest(BaseModel):
Expand Down Expand Up @@ -199,12 +199,11 @@ async def hybrid_search(
base_url=runtime_provider.base_url,
model=runtime_provider.embedding_model,
)
query_embedding = embeddings[0] if embeddings else None
if (
query_embedding is not None
and len(query_embedding) != SEARCH_VECTOR_DIMENSIONS
):
query_embedding = None
query_embedding = (
fit_embedding_vector(embeddings[0], SEARCH_VECTOR_DIMENSIONS)
if embeddings
else None
)

owner_filters = email_owner_filters(
target_user_id, auth_context.organization_id
Expand Down
13 changes: 10 additions & 3 deletions backend/import_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
from db.session import AsyncSessionLocal
from db.models import Email, Attachment
from services.email_parser import parse_eml
from services.embedding import generate_embeddings
from services.embedding import (
STORAGE_EMBEDDING_DIMENSION,
fit_embedding_vector,
generate_embeddings,
)
from services.threading_service import assign_thread_id
import os
from sqlalchemy import select

EMBEDDING_DIMENSION = 1536
EMBEDDING_DIMENSION = STORAGE_EMBEDDING_DIMENSION
IMPORT_USER_ID = os.environ.get("NARUON_IMPORT_USER_ID", "default")
IMPORT_ORGANIZATION_ID = os.environ.get("NARUON_IMPORT_ORGANIZATION_ID", "default")

Expand All @@ -24,7 +28,10 @@ async def generate_fixture_embedding(text: str) -> list[float]:
if not openai_api_key:
return [0.0] * EMBEDDING_DIMENSION

return (await generate_embeddings([text], openai_api_key=openai_api_key))[0]
embeddings = await generate_embeddings([text], openai_api_key=openai_api_key)
if not embeddings:
return [0.0] * EMBEDDING_DIMENSION
return fit_embedding_vector(embeddings[0], EMBEDDING_DIMENSION)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved


async def import_eml_file(session, eml_file: Path) -> bool:
Expand Down
12 changes: 10 additions & 2 deletions backend/scripts/import_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

from services.archive import extract_backup_async
from services.email_parser import parse_eml
from services.embedding import chunk_text, generate_embeddings
from services.embedding import (
STORAGE_EMBEDDING_DIMENSION,
chunk_text,
fit_embedding_vector,
generate_embeddings,
)
from services.threading_service import assign_thread_id
from db.session import AsyncSessionLocal
from db.models import Email, TenantConfig
Expand Down Expand Up @@ -61,7 +66,10 @@ async def process_zip_file(zip_path: str | Path, session: AsyncSession):
) or ""
embeddings = await generate_embeddings([chunks[0]], api_key)
if embeddings:
embedding = embeddings[0]
embedding = fit_embedding_vector(
embeddings[0],
STORAGE_EMBEDDING_DIMENSION,
)
except Exception as e:
logger.error(
f"Failed to generate embedding for {email_data['message_id']}: {e}"
Expand Down
16 changes: 16 additions & 0 deletions backend/services/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from services.llm_provider_urls import build_llm_provider_http_client
from services.exceptions import EmbeddingGenerationError

STORAGE_EMBEDDING_DIMENSION = 1536


def chunk_text(
text: str, chunk_size: int = 1000, chunk_overlap: int = 200
Expand All @@ -18,6 +20,20 @@ def chunk_text(
return splitter.split_text(text)


def fit_embedding_vector(
embedding: list[float],
target_dimension: int = STORAGE_EMBEDDING_DIMENSION,
) -> list[float]:
if target_dimension <= 0:
raise ValueError("target_dimension must be positive")

if len(embedding) == target_dimension:
return list(embedding)
if len(embedding) < target_dimension:
return [*embedding, *([0.0] * (target_dimension - len(embedding)))]
return embedding[:target_dimension]


async def generate_embeddings(
texts: list[str],
openai_api_key: str,
Expand Down
22 changes: 21 additions & 1 deletion backend/tests/test_embedding.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import pytest
import openai
from unittest.mock import patch, AsyncMock
from services.embedding import chunk_text, generate_embeddings
from services.embedding import (
STORAGE_EMBEDDING_DIMENSION,
chunk_text,
fit_embedding_vector,
generate_embeddings,
)
from services.exceptions import EmbeddingGenerationError


Expand All @@ -12,6 +17,21 @@ def test_chunk_text():
assert len(chunks[0]) <= 50


def test_fit_embedding_vector_pads_embeddinggemma_dimension_to_storage_vector():
fitted = fit_embedding_vector([0.25] * 768)

assert len(fitted) == STORAGE_EMBEDDING_DIMENSION
assert fitted[:768] == [0.25] * 768
assert fitted[768:] == [0.0] * (STORAGE_EMBEDDING_DIMENSION - 768)


def test_fit_embedding_vector_truncates_larger_provider_dimension():
fitted = fit_embedding_vector([0.5] * 3072)

assert len(fitted) == STORAGE_EMBEDDING_DIMENSION
assert fitted == [0.5] * STORAGE_EMBEDDING_DIMENSION


@pytest.mark.asyncio
async def test_generate_embeddings_success():
with patch(
Expand Down
49 changes: 49 additions & 0 deletions backend/tests/test_import_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,55 @@ async def commit(self):
assert session.added.embedding == [0.0] * 1536


@pytest.mark.asyncio
async def test_root_importer_handles_empty_embedding_provider_response(
tmp_path, monkeypatch
):
class MockResult:
def scalar_one_or_none(self):
return None

class MockSession:
def __init__(self):
self.added = None

async def execute(self, _query):
return MockResult()

def add(self, obj):
self.added = obj

async def commit(self):
pass

eml_file = tmp_path / "empty-embedding.eml"
eml_file.write_text("Message-ID: <empty-embedding@example.com>\n\nBody")
parsed = {
"message_id": "<empty-embedding@example.com>",
"sender": "sender@example.com",
"recipients": "user@example.com",
"subject": "Empty embedding",
"date": datetime.datetime.now(datetime.timezone.utc),
"body": "Body",
"attachments": [],
}
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
session = MockSession()

with patch.object(import_fixtures, "parse_eml", return_value=parsed), patch.object(
import_fixtures, "generate_embeddings", new_callable=AsyncMock
) as mock_embeddings, patch.object(
import_fixtures, "assign_thread_id", new_callable=AsyncMock
) as mock_assign:
mock_embeddings.return_value = []
mock_assign.return_value = "empty-embedding-thread"

imported = await import_fixtures.import_eml_file(session, eml_file)

assert imported is True
assert session.added.embedding == [0.0] * 1536


@pytest.mark.asyncio
async def test_root_importer_rolls_back_and_returns_false_on_commit_failure(tmp_path):
class MockResult:
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_release_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def test_backend_compose_commands_use_startup_preflight() -> None:
assert 'OLLAMA_NO_CLOUD: "true"' in compose
assert 'OLLAMA_NO_CLOUD: "true"' in live_e2e_compose
assert "OPENAI_BASE_URL: http://ollama:11434/v1" in live_e2e_compose
assert "OPENAI_MODEL: gemma4" in live_e2e_compose
assert "OPENAI_MODEL: gemma4:e2b-it-qat" in live_e2e_compose
assert "OPENAI_EMBEDDING_MODEL: embeddinggemma" in live_e2e_compose
live_nginx = read_repo_text("tests/live/nginx.conf")
assert "proxy_read_timeout 600s" in live_nginx
Expand Down
22 changes: 17 additions & 5 deletions backend/tests/test_repo_hygiene.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ def test_ollama_dockerfile_keeps_pulled_models_available_to_runtime_user():
assert "for attempt in $(seq 1 60)" in dockerfile
assert "if ollama list > /dev/null 2>&1" in dockerfile
assert "cat /tmp/ollama-build.log; exit 1" in dockerfile
assert "ollama pull gemma4" in dockerfile
assert "ollama pull gemma4:e2b-it-qat" in dockerfile
assert "ollama pull embeddinggemma" in dockerfile
assert dockerfile.count("RUN set -eux;") >= 2
assert dockerfile.find("ollama pull gemma4") < dockerfile.find(
assert dockerfile.find("ollama pull gemma4:e2b-it-qat") < dockerfile.find(
"ollama pull embeddinggemma"
)
assert "ollama list | grep -E '^gemma4(:|[[:space:]])'" in dockerfile
assert "ollama list | grep -E '^gemma4:e2b-it-qat[[:space:]]'" in dockerfile
assert (
"ollama list | grep -E '^embeddinggemma(:|[[:space:]])'" in dockerfile
)
Expand Down Expand Up @@ -85,15 +85,27 @@ def test_compose_externalizes_postgres_credentials():

assert "POSTGRES_PASSWORD: postgres" not in compose
assert "postgres:postgres@" not in compose
assert "${POSTGRES_DB" in compose
assert "${POSTGRES_USER" in compose
assert "POSTGRES_DB: ai_email" in compose
assert "POSTGRES_USER: postgres" in compose
assert "${POSTGRES_PASSWORD" in compose
assert "127.0.0.1:15432:5432" in compose
assert "AUTH_SESSION_HMAC_SECRET" in compose
assert "${AUTH_SESSION_HMAC_SECRET:?" in compose
assert "ENCRYPTION_KEY" in compose
assert "${ENCRYPTION_KEY:?" in compose


def test_compose_allows_only_the_local_ollama_provider_host():
local_compose = (REPO_ROOT / "docker-compose.yml").read_text()
live_compose = (REPO_ROOT / "docker-compose.live-e2e.yml").read_text()

for compose in (local_compose, live_compose):
assert 'ALLOW_LOCAL_LLM_PROVIDERS: "true"' in compose
assert 'ALLOWED_LLM_BASE_URL_HOSTS: "ollama"' in compose
assert "OPENAI_BASE_URL: http://ollama:11434/v1" in compose
assert "OPENAI_MODEL: gemma4:e2b-it-qat" in compose


def test_compose_wrapper_uses_operator_env_file_without_bulk_secret_injection():
wrapper = (REPO_ROOT / "scripts" / "naruon_compose.sh").read_text()
gateway_compose = (REPO_ROOT / "docker-compose.gateway.yml").read_text()
Expand Down
4 changes: 2 additions & 2 deletions backend/tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async def override_scoped_db():


@patch("api.search.generate_embeddings", new_callable=AsyncMock)
def test_search_falls_back_to_text_rank_when_embedding_dimension_mismatches(
def test_search_pads_local_embedding_dimension_for_vector_search(
mock_generate_embeddings,
):
mock_generate_embeddings.return_value = [[0.1] * 768]
Expand All @@ -228,4 +228,4 @@ async def override_scoped_db():
assert response.status_code == 200
query_text = str(session.statements[-1]).lower()
assert "ts_rank_cd" in query_text
assert "<=>" not in query_text
assert "<=>" in query_text
12 changes: 6 additions & 6 deletions docker-compose.live-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ services:
db:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: ${POSTGRES_DB:-ai_email_live}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_DB: ai_email_live
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD for live E2E}
POSTGRES_INITDB_ARGS: "--auth-local=scram-sha-256 --auth-host=scram-sha-256"
healthcheck:
Expand All @@ -29,7 +29,7 @@ services:
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL for live E2E}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?Set ENCRYPTION_KEY for live E2E}
AUTH_SESSION_HMAC_SECRET: ${AUTH_SESSION_HMAC_SECRET:?Set AUTH_SESSION_HMAC_SECRET for live E2E}
TRUST_DEV_HEADERS: ${TRUST_DEV_HEADERS:-false}
TRUST_DEV_HEADERS: "false"
DEBUG: "false"
OPENAI_API_KEY: ""
depends_on:
Expand All @@ -43,7 +43,7 @@ services:
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL for live E2E}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?Set ENCRYPTION_KEY for live E2E}
AUTH_SESSION_HMAC_SECRET: ${AUTH_SESSION_HMAC_SECRET:?Set AUTH_SESSION_HMAC_SECRET for live E2E}
TRUST_DEV_HEADERS: ${TRUST_DEV_HEADERS:-false}
TRUST_DEV_HEADERS: "false"
DEBUG: "false"
OPENAI_API_KEY: ""
depends_on:
Expand All @@ -57,14 +57,14 @@ services:
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL for live E2E}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?Set ENCRYPTION_KEY for live E2E}
AUTH_SESSION_HMAC_SECRET: ${AUTH_SESSION_HMAC_SECRET:?Set AUTH_SESSION_HMAC_SECRET for live E2E}
TRUST_DEV_HEADERS: ${TRUST_DEV_HEADERS:-false}
TRUST_DEV_HEADERS: "false"
DEBUG: "false"
ALLOW_LOCAL_LLM_PROVIDERS: "true"
ALLOWED_LLM_BASE_URL_HOSTS: "ollama"
OPENAI_API_KEY: ollama
OPENAI_BASE_URL: http://ollama:11434/v1
OPENAI_EMBEDDING_MODEL: embeddinggemma
OPENAI_MODEL: gemma4
OPENAI_MODEL: gemma4:e2b-it-qat
depends_on:
live-seed:
condition: service_completed_successfully
Expand Down
Loading
Loading