[codex] harden auth and production security - #21
Conversation
|
Warning Review limit reached
More reviews will be available in 46 minutes and 53 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughAdds API bearer gating to most routers, introduces SSE streaming chat with validation, implements a new /capture endpoint/service and frontend page, updates rate-limit behavior, token storage in UI, and broad CI/Docker/K8s/Compose/docs hardening including Caddy headers and pgvector builds. ChangesAuthenticated APIs, streaming chat, capture, and deployment/runtime updates
Sequence Diagram(s)sequenceDiagram
participant Frontend
participant API
participant ChatService
participant Redis
participant DB
participant LLM
Frontend->>API: POST /chat/stream (Bearer)
API->>Redis: rate limit check
API->>ChatService: stream_chat(...)
ChatService->>DB: load history/context
ChatService->>LLM: generate_stream(messages)
LLM-->>ChatService: chunks
ChatService-->>API: validated events
API-->>Frontend: SSE delta/complete
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (9)
.github/workflows/ci.yml (1)
56-70: 💤 Low valueConsider adding explicit container cleanup.
The
second-brain-ci-dbcontainer is started but never explicitly stopped or removed. While GitHub Actions will clean it up during runner teardown, adding an explicit cleanup step (e.g., in anif: always()block) would make the workflow more maintainable and portable.🧹 Suggested cleanup step
Add this step after the test steps:
- name: Cleanup database container if: always() run: docker rm -f second-brain-ci-db || true🤖 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 @.github/workflows/ci.yml around lines 56 - 70, The workflow step "Start cleaned pgvector database" launches a container named second-brain-ci-db but never stops or removes it; add a separate cleanup step that runs unconditionally (use if: always()) after your tests to force-remove the container (targeting second-brain-ci-db) so it won't linger (e.g., run docker rm -f second-brain-ci-db || true in that step); ensure the cleanup step is placed after the test steps and uses the same container name to reliably remove the instance.backend/tests/integration/test_capture_api.py (1)
11-21: 💤 Low valueDuplicate tag in test payload.
Line 20 includes
"capture"twice in the tags list. This is likely intentional to test tag deduplication, but if not, it should be cleaned up.🤖 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/tests/integration/test_capture_api.py` around lines 11 - 21, The test payload builder function _capture_payload has a duplicated "capture" entry in the tags list; either remove the duplicate so tags becomes ["capture", "inbox"] or, if the test intentionally verifies tag deduplication, add an inline comment clarifying that intent and ensure the corresponding test asserts dedup behavior (update tests referencing _capture_payload accordingly).deploy/Dockerfile.caddy (1)
18-24: ⚡ Quick winConsider adding a non-root USER directive.
The runtime stage runs as root, which increases the attack surface. While Caddy needs to bind to privileged ports 80 and 443, modern Caddy can drop privileges after binding. Consider adding a non-root user and letting Caddy handle privilege dropping, or use Docker's capability system to grant
CAP_NET_BIND_SERVICE.🔒 Proposed fix to run as non-root
FROM alpine:3.23.4 -RUN apk add --no-cache ca-certificates mailcap +RUN apk add --no-cache ca-certificates mailcap \ + && adduser -D -u 1000 caddy COPY --from=builder /out/caddy /usr/bin/caddy +USER caddy EXPOSE 80 443 CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]Then in docker-compose, add capability:
caddy: cap_add: - NET_BIND_SERVICE🤖 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 `@deploy/Dockerfile.caddy` around lines 18 - 24, The Dockerfile.caddy runtime stage runs all processes as root, which increases the attack surface. Add a non-root USER directive in the Dockerfile before the CMD instruction that starts Caddy, and then update the docker-compose configuration to grant the NET_BIND_SERVICE capability to the caddy service. This allows the non-root user to bind to the privileged ports 80 and 443 while maintaining security best practices.deploy/k8s/README.md (1)
76-82: ⚡ Quick winClarify how to build the monitoring images.
The monitoring section mentions that templates require "scanned-clean local images" with specific tags like
second-brain-grafana:phase7-clean-required, but the README doesn't provide build instructions for these images. Users who want to enable monitoring won't have a clear path forward without Dockerfiles or build commands for Grafana, Prometheus, and PgBouncer.Consider adding a brief note pointing to where these Dockerfiles live, or documenting the expected build workflow (e.g., base image selection, scanning tools, tag conventions).
🤖 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 `@deploy/k8s/README.md` around lines 76 - 82, The README entry under "Optional Monitoring Templates" references local scanned-clean image tags (e.g., second-brain-grafana:phase7-clean-required) and imagePullPolicy: Never but lacks build instructions; update deploy/k8s/README.md to add a short section that points to where the Dockerfiles live (deploy/grafana/, deploy/prometheus/, deploy/pgbouncer/ or the repo paths), and document the expected build-and-scan workflow: which Dockerfile to use, the docker build and tag convention (use the *-clean-required tag format), the scanning step to run (e.g., SCA or image scanner), and that images must be built locally before applying manifests that reference those tags; mention the tag naming convention and remind that imagePullPolicy: Never requires local images.backend/app/capture/service.py (1)
53-56: 💤 Low valueClarify the port validation intent.
Accessing
parsed.portsolely for its side effect (raisingValueErroron invalid ports) works but triggers static analysis warnings and obscures intent. Consider assigning to_or adding a brief comment.♻️ Suggested clarification
try: - parsed.port + _ = parsed.port # access triggers ValueError on invalid port except ValueError as exc: raise ValueError("capture URL includes an invalid port") from exc🤖 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/app/capture/service.py` around lines 53 - 56, The try/except that accesses parsed.port only for its side effect should be made explicit to avoid static-analysis warnings: replace the bare access with an explicit assignment like "_ = parsed.port" (or add a short comment above it) and keep the existing except ValueError as exc: raise ValueError("capture URL includes an invalid port") from exc so intent is clear; update the code around parsed.port in the capture service where parsed is used.backend/tests/integration/test_dataops_api.py (1)
55-66: Clarify the two-token intent oftest_admin_token_alone_is_not_api_access. (backend/tests/integration/test_dataops_api.py:55-66)
/data/exportrequires both:Authorization: Bearer ...validated byrequire_api_access(must equalapi_token) andX-Second-Brain-Admin-Tokenvalidated byrequire_admin(must equaladmin_token).- In this test,
TOKENistest-admin-token, and the bearer is set to that value while the admin header is also set correctly—so the expected 401 is consistent; the bearer header at line 61 already uses the admin token (no need to change it totest-api-token).- Optional: add a short comment (or tweak the test name) explicitly stating that the admin token must not substitute for the API bearer token.
🤖 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/tests/integration/test_dataops_api.py` around lines 55 - 66, The test test_admin_token_alone_is_not_api_access should clarify that /data/export requires two distinct tokens validated by require_api_access (Authorization Bearer == api_token) and require_admin (X-Second-Brain-Admin-Token == admin_token); update the test by adding a short inline comment (or rename the test to something like test_admin_token_does_not_substitute_api_token) stating that the admin token must not substitute for the API bearer token, and ensure the existing headers keep TOKEN (test-admin-token) for both Authorization and X-Second-Brain-Admin-Token to show the bearer being the admin token is intentional.backend/app/llm/ollama.py (1)
29-32: ⚡ Quick winConsider defensive error handling around JSON parsing.
The
json.loads(line)call on Line 32 will raise if Ollama sends malformed data. While the caller instream_chat()has a try/except wrapper (Lines 325-327 inservice.py), adding a try/except here with a more specific error message would improve debuggability and fail faster on unexpected Ollama responses.🛡️ Proposed defensive handling
for line in r.iter_lines(): if not line: continue - data = json.loads(line) + try: + data = json.loads(line) + except json.JSONDecodeError as e: + raise RuntimeError(f"Ollama returned invalid JSON: {line!r}") from e if data.get("done"):🤖 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/app/llm/ollama.py` around lines 29 - 32, The json.loads(line) inside the for loop that iterates r.iter_lines() can raise on malformed JSON from Ollama; wrap the json.loads(line) call in a try/except that catches json.JSONDecodeError (and optionally ValueError), log a clear, contextual error including the raw line and any request identifiers, and either continue or raise a more specific exception so callers (e.g., stream_chat) get a clearer failure; update the block in ollama.py where the for line in r.iter_lines(): loop and json.loads(line) occur (reference json.loads and the iter_lines loop) to implement this defensive parsing and logging.frontend/lib/api/client.ts (1)
99-111: ⚡ Quick winConsider handling malformed JSON gracefully in SSE parsing.
The
parseSseBlockfunction callsJSON.parse(dataLines.join("\n"))without error handling. If the server emits a malformed data payload, this will throw a synchronous exception that may not surface cleanly to the caller. Consider wrapping the parse in a try-catch and returningnullor logging the error.🛡️ Suggested defensive parsing
function parseSseBlock(block: string): { event: string; data: unknown } | null { let event = "message"; const dataLines: string[] = []; for (const line of block.split("\n")) { if (line.startsWith("event:")) { event = line.slice("event:".length).trim(); } else if (line.startsWith("data:")) { dataLines.push(line.slice("data:".length).trimStart()); } } if (dataLines.length === 0) return null; - return { event, data: JSON.parse(dataLines.join("\n")) as unknown }; + try { + return { event, data: JSON.parse(dataLines.join("\n")) as unknown }; + } catch { + console.warn("Failed to parse SSE data block:", dataLines.join("\n")); + return null; + } }🤖 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 `@frontend/lib/api/client.ts` around lines 99 - 111, The parseSseBlock function currently calls JSON.parse without protection; wrap the parse of dataLines.join("\n") in a try-catch inside parseSseBlock, and on parse failure return null (or optionally log the error) instead of letting the exception propagate so malformed SSE payloads are handled gracefully; update references in parseSseBlock where it returns parsed data to handle the null case accordingly.frontend/app/capture/page.tsx (1)
109-116: ⚡ Quick winConsider wrapping in a
<form>element for better semantics.The current implementation uses a button with
onClickto trigger submission. While this works, wrapping the inputs in a<form>element and handlingonSubmitwould provide better semantics, keyboard support (Enter key submission), and accessibility.♻️ Suggested refactor
- <div className="grid gap-3 p-4"> + <form className="grid gap-3 p-4" onSubmit={(e) => { e.preventDefault(); capture.mutate(); }}> {capture.error && ( <InlineError message={capture.error instanceof Error ? capture.error.message : "Capture failed"} /> )} {/* ...inputs... */} <div className="flex justify-end border-t border-border pt-3"> <button - type="button" - onClick={() => capture.mutate()} + type="submit" disabled={!canSubmit} className="inline-flex h-9 items-center gap-1.5 rounded-lg bg-amber-500 px-3 text-sm font-semibold text-white shadow-sm shadow-amber-200/60 transition-colors hover:bg-amber-600 disabled:cursor-not-allowed disabled:opacity-40 dark:shadow-none" > {capture.isPending ? "Capturing" : "Capture"} <ArrowRight size={14} weight="bold" /> </button> </div> + </form> - </div>🤖 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 `@frontend/app/capture/page.tsx` around lines 109 - 116, Wrap the capture UI in a semantic <form> and move the submit logic from the button onClick to the form's onSubmit handler: call capture.mutate() from an onSubmit that calls event.preventDefault(), change the button to type="submit" (keep disabled={!canSubmit}) and keep using capture.isPending for the label; this will enable Enter-key submission and better accessibility while preserving the existing canSubmit and capture.* behavior.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@backend/app/api/chat.py`:
- Around line 104-105: The except block that yields the generic SSE error
currently swallows the exception; update the except Exception handler in
backend/app/api/chat.py (the block that currently does "except Exception: yield
_format_sse(\"error\", {\"message\": \"streaming chat failed\"})") to log the
caught exception before yielding—use the module/logger instance (e.g.,
logger.exception or logger.error with exc_info=True) to record the stacktrace
and context, then yield the same generic SSE error to the client.
In `@deploy/docker-compose.prod.yml`:
- Around line 91-92: The docker-compose.prod.yml stack currently only defines
volumes: db_data and omits the Prometheus/Grafana services found under
deploy/prometheus/ and deploy/grafana/; update docker-compose.prod.yml to re-add
prometheus and grafana service definitions (and their volumes) and mount the
provided configs from deploy/prometheus/ and deploy/grafana/, ensuring service
names match references used elsewhere (prometheus, grafana) and that any network
and volume entries (e.g., db_data plus prometheus_data/grafana_data) are
declared, or alternatively add a README note in the repo root explaining where
VPS observability is handled (pointing to deploy/k8s/monitoring/ if using
Kubernetes instead) so reviewers know whether Compose intentionally omits
monitoring.
In `@deploy/k8s/redis.yaml`:
- Line 41: The Kubernetes manifest currently references the floating image tag
"redis:7.4-alpine" in deploy/k8s/redis.yaml; audit Redis 7.4 release notes for
Lua VM/jemalloc/LRU eviction, ACL LOAD, BITCOUNT/BITPOS, and hash field
expiration behavior and run integration tests covering Lua scripts, ACL loading,
bit operations, and persistence/RDB/INFO before upgrading; if any behavior
breaks, either pin to a known-good digest (use the multi-arch sha256 digest) or
roll back to the previous 7.3.x image tag, and update the manifest's image field
to the chosen immutable digest/tag and document the verification steps.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 56-70: The workflow step "Start cleaned pgvector database"
launches a container named second-brain-ci-db but never stops or removes it; add
a separate cleanup step that runs unconditionally (use if: always()) after your
tests to force-remove the container (targeting second-brain-ci-db) so it won't
linger (e.g., run docker rm -f second-brain-ci-db || true in that step); ensure
the cleanup step is placed after the test steps and uses the same container name
to reliably remove the instance.
In `@backend/app/capture/service.py`:
- Around line 53-56: The try/except that accesses parsed.port only for its side
effect should be made explicit to avoid static-analysis warnings: replace the
bare access with an explicit assignment like "_ = parsed.port" (or add a short
comment above it) and keep the existing except ValueError as exc: raise
ValueError("capture URL includes an invalid port") from exc so intent is clear;
update the code around parsed.port in the capture service where parsed is used.
In `@backend/app/llm/ollama.py`:
- Around line 29-32: The json.loads(line) inside the for loop that iterates
r.iter_lines() can raise on malformed JSON from Ollama; wrap the
json.loads(line) call in a try/except that catches json.JSONDecodeError (and
optionally ValueError), log a clear, contextual error including the raw line and
any request identifiers, and either continue or raise a more specific exception
so callers (e.g., stream_chat) get a clearer failure; update the block in
ollama.py where the for line in r.iter_lines(): loop and json.loads(line) occur
(reference json.loads and the iter_lines loop) to implement this defensive
parsing and logging.
In `@backend/tests/integration/test_capture_api.py`:
- Around line 11-21: The test payload builder function _capture_payload has a
duplicated "capture" entry in the tags list; either remove the duplicate so tags
becomes ["capture", "inbox"] or, if the test intentionally verifies tag
deduplication, add an inline comment clarifying that intent and ensure the
corresponding test asserts dedup behavior (update tests referencing
_capture_payload accordingly).
In `@backend/tests/integration/test_dataops_api.py`:
- Around line 55-66: The test test_admin_token_alone_is_not_api_access should
clarify that /data/export requires two distinct tokens validated by
require_api_access (Authorization Bearer == api_token) and require_admin
(X-Second-Brain-Admin-Token == admin_token); update the test by adding a short
inline comment (or rename the test to something like
test_admin_token_does_not_substitute_api_token) stating that the admin token
must not substitute for the API bearer token, and ensure the existing headers
keep TOKEN (test-admin-token) for both Authorization and
X-Second-Brain-Admin-Token to show the bearer being the admin token is
intentional.
In `@deploy/Dockerfile.caddy`:
- Around line 18-24: The Dockerfile.caddy runtime stage runs all processes as
root, which increases the attack surface. Add a non-root USER directive in the
Dockerfile before the CMD instruction that starts Caddy, and then update the
docker-compose configuration to grant the NET_BIND_SERVICE capability to the
caddy service. This allows the non-root user to bind to the privileged ports 80
and 443 while maintaining security best practices.
In `@deploy/k8s/README.md`:
- Around line 76-82: The README entry under "Optional Monitoring Templates"
references local scanned-clean image tags (e.g.,
second-brain-grafana:phase7-clean-required) and imagePullPolicy: Never but lacks
build instructions; update deploy/k8s/README.md to add a short section that
points to where the Dockerfiles live (deploy/grafana/, deploy/prometheus/,
deploy/pgbouncer/ or the repo paths), and document the expected build-and-scan
workflow: which Dockerfile to use, the docker build and tag convention (use the
*-clean-required tag format), the scanning step to run (e.g., SCA or image
scanner), and that images must be built locally before applying manifests that
reference those tags; mention the tag naming convention and remind that
imagePullPolicy: Never requires local images.
In `@frontend/app/capture/page.tsx`:
- Around line 109-116: Wrap the capture UI in a semantic <form> and move the
submit logic from the button onClick to the form's onSubmit handler: call
capture.mutate() from an onSubmit that calls event.preventDefault(), change the
button to type="submit" (keep disabled={!canSubmit}) and keep using
capture.isPending for the label; this will enable Enter-key submission and
better accessibility while preserving the existing canSubmit and capture.*
behavior.
In `@frontend/lib/api/client.ts`:
- Around line 99-111: The parseSseBlock function currently calls JSON.parse
without protection; wrap the parse of dataLines.join("\n") in a try-catch inside
parseSseBlock, and on parse failure return null (or optionally log the error)
instead of letting the exception propagate so malformed SSE payloads are handled
gracefully; update references in parseSseBlock where it returns parsed data to
handle the null case accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6f2e53d5-f3c4-4961-86b7-942299047b8d
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (90)
.dockerignore.github/workflows/ci.yml.github/workflows/k8s.yml.gitignoreREADME.mdbackend/.env.examplebackend/README.mdbackend/app/api/briefing.pybackend/app/api/capture.pybackend/app/api/chat.pybackend/app/api/conversations.pybackend/app/api/dataops.pybackend/app/api/ingest.pybackend/app/api/research_jobs.pybackend/app/api/search.pybackend/app/api/sources.pybackend/app/api/tasks.pybackend/app/cache/rate_limit.pybackend/app/cache/redis_client.pybackend/app/capture/__init__.pybackend/app/capture/service.pybackend/app/chat/prompt.pybackend/app/chat/service.pybackend/app/config.pybackend/app/deps.pybackend/app/llm/base.pybackend/app/llm/fake.pybackend/app/llm/gemini.pybackend/app/llm/ollama.pybackend/app/main.pybackend/app/mcp_server.pybackend/app/research/service.pybackend/app/schemas/capture.pybackend/eval/corpus/04-docker-compose-runtime.mdbackend/requirements.prod.txtbackend/requirements.txtbackend/tests/conftest.pybackend/tests/integration/conftest.pybackend/tests/integration/test_api.pybackend/tests/integration/test_briefing.pybackend/tests/integration/test_capture_api.pybackend/tests/integration/test_chat.pybackend/tests/integration/test_dataops_api.pybackend/tests/unit/test_api_auth.pybackend/tests/unit/test_chat_stream.pybackend/tests/unit/test_config.pybackend/tests/unit/test_mcp_server.pybackend/tests/unit/test_prompt.pybackend/tests/unit/test_redis_paths.pybackend/tests/unit/test_research_prompt.pydeploy/.env.prod.exampledeploy/Dockerfile.backenddeploy/Dockerfile.caddydeploy/Dockerfile.frontenddeploy/Dockerfile.pgvectordeploy/caddy/Caddyfiledeploy/cron/second-brain-backupdeploy/docker-compose.prod.ymldeploy/docker-compose.vps.yml.exampledeploy/k8s/README.mddeploy/k8s/api.yamldeploy/k8s/kustomization.yamldeploy/k8s/monitoring/grafana.yamldeploy/k8s/monitoring/prometheus.yamldeploy/k8s/pgbouncer.yamldeploy/k8s/postgres-statefulset.yamldeploy/k8s/redis.yamldeploy/k8s/secret.example.yamldeploy/k8s/worker.yamldeploy/pgbouncer/pgbouncer.inideploy/pgbouncer/userlist.txt.exampledocker-compose.ymldocs/PROGRESS.mddocs/USAGE.mddocs/adr/0014-kubernetes-learning-track.mddocs/implementation-notes.mddocs/query-optimization.mddocs/runbooks/backup-restore.mddocs/runbooks/deploy-checklist.mddocs/runbooks/incident-response.mdfrontend/.env.examplefrontend/.gitignorefrontend/app/admin/page.tsxfrontend/app/capture/page.tsxfrontend/app/chat/page.tsxfrontend/components/ConversationSidebar.tsxfrontend/components/MessageList.tsxfrontend/lib/api/client.tsfrontend/lib/api/types.tsfrontend/package.json
💤 Files with no reviewable changes (2)
- deploy/pgbouncer/userlist.txt.example
- deploy/pgbouncer/pgbouncer.ini
|
@coderabbitai review |
✅ Action performedReview finished.
|
Summary
Security Notes
X-Second-Brain-Admin-Tokenin addition toAuthorization: Bearer <SECOND_BRAIN_API_TOKEN>.SECOND_BRAIN_MCP_ENABLE_MUTATIONS=truefor trusted local clients./chat/streambuffers generated chunks until citation validation passes, and weak or unsupported cited answers are replaced before persistence/emission.Verification
SECOND_BRAIN_TEST_DATABASE_URL=postgresql+psycopg://second_brain:second_brain@localhost:5433/second_brain python -m pytest backend/tests -q->226 passed, 6 warnings31 passed, 21 skipped, 1 warningnpm run lint-> passednpm run build-> passed, with existing Next.js multiple-lockfile warningnpm audit --audit-level=moderate->0 vulnerabilitiesdocker compose -f deploy/docker-compose.prod.yml config --quietwith dummy required env -> passedgit diff --check-> only CRLF normalization warningsSummary by CodeRabbit
New Features
/captureendpoint and web UI for saving bookmarks from the browser/chat/streamwith real-time delta eventsSecurity & Operations
Infrastructure