Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ jobs:
run: |
umask 077
strix_llm_file="$RUNNER_TEMP/strix_llm.txt"
printf '%s' "github_models/gpt-5.4" > "$strix_llm_file"
printf '%s' "github_models/gpt-4o" > "$strix_llm_file"
echo "STRIX_LLM_FILE=$strix_llm_file" >> "$GITHUB_ENV"

- name: Prepare LLM API base input file
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
explicit `if: ${{ always() }}` upload steps when needed.
- Prefer upgrading or removing vulnerable dependencies over downgrading patched
packages unless compatibility evidence is recorded in the PR.
- Strix Security Scan must use `github_models/gpt-5.4` as the default model to bypass Vertex AI GCP credential prerequisites in PR bounds.
- Strix Security Scan must use `github_models/gpt-4o` as the default model to bypass Vertex AI GCP credential prerequisites in PR bounds.

## PR automation and review defaults

Expand Down
Binary file modified backend/__pycache__/import_fixtures.cpython-310.pyc
Binary file not shown.
Binary file modified backend/__pycache__/main.cpython-310.pyc
Binary file not shown.
Binary file modified backend/api/__pycache__/accounts.cpython-310.pyc
Binary file not shown.
Binary file modified backend/api/__pycache__/dav.cpython-310.pyc
Binary file not shown.
Binary file modified backend/api/__pycache__/emails.cpython-310.pyc
Binary file not shown.
Binary file modified backend/api/__pycache__/ontology.cpython-310.pyc
Binary file not shown.
Binary file modified backend/api/__pycache__/runner_ws.cpython-310.pyc
Binary file not shown.
19 changes: 19 additions & 0 deletions backend/api/caldav_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi import APIRouter, Depends, HTTPException
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
from typing import Dict, Any

router = APIRouter()

@router.post("/writeback-intent")
async def register_writeback_intent(payload: Dict[str, Any]):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
# In a real scenario, this would use the auth context dependency.
# Currently just a stub for Phase 10 API wiring.
account_map = payload.get("calendar_account_map", {})
if not account_map:
raise HTTPException(status_code=400, detail="Missing account mapping")

return {"status": "success", "message": "Writeback intent registered", "target": account_map}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
@router.post("/sync")
async def sync_caldav_accounts():
# Deprecated legacy endpoint path, should fail or require strict auth
raise HTTPException(status_code=403, detail="Use specific writeback intent and verified sources instead.")
Binary file modified backend/core/__pycache__/config.cpython-310.pyc
Binary file not shown.
Binary file modified backend/db/__pycache__/models.cpython-310.pyc
Binary file not shown.
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ opentelemetry-sdk==1.25.0
opentelemetry-instrumentation-fastapi==0.46b0
opentelemetry-exporter-otlp==1.25.0
setuptools==78.1.1
websockets==14.1
57 changes: 57 additions & 0 deletions backend/runner/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import asyncio
import logging
from typing import Dict, Any

try:
import websockets
except ImportError:
# Optional dependency for the runner
websockets = None

logger = logging.getLogger(__name__)

class SelfHostedConnector:
def __init__(self, target_ws_url: str, token: str):
self.target_ws_url = target_ws_url
self.token = token
self.connection = None
self.is_connected = False

async def connect(self):
if websockets is None:
logger.error("websockets library is not installed. Runner cannot start.")
return

headers = {"Authorization": f"Bearer {self.token}"}
try:
self.connection = await websockets.connect(self.target_ws_url, extra_headers=headers)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
self.is_connected = True
logger.info(f"Connected to Naruon Gateway at {self.target_ws_url}")
await self._listen_loop()
except Exception as e:
logger.error(f"Failed to connect to Naruon Gateway: {e}")
self.is_connected = False
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

async def _listen_loop(self):
try:
while self.is_connected:
message = await self.connection.recv()
await self.handle_message(message)
except websockets.exceptions.ConnectionClosed:
logger.warning("Connection closed by remote gateway.")
self.is_connected = False
Comment on lines +54 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect broad catches in connector:"
rg -nP --type=py -C2 '\bexcept\s+Exception\b' backend/runner/connector.py

echo
echo "Inspect listen loop region:"
cat -n backend/runner/connector.py | sed -n '47,70p'

Repository: Seongho-Bae/naruon

Length of output: 1890


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Top of backend/runner/connector.py (imports + class header):"
cat -n backend/runner/connector.py | sed -n '1,120p'

echo
echo "Confirm whether asyncio is imported:"
rg -n --type=py '\bimport\s+asyncio\b|\bfrom\s+asyncio\s+import\b' backend/runner/connector.py

echo
echo "Confirm websockets import:"
rg -n --type=py '\bimport\s+websockets\b|\bfrom\s+websockets\s+import\b' backend/runner/connector.py

Repository: Seongho-Bae/naruon

Length of output: 3720


Narrow SelfHostedConnector._listen_loop exception handling to avoid masking logic bugs.

In backend/runner/connector.py lines 54-59, _listen_loop catches all Exception, logs a warning, and stops the loop—this suppresses failures from handle_message and can hide programming errors.

♻️ Proposed fix
     async def _listen_loop(self):
         if not self.connection:
             return
         try:
             while self.is_connected:
                 message = await self.connection.recv()
                 await self.handle_message(message)
-        except Exception as e:
-            if websockets and isinstance(e, websockets.exceptions.ConnectionClosed):
+        except asyncio.CancelledError:
+            raise
+        except (OSError, asyncio.TimeoutError) as e:
+            logger.warning(f"Connection loop ended: {e}")
+            self.is_connected = False
+        except Exception as e:
+            if websockets and isinstance(e, websockets.exceptions.ConnectionClosed):
                 logger.warning("Connection closed by remote gateway.")
+                self.is_connected = False
             else:
-                logger.warning(f"Connection loop ended: {e}")
-            self.is_connected = False
+                raise
🧰 Tools
🪛 Ruff (0.15.13)

[warning] 54-54: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/runner/connector.py` around lines 54 - 59, The broad except Exception
in SelfHostedConnector._listen_loop is masking programming errors from
handle_message; narrow it to catch only expected shutdown exceptions (e.g.,
websockets.exceptions.ConnectionClosed and asyncio.CancelledError) and handle
them by logging the connection-close path and setting self.is_connected = False,
but for any other Exception log the full traceback (use logger.exception) and
re-raise so bugs aren't suppressed; update _listen_loop to import asyncio if
needed and ensure self.is_connected is cleared in the shutdown path or a finally
block.


async def handle_message(self, message: str):
# Dispatch message to internal SMTP/IMAP proxy handlers
logger.debug(f"Received instruction from gateway: {message}")
pass
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

async def send_response(self, response: Dict[str, Any]):
if self.is_connected and self.connection:
import json
await self.connection.send(json.dumps(response))

if __name__ == "__main__":
# Example usage for local bootstrap
connector = SelfHostedConnector("ws://localhost:8080/api/runner/ws", "sample-token")
asyncio.run(connector.connect())
Binary file modified backend/scripts/__pycache__/bootstrap_db.cpython-310.pyc
Binary file not shown.
Binary file modified backend/scripts/__pycache__/import_fixtures.cpython-310.pyc
Binary file not shown.
Binary file modified backend/services/__pycache__/access_policy.cpython-310.pyc
Binary file not shown.
Binary file modified backend/services/__pycache__/archive.cpython-310.pyc
Binary file not shown.
Binary file modified backend/services/__pycache__/calendar_sync.cpython-310.pyc
Binary file not shown.
Binary file modified backend/services/__pycache__/knowledge_extractor.cpython-310.pyc
Binary file not shown.
Binary file modified backend/services/__pycache__/text_safety.cpython-310.pyc
Binary file not shown.
Binary file modified backend/services/__pycache__/threading_service.cpython-310.pyc
Binary file not shown.
26 changes: 26 additions & 0 deletions backend/services/caldav_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import logging
from typing import Dict, Any

logger = logging.getLogger(__name__)

class CalDavService:
def __init__(self):
pass

def determine_writeback_target(self, task_context: Dict[str, Any], connected_accounts: list) -> str:
"""
Determines the most appropriate CalDav account to write back to,
based on the context of the task (e.g., if it originated from a company email).
"""
# Basic ontology/context mock logic
source_email = task_context.get("source_email", "")
for account in connected_accounts:
if account.get("domain") in source_email:
return account.get("account_id")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Fallback to the primary account
if connected_accounts:
return connected_accounts[0].get("account_id")
return "default_system_caldav"

caldav_service = CalDavService()
37 changes: 37 additions & 0 deletions backend/services/email_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import hashlib
import logging
from typing import Dict, Any

logger = logging.getLogger(__name__)

def generate_email_fingerprint(email_data: Dict[str, Any]) -> str:
"""
Generates a unique fingerprint for an email based on its sender, subject, date, and body content.
Used to de-duplicate emails from ZIP imports or forwarding loops.
"""
sender = email_data.get("sender", "")
subject = email_data.get("subject", "")
date = str(email_data.get("date", ""))
body_snippet = email_data.get("body", "")[:500] # First 500 chars
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

raw_str = f"{sender}|{subject}|{date}|{body_snippet}"
return hashlib.sha256(raw_str.encode("utf-8")).hexdigest()

def detect_reply_tracking(email_data: Dict[str, Any]) -> bool:
"""
Detects if the user sent an email that expects a reply.
"""
body = email_data.get("body", "").lower()
return "please reply" in body or "?" in body

def process_self_to_self(email_data: Dict[str, Any], user_email: str) -> bool:
"""
Detects if an email is sent from the user to themselves, turning it into a knowledge node.
"""
sender = email_data.get("sender", "")
recipients = email_data.get("recipients", "")

if user_email in sender and user_email in recipients:
logger.info(f"Self-to-self email detected. Organizing as knowledge node.")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return True
return False
36 changes: 36 additions & 0 deletions backend/services/ontology_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging
from typing import Dict, Any

logger = logging.getLogger(__name__)

class OntologyService:
def __init__(self):
self.relationships = {}

def analyze_sender_relationship(self, user_email: str, sender_email: str, email_content: str) -> Dict[str, Any]:
"""
Analyzes the email content to build a relationship graph (DAG) between the user and the sender.
Returns attributes like the relationship type (e.g., Colleague, Client, Newsletter, Unknown)
and confidence score.
"""
# A simple stub logic for Phase 10 implementation
relationship_type = "Unknown"
confidence = 0.5

if "unsubscribe" in email_content.lower():
relationship_type = "Newsletter"
confidence = 0.9
elif "@" in user_email and "@" in sender_email:
user_domain = user_email.split("@")[1]
sender_domain = sender_email.split("@")[1]
if user_domain == sender_domain:
relationship_type = "Colleague"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
confidence = 0.85

logger.info(f"Analyzed relationship: {sender_email} -> {relationship_type} (conf: {confidence})")
return {
"type": relationship_type,
"confidence": confidence
}

ontology_service = OntologyService()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified backend/tests/__pycache__/test_main.cpython-310-pytest-9.0.3.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified backend/tests/__pycache__/test_search.cpython-310-pytest-9.0.3.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18 changes: 18 additions & 0 deletions backend/tests/test_caldav.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest
from services.caldav_service import caldav_service

def test_determine_writeback_target():
connected_accounts = [
{"account_id": "account1", "domain": "company.com"},
{"account_id": "account2", "domain": "personal.com"}
]

# Should match company.com
task_context_1 = {"source_email": "boss@company.com"}
target_1 = caldav_service.determine_writeback_target(task_context_1, connected_accounts)
assert target_1 == "account1"

# Should fallback to primary
task_context_2 = {"source_email": "friend@other.com"}
target_2 = caldav_service.determine_writeback_target(task_context_2, connected_accounts)
assert target_2 == "account1"
11 changes: 11 additions & 0 deletions backend/tests/test_ontology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import pytest
from services.ontology_service import ontology_service

def test_analyze_sender_relationship():
result1 = ontology_service.analyze_sender_relationship("seongho@company.com", "newsletter@marketing.com", "Please unsubscribe here")
assert result1["type"] == "Newsletter"
assert result1["confidence"] == 0.9

result2 = ontology_service.analyze_sender_relationship("seongho@company.com", "boss@company.com", "Hello")
assert result2["type"] == "Colleague"
assert result2["confidence"] == 0.85
12 changes: 12 additions & 0 deletions docker-compose.infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ services:
networks:
- naruon-network

keycloak:
image: quay.io/keycloak/keycloak:24.0.0
command: start-dev
environment:
KC_DB: dev-file
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "8081:8080"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
networks:
- naruon-network

networks:
naruon-network:
driver: bridge
18 changes: 14 additions & 4 deletions frontend/src/components/EmailDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
type ThreadEmailData,
} from "@/lib/email-threading";

type EmailData = ThreadEmailData;

type EmailData = ThreadEmailData & {
requires_reply?: boolean;
schedule_conflict?: boolean;
};
interface LlmData {
summary: string;
todos: string[];
Expand Down Expand Up @@ -320,8 +322,16 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number
답장 주소: {email.reply_to || email.sender}
</div>
</div>
<div className="hidden whitespace-nowrap rounded-full border border-border bg-card px-3 py-1 text-xs font-medium text-muted-foreground shadow-sm 2xl:block">
{formatEmailDate(email.date)}
<div className="flex flex-col items-end gap-2">
<div className="hidden whitespace-nowrap rounded-full border border-border bg-card px-3 py-1 text-xs font-medium text-muted-foreground shadow-sm 2xl:block">
{formatEmailDate(email.date)}
</div>
{email.requires_reply && (
<Badge variant="outline" className="border-primary/30 text-primary bg-primary/5 text-[10px]">응답 대기 중</Badge>
)}
{email.schedule_conflict && (
<Badge variant="outline" className="border-emerald-500/30 text-emerald-700 bg-emerald-500/5 text-[10px]">일정 충돌 조율</Badge>
)}
</div>
</div>
</div>
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/EmailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface EmailItem {
reply_count?: number;
has_draft?: boolean;
is_self_sent?: boolean;
requires_reply?: boolean;
schedule_conflict?: boolean;
}

let inboxRequest: Promise<EmailItem[]> | null = null;
Expand Down Expand Up @@ -177,11 +179,13 @@ export function EmailList({
<div className="line-clamp-2 w-full text-xs leading-5 text-muted-foreground">
{safeSnippet}
</div>
{(email.unread || email.has_draft || email.is_self_sent) && (
<div className="flex items-center gap-2">
{(email.unread || email.has_draft || email.is_self_sent || email.requires_reply || email.schedule_conflict) && (
<div className="flex items-center gap-2 flex-wrap">
{email.unread && <Badge variant="default" className="bg-emerald-500 text-[10px] text-white">새 메일</Badge>}
{email.has_draft && <Badge variant="secondary" className="border-blue-500/20 bg-blue-500/10 text-[10px] text-blue-700">답장 초안</Badge>}
{email.is_self_sent && <Badge variant="secondary" className="border-purple-500/20 bg-purple-500/10 text-[10px] text-purple-700">지식 정리</Badge>}
{email.requires_reply && <Badge variant="outline" className="border-primary/30 text-[10px] text-primary bg-primary/5">응답 대기 중</Badge>}
{email.schedule_conflict && <Badge variant="outline" className="border-emerald-500/30 text-[10px] text-emerald-700 bg-emerald-500/5">일정 충돌 조율</Badge>}
</div>
)}
</button>
Expand Down
Loading