Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Binary file removed .DS_Store
Binary file not shown.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ POSTGRES_USER=postgres
POSTGRES_PASSWORD=change-me-local-only
DATABASE_URL=postgresql+asyncpg://postgres:change-me-local-only@localhost:5432/ai_email
DEBUG=false
ENCRYPTION_KEY=
# Optional: set only to a high-entropy Fernet.generate_key() value.
# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# ENCRYPTION_KEY=

# AI features. Leave blank to disable LLM/embedding-backed flows locally.
OPENAI_API_KEY=
Expand Down
22 changes: 15 additions & 7 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ jobs:
# On protected/privileged events, fail-closed: missing secrets must
# not silently produce a green check.
if [ "$GITHUB_EVENT_NAME" = "push" ] || [ "$GITHUB_EVENT_NAME" = "schedule" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_target" ]; then
echo '::error::Strix secrets not configured on protected or privileged event; failing closed.'
echo '::error::Strix LLM_API_KEY secret not configured on protected or privileged event; failing closed.'
exit 1
fi
echo 'enabled=false' >> "$GITHUB_OUTPUT"
echo 'Strix secrets not configured; skipping.'
echo 'Strix LLM_API_KEY secret not configured; skipping.'
else
echo 'enabled=true' >> "$GITHUB_OUTPUT"
fi
Expand All @@ -114,7 +114,8 @@ jobs:
id: auth_gate
if: steps.gate.outputs.enabled == 'true'
env:
DEFAULT_PROVIDER: gemini
STRIX_LLM: ${{ secrets.STRIX_LLM }}
DEFAULT_PROVIDER: github
run: |
. "$TRUSTED_WORKSPACE/scripts/ci/strix_model_utils.sh"
strix_llm="github/gpt-4o"
Expand Down Expand Up @@ -222,10 +223,16 @@ jobs:

- name: Prepare Strix model input file
if: steps.gate.outputs.enabled == 'true' && steps.auth_gate.outputs.can_run == 'true'
env:
STRIX_LLM_SECRET: ${{ secrets.STRIX_LLM }}
run: |
umask 077
strix_llm_file="$RUNNER_TEMP/strix_llm.txt"
printf '%s' "github/gpt-4o" > "$strix_llm_file"
if [ -n "$STRIX_LLM_SECRET" ]; then
printf '%s' "$STRIX_LLM_SECRET" > "$strix_llm_file"
else
printf '%s' "github/gpt-5.4" > "$strix_llm_file"
fi
echo "STRIX_LLM_FILE=$strix_llm_file" >> "$GITHUB_ENV"

- name: Prepare LLM API base input file
Expand All @@ -244,10 +251,11 @@ jobs:
if: steps.gate.outputs.enabled == 'true' && steps.auth_gate.outputs.can_run == 'true'
env:
STRIX_LLM_FILE: ${{ env.STRIX_LLM_FILE }}
STRIX_LLM_DEFAULT_PROVIDER: gemini
STRIX_LLM_DEFAULT_PROVIDER: github
GEMINI_LOCATION: GLOBAL
LLM_API_KEY_FILE: ${{ env.LLM_API_KEY_FILE }}
LLM_API_BASE_FILE: ${{ env.LLM_API_BASE_FILE }}
STRIX_GITHUB_FALLBACK_MODELS: "github/gpt-5.4-mini github/gpt-4o"
STRIX_GEMINI_FALLBACK_MODELS: "gemini/gemini-2.5-flash gemini/gemini-2.5-pro"
STRIX_VERTEX_FALLBACK_MODELS: "vertex_ai/gemini-2.5-pro vertex_ai/gemini-2.5-flash"
STRIX_TARGET_PATH: ./
Expand All @@ -256,11 +264,11 @@ jobs:
STRIX_LLM_MAX_RETRIES: 1
LLM_TIMEOUT: 90
STRIX_MEMORY_COMPRESSOR_TIMEOUT: 10
STRIX_TRANSIENT_RETRY_PER_MODEL: 2
STRIX_TRANSIENT_RETRY_PER_MODEL: ${{ github.event_name == 'pull_request_target' && '0' || '2' }}
STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 3
STRIX_PROCESS_TIMEOUT_SECONDS: ${{ github.event_name == 'pull_request_target' && '1200' || '2400' }}
STRIX_TOTAL_TIMEOUT_SECONDS: 4800
STRIX_PR_SCOPE_MAX_FILES_PER_BATCH: 12
STRIX_PR_SCOPE_MAX_FILES_PER_BATCH: ${{ github.event_name == 'pull_request_target' && '3' || '12' }}
STRIX_FAIL_ON_MIN_SEVERITY: MEDIUM
STRIX_DISABLE_PR_SCOPING: ${{ github.event_name == 'pull_request_target' && '0' || '1' }}
GH_TOKEN: ${{ github.event_name == 'pull_request_target' && github.token || '' }}
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,3 @@ frontend/test-results/
frontend/playwright-report/
frontend/playwright/.cache/
frontend/trace_output/

.worktrees/
29 changes: 23 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
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/gpt-4o` as the default model to bypass Vertex AI GCP credential prerequisites in PR bounds.
- Strix Security Scan must use GitHub Models with `github/gpt-5.4` or a newer
GPT-5.4-family route as the default model to bypass Vertex AI GCP credential
prerequisites in PR bounds.

## PR automation and review defaults

Expand Down Expand Up @@ -41,18 +43,27 @@
Data needs repository/ingestion/embedding/quality/WebDAV queues; Security and
Settings need governance and operational control surfaces. Keep provider writes
labeled as future work until source-backed integrations exist.
- Browser frontend writes to signed backend routes must carry the stored
`naruon_session_token` as `Authorization: Bearer` and must not emit or forward
public identity headers such as `X-User-Id`, `X-Organization-Id`,
`X-Group-Id`, `X-Group-Ids`, `X-User-Role`, or `X-Dev-Auth-Token`;
tests/mocks must exercise the signed-session path.
- Browser frontend writes to signed backend routes must rely on the HttpOnly
`naruon_session_token` cookie with `credentials: include`; browser code must
not persist or read that session token through `localStorage`/`sessionStorage`
and must not emit or forward public identity headers such as `X-User-Id`,
`X-Organization-Id`, `X-Group-Id`, `X-Group-Ids`, `X-User-Role`, or
`X-Dev-Auth-Token`; unsafe cookie-authenticated backend methods must pass the
server-side `Origin`/`Referer` allowlist in `ALLOWED_BROWSER_ORIGINS`; production
must set deployed non-localhost origins explicitly, and tests/mocks must
exercise the signed-session path.
- Private backend `/api/*` routers must be registered with the default
`get_auth_context` signed-session dependency; only explicitly documented
public endpoints such as `/api/runtime-config`, `/`, and `/metrics` may omit
it. Admin/provider registry endpoints must enforce role checks in addition to
authentication. LLM provider `base_url` values must fail closed unless they are
HTTPS, exact-host allowlisted by `ALLOWED_LLM_BASE_URL_HOSTS`, and resolve only
to global addresses.
- Tenant SMTP egress must default to `ALLOWED_SMTP_HOSTS=__deny_all__`, reject
wildcard host allowlists and non-SMTP ports at settings load, and still validate
every final DNS answer as globally routable before opening a pinned SMTP socket.
Secret-field encryption must reject missing, invalid, known weak, repeated, or
low-entropy Fernet `ENCRYPTION_KEY` values before encrypting or decrypting.
- Email-derived tasks must stay source-linked to the email/thread and tenant
owner scope. Do not expose new sequential database ids through task APIs; use
opaque public ids for user-visible ticket tasks. Task titles are plain text:
Expand All @@ -71,6 +82,12 @@
server-authoritative source selection and provenance. Do not wire browser
actions back to legacy `/api/calendar/sync` unless a trusted backend credential
dependency and source-owner contract are explicitly in scope.
- Long destination pages rendered inside `DashboardLayout` must provide their own
`max-h-full overflow-y-auto` content region, because the shell intentionally
uses viewport-height overflow containment. Do not leave Settings/Data/Security
style pages as clipped legacy `bg-white` islands; use tokenized `bg-card`
surfaces and persistent labels for provider, IMAP, SMTP, CalDAV, and WebDAV
credentials so typed values do not erase field meaning.

## Development environment and tooling defaults

Expand Down
68 changes: 46 additions & 22 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ described as real delivery.
Tenant-provided SMTP destinations are not a general outbound socket primitive.
`backend/api/tenant_config.py`, `backend/api/emails.py`, and the final
`backend/services/email_client.py` network sink enforce the operator-controlled
`ALLOWED_SMTP_HOSTS` and `ALLOWED_SMTP_PORTS` allowlists. The service also
rejects loopback, link-local, private, reserved, and otherwise non-global DNS
answers before opening a pinned socket to the selected global address, so stale
database rows or direct service calls fail closed instead of reaching internal
network targets or re-resolving DNS after validation.
`ALLOWED_SMTP_HOSTS` and `ALLOWED_SMTP_PORTS` allowlists. SMTP egress defaults to
the explicit `__deny_all__` host marker rather than an ambiguous empty string,
and settings reject wildcard hosts and non-SMTP ports before startup. The service
also rejects loopback, link-local, private, reserved, and otherwise non-global
DNS answers before opening a pinned socket to the selected global address, so
stale database rows or direct service calls fail closed instead of reaching
internal network targets or re-resolving DNS after validation.

Private-network IMAP/SMTP/CalDAV/CardDAV/WebDAV access belongs behind the
outbound-only self-hosted connector boundary. GitHub self-hosted runners can
Expand Down Expand Up @@ -157,9 +159,24 @@ The gate fails closed when a changed PR-head blob cannot be validated or copied;
it must never fall back to scanning trusted-base content for a modified PR path.
Pull request scans split scoped changed files into small bounded batches before
the timeout-driven rebalance path, so large PRs do not spend the whole required
check budget on one oversized Strix invocation. Strix remains a required
check budget on one oversized Strix invocation. PR scans also disable same-model
transient retries: timeout budget is spent on smaller scan scopes or distinct
fallback models instead of retrying the same slow provider path. The default
Strix model route uses LiteLLM's GitHub Models provider (`github/*`) with the
same secret material forwarded as `GITHUB_API_KEY`, while Vertex/Gemini-specific
routes remain available when explicitly configured. Gemini `BadRequestError`
output with LiteLLM/provider context is treated as model-route retryable, so an
invalid or retired configured model can fall forward to the known Gemini fallback
list without suppressing findings. GitHub Models routes use their own fallback
list before falling back to the generic list. Threshold findings
emitted by any failed attempt remain blocking even if a later fallback model
succeeds, except for PR findings that the gate has already classified as
retryable model inconsistency. If a PR batch exhausts the total Strix budget
before Strix completes, the required gate still fails closed and records provider
outage evidence for follow-up; partial zero-finding output is not merge evidence.
Any reported Medium-or-higher finding also fails closed. Strix remains a required
Medium-or-higher gate, while third-party LLM/provider warnings are tracked
separately unless they make the scan incomplete.
separately only when the scan completed.
Merge-gate governance for Strix, CodeRabbit, and required review evidence is
documented in `docs/development/merge-gate-policy.md`.

Expand Down Expand Up @@ -203,23 +220,30 @@ provider endpoints additionally require an operator-owned egress allowlist so an
organization admin cannot point LLM traffic at localhost, private networks, or
cloud metadata services.

The browser API client reads `naruon_session_token` from local storage and sends
it as the bearer session on signed routes. It does not synthesize or forward
public identity headers such as `X-User-Id`, `X-Organization-Id`, `X-Group-Id`,
`X-Group-Ids`, `X-User-Role`, or `X-Dev-Auth-Token`; any local development
identity-header flow is limited to explicit unsigned/test harness paths and is
not accepted by authenticated runtime dependencies. UI flows that create
source-linked tasks or other server-side writes must keep that signed-session
path covered in fast tests and E2E mocks so authenticated backend behavior is
not masked by stale fixtures.
Caller-supplied `Authorization` headers are dropped before the stored signed
session is applied, so browser code cannot shadow the bearer token with a
case-variant header.
The browser API client relies on the HttpOnly `naruon_session_token` cookie for
signed routes and sends requests with `credentials: include`. It does not read or
persist session tokens in `localStorage` or `sessionStorage`, and it does not
synthesize or forward public identity headers such as `X-User-Id`,
`X-Organization-Id`, `X-Group-Id`, `X-Group-Ids`, `X-User-Role`, or
`X-Dev-Auth-Token`; any local development identity-header flow is limited to
explicit unsigned/test harness paths and is not accepted by authenticated
runtime dependencies. UI flows that create source-linked tasks or other
server-side writes must keep that signed-session cookie path covered in fast
tests and E2E mocks so authenticated backend behavior is not masked by stale
fixtures. Caller-supplied `Authorization` headers are dropped so browser code
cannot shadow the server-authoritative cookie session with a case-variant bearer
header. Because cookies are ambient browser credentials, unsafe cookie-backed
methods (`POST`, `PUT`, `PATCH`, `DELETE`) also require a same-site `Origin` or
`Referer` whose origin exactly matches `ALLOWED_BROWSER_ORIGINS`; non-browser
bearer sessions are not subject to that browser-origin gate. Production rejects
the built-in localhost-only origin defaults, so deployments must provide their
actual frontend origins before cookie-backed writes work.

Secret-field encryption has no code fallback key. `backend/db/models.py` requires
an explicit, valid Fernet `ENCRYPTION_KEY` before encrypting or decrypting OAuth,
OpenAI, SMTP, IMAP, Google, and runner registration token fields, even in debug
mode. Invalid passphrase-style keys fail closed instead of being transformed into
an explicit, valid, high-entropy Fernet `ENCRYPTION_KEY` before encrypting or
decrypting OAuth, OpenAI, SMTP, IMAP, Google, and runner registration token
fields, even in debug mode. Invalid passphrase-style, known weak, repeated, or
low-entropy Fernet-format keys fail closed instead of being transformed into
derived keys. Decryption failures return `None` rather than ciphertext, so routes
that touch encrypted values must surface operator-facing missing-key or
unavailable-secret behavior without exposing encrypted blobs.
Expand Down
29 changes: 26 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
## [Unreleased]

### 수정
- Seongho Bae (@seonghobae): PR Strix required gate가 Gemini 권한 오류에 막히지
않도록 기본 LLM route를 LiteLLM GitHub Models(`github/gpt-5.4`)로 전환하고,
`LLM_API_KEY`를 GitHub provider용 `GITHUB_API_KEY`로 전달하며 별도 GitHub
fallback model 목록을 검증하도록 했습니다.
- Seongho Bae (@seonghobae): PR Strix가 지적한 SMTP SSRF/약한 암호화 키 경로를
fail-closed로 보강해, SMTP egress 기본값을 명시적 `__deny_all__` marker로
바꾸고 wildcard host/non-SMTP port 설정을 거부하며, `ENCRYPTION_KEY`는
Fernet 형식뿐 아니라 알려진 약한 값·반복 바이트·낮은 엔트로피까지 검증하도록
했습니다.
- Seongho Bae (@seonghobae): PR Strix가 Gemini/LiteLLM `BadRequestError`로
중단될 때 이를 취약점 우회가 아니라 model-route 장애로 분류해, 알려진 Gemini
fallback model로 재시도하되 실패한 시도에서 threshold finding이 보고되면 fallback
성공 뒤에도 required gate가 계속 실패하도록 했습니다.
- Seongho Bae (@seonghobae): post-merge Strix 실패를 막기 위해 LLM draft의 사용자
instruction을 JSON user message로 system prompt에서 분리하고, 브라우저 세션을
HttpOnly cookie 기반 `credentials: include` + `ALLOWED_BROWSER_ORIGINS` CSRF
gate 경로로 전환하고 production localhost allowlist default를 fail-closed
처리했으며, backend Docker image에서 `gcc`/`libpq-dev` build dependency를
제거했습니다.
- Seongho Bae (@seonghobae): PR Strix 스캔이 provider timeout에서 같은 모델을
반복 재시도하느라 required check budget을 소진하지 않도록 PR 이벤트의
same-model transient retry를 끄고 PR batch 크기를 줄였으며, 총 timeout으로
불완전하게 끝난 partial zero-finding 출력은 merge evidence로 인정하지 않고
required security gate가 fail-closed 되도록 명시했습니다.
- Seongho Bae (@seonghobae): LLM provider `base_url`을 HTTPS/exact-host allowlist와
global DNS 응답 검증으로 제한하고, LLM 호출 sink에서도 같은 검증을 반복해
provider registry 기반 SSRF 경로를 fail-closed 처리했습니다.
Expand All @@ -17,9 +41,8 @@
inventory 노출을 방지했습니다.
- Seongho Bae (@seonghobae): frontend API client에서 `localStorage.naruon_dev_user`
기반 `X-User-Id` 개발용 header 주입을 제거하고, caller-provided public identity
headers를 strip하며, legacy 개발용 계정 스위처를 제거해 signed
`Authorization: Bearer` session 경로만 backend write/read에 쓰이도록
정리했습니다.
headers를 strip하며, legacy 개발용 계정 스위처를 제거해 signed session 경로만
backend write/read에 쓰이도록 정리했습니다.
- Seongho Bae (@seonghobae): runtime 인증 dependency에서 개발용 `X-User-*`,
`X-Organization-*`, `X-Group-*`, `X-Dev-Auth-Token` 헤더 인증 경로를
제거해, 배포 환경 변수 오설정만으로 공개 요청이 identity/role/scope를
Expand Down
5 changes: 0 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app

# Install system dependencies if any are needed for pgvector/psycopg2
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc libpq-dev \
&& rm -rf /var/lib/apt/lists/*

COPY backend/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

Expand Down
Loading
Loading