Add VoidLink: Remote AI interface with React Native mobile app#2
Add VoidLink: Remote AI interface with React Native mobile app#2CCguvycu wants to merge 3 commits into
Conversation
Full-stack system for accessing local Ollama AI models from mobile/desktop: Backend (Python FastAPI): - JWT authentication with device pairing and QR code linking - Ollama API integration with SSE streaming and WebSocket support - SQLite database for conversations, messages, folders, and devices - Multi-model support: Llama 3, Mistral, DeepSeek, Qwen, CodeLlama, Phi-3 - GPU/CPU/RAM monitoring endpoints via psutil + GPUtil - File upload with type validation and path traversal protection - Permission-gated terminal execution (read-only command allowlist) - Character modes and custom system prompts - Memory context injection across conversations - Auto-generated conversation titles via LLM - Rate limiting, CORS, and bcrypt password hashing - LAN auto-discovery endpoint Mobile App (React Native Expo): - Cyberpunk dark UI with glow effects and smooth animations - SSE streaming chat interface with real-time token display - Model selector with live Ollama model list - Voice input via Expo AV recording - Image upload to chat - Markdown rendering with syntax highlighting - Conversation folders with color coding and swipe-to-delete - QR code device pairing screen - System monitor screen (CPU/RAM/GPU/disk live charts) - Model manager (download/delete models) - Permission-gated terminal screen - Character mode and system prompt settings - Offline conversation caching via AsyncStorage - Zustand state management Docker: - Dockerfile for backend - docker-compose with optional Nginx reverse proxy - Nginx config with WebSocket proxying and TLS support Documentation: - Full API reference - Setup instructions (local, Tailscale, Cloudflare, Nginx+Let's Encrypt) - APK build guide (EAS cloud + local Android SDK) - Security hardening checklist - Systemd service config - Obsidian vault integration notes https://claude.ai/code/session_01JRMpBUWAnqmPVV1xxmbA3e
📝 WalkthroughWalkthroughVoidLink implements a FastAPI backend (auth, chat streaming, model management, files, system) with SQLAlchemy and Ollama integration, plus a React Native Expo mobile app (Zustand stores, API client, chat UI), desktop packaging, Docker/compose, and documentation. ChangesVoidLink Application
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~65 minutes
✨ Finishing Touches🧪 Generate unit tests (beta)
|
…o-discovery Windows Desktop App (voidlink/desktop/): - tray_app.py: full system tray application that auto-installs Ollama, starts the FastAPI backend in-process, generates secure credentials on first run, and shows a welcome dialog with the server URL and password - voidlink.spec: PyInstaller spec bundles entire backend + tray into VoidLink.exe - icon_gen.py: generates cyberpunk hexagon .ico from PIL - version_info.txt: Windows version metadata for the EXE - build_windows.bat: one double-click build script (installs deps, runs PyInstaller) Windows Installer (voidlink/installer/): - voidlink_setup.iss: Inno Setup script producing a polished VoidLink-Setup.exe with silent Ollama install, startup task, desktop shortcut, and welcome text Mobile — zero-config connection: - useServerDiscovery.ts: scans LAN subnet for VoidLink servers automatically, probing up to 30 hosts in parallel with 1.5s timeout per probe - login.tsx: rewritten with auto-discover phase that finds the server and pre-fills the URL; falls back to manual entry + QR scan; shows credentials hint pointing to the tray icon - build_apk.bat: one double-click script to build APK via EAS cloud build User experience after this change: PC: double-click VoidLink-Setup.exe → everything starts, no config Phone: install APK → open → server appears automatically → enter password → done https://claude.ai/code/session_01JRMpBUWAnqmPVV1xxmbA3e
There was a problem hiding this comment.
Actionable comments posted: 4
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
voidlink/docker/Dockerfile.backend (1)
1-27:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun container as non-root user for security.
The container runs as root, which violates the principle of least privilege. An attacker who compromises the application would have root access inside the container.
🔒 Proposed fix to add non-root user
# Create upload directory RUN mkdir -p /app/uploads +# Create non-root user +RUN useradd -m -u 1000 appuser && \ + chown -R appuser:appuser /app + +USER appuser + EXPOSE 8000 ENV PYTHONUNBUFFERED=1🤖 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 `@voidlink/docker/Dockerfile.backend` around lines 1 - 27, The Dockerfile currently runs as root; create a non-root user and switch to it before CMD to follow least privilege. Add steps after copying files and creating /app/uploads to create a user/group (e.g., appuser), chown /app and /app/uploads to that user, and set appropriate permissions, then set USER appuser so the process (started by CMD ["python","run.py"]) runs unprivileged; ensure any files the app needs to write are owned by that user and keep WORKDIR and PYTHONUNBUFFERED as-is.
🟠 Major comments (27)
voidlink/mobile/src/components/VoiceInput.tsx-60-63 (1)
60-63:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftVoice input is not actually transcribed yet.
onTranscriptalways receives placeholder text, so the feature is non-functional for end users. Either wire STT before release or change the callback contract to return recording URI/status until transcription is implemented.🤖 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 `@voidlink/mobile/src/components/VoiceInput.tsx` around lines 60 - 63, The current implementation in VoiceInput.tsx calls onTranscript with a placeholder string instead of real STT results; update the behavior so onTranscript receives either the recording URI or a status object until transcription is implemented, or wire a real STT service (e.g., Whisper) to produce text. Specifically, change the try/catch block that now calls onTranscript('[Voice input recorded - connect Whisper API for transcription]') to instead: (a) return a consistent payload shape (e.g., { uri, status: 'recorded' } or { text, uri, status: 'transcribed' }) so consumers of onTranscript can handle both recorded-only and transcribed states, and (b) add a clear TODO and integration point where the Whisper/Cloud STT call will replace the placeholder and populate text; ensure the onTranscript signature and any callers are updated to accept the new payload shape (reference onTranscript and the VoiceInput component) so the feature behaves correctly for end users.voidlink/backend/.env.example-12-12 (1)
12-12:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAccess token TTL is too long for a bearer access token.
43200minutes (30 days) substantially increases blast radius after token theft. Use short-lived access tokens and refresh flow.Suggested change
-ACCESS_TOKEN_EXPIRE_MINUTES=43200 +ACCESS_TOKEN_EXPIRE_MINUTES=30🤖 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 `@voidlink/backend/.env.example` at line 12, Replace the overly long ACCESS_TOKEN_EXPIRE_MINUTES value in .env.example with a short-lived TTL (e.g., 15–60 minutes) and update any code that reads ACCESS_TOKEN_EXPIRE_MINUTES (token issuance logic that sets JWT expiry) to expect the shorter lifetime; also document in .env.example that refresh tokens / a refresh flow should be used for long sessions so the app uses short-lived bearer tokens and refresh tokens instead.voidlink/backend/.env.example-15-16 (1)
15-16:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid concrete admin defaults in the env template.
These values are deployable as-is and are easy to accidentally ship. Use non-functional placeholders and require override at startup.
Suggested change
-ADMIN_USERNAME=admin -ADMIN_PASSWORD=voidlink-secure-password-2024 +ADMIN_USERNAME=change-me +ADMIN_PASSWORD=change-me-strong-random-password🤖 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 `@voidlink/backend/.env.example` around lines 15 - 16, Replace the concrete defaults in the env template by changing ADMIN_USERNAME and ADMIN_PASSWORD to non-functional placeholders (e.g., ADMIN_USERNAME=__REPLACE_ME__ and ADMIN_PASSWORD=__REPLACE_ME__), and add a startup guard that checks these environment variables at runtime and exits with an error if they still equal the placeholder values so deploys must explicitly override them; reference the environment variable names ADMIN_USERNAME and ADMIN_PASSWORD when implementing the check.voidlink/mobile/app.json-32-40 (1)
32-40:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRemove unnecessary Android permissions; keep boot completion permission for notifications.
READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE,USE_BIOMETRIC, andUSE_FINGERPRINTare unnecessary for the configured feature set (no image picker or biometric operations). However,RECEIVE_BOOT_COMPLETEDis required byexpo-notifications. Remove only the four unnecessary permissions to reduce Play policy/privacy risk.Suggested change
"permissions": [ "RECORD_AUDIO", "CAMERA", - "READ_EXTERNAL_STORAGE", - "WRITE_EXTERNAL_STORAGE", "RECEIVE_BOOT_COMPLETED", "VIBRATE", - "USE_BIOMETRIC", - "USE_FINGERPRINT" ]🤖 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 `@voidlink/mobile/app.json` around lines 32 - 40, The Android permissions list in the app config currently includes unnecessary entries; update the "permissions" array to remove "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE", "USE_BIOMETRIC", and "USE_FINGERPRINT" while keeping "RECEIVE_BOOT_COMPLETED" (required by expo-notifications) and other needed permissions like "RECORD_AUDIO", "CAMERA", "VIBRATE"; locate the permissions array in the app configuration (the "permissions" JSON key) and delete only those four permission strings so the app declares only the required Android permissions.voidlink/mobile/src/components/VoiceInput.tsx-18-31 (1)
18-31:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd audio mode reset and unmount cleanup for recording lifecycle.
The
stopRecordingfunction stops the recording but never resets the audio mode back toallowsRecordingIOS: false, leaving the app in recording mode. Additionally, there's no cleanup on component unmount, which can leak recorder resources if the component unmounts while recording is active. Expo's documentation explicitly recommends resetting the audio mode after stopping and cleaning up active recordings during unmount.Add a cleanup effect for unmount and reset the audio mode in the
stopRecordingfinally block:Suggested change
useEffect(() => { @@ - }, [isRecording]); + }, [isRecording, pulseAnim]); + + useEffect(() => { + return () => { + if (recordingRef.current) { + recordingRef.current.stopAndUnloadAsync().catch(() => {}); + recordingRef.current = null; + } + Audio.setAudioModeAsync({ + allowsRecordingIOS: false, + playsInSilentModeIOS: true, + }).catch(() => {}); + }; + }, []); @@ const stopRecording = async () => { @@ } catch (e) { console.error('Failed to stop recording', e); } finally { + await Audio.setAudioModeAsync({ + allowsRecordingIOS: false, + playsInSilentModeIOS: true, + }); recordingRef.current = 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 `@voidlink/mobile/src/components/VoiceInput.tsx` around lines 18 - 31, The stopRecording function must reset the audio mode and ensure recorder resources are cleaned up; update stopRecording to include a finally block that calls Audio.setAudioModeAsync({ allowsRecordingIOS: false }) and clears any recording instance (recorder/unloadAsync) after stopping, and ensure any errors are handled. Add an unmount cleanup effect (a useEffect with return) that checks the recorder/recording instance and, if active, stops and unloads it, and also stops pulseAnim and resets pulseAnim.setValue(1) so animations are cleared on unmount; keep the existing isRecording effect but move/augment cleanup there if preferable. Ensure you reference and operate on the same recorder/recording variable used in the component and call Audio.setAudioModeAsync to reset allowsRecordingIOS to false.voidlink/docker/.env.docker-12-12 (1)
12-12:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse a restrictive CORS default instead of
*.Line 12 sets
ALLOWED_ORIGINS=*, which is an unsafe default for authenticated APIs. Set explicit origins (or fail startup when left as wildcard in non-dev).🤖 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 `@voidlink/docker/.env.docker` at line 12, Replace the unsafe ALLOWED_ORIGINS=* default with explicit allowed origins (e.g., https://app.example.com, https://admin.example.com) in the env template and add a startup validation that fails fast when ALLOWED_ORIGINS is '*' while NODE_ENV is not 'development'; specifically, update the env entry ALLOWED_ORIGINS and add validation logic in your config/bootstrap code to reject or exit with a clear error if ALLOWED_ORIGINS === '*' in non-dev environments (or require a comma-separated allowlist) so the service never runs with a wildcard CORS in production.voidlink/backend/app/services/system_service.py-74-78 (1)
74-78:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun executor tasks concurrently instead of sequential awaits.
These
await loop.run_in_executor(...)calls run one-by-one right now, which adds avoidable latency to every stats request/stream tick.Suggested fix
async def get_full_system_stats() -> Dict[str, Any]: loop = asyncio.get_event_loop() - cpu = await loop.run_in_executor(None, get_cpu_info) - ram = await loop.run_in_executor(None, get_ram_info) - disk = await loop.run_in_executor(None, get_disk_info) - gpu = await loop.run_in_executor(None, get_gpu_info) - net = await loop.run_in_executor(None, get_network_info) + cpu, ram, disk, gpu, net = await asyncio.gather( + loop.run_in_executor(None, get_cpu_info), + loop.run_in_executor(None, get_ram_info), + loop.run_in_executor(None, get_disk_info), + loop.run_in_executor(None, get_gpu_info), + loop.run_in_executor(None, get_network_info), + )🤖 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 `@voidlink/backend/app/services/system_service.py` around lines 74 - 78, The sequential awaits on loop.run_in_executor for get_cpu_info, get_ram_info, get_disk_info, get_gpu_info, and get_network_info cause unnecessary latency; instead schedule them concurrently (e.g., create futures via loop.run_in_executor for each of the five calls and await them together with asyncio.gather or await asyncio.wait) so all blocking info collectors run in parallel and then unpack their results into cpu, ram, disk, gpu, net; update the block that currently calls loop.run_in_executor sequentially to build the five executor futures and await them concurrently.voidlink/backend/app/services/system_service.py-58-59 (1)
58-59:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid blanket
except Exceptionin GPU stats collection.Line 58 swallows all failures, including unexpected bugs, and returns empty GPU data silently. Catch only expected errors (e.g., import/runtime probe errors) and log unexpected ones.
Suggested fix
- except Exception: + except (ImportError, AttributeError, OSError): return []🤖 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 `@voidlink/backend/app/services/system_service.py` around lines 58 - 59, The blanket except in the GPU stats collection (the block that currently reads "except Exception: return []") should be narrowed: catch only the expected errors (e.g., ImportError, OSError, RuntimeError or the specific probe/import error your GPU probe raises) and return [] for those, while logging the exception; for any other unexpected exception, log it with logger.exception (or process logger used across the service) and re-raise so bugs aren't silently swallowed. Update the try/except around the GPU probe in system_service.py to import or reference the module-level logger, explicitly handle expected exceptions (log.debug/info or warn and return []), and use logger.exception + raise for unexpected exceptions.voidlink/docs/APK_BUILD.md-93-101 (1)
93-101:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid documenting plaintext signing secrets in
build.gradle.Lines 98-100 embed passwords directly in source config, which is a credential leak risk. Document
gradle.properties/ env-var based loading instead.🤖 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 `@voidlink/docs/APK_BUILD.md` around lines 93 - 101, The documentation currently shows plaintext secrets embedded in the android build config (the signingConfigs -> release block with storeFile, storePassword, keyAlias, keyPassword); remove those example passwords from build.gradle and instead document loading them from secure sources (gradle.properties or environment variables) and referencing them via project properties when configuring signingConfigs. Update the APK_BUILD.md example to describe using gradle.properties (or ENV vars) for storePassword and keyPassword and show that build.gradle should read those properties rather than containing literal secrets, and mention how to keep the actual keystore path (storeFile) handled securely.voidlink/mobile/app/models.tsx-99-103 (1)
99-103:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDelete flow currently reports success without deleting.
Lines 100-102 show
${name} removedbut the delete call is disabled, so the UI reports a false success.Suggested fix
- await modelsApi.list(); // health check - // await modelsApi.delete(name); + await modelsApi.delete(name); showMessage({ message: `${name} removed`, type: 'success' });🤖 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 `@voidlink/mobile/app/models.tsx` around lines 99 - 103, The UI reports success without performing deletion because the modelsApi.delete call is commented out; restore the actual delete flow by invoking modelsApi.delete(name) (instead of modelsApi.list()) inside the try block, await its resolution, then call fetchModels() and showMessage({ message: `${name} removed`, type: 'success' }) only after delete succeeds; also catch and log/notify on errors so failures don't display a success (refer to modelsApi.delete, modelsApi.list, fetchModels, showMessage and the name variable).voidlink/docker/nginx.conf-30-38 (1)
30-38:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDisable proxy buffering for streaming API responses.
The
/api/chat/sendand/api/models/pullendpoints returnStreamingResponsewith JSON-stream format. Nginx proxy buffering (enabled by default) buffers these responses before sending to clients, causing delays and batching of streamed tokens that degrade the real-time chat UX.Add the following to the
/api/location:location /api/ { proxy_pass http://voidlink; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_cache off; proxy_read_timeout 300s; proxy_send_timeout 300s; }🤖 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 `@voidlink/docker/nginx.conf` around lines 30 - 38, The /api/ location is currently proxied with default buffering which delays JSON-stream streaming responses from endpoints like /api/chat/send and /api/models/pull; update the location /api/ block to disable proxy buffering and use HTTP/1.1 for streaming by adding directives such as proxy_buffering off, proxy_http_version 1.1, proxy_set_header Connection "", and optionally proxy_request_buffering off so streamed tokens are passed through immediately.voidlink/mobile/app/terminal.tsx-50-58 (1)
50-58:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBatch terminal output updates instead of calling
setLinesper line.Lines 50-58 dispatch many state updates for large command output, causing avoidable render churn. Collect the output lines into an array and batch them into a single
setLinescall.Suggested fix
- if (stdout) { - stdout.split('\n').filter(Boolean).forEach((line: string) => { - setLines((prev) => [...prev, { type: 'output', content: line }]); - }); - } - if (stderr) { - stderr.split('\n').filter(Boolean).forEach((line: string) => { - setLines((prev) => [...prev, { type: 'error', content: line }]); - }); - } + const next: TerminalLine[] = []; + if (stdout) { + next.push(...stdout.split('\n').filter(Boolean).map((line: string) => ({ type: 'output' as const, content: line }))); + } + if (stderr) { + next.push(...stderr.split('\n').filter(Boolean).map((line: string) => ({ type: 'error' as const, content: line }))); + } if (!stdout && !stderr) { - setLines((prev) => [...prev, { type: 'system', content: `[exit ${returncode}]` }]); + next.push({ type: 'system', content: `[exit ${returncode}]` }); } + setLines((prev) => [...prev, ...next]);🤖 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 `@voidlink/mobile/app/terminal.tsx` around lines 50 - 58, The current logic calls setLines repeatedly inside stdout and stderr .forEach loops causing many state updates; instead, collect new output and error entries into arrays and call setLines once per stream (or once total) to append them in batch: for stdout gather stdout.split('\n').filter(Boolean).map(line => ({type: 'output', content: line})) and for stderr map to ({type: 'error', content: line})), then call setLines(prev => [...prev, ...newEntries]) (use the existing setLines and lines shape) so only a single state update is triggered per stream or combined once for both.voidlink/mobile/app/qr-scan.tsx-11-20 (1)
11-20:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd permission request logic to prevent dead-end on first launch.
The component displays "Camera permission required" but never calls
requestPermission(), leaving the user stuck on this screen. Add auseEffecthook to automatically request camera permission when needed:Suggested fix
-import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; @@ const [permission, requestPermission] = useCameraPermissions(); @@ + useEffect(() => { + if (permission && !permission.granted) { + requestPermission(); + } + }, [permission, requestPermission]); + + if (!permission) { + return ( + <View style={styles.center}> + <Text style={styles.text}>Checking camera permission...</Text> + </View> + ); + } + if (!permission?.granted) {🤖 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 `@voidlink/mobile/app/qr-scan.tsx` around lines 11 - 20, The component currently renders a dead-end when camera permission is not granted because it never calls requestPermission(); add a useEffect hook that runs when permission changes (e.g., dependency [permission?.granted]) and calls requestPermission() automatically when permission is undefined or not granted (optionally guard with permission?.canAskAgain) so the permission prompt appears on first launch; ensure you handle the returned status/promise from requestPermission() and only show the "Camera permission required" view if the user ultimately denies permission.voidlink/backend/app/core/database.py-6-10 (1)
6-10:⚠️ Potential issue | 🟠 Major | ⚡ Quick winConditionally apply
check_same_threadonly for SQLite databases.The
check_same_threadparameter is SQLite-specific. Passing it unconditionally tocreate_async_engine()will cause a TypeError ifDATABASE_URLis configured for PostgreSQL or MySQL, even though the default uses SQLite. While this doesn't affect the current default setup, it breaks any deployment that switches to a different database engine.The suggested fix is correct and minimal:
Suggested fix
+from sqlalchemy.engine import make_url + -engine = create_async_engine( - settings.DATABASE_URL, - echo=settings.DEBUG, - connect_args={"check_same_thread": False}, -) +db_url = make_url(settings.DATABASE_URL) +engine_kwargs = {"echo": settings.DEBUG} +if db_url.drivername.startswith("sqlite"): + engine_kwargs["connect_args"] = {"check_same_thread": False} + +engine = create_async_engine(settings.DATABASE_URL, **engine_kwargs)🤖 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 `@voidlink/backend/app/core/database.py` around lines 6 - 10, The create_async_engine call is passing connect_args={"check_same_thread": False} unconditionally which is SQLite-specific; update the engine creation so that before calling create_async_engine you detect whether settings.DATABASE_URL points to SQLite (e.g., startswith "sqlite://" or parse via sqlalchemy.engine.url.make_url) and only include connect_args with "check_same_thread": False when SQLite is detected; leave connect_args out or pass an empty dict for other backends so engine, create_async_engine, settings.DATABASE_URL and the connect_args handling are adjusted accordingly.voidlink/mobile/app/models.tsx-60-82 (1)
60-82:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate pull response status and parse NDJSON incrementally.
Line 81 shows a success message even when the pull request fails, because the code never checks
res.ok— HTTP errors from the server don't reject the fetch promise. Additionally, lines 74-77 drop progress updates when JSON objects span multiple chunks, since the code naively parses each raw chunk as complete JSON. The backend returnsapplication/x-ndjson(one JSON object per line), so implement a newline-buffered parser and validate the response status.Suggested fix
- const res = await fetch(`${(await import('../src/services/api')).getBaseUrl()}/api/models/pull`, { + const res = await fetch(`${(await import('../src/services/api')).getBaseUrl()}/api/models/pull`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${await (await import('../src/services/api')).getStoredToken()}`, }, body: JSON.stringify({ model_name: modelName }), }); + if (!res.ok || !res.body) { + throw new Error(`Pull failed (${res.status})`); + } const reader = res.body?.getReader(); const decoder = new TextDecoder(); + let buffer = ''; if (reader) { while (true) { const { done, value } = await reader.read(); if (done) break; - const text = decoder.decode(value); - try { - const data = JSON.parse(text.trim()); - if (data.status) setPullProgress(data.status); - } catch { /* ignore */ } + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() ?? ''; + for (const line of lines) { + if (!line.trim()) continue; + try { + const data = JSON.parse(line); + if (data.status) setPullProgress(data.status); + } catch { /* ignore malformed line */ } + } } }🤖 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 `@voidlink/mobile/app/models.tsx` around lines 60 - 82, Check HTTP response status before treating the pull as successful and implement line-buffered NDJSON parsing for the stream: after fetching with getBaseUrl()/getStoredToken(), if !res.ok read and parse the full response text (or JSON) and call showMessage with an error (do NOT call the success message or fetchModels); if res.ok, create a TextDecoder and accumulate decoded chunks into a buffer string, split by newline into complete lines, JSON.parse each non-empty line and call setPullProgress(data.status) for updates, keep any trailing partial line in the buffer for the next chunk, and after the reader finishes show the success message and await fetchModels().voidlink/docker/docker-compose.yml-16-18 (1)
16-18:⚠️ Potential issue | 🟠 Major | ⚡ Quick winWeak default secrets pose a security risk.
The default
SECRET_KEYandADMIN_PASSWORDare easily guessable. If users deploy without changing these, the system is vulnerable to unauthorized access.Consider adding a startup check that refuses to run with default secrets, or at minimum, add prominent warnings in deployment documentation.
🤖 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 `@voidlink/docker/docker-compose.yml` around lines 16 - 18, The compose defaults expose weak credentials (SECRET_KEY, ADMIN_USERNAME, ADMIN_PASSWORD); remove these insecure fallback values and implement a startup validation in the container entrypoint or app bootstrap that checks if SECRET_KEY == "change-this-secret-key-in-production" or ADMIN_PASSWORD == "changeme" (and optionally ADMIN_USERNAME == "admin"), log a clear error and exit non‑zero if they are unchanged; alternatively require those env vars to be set (no defaults in the compose) and surface a prominent warning in deployment docs instructing operators to set secure values before run.voidlink/docker/docker-compose.yml-22-22 (1)
22-22:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCORS wildcard allows requests from any origin.
ALLOWED_ORIGINS=*permits cross-origin requests from any domain, which could enable CSRF-style attacks or unauthorized API access from malicious sites.🔒 Recommended configuration
- - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:8081,http://192.168.1.100:8081}Or document that users must explicitly set
ALLOWED_ORIGINSin their.envfile to the mobile app's origin.🤖 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 `@voidlink/docker/docker-compose.yml` at line 22, The docker-compose default exposes ALLOWED_ORIGINS=* which allows any origin; change the default in the ALLOWED_ORIGINS environment variable declaration so it does not default to a wildcard (e.g., remove the wildcard default or set a safe default like an empty string), and document/update project docs/.env.example to require explicitly setting ALLOWED_ORIGINS to the allowed origins (or a comma-separated list) for the mobile/web app origin(s); ensure the deployment uses the ALLOWED_ORIGINS env var (referenced as ALLOWED_ORIGINS in docker-compose.yml) so the app enforces a restrictive CORS policy in production.voidlink/backend/app/api/files.py-27-28 (1)
27-28:⚠️ Potential issue | 🟠 Major | ⚡ Quick winTrusting client MIME type alone is spoofable.
Line 27 validates only
file.content_type, which is client-controlled. Add server-side content sniffing (magic bytes) for types like images/PDF to enforce actual file format.🤖 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 `@voidlink/backend/app/api/files.py` around lines 27 - 28, The current check relies only on client-provided file.content_type (variable file and constant ALLOWED_TYPES); instead perform server-side content sniffing by reading the file's leading bytes from the UploadFile (seek/read a small header chunk), detect the true mime/type using a reliable library (e.g., python-magic or imghdr for images, PDF header bytes for PDFs), compare that detected type against ALLOWED_TYPES, reset the file pointer (seek(0)) before further processing, and raise the same HTTPException when the detected type is not allowed; update the validation around file.content_type to use the sniffed result instead of trusting the client value.voidlink/mobile/app/(tabs)/index.tsx-33-40 (1)
33-40:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFolder-scoped refresh can permanently narrow the “All” list.
On Line 36, refresh fetches with
selectedFolderand then overwrites the sharedconversationsstore. After refreshing in a folder, switching to “All” can still show only that subset.Suggested fix
- const [convsRes, foldersRes] = await Promise.all([ - chatApi.getConversations(selectedFolder ?? undefined), - chatApi.getFolders(), - ]); + const [convsRes, foldersRes] = await Promise.all([ + chatApi.getConversations(), // keep canonical full list in store + chatApi.getFolders(), + ]);- const matchFolder = selectedFolder == null || c.folderId === selectedFolder; + const matchFolder = selectedFolder == null || c.folderId === selectedFolder;(Keep folder filtering client-side as you already do in
filtered.)Also applies to: 46-50, 146-158
🤖 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 `@voidlink/mobile/app/`(tabs)/index.tsx around lines 33 - 40, fetchData (and other refresh/load spots that call chatApi.getConversations with selectedFolder) is overwriting the global conversations state with a folder-scoped subset; change those calls to fetch the full conversation list (call chatApi.getConversations(undefined) or omit the folder arg) and update setConversations with that full result, then rely on the existing filtered computed list to show folder-scoped views. Locate functions like fetchData and other refresh/load helpers that use chatApi.getConversations(selectedFolder) (also the similar calls around the other noted blocks) and replace the argument so the global store always contains all conversations while folder filtering remains client-side.voidlink/backend/app/main.py-99-104 (1)
99-104:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGlobal error response leaks internal exception type.
Line 103 exposes
type(exc).__name__to clients. This increases information leakage and can aid endpoint probing.Suggested fix
return JSONResponse( status_code=500, - content={"detail": "Internal server error", "type": type(exc).__name__}, + content={"detail": "Internal server error"}, )🤖 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 `@voidlink/backend/app/main.py` around lines 99 - 104, The global_exception_handler currently returns type(exc).__name__ to clients (see global_exception_handler, Request, JSONResponse and type(exc).__name__), which leaks internal exception types; remove the exception type from the returned JSON content and instead log the full exception internally (e.g., logger.exception or similar) and return a generic error payload (optionally a non-sensitive error id/trace token generated server-side) so clients get no internal type details while you retain diagnostics in server logs.voidlink/backend/app/services/user_service.py-50-57 (1)
50-57:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
update_user_settingsis vulnerable to mass assignment.On Line 54–56, any attribute present on
Usercan be updated (including sensitive fields like role/admin flags or password fields) if upstream validation misses one case.Suggested fix
async def update_user_settings(db: AsyncSession, user_id: int, **kwargs): + allowed_fields = {"email", "memory_context"} # expand intentionally result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if user: for k, v in kwargs.items(): - if hasattr(user, k): + if k in allowed_fields: setattr(user, k, v)🤖 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 `@voidlink/backend/app/services/user_service.py` around lines 50 - 57, The update_user_settings function currently allows mass-assignment on the User model; restrict updates by defining an explicit whitelist of allowed updatable fields (e.g., display_name, bio, preferences) and only set attributes from kwargs that appear in that whitelist, ignoring or rejecting any keys that map to sensitive fields (role, is_admin, password, created_at, etc.); handle special cases like password changes via a dedicated method (e.g., change_password) that performs hashing/validation, and then commit the session; reference update_user_settings and the User model when making this change.voidlink/backend/app/main.py-89-96 (1)
89-96:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHealth endpoint reports healthy even when dependency is down.
On Line 94, status is always
"healthy"even ifollama_okis false. That can mask outages in probes and dashboards.Suggested fix
`@app.get`("/health") async def health(): from app.services.ollama_service import check_ollama_health ollama_ok = await check_ollama_health() + overall = "healthy" if ollama_ok else "degraded" return { - "status": "healthy", + "status": overall, "ollama": "connected" if ollama_ok else "disconnected", }🤖 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 `@voidlink/backend/app/main.py` around lines 89 - 96, The health() endpoint always returns "status": "healthy" even when check_ollama_health() fails; update health() (the GET "/health" handler) to set the status to "unhealthy" when ollama_ok is False and return an appropriate non-200 code (e.g., raise fastapi.HTTPException(status_code=503) or return Response(status_code=503)) so probes see a failing health check; ensure you reference check_ollama_health and the health() function when making the change.voidlink/backend/app/core/config.py-17-17 (1)
17-17:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHardcoded default admin password is unsafe.
Line 17 defaults to
"changeme", which is risky combined with automatic admin seeding.Suggested fix
- ADMIN_PASSWORD: str = "changeme" + ADMIN_PASSWORD: strRequire explicit env configuration and fail startup when missing.
🤖 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 `@voidlink/backend/app/core/config.py` at line 17, The ADMIN_PASSWORD default ("changeme") is unsafe; remove the hardcoded default and require an explicit environment value by reading ADMIN_PASSWORD from the environment (or settings source) and failing startup if it's missing/empty: replace the constant ADMIN_PASSWORD assignment in config.py with logic that fetches the env var and raises a clear exception (e.g., RuntimeError) when ADMIN_PASSWORD is not set, so any automatic admin seeding uses an explicit, provided password.voidlink/backend/app/services/ollama_service.py-57-63 (1)
57-63:⚠️ Potential issue | 🟠 Major | ⚡ Quick winStreaming endpoints disable all timeouts, risking indefinite hangs on stalled upstream connections.
Lines 57 (
pull_model) and 99 (chat_stream) usetimeout=None, which disables connect, read, write, and pool timeouts entirely. If Ollama stalls, these async generators can hang indefinitely. Use a bounded pattern instead—for example,httpx.Timeout(connect=10.0, read=None)to allow streaming while preventing connection hangs.🤖 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 `@voidlink/backend/app/services/ollama_service.py` around lines 57 - 63, The streaming calls in pull_model and chat_stream create AsyncClient with timeout=None which disables all timeouts and can hang indefinitely; change to use a bounded httpx.Timeout (e.g. httpx.Timeout(connect=10.0, read=None) or similar) when creating httpx.AsyncClient so you keep a long/disabled read timeout for streaming but still enforce connect (and optionally write/pool) timeouts to avoid stalled upstream hangs; update the AsyncClient(...) timeout argument in pull_model and chat_stream to a configured httpx.Timeout instance.voidlink/backend/app/services/conversation_service.py-76-79 (1)
76-79:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd user authorization check and handle missing conversation.
The conversation is fetched without verifying ownership or existence. If
convisNone(conversation deleted), the message is still committed buttotal_tokensis not updated. This could lead to orphaned messages and incorrect token counts.🔒 Proposed fix to add validation
db.add(msg) result = await db.execute(select(Conversation).where(Conversation.id == conv_id)) conv = result.scalar_one_or_none() - if conv: + if not conv: + raise ValueError(f"Conversation {conv_id} not found") - conv.total_tokens = (conv.total_tokens or 0) + tokens + conv.total_tokens = (conv.total_tokens or 0) + tokens await db.commit()Note: Since this is a service function, you might want to let the caller handle user_id validation, or add it as a parameter if needed.
🤖 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 `@voidlink/backend/app/services/conversation_service.py` around lines 76 - 79, The Conversation fetch in conversation_service.py (using db.execute(select(Conversation).where(Conversation.id == conv_id))) lacks ownership and existence checks: ensure the service verifies the conversation exists and belongs to the requesting user before updating conv.total_tokens (check Conversation.user_id == user_id), and if conv is None or user_id mismatches, raise a clear exception (e.g., NotFound or PermissionDenied) so the calling code can abort committing the message and avoid orphaned messages and incorrect token counts; if this function currently lacks a user_id parameter, add it to the signature so you can perform the ownership check.voidlink/backend/app/api/system.py-109-110 (1)
109-110: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winUse dependency injection for database session.
This endpoint creates
AsyncSessionLocaldirectly instead of usingdb: AsyncSession = Depends(get_db)like other endpoints. This inconsistency makes the code harder to maintain and bypasses any session lifecycle management inget_db.♻️ Proposed fix to use dependency injection
`@router.put`("/settings") async def update_settings( req: UserSettingsUpdate, current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), ): - from app.core.database import AsyncSessionLocal - async with AsyncSessionLocal() as db: - updates = {k: v for k, v in req.model_dump().items() if v is not None} - user = await update_user_settings(db, current_user.id, **updates) - return { - "system_prompt": user.system_prompt, - "character_mode": user.character_mode, - "memory_enabled": user.memory_enabled, - } + updates = {k: v for k, v in req.model_dump().items() if v is not None} + user = await update_user_settings(db, current_user.id, **updates) + return { + "system_prompt": user.system_prompt, + "character_mode": user.character_mode, + "memory_enabled": user.memory_enabled, + }🤖 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 `@voidlink/backend/app/api/system.py` around lines 109 - 110, The endpoint currently instantiates AsyncSessionLocal directly; change it to accept the session via FastAPI dependency injection by adding a parameter like db: AsyncSession = Depends(get_db) to the endpoint signature (replace any "async with AsyncSessionLocal() as db" usage), remove the direct AsyncSessionLocal import/usage, and ensure Depends and AsyncSession are imported; update code paths inside the endpoint to use the injected db variable and rely on get_db for lifecycle management.voidlink/backend/app/services/conversation_service.py-128-130 (1)
128-130:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUnused query and potential data integrity issue.
Lines 128-130 execute a query for conversations in the folder but don't use the result. More importantly, deleting a folder without handling its conversations will leave them with invalid
folder_idforeign keys (unless the database has CASCADE or SET NULL configured). This could cause referential integrity errors or orphaned data.🔧 Proposed fix to handle conversations
Option 1: Set folder_id to NULL for conversations:
folder = result.scalar_one_or_none() if folder: - await db.execute( - select(Conversation).where(Conversation.folder_id == folder_id) - ) + # Clear folder_id from conversations + result = await db.execute( + select(Conversation).where(Conversation.folder_id == folder_id) + ) + conversations = result.scalars().all() + for conv in conversations: + conv.folder_id = None await db.delete(folder) await db.commit() return True return FalseOption 2: Prevent deletion if conversations exist:
folder = result.scalar_one_or_none() if folder: - await db.execute( + result = await db.execute( select(Conversation).where(Conversation.folder_id == folder_id) ) + if result.scalars().first(): + raise ValueError("Cannot delete folder with existing conversations") await db.delete(folder) await db.commit() return True return False🤖 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 `@voidlink/backend/app/services/conversation_service.py` around lines 128 - 130, The select(Conversation).where(Conversation.folder_id == folder_id) is executed but its result is unused, and deleting a folder without handling related Conversation rows will orphan foreign keys; update the folder-deletion flow in conversation_service.py to either (A) run an UPDATE on Conversation to set folder_id = NULL for all rows matching Conversation.folder_id == folder_id before deleting the Folder, or (B) run a COUNT/SELECT for Conversation rows with that folder_id and raise/abort the deletion if any exist (prevent deletion). Remove the unused db.execute(select(...)) call and replace it with the chosen handling (use Conversation model and the existing db session methods to execute the UPDATE or the existence check and raise a clear error).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0d73eee3-561b-4258-b04b-38bb9835357d
📒 Files selected for processing (59)
voidlink/.gitignorevoidlink/README.mdvoidlink/backend/.env.examplevoidlink/backend/app/__init__.pyvoidlink/backend/app/api/__init__.pyvoidlink/backend/app/api/auth.pyvoidlink/backend/app/api/chat.pyvoidlink/backend/app/api/files.pyvoidlink/backend/app/api/models.pyvoidlink/backend/app/api/system.pyvoidlink/backend/app/core/__init__.pyvoidlink/backend/app/core/config.pyvoidlink/backend/app/core/database.pyvoidlink/backend/app/core/security.pyvoidlink/backend/app/main.pyvoidlink/backend/app/models/__init__.pyvoidlink/backend/app/models/conversation.pyvoidlink/backend/app/models/device.pyvoidlink/backend/app/models/user.pyvoidlink/backend/app/services/__init__.pyvoidlink/backend/app/services/conversation_service.pyvoidlink/backend/app/services/ollama_service.pyvoidlink/backend/app/services/system_service.pyvoidlink/backend/app/services/user_service.pyvoidlink/backend/app/utils/__init__.pyvoidlink/backend/requirements.txtvoidlink/backend/run.pyvoidlink/docker/.env.dockervoidlink/docker/Dockerfile.backendvoidlink/docker/docker-compose.ymlvoidlink/docker/nginx.confvoidlink/docs/APK_BUILD.mdvoidlink/docs/DEPLOYMENT.mdvoidlink/mobile/app.jsonvoidlink/mobile/app/(tabs)/_layout.tsxvoidlink/mobile/app/(tabs)/index.tsxvoidlink/mobile/app/(tabs)/new-chat.tsxvoidlink/mobile/app/(tabs)/profile.tsxvoidlink/mobile/app/(tabs)/system.tsxvoidlink/mobile/app/_layout.tsxvoidlink/mobile/app/chat/[id].tsxvoidlink/mobile/app/index.tsxvoidlink/mobile/app/login.tsxvoidlink/mobile/app/models.tsxvoidlink/mobile/app/qr-scan.tsxvoidlink/mobile/app/terminal.tsxvoidlink/mobile/babel.config.jsvoidlink/mobile/eas.jsonvoidlink/mobile/package.jsonvoidlink/mobile/src/components/CyberButton.tsxvoidlink/mobile/src/components/GlowText.tsxvoidlink/mobile/src/components/MessageBubble.tsxvoidlink/mobile/src/components/ModelSelector.tsxvoidlink/mobile/src/components/VoiceInput.tsxvoidlink/mobile/src/services/api.tsvoidlink/mobile/src/store/useAuthStore.tsvoidlink/mobile/src/store/useChatStore.tsvoidlink/mobile/src/utils/theme.tsvoidlink/mobile/tsconfig.json
| content = await file.read() | ||
| if len(content) > settings.max_upload_bytes: | ||
| raise HTTPException(status_code=413, detail=f"File too large. Max {settings.MAX_UPLOAD_SIZE_MB}MB") |
There was a problem hiding this comment.
Upload path is vulnerable to memory exhaustion.
Line 30 reads the full file into memory before enforcing max size (Line 31). Large uploads can exhaust worker memory.
Suggested fix
- content = await file.read()
- if len(content) > settings.max_upload_bytes:
- raise HTTPException(status_code=413, detail=f"File too large. Max {settings.MAX_UPLOAD_SIZE_MB}MB")
+ max_bytes = settings.max_upload_bytes
+ size = 0
+ chunks: list[bytes] = []
+ while True:
+ chunk = await file.read(1024 * 1024)
+ if not chunk:
+ break
+ size += len(chunk)
+ if size > max_bytes:
+ raise HTTPException(status_code=413, detail=f"File too large. Max {settings.MAX_UPLOAD_SIZE_MB}MB")
+ chunks.append(chunk)
+ content = b"".join(chunks)🤖 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 `@voidlink/backend/app/api/files.py` around lines 30 - 32, The current code
calls await file.read() into memory then checks settings.max_upload_bytes,
risking memory exhaustion; change to stream the upload in chunks (e.g., call
await file.read(CHUNK_SIZE) repeatedly) and track a running total_size, and if
total_size exceeds settings.max_upload_bytes immediately raise
HTTPException(413) using settings.MAX_UPLOAD_SIZE_MB in the message; only buffer
to memory (or write to a temp file/BytesIO) after confirming the total is within
the limit. Use the UploadFile instance read() loop and the
settings.max_upload_bytes/ settings.MAX_UPLOAD_SIZE_MB symbols to locate and
update the logic.
| PORT: int = 8000 | ||
| DEBUG: bool = False | ||
|
|
||
| SECRET_KEY: str = secrets.token_urlsafe(32) |
There was a problem hiding this comment.
SECRET_KEY random default can break auth across restarts/workers.
Line 12 generates a new key at process start if env is missing. Tokens can become invalid after restart and between workers with different process-level keys.
Suggested fix
- SECRET_KEY: str = secrets.token_urlsafe(32)
+ SECRET_KEY: strFail fast if missing (and keep generation only in .env.example instructions, not runtime defaults).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| SECRET_KEY: str = secrets.token_urlsafe(32) | |
| SECRET_KEY: str |
🤖 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 `@voidlink/backend/app/core/config.py` at line 12, The SECRET_KEY is currently
generated at process start which breaks auth across restarts/workers; change the
config so SECRET_KEY is read from the environment without a runtime fallback and
raise/exit immediately if it's not provided (fail-fast), leaving any token
generation instructions only in .env.example or docs; update the configuration
assignment that references SECRET_KEY to validate presence and throw a clear
error instead of calling secrets.token_urlsafe at runtime.
| StyleSheet, | ||
| Animated, | ||
| TouchableOpacity, | ||
| Clipboard, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify Clipboard deprecation and check for `@react-native-clipboard/clipboard`
# Check if `@react-native-clipboard/clipboard` is in package.json
cd voidlink/mobile
if [ -f package.json ]; then
echo "=== Checking mobile package.json for clipboard package ==="
cat package.json | jq '.dependencies["@react-native-clipboard/clipboard"]'
fi
# Search for other Clipboard usages
echo "=== Searching for other Clipboard imports ==="
rg -n "from 'react-native'" --type ts --type tsx -A 1 -B 1 | rg -i clipboardRepository: CCguvycu/cy
Length of output: 211
🏁 Script executed:
# Read the MessageBubble.tsx file to see imports and usage
head -40 voidlink/mobile/src/components/MessageBubble.tsxRepository: CCguvycu/cy
Length of output: 1227
🏁 Script executed:
# Check React Native version in package.json
cat voidlink/mobile/package.json | jq '.dependencies."react-native"'Repository: CCguvycu/cy
Length of output: 64
🏁 Script executed:
# Search for Clipboard imports across the codebase
rg "Clipboard" --type typescript --type tsx voidlink/mobile/src/Repository: CCguvycu/cy
Length of output: 90
🏁 Script executed:
# Search for Clipboard imports and usage with simpler pattern
rg "Clipboard" voidlink/mobile/src/ -nRepository: CCguvycu/cy
Length of output: 213
🌐 Web query:
React Native 0.74.1 Clipboard API availability removed deprecated
💡 Result:
The Clipboard API was not removed or newly deprecated in React Native 0.74.1. It was extracted from React Native core and moved to the community package @react-native-clipboard/clipboard around 2020 [1][2], with a deprecation notice added to the docs at that time [1]. The official documentation currently marks it as removed, recommending community packages [3][4]. React Native 0.74 release notes mention removals of other deprecated APIs like PropTypes and changes to PushNotificationIOS, but not Clipboard [5][6][7]. In 0.74.1 specifically, no Clipboard-related changes are noted [6]. Issues in 0.74.x relate to compatibility fixes needed in the community clipboard package for New Architecture, not core removal [8][9][10][11]. Use @react-native-clipboard/clipboard (latest v1.16.3 as of 2025 [2]) for Clipboard functionality in RN 0.74.1.
Citations:
- 1: docs: add deprecation notice for clipboard react/react-native-website#1608
- 2: https://www.npmjs.com/package/@react-native-clipboard/clipboard
- 3: https://reactnative.dev/docs/clipboard
- 4: https://reactnative.dev/docs/next/clipboard
- 5: https://reactnative.dev/blog/2024/04/22/release-0.74
- 6: https://github.com/facebook/react-native/releases/tag/v0.74.1
- 7: https://github.com/facebook/react-native/releases/tag/v0.74.0
- 8: feat: prepare lib for RN 0.74 react-native-clipboard/clipboard#226
- 9: Allowing RN 0.74 as a peer dependency react-native-clipboard/clipboard#239
- 10: react-native-clipboard_clipboard not working RN 0.75.3 in new architecture react-native-clipboard/clipboard#259
- 11: Library unusable with react native 0.74.2 react-native-clipboard/clipboard#250
Replace deprecated Clipboard import with @react-native-clipboard/clipboard.
The Clipboard module is no longer exported from react-native in version 0.74.1. This import at line 8 will cause a runtime error when the code executes the Clipboard.setString() call at line 34. Use the community package @react-native-clipboard/clipboard instead.
First, install the package:
npm install `@react-native-clipboard/clipboard`
npx pod-install # iOS onlyThen update the import:
Diff
import {
View,
Text,
StyleSheet,
Animated,
TouchableOpacity,
- Clipboard,
} from 'react-native';
+import Clipboard from '@react-native-clipboard/clipboard';🤖 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 `@voidlink/mobile/src/components/MessageBubble.tsx` at line 8, The file imports
Clipboard from react-native which is deprecated; replace that import with the
community package `@react-native-clipboard/clipboard` and update usages (e.g., the
Clipboard.setString call inside MessageBubble.tsx) accordingly: install
`@react-native-clipboard/clipboard` (npm install `@react-native-clipboard/clipboard`
and npx pod-install for iOS), change the import to import Clipboard from
'@react-native-clipboard/clipboard', and ensure any references to
Clipboard.setString(...) still point to the new Clipboard export.
| }) => fetch(`${_baseUrl}/api/chat/send`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: '', | ||
| }, | ||
| body: JSON.stringify(data), | ||
| }), |
There was a problem hiding this comment.
Critical: sendMessage has empty Authorization header.
Line 95 sets Authorization: '', which means requests will fail authentication. This function uses fetch directly instead of the Axios instance, bypassing the token interceptor. Either use api().post(...) or fetch the token and include it like the streamChat function does (lines 156-161).
🐛 Proposed fix to include token
sendMessage: (data: {
conversation_id?: number;
model: string;
message: string;
system_prompt?: string;
character_mode?: string;
temperature?: number;
folder_id?: number;
attachments?: string[];
- }) => fetch(`${_baseUrl}/api/chat/send`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: '',
- },
- body: JSON.stringify(data),
- }),
+ }) => api().post('/api/chat/send', data),Note: If you need streaming with fetch, you should move this to a separate streaming function (or merge with streamChat), since api().post() won't support reading the response body as a stream.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| }) => fetch(`${_baseUrl}/api/chat/send`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: '', | |
| }, | |
| body: JSON.stringify(data), | |
| }), | |
| }) => api().post('/api/chat/send', data), |
🤖 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 `@voidlink/mobile/src/services/api.ts` around lines 91 - 98, The sendMessage
implementation sets Authorization: '' and bypasses the Axios instance; fix by
either (A) replace the fetch call with the Axios helper
api().post('/api/chat/send', data) so the token interceptor is applied (update
the sendMessage export accordingly), or (B) if you must use fetch for streaming,
copy the token retrieval used in streamChat (the code that reads await (await
api()).getAccessToken() or similar) and set Authorization: `Bearer ${token}` in
the headers before sending; ensure the function name sendMessage is updated to
use one of these two approaches so requests include a valid token.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
voidlink/mobile/build_apk.bat (1)
1-44:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse CRLF line endings for this
.batfileLF-only endings can cause parsing edge cases in
cmd.exe. Save this script with Windows CRLF endings to avoid environment-specific failures.🤖 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 `@voidlink/mobile/build_apk.bat` around lines 1 - 44, The build_apk.bat script uses LF line endings which can break cmd.exe parsing; re-save this batch file (build_apk.bat) with Windows CRLF (\r\n) line endings so commands like "@echo off", "call npm install", "call eas login" and "call eas build --platform android --profile preview --non-interactive" run reliably on Windows; ensure your editor or Git core.autocrlf is configured to commit the file with CRLF endings.voidlink/desktop/build_windows.bat (1)
1-59:⚠️ Potential issue | 🟠 Major | ⚡ Quick winConvert file to Windows (CRLF) line endings.
The batch file currently uses Unix line endings (LF-only), which can cause unpredictable failures in Windows batch parsing, especially with GOTO/CALL statements and at 512-byte boundaries. As per static analysis hints, this can lead to script malfunction.
🔧 How to fix
Convert the file to CRLF line endings using one of these methods:
Git configuration (automatic):
git config core.autocrlf true git rm --cached build_windows.bat git add build_windows.bat git commit -m "Fix line endings"dos2unix tool:
unix2dos voidlink/desktop/build_windows.batIn your editor:
- VS Code: Click "LF" in status bar → Select "CRLF"
- Notepad++: Edit → EOL Conversion → Windows (CRLF)
- Vim:
:set ff=dosAdd .gitattributes to prevent future issues:
*.bat text eol=crlf🤖 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 `@voidlink/desktop/build_windows.bat` around lines 1 - 59, The build_windows.bat file uses LF-only line endings which can break Windows batch parsing; convert build_windows.bat to CRLF (Windows) line endings, recommit the corrected file, and ensure future commits enforce CRLF for .bat files (e.g., add a .gitattributes entry forcing eol=crlf for *.bat or enable core.autocrlf) so the installer/build script runs reliably on Windows.
🧹 Nitpick comments (3)
voidlink/installer/voidlink_setup.iss (1)
47-48: 💤 Low valueWizard image files are optional but referenced.
Lines 47-48 reference custom wizard images (
wizard_banner.bmp,wizard_icon.bmp). If these files are missing, Inno Setup will use default images, but you may want to either create them or remove these lines.🤖 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 `@voidlink/installer/voidlink_setup.iss` around lines 47 - 48, The installer references custom wizard images via the WizardImageFile and WizardSmallImageFile directives but the files (wizard_banner.bmp, wizard_icon.bmp) may be missing; either add those bitmap files to the installer resources (ensuring correct names and paths) or remove/comment out the WizardImageFile and WizardSmallImageFile lines in voidlink_setup.iss so Inno Setup falls back to the defaults; update any installer build scripts or resource copying steps to include the images if you choose to add them.voidlink/desktop/tray_app.py (2)
268-273: ⚖️ Poor tradeoff
stop_backend()doesn't handle in-process uvicorn.When running frozen (PyInstaller bundle), the backend runs in-process via
_run_uvicorn_inprocess(), butstop_backend()only terminates subprocess instances. The app relies onos.kill(os.getpid(), 9)at line 354 to force-kill everything on quit.While SIGKILL works for a tray app, a graceful shutdown would allow the backend to close database connections and finish pending requests. Consider storing the
uvicorn.Serverinstance globally and callingserver.should_exit = Trueinstop_backend().🤖 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 `@voidlink/desktop/tray_app.py` around lines 268 - 273, stop_backend() currently only terminates subprocesses via _server_process and ignores in-process uvicorn started by _run_uvicorn_inprocess(); modify the module to store the uvicorn.Server instance returned/created by _run_uvicorn_inprocess() in a global (e.g., _uvicorn_server) and update stop_backend() to: if _uvicorn_server is set, set _uvicorn_server.should_exit = True (and optionally await or join the server shutdown if applicable), else fall back to terminating _server_process and clearing globals (_uvicorn_server and _server_process) to ensure graceful shutdown of in-process servers and proper cleanup of resources like DB connections.
172-198: ⚖️ Poor tradeoffConsider adding installer hash verification.
The function downloads and executes the Ollama installer without verifying its hash. While the HTTPS URL provides transport security, adding SHA256 verification would protect against compromised download servers or MITM attacks.
💡 Example implementation with hash verification
import hashlib def download_ollama(progress_callback=None): """Download and install Ollama silently.""" if platform.system() != 'Windows': return False installer_path = DATA_DIR / 'OllamaSetup.exe' # Expected SHA256 would need to be updated with each Ollama release expected_hash = "..." # TODO: Fetch from Ollama's release API log.info("Downloading Ollama installer...") try: def reporthook(count, block_size, total_size): if progress_callback and total_size > 0: pct = int(count * block_size * 100 / total_size) progress_callback(min(pct, 100)) urllib.request.urlretrieve(OLLAMA_WINDOWS_URL, installer_path, reporthook) # Verify hash sha256 = hashlib.sha256() with open(installer_path, 'rb') as f: for chunk in iter(lambda: f.read(4096), b''): sha256.update(chunk) if expected_hash and sha256.hexdigest() != expected_hash: log.error("Installer hash mismatch") installer_path.unlink(missing_ok=True) return False log.info("Running Ollama installer silently...") # ... rest of function🤖 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 `@voidlink/desktop/tray_app.py` around lines 172 - 198, The download_ollama function currently runs the downloaded installer without verifying integrity; compute a SHA256 of installer_path after urlretrieve and compare it against a trusted expected_hash (obtained from a constant, config, or secure release API) before running subprocess.run; on mismatch should log an error, unlink installer_path, and return False; keep existing progress_callback and exception handling and only run subprocess.run([str(installer_path), '/S']) when the hash check passes.
🤖 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 `@voidlink/desktop/tray_app.py`:
- Around line 222-242: The subprocess call in start_backend_server currently
discards stdout/stderr (hiding startup errors); change it so stderr is captured
to a persistent log file (e.g., open a file like BUNDLE_DIR / 'backend.log' and
pass that file handle as stderr to subprocess.Popen) while keeping stdout
suppressed, and ensure the file handle is opened before calling Popen and
closed/kept alive for the lifetime of _server_process; reference
start_backend_server, _server_process, BUNDLE_DIR, env_path and port when making
the change so backend startup exceptions are preserved for diagnosis.
- Around line 200-214: The subprocess call in ensure_default_model uses the bare
'ollama' command which can fail if not in PATH; call find_ollama() (or a
module-level OLLAMA_PATH set from find_ollama()) to get the full executable path
and use that in the subprocess.Popen argument list instead of 'ollama', and
guard for a None/empty result (log and skip pull if not found) so
ensure_default_model and any callers (e.g., where ensure_default_model is
invoked) use the resolved path.
In `@voidlink/installer/voidlink_setup.iss`:
- Line 32: Add the missing license.txt referenced by the installer
configuration: create a plain-text file named license.txt in the installer
directory (to satisfy the LicenseFile=license.txt entry) containing the software
license or placeholder legal text, then commit it so the Inno Setup script can
compile successfully.
In `@voidlink/mobile/app/login.tsx`:
- Around line 42-65: runDiscovery can leave stale selection behind and
handleConnect currently falls back to the hidden serverUrl, allowing old
endpoints to be submitted; at the start of runDiscovery clear the
discovery-driven state by calling setSelectedServer('') and
setServerUrlState('') (or equivalent) before calling discover(), and change
handleConnect so that when in scan/discovery mode it uses only selectedServer
(no fallback to serverUrl) — if selectedServer is empty show the "Enter a server
URL" warning instead of submitting serverUrl; update references to runDiscovery,
handleConnect, selectedServer, serverUrl, setSelectedServer, setServerUrlState
and setServerUrl accordingly.
In `@voidlink/mobile/build_apk.bat`:
- Around line 19-41: The script prints a success message regardless of failures;
after each external command that can fail (the global installer npm install -g
eas-cli, call npm install, call eas login, and call eas build --platform android
--profile preview --non-interactive) check ERRORLEVEL immediately and exit with
a non-zero code and an error message if it failed, and move the final
success/“Build submitted” messages to only run after the eas build check passes;
specifically add ERRORLEVEL checks following the npm install -g eas-cli
invocation, the existing call npm install block, the call eas login line, and
the call eas build line so the script fails fast on any error.
In `@voidlink/mobile/src/hooks/useServerDiscovery.ts`:
- Around line 19-25: The probe timeout (timer) is only cleared on the success
path; wrap the fetch call that uses the AbortController and timer in a
try/finally inside useServerDiscovery (referencing controller, timer,
DISCOVERY_TIMEOUT_MS and the fetch to `${url}/api/system/discovery`) and move
clearTimeout(timer) into the finally block so the timeout is always cleared even
when fetch rejects; do the same change for the second probe block (lines around
the other fetch at 38-41) to avoid leaving timers alive during batch scans.
- Around line 6-7: The hard-coded SCAN_HOSTS = 30 in useServerDiscovery.ts
limits discovery to .1-.30 and must be made configurable or adaptive; change
SCAN_HOSTS into a configurable parameter (e.g., accept an option or ENV var) or
compute the host range from the current subnet/mask (derive start/end from the
local IP and netmask to cover a full /24 or an adaptive window), and update any
scanning logic that references SCAN_HOSTS (and the related code around lines
~75-80) to use the new configurable/adaptive range so discovery won't miss hosts
with higher addresses.
---
Outside diff comments:
In `@voidlink/desktop/build_windows.bat`:
- Around line 1-59: The build_windows.bat file uses LF-only line endings which
can break Windows batch parsing; convert build_windows.bat to CRLF (Windows)
line endings, recommit the corrected file, and ensure future commits enforce
CRLF for .bat files (e.g., add a .gitattributes entry forcing eol=crlf for *.bat
or enable core.autocrlf) so the installer/build script runs reliably on Windows.
In `@voidlink/mobile/build_apk.bat`:
- Around line 1-44: The build_apk.bat script uses LF line endings which can
break cmd.exe parsing; re-save this batch file (build_apk.bat) with Windows CRLF
(\r\n) line endings so commands like "@echo off", "call npm install", "call eas
login" and "call eas build --platform android --profile preview
--non-interactive" run reliably on Windows; ensure your editor or Git
core.autocrlf is configured to commit the file with CRLF endings.
---
Nitpick comments:
In `@voidlink/desktop/tray_app.py`:
- Around line 268-273: stop_backend() currently only terminates subprocesses via
_server_process and ignores in-process uvicorn started by
_run_uvicorn_inprocess(); modify the module to store the uvicorn.Server instance
returned/created by _run_uvicorn_inprocess() in a global (e.g., _uvicorn_server)
and update stop_backend() to: if _uvicorn_server is set, set
_uvicorn_server.should_exit = True (and optionally await or join the server
shutdown if applicable), else fall back to terminating _server_process and
clearing globals (_uvicorn_server and _server_process) to ensure graceful
shutdown of in-process servers and proper cleanup of resources like DB
connections.
- Around line 172-198: The download_ollama function currently runs the
downloaded installer without verifying integrity; compute a SHA256 of
installer_path after urlretrieve and compare it against a trusted expected_hash
(obtained from a constant, config, or secure release API) before running
subprocess.run; on mismatch should log an error, unlink installer_path, and
return False; keep existing progress_callback and exception handling and only
run subprocess.run([str(installer_path), '/S']) when the hash check passes.
In `@voidlink/installer/voidlink_setup.iss`:
- Around line 47-48: The installer references custom wizard images via the
WizardImageFile and WizardSmallImageFile directives but the files
(wizard_banner.bmp, wizard_icon.bmp) may be missing; either add those bitmap
files to the installer resources (ensuring correct names and paths) or
remove/comment out the WizardImageFile and WizardSmallImageFile lines in
voidlink_setup.iss so Inno Setup falls back to the defaults; update any
installer build scripts or resource copying steps to include the images if you
choose to add them.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 306f9986-c247-467a-a7a3-67f98d8d5fb9
📒 Files selected for processing (11)
voidlink/desktop/build_windows.batvoidlink/desktop/icon_gen.pyvoidlink/desktop/requirements-desktop.txtvoidlink/desktop/tray_app.pyvoidlink/desktop/version_info.txtvoidlink/desktop/voidlink.specvoidlink/installer/voidlink_setup.issvoidlink/mobile/app/login.tsxvoidlink/mobile/build_apk.batvoidlink/mobile/package.jsonvoidlink/mobile/src/hooks/useServerDiscovery.ts
✅ Files skipped from review due to trivial changes (2)
- voidlink/desktop/requirements-desktop.txt
- voidlink/desktop/version_info.txt
🚧 Files skipped from review as they are similar to previous changes (1)
- voidlink/mobile/package.json
| def ensure_default_model(): | ||
| """Pull phi3 (smallest) if no models installed.""" | ||
| try: | ||
| req = urllib.request.urlopen('http://localhost:11434/api/tags', timeout=5) | ||
| data = json.loads(req.read()) | ||
| if not data.get('models'): | ||
| log.info("No models found. Pulling phi3 (smallest model)...") | ||
| subprocess.Popen( | ||
| ['ollama', 'pull', 'phi3'], | ||
| stdout=subprocess.DEVNULL, | ||
| stderr=subprocess.DEVNULL, | ||
| ) | ||
| except Exception: | ||
| pass | ||
|
|
There was a problem hiding this comment.
Use full Ollama path from find_ollama() instead of bare command.
Line 208 invokes 'ollama' without a full path, which will fail if the Ollama executable isn't in PATH. Since find_ollama() already locates the executable, pass the path as a parameter or use a module-level variable.
🔧 Proposed fix
-def ensure_default_model():
+def ensure_default_model(ollama_path: str = 'ollama'):
"""Pull phi3 (smallest) if no models installed."""
try:
req = urllib.request.urlopen('http://localhost:11434/api/tags', timeout=5)
data = json.loads(req.read())
if not data.get('models'):
log.info("No models found. Pulling phi3 (smallest model)...")
subprocess.Popen(
- ['ollama', 'pull', 'phi3'],
+ [ollama_path, 'pull', 'phi3'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except Exception:
passThen update line 546 to pass the path:
- threading.Thread(target=ensure_default_model, daemon=True).start()
+ threading.Thread(target=ensure_default_model, args=(ollama_path,), daemon=True).start()🧰 Tools
🪛 Ruff (0.15.12)
[error] 208-208: Starting a process with a partial executable path
(S607)
[error] 212-213: try-except-pass detected, consider logging the exception
(S110)
[warning] 212-212: 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 `@voidlink/desktop/tray_app.py` around lines 200 - 214, The subprocess call in
ensure_default_model uses the bare 'ollama' command which can fail if not in
PATH; call find_ollama() (or a module-level OLLAMA_PATH set from find_ollama())
to get the full executable path and use that in the subprocess.Popen argument
list instead of 'ollama', and guard for a None/empty result (log and skip pull
if not found) so ensure_default_model and any callers (e.g., where
ensure_default_model is invoked) use the resolved path.
| def start_backend_server(env_path: Path, port: int): | ||
| """Start the FastAPI backend in a thread.""" | ||
| global _server_process | ||
|
|
||
| if getattr(sys, 'frozen', False): | ||
| # Run embedded uvicorn directly in-process | ||
| _run_uvicorn_inprocess(env_path, port) | ||
| else: | ||
| # Dev mode: spawn python subprocess | ||
| python = sys.executable | ||
| main_path = BUNDLE_DIR / 'app' / 'main.py' | ||
| env = {**os.environ, 'DOTENV_PATH': str(env_path)} | ||
| _server_process = subprocess.Popen( | ||
| [python, '-m', 'uvicorn', 'app.main:app', | ||
| '--host', '0.0.0.0', '--port', str(port), '--no-access-log'], | ||
| cwd=str(BUNDLE_DIR), | ||
| env=env, | ||
| stdout=subprocess.DEVNULL, | ||
| stderr=subprocess.DEVNULL, | ||
| ) | ||
|
|
There was a problem hiding this comment.
Backend startup errors are silently discarded.
Lines 239-240 redirect both stdout and stderr to DEVNULL, which makes backend startup failures invisible. If the FastAPI app crashes on startup (e.g., due to import errors, config issues, or database problems), the tray app will wait for the port to open and eventually time out with no diagnostic information.
🔍 Proposed fix: Redirect stderr to log file
+_backend_log = DATA_DIR / 'backend.log'
+
def start_backend_server(env_path: Path, port: int):
"""Start the FastAPI backend in a thread."""
global _server_process
if getattr(sys, 'frozen', False):
# Run embedded uvicorn directly in-process
_run_uvicorn_inprocess(env_path, port)
else:
# Dev mode: spawn python subprocess
python = sys.executable
main_path = BUNDLE_DIR / 'app' / 'main.py'
env = {**os.environ, 'DOTENV_PATH': str(env_path)}
+
+ with open(_backend_log, 'a') as log_file:
- _server_process = subprocess.Popen(
- [python, '-m', 'uvicorn', 'app.main:app',
- '--host', '0.0.0.0', '--port', str(port), '--no-access-log'],
- cwd=str(BUNDLE_DIR),
- env=env,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- )
+ _server_process = subprocess.Popen(
+ [python, '-m', 'uvicorn', 'app.main:app',
+ '--host', '0.0.0.0', '--port', str(port), '--no-access-log'],
+ cwd=str(BUNDLE_DIR),
+ env=env,
+ stdout=subprocess.DEVNULL,
+ stderr=log_file,
+ )🧰 Tools
🪛 Ruff (0.15.12)
[error] 234-234: subprocess call: check for execution of untrusted input
(S603)
[error] 236-236: Possible binding to all interfaces
(S104)
🤖 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 `@voidlink/desktop/tray_app.py` around lines 222 - 242, The subprocess call in
start_backend_server currently discards stdout/stderr (hiding startup errors);
change it so stderr is captured to a persistent log file (e.g., open a file like
BUNDLE_DIR / 'backend.log' and pass that file handle as stderr to
subprocess.Popen) while keeping stdout suppressed, and ensure the file handle is
opened before calling Popen and closed/kept alive for the lifetime of
_server_process; reference start_backend_server, _server_process, BUNDLE_DIR,
env_path and port when making the change so backend startup exceptions are
preserved for diagnosis.
| DefaultDirName={autopf}\VoidLink | ||
| DefaultGroupName=VoidLink | ||
| AllowNoIcons=yes | ||
| LicenseFile=license.txt |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for files referenced by the Inno Setup script
cd voidlink/installer
echo "Checking for required files..."
for file in license.txt wizard_banner.bmp wizard_icon.bmp; do
if [ -f "$file" ]; then
echo "✓ Found: $file"
else
echo "✗ Missing: $file"
fi
done
echo ""
echo "Note: license.txt is required, wizard images are optional"Repository: CCguvycu/cy
Length of output: 224
Add the missing license.txt file to the installer directory.
Line 32 references license.txt which is required for Inno Setup compilation, but this file is missing from voidlink/installer/. Create or add the file to resolve the build failure.
🤖 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 `@voidlink/installer/voidlink_setup.iss` at line 32, Add the missing
license.txt referenced by the installer configuration: create a plain-text file
named license.txt in the installer directory (to satisfy the
LicenseFile=license.txt entry) containing the software license or placeholder
legal text, then commit it so the Inno Setup script can compile successfully.
| const runDiscovery = async () => { | ||
| const found = await discover(); | ||
| if (found.length === 1) { | ||
| // Exactly one server found — pre-select it | ||
| setSelectedServer(found[0].url); | ||
| setServerUrlState(found[0].url); | ||
| } | ||
| }; | ||
|
|
||
| const handleConnect = async (urlOverride?: string) => { | ||
| const url = (urlOverride || selectedServer || serverUrl).trim(); | ||
| if (!url) { | ||
| showMessage({ message: 'Enter a server URL', type: 'warning' }); | ||
| return; | ||
| } | ||
| if (!password.trim()) { | ||
| showMessage({ message: 'Enter your password', type: 'warning' }); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| await setServerUrl(url); | ||
| await login(username.trim(), password); | ||
| router.replace('/(tabs)'); |
There was a problem hiding this comment.
Reset stale endpoint state between discovery/manual flows.
runDiscovery() leaves selectedServer/serverUrl untouched unless exactly one server is found, and handleConnect() in scan mode still falls back to the hidden serverUrl. After a manual attempt or a previous scan, CONNECT can submit an old URL that is no longer the visible selection. Clear the scan selection before each discovery pass, and keep scan-mode submits tied to the currently selected discovered server only.
One way to tighten the state flow
const runDiscovery = async () => {
+ setSelectedServer('');
+ setServerUrlState('');
const found = await discover();
if (found.length === 1) {
// Exactly one server found — pre-select it
setSelectedServer(found[0].url);
setServerUrlState(found[0].url);
@@
const handleConnect = async (urlOverride?: string) => {
- const url = (urlOverride || selectedServer || serverUrl).trim();
+ const url = (urlOverride ?? selectedServer).trim();
if (!url) {
showMessage({ message: 'Enter a server URL', type: 'warning' });
return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const runDiscovery = async () => { | |
| const found = await discover(); | |
| if (found.length === 1) { | |
| // Exactly one server found — pre-select it | |
| setSelectedServer(found[0].url); | |
| setServerUrlState(found[0].url); | |
| } | |
| }; | |
| const handleConnect = async (urlOverride?: string) => { | |
| const url = (urlOverride || selectedServer || serverUrl).trim(); | |
| if (!url) { | |
| showMessage({ message: 'Enter a server URL', type: 'warning' }); | |
| return; | |
| } | |
| if (!password.trim()) { | |
| showMessage({ message: 'Enter your password', type: 'warning' }); | |
| return; | |
| } | |
| try { | |
| await setServerUrl(url); | |
| await login(username.trim(), password); | |
| router.replace('/(tabs)'); | |
| const runDiscovery = async () => { | |
| setSelectedServer(''); | |
| setServerUrlState(''); | |
| const found = await discover(); | |
| if (found.length === 1) { | |
| // Exactly one server found — pre-select it | |
| setSelectedServer(found[0].url); | |
| setServerUrlState(found[0].url); | |
| } | |
| }; | |
| const handleConnect = async (urlOverride?: string) => { | |
| const url = (urlOverride ?? selectedServer).trim(); | |
| if (!url) { | |
| showMessage({ message: 'Enter a server URL', type: 'warning' }); | |
| return; | |
| } | |
| if (!password.trim()) { | |
| showMessage({ message: 'Enter your password', type: 'warning' }); | |
| return; | |
| } | |
| try { | |
| await setServerUrl(url); | |
| await login(username.trim(), password); | |
| router.replace('/(tabs)'); |
🤖 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 `@voidlink/mobile/app/login.tsx` around lines 42 - 65, runDiscovery can leave
stale selection behind and handleConnect currently falls back to the hidden
serverUrl, allowing old endpoints to be submitted; at the start of runDiscovery
clear the discovery-driven state by calling setSelectedServer('') and
setServerUrlState('') (or equivalent) before calling discover(), and change
handleConnect so that when in scan/discovery mode it uses only selectedServer
(no fallback to serverUrl) — if selectedServer is empty show the "Enter a server
URL" warning instead of submitting serverUrl; update references to runDiscovery,
handleConnect, selectedServer, serverUrl, setSelectedServer, setServerUrlState
and setServerUrl accordingly.
| npm install -g eas-cli | ||
| ) | ||
|
|
||
| echo [1/3] Installing dependencies... | ||
| call npm install | ||
| IF ERRORLEVEL 1 (echo [ERROR] npm install failed & pause & exit /b 1) | ||
|
|
||
| echo. | ||
| echo [2/3] Logging into Expo... | ||
| echo (Create a free account at expo.dev if you don't have one) | ||
| echo. | ||
| call eas login | ||
|
|
||
| echo. | ||
| echo [3/3] Building APK... | ||
| echo This will take 5-10 minutes in the cloud. | ||
| echo You'll get a download link when done. | ||
| echo. | ||
| call eas build --platform android --profile preview --non-interactive | ||
|
|
||
| echo. | ||
| echo ✓ Build submitted! Check your email or expo.dev for the download link. | ||
| echo Install the APK on your phone, then connect to your VoidLink server. |
There was a problem hiding this comment.
Fail fast on EAS install/login/build failures before showing success
npm install -g eas-cli (Line 19), eas login (Line 30), and eas build (Line 37) are not guarded. If any fails, Line 40 still prints a success message, which is misleading.
Suggested patch
eas --version >nul 2>&1
IF ERRORLEVEL 1 (
echo Installing EAS CLI...
- npm install -g eas-cli
+ call npm install -g eas-cli
+ IF ERRORLEVEL 1 (
+ echo [ERROR] Failed to install EAS CLI.
+ pause & exit /b 1
+ )
)
@@
call eas login
+IF ERRORLEVEL 1 (
+ echo [ERROR] Expo login failed.
+ pause & exit /b 1
+)
@@
call eas build --platform android --profile preview --non-interactive
+IF ERRORLEVEL 1 (
+ echo [ERROR] Build submission failed.
+ pause & exit /b 1
+)
echo.
echo ✓ Build submitted! Check your email or expo.dev for the download link.🧰 Tools
🪛 Blinter (1.0.112)
[error] 30-30: Missing colon in CALL statement. Explanation: CALL statements to labels require a colon (unlike GOTO where colon is optional). Recommendation: Use CALL :label_name with colon when calling internal subroutines. Context: CALL to label 'eas' should use colon: CALL :eas
(E014)
🤖 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 `@voidlink/mobile/build_apk.bat` around lines 19 - 41, The script prints a
success message regardless of failures; after each external command that can
fail (the global installer npm install -g eas-cli, call npm install, call eas
login, and call eas build --platform android --profile preview
--non-interactive) check ERRORLEVEL immediately and exit with a non-zero code
and an error message if it failed, and move the final success/“Build submitted”
messages to only run after the eas build check passes; specifically add
ERRORLEVEL checks following the npm install -g eas-cli invocation, the existing
call npm install block, the call eas login line, and the call eas build line so
the script fails fast on any error.
| // Scan these common LAN subnets | ||
| const SCAN_HOSTS = 30; // scan .1 to .30 per subnet |
There was a problem hiding this comment.
Expand or make the scan window adaptive.
Scanning only .1 through .30 will miss a lot of normal LAN deployments where DHCP assigns clients higher addresses like .100+, so auto-discovery silently fails on otherwise healthy networks. That undermines the zero-config flow this PR is adding. At minimum, make the host range configurable or derive a wider/adaptive window from the current subnet instead of hard-coding the first 30 hosts.
Also applies to: 75-80
🤖 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 `@voidlink/mobile/src/hooks/useServerDiscovery.ts` around lines 6 - 7, The
hard-coded SCAN_HOSTS = 30 in useServerDiscovery.ts limits discovery to .1-.30
and must be made configurable or adaptive; change SCAN_HOSTS into a configurable
parameter (e.g., accept an option or ENV var) or compute the host range from the
current subnet/mask (derive start/end from the local IP and netmask to cover a
full /24 or an adaptive window), and update any scanning logic that references
SCAN_HOSTS (and the related code around lines ~75-80) to use the new
configurable/adaptive range so discovery won't miss hosts with higher addresses.
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS); | ||
|
|
||
| const res = await fetch(`${url}/api/system/discovery`, { | ||
| signal: controller.signal, | ||
| }); | ||
| clearTimeout(timer); |
There was a problem hiding this comment.
Always clear the probe timeout.
clearTimeout(timer) only runs on the success path. When fetch rejects early, each probe leaves its timer alive until it fires, which is avoidable churn during batch scans. Move the cleanup into a finally block.
Suggested fix
async function probeHost(ip: string, port: number): Promise<DiscoveredServer | null> {
const url = `http://${ip}:${port}`;
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS);
try {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS);
-
const res = await fetch(`${url}/api/system/discovery`, {
signal: controller.signal,
});
- clearTimeout(timer);
if (res.ok) {
const data = await res.json();
if (data.service === 'VoidLink') {
return {
@@
} catch {
// Host not reachable
+ } finally {
+ clearTimeout(timer);
}
return null;
}Also applies to: 38-41
🤖 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 `@voidlink/mobile/src/hooks/useServerDiscovery.ts` around lines 19 - 25, The
probe timeout (timer) is only cleared on the success path; wrap the fetch call
that uses the AbortController and timer in a try/finally inside
useServerDiscovery (referencing controller, timer, DISCOVERY_TIMEOUT_MS and the
fetch to `${url}/api/system/discovery`) and move clearTimeout(timer) into the
finally block so the timeout is always cleared even when fetch rejects; do the
same change for the second probe block (lines around the other fetch at 38-41)
to avoid leaving timers alive during batch scans.
…ackage.json - Generate icon.png, splash.png, adaptive-icon.png, favicon.png, notification-icon.png - Remove plugins that caused prebuild failures (expo-camera, expo-barcode-scanner, expo-notifications complex config) - keeping only essential ones - Remove unused packages (lottie, mmkv, syntax-highlighter, vector-icons) that conflicted during Android prebuild - Simplify extra.eas config https://claude.ai/code/session_01JRMpBUWAnqmPVV1xxmbA3e
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@voidlink/mobile/app.json`:
- Around line 61-63: Replace the non-UUID EAS projectId value under the "eas"
object (the extra.eas.projectId entry) with the actual EAS project UUID (v4
format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx); obtain it by running `eas init` or
from your EAS project dashboard and update the projectId string to that UUID so
EAS linkage validation will succeed.
In `@voidlink/mobile/package.json`:
- Around line 10-11: The package.json scripts "build:apk" and "lint" reference
external CLIs but those packages are missing from devDependencies; update
package.json's devDependencies to add "eas-cli" (or "eas" package if you prefer
the CLI package name used in your environment) and "eslint" with appropriate
version ranges so running npm/yarn install provides those CLIs; ensure you add
them under the devDependencies object (the same section that currently exists)
so the "build:apk" and "lint" scripts work in a clean environment.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9778d4c1-4301-4f53-af4e-4ffe74d5b6d7
⛔ Files ignored due to path filters (5)
voidlink/mobile/assets/adaptive-icon.pngis excluded by!**/*.pngvoidlink/mobile/assets/favicon.pngis excluded by!**/*.pngvoidlink/mobile/assets/icon.pngis excluded by!**/*.pngvoidlink/mobile/assets/notification-icon.pngis excluded by!**/*.pngvoidlink/mobile/assets/splash.pngis excluded by!**/*.png
📒 Files selected for processing (2)
voidlink/mobile/app.jsonvoidlink/mobile/package.json
| "eas": { | ||
| "projectId": "voidlink-app" | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="voidlink/mobile/app.json"
PROJECT_ID="$(jq -r '.expo.extra.eas.projectId // empty' "$FILE")"
echo "projectId: $PROJECT_ID"
if [[ "$PROJECT_ID" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ ]]; then
echo "UUID format: OK"
else
echo "UUID format: INVALID"
exit 1
fiRepository: CCguvycu/cy
Length of output: 100
🌐 Web query:
EAS projectId format requirements Expo documentation
💡 Result:
The EAS projectId is a unique identifier for each Expo Application Services (EAS) project, formatted as a UUID (Universally Unique Identifier), specifically a version 4 UUID in the standard hyphenated format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, where x is a hexadecimal digit [1][2][3]. Examples include "0cd3da2d-xxx-xxx-xxx-xxxxxxxxxx" [2], "23847707-bf15-424c-b1dc-7a57fc34a23d" [3], and "db096609-2c67-431d-b83d-88cabf8511b1" [3]. It is automatically generated and assigned when you run eas init or similar commands like eas build:configure, which links your local project to the EAS project on Expo's servers [1][2][4]. The projectId is stored in your app config (app.json or app.config.js) under the key extra.eas.projectId [1][2][5][6]. Official Expo documentation does not explicitly define a regex or strict format beyond it being a unique ID, but validation occurs during EAS CLI commands and builds to ensure it matches the server-side project ID, owner, and slug [7][8][9]. Mismatches (e.g., due to dynamic config based on env vars) cause errors like "Project id defined in app.json does not match current projectId" [6][3][7]. No specific length or character restrictions are documented beyond standard UUIDv4 format (36 characters including hyphens). It remains constant even if the project is transferred between accounts [1]. For multiple environments (dev/prod), use separate projects with distinct projectIds [3][10].
Citations:
- 1: https://github.com/expo/fyi/blob/main/eas-project-id.md
- 2: https://docs.expo.dev/tutorial/eas/configure-development-build/
- 3: https://github.com/expo/fyi/blob/main/eas-config-mismatch.md
- 4: [eas-cli] add 'eas init' command expo/eas-cli#402
- 5: https://docs.expo.dev/eas-update/getting-started/
- 6: EAS project not configured. expo/eas-cli#1426
- 7: [build-tools] add project id validation expo/eas-build#228
- 8: https://github.com/expo/eas-cli/blob/v16.3.3/packages/eas-cli/src/commands/project/init.ts
- 9: [21/21][eas-cli] Validate that owner and projectId and slug all are in alignment expo/eas-cli#1405
- 10: https://stackoverflow.com/questions/74060001/is-there-a-better-way-to-configure-eas-builds-so-they-are-easier-to-differentiat
extra.eas.projectId must be a UUID for EAS linkage
The projectId "voidlink-app" is not in the required UUID v4 format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). EAS validates this during build/update operations and will fail if mismatched. Generate a valid projectId by running eas init or retrieving the UUID from your EAS project dashboard, then update the config accordingly.
🔧 Proposed fix
"extra": {
"eas": {
- "projectId": "voidlink-app"
+ "projectId": "YOUR-VALID-UUID-FROM-EAS"
}
}🤖 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 `@voidlink/mobile/app.json` around lines 61 - 63, Replace the non-UUID EAS
projectId value under the "eas" object (the extra.eas.projectId entry) with the
actual EAS project UUID (v4 format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx); obtain
it by running `eas init` or from your EAS project dashboard and update the
projectId string to that UUID so EAS linkage validation will succeed.
| "build:apk": "eas build --platform android --profile preview", | ||
| "lint": "eslint src --ext .ts,.tsx" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Scripts that invoke eas/eslint =="
rg -n '"[^"]+"\s*:\s*".*\b(eas|eslint)\b.*"' --glob 'package.json'
echo
echo "== Manifests declaring eslint/eas-cli =="
rg -n '"(eslint|eas-cli)"\s*:' --glob 'package.json' || true
echo
echo "== Workspace context from root package.json (if present) =="
if [ -f package.json ]; then
rg -n '"workspaces"|"packageManager"' package.json -C2 || true
fiRepository: CCguvycu/cy
Length of output: 366
🏁 Script executed:
cat -n voidlink/mobile/package.json | head -60Repository: CCguvycu/cy
Length of output: 1949
Scripts rely on undeclared CLIs (eas, eslint)
Lines 10–11 invoke eas and eslint, but the devDependencies block (lines 44–49) does not declare eas-cli or eslint. In a clean environment, these scripts will fail. Add eslint and eas-cli to devDependencies.
🤖 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 `@voidlink/mobile/package.json` around lines 10 - 11, The package.json scripts
"build:apk" and "lint" reference external CLIs but those packages are missing
from devDependencies; update package.json's devDependencies to add "eas-cli" (or
"eas" package if you prefer the CLI package name used in your environment) and
"eslint" with appropriate version ranges so running npm/yarn install provides
those CLIs; ensure you add them under the devDependencies object (the same
section that currently exists) so the "build:apk" and "lint" scripts work in a
clean environment.
Summary
This PR introduces VoidLink, a complete remote AI interface system for controlling locally-hosted Ollama models from mobile devices. The project includes a FastAPI backend, React Native Expo mobile app, Docker deployment configs, and comprehensive documentation.
Key Changes
Backend (Python/FastAPI)
Mobile App (React Native/Expo)
Deployment & Documentation
Notable Implementation Details
https://claude.ai/code/session_01JRMpBUWAnqmPVV1xxmbA3e
Summary by CodeRabbit
New Features
Documentation
Chores