Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 3 additions & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
version: 2
reviews:
request_changes_workflow: true
pre_merge_checks:
docstrings:
mode: off
1 change: 1 addition & 0 deletions .github/workflows/bandit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:

jobs:
security:
Expand Down
72 changes: 39 additions & 33 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ concurrency:
permissions:
actions: read
contents: read
models: read

jobs:
strix:
Expand Down Expand Up @@ -80,22 +81,19 @@ jobs:
- name: Gate Strix secrets
id: gate
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
STRIX_GITHUB_MODELS_MODEL: ${{ secrets.STRIX_LLM || 'openai/gpt-5' }}
run: |
strix_llm="github/gpt-4o"
llm_api_key="$(printf '%s' "$LLM_API_KEY" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [ -z "$llm_api_key" ]; then
# 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.'
strix_model="$(printf '%s' "$STRIX_GITHUB_MODELS_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
case "$strix_model" in
openai/gpt-5 | openai/gpt-5-* | openai/gpt-5.[0-9]* | openai/gpt-[6-9]* | openai/gpt-[1-9][0-9]* | \
openai/openai/gpt-5 | openai/openai/gpt-5-* | openai/openai/gpt-5.[0-9]* | openai/openai/gpt-[6-9]* | openai/openai/gpt-[1-9][0-9]*)
echo 'enabled=true' >> "$GITHUB_OUTPUT"
;;
*)
echo '::error::STRIX_LLM must select a GitHub Models OpenAI GPT-5 or newer model, for example openai/gpt-5.'
exit 1
fi
echo 'enabled=false' >> "$GITHUB_OUTPUT"
echo 'Strix secrets not configured; skipping.'
else
echo 'enabled=true' >> "$GITHUB_OUTPUT"
fi
;;
esac

- name: Gate GCP credentials
id: gcp_gate
Expand All @@ -114,10 +112,11 @@ jobs:
id: auth_gate
if: steps.gate.outputs.enabled == 'true'
env:
DEFAULT_PROVIDER: gemini
STRIX_GITHUB_MODELS_MODEL: ${{ secrets.STRIX_LLM || 'openai/gpt-5' }}
DEFAULT_PROVIDER: openai
run: |
. "$TRUSTED_WORKSPACE/scripts/ci/strix_model_utils.sh"
strix_llm="github/gpt-4o"
strix_llm="$(trim_whitespace "$STRIX_GITHUB_MODELS_MODEL")"
requires_vertex_auth='false'
model_auth_rc=0
model_requires_vertex_auth "$strix_llm" || model_auth_rc=$?
Expand Down Expand Up @@ -195,7 +194,7 @@ jobs:
- name: Mask LLM API key
if: steps.gate.outputs.enabled == 'true' && steps.auth_gate.outputs.can_run == 'true'
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_API_KEY: ${{ github.token }}
run: |
# Sanitize CR/LF before masking to prevent broken ::add-mask::
# commands and potential workflow command injection.
Expand All @@ -211,7 +210,7 @@ jobs:
- name: Prepare LLM API key input file
if: steps.gate.outputs.enabled == 'true' && steps.auth_gate.outputs.can_run == 'true'
env:
LLM_API_KEY_SECRET: ${{ secrets.LLM_API_KEY }}
LLM_API_KEY_SECRET: ${{ github.token }}
run: |
if [ -n "$LLM_API_KEY_SECRET" ]; then
umask 077
Expand All @@ -222,37 +221,44 @@ jobs:

- name: Prepare Strix model input file
if: steps.gate.outputs.enabled == 'true' && steps.auth_gate.outputs.can_run == 'true'
env:
STRIX_GITHUB_MODELS_MODEL: ${{ secrets.STRIX_LLM || 'openai/gpt-5' }}
run: |
umask 077
strix_llm_file="$RUNNER_TEMP/strix_llm.txt"
printf '%s' "github/gpt-4o" > "$strix_llm_file"
strix_model="$(printf '%s' "$STRIX_GITHUB_MODELS_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
case "$strix_model" in
openai/gpt-5 | openai/gpt-5-* | openai/gpt-5.[0-9]* | openai/gpt-[6-9]* | openai/gpt-[1-9][0-9]*)
printf 'openai/%s' "$strix_model" > "$strix_llm_file"
;;
openai/openai/gpt-5 | openai/openai/gpt-5-* | openai/openai/gpt-5.[0-9]* | openai/openai/gpt-[6-9]* | openai/openai/gpt-[1-9][0-9]*)
printf '%s' "$strix_model" > "$strix_llm_file"
;;
*)
echo '::error::STRIX_LLM must select a GitHub Models OpenAI GPT-5 or newer model, for example openai/gpt-5.'
exit 1
;;
esac
echo "STRIX_LLM_FILE=$strix_llm_file" >> "$GITHUB_ENV"

- name: Prepare LLM API base input file
if: steps.gate.outputs.enabled == 'true' && steps.auth_gate.outputs.can_run == 'true'
env:
LLM_API_BASE_SECRET: ${{ secrets.LLM_API_BASE }}
run: |
if [ -n "$LLM_API_BASE_SECRET" ]; then
umask 077
llm_api_base_file="$RUNNER_TEMP/llm_api_base.txt"
printf '%s' "$LLM_API_BASE_SECRET" > "$llm_api_base_file"
echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV"
fi
umask 077
llm_api_base_file="$RUNNER_TEMP/llm_api_base.txt"
printf '%s' "https://models.github.ai/inference" > "$llm_api_base_file"
echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV"

- name: Run Strix (quick)
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
GEMINI_LOCATION: GLOBAL
STRIX_LLM_DEFAULT_PROVIDER: openai
LLM_API_KEY_FILE: ${{ env.LLM_API_KEY_FILE }}
LLM_API_BASE_FILE: ${{ env.LLM_API_BASE_FILE }}
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: ./
STRIX_SOURCE_DIRS: .
STRIX_REASONING_EFFORT: ${{ github.event_name == 'push' && 'low' || 'minimal' }}
STRIX_REASONING_EFFORT: low
STRIX_LLM_MAX_RETRIES: 1
LLM_TIMEOUT: 90
STRIX_MEMORY_COMPRESSOR_TIMEOUT: 10
Expand Down Expand Up @@ -285,5 +291,5 @@ jobs:
with:
name: strix-reports
path: strix_runs/
if-no-files-found: warn
if-no-files-found: error
retention-days: 5
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ backend/.venv/
venv/
.venv/
__pycache__/
**/__pycache__/
*.py[cod]
*$py.class
*.so
Expand Down
7 changes: 6 additions & 1 deletion 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 an OpenAI GPT-5-or-newer
model through `models: read`; do not silently fall back to Gemini, GPT-4o, or
other providers when this gate is configured for PR evidence.

## PR automation and review defaults

Expand Down Expand Up @@ -64,6 +66,9 @@
avoid single-token columns such as `id`, `title`, `status`, or `priority` on
newly introduced objects.
- When reviews find public/private identifier leaks, stale API fixture shapes, or recurring bug patterns, update tests, frontend mocks, E2E mocks, README examples, architecture docs, and explicitly record the anti-pattern in `AGENTS.md` so the same bug pattern does not reappear in copied examples.
- When reviews find inert navigation/dead-space controls, either wire them to an
implemented workspace route/API or remove the control; do not leave
high-traffic drawer/sidebar entries as permanent `준비 중` copy.
- Execution steps resulting in `Timeout`, `Fatal`, `Warn`, or `Denied` outputs are considered hard failures. Tests must run without these warnings to be considered passing.
- DB-affecting API slices need both mocked fast tests and a real PostgreSQL
bootstrap/smoke path before PR merge evidence is considered complete.
Expand Down
5 changes: 3 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ 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
Medium-or-higher gate, while third-party LLM/provider warnings are tracked
separately unless they make the scan incomplete.
Medium-or-higher gate. The workflow uses GitHub Models through `models: read`,
the OpenAI-compatible GitHub Models endpoint, and an OpenAI GPT-5-or-newer
model; non-GPT-5 provider fallbacks are not valid PR evidence for this gate.
Merge-gate governance for Strix, CodeRabbit, and required review evidence is
documented in `docs/development/merge-gate-policy.md`.

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ mail/calendar/file systems.
open-source observability.
- PR automation is metadata-only and uses current-head robot-review evidence plus
required checks. Human approval is not awaited by default under repo policy.
- Strix PR/security evidence runs through GitHub Models with an OpenAI GPT-5+
model and the workflow `models: read` permission; pending CodeRabbit or check
evidence is a wait state, not a hard blocker.

## Agentic Ontology & Auto-Organization (Planned)

Expand Down Expand Up @@ -77,6 +80,12 @@ npm run build
npm run dev
```

`next.config.ts` caps static generation concurrency by default
(`NEXT_STATIC_GENERATION_MAX_CONCURRENCY=2`,
`NEXT_STATIC_GENERATION_MIN_PAGES_PER_WORKER=50`) so constrained CI/build
machines do not fan out excessive Node/PostCSS workers. Raise those values only
with explicit build evidence.

## Threading proof points

- Canonical thread IDs are assigned in `backend/services/threading_service.py`.
Expand Down
14 changes: 11 additions & 3 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,19 @@ mailbox owners.

## Warning classification

Current backend test warnings are known dependency/toolchain warnings:
Backend evidence should be collected with warnings promoted to errors whenever
the slice is under active change:

```bash
PYTHONWARNINGS=error python3 -m pytest -q
```

Historical third-party dependency warnings should be isolated or filtered with
an explicit rationale before merge evidence is accepted:

- `starlette.formparsers` imports `multipart`, which emits a pending deprecation
warning.
- Compiled SWIG metadata emits `__module__` deprecation warnings during import.

Threading code should not add new warnings. Treat any new warning from
application modules as a regression.
Threading code should not add new warnings. Treat any new application warning
or log output containing `Timeout`, `Fatal`, `Warn`, or `Denied` as a regression.
4 changes: 4 additions & 0 deletions backend/api/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ async def summarize_endpoint(request: SummarizeRequest, user_id: str | None = No
status_code=500,
detail="An internal server error occurred while processing the request.",
)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=500,
Expand All @@ -69,6 +71,8 @@ async def draft_endpoint(request: DraftRequest, user_id: str | None = None, db:
status_code=500,
detail="An internal server error occurred while processing the request.",
)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=500,
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
32 changes: 29 additions & 3 deletions backend/tests/test_llm_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@


class MockTenantConfig:
def __init__(self):
self.openai_api_key = "test-key"
def __init__(self, openai_api_key="test-key"):
self.openai_api_key = openai_api_key


class MockSession:
def __init__(self, tenant_config=None):
self.tenant_config = tenant_config or MockTenantConfig()

async def scalar(self, stmt):
return MockTenantConfig()
return self.tenant_config


@pytest.fixture
Expand Down Expand Up @@ -74,3 +77,26 @@ def test_draft_endpoint(mock_draft, client):
)
assert resp.status_code == 200
assert resp.json() == {"draft": "This is a draft reply."}


def test_llm_endpoints_preserve_missing_key_400():
async def override_get_db():
yield MockSession(MockTenantConfig(openai_api_key=None))

app.dependency_overrides[get_db] = override_get_db
try:
with TestClient(app, headers={"X-User-Id": "testuser"}) as test_client:
summarize = test_client.post(
"/api/llm/summarize", json={"email_body": "test email"}
)
draft = test_client.post(
"/api/llm/draft",
json={"email_body": "test email", "instruction": "reply nicely"},
)
finally:
app.dependency_overrides.clear()

assert summarize.status_code == 400
assert summarize.json() == {"detail": "OpenAI API key not configured"}
assert draft.status_code == 400
assert draft.json() == {"detail": "OpenAI API key not configured"}
24 changes: 17 additions & 7 deletions docs/operations/open-source-apm.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

## 확인된 사실 / Confirmed

- `backend/main.py` currently exposes the FastAPI app and routers, but no
OpenTelemetry instrumentation is wired yet.
- `docker-compose.yml` currently contains db/backend/frontend only; it does not
run Prometheus, Grafana, Loki, Tempo, Jaeger, or an OpenTelemetry Collector.
- `backend/main.py` exposes Prometheus metrics and optional OpenTelemetry
FastAPI instrumentation.
- `backend/requirements.txt` includes `prometheus-fastapi-instrumentator` and
OpenTelemetry dependencies used by the FastAPI app.
- `docker-compose.observability.yml`, `docker-compose.apm.yml`, and
`docker-compose.infra.yml` document local Prometheus/APM stack entry points.
- `docker-compose.live-e2e.yml` proves the image-based smoke path before any APM
stack is claimed as production-ready.

Expand All @@ -30,8 +32,16 @@

## 도입 기준

- Add instrumentation behind explicit environment variables.
- Keep `/healthz`, `/readyz`, and `/metrics` semantics separate if those endpoints
are added later.
- Keep instrumentation behind explicit environment variables where tracing can
export outside the process.
- Keep `/healthz`, `/readyz`, and `/metrics` semantics separate as the stack
matures.
- Do not claim APM production readiness until a live stack shows trace, metric,
and log evidence without leaking user email content.

## Remaining gaps

- Add connector heartbeat, sync lag, writeback conflict, and AI action audit
dashboards.
- Add log and trace redaction tests for email bodies, provider tokens, DSNs, and
calendar/file descriptions.
17 changes: 17 additions & 0 deletions docs/operations/source-of-truth-and-writeback-sovereignty.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@
- GitHub self-hosted runners remain CI smoke infrastructure. They can validate
internal connectivity, but they are not the production relay component.

## Source registry contract

Every writeback-capable account should resolve through a server-authoritative
source registry before any provider mutation is attempted. The registry record
must include an opaque `source_id`, tenant/workspace owner scope, protocol
(`imap`, `smtp`, `pop3`, `caldav`, `carddav`, `webdav`), provider host, capability
flags, consent state, data region, connector route, credential reference, and
conflict metadata such as remote ids or ETags. Browser requests may reference
`source_id`, but must never supply ownership, credential, region, or capability
claims.

Implemented slices currently cover signed task/mail provenance, dedupe/threading
contracts, source-linked ticket tasks, and writeback intent documentation. Real
CalDAV/WebDAV mutation, POP3 runtime sync, sent-mail reply tracking persistence,
self-sent knowledge capture, and sender DAG/ontology action selection remain
episode work and must preserve this registry boundary.

## Policy and audit requirements

- RBAC grants are necessary but never sufficient when ABAC denies apply. Region,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

- [x] **Step 1: Write failing unit tests**

Update `frontend/src/app/page.test.tsx` with tests that seed `localStorage.setItem('naruon_startup_view', 'calendar')` and expect the mobile calendar region to be the active startup panel, seed `dashboard` and expect a dashboard overview region, and verify the default remains the email workspace.
Update `frontend/src/app/page.test.tsx` with tests that seed `localStorage.setItem('naruon_startup_view', 'calendar')` and expect the mobile calendar region to be the active startup panel, seed `dashboard` and expect a dashboard overview region, and verify the default opens the dashboard workspace.

- [x] **Step 2: Verify RED**

Expand Down
Loading