Skip to content

[Android SDK] [Kotlin SDK] Add LoRA Adapter Support#412

Closed
Siddhesh2377 wants to merge 1668 commits intomainfrom
siddhesh/lora-demo
Closed

[Android SDK] [Kotlin SDK] Add LoRA Adapter Support#412
Siddhesh2377 wants to merge 1668 commits intomainfrom
siddhesh/lora-demo

Conversation

@Siddhesh2377
Copy link
Copy Markdown
Collaborator

@Siddhesh2377 Siddhesh2377 commented Feb 22, 2026

Summary

  • Adds LoRA (Low-Rank Adaptation) adapter support to the Kotlin SDK and Android example app
  • Exposes RunAnywhere+LoRA and RunAnywhere+LoRADownload extensions for loading and downloading LoRA adapters
  • Adds a LoraAdapterCatalog for managing available adapters
  • Implements LoRA loading/unloading in the C++ LLaMA.cpp backend via JNI
  • Updates the Android demo app with a dedicated Lora screen, prompt templates, and LoRA-aware chat UI

Changes

  • Kotlin SDK (sdk/runanywhere-kotlin/): New LoRA extensions, adapter catalog, bridge updates
  • C++ Commons (sdk/runanywhere-commons/): LoRA support in LLaMA.cpp backend, new JNI bindings
  • Android Demo (examples/android/): New LoraScreen, LoraViewModel, updated ChatScreen and ChatViewModel with LoRA context
  • Docs: Updated docs/impl/lora_adapter_support.md

Test plan

  • Build Kotlin SDK and verify LoRA extensions compile
  • Run Android demo app and navigate to Lora screen
  • Load a LoRA adapter and verify it affects model output
  • Test adapter download via RunAnywhere+LoRADownload
  • Verify unloading a LoRA adapter restores default behavior

🤖 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.

  • Behavior:
    • Adds LoRA adapter support to Kotlin SDK and Android app, enabling loading, unloading, and compatibility checking.
    • Introduces RunAnywhere+LoRA and RunAnywhere+LoRADownload extensions for managing LoRA adapters.
    • Implements LoRA loading/unloading in C++ backend via JNI.
    • Updates Android app with LoRA screen, prompt templates, and chat UI.
  • Kotlin SDK:
    • Adds LoraAdapterCatalog for managing adapters.
    • Adds LoraCompatibilityResult, LoraDownloadState, and LoraDownloadProgress data classes.
    • Implements loadLoraAdapter, removeLoraAdapter, clearLoraAdapters, getLoadedLoraAdapters, and checkLoraCompatibility functions.
    • Implements downloadLoraAdapter and downloadLoraFromCatalog functions.
  • JNI and C++:
    • Adds racLlmComponentCheckLoraCompat and racLlmComponentLoadLora functions in runanywhere_commons_jni.cpp.
    • Updates llamacpp_backend.cpp with check_lora_compatibility and read_gguf_metadata methods.
    • Updates rac_llm_llamacpp.cpp with rac_llm_llamacpp_check_lora_compat and rac_llm_llamacpp_read_gguf_info functions.

This description was created by Ellipsis for 07700f2. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • LoRA adapter compatibility checking without loading weights
    • LoRA adapter downloads with progress tracking and catalog support
    • LoRA scale adjustment UI for fine-tuning loaded adapters
  • Documentation

    • Added comprehensive LoRA adapter support documentation with architecture diagrams and workflow descriptions
  • Chores

    • Removed deprecated example application module
    • Updated IDE and Git configurations

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:

  • C++ backend integrates llama.cpp LoRA APIs with context recreation and KV cache management
  • Component layer provides vtable-based dispatch for backend-agnostic LoRA operations
  • JNI bridge exposes C functions to Kotlin with proper memory management
  • Kotlin SDK implements public API with compatibility checking and HTTP download support
  • Android demo apps showcase LoRA loading, stacking, and catalog-based downloads

Key features:

  • Load/remove/clear LoRA adapters with individual scale factors
  • Stack multiple adapters simultaneously
  • Pre-load compatibility check via GGUF metadata parsing (prevents incompatible loads)
  • Download adapters from URLs or built-in catalog with progress tracking
  • Atomic downloads using temp files with rename
  • Thread-safe operations with proper locking at all layers

Documentation:

  • 1000+ line implementation guide in docs/impl/lora_adapter_support.md covering all layers with C/Swift usage examples

Confidence Score: 4/5

  • Safe to merge with minor documentation clarifications needed
  • Well-architected implementation following established SDK patterns with proper error handling, thread safety, and comprehensive documentation. Score reflects solid engineering with a few minor areas for improvement (catalog location, test coverage documentation).
  • Pay attention to sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/temp/LoraAdapterCatalog.kt - marked as temporary but may need permanent home before release

Important Files Changed

Filename Overview
sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp Adds LoRA adapter loading, compatibility checking via GGUF metadata parsing, and context recreation logic
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp Adds JNI bridge functions for LoRA operations with proper error handling
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRADownload.jvmAndroid.kt Implements HTTP download with progress tracking, temp files, and atomic rename
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/temp/LoraAdapterCatalog.kt Hardcoded catalog of 5 LoRA adapters for Qwen model
examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraViewModel.kt Dedicated LoRA demo ViewModel with download, compatibility checking, and generation

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:#FF9800
Loading

Last reviewed commit: 07700f2

josuediazflores and others added 30 commits February 3, 2026 18:32
…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>
- 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>
- 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.
* Optimise resampling and heap thrashing

opti

* improved even more for integer ratios

* Update whispercpp_backend.cpp
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
shubhammalhotra28 and others added 16 commits February 20, 2026 20:29
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
…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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 22, 2026

Important

Review skipped

Too many files!

This PR contains 261 files, which is 111 over the limit of 150.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
LoRA Implementation Documentation
docs/impl/lora_adapter_support.md
Comprehensive new documentation describing LoRA adapter compatibility checking, download workflows, architecture integration, error handling, and API changes across Kotlin, JNI, and C++ layers.
C++ Backend LoRA Support
sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp, llamacpp_backend.h
Adds LoRA compatibility checking and GGUF metadata reading; includes pre-load validation comparing adapter and model architectures with error messaging.
C API Layer - LoRA Headers
sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h, sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h, sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h
Public API declarations for LoRA compatibility checking and vtable expansion; adds check_lora_compat function pointer to service ops structure.
C API Implementation
sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp, rac_backend_llamacpp_register.cpp, sdk/runanywhere-commons/src/features/llm/llm_component.cpp
Implements public C APIs for LoRA checking; vtable registration and component-level wrapper with mutex-protected access and error handling.
JNI & Native Bridge
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp, sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt, sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeLLM.kt
JNI bindings exposing LoRA compatibility check and Kotlin wrapper with component state validation.
Kotlin Public APIs - LoRA Compatibility
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.kt, sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.jvmAndroid.kt
Expect/actual declarations for LoRA compatibility checking; includes LoraCompatibilityResult data class and guard checks for SDK initialization.
Kotlin Public APIs - LoRA Download
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRADownload.kt, sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRADownload.jvmAndroid.kt
Expect/actual declarations for LoRA downloads via HTTP; includes LoraDownloadState enum, LoraDownloadProgress data class, Flow-based progress reporting, and atomic file operations.
LoRA Catalog
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/temp/LoraAdapterCatalog.kt
Hardcoded LoRA adapter catalog with five predefined entries and lookup utility; provides data class LoraAdapterEntry for catalog metadata.
Android Chat UI Integration
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt, ChatViewModel.kt
Extends ChatTopBar with LoRA indicator, adds LoRA button to ChatInputView, introduces LoraDialog and LoraScaleDialog composables; ChatViewModel expanded with LoRA state (catalog, downloaded, download progress) and methods for catalog loading, adapter downloading/loading, and clearing.
Model Selection LoRA Support
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/models/ModelSelectionBottomSheet.kt
Adds supportsLora flag to AIModel data class; detects LoRA-compatible models by name pattern and renders LoRA badge on UI.
RunAnyWhereLora Module Removal
examples/android/RunAnyWhereLora/...
Complete removal of RunAnyWhereLora example module including: LoraScreen.kt, LoraViewModel.kt, MainActivity.kt, LoraApplication.kt, all theme/color/resource definitions, Gradle/IDE configurations, and build scripts.
IDE VCS Configuration
.idea/vcs.xml
Updates Git VCS directory mappings from legacy dev paths to Android/WebGPU build paths (llamacpp-src, nlohmann_json-src, whispercpp-src, stable_diffusion_cpp-src).
RAGBridge Cleanup
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/rag/RAGBridge.kt
Removes @Keep annotation from RAGBridge object and associated unused import.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

kotlin-sdk, android-sample, enhancement

Suggested reviewers

  • sanchitmonga22

Poem

🐰 Hop along the LoRA trail,
Adapters download without fail,
Compatibility checked with care,
Chat now dons a LoRA flair,
Boinc! Old modules disappear in air! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[Android SDK] [Kotlin SDK] Add LoRA Adapter Support' accurately describes the main objective of the PR: introducing LoRA adapter support across the Kotlin SDK and Android platform.
Description check ✅ Passed The PR description is comprehensive and covers the key aspects including summary, changes across different layers, and a test plan. However, some template sections like 'Type of Change', 'Testing' checkboxes, and 'Labels' are not filled out according to the template structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch siddhesh/lora-demo

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Looks good to me! 👍

Reviewed everything up to 07700f2 in 16 seconds. Click for details.
  • Reviewed 3221 lines of code in 24 files
  • Skipped 0 files when reviewing.
  • Skipped posting 0 draft 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 Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

24 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +106 to +110
if (destFile.exists()) destFile.delete()
if (!tempFile.renameTo(destFile)) {
tempFile.copyTo(destFile, overwrite = true)
tempFile.delete()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fallback copyTo after renameTo fails could leave both files present temporarily

Suggested change
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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Return RAC_ERROR_NULL_POINTER for invalid args.
Null lora_path / out_error currently becomes RAC_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.downloadedPath will hold the same value as before clearDownloadState() resets it to null. This should work correctly since the flow is null → path → null → path. However, there is a subtle timing concern: clearDownloadState() on line 164 sets the path back to null, which means the next LaunchedEffect recomposition will see null and skip. On the subsequent download completion, it will see the path again and trigger. This looks correct.

One concern: viewModel.clearDownloadState() and showDownloadDialog = false are both executed in sequence, but the dialog dismissal triggers recomposition which could race with the state update. Consider whether clearDownloadState should be called after setting pendingLoraPath to 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-interactive SuggestionChip with empty onClick may confuse users.

These chips look tappable but do nothing. Consider either using a plain Text/surface composable instead, or wiring the onClick to 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 match LoraAdapterEntry.id values in LoraAdapterCatalog. 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 when error is 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.
Deriving supportsLora from name is brittle and not type-safe. Prefer a structured field (e.g., ModelInfo.supportsLora or 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 than state + 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 + 350m name 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_compatibility returns bool plus a mutable std::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.

Comment on lines +5 to +16
<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" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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
done

Repository: 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"
fi

Repository: 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.

Comment on lines +720 to +748
```
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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).

Comment on lines +793 to +802
TextButton(
onClick = {
if (urlInput.isNotBlank() && nameInput.isNotBlank()) {
onDownloadFromUrl(urlInput.trim(), nameInput.trim())
}
},
enabled = urlInput.isNotBlank() && nameInput.isNotBlank(),
) {
Text("Download")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +136 to +168
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}",
),
)
}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +247 to +275
/**
* 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
/**
* 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.

Comment on lines +266 to +280
/**
* @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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +64 to +67

/** 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 2

Repository: 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 30

Repository: 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 1

Repository: 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 5

Repository: 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 2

Repository: 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.cpp

Repository: 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 -20

Repository: 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).

Comment on lines +889 to +939
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -20

Repository: 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 -5

Repository: 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.cpp

Repository: 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.h

Repository: 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 -30

Repository: 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.cpp

Repository: 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 -80

Repository: 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 -40

Repository: 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 -100

Repository: 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 -80

Repository: 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 -100

Repository: 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.cpp

Repository: 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.cpp

Repository: 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.cpp

Repository: 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.cpp

Repository: 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.cpp

Repository: 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 -40

Repository: 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 -50

Repository: 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.cpp

Repository: 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.

Comment on lines +61 to +69
* 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find all occurrences of checkLoraCompatibility - declaration, implementations, and call sites
rg -n "checkLoraCompatibility" -B 2 -A 2

Repository: 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 -20

Repository: 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 -100

Repository: 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.

Suggested change
* 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.

Comment on lines +26 to +142
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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Siddhesh2377 and others added 2 commits February 23, 2026 01:47
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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we displaying these anywhere within the Ui so the user knows that these are th set of prompts applied ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet, do u want me to display them ?

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Avoid hard‑coded model name heuristics for LoRA capability.

isLoraCompatibleModel is derived from nameLower.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.

Comment on lines +252 to 261
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"),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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 = false

Then 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.

Comment on lines +409 to +447
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +12 to +13
import com.runanywhere.sdk.temp.LoraAdapterCatalog
import com.runanywhere.sdk.temp.LoraAdapterEntry
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +79 to +141
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

CancellationException swallowed, and IO loop is not cancellation-cooperative.

Two related issues:

  1. Swallowed cancellation (lines 130–141): CancellationException is a subtype of Exception. When the flow collector cancels, withContext(Dispatchers.IO) throws CancellationException, which is caught here, emitting a spurious ERROR state and logging a false "LoRA download failed" error. As a rule of thumb, CancellationException should always be rethrown, even if all other exceptions are caught.

  2. Non-cooperative IO loop (lines 79–101): The while loop performs blocking Java IO with no isActive guard. 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. Because input.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.

Comment on lines +17 to +119
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

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

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: read

below 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.

Suggested changeset 1
.github/workflows/publish-maven-central.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/publish-maven-central.yml b/.github/workflows/publish-maven-central.yml
--- a/.github/workflows/publish-maven-central.yml
+++ b/.github/workflows/publish-maven-central.yml
@@ -1,5 +1,8 @@
 name: Publish to Maven Central
 
+permissions:
+  contents: read
+
 on:
   workflow_dispatch:
     inputs:
EOF
@@ -1,5 +1,8 @@
name: Publish to Maven Central

permissions:
contents: read

on:
workflow_dispatch:
inputs:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +22 to +39
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

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

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: and on:), add:
    permissions:
      contents: read
  • This leaves all job logic intact, but ensures the GITHUB_TOKEN is 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.

Suggested changeset 1
.github/workflows/web-sdk-release.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/web-sdk-release.yml b/.github/workflows/web-sdk-release.yml
--- a/.github/workflows/web-sdk-release.yml
+++ b/.github/workflows/web-sdk-release.yml
@@ -1,5 +1,8 @@
 name: Web SDK - Build & Release
 
+permissions:
+  contents: read
+
 on:
   push:
     paths:
EOF
@@ -1,5 +1,8 @@
name: Web SDK - Build & Release

permissions:
contents: read

on:
push:
paths:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +40 to +76
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

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

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: read

This 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.

Suggested changeset 1
.github/workflows/web-sdk-release.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/web-sdk-release.yml b/.github/workflows/web-sdk-release.yml
--- a/.github/workflows/web-sdk-release.yml
+++ b/.github/workflows/web-sdk-release.yml
@@ -17,6 +17,9 @@
         required: false
         default: 'all'
 
+permissions:
+  contents: read
+
 jobs:
   typecheck:
     name: TypeScript Typecheck
EOF
@@ -17,6 +17,9 @@
required: false
default: 'all'

permissions:
contents: read

jobs:
typecheck:
name: TypeScript Typecheck
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +77 to +100
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

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

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.

Suggested changeset 1
.github/workflows/web-sdk-release.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/web-sdk-release.yml b/.github/workflows/web-sdk-release.yml
--- a/.github/workflows/web-sdk-release.yml
+++ b/.github/workflows/web-sdk-release.yml
@@ -1,4 +1,6 @@
 name: Web SDK - Build & Release
+permissions:
+  contents: read
 
 on:
   push:
EOF
@@ -1,4 +1,6 @@
name: Web SDK - Build & Release
permissions:
contents: read

on:
push:
Copilot is powered by AI and may make mistakes. Always verify output.
}

// ========== YouTube Rules ==========
if (url.includes('youtube.com')) {

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
youtube.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

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, returns false.
    • Normalizes hostname to lowercase.
    • Returns true if hostname === 'youtube.com' or hostname.endsWith('.youtube.com'). Optionally, we can also consider hostname === '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.
  • 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 pathname and search and use them instead of url.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.

Suggested changeset 1
Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts b/Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts
--- a/Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts
+++ b/Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts
@@ -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');
       }
     }
EOF
@@ -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');
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
}

// ========== Google Rules ==========
if (url.includes('google.com')) {

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
google.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

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 a try/catch.
    • Derive a boolean isGoogleHost from parsed.hostname === 'google.com' || parsed.hostname.endsWith('.google.com').
  • Replace if (url.includes('google.com')) { with if (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.


Suggested changeset 1
Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts b/Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts
--- a/Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts
+++ b/Playground/on-device-browser-agent/src/background/agents/navigator-agent.ts
@@ -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 =>
EOF
@@ -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 =>
Copilot is powered by AI and may make mistakes. Always verify output.
const taskLower = task.toLowerCase();
return (
taskLower.includes('youtube') ||
url.includes('youtube.com') ||

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
youtube.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

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 URL class to parse the URL string inside canHandle and getState.
  • Extract hostname and check if it is exactly youtube.com or ends with .youtube.com. This preserves support for subdomains such as www.youtube.com and region-specific hosts like music.youtube.com, but rejects youtube.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), replace url.includes('youtube.com') with a small helper that:
    • Tries to construct new URL(url).
    • Returns true if the hostname is youtube.com or ends with .youtube.com, false otherwise.
  • In getState(dom, history), where const 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; URL is available in modern browser and Node runtimes. If the environment might not support URL globally, the project would need a polyfill, but we won’t edit anything outside this file.
Suggested changeset 1
Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts b/Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts
--- a/Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts
+++ b/Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts
@@ -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';
     }
 
EOF
@@ -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';
}

Copilot is powered by AI and may make mistakes. Always verify output.
const url = dom.url || '';

// Not on YouTube yet
if (!url.includes('youtube.com')) {

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
youtube.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

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 YouTubeStateMachine that, given a URL string, safely parses it (using the standard URL constructor) and checks the hostname against an explicit allowlist or suffix match.
  • Replace the substring checks url.includes('youtube.com') in both canHandle (line 36) and getState (line 55) with calls to this helper, such as this.isYouTubeUrl(url).

Implementation details for Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts:

  1. Add a private method inside YouTubeStateMachine (e.g., just below private 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.

  2. In canHandle, line 36, change url.includes('youtube.com') to this.isYouTubeUrl(url).

  3. In getState, line 55, change the negated YouTube check from if (!url.includes('youtube.com')) to if (!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.

Suggested changeset 1
Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts b/Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts
--- a/Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts
+++ b/Playground/on-device-browser-agent/src/background/agents/state-machines/youtube.ts
@@ -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';
     }
 
EOF
@@ -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';
}

Copilot is powered by AI and may make mistakes. Always verify output.
}

// 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

This check does not consider data: and vbscript:.

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.

Suggested changeset 1
Playground/on-device-browser-agent/src/content/action-executor.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Playground/on-device-browser-agent/src/content/action-executor.ts b/Playground/on-device-browser-agent/src/content/action-executor.ts
--- a/Playground/on-device-browser-agent/src/content/action-executor.ts
+++ b/Playground/on-device-browser-agent/src/content/action-executor.ts
@@ -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)
EOF
@@ -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)
Copilot is powered by AI and may make mistakes. Always verify output.

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

DOM text
is reinterpreted as HTML without escaping meta-characters.

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:

  1. Sets the icon using innerHTML from the trusted TOAST_ICONS string.
  2. Creates a span element.
  3. Sets span.textContent = message;.
  4. 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.

Suggested changeset 1
examples/web/RunAnywhereAI/src/components/dialogs.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/web/RunAnywhereAI/src/components/dialogs.ts b/examples/web/RunAnywhereAI/src/components/dialogs.ts
--- a/examples/web/RunAnywhereAI/src/components/dialogs.ts
+++ b/examples/web/RunAnywhereAI/src/components/dialogs.ts
@@ -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(() => {
EOF
@@ -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(() => {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +80 to +89
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

DOM text
is reinterpreted as HTML without escaping meta-characters.

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 the modelName usages:
    • At line 280, change the message argument to escape modelName via the existing escapeHtml helper.
    • At line 288, also escape modelName before passing it to showToast, since showToast uses innerHTML.
  • In examples/web/RunAnywhereAI/src/components/dialogs.ts, harden showConfirmDialog by HTML-escaping title, confirmLabel, and cancelLabel when building backdrop.innerHTML, using a new local escapeHtml helper. Leave message as-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.

Suggested changeset 2
examples/web/RunAnywhereAI/src/components/dialogs.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/web/RunAnywhereAI/src/components/dialogs.ts b/examples/web/RunAnywhereAI/src/components/dialogs.ts
--- a/examples/web/RunAnywhereAI/src/components/dialogs.ts
+++ b/examples/web/RunAnywhereAI/src/components/dialogs.ts
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+}
+
 /**
  * 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>
     `;
EOF
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

/**
* 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>
`;
examples/web/RunAnywhereAI/src/views/storage.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/web/RunAnywhereAI/src/views/storage.ts b/examples/web/RunAnywhereAI/src/views/storage.ts
--- a/examples/web/RunAnywhereAI/src/views/storage.ts
+++ b/examples/web/RunAnywhereAI/src/views/storage.ts
@@ -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();
       });
     });
EOF
@@ -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();
});
});
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +19 to +86
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

DOM text
is reinterpreted as HTML without escaping meta-characters.
DOM text
is reinterpreted as HTML without escaping meta-characters.

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 the container.innerHTML template so that the value attributes for #settings-api-key and #settings-base-url are empty (no ${settings.*} interpolation).
  • Right after the DOM has been created and the inputs queried (apiKeyInput and baseURLInput), explicitly assign their .value from settings.apiKey and settings.baseURL. This ensures those values are inserted as plain text properties, not re-parsed HTML.
  • Leave loadSettings as-is, since it already uses .value assignments and does not rely on initial value="..." attributes.

All of these changes are in examples/web/RunAnywhereAI/src/views/settings.ts. No new imports or helper methods are needed.

Suggested changeset 1
examples/web/RunAnywhereAI/src/views/settings.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/web/RunAnywhereAI/src/views/settings.ts b/examples/web/RunAnywhereAI/src/views/settings.ts
--- a/examples/web/RunAnywhereAI/src/views/settings.ts
+++ b/examples/web/RunAnywhereAI/src/views/settings.ts
@@ -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();
EOF
@@ -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();
Copilot is powered by AI and may make mistakes. Always verify output.

function saveSettings(): void {
try {
localStorage.setItem('runanywhere-settings', JSON.stringify(settings));

Check failure

Code scanning / CodeQL

Clear text storage of sensitive information High

This stores sensitive data returned by
an access to apiKeyInput
as clear text.
This stores sensitive data returned by
an access to apiKeyInput
as clear text.

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.


Suggested changeset 1
examples/web/RunAnywhereAI/src/views/settings.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/web/RunAnywhereAI/src/views/settings.ts b/examples/web/RunAnywhereAI/src/views/settings.ts
--- a/examples/web/RunAnywhereAI/src/views/settings.ts
+++ b/examples/web/RunAnywhereAI/src/views/settings.ts
@@ -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 */ }
EOF
@@ -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 */ }
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +195 to +202
app.innerHTML = `
<div class="error-view">
<div class="error-icon">&#9888;&#65039;</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

Exception text
is reinterpreted as HTML without escaping meta-characters.
Exception text
is reinterpreted as HTML without escaping meta-characters.

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 the

    has no${message}interpolation and instead includes an empty

    with the same classes and perhaps anid` or data attribute.

  • After setting innerHTML, query that <p> element and set its textContent = 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.

Suggested changeset 1
examples/web/RunAnywhereAI/src/main.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/examples/web/RunAnywhereAI/src/main.ts b/examples/web/RunAnywhereAI/src/main.ts
--- a/examples/web/RunAnywhereAI/src/main.ts
+++ b/examples/web/RunAnywhereAI/src/main.ts
@@ -196,11 +196,16 @@
     <div class="error-view">
       <div class="error-icon">&#9888;&#65039;</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();
EOF
@@ -196,11 +196,16 @@
<div class="error-view">
<div class="error-icon">&#9888;&#65039;</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();
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.