[Android SDK] [Kotlin SDK] Add LoRA Adapter Support#412
[Android SDK] [Kotlin SDK] Add LoRA Adapter Support#412Siddhesh2377 wants to merge 1668 commits intomainfrom
Conversation
…ult setup note - Switch c-cpp from autobuild to manual (no build system at repo root) - Add Build C/C++ step: cmake in sdk/runanywhere-commons with minimal opts - Comment: disable Default setup in repo settings when using this workflow
Skip LLM model check and loading when --moltbot is enabled, allowing the voice assistant to run with only VAD, STT, TTS, and wake word models. The LLM processing happens remotely via the OpenClaw agent instead. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This adds a new voice assistant component that acts as a channel for OpenClaw: - Wake Word Detection (Hey Jarvis) using openWakeWord - Voice Activity Detection (VAD) using Silero - Speech-to-Text (ASR) using Whisper Tiny - Text-to-Speech (TTS) using Piper Key fixes to runanywhere-commons wakeword_onnx.cpp: 1. Added 480-sample audio context overlap for proper melspectrogram computation at frame boundaries (matching Python implementation) 2. Pre-initialize melspec buffer with 76 frames of padding to enable immediate detection (matching Python's np.ones((76, 32))) This fixes wake word detection which was returning near-zero confidence (0.000008) and now correctly detects with high confidence (0.98+). Test suite includes: - Detection tests for various "Hey Jarvis" variations - Rejection tests for noise, silence, similar words, and commands - Full pipeline tests Project structure: - tests/: Test code, audio files, and test scripts - scripts/: Build and model download scripts Co-authored-by: Cursor <cursoragent@cursor.com>
Changes:
- Add ModelRequiredOverlay for onboarding experience - Add VoiceAssistantParticleView for visual effects - Add ModelLogoHelper utility and logo assets - Refactor all screens (Chat, STT, TTS, Voice, Settings) with UI improvements - Configure production API credentials - Bump version to 0.1.5 +1,897 insertions, -1,436 deletions
…validation - Add first-time Raspberry Pi setup steps (build commons, download models, run) - Add log viewing section for both hybrid assistant and OpenClaw gateway - Add HTML detection in download-models.sh to catch corrupt Git LFS downloads - Update wake word model sizes to match actual GitHub release files - Add Git LFS warning section for wake word ONNX models - Update test audio WAV files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The hybrid assistant now connects directly to OpenClaw gateway via WebSocket on port 8082 instead of HTTP POST to the old voice bridge on port 8081. This eliminates the Node.js voice bridge relay entirely. - Implement minimal WebSocket client (handshake, masked text frames, ping/pong, background receive loop) using raw sockets - Send OpenClaw connect message with device capabilities on startup - Send transcriptions as WebSocket text frames - Receive speak messages via background receive loop into queue - Update default URL from http://localhost:8081 to ws://localhost:8082 - Remove unused OpenClawHttpClient class Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds openclaw-voice.service that starts the hybrid assistant alongside
the OpenClaw gateway. Uses BindsTo= so it starts/stops with the gateway.
Install: cp scripts/openclaw-voice.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable openclaw-voice.service
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Kokoro TTS English v0.19 support (24kHz, 11 speakers) - Add TTSModelType::KOKORO to onnx_backend for Kokoro model detection - Auto-detect Kokoro models via voices.bin presence - Support both model.onnx and model.int8.onnx filenames - Add text sanitization for TTS to remove special characters: - Removes markdown formatting (*, _, `, #, ~) - Removes emojis and Unicode symbols - Removes brackets, braces, and other awkward characters - Keeps natural punctuation for proper speech pauses - Update download script with Kokoro English model (~330MB) - Keep Piper TTS as alternative option (--piper flag) Co-authored-by: Cursor <cursoragent@cursor.com>
- Add diffusion model type support in C++ model management - Add KokoroTTSProvider and TTSRouter for Kotlin TTS - Add QNN SDK download script for Android - Update Swift bridge for diffusion model types Co-authored-by: Cursor <cursoragent@cursor.com>
Optimised llamacpp_backend.cpp
- Skip audio processing while TTS is playing to prevent feedback loops - Split TTS output into sentences for faster streaming playback - Add 500ms cooldown after TTS before re-enabling listening - Add linux-voice-assistant package.json for Node.js voice client - Add node_modules to gitignore Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…onse
Adds audio/TTS feedback while the user waits for OpenClaw to process their request:
- Immediate acknowledgment: gentle chime + random phrase ("Let me think about that")
- Periodic waiting tones: subtle sounds every ~3 seconds
- Waiting phrases: spoken updates for longer waits ("Still working on that")
- Automatic interruption when response arrives
This creates a warm, professional experience where the user knows the assistant
is actively processing their request.
Co-authored-by: Cursor <cursoragent@cursor.com>
…law response" This reverts commit f93213d.
- Improved text preparation for natural-sounding speech synthesis by: - Preserving natural punctuation for proper prosody - Removing markdown formatting, emojis, and unicode symbols - Converting symbols to spoken equivalents (e.g., "&" to "and") - Normalizing whitespace for cleaner output - Added helper functions for UTF-8 sequence handling and symbol conversion This update ensures a more seamless and natural TTS experience for users.
Autonomous Android agent that navigates phone UI to accomplish user goals. Uses accessibility service for screen reading/actions, GPT-4o Vision for visual understanding, unified tool calling for all UI actions, and on-device LLM fallback via RunAnywhere SDK. Includes voice mode with Whisper STT and TTS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Force-add files excluded by root .gitignore but required to build the android-use-agent project. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Block Bixby packages from app search results
- Add Samsung-specific package fallbacks
- Fix X/Twitter launch with explicit ComponentName intent
- Update prompts to use current app names ("X" not "Twitter")
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds VLM component types, bridge layer, and Android UI screens for vision-language model inference. Updates .gitignore to exclude IDE and project-specific files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pport Add x86_64 ABI support for Android emulator development
- Bump swift-asn1 dependency version to 1.5.1 - Refactor ChatScreen to improve input handling and logging - Introduce ModelRequiredOverlay for model selection prompts - Add VoiceAssistantParticleView for enhanced visual effects - Implement utility functions for model logo retrieval - Add new drawable resources for model logos +1,200 insertions, -800 deletions
Web SDK : Small bug fix + n/w layer for development
#394) * feat(web): implement streaming model import via WORKERFS mounting (Issue #372) # Conflicts: # sdk/runanywhere-web/packages/core/src/Infrastructure/ModelDownloader.ts # sdk/runanywhere-web/packages/core/src/Infrastructure/ModelLoaderTypes.ts # sdk/runanywhere-web/packages/core/src/Infrastructure/ModelManager.ts # sdk/runanywhere-web/packages/core/src/Infrastructure/OPFSStorage.ts # sdk/runanywhere-web/packages/llamacpp/src/Extensions/RunAnywhere+TextGeneration.ts * fix(web): address PR #394 feedback (OOM fallback, local file streaming) # Conflicts: # sdk/runanywhere-web/packages/core/src/Infrastructure/LocalFileStorage.ts # sdk/runanywhere-web/packages/core/src/Infrastructure/ModelDownloader.ts # sdk/runanywhere-web/packages/llamacpp/src/Extensions/RunAnywhere+TextGeneration.ts * fix(web): add index signatures * fix(web): address PR #394 bot review comments
…current initialization (#402)
…String (#404) * fix(ios-sdk): propagate STTOptions.language to C bridge via withCString STTOptions.language was silently ignored during transcription, causing the Whisper ONNX decoder to always default to English regardless of the language passed by the caller. Root causes: - Dangling pointer: (options.language as NSString).utf8String stored a pointer into a temporary NSString. ARC released it after the assignment, leaving cOptions.language pointing to freed memory before the C call. The backend read garbage and fell back to 'en'. - Incomplete mapping: only language and sample_rate were forwarded to rac_stt_options_t. detect_language, enable_punctuation, enable_diarization, enable_timestamps, audio_format, and max_speakers were silently zero-initialized regardless of caller input. - processStreamingAudio never set a language field at all. Fix: add a private STTOptions.withCOptions<T> helper that uses withCString to guarantee pointer validity for the full duration of the C call and maps all fields in one place. Apply it to transcribeWithOptions, transcribeStream, and processStreamingAudio. Fixes #350 * refactor(ios-sdk): remove duplicate withCOptions, use existing STTTypes.swift impl The private withCOptions extension in RunAnywhere+STT.swift duplicated the public STTOptions.withCOptions already defined in STTTypes.swift:127. Both used withCString for pointer safety and mapped all rac_stt_options_t fields identically. Remove the duplicate and update the three call sites to use the public method's UnsafePointer signature (cOptionsPtr instead of &cOptions). * fix(ios-sdk): address review comments on processStreamingAudio and STTStreamingContext - Change processStreamingAudio signature from language: String to options: STTOptions = STTOptions() so detectLanguage and all other fields are preserved, matching transcribeWithOptions/transcribeStream - Add missing isLoaded guard to processStreamingAudio before getHandle(), surfacing an actionable notInitialized error instead of processingFailed - Add clarifying comment on sttResult in processStreamingAudio: value is intentionally unused as the C layer delivers results via CppEventBridge - Fix STTStreamingContext data race: replace @unchecked Sendable + bare var with OSAllocatedUnfairLock-protected finalText so C callback writes and async continuation reads are properly synchronised * fix(ios-sdk): address final review comments on STTStreamingContext and guard order - Mark onPartialResult as @sendable so STTStreamingContext's explicit Sendable conformance is sound; a non-@sendable stored closure makes the conformance unsound under Swift's strict concurrency checking - Swap guard order in processStreamingAudio to match transcribeWithOptions and transcribeStream: getHandle() first, then isLoaded check
Co-authored-by: sakirr <sakirahmed75531@gmail.com>
Mac changes (Changes needed to enable MacOS for distribution)
* feat(lora): add LoRA adapter support across SDK + demo app
Implement LoRA (Low-Rank Adaptation) adapter hot-swapping for llama.cpp
backend across all 6 SDK layers (C++ -> C API -> Component -> JNI ->
Kotlin Bridge -> Kotlin Public API).
- Add load/remove/clear/query LoRA adapter operations
- Use vtable dispatch in component layer to decouple librac_commons
from librac_backend_llamacpp (fixes linker errors)
- Add LoRA vtable entries to rac_llm_service_ops_t
- Fix AttachCurrentThread cast for Android NDK C++ JNI build
- Add RunAnyWhereLora Android demo app with Material 3 Q&A UI
- Add comprehensive implementation docs with C/C++ API reference
* feat(ci): add selectable build targets to Build All workflow + fix Swift concurrency errors
Rewrite build-all-test.yml with 9 boolean checkbox inputs so each build
target can be toggled independently from the GitHub Actions UI:
- C++ Android Backends (arm64-v8a, armeabi-v7a, x86_64 matrix)
- C++ iOS Backends (XCFramework)
- Kotlin SDK (JVM + Android)
- Swift SDK (iOS/macOS)
- Web SDK (TypeScript)
- Flutter SDK (Dart analyze via Melos)
- React Native SDK (TypeScript via Lerna)
- Android Example Apps (RunAnywhereAI + RunAnyWhereLora)
- IntelliJ Plugin
Fix two Swift strict-concurrency errors that fail the Swift SDK build:
- LiveTranscriptionSession: add @unchecked Sendable (safe because class
is @mainactor, all access serialized)
- RunAnywhere+VisionLanguage: add Sendable conformance to rac_vlm_image_t
so the C struct can cross the Task boundary in the streaming builder;
simplify StreamingCollector to start timing at init
* fix(swift): resolve strict concurrency errors in LiveTranscriptionSession and VLM streaming
LiveTranscriptionSession.swift:
- Replace [weak self] captures with strong `let session = self` before
closures to avoid captured var in @Sendable/@task contexts (class is
@mainactor @unchecked Sendable so strong ref is safe, bounded by
stream lifecycle)
- Wrap deprecated startStreamingTranscription call in @available helper
to silence deprecation warning until migration to transcribeStream API
RunAnywhere+VisionLanguage.swift:
- Add `let capturedCImage = cImage` before AsyncThrowingStream closure
so the Task captures an immutable let instead of a mutable var
- Add `extension rac_vlm_image_t: @unchecked Sendable {}` for the C
struct to cross Task concurrency boundaries safely
- Simplify StreamingCollector to initialize startTime at init instead
of requiring a separate async start() call
* fix(jni): address CodeRabbit review findings in LoRA JNI functions
- Replace raw -1 returns with RAC_ERROR_INVALID_HANDLE/RAC_ERROR_INVALID_ARGUMENT
to match codebase error handling conventions
- Use getCString() helper instead of raw GetStringUTFChars/ReleaseStringUTFChars
- Add missing result logging to racLlmComponentRemoveLora and racLlmComponentClearLora
- Use rac_free() instead of free() in racLlmComponentGetLoraInfo for consistency
- Clarify LoRA adapter memory ownership comments (adapters freed automatically
with model per llama.cpp b8011 API — llama_adapter_lora_free is deprecated)
* ios initial changes * minimal sample needed to test lora * updating docs * addressed the comments
* fix(ios-sdk): replace full filesystem paths with lastPathComponent in logs to prevent PII leakage * fix(ios-sdk): redact path in error message and migrate to URL(filePath:) Swift 6 API * fix(ios-sdk): consolidate URL(filePath:) into single modelURL constant in DiffusionPlatformService
* VLM in Flutter SDK Have made changes to the flutter sdk for supporting vlm, following the swift sdk. Once the example app also has the corresponding changes, ill be able to fully test out this thing. Have test built it. * Flutter example app * React-Native Sdk VLM support * React Native EXample app Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Pod and infoplist for camera Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: Implement RAG (Retrieval-Augmented Generation) with ONNX and LlamaCPP backends Implement complete on-device RAG with document management, semantic search, and contextual answer generation. Core Implementation (runanywhere-commons): - Implement RAG pipeline with document chunking and vector retrieval - Implement ONNX embedding provider (all-MiniLM-L6-v2) - Implement LlamaCPP and ONNX text generators - Implement USearch-based vector store for fast similarity search - Implement structured logging with metadata support - Update CMake build system for RAG backend integration React Native Integration: - Create @runanywhere/rag package with TypeScript API - Add NitroModules global initialization for proper JSI binding - Generate Nitro bindings for Android and iOS platforms - Improve model path resolution for ONNX single-file models - Implement RAG configuration interface with tunable parameters - Update build scripts for RAG module compilation Example Application: - Implement RAGScreen demo with interactive document Q&A - Implement model selection UI for embedding and LLM models - Implement document management (add, clear, batch operations) - Display retrieval sources with similarity scores - Show timing metrics (retrieval, generation, total) - Implement model catalog entries for embedding models - Update navigation to include RAG tab This enables privacy-preserving RAG with all processing happening on-device, supporting both ONNX and LlamaCPP backends. * fix: remove postinstall-postinstall to resolve workspace conflicts - Remove postinstall-postinstall dependency from example app - Update example app package-lock.json with refreshed dependencies * style: Clean code * feat: Add Vision tab alongside RAG (7 tabs total) - Added Vision imports (VisionHubScreen, VLMScreen) to TabNavigator - Extended tab icons with Vision eye icon - Extended tab labels with Vision label - Added VisionStackScreen component for Vision hub → VLM navigation - Updated RootTabParamList type to include Vision tab Navigation now includes: 1. Chat (default) 2. STT (speech-to-text) 3. TTS (text-to-speech) 4. Voice (voice chat) 5. RAG (retrieval-augmented generation) 6. Vision (image understanding) 7. Settings Both RAG and Vision features are now accessible from bottom tab navigation. * fix: Resolve App.tsx merge conflict - combine optional LlamaCPP with all models * fix: Stub ToolCallingBridge functions pending commons library updates - Copied rac_tool_calling.h header to core package includes - Stubbed out ToolCallingBridge function calls in HybridRunAnywhereCore - Tool calling functions (rac_tool_call_*) not yet available in commons v0.1.4 - Excludes ToolCallingBridge.cpp from Android build until commons is updated - RAG functionality unaffected - tool calling is separate feature - Allows Android build to complete successfully with 7-tab navigation * fix: add missing @react-navigation/native-stack dependency for tab navigation * style: Clean code * style: Clean code * fix: Remove unused imports in RunAnywhere example Clean up unused imports in the React Native RunAnywhere example: remove LLMFramework from the @runanywhere/core import in App.tsx, and drop IconSize and ModelRequiredOverlay imports from RAGScreen.tsx (keeping Spacing, Padding, BorderRadius). This removes unused symbols and addresses related import warnings. * style: Clean code Delete the backup file sdk/runanywhere-commons/src/backends/rag/llamacpp_generator.cpp.bak which contained an older LlamaCPP generator implementation. The file was redundant and is removed to clean up the repository and avoid confusion with the active implementation. * style: Clean code * refactor(rag): add proper error handling for ONNX Runtime C API calls Fixes critical bug where ORT API return statuses were silently ignored in onnx_embedding_provider.cpp and onnx_generator.cpp, which could cause undefined behavior and resource leaks on allocation failures. Solution: - Create shared ort_guards.h with RAII wrappers (OrtStatusGuard, OrtValueGuard, OrtMemoryInfoGuard) - Refactor onnx_embedding_provider.cpp to use RAII guards - Refactor onnx_generator.cpp to use RAII guards - Remove duplicate RAII class definitions Benefits: - All ORT API errors now properly checked - Automatic resource cleanup (exception-safe) - Eliminates code duplication - Zero-cost abstraction with compiler optimizations * refactor(rag): fix critical memory safety and data integrity bugs in RAG backend This commit addresses four critical bugs in the RAG C++ implementation: 1. ORT status guard memory leak (ort_guards.h, onnx_generator.cpp) - Refactored get_address() to be a pure accessor - Changed to explicit reset() pattern to prevent memory leaks - Fixed dangling pointer issues in onnx_generator.cpp from vector reallocation 2. Hash collision vulnerability (vector_store_usearch.cpp) - Replaced std::hash<std::string> with monotonic counter (next_key_) - Ensures collision-free key generation for vector store entries - Added duplicate detection in add_chunks_batch() 3. Metadata persistence bug (vector_store_usearch.cpp) - Implemented JSON serialization for save/load operations - Now persists chunks_, id_to_key_, and next_key_ alongside USearch index - Added proper deserialization with state reconstruction These fixes ensure memory safety, prevent silent data loss, and enable proper state persistence across save/load cycles. * refactor(rag): enhance ONNX embedding provider with error handling - Implement OrtSessionOptionsGuard class for automatic session options cleanup - Fix GetTensorMutableData null-pointer dereference with proper status checking - Implement error handling for session options creation/configuration - Replace manual cleanup with RAII pattern (eliminates 3 ReleaseSessionOptions calls) - Rename status_guard to output_status_guard to fix variable shadowing This ensures robust error handling, automatic resource cleanup, and proper C++ exception safety in the ONNX embedding provider used by the RAG backend. All fixes verified with successful Android APK build (arm64-v8a). * feat(rag): Enhance error handling and debugging for RAG module Comprehensive improvements to RAG module reliability and debuggability: **TypeScript (Frontend)** - Add step-by-step logging in RAG.ts constructor for module initialization - Track NitroModules proxy availability and hybrid object creation - Include detailed error messages with stack traces for troubleshooting - Wrap createRAG() function in try-catch with logging **C++ Backend (Error Handling)** - vector_store_usearch.cpp: Add error checking for USearch operations * Validate return values for add(), remove(), save(), load() * Log detailed error messages for failed vector store operations * Gracefully handle batch operation failures - onnx_generator.cpp: Add robust error checking for ONNX Runtime * Validate OrtStatus for all CreateTensorWithDataAsOrtValue calls * Check for null tensors (input_ids, attention_mask, position_ids) * Release status objects prope Comprehensive improvements to RAG module reliability and debuggability: **TypeScript (Frontend)** - Add step-by-step logging in RAG.ts ceLi **TypeScript (Frontend)** - Add step-by-step logging in RAG.ts constr.sh- Add step-by-step loggiso- Track NitroModules proxy availability and hybrid object creation - Incl0.- Include detailed error messages with stack traces for troubleshnc- Wrap createRAG() function in try-catch with logging **C++ Backend (Ll **C++ Backend (Error Handling)** - vector_store_use - - vector_store_usearch.cpp: Addea * Validate return values for add(), remove(), save(), load() * Loc * Log detailed error messages for failed vector store opera * fix(rag): Harden vector store load/search error handling - fix arm_fp16 include guard precedence for Apple targets - make add_chunks_batch report success only when chunks added - remove noexcept from search to avoid terminate on exceptions - make load() robust with JSON try/catch and atomic state update * feat(rag): Add vocab-backed tokenizer and strict embedding setup - load WordPiece vocab and enforce presence before model init - add tokenization cache for repeat words - pass vocab path via embeddingConfigJson - download vocab.txt as separate ONNX model * feat(rag): add LRU cache to tokenizer for repeated inputs * feat(rag): add NEON SIMD tokenization with safe fallback * fix: modernize Metro config and add secure storage method compatibility - Replace deprecated exclusionList/blacklistRE with blockList API (metro-config) - Fixes React Native 0.83.1 build compatibility - Add dual-method support in SecureStorageService for TypeScript/C++ interop - Enables fallback between secureStorageStore/Retrieve and secureStorageSet/Get * fix(rag): correct Android C++ lib name and regenerate Nitrogen outputs * fix(react-native): Fix RAG module initialization and improve core functionality Main fix: - Fix RAG package to use TypeScript source instead of compiled JS Changed package.json main entry from 'lib/index.js' to 'src/index.ts' This resolves 'Cannot find module' error when initializing RAG Aligns with core package structure for proper Metro bundling Additional improvements: - Improve Metro module resolution with explicit extraNodeModules mapping - Add safety flag to prevent duplicate NitroModules native install() calls - Add semantic aliases for secure storage (Store/Retrieve) - Remove pre-bundled JS asset to force Metro dev server usage Fixes React Native Android and iOS RAG module initialization. * fix(react-native/rag): Refactor iOS build to use commons toolchain system Replaces direct Xcode generator CMake invocations with commons build-ios.sh to fix CXX compiler identification failures and create proper XCFrameworks. Changes: - Refactored build-ios-libs.sh to call commons/scripts/build-ios.sh instead of manually configuring CMake with Xcode generator - Added lipo step to create universal simulator static libraries (arm64+x86_64) before XCFramework creation to avoid duplicate architecture errors - Removed ~70 lines of duplicate CMake configuration - Fixed "No known features for CXX compiler" error during simulator builds - Fixed "Both ios-arm64-simulator and ios-x86_64-simulator represent two equivalent library definitions" XCFramework error Technical Details: - Leverages proven toolchain-based builds (ios.toolchain.cmake) instead of Xcode generator which had compiler identification issues - Creates universal simulator libs with lipo before bundling into XCFramework - XCFrameworks now properly contain: - ios-arm64/ (device: 1.2M librac_backend_rag.a) - ios-arm64_x86_64-simulator/ (universal: 2.4M librac_backend_rag.a) - Build output verified: iOS 3.6M XCFramework, Android 41M .so libs Impact: - iOS builds: More reliable, reuses commons infrastructure - Android builds: No changes, release APKs unaffected - React Native integration: Podspec correctly references bundled XCFrameworks Testing: - Full --setup --clean build succeeds - All architectures built (OS arm64, SIMULATORARM64, SIMULATOR x86_64) - XCFrameworks created successfully with proper structure - Android JNI libraries unchanged in jniLibs/arm64-v8a/ * fix(react-native/core): Add framework search paths for RACommons.xcframework The iOS build was failing because RACommons.xcframework headers use framework-relative includes (e.g., `<RACommons/rac_error.h>`), but Xcode couldn't locate the framework during compilation. Changes: - Added HEADER_SEARCH_PATHS for both device (ios-arm64) and simulator (ios-arm64_x86_64-simulator) slices of RACommons.xcframework - Added FRAMEWORK_SEARCH_PATHS pointing to ios/Binaries directory This allows both: 1. Direct includes from C++ bridge files (`#include "rac_telemetry_manager.h"`) 2. Framework-relative includes from RACommons headers (`#include <RACommons/rac_error.h>`) to resolve correctly during the build process. Fixes: iOS build errors related to missing RACommons headers Related: PR #349 (RAG improvements) * fix(react-native/ios): resolve XCFramework and header path issues for iOS build - Rebuild RACommons.xcframework with ios-arm64_x86_64-simulator slice and embedding category definitions - Update RAG module to use <RACommons/...> headers on iOS for proper framework resolution - Add nlohmann/json.hpp header search path to RunAnywhereRAG.podspec - Copy RABackendLLAMACPP.xcframework from build artifacts to SDK and example node_modules These changes fix iOS simulator build failures due to missing headers, missing simulator architecture slices, and undefined embedding category symbols. * Optimize RAG backend: reduce lock contention and fix ORT API version - Extract config values under lock, minimize critical section - Move expensive operations (search, generation) outside mutex - Change embedding_provider and text_generator to shared_ptr for safe copying - Add search_with_provider() helper to reduce pointer dereferencing - Inline prompt formatting for performance - Set ORT_API_VERSION=17 to match bundled libonnxruntime - Remove redundant library loading in RAGPackage This reduces blocking time for concurrent access and improves throughput * Add GTest thread-safety coverage for RAG backend * Add NDK 26 rebuild helper for Android RAG * Fix suffix-only model extension stripping * Harden RAG pipeline creation on failure * test: Add comprehensive unit tests for RAG backend components - Add chunker_test.cpp: 24 test cases covering DocumentChunker functionality * Basic text processing (empty, single/multi-line) * Token estimation and proportionality * Configuration customization (chunk size, overlap) * Boundary conditions (punctuation, whitespace, special chars) * Memory efficiency with large text (100KB+) * Move semantics and thread safety - Add simple_tokenizer_test.cpp: Placeholder for SimpleTokenizer tests * Ready for expansion when class is extracted to public interface - Update CMakeLists.txt: Integrate test targets into build system * rac_chunker_test executable with GoogleTest discovery * rac_simple_tokenizer_test executable with GoogleTest discovery * Proper linking to rac_backend_rag, threads, and GTest All 29 tests passing (24 chunker + 1 tokenizer placeholder + 1 thread safety + 3 executable tests) C++17 standard with proper memory management and best practices * docs(android): document React Native RAG app build workflow Add REACT_NATIVE_ANDROID_RAG_APP_BUILD.md with: - rebuild-android-ndk26.sh reference and execution flow - 4-step build process: clean, build natives, distribute, build APK - Prerequisites and environment configuration - Manual step-by-step alternative - Development iteration cycle (2-3min vs 15+min full rebuild) - Verification and device testing - Troubleshooting common issues - CI/CD integration examples (GitHub Actions, pre-commit hooks) Targets developers working on RAG+embedding+storage features. * fixed rag for ios-react-native Had to change the build scripts a bit to make it work. Llamacpp wasnt included earlier. Currently this matches the android react native and it has hardcoded docs and doesnt allow user to input docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(01-01): Add RAG C headers to CRACommons umbrella and extend error/event/component enums - Copy rac_rag_pipeline.h and rac_rag.h to CRACommons/include with flattened include paths - Add rac_rag_pipeline.h and rac_rag.h to CRACommons.h umbrella header - Add ErrorCategory.rag case for RAG-categorized errors - Add SDKComponent.rag case with displayName "Retrieval-Augmented Generation" - Add EventCategory.rag case for RAG event filtering - Add SDKError.rag() factory method - Fix exhaustive switch statements in RunAnywhere+Frameworks.swift and CppBridge+Services.swift * feat(01-01): Create CppBridge+RAG.swift actor for thread-safe RAG pipeline management - Add CppBridge.RAG actor following the CppBridge+STT.swift pattern - Implement createPipeline, destroy, addDocument, clearDocuments, documentCount, query - All methods delegate to C API (rac_rag_pipeline_create, rac_rag_add_document, etc.) - Actor provides Swift 6 concurrency-safe access without NSLock - Guard checks on pipeline handle throw SDKError.rag(.notInitialized) when not created * feat(01-02): create RAGTypes.swift with Swift wrappers over C RAG types - RAGConfiguration: wraps rac_rag_config_t with withCConfig sync bridge - RAGQueryOptions: wraps rac_rag_query_t with withCQuery sync bridge - RAGSearchResult: wraps rac_search_result_t with init(from:) bridge - RAGResult: wraps rac_rag_result_t with init(from:) bridge - All types conform to Sendable - C string lifetime managed via nested withCString closures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(01-02): create RunAnywhere+RAG.swift public API and RAGEvents.swift - RunAnywhere+RAG: ragCreatePipeline, ragDestroyPipeline, ragIngest, ragClearDocuments, ragDocumentCount, ragQuery as public static API - RAGEvents: ingestionStarted, ingestionComplete, queryStarted, queryComplete, pipelineCreated, pipelineDestroyed, error factory methods - All RAG events published to EventBus.shared with category .rag - CppBridge.RAG: added createPipeline(swiftConfig:) and query(swiftOptions:) overloads to contain C string pointer lifetimes within actor methods Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(02-01): create DocumentService for PDF and JSON text extraction - Add DocumentType enum (pdf/json/unsupported) derived from file extension - Add DocumentServiceError with descriptive LocalizedError messages - Implement DocumentService.extractText(from:) for PDF via PDFKit and JSON via JSONSerialization - Handle security-scoped resource access for document picker URLs - Recursively extract all string values from nested JSON structures * feat(02-01): create RAGViewModel for document lifecycle and query orchestration - Add MessageRole enum (user/assistant/system) - Add RAGViewModel @mainactor @observable with document and query state - Implement loadDocument(url:config:) for extraction + pipeline creation + ingestion - Implement askQuestion() for RAG query with conversation history - Implement clearDocument() to destroy pipeline and reset all state - Wire RunAnywhere.ragIngest and RunAnywhere.ragQuery SDK calls - Persistent isDocumentLoaded state across multiple askQuestion calls * feat(02-02): create DocumentRAGView with document picker and Q&A interface - SwiftUI view binding to RAGViewModel via @State - fileImporter modifier for PDF and JSON document selection - Document status bar (no-doc / loading / loaded states) - Inline error banner with dismiss action - ScrollViewReader message list with auto-scroll on new messages - Input bar with disabled state when no document loaded or querying - RAGMessageBubble with user/assistant alignment using AppColors - ragConfig placeholder with empty model paths (wired in future iteration) * feat(02-02): wire DocumentRAGView into ContentView MoreHubView - Add NavigationLink to DocumentRAGView as first item in MoreHubView - Use doc.text.magnifyingglass SF Symbol with indigo color - Placed before existing Transcribe/Speak/Storage links - RAG feature now accessible from More tab navigation * fix(ios-rag): add model selection UI for embedding and LLM models Root cause: DocumentRAGView used empty placeholder strings for embeddingModelPath and llmModelPath in RAGConfiguration, causing pipeline creation to fail with error -102 (notInitialized). - Add .ragEmbedding and .ragLLM cases to ModelSelectionContext, with framework-level filtering (ONNX for embedding, llamaCpp for LLM) - Add allowedFrameworks property to ModelSelectionContext for precise framework filtering - Update ModelSelectionSheet to filter by allowedFrameworks and skip memory-loading for RAG contexts - Rewrite DocumentRAGView to track selectedEmbeddingModel and selectedLLMModel, show model picker rows, compute RAGConfiguration from real localPaths, and gate document selection on both models being ready Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ios-rag): add embedding model support for RAG pipeline Root cause: Three issues prevented embedding models from appearing and downloading in the iOS app RAG feature: 1. ModelCategory enum was missing .embedding case (C layer had RAC_MODEL_CATEGORY_EMBEDDING = 7 but Swift enum didn't expose it). Added .embedding to ModelTypes.swift and updated C bridge conversions in ModelTypes+CppBridge.swift and CppBridge+ModelAssignment.swift. 2. No ONNX embedding models registered in RunAnywhereAIApp.swift. Added all-MiniLM-L6-v2 (model.onnx) and its vocab file, matching the React Native example app. 3. ModelSelectionSheet .ragEmbedding context was filtering by [.language] instead of [.embedding]. Updated to use the correct category. Also excluded vocab/tokenizer files from the picker so users only see selectable embedding models. 4. DocumentRAGView now resolves vocab path and passes it as embeddingConfigJSON to RAGConfiguration, matching the React Native RAGScreen pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * RAG code swift * build scripts +package.swift * Addressed the bot issues + removed accidental docs * docs(01-01): complete RAG C API bridge plan - add SUMMARY, update STATE, ROADMAP, REQUIREMENTS - 01-01-SUMMARY.md: documents what was built, decisions, and deviations - STATE.md: advance to plan 1 complete, add 3 architectural decisions - ROADMAP.md: mark 01-01-PLAN.md complete, progress 1/2 in Phase 1 - REQUIREMENTS.md: mark SDK-01 complete * docs(01-02): complete RAG public API plan - add SUMMARY, update STATE, ROADMAP, REQUIREMENTS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(02): create phase plan for iOS app RAG feature Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs(02-01): complete iOS app RAG data/logic layer plan - add SUMMARY, update STATE, ROADMAP, REQUIREMENTS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(02-02): complete RAG UI plan - add SUMMARY, update STATE, ROADMAP - 02-02-SUMMARY.md: DocumentRAGView + ContentView wiring complete - STATE.md: Phase 2 complete (2/2 plans), progress 100% - ROADMAP.md: Phase 2 marked complete with 2 summaries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Kotlin-RAG Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove planning directory from Kotlin branch * KOTLIN-RAG Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Made this pathfinding consistent with the other modules Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Reduce the cosine similarity values so that you get results. --------- Co-authored-by: Hsin-Wen Chang <bow1226@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 261 files, which is 111 over the limit of 150. You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis pull request adds comprehensive LoRA (Low-Rank Adapter) support across the entire stack—C++ backend, C APIs, JNI bindings, Kotlin SDK, and Android UI. It introduces LoRA compatibility checking (with GGUF metadata reading), download workflows with progress reporting, and catalog management. Concurrently, the RunAnyWhereLora example module is largely removed and replaced with LoRA integration in the main ChatScreen UI. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Kotlin as Kotlin SDK
participant JNI as JNI Bridge
participant C_API as C API (llm_component)
participant Backend as C++ Backend
participant GGUF as GGUF File
User->>Kotlin: checkLoraCompatibility(loraPath)
activate Kotlin
Kotlin->>Kotlin: Validate SDK initialized
Kotlin->>JNI: racLlmComponentCheckLoraCompat(handle, loraPath)
activate JNI
JNI->>C_API: rac_llm_component_check_lora_compat(...)
activate C_API
C_API->>C_API: Acquire mutex
C_API->>Backend: llm_service->ops->check_lora_compat(...)
activate Backend
Backend->>GGUF: Read LoRA metadata (architecture)
GGUF-->>Backend: GGUF data
Backend->>Backend: Compare architectures with loaded model
alt Architectures match
Backend-->>C_API: RAC_SUCCESS
else Architectures mismatch
Backend-->>C_API: RAC_ERROR_VALIDATION_FAILED + error message
end
deactivate Backend
C_API->>C_API: Release mutex
C_API-->>JNI: result + error string
deactivate C_API
JNI-->>Kotlin: error message or null
deactivate JNI
alt Compatible
Kotlin-->>User: LoraCompatibilityResult(isCompatible=true)
else Incompatible
Kotlin-->>User: LoraCompatibilityResult(isCompatible=false, error=...)
end
deactivate Kotlin
sequenceDiagram
actor User
participant Android as Android UI
participant ViewModel as ChatViewModel
participant Kotlin as Kotlin SDK
participant HTTP as HTTP Server
participant FileSystem as Local Storage
User->>Android: Click Download LoRA
activate Android
Android->>ViewModel: downloadLoraFromCatalog(entry)
activate ViewModel
ViewModel->>Kotlin: downloadLoraAdapter(entry.url, entry.filename)
activate Kotlin
Kotlin->>Kotlin: Create temp file in ModelsDirectory/lora
Kotlin->>HTTP: Open HttpURLConnection
activate HTTP
HTTP-->>Kotlin: contentLengthLong
deactivate HTTP
Kotlin->>Kotlin: Emit DOWNLOADING state with totalBytes
Kotlin->>HTTP: Read stream data
activate HTTP
loop Stream chunks
HTTP-->>Kotlin: Data chunk
Kotlin->>FileSystem: Write to temp file
Kotlin->>Kotlin: Update progress (~150ms interval)
Kotlin-->>ViewModel: Progress update
ViewModel->>Android: Update UI progress
end
HTTP-->>Kotlin: Stream complete
deactivate HTTP
Kotlin->>FileSystem: Atomic rename temp→final or copy+delete
Kotlin->>Kotlin: Emit COMPLETED state with localPath
Kotlin-->>ViewModel: Final progress with path
ViewModel->>Android: Render completion
deactivate Kotlin
ViewModel->>ViewModel: Scan downloads & refresh adapters
deactivate ViewModel
Android->>User: Show downloaded adapter ready to load
deactivate Android
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Important
Looks good to me! 👍
Reviewed everything up to 07700f2 in 16 seconds. Click for details.
- Reviewed
3221lines of code in24files - Skipped
0files when reviewing. - Skipped posting
0draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
Workflow ID: wflow_53KetZF6Bc4rq3wc
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
| if (destFile.exists()) destFile.delete() | ||
| if (!tempFile.renameTo(destFile)) { | ||
| tempFile.copyTo(destFile, overwrite = true) | ||
| tempFile.delete() | ||
| } |
There was a problem hiding this comment.
fallback copyTo after renameTo fails could leave both files present temporarily
| if (destFile.exists()) destFile.delete() | |
| if (!tempFile.renameTo(destFile)) { | |
| tempFile.copyTo(destFile, overwrite = true) | |
| tempFile.delete() | |
| } | |
| if (!tempFile.renameTo(destFile)) { | |
| if (destFile.exists()) destFile.delete() | |
| tempFile.copyTo(destFile, overwrite = true) | |
| tempFile.delete() | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRADownload.jvmAndroid.kt
Line: 106-110
Comment:
fallback `copyTo` after `renameTo` fails could leave both files present temporarily
```suggestion
if (!tempFile.renameTo(destFile)) {
if (destFile.exists()) destFile.delete()
tempFile.copyTo(destFile, overwrite = true)
tempFile.delete()
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
sdk/runanywhere-commons/src/backends/llamacpp/rac_backend_llamacpp_register.cpp (1)
129-147:⚠️ Potential issue | 🟡 MinorReturn RAC_ERROR_NULL_POINTER for invalid args.
Nulllora_path/out_errorcurrently becomesRAC_ERROR_VALIDATION_FAILED, which makes API misuse look like incompatibility.Suggested fix
static rac_result_t llamacpp_vtable_check_lora_compat(void* impl, const char* lora_path, char** out_error) { + if (lora_path == nullptr || out_error == nullptr) { + return RAC_ERROR_NULL_POINTER; + } rac_bool_t result = rac_llm_llamacpp_check_lora_compat(impl, lora_path, out_error); return (result == RAC_TRUE) ? RAC_SUCCESS : RAC_ERROR_VALIDATION_FAILED; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/backends/llamacpp/rac_backend_llamacpp_register.cpp` around lines 129 - 147, The check function llamacpp_vtable_check_lora_compat should validate input pointers and return RAC_ERROR_NULL_POINTER for null args instead of treating them as validation failures; update llamacpp_vtable_check_lora_compat to first verify impl, lora_path, and out_error are non-NULL and return RAC_ERROR_NULL_POINTER immediately if any are NULL, and only then call rac_llm_llamacpp_check_lora_compat and map its boolean result to RAC_SUCCESS or RAC_ERROR_VALIDATION_FAILED; keep g_llamacpp_ops.check_lora_compat pointing to llamacpp_vtable_check_lora_compat.
🧹 Nitpick comments (8)
examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt (2)
159-167: Potential missed trigger if download path is reused.If the user downloads the same adapter file twice (e.g., after deleting it),
state.loraDownload.downloadedPathwill hold the same value as beforeclearDownloadState()resets it tonull. This should work correctly since the flow isnull → path → null → path. However, there is a subtle timing concern:clearDownloadState()on line 164 sets the path back tonull, which means the nextLaunchedEffectrecomposition will seenulland skip. On the subsequent download completion, it will see the path again and trigger. This looks correct.One concern:
viewModel.clearDownloadState()andshowDownloadDialog = falseare both executed in sequence, but the dialog dismissal triggers recomposition which could race with the state update. Consider whetherclearDownloadStateshould be called after settingpendingLoraPathto ensure the scale dialog appears before the download state is cleared.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt` around lines 159 - 167, Reorder the state updates in the LaunchedEffect handling state.loraDownload.downloadedPath so the local UI state is captured before clearing the download state: when path != null, first assign pendingLoraPath = path, then set showDownloadDialog = false (to trigger the scale dialog UI), and only after those UI state updates call viewModel.clearDownloadState(); this ensures LaunchedEffect/compose recomposition shows the dialog based on pendingLoraPath before the shared viewModel download state is reset.
897-914: Non-interactiveSuggestionChipwith emptyonClickmay confuse users.These chips look tappable but do nothing. Consider either using a plain
Text/surface composable instead, or wiring theonClickto pre-fill the prompt input so the user can try the sample prompt after loading the adapter.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt` around lines 897 - 914, The SuggestionChip instances in the FlowRow are non-interactive because onClick is an empty lambda; either replace SuggestionChip with a non-tappable composable (e.g., Text or Surface) to avoid misleading affordance, or wire SuggestionChip's onClick to populate the prompt input state used by this screen (i.e., update the prompt input state or call the existing prompt handler/callback when a sample prompt from template.samplePrompts is clicked) so tapping a chip pre-fills the input for the user to submit.examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraPromptTemplates.kt (1)
13-53: Template keys are stringly-coupled to catalog adapter IDs.The template map keys (e.g.,
"chat-assistant-lora") must exactly matchLoraAdapterEntry.idvalues inLoraAdapterCatalog. Consider referencing the catalog IDs directly to avoid silent mismatches if IDs change.Example using catalog constants
import com.runanywhere.sdk.temp.LoraAdapterCatalog val loraPromptTemplates: Map<String, LoraPromptTemplate> = mapOf( LoraAdapterCatalog.adapters[0].id to LoraPromptTemplate( // ... ), // or define ID constants in the catalog )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraPromptTemplates.kt` around lines 13 - 53, The map loraPromptTemplates uses hard-coded string keys (e.g., "chat-assistant-lora") that must match LoraAdapterEntry.id values in LoraAdapterCatalog; change the keys to reference the catalog IDs instead (for example via LoraAdapterCatalog.adapters[index].id or defined constants on LoraAdapterCatalog) so keys stay in sync, update any usages expecting those keys accordingly, and ensure imports for LoraAdapterCatalog are added next to LoraPromptTemplate where loraPromptTemplates is declared.sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp (1)
1184-1211: Consider surfacing the RAC error code whenerroris null.
Right now the fallback “Incompatible LoRA adapter” hides cases like component-not-ready or invalid argument.Suggested tweak
jstring jresult = nullptr; if (error) { jresult = env->NewStringUTF(error); rac_free(error); } else { - jresult = env->NewStringUTF("Incompatible LoRA adapter"); + const char* msg = rac_error_message(result); + jresult = env->NewStringUTF(msg && *msg ? msg : "Incompatible LoRA adapter"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 1184 - 1211, The JNI function racLlmComponentCheckLoraCompat currently returns a generic "Incompatible LoRA adapter" when rac_llm_component_check_lora_compat fails but provides no error string; update the failure branch so when error == nullptr you include the numeric rac_result_t (result) in the returned message (e.g. "Incompatible LoRA adapter (error code: %d)") by formatting into a local buffer and calling env->NewStringUTF, and keep the existing behavior of env->NewStringUTF(error) + rac_free(error) when error is non-null; reference racLlmComponentCheckLoraCompat, rac_llm_component_check_lora_compat, rac_result_t and RAC_SUCCESS to locate and modify the code.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.kt (1)
252-260: Avoid string-based LoRA detection.
DerivingsupportsLorafromnameis brittle and not type-safe. Prefer a structured field (e.g.,ModelInfo.supportsLoraor catalog metadata) and compute this upstream.As per coding guidelines: Always use structured types and data classes instead of strings directly for type safety and consistency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.kt` around lines 252 - 260, The current supportsLora flag is computed by brittle string checks in ModelSelectionBottomSheet (the nameLower.contains("lfm2") && nameLower.contains("350m") logic) — change this to read a typed property from the model metadata instead: add a boolean supportsLora field to the model info/data class that is populated upstream when the model catalog or parsing logic determines Lora support, and then replace the string-based expression in the AIModel constructor with that property (use m.supportsLora in the AIModel(...) call). Update the code path that builds/loads m (the model metadata factory or parser) to compute and set supportsLora based on structured criteria rather than name substrings.sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRADownload.kt (1)
19-47: Consider sealed progress/error types instead of enum + nullable error.A sealed hierarchy (e.g.,
Pending/Downloading/Completed/Error) would make error handling more type-safe thanstate + error: String?, and avoids nullable payloads.
As per coding guidelines, "Use sealed classes for type-safe error handling rather than generic exception handling".Also applies to: 61-82
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.kt around lines 19 - 47, Replace the enum+nullable pattern LoraDownloadState + LoraDownloadProgress with a sealed class hierarchy to make states and payloads type-safe: create a sealed class LoraDownloadProgress with subclasses Pending, Downloading(progress: Float, bytesDownloaded: Long, totalBytes: Long?), Completed(localPath: String, bytesDownloaded: Long, totalBytes: Long?), and Error(message: String), and update any callers that construct or inspect LoraDownloadState/LoraDownloadProgress to use sealed-type pattern matching instead of state+nullable fields; apply the same refactor to the other progress/error pair in this file that follows the same pattern.examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt (1)
747-758: Hard‑coded LoRA compatibility checks are brittle.The
lfm2 + 350mname heuristic appears in multiple spots and can silently hide LoRA for other compatible models. Consider centralizing this in a helper and/or deriving it from model metadata or backend capabilities.Also applies to: 771-814
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt` around lines 747 - 758, The hard-coded LoRA compatibility heuristic in ChatViewModel (the local loraCompat = nameLower.contains("lfm2") && nameLower.contains("350m") logic used before updating _uiState and calling loadLoraCatalog()/scanDownloadedAdapters()) should be replaced with a centralized helper or service: extract the check into a single function like isModelLoraCompatible(modelName: String) or, better, consult model metadata/backend capability (e.g., model metadata field or capability flag) and use that result to set isLoraCompatibleModel and gate loadLoraCatalog()/scanDownloadedAdapters(); update all other occurrences (the other places mentioned around the 771-814 range) to call the same helper/service so the heuristic is maintained in one place.sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp (1)
889-939: Prefer a structured compatibility result over raw strings.
check_lora_compatibilityreturnsboolplus a mutablestd::string&for errors. Consider returning a small struct (e.g.,{bool ok; rac_error_t code; std::string message;}) to align with the structured-type rule and avoid stale error reuse.
As per coding guidelines, "Always use structured types, never use strings directly for consistency and scalability".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp` around lines 889 - 939, check_lora_compatibility currently returns bool + an out std::string&; change it to return a small structured result type (e.g., struct LoraCompatResult { bool ok; rac_error_t code; std::string message; }) and remove the mutable std::string& parameter, populate appropriate codes (e.g., RAC_OK, RAC_ERR_NO_MODEL, RAC_ERR_READ_LORA, RAC_ERR_ARCH_MISMATCH) and messages inside check_lora_compatibility, update all callers to use the new LoraCompatResult, and ensure resources (like lora_ctx) and logging (LOGI/LOGE) remain the same while filling result.message instead of using the out-string to avoid stale error reuse and to satisfy the structured-type rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.idea/vcs.xml:
- Around line 5-16: Remove all machine-local VCS mapping entries in
.idea/vcs.xml that point into generated build directories and local tools (the
<mapping directory=".../_deps/..." vcs="Git" /> and the emsdk mapping); keep
only the root mapping entry (the mapping with directory="") in the file, and
commit that change. Optionally, consider removing .idea/vcs.xml from version
control or update ignore rules so only machine-agnostic IDE metadata is tracked.
In `@docs/impl/lora_adapter_support.md`:
- Around line 720-748: Update the two fenced code blocks that show the check
flow and the download flow to include a language tag (use "text") so they read
```text ... ``` instead of ``` ... ```, and fix the typo "cal" to "call" in the
trailing sentence; ensure the same changes are applied to the duplicate block at
lines noted (the second occurrence shown around RunAnywhere.downloadLoraAdapter
/ LoraDownloadProgress).
In
`@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt`:
- Around line 793-802: The TextButton currently enables/downloads based only on
urlInput.isNotBlank(); add client-side URL validation so the button only enables
and triggers when the URL is well-formed (e.g., starts with "https://" or
matches Android's URL pattern). Create a helper like isValidUrl(url: String)
used both in the enabled expression and in the onClick guard before calling
onDownloadFromUrl(urlInput.trim(), nameInput.trim()), and return/notify the user
if invalid; reference the existing urlInput, nameInput, onDownloadFromUrl, and
the TextButton block in LoraScreen.kt when applying the change.
In
`@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraViewModel.kt`:
- Around line 136-168: The filename can contain path traversal segments; in
downloadLoraFromUrl sanitize it by extracting only the basename before appending
".gguf" (e.g. use File(filename).name or
Paths.get(filename).fileName.toString()), reject or replace empty basenames with
a safe default, and then use that sanitizedFilename when calling
RunAnywhere.downloadLoraAdapter so downloads cannot escape the intended lora/
directory.
In `@sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h`:
- Around line 247-275: Update the two function docblocks to instruct callers to
use rac_free instead of free(): change the "caller must free with free()" note
in rac_llm_llamacpp_check_lora_compat and the "caller must free with free()"
note in rac_llm_llamacpp_read_gguf_info to "caller must free with rac_free" so
docs correctly reflect the repository's memory-deallocation API.
In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h`:
- Around line 266-280: Update the rac_llm_component_check_lora_compat
declaration docs to explicitly list possible rac_result_t error codes (e.g.,
RAC_SUCCESS, RAC_ERROR_INVALID_ARGUMENT, RAC_ERROR_COMPONENT_NOT_READY,
RAC_ERROR_IO, etc.), state the lifecycle requirement that a model must be loaded
into the component before calling this function, and clarify ownership/cleanup
for out_error (caller must free with rac_free when non-NULL); reference the
function name rac_llm_component_check_lora_compat, parameter types rac_handle_t
and char**, return type rac_result_t, and the rac_free helper so callers know
expected states and error handling.
In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h`:
- Around line 64-67: Update the comment for the vtable function pointer
check_lora_compat to document out_error ownership: state that when
check_lora_compat(void* impl, const char* lora_path, char** out_error) returns
an error (non-RAC_SUCCESS) it allocates *out_error and the caller is responsible
for freeing it with free(), and when the function returns RAC_SUCCESS it sets
*out_error to NULL (no allocation).
In `@sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp`:
- Around line 889-939: The method check_lora_compatibility reads shared state
(model_ and model_loaded_) without synchronization, risking races with
load_model()/unload_model(); similarly is_model_loaded() and get_model_info()
need protection. Fix by adding std::lock_guard<std::mutex> lock(mutex_) at the
start of each of these const methods (check_lora_compatibility, is_model_loaded,
get_model_info) so the body executes while holding mutex_, preventing
use-after-free when the C API wrapper rac_llm_llamacpp_check_lora_compat() is
invoked concurrently.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.kt:
- Around line 61-69: Make the API function suspend: change the declaration of
checkLoraCompatibility to "suspend fun
RunAnywhere.checkLoraCompatibility(loraPath: String): LoraCompatibilityResult"
and update all platform-specific actual implementations accordingly (the
JVM/native implementations that call JNI and use synchronized(lock)). Update any
callers (or ensure they still wrap with withContext(Dispatchers.IO)) to match
the new suspend signature. Keep the function body logic unchanged (GGUF metadata
read, architecture compare) but mark it suspend so blocking JNI/synchronized
calls are clearly asynchronous.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.jvmAndroid.kt:
- Around line 26-142: The download uses filename directly which allows path
traversal and arbitrary extensions; in RunAnywhere.downloadLoraAdapter sanitize
filename by deriving a safe basename (strip any directory components, reject or
replace path separators), ensure it is non-empty, and enforce the .gguf
extension (append or replace other extensions) before creating destFile and
tempFile; use this safeFilename variable in the File(destDir, safeFilename) and
File(destDir, "$safeFilename.tmp") calls and validate the resulting path stays
under destDir.
---
Outside diff comments:
In
`@sdk/runanywhere-commons/src/backends/llamacpp/rac_backend_llamacpp_register.cpp`:
- Around line 129-147: The check function llamacpp_vtable_check_lora_compat
should validate input pointers and return RAC_ERROR_NULL_POINTER for null args
instead of treating them as validation failures; update
llamacpp_vtable_check_lora_compat to first verify impl, lora_path, and out_error
are non-NULL and return RAC_ERROR_NULL_POINTER immediately if any are NULL, and
only then call rac_llm_llamacpp_check_lora_compat and map its boolean result to
RAC_SUCCESS or RAC_ERROR_VALIDATION_FAILED; keep
g_llamacpp_ops.check_lora_compat pointing to llamacpp_vtable_check_lora_compat.
---
Nitpick comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt`:
- Around line 747-758: The hard-coded LoRA compatibility heuristic in
ChatViewModel (the local loraCompat = nameLower.contains("lfm2") &&
nameLower.contains("350m") logic used before updating _uiState and calling
loadLoraCatalog()/scanDownloadedAdapters()) should be replaced with a
centralized helper or service: extract the check into a single function like
isModelLoraCompatible(modelName: String) or, better, consult model
metadata/backend capability (e.g., model metadata field or capability flag) and
use that result to set isLoraCompatibleModel and gate
loadLoraCatalog()/scanDownloadedAdapters(); update all other occurrences (the
other places mentioned around the 771-814 range) to call the same helper/service
so the heuristic is maintained in one place.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.kt`:
- Around line 252-260: The current supportsLora flag is computed by brittle
string checks in ModelSelectionBottomSheet (the nameLower.contains("lfm2") &&
nameLower.contains("350m") logic) — change this to read a typed property from
the model metadata instead: add a boolean supportsLora field to the model
info/data class that is populated upstream when the model catalog or parsing
logic determines Lora support, and then replace the string-based expression in
the AIModel constructor with that property (use m.supportsLora in the
AIModel(...) call). Update the code path that builds/loads m (the model metadata
factory or parser) to compute and set supportsLora based on structured criteria
rather than name substrings.
In
`@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraPromptTemplates.kt`:
- Around line 13-53: The map loraPromptTemplates uses hard-coded string keys
(e.g., "chat-assistant-lora") that must match LoraAdapterEntry.id values in
LoraAdapterCatalog; change the keys to reference the catalog IDs instead (for
example via LoraAdapterCatalog.adapters[index].id or defined constants on
LoraAdapterCatalog) so keys stay in sync, update any usages expecting those keys
accordingly, and ensure imports for LoraAdapterCatalog are added next to
LoraPromptTemplate where loraPromptTemplates is declared.
In
`@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt`:
- Around line 159-167: Reorder the state updates in the LaunchedEffect handling
state.loraDownload.downloadedPath so the local UI state is captured before
clearing the download state: when path != null, first assign pendingLoraPath =
path, then set showDownloadDialog = false (to trigger the scale dialog UI), and
only after those UI state updates call viewModel.clearDownloadState(); this
ensures LaunchedEffect/compose recomposition shows the dialog based on
pendingLoraPath before the shared viewModel download state is reset.
- Around line 897-914: The SuggestionChip instances in the FlowRow are
non-interactive because onClick is an empty lambda; either replace
SuggestionChip with a non-tappable composable (e.g., Text or Surface) to avoid
misleading affordance, or wire SuggestionChip's onClick to populate the prompt
input state used by this screen (i.e., update the prompt input state or call the
existing prompt handler/callback when a sample prompt from
template.samplePrompts is clicked) so tapping a chip pre-fills the input for the
user to submit.
In `@sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp`:
- Around line 889-939: check_lora_compatibility currently returns bool + an out
std::string&; change it to return a small structured result type (e.g., struct
LoraCompatResult { bool ok; rac_error_t code; std::string message; }) and remove
the mutable std::string& parameter, populate appropriate codes (e.g., RAC_OK,
RAC_ERR_NO_MODEL, RAC_ERR_READ_LORA, RAC_ERR_ARCH_MISMATCH) and messages inside
check_lora_compatibility, update all callers to use the new LoraCompatResult,
and ensure resources (like lora_ctx) and logging (LOGI/LOGE) remain the same
while filling result.message instead of using the out-string to avoid stale
error reuse and to satisfy the structured-type rule.
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp`:
- Around line 1184-1211: The JNI function racLlmComponentCheckLoraCompat
currently returns a generic "Incompatible LoRA adapter" when
rac_llm_component_check_lora_compat fails but provides no error string; update
the failure branch so when error == nullptr you include the numeric rac_result_t
(result) in the returned message (e.g. "Incompatible LoRA adapter (error code:
%d)") by formatting into a local buffer and calling env->NewStringUTF, and keep
the existing behavior of env->NewStringUTF(error) + rac_free(error) when error
is non-null; reference racLlmComponentCheckLoraCompat,
rac_llm_component_check_lora_compat, rac_result_t and RAC_SUCCESS to locate and
modify the code.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.kt:
- Around line 19-47: Replace the enum+nullable pattern LoraDownloadState +
LoraDownloadProgress with a sealed class hierarchy to make states and payloads
type-safe: create a sealed class LoraDownloadProgress with subclasses Pending,
Downloading(progress: Float, bytesDownloaded: Long, totalBytes: Long?),
Completed(localPath: String, bytesDownloaded: Long, totalBytes: Long?), and
Error(message: String), and update any callers that construct or inspect
LoraDownloadState/LoraDownloadProgress to use sealed-type pattern matching
instead of state+nullable fields; apply the same refactor to the other
progress/error pair in this file that follows the same pattern.
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/sdcpp/arm64-v8a/_deps/nlohmann_json-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/sdcpp/arm64-v8a/_deps/opencl_headers-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/sdcpp/arm64-v8a/_deps/stable_diffusion_cpp-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/sdcpp/arm64-v8a/_deps/stable_diffusion_cpp-src/ggml" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/unified/arm64-v8a/_deps/llamacpp-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/unified/arm64-v8a/_deps/nlohmann_json-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/dev-core/_deps/nlohmann_json-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/dev/_deps/llamacpp-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/dev/_deps/nlohmann_json-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/unified/x86_64/_deps/llamacpp-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/unified/x86_64/_deps/nlohmann_json-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-web/emsdk" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-web/wasm/build-webgpu/_deps/llamacpp-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-web/wasm/build-webgpu/_deps/nlohmann_json-src" vcs="Git" /> | ||
| <mapping directory="$PROJECT_DIR$/sdk/runanywhere-web/wasm/build-webgpu/_deps/whispercpp-src" vcs="Git" /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for .gitignore entries related to .idea/
echo "=== Checking .gitignore files for .idea/ entries ==="
find . -name ".gitignore" -type f | head -20 | while read f; do
if grep -q "\.idea" "$f" 2>/dev/null; then
echo "Found in: $f"
grep -n "\.idea" "$f"
fi
doneRepository: RunanywhereAI/runanywhere-sdks
Length of output: 2011
🏁 Script executed:
#!/bin/bash
# View the full content of .idea/vcs.xml with line numbers
echo "=== Full content of .idea/vcs.xml ==="
if [ -f ".idea/vcs.xml" ]; then
cat -n ".idea/vcs.xml"
else
echo ".idea/vcs.xml not found"
fiRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1869
Remove all machine-local build-artifact paths from .idea/vcs.xml.
Every entry at lines 5–16 points into generated build directories (_deps/… populated by CMake) or locally-installed tools (emsdk). These paths:
- Do not exist on any other developer's machine unless they have built the exact same way.
- Are ephemeral, architecture-specific, and differ per build directory.
- Will cause spurious "unregistered VCS root detected" warnings for teammates.
The root mapping on line 4 (directory="") already covers the entire repository. The entries below add no value and create friction.
🛠️ Proposed fix — keep only the root mapping
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/sdcpp/arm64-v8a/_deps/nlohmann_json-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/sdcpp/arm64-v8a/_deps/opencl_headers-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/sdcpp/arm64-v8a/_deps/stable_diffusion_cpp-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/sdcpp/arm64-v8a/_deps/stable_diffusion_cpp-src/ggml" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/unified/arm64-v8a/_deps/llamacpp-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/unified/arm64-v8a/_deps/nlohmann_json-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/unified/x86_64/_deps/llamacpp-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-commons/build/android/unified/x86_64/_deps/nlohmann_json-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-web/emsdk" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-web/wasm/build-webgpu/_deps/llamacpp-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-web/wasm/build-webgpu/_deps/nlohmann_json-src" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/sdk/runanywhere-web/wasm/build-webgpu/_deps/whispercpp-src" vcs="Git" />
</component>Note: .idea/vcs.xml is currently tracked (via !.idea/vcs.xml in .gitignore). Consider whether this file belongs in version control at all, or adopt a policy to exclude only machine-agnostic IDE metadata (e.g., .idea/codeStyles/, .idea/inspectionProfiles/).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.idea/vcs.xml around lines 5 - 16, Remove all machine-local VCS mapping
entries in .idea/vcs.xml that point into generated build directories and local
tools (the <mapping directory=".../_deps/..." vcs="Git" /> and the emsdk
mapping); keep only the root mapping entry (the mapping with directory="") in
the file, and commit that change. Optionally, consider removing .idea/vcs.xml
from version control or update ignore rules so only machine-agnostic IDE
metadata is tracked.
| ``` | ||
| 1. Kotlin calls RunAnywhere.checkLoraCompatibility(path) | ||
| 2. CppBridgeLLM.checkLoraCompatibility(path) | ||
| - synchronized(lock), validates LLM state == READY | ||
| 3. JNI: racLlmComponentCheckLoraCompat(handle, path) | ||
| - Returns null (compatible) or error string (incompatible) | ||
| 4. rac_llm_component_check_lora_compat(handle, path, &out_error) | ||
| - Dispatches through vtable: ops->check_lora_compat() | ||
| 5. Backend wrapper converts rac_bool_t to rac_result_t | ||
| 6. rac_llm_llamacpp_check_lora_compat(handle, path, &out_error) | ||
| - Casts to C++ impl | ||
| 7. LlamaCppTextGeneration::check_lora_compatibility(path, error_msg) | ||
| - gguf_init_from_file(lora_path, {.no_alloc=true}) | ||
| - gguf_find_key() for "general.architecture" | ||
| - llama_model_meta_val_str() for model architecture | ||
| - Compare strings, return false with error if mismatch | ||
| ``` | ||
|
|
||
| ### Download Flow | ||
|
|
||
| Downloads are handled entirely in Kotlin (no C++ involvement): | ||
|
|
||
| ``` | ||
| 1. RunAnywhere.downloadLoraAdapter(url, filename) returns Flow<LoraDownloadProgress> | ||
| 2. Opens HttpURLConnection to the URL | ||
| 3. Streams to temp file: {models_dir}/lora/{filename}.tmp | ||
| 4. Emits progress updates every 150ms | ||
| 5. On completion, atomic rename: .tmp -> .gguf | ||
| 6. Final emission with state=COMPLETED and localPath set |
There was a problem hiding this comment.
Doc lint: add fenced code block languages + fix typo.
Two fenced blocks are missing a language tag, and “cal” should be “call”.
✍️ Proposed fix
-```
+```text
1. Kotlin calls RunAnywhere.checkLoraCompatibility(path)
...
-```
+```
-```
+```text
1. RunAnywhere.downloadLoraAdapter(url, filename) returns Flow<LoraDownloadProgress>
...
-```
+```
-This makes the check fast (< 1ms for typical LoRA files) and safe to cal repeatedly.
+This makes the check fast (< 1ms for typical LoRA files) and safe to call repeatedly.Also applies to: 842-847
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 720-720: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 742-742: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/impl/lora_adapter_support.md` around lines 720 - 748, Update the two
fenced code blocks that show the check flow and the download flow to include a
language tag (use "text") so they read ```text ... ``` instead of ``` ... ```,
and fix the typo "cal" to "call" in the trailing sentence; ensure the same
changes are applied to the duplicate block at lines noted (the second occurrence
shown around RunAnywhere.downloadLoraAdapter / LoraDownloadProgress).
| TextButton( | ||
| onClick = { | ||
| if (urlInput.isNotBlank() && nameInput.isNotBlank()) { | ||
| onDownloadFromUrl(urlInput.trim(), nameInput.trim()) | ||
| } | ||
| }, | ||
| enabled = urlInput.isNotBlank() && nameInput.isNotBlank(), | ||
| ) { | ||
| Text("Download") | ||
| } |
There was a problem hiding this comment.
No URL format validation before triggering download.
The download button only checks isNotBlank() but doesn't validate that urlInput is a valid URL (e.g., starts with https://). Invalid URLs will fail at the network layer, but basic client-side validation provides better UX.
Proposed fix
TextButton(
onClick = {
- if (urlInput.isNotBlank() && nameInput.isNotBlank()) {
+ val trimmedUrl = urlInput.trim()
+ if (trimmedUrl.isNotBlank() && nameInput.isNotBlank() &&
+ (trimmedUrl.startsWith("http://") || trimmedUrl.startsWith("https://"))
+ ) {
- onDownloadFromUrl(urlInput.trim(), nameInput.trim())
+ onDownloadFromUrl(trimmedUrl, nameInput.trim())
}
},
- enabled = urlInput.isNotBlank() && nameInput.isNotBlank(),
+ enabled = urlInput.isNotBlank() && nameInput.isNotBlank() &&
+ (urlInput.trim().startsWith("http://") || urlInput.trim().startsWith("https://")),
) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt`
around lines 793 - 802, The TextButton currently enables/downloads based only on
urlInput.isNotBlank(); add client-side URL validation so the button only enables
and triggers when the URL is well-formed (e.g., starts with "https://" or
matches Android's URL pattern). Create a helper like isValidUrl(url: String)
used both in the enabled expression and in the onClick guard before calling
onDownloadFromUrl(urlInput.trim(), nameInput.trim()), and return/notify the user
if invalid; reference the existing urlInput, nameInput, onDownloadFromUrl, and
the TextButton block in LoraScreen.kt when applying the change.
| fun downloadLoraFromUrl(url: String, filename: String) { | ||
| val sanitizedFilename = if (filename.endsWith(".gguf")) filename else "$filename.gguf" | ||
| downloadJob?.cancel() | ||
| downloadJob = viewModelScope.launch { | ||
| _uiState.update { | ||
| it.copy(loraDownload = LoraDownloadUiState(isDownloading = true)) | ||
| } | ||
| try { | ||
| RunAnywhere.downloadLoraAdapter(url, sanitizedFilename).collect { progress -> | ||
| _uiState.update { | ||
| it.copy( | ||
| loraDownload = it.loraDownload.copy( | ||
| progress = progress.progress, | ||
| error = progress.error, | ||
| isDownloading = progress.state == LoraDownloadState.DOWNLOADING || | ||
| progress.state == LoraDownloadState.PENDING, | ||
| downloadedPath = progress.localPath, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| } catch (e: Exception) { | ||
| Log.e(TAG, "LoRA download failed: ${e.message}", e) | ||
| _uiState.update { | ||
| it.copy( | ||
| loraDownload = LoraDownloadUiState( | ||
| error = "Download failed: ${e.message}", | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Sanitize user‑provided filenames to prevent path traversal.
filename is only extension-sanitized, so values like ../foo can escape the lora/ dir. Normalize to a basename before appending .gguf.
🔒 Proposed fix
- val sanitizedFilename = if (filename.endsWith(".gguf")) filename else "$filename.gguf"
+ val safeName = File(filename).name
+ val sanitizedFilename = if (safeName.endsWith(".gguf")) safeName else "$safeName.gguf"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraViewModel.kt`
around lines 136 - 168, The filename can contain path traversal segments; in
downloadLoraFromUrl sanitize it by extracting only the basename before appending
".gguf" (e.g. use File(filename).name or
Paths.get(filename).fileName.toString()), reject or replace empty basenames with
a safe default, and then use that sanitizedFilename when calling
RunAnywhere.downloadLoraAdapter so downloads cannot escape the intended lora/
directory.
| /** | ||
| * Check if a LoRA adapter is compatible with the currently loaded model. | ||
| * | ||
| * Reads the GGUF metadata header of the LoRA file and compares | ||
| * general.architecture with the loaded model. | ||
| * | ||
| * @param handle Service handle (must have a model loaded) | ||
| * @param lora_path Path to the LoRA adapter GGUF file | ||
| * @param out_error Output: error message if incompatible (caller must free with free()). | ||
| * Set to NULL if compatible. | ||
| * @return RAC_TRUE if compatible, RAC_FALSE if not | ||
| */ | ||
| RAC_LLAMACPP_API rac_bool_t rac_llm_llamacpp_check_lora_compat(rac_handle_t handle, | ||
| const char* lora_path, | ||
| char** out_error); | ||
|
|
||
| /** | ||
| * Read GGUF metadata from a file without loading the model weights. | ||
| * | ||
| * Returns a JSON object containing all scalar metadata key-value pairs | ||
| * and tensor count. Useful for inspecting LoRA adapter properties. | ||
| * | ||
| * @param path Path to the GGUF file | ||
| * @param out_json Output: JSON string (caller must free with free()) | ||
| * @return RAC_SUCCESS or error code | ||
| */ | ||
| RAC_LLAMACPP_API rac_result_t rac_llm_llamacpp_read_gguf_info(const char* path, | ||
| char** out_json); | ||
|
|
There was a problem hiding this comment.
Doc should reference rac_free, not free().
The APIs return strdup’d memory that callers typically release via rac_free in this repo.
Doc fix
- * `@param` out_error Output: error message if incompatible (caller must free with free()).
+ * `@param` out_error Output: error message if incompatible (caller must free with rac_free()).
...
- * `@param` out_json Output: JSON string (caller must free with free())
+ * `@param` out_json Output: JSON string (caller must free with rac_free())📝 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.
| /** | |
| * Check if a LoRA adapter is compatible with the currently loaded model. | |
| * | |
| * Reads the GGUF metadata header of the LoRA file and compares | |
| * general.architecture with the loaded model. | |
| * | |
| * @param handle Service handle (must have a model loaded) | |
| * @param lora_path Path to the LoRA adapter GGUF file | |
| * @param out_error Output: error message if incompatible (caller must free with free()). | |
| * Set to NULL if compatible. | |
| * @return RAC_TRUE if compatible, RAC_FALSE if not | |
| */ | |
| RAC_LLAMACPP_API rac_bool_t rac_llm_llamacpp_check_lora_compat(rac_handle_t handle, | |
| const char* lora_path, | |
| char** out_error); | |
| /** | |
| * Read GGUF metadata from a file without loading the model weights. | |
| * | |
| * Returns a JSON object containing all scalar metadata key-value pairs | |
| * and tensor count. Useful for inspecting LoRA adapter properties. | |
| * | |
| * @param path Path to the GGUF file | |
| * @param out_json Output: JSON string (caller must free with free()) | |
| * @return RAC_SUCCESS or error code | |
| */ | |
| RAC_LLAMACPP_API rac_result_t rac_llm_llamacpp_read_gguf_info(const char* path, | |
| char** out_json); | |
| /** | |
| * Check if a LoRA adapter is compatible with the currently loaded model. | |
| * | |
| * Reads the GGUF metadata header of the LoRA file and compares | |
| * general.architecture with the loaded model. | |
| * | |
| * `@param` handle Service handle (must have a model loaded) | |
| * `@param` lora_path Path to the LoRA adapter GGUF file | |
| * `@param` out_error Output: error message if incompatible (caller must free with rac_free()). | |
| * Set to NULL if compatible. | |
| * `@return` RAC_TRUE if compatible, RAC_FALSE if not | |
| */ | |
| RAC_LLAMACPP_API rac_bool_t rac_llm_llamacpp_check_lora_compat(rac_handle_t handle, | |
| const char* lora_path, | |
| char** out_error); | |
| /** | |
| * Read GGUF metadata from a file without loading the model weights. | |
| * | |
| * Returns a JSON object containing all scalar metadata key-value pairs | |
| * and tensor count. Useful for inspecting LoRA adapter properties. | |
| * | |
| * `@param` path Path to the GGUF file | |
| * `@param` out_json Output: JSON string (caller must free with rac_free()) | |
| * `@return` RAC_SUCCESS or error code | |
| */ | |
| RAC_LLAMACPP_API rac_result_t rac_llm_llamacpp_read_gguf_info(const char* path, | |
| char** out_json); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h` around lines
247 - 275, Update the two function docblocks to instruct callers to use rac_free
instead of free(): change the "caller must free with free()" note in
rac_llm_llamacpp_check_lora_compat and the "caller must free with free()" note
in rac_llm_llamacpp_read_gguf_info to "caller must free with rac_free" so docs
correctly reflect the repository's memory-deallocation API.
| /** | ||
| * @brief Check if a LoRA adapter is compatible with the loaded model | ||
| * | ||
| * Reads GGUF metadata from the LoRA file and compares architecture | ||
| * with the currently loaded model. | ||
| * | ||
| * @param handle Component handle | ||
| * @param lora_path Path to the LoRA adapter GGUF file | ||
| * @param out_error Output: error message if incompatible (caller must free with rac_free). | ||
| * Set to NULL if compatible. | ||
| * @return RAC_SUCCESS if compatible, error code if not | ||
| */ | ||
| RAC_API rac_result_t rac_llm_component_check_lora_compat(rac_handle_t handle, | ||
| const char* lora_path, | ||
| char** out_error); |
There was a problem hiding this comment.
Document explicit error codes and lifecycle requirement.
The new API returns “error code if not,” but doesn’t list expected codes (e.g., component not ready, invalid argument) or state that a model must be loaded.
Doc tweak
- * `@return` RAC_SUCCESS if compatible, error code if not
+ * `@return` RAC_SUCCESS if compatible,
+ * RAC_ERROR_COMPONENT_NOT_READY if no model is loaded,
+ * RAC_ERROR_INVALID_ARGUMENT for null inputs,
+ * RAC_ERROR_VALIDATION_FAILED when incompatible.As per coding guidelines: Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h` around
lines 266 - 280, Update the rac_llm_component_check_lora_compat declaration docs
to explicitly list possible rac_result_t error codes (e.g., RAC_SUCCESS,
RAC_ERROR_INVALID_ARGUMENT, RAC_ERROR_COMPONENT_NOT_READY, RAC_ERROR_IO, etc.),
state the lifecycle requirement that a model must be loaded into the component
before calling this function, and clarify ownership/cleanup for out_error
(caller must free with rac_free when non-NULL); reference the function name
rac_llm_component_check_lora_compat, parameter types rac_handle_t and char**,
return type rac_result_t, and the rac_free helper so callers know expected
states and error handling.
|
|
||
| /** Check LoRA adapter compatibility (optional, NULL if not supported). | ||
| * Returns RAC_SUCCESS if compatible. out_error is set on incompatibility. */ | ||
| rac_result_t (*check_lora_compat)(void* impl, const char* lora_path, char** out_error); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find all references to check_lora_compat
rg -n "check_lora_compat" --type=cpp --type=c --type=h -B 2 -A 2Repository: RunanywhereAI/runanywhere-sdks
Length of output: 8117
🏁 Script executed:
# Find the vtable struct definition to understand the full context
rg -n "rac_llm_service_ops" --type=h -A 30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 15611
🏁 Script executed:
# Search for other out_error parameters to see how they're documented
rg -n "out_error" sdk/runanywhere-commons/include/rac/ --type=h -B 2 -A 1Repository: RunanywhereAI/runanywhere-sdks
Length of output: 3282
🏁 Script executed:
# Check for implementations of LoRA functions to understand memory ownership
rg -n "load_lora|remove_lora|clear_lora" --type=cpp --type=c -A 5Repository: RunanywhereAI/runanywhere-sdks
Length of output: 26665
🏁 Script executed:
# Check the actual implementation to see which free function is used
rg -n "free.*out_error|out_error.*free" sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp -A 2 -B 2Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Check the actual implementation of check_lora_compat to see how out_error is set
sed -n '409,440p' sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1157
🏁 Script executed:
# Check if there's a pattern for memory allocation in the error handling
rg -n "strdup\|malloc\|new" sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
Document memory ownership for out_error parameter.
The vtable operation lacks critical lifecycle documentation present in the corresponding public API headers. According to the coding guideline requiring vtable headers to document lifecycle requirements, the comment should specify that the caller is responsible for freeing *out_error (via free()), and that *out_error is only allocated when the function returns an error code (incompatible). When compatible, *out_error is set to NULL.
Suggested documentation improvement
/** Check LoRA adapter compatibility (optional, NULL if not supported).
- * Returns RAC_SUCCESS if compatible. out_error is set on incompatibility. */
+ * Returns RAC_SUCCESS if compatible.
+ * On incompatibility, returns an error code and sets *out_error to a
+ * heap-allocated string describing the issue (caller must free()).
+ * *out_error is set to NULL when compatible. */
rac_result_t (*check_lora_compat)(void* impl, const char* lora_path, char** out_error);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h` around
lines 64 - 67, Update the comment for the vtable function pointer
check_lora_compat to document out_error ownership: state that when
check_lora_compat(void* impl, const char* lora_path, char** out_error) returns
an error (non-RAC_SUCCESS) it allocates *out_error and the caller is responsible
for freeing it with free(), and when the function returns RAC_SUCCESS it sets
*out_error to NULL (no allocation).
| bool LlamaCppTextGeneration::check_lora_compatibility( | ||
| const std::string& lora_path, std::string& error_message) const { | ||
| if (!model_loaded_ || !model_) { | ||
| error_message = "No model loaded"; | ||
| return false; | ||
| } | ||
|
|
||
| struct gguf_init_params params = { | ||
| /*.no_alloc =*/ true, | ||
| /*.ctx =*/ nullptr, | ||
| }; | ||
|
|
||
| struct gguf_context* lora_ctx = gguf_init_from_file(lora_path.c_str(), params); | ||
| if (!lora_ctx) { | ||
| error_message = "Failed to read LoRA file: " + lora_path; | ||
| return false; | ||
| } | ||
|
|
||
| // Read architecture from LoRA GGUF | ||
| std::string lora_arch; | ||
| int64_t arch_key = gguf_find_key(lora_ctx, "general.architecture"); | ||
| if (arch_key >= 0) { | ||
| lora_arch = gguf_get_val_str(lora_ctx, arch_key); | ||
| } | ||
|
|
||
| gguf_free(lora_ctx); | ||
|
|
||
| // Read architecture from loaded model | ||
| char model_arch_buf[256] = {}; | ||
| int len = llama_model_meta_val_str(model_, "general.architecture", model_arch_buf, sizeof(model_arch_buf)); | ||
| std::string model_arch = (len > 0) ? std::string(model_arch_buf) : ""; | ||
|
|
||
| if (lora_arch.empty()) { | ||
| LOGI("LoRA file has no general.architecture key, skipping architecture check"); | ||
| return true; | ||
| } | ||
|
|
||
| if (model_arch.empty()) { | ||
| LOGI("Loaded model has no general.architecture key, skipping architecture check"); | ||
| return true; | ||
| } | ||
|
|
||
| if (lora_arch != model_arch) { | ||
| error_message = "Architecture mismatch: model=" + model_arch + ", lora=" + lora_arch; | ||
| LOGE("LoRA compatibility check failed: %s", error_message.c_str()); | ||
| return false; | ||
| } | ||
|
|
||
| LOGI("LoRA compatibility check passed: architecture=%s", model_arch.c_str()); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the actual files in the repository
find sdk/runanywhere-commons -type f -name "*.cpp" -o -name "*.h" | grep -E "(llamacpp|llm)" | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 1449
🏁 Script executed:
# Look for C API wrapper files
fd -type f -name "rac_llm*.cpp" sdk/runanywhere-commons/Repository: RunanywhereAI/runanywhere-sdks
Length of output: 244
🏁 Script executed:
# Search for check_lora_compatibility usage
rg -n "check_lora_compat" sdk/runanywhere-commons/Repository: RunanywhereAI/runanywhere-sdks
Length of output: 2327
🏁 Script executed:
# Look at the LlamaCppTextGeneration class definition to find mutex
fd -type f -name "*llamacpp*.h" sdk/runanywhere-commons/ | head -5Repository: RunanywhereAI/runanywhere-sdks
Length of output: 244
🏁 Script executed:
# Examine the C API wrapper function
sed -n '400,440p' sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1418
🏁 Script executed:
# Check the LlamaCppTextGeneration class definition for mutex
cat sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 5760
🏁 Script executed:
# Check other method implementations that access model_ to see locking pattern
rg -n "model_\|mutex" sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp | head -30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Get the full implementation of check_lora_compatibility
sed -n '889,939p' sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1771
🏁 Script executed:
# Check load_model and unload_model implementations to see locking pattern
rg -A 15 "bool LlamaCppTextGeneration::load_model\|bool LlamaCppTextGeneration::unload_model" sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp | head -80Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Check generate method implementation for locking pattern
rg -A 10 "TextGenerationResult LlamaCppTextGeneration::generate" sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp | head -40Repository: RunanywhereAI/runanywhere-sdks
Length of output: 475
🏁 Script executed:
# Check for mutex lock usage in generate and other methods
rg -B 2 -A 10 "std::lock_guard|std::unique_lock" sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp | head -100Repository: RunanywhereAI/runanywhere-sdks
Length of output: 3089
🏁 Script executed:
# Check the complete load_model implementation
sed -n '/^bool LlamaCppTextGeneration::load_model/,/^bool LlamaCpp/p' sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp | head -80Repository: RunanywhereAI/runanywhere-sdks
Length of output: 3636
🏁 Script executed:
# Check all const method implementations that access model_
rg -B 5 -A 15 "bool.*::check_lora_compatibility|is_model_loaded|get_model_info" sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp | head -100Repository: RunanywhereAI/runanywhere-sdks
Length of output: 1920
🏁 Script executed:
# Check line 959 to see what context check_lora_compatibility is called from
sed -n '950,970p' sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 848
🏁 Script executed:
# Check is_model_loaded implementation
sed -n '/^bool LlamaCppTextGeneration::is_model_loaded/,/^}/p' sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 156
🏁 Script executed:
# Check if get_model_info also lacks locking (saw it doesn't lock either)
sed -n '/^nlohmann::json LlamaCppTextGeneration::get_model_info/,/^}/p' sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 739
🏁 Script executed:
# Check the C API wrapper context more thoroughly
sed -n '400,430p' sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1088
🏁 Script executed:
# Check if there's any locking in rac_llm_llamacpp.cpp at all
rg -n "lock_guard|unique_lock|mutex" sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Check the JNI wrapper
cat sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp | sed -n '1190,1210p'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 659
🏁 Script executed:
# Check if there are other const methods that read model_ and whether they lock
rg -B 3 "model_\|model_loaded_" sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp | grep -E "const.*\{|lock_guard|model_loaded_|model_" | head -40Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Look at the full structure of rac_llm_llamacpp_handle_impl to understand what text_gen points to
rg -B 5 -A 10 "rac_llm_llamacpp_handle_impl" sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp | head -50Repository: RunanywhereAI/runanywhere-sdks
Length of output: 1761
🏁 Script executed:
# Double-check: verify if the backend mutex in rac_llm_llamacpp_handle_impl is ever used
rg -n "backend.*mutex\|backend->.*lock" sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Check if any wrappers lock the backend mutex
sed -n '1,100p' sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp | grep -A 5 "rac_llm_llamacpp_create\|extern"Repository: RunanywhereAI/runanywhere-sdks
Length of output: 369
Add mutex locking to check_lora_compatibility() and other const accessors that read model_/model_loaded_.
check_lora_compatibility() reads model_ and model_loaded_ without holding mutex_, creating a race condition with concurrent load_model() and unload_model() calls. The same issue affects is_model_loaded() and get_model_info(). While internal callers like load_lora_adapter() already hold the mutex, the C API wrapper rac_llm_llamacpp_check_lora_compat() has no synchronization mechanism. Wrap these const method bodies with std::lock_guard<std::mutex> lock(mutex_) to prevent use-after-free crashes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp` around
lines 889 - 939, The method check_lora_compatibility reads shared state (model_
and model_loaded_) without synchronization, risking races with
load_model()/unload_model(); similarly is_model_loaded() and get_model_info()
need protection. Fix by adding std::lock_guard<std::mutex> lock(mutex_) at the
start of each of these const methods (check_lora_compatibility, is_model_loaded,
get_model_info) so the body executes while holding mutex_, preventing
use-after-free when the C API wrapper rac_llm_llamacpp_check_lora_compat() is
invoked concurrently.
| * Check if a LoRA adapter file is compatible with the currently loaded model. | ||
| * | ||
| * Reads GGUF metadata from the LoRA file and compares architecture | ||
| * with the loaded model. Does not load the adapter. | ||
| * | ||
| * @param loraPath Path to the LoRA adapter GGUF file | ||
| * @return Compatibility result with isCompatible flag and optional error | ||
| */ | ||
| expect fun RunAnywhere.checkLoraCompatibility(loraPath: String): LoraCompatibilityResult |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find all occurrences of checkLoraCompatibility - declaration, implementations, and call sites
rg -n "checkLoraCompatibility" -B 2 -A 2Repository: RunanywhereAI/runanywhere-sdks
Length of output: 12511
🏁 Script executed:
# Find the actual implementations in platform-specific modules
fd -t f "*.kt" | xargs grep -l "actual.*checkLoraCompatibility" | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 357
🏁 Script executed:
# Check the structure of the RunAnywhere+LoRA.kt file to understand context
cat -n sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.kt | head -100Repository: RunanywhereAI/runanywhere-sdks
Length of output: 2701
Make checkLoraCompatibility a suspend function for consistency with other LoRA functions and to properly indicate blocking I/O.
The function performs GGUF metadata file reading via JNI and uses synchronized(lock) blocking calls. All four other LoRA functions in the same API (loadLoraAdapter, removeLoraAdapter, clearLoraAdapters, getLoadedLoraAdapters) are already suspend. Additionally, both call sites wrap this function in withContext(Dispatchers.IO), confirming it needs async handling even though the operation is relatively lightweight.
Signature update
-expect fun RunAnywhere.checkLoraCompatibility(loraPath: String): LoraCompatibilityResult
+expect suspend fun RunAnywhere.checkLoraCompatibility(loraPath: String): LoraCompatibilityResult📝 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.
| * Check if a LoRA adapter file is compatible with the currently loaded model. | |
| * | |
| * Reads GGUF metadata from the LoRA file and compares architecture | |
| * with the loaded model. Does not load the adapter. | |
| * | |
| * @param loraPath Path to the LoRA adapter GGUF file | |
| * @return Compatibility result with isCompatible flag and optional error | |
| */ | |
| expect fun RunAnywhere.checkLoraCompatibility(loraPath: String): LoraCompatibilityResult | |
| * Check if a LoRA adapter file is compatible with the currently loaded model. | |
| * | |
| * Reads GGUF metadata from the LoRA file and compares architecture | |
| * with the loaded model. Does not load the adapter. | |
| * | |
| * `@param` loraPath Path to the LoRA adapter GGUF file | |
| * `@return` Compatibility result with isCompatible flag and optional error | |
| */ | |
| expect suspend fun RunAnywhere.checkLoraCompatibility(loraPath: String): LoraCompatibilityResult |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.kt
around lines 61 - 69, Make the API function suspend: change the declaration of
checkLoraCompatibility to "suspend fun
RunAnywhere.checkLoraCompatibility(loraPath: String): LoraCompatibilityResult"
and update all platform-specific actual implementations accordingly (the
JVM/native implementations that call JNI and use synchronized(lock)). Update any
callers (or ensure they still wrap with withContext(Dispatchers.IO)) to match
the new suspend signature. Keep the function body logic unchanged (GGUF metadata
read, architecture compare) but mark it suspend so blocking JNI/synchronized
calls are clearly asynchronous.
| actual fun RunAnywhere.downloadLoraAdapter(url: String, filename: String): Flow<LoraDownloadProgress> = | ||
| callbackFlow { | ||
| trySend( | ||
| LoraDownloadProgress( | ||
| progress = 0f, | ||
| bytesDownloaded = 0, | ||
| totalBytes = null, | ||
| state = LoraDownloadState.PENDING, | ||
| ), | ||
| ) | ||
|
|
||
| try { | ||
| val destDir = File(CppBridgeModelPaths.getModelsDirectory(), "lora") | ||
|
|
||
| val localPath = withContext(Dispatchers.IO) { | ||
| destDir.mkdirs() | ||
|
|
||
| val destFile = File(destDir, filename) | ||
| val tempFile = File(destDir, "$filename.tmp") | ||
|
|
||
| loraDownloadLogger.info("Downloading LoRA: $url -> ${destFile.absolutePath}") | ||
|
|
||
| val connection = URL(url).openConnection() as HttpURLConnection | ||
| connection.connectTimeout = 30_000 | ||
| connection.readTimeout = 60_000 | ||
| connection.setRequestProperty("User-Agent", "RunAnywhere-SDK/Kotlin") | ||
|
|
||
| try { | ||
| connection.connect() | ||
|
|
||
| val responseCode = connection.responseCode | ||
| if (responseCode != HttpURLConnection.HTTP_OK) { | ||
| throw Exception("HTTP error: $responseCode") | ||
| } | ||
|
|
||
| val totalBytes = connection.contentLengthLong | ||
|
|
||
| trySend( | ||
| LoraDownloadProgress( | ||
| progress = 0f, | ||
| bytesDownloaded = 0, | ||
| totalBytes = if (totalBytes > 0) totalBytes else null, | ||
| state = LoraDownloadState.DOWNLOADING, | ||
| ), | ||
| ) | ||
|
|
||
| connection.inputStream.use { input -> | ||
| FileOutputStream(tempFile).use { output -> | ||
| val buffer = ByteArray(8192) | ||
| var bytesDownloaded = 0L | ||
| var lastProgressTime = System.currentTimeMillis() | ||
| var bytesRead: Int | ||
|
|
||
| while (input.read(buffer).also { bytesRead = it } != -1) { | ||
| output.write(buffer, 0, bytesRead) | ||
| bytesDownloaded += bytesRead | ||
|
|
||
| val now = System.currentTimeMillis() | ||
| if (now - lastProgressTime >= 150) { | ||
| lastProgressTime = now | ||
| val progress = if (totalBytes > 0) { | ||
| bytesDownloaded.toFloat() / totalBytes | ||
| } else { | ||
| 0f | ||
| } | ||
|
|
||
| trySend( | ||
| LoraDownloadProgress( | ||
| progress = progress, | ||
| bytesDownloaded = bytesDownloaded, | ||
| totalBytes = if (totalBytes > 0) totalBytes else null, | ||
| state = LoraDownloadState.DOWNLOADING, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Move temp to final | ||
| if (destFile.exists()) destFile.delete() | ||
| if (!tempFile.renameTo(destFile)) { | ||
| tempFile.copyTo(destFile, overwrite = true) | ||
| tempFile.delete() | ||
| } | ||
|
|
||
| loraDownloadLogger.info("LoRA downloaded: ${destFile.absolutePath} (${destFile.length()} bytes)") | ||
| destFile.absolutePath | ||
| } finally { | ||
| connection.disconnect() | ||
| } | ||
| } | ||
|
|
||
| trySend( | ||
| LoraDownloadProgress( | ||
| progress = 1f, | ||
| bytesDownloaded = File(localPath).length(), | ||
| totalBytes = File(localPath).length(), | ||
| state = LoraDownloadState.COMPLETED, | ||
| localPath = localPath, | ||
| ), | ||
| ) | ||
|
|
||
| close() | ||
| } catch (e: Exception) { | ||
| loraDownloadLogger.error("LoRA download failed: ${e.message}") | ||
| trySend( | ||
| LoraDownloadProgress( | ||
| progress = 0f, | ||
| bytesDownloaded = 0, | ||
| totalBytes = null, | ||
| state = LoraDownloadState.ERROR, | ||
| error = e.message ?: "Download failed", | ||
| ), | ||
| ) | ||
| close(e) | ||
| } |
There was a problem hiding this comment.
Sanitize filename to prevent path traversal and enforce .gguf.
filename is used directly in File(destDir, filename), so values like ../foo can escape the lora/ directory and overwrite arbitrary files. Please normalize to a basename and enforce the extension before constructing the paths.
🔒 Proposed fix
- val destFile = File(destDir, filename)
- val tempFile = File(destDir, "$filename.tmp")
+ val safeName = File(filename).name
+ val finalName = if (safeName.endsWith(".gguf")) safeName else "$safeName.gguf"
+ val destFile = File(destDir, finalName)
+ val tempFile = File(destDir, "$finalName.tmp")📝 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.
| actual fun RunAnywhere.downloadLoraAdapter(url: String, filename: String): Flow<LoraDownloadProgress> = | |
| callbackFlow { | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = 0f, | |
| bytesDownloaded = 0, | |
| totalBytes = null, | |
| state = LoraDownloadState.PENDING, | |
| ), | |
| ) | |
| try { | |
| val destDir = File(CppBridgeModelPaths.getModelsDirectory(), "lora") | |
| val localPath = withContext(Dispatchers.IO) { | |
| destDir.mkdirs() | |
| val destFile = File(destDir, filename) | |
| val tempFile = File(destDir, "$filename.tmp") | |
| loraDownloadLogger.info("Downloading LoRA: $url -> ${destFile.absolutePath}") | |
| val connection = URL(url).openConnection() as HttpURLConnection | |
| connection.connectTimeout = 30_000 | |
| connection.readTimeout = 60_000 | |
| connection.setRequestProperty("User-Agent", "RunAnywhere-SDK/Kotlin") | |
| try { | |
| connection.connect() | |
| val responseCode = connection.responseCode | |
| if (responseCode != HttpURLConnection.HTTP_OK) { | |
| throw Exception("HTTP error: $responseCode") | |
| } | |
| val totalBytes = connection.contentLengthLong | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = 0f, | |
| bytesDownloaded = 0, | |
| totalBytes = if (totalBytes > 0) totalBytes else null, | |
| state = LoraDownloadState.DOWNLOADING, | |
| ), | |
| ) | |
| connection.inputStream.use { input -> | |
| FileOutputStream(tempFile).use { output -> | |
| val buffer = ByteArray(8192) | |
| var bytesDownloaded = 0L | |
| var lastProgressTime = System.currentTimeMillis() | |
| var bytesRead: Int | |
| while (input.read(buffer).also { bytesRead = it } != -1) { | |
| output.write(buffer, 0, bytesRead) | |
| bytesDownloaded += bytesRead | |
| val now = System.currentTimeMillis() | |
| if (now - lastProgressTime >= 150) { | |
| lastProgressTime = now | |
| val progress = if (totalBytes > 0) { | |
| bytesDownloaded.toFloat() / totalBytes | |
| } else { | |
| 0f | |
| } | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = progress, | |
| bytesDownloaded = bytesDownloaded, | |
| totalBytes = if (totalBytes > 0) totalBytes else null, | |
| state = LoraDownloadState.DOWNLOADING, | |
| ), | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| // Move temp to final | |
| if (destFile.exists()) destFile.delete() | |
| if (!tempFile.renameTo(destFile)) { | |
| tempFile.copyTo(destFile, overwrite = true) | |
| tempFile.delete() | |
| } | |
| loraDownloadLogger.info("LoRA downloaded: ${destFile.absolutePath} (${destFile.length()} bytes)") | |
| destFile.absolutePath | |
| } finally { | |
| connection.disconnect() | |
| } | |
| } | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = 1f, | |
| bytesDownloaded = File(localPath).length(), | |
| totalBytes = File(localPath).length(), | |
| state = LoraDownloadState.COMPLETED, | |
| localPath = localPath, | |
| ), | |
| ) | |
| close() | |
| } catch (e: Exception) { | |
| loraDownloadLogger.error("LoRA download failed: ${e.message}") | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = 0f, | |
| bytesDownloaded = 0, | |
| totalBytes = null, | |
| state = LoraDownloadState.ERROR, | |
| error = e.message ?: "Download failed", | |
| ), | |
| ) | |
| close(e) | |
| } | |
| actual fun RunAnywhere.downloadLoraAdapter(url: String, filename: String): Flow<LoraDownloadProgress> = | |
| callbackFlow { | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = 0f, | |
| bytesDownloaded = 0, | |
| totalBytes = null, | |
| state = LoraDownloadState.PENDING, | |
| ), | |
| ) | |
| try { | |
| val destDir = File(CppBridgeModelPaths.getModelsDirectory(), "lora") | |
| val localPath = withContext(Dispatchers.IO) { | |
| destDir.mkdirs() | |
| val safeName = File(filename).name | |
| val finalName = if (safeName.endsWith(".gguf")) safeName else "$safeName.gguf" | |
| val destFile = File(destDir, finalName) | |
| val tempFile = File(destDir, "$finalName.tmp") | |
| loraDownloadLogger.info("Downloading LoRA: $url -> ${destFile.absolutePath}") | |
| val connection = URL(url).openConnection() as HttpURLConnection | |
| connection.connectTimeout = 30_000 | |
| connection.readTimeout = 60_000 | |
| connection.setRequestProperty("User-Agent", "RunAnywhere-SDK/Kotlin") | |
| try { | |
| connection.connect() | |
| val responseCode = connection.responseCode | |
| if (responseCode != HttpURLConnection.HTTP_OK) { | |
| throw Exception("HTTP error: $responseCode") | |
| } | |
| val totalBytes = connection.contentLengthLong | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = 0f, | |
| bytesDownloaded = 0, | |
| totalBytes = if (totalBytes > 0) totalBytes else null, | |
| state = LoraDownloadState.DOWNLOADING, | |
| ), | |
| ) | |
| connection.inputStream.use { input -> | |
| FileOutputStream(tempFile).use { output -> | |
| val buffer = ByteArray(8192) | |
| var bytesDownloaded = 0L | |
| var lastProgressTime = System.currentTimeMillis() | |
| var bytesRead: Int | |
| while (input.read(buffer).also { bytesRead = it } != -1) { | |
| output.write(buffer, 0, bytesRead) | |
| bytesDownloaded += bytesRead | |
| val now = System.currentTimeMillis() | |
| if (now - lastProgressTime >= 150) { | |
| lastProgressTime = now | |
| val progress = if (totalBytes > 0) { | |
| bytesDownloaded.toFloat() / totalBytes | |
| } else { | |
| 0f | |
| } | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = progress, | |
| bytesDownloaded = bytesDownloaded, | |
| totalBytes = if (totalBytes > 0) totalBytes else null, | |
| state = LoraDownloadState.DOWNLOADING, | |
| ), | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| // Move temp to final | |
| if (destFile.exists()) destFile.delete() | |
| if (!tempFile.renameTo(destFile)) { | |
| tempFile.copyTo(destFile, overwrite = true) | |
| tempFile.delete() | |
| } | |
| loraDownloadLogger.info("LoRA downloaded: ${destFile.absolutePath} (${destFile.length()} bytes)") | |
| destFile.absolutePath | |
| } finally { | |
| connection.disconnect() | |
| } | |
| } | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = 1f, | |
| bytesDownloaded = File(localPath).length(), | |
| totalBytes = File(localPath).length(), | |
| state = LoraDownloadState.COMPLETED, | |
| localPath = localPath, | |
| ), | |
| ) | |
| close() | |
| } catch (e: Exception) { | |
| loraDownloadLogger.error("LoRA download failed: ${e.message}") | |
| trySend( | |
| LoraDownloadProgress( | |
| progress = 0f, | |
| bytesDownloaded = 0, | |
| totalBytes = null, | |
| state = LoraDownloadState.ERROR, | |
| error = e.message ?: "Download failed", | |
| ), | |
| ) | |
| close(e) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.jvmAndroid.kt
around lines 26 - 142, The download uses filename directly which allows path
traversal and arbitrary extensions; in RunAnywhere.downloadLoraAdapter sanitize
filename by deriving a safe basename (strip any directory components, reject or
replace path separators), ensure it is non-empty, and enforce the .gguf
extension (append or replace other extensions) before creating destFile and
tempFile; use this safeFilename variable in the File(destDir, safeFilename) and
File(destDir, "$safeFilename.tmp") calls and validate the resulting path stays
under destDir.
LoRA functionality has been integrated into the main RunAnywhereAI Android example app instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| "Translate English to French:\nI would like to order a coffee please.\n\nFrench:", | ||
| ), | ||
| ), | ||
| "uncensored-chat-lora" to LoraPromptTemplate( |
There was a problem hiding this comment.
are we displaying these anywhere within the Ui so the user knows that these are th set of prompts applied ?
There was a problem hiding this comment.
Not yet, do u want me to display them ?
07700f2 to
c0dac5d
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt (1)
747-813:⚠️ Potential issue | 🟡 MinorAvoid hard‑coded model name heuristics for LoRA capability.
isLoraCompatibleModelis derived fromnameLower.contains("lfm2") && nameLower.contains("350m"), which risks false negatives/positives as model names evolve. Prefer a capability flag from the SDK, model metadata, or an explicit allowlist in config so LoRA UI isn’t unintentionally hidden.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt` around lines 747 - 813, The current heuristic computing isLoraCompatibleModel (nameLower.contains("lfm2") && nameLower.contains("350m")) is brittle; replace it with a robust capability check by reading the model's metadata or an SDK-provided flag (e.g., use RunAnywhere.availableModels() -> model.capabilities or a hypothetical RunAnywhere.modelSupportsLora(model) call) or fall back to a configurable allowlist, and update every place that sets isLoraCompatibleModel (including checkModelStatus, the loaded-model branch using RunAnywhere.currentLLMModel()/currentLLMModelId, and the chatModel branch) to call a single helper like isModelLoraCompatible(model) instead of string matching; ensure loadLoraCatalog() and scanDownloadedAdapters() are only invoked when that helper returns true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.kt`:
- Around line 252-261: The LoRA capability check is using a brittle name-string
heuristic; update the model data and mapping to be data-driven: add a
supportsLora: Boolean property to the ModelInfo type in the SDK (default false)
and populate it from the catalog, then change the conversion in
ModelSelectionBottomSheet.toAIModel (where AIModel(...) is constructed from m)
to set isDownloaded as before but set supportsLora = m.supportsLora (or, if you
need a transitional fallback, check m.id against a maintained Set of
knownLoRACompatibleIds instead of using nameLower.contains(...)). Ensure
references: ModelInfo, toAIModel, AIModel, ModelSelectionBottomSheet, and
supportsLora are updated accordingly.
In `@sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp`:
- Around line 409-447: Both functions rac_llm_llamacpp_check_lora_compat and
rac_llm_llamacpp_read_gguf_info currently return allocated raw C strings via
char**/strdup which violates the API guideline; change their signatures and
implementations to use the project’s structured string/buffer type (the
library-wide string/buffer wrapper used for other C APIs), e.g., replace char**
out_error and char** out_json with that structured output type, populate the
structured object with the error or JSON payload and its length (do not call
strdup), and return appropriate rac_result_t/RAC_TRUE/RAC_FALSE values; update
uses of h->text_gen->check_lora_compatibility and
runanywhere::LlamaCppTextGeneration::read_gguf_metadata to fill the structured
buffer and use rac_error_set_details only for setting error metadata, ensuring
ownership and lifecycle are explicit and callers receive the structured buffer
API instead of raw strings.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.kt:
- Around line 12-13: Public APIs availableLoraAdapters() and
downloadLoraFromCatalog() currently expose
com.runanywhere.sdk.temp.LoraAdapterEntry and LoraAdapterCatalog; move these
types out of the temp package into a stable package (e.g.,
com.runanywhere.sdk.public.extensions), update their package declarations and
all imports/references (including the signatures of availableLoraAdapters and
downloadLoraFromCatalog) to the new package, and update any module/export
configuration and docs to expose the new stable package instead of
com.runanywhere.sdk.temp so consumers no longer depend on the temporary package.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.jvmAndroid.kt:
- Around line 79-141: The catch-all exception handler currently swallows
CancellationException and must rethrow it, and the blocking IO loop around
input.read(buffer) must be made cancellation-cooperative. Change the catch block
that reads "catch (e: Exception)" to rethrow if (e is CancellationException) and
only handle/log non-cancellation errors (so the LoraDownloadState.ERROR send and
log are skipped on cancellation). Replace the direct blocking input.read(buffer)
loop (the code that runs inside withContext(Dispatchers.IO) and calls
input.read(buffer).also { bytesRead = it }) with a cancellation-aware read:
perform the blocking read inside a suspendCancellableCoroutine (or otherwise
arrange a background IO task) and register invokeOnCancellation to close the
connection/input stream so the blocking read is interrupted; after each
successful read check coroutineContext.isActive/ensureActive and then emit
progress via trySend as before. Use the existing symbols input.read(buffer),
withContext(Dispatchers.IO), trySend(LoraDownloadProgress...), and the catch
block around the download to locate and implement these changes.
---
Outside diff comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt`:
- Around line 747-813: The current heuristic computing isLoraCompatibleModel
(nameLower.contains("lfm2") && nameLower.contains("350m")) is brittle; replace
it with a robust capability check by reading the model's metadata or an
SDK-provided flag (e.g., use RunAnywhere.availableModels() -> model.capabilities
or a hypothetical RunAnywhere.modelSupportsLora(model) call) or fall back to a
configurable allowlist, and update every place that sets isLoraCompatibleModel
(including checkModelStatus, the loaded-model branch using
RunAnywhere.currentLLMModel()/currentLLMModelId, and the chatModel branch) to
call a single helper like isModelLoraCompatible(model) instead of string
matching; ensure loadLoraCatalog() and scanDownloadedAdapters() are only invoked
when that helper returns true.
---
Duplicate comments:
In @.idea/vcs.xml:
- Around line 5-16: The .idea/vcs.xml contains machine-local VCS mappings for
CMake _deps and local emsdk that should be removed; edit .idea/vcs.xml to delete
the specific <mapping> entries that reference directories containing "_deps/"
(e.g., any mapping with paths like "build/.../_deps/llamacpp-src",
"_deps/nlohmann_json-src", "_deps/stable_diffusion_cpp-src", "ggml",
"whispercpp-src") and the "sdk/runanywhere-web/emsdk" mapping, leaving only the
root <mapping directory=""/> entry; additionally, ensure .idea/vcs.xml is
ignored (add .idea/vcs.xml to .gitignore or remove any negation rule like
"!.idea/vcs.xml") so these IDE-specific mappings aren’t committed again.
In `@docs/impl/lora_adapter_support.md`:
- Around line 720-749: Add proper fenced-code block language markers and correct
the typo "cal"→"call": update the first block that documents the compatibility
check (the one starting with "1. Kotlin calls
RunAnywhere.checkLoraCompatibility(path)" and the block describing the download
flow that starts with "1. RunAnywhere.downloadLoraAdapter(url, filename) returns
Flow<LoraDownloadProgress") to use explicit language tags (e.g., ```text or
```markdown) for both fenced blocks, and change the sentence "safe to cal
repeatedly" to "safe to call repeatedly"; apply the same fenced-block language
fix to the other occurrence of the download block later in the file (the second
RunAnywhere.downloadLoraAdapter block).
In `@sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h`:
- Around line 266-280: Update the doc comment for
rac_llm_component_check_lora_compat to explicitly list possible returned
rac_result_t error codes (e.g., RAC_SUCCESS, RAC_ERROR_INVALID_ARGUMENT,
RAC_ERROR_INCOMPATIBLE_ARCH, RAC_ERROR_IO, etc.), describe required
vtable/component operations that must be present or initialized before calling
(reference the component handle / vtable methods used by
rac_llm_component_check_lora_compat), and state lifecycle prerequisites (e.g.,
model must be loaded, component initialized, and ownership/validity of lora_path
and out_error). Also mention that out_error, when non-NULL, must be freed with
rac_free (already referenced) and clarify caller vs callee responsibility for
allocation and error string semantics.
In `@sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp`:
- Around line 889-939: check_lora_compatibility reads model_loaded_ and model_
without synchronization and can race with load_model()/unload_model(); fix by
acquiring the backend/model mutex at the start of check_lora_compatibility
(e.g., add std::lock_guard<std::mutex> lock(model_mutex_); or the existing mutex
that load_model()/unload_model() use) so all accesses to model_loaded_ and
model_ inside this method are protected until the function returns.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.kt:
- Around line 59-69: The declaration for checkLoraCompatibility should be made
suspend like the other LoRA APIs: change expect fun
RunAnywhere.checkLoraCompatibility(loraPath: String): LoraCompatibilityResult to
expect suspend fun RunAnywhere.checkLoraCompatibility(loraPath: String):
LoraCompatibilityResult, and update the JVM/Android actual implementation (the
actual in RunAnywhere+LoRA.jvmAndroid.kt) to wrap the blocking JNI call
(CppBridgeLLM.checkLoraCompatibility) inside withContext(Dispatchers.IO) so the
GGUF metadata I/O runs on the IO dispatcher and returns a
LoraCompatibilityResult.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRA.jvmAndroid.kt:
- Around line 85-96: Change the synchronous actual implementation of
RunAnywhere.checkLoraCompatibility to match the expect declaration by making it
suspend and offloading the blocking JNI/file IO to a background dispatcher;
update the expect declaration in RunAnywhere+LoRA.kt first if it’s still
non-suspending, then implement the actual function (in
RunAnywhere+LoRA.jvmAndroid.kt) as suspend and call
CppBridgeLLM.checkLoraCompatibility inside a withContext(Dispatchers.IO) (or
equivalent coroutine dispatcher) before returning the LoraCompatibilityResult so
the JNI call and GGUF file reads do not block the caller thread.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.jvmAndroid.kt:
- Around line 43-44: The code uses the raw filename to build destFile and
tempFile (val destFile = File(destDir, filename) and val tempFile =
File(destDir, "$filename.tmp"), leaving the code vulnerable to path traversal;
sanitize and normalize the incoming filename before constructing files by
extracting only the base name (reject or strip any directory components and path
separators), validate it does not equal "." or ".." and does not contain
separators or null bytes, and then use that safeName when creating
destFile/tempFile (also resolve and validate the resulting File is within
destDir via canonical path checks) to prevent escaping the intended target
directory.
| val nameLower = m.name.lowercase() | ||
| return AIModel( | ||
| name = m.name, | ||
| logoResId = getModelLogoResId(m), | ||
| format = formatStr, | ||
| formatColor = formatColor, | ||
| size = sizeStr, | ||
| isDownloaded = m.isDownloaded || m.framework == InferenceFramework.FOUNDATION_MODELS || m.framework == InferenceFramework.SYSTEM_TTS, | ||
| supportsLora = nameLower.contains("lfm2") && nameLower.contains("350m"), | ||
| ) |
There was a problem hiding this comment.
LoRA capability determined by hardcoded name-string heuristic — will silently miss future models.
nameLower.contains("lfm2") && nameLower.contains("350m") matches only one specific model variant. Any new LoRA-compatible model added to the catalog, or a model rename, will silently produce no badge. The capability flag should be data-driven — either a supportsLora: Boolean field on ModelInfo populated by the SDK/catalog, or a set of known-compatible model IDs. As per coding guidelines, structured types should be used instead of ad-hoc string checks.
♻️ Suggested direction
In ModelInfo, expose:
// In sdk/runanywhere-kotlin commonMain
val supportsLora: Boolean = falseThen in toAIModel:
- val nameLower = m.name.lowercase()
return AIModel(
...
- supportsLora = nameLower.contains("lfm2") && nameLower.contains("350m"),
+ supportsLora = m.supportsLora,
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.kt`
around lines 252 - 261, The LoRA capability check is using a brittle name-string
heuristic; update the model data and mapping to be data-driven: add a
supportsLora: Boolean property to the ModelInfo type in the SDK (default false)
and populate it from the catalog, then change the conversion in
ModelSelectionBottomSheet.toAIModel (where AIModel(...) is constructed from m)
to set isDownloaded as before but set supportsLora = m.supportsLora (or, if you
need a transitional fallback, check m.id against a maintained Set of
knownLoRACompatibleIds instead of using nameLower.contains(...)). Ensure
references: ModelInfo, toAIModel, AIModel, ModelSelectionBottomSheet, and
supportsLora are updated accordingly.
| rac_bool_t rac_llm_llamacpp_check_lora_compat(rac_handle_t handle, | ||
| const char* lora_path, | ||
| char** out_error) { | ||
| if (handle == nullptr || lora_path == nullptr || out_error == nullptr) { | ||
| return RAC_FALSE; | ||
| } | ||
|
|
||
| *out_error = nullptr; | ||
|
|
||
| auto* h = static_cast<rac_llm_llamacpp_handle_impl*>(handle); | ||
| if (!h->text_gen) { | ||
| *out_error = strdup("No text generation component"); | ||
| return RAC_FALSE; | ||
| } | ||
|
|
||
| std::string error_message; | ||
| bool compatible = h->text_gen->check_lora_compatibility(lora_path, error_message); | ||
|
|
||
| if (!compatible && !error_message.empty()) { | ||
| *out_error = strdup(error_message.c_str()); | ||
| } | ||
|
|
||
| return compatible ? RAC_TRUE : RAC_FALSE; | ||
| } | ||
|
|
||
| rac_result_t rac_llm_llamacpp_read_gguf_info(const char* path, char** out_json) { | ||
| if (path == nullptr || out_json == nullptr) { | ||
| return RAC_ERROR_NULL_POINTER; | ||
| } | ||
|
|
||
| auto info = runanywhere::LlamaCppTextGeneration::read_gguf_metadata(path); | ||
| if (info.empty()) { | ||
| rac_error_set_details("Failed to read GGUF metadata"); | ||
| return RAC_ERROR_MODEL_LOAD_FAILED; | ||
| } | ||
|
|
||
| std::string json_str = info.dump(); | ||
| *out_json = strdup(json_str.c_str()); | ||
|
|
There was a problem hiding this comment.
Use structured return types instead of raw C strings.
These new C API functions expose char** outputs and allocate via strdup, which violates the repo requirement to avoid raw strings in commons C/C++ APIs. Please switch to the project’s structured string/buffer type (or introduce one) so ownership, length, and lifecycle are explicit.
As per coding guidelines: “Always use structured types, never use strings directly for consistency and scalability”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp` around
lines 409 - 447, Both functions rac_llm_llamacpp_check_lora_compat and
rac_llm_llamacpp_read_gguf_info currently return allocated raw C strings via
char**/strdup which violates the API guideline; change their signatures and
implementations to use the project’s structured string/buffer type (the
library-wide string/buffer wrapper used for other C APIs), e.g., replace char**
out_error and char** out_json with that structured output type, populate the
structured object with the error or JSON payload and its length (do not call
strdup), and return appropriate rac_result_t/RAC_TRUE/RAC_FALSE values; update
uses of h->text_gen->check_lora_compatibility and
runanywhere::LlamaCppTextGeneration::read_gguf_metadata to fill the structured
buffer and use rac_error_set_details only for setting error metadata, ensuring
ownership and lifecycle are explicit and callers receive the structured buffer
API instead of raw strings.
| import com.runanywhere.sdk.temp.LoraAdapterCatalog | ||
| import com.runanywhere.sdk.temp.LoraAdapterEntry |
There was a problem hiding this comment.
Public API surface depends on temp package types.
LoraAdapterEntry from com.runanywhere.sdk.temp appears in two public function signatures (availableLoraAdapters() and downloadLoraFromCatalog()). Once this ships as part of the SDK, consumers depend on com.runanywhere.sdk.temp.LoraAdapterEntry, making the temp package sticky regardless of its intended temporary status. The type and its catalog should be promoted to a stable package (e.g., com.runanywhere.sdk.public.extensions) before this API is released.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.kt
around lines 12 - 13, Public APIs availableLoraAdapters() and
downloadLoraFromCatalog() currently expose
com.runanywhere.sdk.temp.LoraAdapterEntry and LoraAdapterCatalog; move these
types out of the temp package into a stable package (e.g.,
com.runanywhere.sdk.public.extensions), update their package declarations and
all imports/references (including the signatures of availableLoraAdapters and
downloadLoraFromCatalog) to the new package, and update any module/export
configuration and docs to expose the new stable package instead of
com.runanywhere.sdk.temp so consumers no longer depend on the temporary package.
| while (input.read(buffer).also { bytesRead = it } != -1) { | ||
| output.write(buffer, 0, bytesRead) | ||
| bytesDownloaded += bytesRead | ||
|
|
||
| val now = System.currentTimeMillis() | ||
| if (now - lastProgressTime >= 150) { | ||
| lastProgressTime = now | ||
| val progress = if (totalBytes > 0) { | ||
| bytesDownloaded.toFloat() / totalBytes | ||
| } else { | ||
| 0f | ||
| } | ||
|
|
||
| trySend( | ||
| LoraDownloadProgress( | ||
| progress = progress, | ||
| bytesDownloaded = bytesDownloaded, | ||
| totalBytes = if (totalBytes > 0) totalBytes else null, | ||
| state = LoraDownloadState.DOWNLOADING, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Move temp to final | ||
| if (destFile.exists()) destFile.delete() | ||
| if (!tempFile.renameTo(destFile)) { | ||
| tempFile.copyTo(destFile, overwrite = true) | ||
| tempFile.delete() | ||
| } | ||
|
|
||
| loraDownloadLogger.info("LoRA downloaded: ${destFile.absolutePath} (${destFile.length()} bytes)") | ||
| destFile.absolutePath | ||
| } finally { | ||
| connection.disconnect() | ||
| } | ||
| } | ||
|
|
||
| trySend( | ||
| LoraDownloadProgress( | ||
| progress = 1f, | ||
| bytesDownloaded = File(localPath).length(), | ||
| totalBytes = File(localPath).length(), | ||
| state = LoraDownloadState.COMPLETED, | ||
| localPath = localPath, | ||
| ), | ||
| ) | ||
|
|
||
| close() | ||
| } catch (e: Exception) { | ||
| loraDownloadLogger.error("LoRA download failed: ${e.message}") | ||
| trySend( | ||
| LoraDownloadProgress( | ||
| progress = 0f, | ||
| bytesDownloaded = 0, | ||
| totalBytes = null, | ||
| state = LoraDownloadState.ERROR, | ||
| error = e.message ?: "Download failed", | ||
| ), | ||
| ) | ||
| close(e) |
There was a problem hiding this comment.
CancellationException swallowed, and IO loop is not cancellation-cooperative.
Two related issues:
-
Swallowed cancellation (lines 130–141):
CancellationExceptionis a subtype ofException. When the flow collector cancels,withContext(Dispatchers.IO)throwsCancellationException, which is caught here, emitting a spuriousERRORstate and logging a false "LoRA download failed" error. As a rule of thumb,CancellationExceptionshould always be rethrown, even if all other exceptions are caught. -
Non-cooperative IO loop (lines 79–101): The
whileloop performs blocking Java IO with noisActiveguard. When the coroutine is cancelled, the coroutine continues running until it reaches a suspension point where the suspending function checks whether it has been canceled. Becauseinput.read(buffer)is a plain blocking call with no suspension points, the download thread continues consuming network resources until the 60-second read timeout fires.
🐛 Proposed fix
while (input.read(buffer).also { bytesRead = it } != -1) {
+ if (!isActive) break
output.write(buffer, 0, bytesRead)+ } catch (e: CancellationException) {
+ throw e // propagate cancellation; don't treat it as a download failure
} catch (e: Exception) {
loraDownloadLogger.error("LoRA download failed: ${e.message}")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere`+LoRADownload.jvmAndroid.kt
around lines 79 - 141, The catch-all exception handler currently swallows
CancellationException and must rethrow it, and the blocking IO loop around
input.read(buffer) must be made cancellation-cooperative. Change the catch block
that reads "catch (e: Exception)" to rethrow if (e is CancellationException) and
only handle/log non-cancellation errors (so the LoraDownloadState.ERROR send and
log are skipped on cancellation). Replace the direct blocking input.read(buffer)
loop (the code that runs inside withContext(Dispatchers.IO) and calls
input.read(buffer).also { bytesRead = it }) with a cancellation-aware read:
perform the blocking read inside a suspendCancellableCoroutine (or otherwise
arrange a background IO task) and register invokeOnCancellation to close the
connection/input stream so the blocking read is interrupted; after each
successful read check coroutineContext.isActive/ensureActive and then emit
progress via trySend as before. Use the existing symbols input.read(buffer),
withContext(Dispatchers.IO), trySend(LoraDownloadProgress...), and the catch
block around the download to locate and implement these changes.
c0dac5d to
ba9e2ef
Compare
| name: Publish to Maven Central | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Set up JDK | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: ${{ env.JAVA_VERSION }} | ||
| distribution: ${{ env.JAVA_DISTRIBUTION }} | ||
|
|
||
| - name: Setup Gradle | ||
| uses: gradle/gradle-build-action@v3 | ||
| with: | ||
| gradle-version: 8.13 | ||
|
|
||
| - name: Set up Android SDK | ||
| uses: android-actions/setup-android@v3 | ||
| with: | ||
| cmdline-tools-version: 11076708 | ||
| packages: platform-tools platforms;android-35 build-tools;35.0.0 ndk;27.0.12077973 | ||
|
|
||
| - name: Download native libraries | ||
| working-directory: sdk/runanywhere-kotlin | ||
| run: | | ||
| chmod +x gradlew | ||
| ./gradlew downloadJniLibs -Prunanywhere.nativeLibVersion=v${{ inputs.version }} | ||
| env: | ||
| SDK_VERSION: ${{ inputs.version }} | ||
|
|
||
| - name: Import GPG key | ||
| run: | | ||
| echo "${{ secrets.GPG_SIGNING_KEY }}" | gpg --batch --import | ||
| # Get the key fingerprint and set trust | ||
| KEY_FP=$(gpg --list-secret-keys --keyid-format LONG | grep -A1 "sec" | tail -1 | tr -d ' ') | ||
| echo "${KEY_FP}:6:" | gpg --import-ownertrust || true | ||
| # List imported keys for debugging | ||
| gpg --list-secret-keys --keyid-format LONG | ||
|
|
||
| - name: Configure Gradle for GPG signing | ||
| run: | | ||
| mkdir -p ~/.gradle | ||
| cat >> ~/.gradle/gradle.properties << EOF | ||
| signing.gnupg.executable=gpg | ||
| signing.gnupg.useLegacyGpg=false | ||
| signing.gnupg.keyName=${{ secrets.GPG_KEY_ID }} | ||
| signing.gnupg.passphrase=${{ secrets.GPG_SIGNING_PASSWORD }} | ||
| EOF | ||
|
|
||
| - name: Publish main SDK to Maven Central | ||
| working-directory: sdk/runanywhere-kotlin | ||
| run: | | ||
| ./gradlew publishAllPublicationsToMavenCentralRepository --no-daemon --info | ||
| env: | ||
| SDK_VERSION: ${{ inputs.version }} | ||
| MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | ||
| MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} | ||
|
|
||
| - name: Publish LlamaCPP module | ||
| working-directory: sdk/runanywhere-kotlin | ||
| run: | | ||
| ./gradlew :modules:runanywhere-core-llamacpp:publishAllPublicationsToMavenCentralRepository --no-daemon --info | ||
| env: | ||
| SDK_VERSION: ${{ inputs.version }} | ||
| MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | ||
| MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} | ||
|
|
||
| - name: Publish ONNX module | ||
| working-directory: sdk/runanywhere-kotlin | ||
| run: | | ||
| ./gradlew :modules:runanywhere-core-onnx:publishAllPublicationsToMavenCentralRepository --no-daemon --info | ||
| env: | ||
| SDK_VERSION: ${{ inputs.version }} | ||
| MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | ||
| MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} | ||
|
|
||
| - name: Summary | ||
| run: | | ||
| echo "## Published to Maven Central" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "Version: \`${{ inputs.version }}\`" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "### Artifacts:" >> $GITHUB_STEP_SUMMARY | ||
| echo "- \`io.github.sanchitmonga22:runanywhere-sdk:${{ inputs.version }}\`" >> $GITHUB_STEP_SUMMARY | ||
| echo "- \`io.github.sanchitmonga22:runanywhere-llamacpp:${{ inputs.version }}\`" >> $GITHUB_STEP_SUMMARY | ||
| echo "- \`io.github.sanchitmonga22:runanywhere-onnx:${{ inputs.version }}\`" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "### Consumer Usage:" >> $GITHUB_STEP_SUMMARY | ||
| echo "\`\`\`kotlin" >> $GITHUB_STEP_SUMMARY | ||
| echo "// settings.gradle.kts" >> $GITHUB_STEP_SUMMARY | ||
| echo "repositories {" >> $GITHUB_STEP_SUMMARY | ||
| echo " mavenCentral()" >> $GITHUB_STEP_SUMMARY | ||
| echo "}" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "// build.gradle.kts" >> $GITHUB_STEP_SUMMARY | ||
| echo "implementation(\"io.github.sanchitmonga22:runanywhere-sdk:${{ inputs.version }}\")" >> $GITHUB_STEP_SUMMARY | ||
| echo "implementation(\"io.github.sanchitmonga22:runanywhere-llamacpp:${{ inputs.version }}\") // Optional: LLM" >> $GITHUB_STEP_SUMMARY | ||
| echo "implementation(\"io.github.sanchitmonga22:runanywhere-onnx:${{ inputs.version }}\") // Optional: STT/TTS" >> $GITHUB_STEP_SUMMARY | ||
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
To fix the problem, add an explicit permissions block that restricts the GITHUB_TOKEN to the minimum needed. This workflow performs read-only operations on the repository (checkout) and writes only to the step summary, which does not require elevated token scopes. Therefore contents: read at the workflow or job level is sufficient and aligns with the CodeQL recommendation.
The best targeted fix without changing existing functionality is to define permissions at the workflow root, just after name: and before on:. This will apply to the single publish job and any future jobs unless they override permissions. Concretely, in .github/workflows/publish-maven-central.yml, insert:
permissions:
contents: readbelow the existing name: Publish to Maven Central line. No additional imports, methods, or definitions are required because this is a configuration-only change to the GitHub Actions workflow.
| @@ -1,5 +1,8 @@ | ||
| name: Publish to Maven Central | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: |
| name: TypeScript Typecheck | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| - name: Install dependencies | ||
| working-directory: sdk/runanywhere-web | ||
| run: npm install | ||
|
|
||
| - name: Typecheck | ||
| working-directory: sdk/runanywhere-web/packages/core | ||
| run: npx tsc --noEmit | ||
|
|
||
| build-wasm: |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, fix this by adding an explicit permissions section that grants only the minimal required permissions for the workflow. Since all the jobs here only read repository contents and upload artifacts (which does not require repository write permissions), contents: read at the workflow root is sufficient and will apply to all jobs.
Best concrete fix without changing existing functionality:
- Edit
.github/workflows/web-sdk-release.yml. - At the top workflow level (alongside
name:andon:), add:permissions: contents: read
- This leaves all job logic intact, but ensures the
GITHUB_TOKENis restricted to read-only repository contents for all jobs in this workflow.
No imports, methods, or additional definitions are needed because this is a YAML configuration change only.
| @@ -1,5 +1,8 @@ | ||
| name: Web SDK - Build & Release | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: | ||
| paths: |
| name: Build WASM (${{ matrix.backends }}) | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| backends: [core-only, all-backends] | ||
| include: | ||
| - backends: core-only | ||
| flags: '' | ||
| - backends: all-backends | ||
| flags: '--all-backends' | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: recursive | ||
|
|
||
| - name: Setup Emscripten | ||
| uses: mymindstorm/setup-emsdk@v14 | ||
| with: | ||
| version: '3.1.51' | ||
|
|
||
| - name: Verify Emscripten | ||
| run: emcc --version | ||
|
|
||
| - name: Build WASM | ||
| working-directory: sdk/runanywhere-web | ||
| run: ./wasm/scripts/build.sh ${{ matrix.flags }} | ||
|
|
||
| - name: Upload WASM artifacts | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: wasm-${{ matrix.backends }} | ||
| path: | | ||
| sdk/runanywhere-web/packages/core/wasm/racommons.js | ||
| sdk/runanywhere-web/packages/core/wasm/racommons.wasm | ||
| retention-days: 30 | ||
|
|
||
| build-ts: |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, the problem is fixed by explicitly specifying permissions for the GITHUB_TOKEN either at the top level of the workflow (so it applies to all jobs) or on individual jobs, and setting them to the minimum needed. For this workflow, the jobs only check out code, set up tools, build, and upload artifacts. Those operations only require read access to repository contents; they do not push commits, create releases, or modify issues/PRs. Therefore, contents: read is sufficient as a baseline.
The single best fix without changing existing functionality is to add a root-level permissions block after the on: section and before the jobs: section in .github/workflows/web-sdk-release.yml:
permissions:
contents: readThis will apply to all three jobs (typecheck, build-wasm, and build-ts) because none of them currently declares its own permissions block. No imports or additional definitions are needed; this is purely a declarative change to the workflow YAML. No changes within the job bodies are required.
| @@ -17,6 +17,9 @@ | ||
| required: false | ||
| default: 'all' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| typecheck: | ||
| name: TypeScript Typecheck |
| name: Build TypeScript | ||
| runs-on: ubuntu-latest | ||
| needs: typecheck | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| - name: Install dependencies | ||
| working-directory: sdk/runanywhere-web | ||
| run: npm install | ||
|
|
||
| - name: Build | ||
| working-directory: sdk/runanywhere-web/packages/core | ||
| run: npx tsc | ||
|
|
||
| - name: Upload dist | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: web-sdk-dist | ||
| path: sdk/runanywhere-web/packages/core/dist/ | ||
| retention-days: 30 |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
To fix the problem, explicitly restrict the GITHUB_TOKEN permissions used by this workflow to the minimum required. All jobs in this workflow only need to read repository contents (for actions/checkout) and upload artifacts, which does not require repository write scopes. The simplest and safest fix is to define a top-level permissions block setting contents: read. This applies to all jobs (typecheck, build-wasm, and build-ts) since none of them currently override permissions.
Concretely, in .github/workflows/web-sdk-release.yml, add a permissions: section near the top of the file (after the name: line and before on:) with contents: read. No additional imports or other code changes are needed, and existing functionality will remain the same except that the GITHUB_TOKEN will no longer have excess privileges if the repository default is read-write.
| @@ -1,4 +1,6 @@ | ||
| name: Web SDK - Build & Release | ||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: |
| } | ||
|
|
||
| // ========== YouTube Rules ========== | ||
| if (url.includes('youtube.com')) { |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, substring checks on the full URL string should be replaced with checks on parsed URL components, typically hostname or origin, and they should be compared against a whitelist of allowed hosts or patterns. For YouTube, we want to identify when the current page is genuinely on a YouTube domain (e.g., youtube.com or regional/WWW subdomains like www.youtube.com, m.youtube.com, www.youtube.co.uk, etc.), not when youtube.com appears anywhere in the URL.
The best fix here is to parse the url string using the standard URL constructor (available in modern JS/TS runtimes) into an object, then create a small helper to decide “is this a YouTube URL?” by inspecting hostname. We then replace the existing if (url.includes('youtube.com')) with a call to this helper. Inside the same YouTube block, the checks for paths like /results and /watch can safely remain as substring checks on the path portion of the parsed URL (pathname and possibly search) instead of the entire string. This avoids false positives where youtube.com appears in query or path segments of non‑YouTube hosts.
Concretely, within navigator-agent.ts:
- Add a small helper function (e.g.,
isYouTubeUrl(url: string): boolean) near the top or as a private method of the class (depending on context) that:- Wraps
new URL(url)in a try/catch; on error, returnsfalse. - Normalizes
hostnameto lowercase. - Returns
trueifhostname === 'youtube.com'orhostname.endsWith('.youtube.com'). Optionally, we can also considerhostname === 'youtu.be'if desired, but the current behavior only checks for'youtube.com', so we will keep behavior focused on YouTube domains, but more precise.
- Wraps
- In the YouTube rules section, change
if (url.includes('youtube.com'))to use this helper. - Additionally, we can parse the URL once in that block to derive
pathnameandsearchand use them instead ofurl.includes('/results'), etc., to avoid edge cases; that keeps functionality the same but more robust and does not require changes elsewhere.
No new external library is needed; the built-in URL is sufficient.
| @@ -31,6 +31,21 @@ | ||
| 'github': 'https://www.github.com', | ||
| }; | ||
|
|
||
| /** | ||
| * Returns true if the given URL points to a YouTube host. | ||
| * This avoids substring checks on the full URL string. | ||
| */ | ||
| function isYouTubeUrl(rawUrl: string): boolean { | ||
| try { | ||
| const parsed = new URL(rawUrl); | ||
| const host = parsed.hostname.toLowerCase(); | ||
| // Match youtube.com and its subdomains (e.g., www.youtube.com, m.youtube.com) | ||
| return host === 'youtube.com' || host.endsWith('.youtube.com'); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export class NavigatorAgent extends BaseAgent<NavigatorOutput> { | ||
| protected systemPrompt = `Browser automation agent. Pick ONE action based on the page. | ||
|
|
||
| @@ -189,9 +204,21 @@ | ||
| } | ||
|
|
||
| // ========== YouTube Rules ========== | ||
| if (url.includes('youtube.com')) { | ||
| if (isYouTubeUrl(url)) { | ||
| let pathname = ''; | ||
| let search = ''; | ||
| try { | ||
| const parsed = new URL(url); | ||
| pathname = parsed.pathname || ''; | ||
| search = parsed.search || ''; | ||
| } catch { | ||
| // If URL parsing fails, fall back to string checks on the raw URL. | ||
| pathname = url; | ||
| search = url; | ||
| } | ||
|
|
||
| // On YouTube search results - click first video | ||
| if (url.includes('/results') || url.includes('search_query')) { | ||
| if (pathname.includes('/results') || search.includes('search_query')) { | ||
| const video = dom.interactiveElements.find(e => | ||
| e.tag === 'a' && e.selector.includes('video-title') || | ||
| (e.tag === 'a' && e.text.length > 10 && !e.text.toLowerCase().includes('filter')) | ||
| @@ -200,7 +226,7 @@ | ||
| } | ||
|
|
||
| // On video page - task complete for "play/watch" tasks | ||
| if (url.includes('/watch') && (task.includes('play') || task.includes('watch') || task.includes('video'))) { | ||
| if (pathname.includes('/watch') && (task.includes('play') || task.includes('watch') || task.includes('video'))) { | ||
| return this.act('done', { result: 'Video playing' }, 'Done'); | ||
| } | ||
| } |
| } | ||
|
|
||
| // ========== Google Rules ========== | ||
| if (url.includes('google.com')) { |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, the problem should be fixed by parsing the URL and making decisions based on its structured components (like hostname and pathname) instead of using substring checks on the raw URL string. For host checks, we should either exactly match a whitelist of allowed hostnames (e.g., www.google.com, www.google.co.uk, etc.) or, if appropriate, match well-defined suffixes in the hostname (e.g., hostname === 'google.com' or hostname.endsWith('.google.com')). This prevents arbitrary hosts from passing the check just because they contain "google.com" somewhere else in the URL.
For this specific code, the minimal, behavior-preserving change is to replace if (url.includes('google.com')) with a hostname-based check. Since this is a TypeScript project and we cannot assume surrounding code, the best approach is to use the standard URL class available in modern JavaScript/TypeScript runtimes. We will attempt to construct a URL from the url string and, if that fails (invalid URL), we simply treat it as not matching Google. Once parsed, we will inspect parsed.hostname and check whether it is equal to google.com or ends with .google.com. This keeps the original intent (“is this a Google page?”) but avoids incorrect matches like https://evil-google.com or https://google.com.attacker.net.
Concretely, within navigator-agent.ts in the block starting at line 221, we will:
- Introduce a small, localized parsing block before the Google rules:
- Try to create
new URL(url)inside atry/catch. - Derive a boolean
isGoogleHostfromparsed.hostname === 'google.com' || parsed.hostname.endsWith('.google.com').
- Try to create
- Replace
if (url.includes('google.com')) {withif (isGoogleHost) {. - Keep the inner logic (checking
url.includes('/search'), etc.) unchanged to avoid altering behavior beyond the host identification.
This requires no new imports or external dependencies; URL is standard.
| @@ -219,7 +219,17 @@ | ||
| } | ||
|
|
||
| // ========== Google Rules ========== | ||
| if (url.includes('google.com')) { | ||
| let isGoogleHost = false; | ||
| try { | ||
| const parsedUrl = new URL(url); | ||
| const host = parsedUrl.hostname.toLowerCase(); | ||
| isGoogleHost = host === 'google.com' || host.endsWith('.google.com'); | ||
| } catch { | ||
| // If URL parsing fails, treat as non-Google host | ||
| isGoogleHost = false; | ||
| } | ||
|
|
||
| if (isGoogleHost) { | ||
| // On search results - click first result | ||
| if (url.includes('/search')) { | ||
| const result = dom.interactiveElements.find(e => |
| const taskLower = task.toLowerCase(); | ||
| return ( | ||
| taskLower.includes('youtube') || | ||
| url.includes('youtube.com') || |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, the fix is to stop treating the entire URL as an opaque string when deciding whether it belongs to YouTube. Instead, parse the URL and check its hostname (and possibly protocol) against a whitelist or at least a strict pattern that correctly handles subdomains (e.g., www.youtube.com, m.youtube.com). That avoids misclassifying URLs where youtube.com only appears in the path, query, or as part of a different domain.
For this file, the safest, minimal-change approach is:
- Use the standard
URLclass to parse the URL string insidecanHandleandgetState. - Extract
hostnameand check if it is exactlyyoutube.comor ends with.youtube.com. This preserves support for subdomains such aswww.youtube.comand region-specific hosts likemusic.youtube.com, but rejectsyoutube.com.evil.com. - Handle invalid or relative URLs gracefully by catching exceptions or falling back to conservative behavior (e.g., treat as non‑YouTube).
Concretely:
- In
canHandle(url, task), replaceurl.includes('youtube.com')with a small helper that:- Tries to construct
new URL(url). - Returns
trueif the hostname isyoutube.comor ends with.youtube.com,falseotherwise.
- Tries to construct
- In
getState(dom, history), whereconst url = dom.url || '', use the same helper to decide whether “Not on YouTube yet” instead of!url.includes('youtube.com'). - Implement this helper as a private method on
YouTubeStateMachine(e.g.,private isYouTubeUrl(rawUrl: string): boolean) so both methods share the same logic. - No new imports are needed;
URLis available in modern browser and Node runtimes. If the environment might not supportURLglobally, the project would need a polyfill, but we won’t edit anything outside this file.
| @@ -27,13 +27,31 @@ | ||
| private searchQuery: string = ''; | ||
|
|
||
| /** | ||
| * Determine whether a URL belongs to YouTube. | ||
| * Considers youtube.com and any of its subdomains (e.g., www.youtube.com, m.youtube.com). | ||
| */ | ||
| private isYouTubeUrl(rawUrl: string): boolean { | ||
| if (!rawUrl) { | ||
| return false; | ||
| } | ||
| try { | ||
| const parsed = new URL(rawUrl); | ||
| const host = parsed.hostname.toLowerCase(); | ||
| return host === 'youtube.com' || host.endsWith('.youtube.com'); | ||
| } catch { | ||
| // If the URL is invalid or cannot be parsed, treat it as non-YouTube. | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if this state machine can handle the task | ||
| */ | ||
| canHandle(url: string, task: string): boolean { | ||
| const taskLower = task.toLowerCase(); | ||
| return ( | ||
| taskLower.includes('youtube') || | ||
| url.includes('youtube.com') || | ||
| this.isYouTubeUrl(url) || | ||
| (taskLower.includes('video') && (taskLower.includes('watch') || taskLower.includes('play'))) | ||
| ); | ||
| } | ||
| @@ -52,7 +64,7 @@ | ||
| const url = dom.url || ''; | ||
|
|
||
| // Not on YouTube yet | ||
| if (!url.includes('youtube.com')) { | ||
| if (!this.isYouTubeUrl(url)) { | ||
| return 'NAVIGATING'; | ||
| } | ||
|
|
| const url = dom.url || ''; | ||
|
|
||
| // Not on YouTube yet | ||
| if (!url.includes('youtube.com')) { |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, the fix is to stop checking for youtube.com as a substring of the entire URL, and instead parse the URL and verify that its hostname is either youtube.com or a recognized subdomain (such as www.youtube.com, m.youtube.com, etc.). This avoids accidentally matching occurrences in the path, query string, or in attacker‑controlled hostnames such as evil-youtube.com.
The best way to fix this without changing existing functionality is:
- Introduce a small helper inside
YouTubeStateMachinethat, given a URL string, safely parses it (using the standardURLconstructor) and checks the hostname against an explicit allowlist or suffix match. - Replace the substring checks
url.includes('youtube.com')in bothcanHandle(line 36) andgetState(line 55) with calls to this helper, such asthis.isYouTubeUrl(url).
Implementation details for Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts:
-
Add a private method inside
YouTubeStateMachine(e.g., just belowprivate searchQuery: string = '';):private isYouTubeUrl(url: string): boolean { try { const parsed = new URL(url); const hostname = parsed.hostname.toLowerCase(); return ( hostname === 'youtube.com' || hostname.endsWith('.youtube.com') ); } catch { // If URL constructor throws (e.g., relative URL), fall back to simple checks const lower = url.toLowerCase(); return lower.startsWith('https://youtube.com') || lower.startsWith('http://youtube.com') || lower.includes('://www.youtube.com') || lower.includes('://m.youtube.com'); } }
This keeps behavior broadly similar (recognizing main and common subdomains) while being much safer about host checks.
-
In
canHandle, line 36, changeurl.includes('youtube.com')tothis.isYouTubeUrl(url). -
In
getState, line 55, change the negated YouTube check fromif (!url.includes('youtube.com'))toif (!this.isYouTubeUrl(url)).
No additional imports are needed because URL is available in modern TypeScript/JavaScript runtimes that support the DOM or standard web APIs, which is consistent with code that already works with browser DOM state.
| @@ -27,13 +27,34 @@ | ||
| private searchQuery: string = ''; | ||
|
|
||
| /** | ||
| * Determine if the given URL points to YouTube. | ||
| * Uses hostname checks to avoid matching "youtube.com" in non-host parts. | ||
| */ | ||
| private isYouTubeUrl(url: string): boolean { | ||
| try { | ||
| const parsed = new URL(url); | ||
| const hostname = parsed.hostname.toLowerCase(); | ||
| return hostname === 'youtube.com' || hostname.endsWith('.youtube.com'); | ||
| } catch { | ||
| // Fallback for relative or invalid URLs: keep a conservative match behavior. | ||
| const lower = url.toLowerCase(); | ||
| return ( | ||
| lower.startsWith('https://youtube.com') || | ||
| lower.startsWith('http://youtube.com') || | ||
| lower.includes('://www.youtube.com') || | ||
| lower.includes('://m.youtube.com') | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if this state machine can handle the task | ||
| */ | ||
| canHandle(url: string, task: string): boolean { | ||
| const taskLower = task.toLowerCase(); | ||
| return ( | ||
| taskLower.includes('youtube') || | ||
| url.includes('youtube.com') || | ||
| this.isYouTubeUrl(url) || | ||
| (taskLower.includes('video') && (taskLower.includes('watch') || taskLower.includes('play'))) | ||
| ); | ||
| } | ||
| @@ -52,7 +67,7 @@ | ||
| const url = dom.url || ''; | ||
|
|
||
| // Not on YouTube yet | ||
| if (!url.includes('youtube.com')) { | ||
| if (!this.isYouTubeUrl(url)) { | ||
| return 'NAVIGATING'; | ||
| } | ||
|
|
| } | ||
|
|
||
| // Check if it was a link that should navigate | ||
| if (el instanceof HTMLAnchorElement && el.href && !el.href.startsWith('javascript:')) { |
Check failure
Code scanning / CodeQL
Incomplete URL scheme check High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, the problem should be fixed by making the URL scheme validation comprehensive: when deciding whether to auto-navigate to an href, reject not just javascript: but also other executable schemes such as data: and vbscript:. Ideally, this is done by normalizing the URL (trimming whitespace, lowercasing the scheme part) and checking it against a denylist, or conversely, by only allowing a safe list like http:, https:, and mailto:. To minimize behavioral change, we should keep the current logic and just expand the disallowed schemes.
For this specific file, the best minimal change is to adjust the condition on line 261 so that it refuses to navigate when el.href starts with any of the dangerous schemes. Because el.href is a fully resolved URL string and startsWith is case-sensitive, we should normalize case via toLowerCase() before checking; this avoids breaking existing behavior where a site might use JavaScript: or mixed-case schemes. We can do that inline in the condition by deriving a lowercase version of el.href and checking it against "javascript:", "data:", and "vbscript:". No new imports or helper methods are required; a one-line change to the if condition is sufficient.
Concretely, in Playground/on-device-browser-agent/src/content/action-executor.ts, locate the if (el instanceof HTMLAnchorElement && el.href && !el.href.startsWith('javascript:')) { block and replace the !el.href.startsWith('javascript:') part with a more complete check that lowercases the href and rejects any of the three schemes.
| @@ -258,7 +258,13 @@ | ||
| } | ||
|
|
||
| // Check if it was a link that should navigate | ||
| if (el instanceof HTMLAnchorElement && el.href && !el.href.startsWith('javascript:')) { | ||
| if ( | ||
| el instanceof HTMLAnchorElement && | ||
| el.href && | ||
| !el.href.toLowerCase().startsWith('javascript:') && | ||
| !el.href.toLowerCase().startsWith('data:') && | ||
| !el.href.toLowerCase().startsWith('vbscript:') | ||
| ) { | ||
| const targetHref = el.href; | ||
|
|
||
| // Give click a moment to work (some sites use JS navigation) |
|
|
||
| const toast = document.createElement('div'); | ||
| toast.className = 'toast'; | ||
| toast.innerHTML = `${TOAST_ICONS[variant]}<span>${message}</span>`; |
Check failure
Code scanning / CodeQL
DOM text reinterpreted as HTML High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
To fix this, we should ensure that any untrusted text (message passed to showToast) is not interpreted as HTML. Instead of interpolating message directly into a string assigned to innerHTML, we should build the DOM tree explicitly: keep the SVG icon as HTML (trusted, static) and attach the user-controlled message as textContent of a <span>. This preserves existing visuals and behavior while preventing injected HTML from being executed.
Concretely, in examples/web/RunAnywhereAI/src/components/dialogs.ts, in showToast, replace:
toast.innerHTML = `${TOAST_ICONS[variant]}<span>${message}</span>`;with code that:
- Sets the icon using
innerHTMLfrom the trustedTOAST_ICONSstring. - Creates a
spanelement. - Sets
span.textContent = message;. - Appends the span to the toast.
Because we are only manipulating DOM APIs already in use (createElement, appendChild) we do not need any new imports or dependencies. No changes are required to storage.ts; its use of showToast remains the same, but the sink is now safe.
| @@ -43,7 +43,13 @@ | ||
|
|
||
| const toast = document.createElement('div'); | ||
| toast.className = 'toast'; | ||
| toast.innerHTML = `${TOAST_ICONS[variant]}<span>${message}</span>`; | ||
|
|
||
| // Insert trusted icon markup, then append message as text to avoid HTML interpretation. | ||
| toast.innerHTML = TOAST_ICONS[variant]; | ||
| const messageSpan = document.createElement('span'); | ||
| messageSpan.textContent = message; | ||
| toast.appendChild(messageSpan); | ||
|
|
||
| document.body.appendChild(toast); | ||
|
|
||
| requestAnimationFrame(() => { |
| backdrop.innerHTML = ` | ||
| <div class="dialog-box"> | ||
| <h3 class="dialog-title">${title}</h3> | ||
| <p class="dialog-message">${message}</p> | ||
| <div class="dialog-actions"> | ||
| <button class="dialog-btn dialog-btn--cancel" data-role="cancel">${cancelLabel}</button> | ||
| <button class="${confirmClass}" data-role="confirm">${confirmLabel}</button> | ||
| </div> | ||
| </div> | ||
| `; |
Check failure
Code scanning / CodeQL
DOM text reinterpreted as HTML High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, the problem is that showConfirmDialog accepts a plain message string and interpolates it directly into innerHTML, and callers compose that message string using potentially tainted values plus HTML markup. To fix this without changing visible behavior, we should either: (a) make showConfirmDialog build the DOM using textContent for all dynamic pieces (and non-string attributes for buttons), or (b) ensure that any dynamic parts included in the message string are HTML-escaped before interpolation. Since we already have a use case that needs <strong> styling, the best robust approach is to restructure showConfirmDialog to create DOM nodes programmatically and only allow a simple, well-defined subset of markup, or to pass the untrusted data separately and inject it as textContent into the appropriate places.
The minimal, clean fix within the shown snippets is: introduce a small escapeHtml helper in examples/web/RunAnywhereAI/src/components/dialogs.ts and use it to escape the interpolated message (and title, confirmLabel, cancelLabel) before assigning to backdrop.innerHTML, and separately escape any untrusted portions before including them in the message string in storage.ts. In storage.ts, we can reuse the already-defined escapeHtml to sanitize modelName both when constructing the confirmation message and when passing modelName to showToast. Because the confirmation message uses <strong>...</strong>, we can preserve that markup by only escaping the interpolated modelName, not the whole message, like: `Remove <strong>${escapeHtml(modelName)}</strong> ...`. In dialogs.ts, escaping message before embedding it will double-escape <strong> from trusted callers, so instead we should only escape title, confirmLabel, and cancelLabel, and treat message as already-encoded HTML from the caller; then in our tainted call site we ensure modelName is escaped inside that HTML. That keeps existing rich-text capability while ensuring tainted data is encoded before reaching innerHTML.
Concretely:
- In
examples/web/RunAnywhereAI/src/views/storage.ts, update themodelNameusages:- At line 280, change the
messageargument to escapemodelNamevia the existingescapeHtmlhelper. - At line 288, also escape
modelNamebefore passing it toshowToast, sinceshowToastusesinnerHTML.
- At line 280, change the
- In
examples/web/RunAnywhereAI/src/components/dialogs.ts, hardenshowConfirmDialogby HTML-escapingtitle,confirmLabel, andcancelLabelwhen buildingbackdrop.innerHTML, using a new localescapeHtmlhelper. Leavemessageas-is to allow the caller to provide limited markup, and rely on the caller (storage.ts) to escape any untrusted fragments before composing the message string.
No behavior visible to users changes other than preventing HTML injection through modelName and other parameters that might be untrusted.
| @@ -29,6 +29,10 @@ | ||
| info: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`, | ||
| }; | ||
|
|
||
| function escapeHtml(str: string): string { | ||
| return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | ||
| } | ||
|
|
||
| /** | ||
| * Show a transient toast notification at the top of the viewport. | ||
| * Auto-dismisses after `durationMs` (default 3 s). | ||
| @@ -79,11 +83,11 @@ | ||
|
|
||
| backdrop.innerHTML = ` | ||
| <div class="dialog-box"> | ||
| <h3 class="dialog-title">${title}</h3> | ||
| <h3 class="dialog-title">${escapeHtml(title)}</h3> | ||
| <p class="dialog-message">${message}</p> | ||
| <div class="dialog-actions"> | ||
| <button class="dialog-btn dialog-btn--cancel" data-role="cancel">${cancelLabel}</button> | ||
| <button class="${confirmClass}" data-role="confirm">${confirmLabel}</button> | ||
| <button class="dialog-btn dialog-btn--cancel" data-role="cancel">${escapeHtml(cancelLabel)}</button> | ||
| <button class="${confirmClass}" data-role="confirm">${escapeHtml(confirmLabel)}</button> | ||
| </div> | ||
| </div> | ||
| `; |
| @@ -277,7 +277,7 @@ | ||
|
|
||
| const confirmed = await showConfirmDialog( | ||
| 'Delete Model', | ||
| `Remove <strong>${modelName}</strong> (${formatBytes(modelSize)}) from storage? You will need to re-download it to use again.`, | ||
| `Remove <strong>${escapeHtml(modelName)}</strong> (${formatBytes(modelSize)}) from storage? You will need to re-download it to use again.`, | ||
| 'Delete', | ||
| 'Cancel', | ||
| true, | ||
| @@ -285,7 +285,7 @@ | ||
| if (!confirmed) return; | ||
|
|
||
| await ModelManager.deleteModel(modelId); | ||
| showToast(`${modelName} removed (freed ${formatBytes(modelSize)})`, 'info'); | ||
| showToast(`${escapeHtml(modelName)} removed (freed ${formatBytes(modelSize)})`, 'info'); | ||
| refreshStorage(); | ||
| }); | ||
| }); |
| container.innerHTML = ` | ||
| <div class="toolbar"> | ||
| <div class="toolbar-title">Settings</div> | ||
| <div class="toolbar-actions"></div> | ||
| </div> | ||
| <div class="settings-form"> | ||
|
|
||
| <!-- Generation --> | ||
| <div class="settings-section"> | ||
| <div class="settings-section-title">Generation</div> | ||
| <div class="setting-row"> | ||
| <span class="setting-label">Temperature</span> | ||
| <div class="flex items-center gap-sm"> | ||
| <span class="setting-value" id="settings-temp-val">${settings.temperature.toFixed(1)}</span> | ||
| <input type="range" id="settings-temp" min="0" max="2" step="0.1" value="${settings.temperature}"> | ||
| </div> | ||
| </div> | ||
| <div class="setting-row"> | ||
| <span class="setting-label">Max Tokens</span> | ||
| <div class="flex items-center gap-sm"> | ||
| <button class="btn btn-sm" id="settings-tokens-minus">-</button> | ||
| <span class="setting-value" id="settings-tokens-val">${settings.maxTokens}</span> | ||
| <button class="btn btn-sm" id="settings-tokens-plus">+</button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- API Configuration --> | ||
| <div class="settings-section"> | ||
| <div class="settings-section-title">API Configuration</div> | ||
| <div class="setting-row setting-row--stacked"> | ||
| <label class="label">API Key</label> | ||
| <input type="password" class="text-input w-full" id="settings-api-key" placeholder="Enter API key..." value="${settings.apiKey}"> | ||
| </div> | ||
| <div class="setting-row setting-row--stacked"> | ||
| <label class="label">Base URL</label> | ||
| <input type="url" class="text-input w-full" id="settings-base-url" placeholder="https://api.runanywhere.ai" value="${settings.baseURL}"> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- Logging --> | ||
| <div class="settings-section"> | ||
| <div class="settings-section-title">Logging</div> | ||
| <div class="setting-row"> | ||
| <span class="setting-label">Analytics</span> | ||
| <div class="toggle ${settings.analytics ? 'on' : ''}" id="settings-analytics-toggle"></div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- About --> | ||
| <div class="settings-section"> | ||
| <div class="settings-section-title">About</div> | ||
| <div class="setting-row"> | ||
| <span class="setting-label">SDK Version</span> | ||
| <span class="setting-value">0.1.0</span> | ||
| </div> | ||
| <div class="setting-row"> | ||
| <span class="setting-label">Platform</span> | ||
| <span class="setting-value">Web (Emscripten WASM)</span> | ||
| </div> | ||
| <div class="setting-row cursor-pointer" id="settings-docs-link"> | ||
| <span class="setting-label text-accent">Documentation</span> | ||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="1.5" width="16" height="16"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> | ||
| </div> | ||
| </div> | ||
|
|
||
| </div> | ||
| `; |
Check failure
Code scanning / CodeQL
DOM text reinterpreted as HTML High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general terms, the problem comes from constructing a large HTML string with innerHTML that directly interpolates untrusted values (${settings.apiKey}, ${settings.baseURL}). To fix this, we should avoid interpreting those values as HTML by either (1) escaping/encoding them before inserting into the string, or (2) building the DOM with document.createElement / property assignment and setting input values via .value rather than HTML text. Given the existing code, the smallest non-breaking change is to remove the interpolation of these untrusted values from the template literal and instead set them after the DOM is created via .value assignments, which are not parsed as HTML.
Concretely:
- In
initSettingsTab, change thecontainer.innerHTMLtemplate so that thevalueattributes for#settings-api-keyand#settings-base-urlare empty (no${settings.*}interpolation). - Right after the DOM has been created and the inputs queried (
apiKeyInputandbaseURLInput), explicitly assign their.valuefromsettings.apiKeyandsettings.baseURL. This ensures those values are inserted as plain text properties, not re-parsed HTML. - Leave
loadSettingsas-is, since it already uses.valueassignments and does not rely on initialvalue="..."attributes.
All of these changes are in examples/web/RunAnywhereAI/src/views/settings.ts. No new imports or helper methods are needed.
| @@ -48,11 +48,11 @@ | ||
| <div class="settings-section-title">API Configuration</div> | ||
| <div class="setting-row setting-row--stacked"> | ||
| <label class="label">API Key</label> | ||
| <input type="password" class="text-input w-full" id="settings-api-key" placeholder="Enter API key..." value="${settings.apiKey}"> | ||
| <input type="password" class="text-input w-full" id="settings-api-key" placeholder="Enter API key..."> | ||
| </div> | ||
| <div class="setting-row setting-row--stacked"> | ||
| <label class="label">Base URL</label> | ||
| <input type="url" class="text-input w-full" id="settings-base-url" placeholder="https://api.runanywhere.ai" value="${settings.baseURL}"> | ||
| <input type="url" class="text-input w-full" id="settings-base-url" placeholder="https://api.runanywhere.ai"> | ||
| </div> | ||
| </div> | ||
|
|
||
| @@ -116,6 +113,8 @@ | ||
| // API inputs | ||
| const apiKeyInput = container.querySelector('#settings-api-key') as HTMLInputElement; | ||
| const baseURLInput = container.querySelector('#settings-base-url') as HTMLInputElement; | ||
| apiKeyInput.value = settings.apiKey; | ||
| baseURLInput.value = settings.baseURL; | ||
| apiKeyInput.addEventListener('change', () => { | ||
| settings.apiKey = apiKeyInput.value; | ||
| saveSettings(); |
|
|
||
| function saveSettings(): void { | ||
| try { | ||
| localStorage.setItem('runanywhere-settings', JSON.stringify(settings)); |
Check failure
Code scanning / CodeQL
Clear text storage of sensitive information High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, the problem is that the sensitive apiKey is being stored in cleartext inside localStorage as part of the serialized settings object. To fix this, we should avoid persisting the API key alongside non-sensitive settings, and instead keep it only in memory or, if persistence is truly required, store an encrypted form or use a more secure mechanism. Given the constraints and the code shown, the most straightforward and least invasive fix is: (1) stop persisting apiKey to localStorage, and (2) stop restoring it from localStorage. This preserves all existing non-sensitive functionality while removing the insecure cleartext storage.
Concretely, in saveSettings, before serializing and writing to localStorage, we can construct a shallow copy of settings with apiKey removed (or explicitly set to an empty string) and serialize that copy instead of the original settings object. This ensures the in-memory settings.apiKey still works for current usage, but it is not written to disk. In loadSettings, after parsing the saved JSON, we should explicitly ignore any stored apiKey value (in case older data exists) and avoid populating either settings.apiKey or the #settings-api-key input with data from storage. This means users will need to re-enter their API key on each reload, which is the secure behavior implied by the recommendation to avoid cleartext storage. No external libraries are needed; we only adjust object copying and assignment logic in saveSettings and loadSettings, and remove the UI update that injects a stored key into the input field.
| @@ -144,7 +144,8 @@ | ||
|
|
||
| function saveSettings(): void { | ||
| try { | ||
| localStorage.setItem('runanywhere-settings', JSON.stringify(settings)); | ||
| const { apiKey, ...settingsToPersist } = settings; | ||
| localStorage.setItem('runanywhere-settings', JSON.stringify(settingsToPersist)); | ||
| } catch { /* storage may not be available */ } | ||
| } | ||
|
|
||
| @@ -152,13 +153,16 @@ | ||
| try { | ||
| const saved = localStorage.getItem('runanywhere-settings'); | ||
| if (saved) { | ||
| Object.assign(settings, JSON.parse(saved)); | ||
| const parsed = JSON.parse(saved); | ||
| // Never restore apiKey from storage, even if present | ||
| delete parsed.apiKey; | ||
| Object.assign(settings, parsed); | ||
| // Update UI | ||
| (container.querySelector('#settings-temp') as HTMLInputElement).value = String(settings.temperature); | ||
| container.querySelector('#settings-temp-val')!.textContent = settings.temperature.toFixed(1); | ||
| container.querySelector('#settings-tokens-val')!.textContent = String(settings.maxTokens); | ||
| container.querySelector('#settings-analytics-toggle')!.classList.toggle('on', settings.analytics); | ||
| (container.querySelector('#settings-api-key') as HTMLInputElement).value = settings.apiKey; | ||
| // Do not populate the API key field from storage | ||
| (container.querySelector('#settings-base-url') as HTMLInputElement).value = settings.baseURL; | ||
| } | ||
| } catch { /* storage may not be available */ } |
| app.innerHTML = ` | ||
| <div class="error-view"> | ||
| <div class="error-icon">⚠️</div> | ||
| <h2>Initialization Failed</h2> | ||
| <p class="text-secondary max-w-md">${message}</p> | ||
| <button class="btn btn-primary btn-lg" id="retry-btn">Retry</button> | ||
| </div> | ||
| `; |
Check warning
Code scanning / CodeQL
Exception text reinterpreted as HTML Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 1 month ago
In general, to fix this class of vulnerability, any string that may contain user-controlled or otherwise untrusted content must not be written directly into the DOM using innerHTML (or similar APIs) unless it is properly escaped for the HTML context. Instead, either (a) use text-only setters like textContent, or (b) sanitize/escape the content before inserting as HTML.
Here, the best fix is to refactor showErrorView in examples/web/RunAnywhereAI/src/main.ts so that the DOM structure is still built via innerHTML for the static HTML fragment, but the dynamic message value is inserted as text via textContent. Specifically:
- Change the
app.innerHTML = \...`block so that thehas no${message}interpolation and instead includes an emptywith the same classes and perhaps anid` or data attribute. - After setting
innerHTML, query that<p>element and set itstextContent = message. - Keep the rest of the structure and behavior (including the retry button and event listener) unchanged.
No new external libraries are required; we only use standard DOM APIs.
| @@ -196,11 +196,16 @@ | ||
| <div class="error-view"> | ||
| <div class="error-icon">⚠️</div> | ||
| <h2>Initialization Failed</h2> | ||
| <p class="text-secondary max-w-md">${message}</p> | ||
| <p class="text-secondary max-w-md" id="error-message"></p> | ||
| <button class="btn btn-primary btn-lg" id="retry-btn">Retry</button> | ||
| </div> | ||
| `; | ||
|
|
||
| const errorMessageEl = document.getElementById('error-message'); | ||
| if (errorMessageEl) { | ||
| errorMessageEl.textContent = message; | ||
| } | ||
|
|
||
| document.getElementById('retry-btn')!.addEventListener('click', () => { | ||
| app.innerHTML = ''; | ||
| main(); |
Summary
RunAnywhere+LoRAandRunAnywhere+LoRADownloadextensions for loading and downloading LoRA adaptersLoraAdapterCatalogfor managing available adaptersChanges
sdk/runanywhere-kotlin/): New LoRA extensions, adapter catalog, bridge updatessdk/runanywhere-commons/): LoRA support in LLaMA.cpp backend, new JNI bindingsexamples/android/): NewLoraScreen,LoraViewModel, updatedChatScreenandChatViewModelwith LoRA contextdocs/impl/lora_adapter_support.mdTest plan
RunAnywhere+LoRADownload🤖 Generated with Claude Code
Important
Adds LoRA adapter support to Kotlin SDK and Android app, including loading, unloading, compatibility checking, and downloading from URLs or a catalog.
RunAnywhere+LoRAandRunAnywhere+LoRADownloadextensions for managing LoRA adapters.LoraAdapterCatalogfor managing adapters.LoraCompatibilityResult,LoraDownloadState, andLoraDownloadProgressdata classes.loadLoraAdapter,removeLoraAdapter,clearLoraAdapters,getLoadedLoraAdapters, andcheckLoraCompatibilityfunctions.downloadLoraAdapteranddownloadLoraFromCatalogfunctions.racLlmComponentCheckLoraCompatandracLlmComponentLoadLorafunctions inrunanywhere_commons_jni.cpp.llamacpp_backend.cppwithcheck_lora_compatibilityandread_gguf_metadatamethods.rac_llm_llamacpp.cppwithrac_llm_llamacpp_check_lora_compatandrac_llm_llamacpp_read_gguf_infofunctions.This description was created by
for 07700f2. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Documentation
Chores
Greptile Summary
Adds comprehensive LoRA (Low-Rank Adaptation) adapter support across the entire SDK stack, enabling runtime model fine-tuning without reloading base models.
Implementation spans 6 architectural layers:
Key features:
Documentation:
docs/impl/lora_adapter_support.mdcovering all layers with C/Swift usage examplesConfidence Score: 4/5
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/temp/LoraAdapterCatalog.kt- marked as temporary but may need permanent home before releaseImportant Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TB subgraph Public["Public API Layer (Kotlin)"] A[RunAnywhere.loadLoraAdapter] A1[RunAnywhere.downloadLoraAdapter] A2[RunAnywhere.checkLoraCompatibility] end subgraph Bridge["Kotlin Bridge Layer"] B[CppBridgeLLM.loadLoraAdapter] B1[CppBridgeLLM.checkLoraCompatibility] end subgraph JNI["JNI Layer"] C[racLlmComponentLoadLora] C1[racLlmComponentCheckLoraCompat] end subgraph Component["Component C API"] D[rac_llm_component_load_lora] D1[rac_llm_component_check_lora_compat] end subgraph Vtable["Vtable Dispatch"] E[ops->load_lora] E1[ops->check_lora_compat] end subgraph Backend["LlamaCPP Backend"] F[rac_llm_llamacpp_load_lora] F1[rac_llm_llamacpp_check_lora_compat] end subgraph CPP["C++ Implementation"] G[LlamaCppTextGeneration::load_lora_adapter] G1[Check GGUF metadata] G2[Load adapter via llama.cpp] G3[Recreate context] G4[Apply all adapters] G5[Clear KV cache] end A --> B A1 --> HTTP[HTTP Download with<br/>progress tracking] A2 --> B1 B --> C B1 --> C1 C --> D C1 --> D1 D --> E D1 --> E1 E --> F E1 --> F1 F --> G F1 --> G1 G --> G1 G1 --> G2 G2 --> G3 G3 --> G4 G4 --> G5 HTTP --> A style A fill:#4CAF50 style A1 fill:#4CAF50 style A2 fill:#4CAF50 style G5 fill:#FF9800Last reviewed commit: 07700f2