Commit Graph
17 Commits
Author SHA1 Message Date
M57andM57 040fefdd4b security: disable Android Auto Backup (issue #636) (#642)
Set android:allowBackup="false" to stop the OS from snapshotting
/data/data/com.m57.hermescontrol/ (server_store.json, SQLCipher DB,
EncryptedSharedPreferences tokens/cookies, PersistentCookieJar) into
the user's Google Drive. Removes a credential + chat-history exfil
surface via Google account compromise.

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-20 17:45:39 +03:00
M57andhy4ri 26b104b2bb [feature] issue #634: HTTPS + reverse-proxy server endpoint support (#638)
* [feature] issue #634: HTTPS + reverse-proxy server endpoint support

- Add ServerEndpoint model (scheme-aware base URL, resolve/webSocketUrl/relativeRequestPath, cleartext policy)
- AuthManager: baseUrl is canonical; setHost/setPort derive from endpoint; endpointForBuild() for scheme-aware URLs
- ApiClient/CookieManager/HermesWsClient: route through ServerEndpoint (no hardcoded http://)
- Data migration: ServerUrlMigration converts legacy host/port to baseUrl
- UI: Connect/AuthLogin/Settings now use a single Server URL field with transport warning
- Build: ALLOW_CLEARTEXT buildConfigField + manifest placeholder for dev
- Tests: ServerEndpointTest + ServerUrlMigrationTest; adapt existing suite to baseUrl API

Co-Authored-By: M57 <Hy4ri@users.noreply.github.com>

* fix(authlogin): basic-auth derives token automatically, no manual entry

connectBasicAuth() built a fresh OkHttpClient for the password-login call and
a different one for the ws-ticket call, so the Set-Cookie from
/auth/password-login (the session token lives in the cookie, not the JSON body)
never reached /api/auth/ws-ticket. That 401'd the ticket request, returned null,
and the UI fell back to asking the user to paste a token.

Both calls now use the shared OkHttpProvider.probe client (same CookieManager
jar), so the session cookie flows through and the WS ticket mints automatically —
matching the dashboard contract (routes.py: password-login sets Set-Cookie,
ws-ticket requires it).

Add AuthLoginBasicAuthFlowTest proving the cookie propagates on the shared
client (and that ws-ticket 401s without it).

Fixes the basic-auth token prompt reported against issue #634.

* chore: remove TASK-634-TESTS.md (stray task-tracking doc, not part of feature)

* ci: broaden .gitguardian.yml test-file ignores (unit + instrumented + variant dirs)

* feat(authlogin): smart auth-mode detection from /api/status + drop pairing-string login

- Replace 302-sniff probe with authoritative GET /api/status read
  (auth_required + auth_providers) to derive DashboardAuthMode.
- Add OAUTH mode (stubbed, 'coming soon' UI, TODO tracks issue #639).
- Render login UI by detected mode: token (loopback), username+password
  (basic/gated), or OAuth notice.
- Remove legacy pairing-string box + onPairingString/applyLegacyPairing.
- Extract internal deriveAuthMode() + add 5 unit tests.

Closes nothing; OAuth tracked in #639.

* fix(chat): don't block WS connect on empty token in gated (ticket) mode

connectWebSocket() gated on getToken() != null, correct for loopback
(token) mode but wrong for gated (ticket) mode: the ticket is minted
fresh by HermesWsClient.refreshWsTicketIfNeeded() from the persisted
session cookie, so getToken() is expected empty at connect time. The
early return left the WS unopened -> DISCONNECTED -> chat showed
'reconnect' immediately after basic-auth login.

Only require a non-blank token in non-gated mode.

Fixes #640.

* test: remove onPairingString test cases for deleted pairing-login path

The pairing-string login (onPairingString/applyLegacyPairing) was removed
from ConnectViewModel, so the 5 tests that exercised it (3 in
ConnectViewModelTest, 2 in E2eIntegrationTest) no longer compile. Remove
them. Also drop the now-orphaned connect_pairing_string from values-ko.

Fixes the Unit & Integration Tests CI failure on PR #638.

---------

Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
2026-07-20 17:26:10 +03:00
M57andHermes Agent 4849f4c1c6 feat(#195): chat attachment support for files and images (#418)
* feat(#195): add chat attachment support for files and images

- Add Attachment data model with computed properties (isImage, formattedSize, fileExtension)
- Extend ChatMessage with optional attachments list
- Add Coil 2.7.0 for async image loading in Compose
- Extend ChatViewModel with pending attachment state, addAttachment/removeAttachment/clearAttachments
- Build attachment-prefixed message text (data URIs for images, file refs for others)
- Add paperclip button and file picker launcher (ActivityResultContracts.GetContent) to ChatScreen
- Add animated attachment preview chips with thumbnails before sending
- Render attachments inline in UserBubble (AsyncImage for images, file cards for others)
- Add string resources for attachment UI features

* fix(#195): rewrite attachment upload protocol to match Hermes backend RPC methods

BREAKS: removes old buildAttachmentText() that embedded base64 data URIs in prompt text.

Changes:
- Replace inline data-URI encoding with proper backend RPC flow:
  - image.attach_bytes for images (fire-and-forget, auto-queues to session)
  - file.attach for non-image files (awaited, returns @file: ref text)
- Add CompletableDeferred to PendingRequest so sendMessage can await RPC responses
- Add readContentUriBytes() to read file content from content:// URIs
- Add sendRpcAndAwait() suspend function for sequential RPC calls
- Add RpcCallException for awaited RPC error handling
- Suppress error UI for awaited RPCs (caller handles them directly)
- Keep ChatScreen/ChatBubble UI layer unchanged (file picker, previews, inline images)

* feat(#195): add camera capture support via FileProvider + TakePicture contract

- Creates temporary photo files in cacheDir via FileProvider
- Launches system camera with ActivityResultContracts.TakePicture
- Adds camera icon button (📷) next to the paperclip in ChatInputBar
- Captured photos added as image/jpeg attachments via same addAttachment flow
- FileProvider configured with cache-root path in res/xml/file_paths.xml
- Error handling for launch failures and capture failures with snackbar

* refactor(#195): group camera/image/file into single attach dropdown

- Replaces separate 📷 and 📎 buttons with a single 📎 button
- DropdownMenu opens with three options:
  - 📷 Camera → TakePicture via FileProvider temp URI
  - 🖼️ Image → GetContent("image/*")
  - 📄 File → GetContent("*/*")
- Removes onAttachClick, adds onImageTap/onFileTap callbacks
- Input bar stays clean with one attachment affordance

* fix(#195): add missing imports for CI compilation

ChatScreen.kt:
- android.util.Log
- androidx.compose.animation.core.* (infiniteRepeatable, tween, LinearEasing, RepeatMode, rememberInfiniteTransition)
- androidx.compose.foundation.layout.widthIn
- androidx.compose.material.icons.filled.AttachFile
- androidx.compose.material.icons.filled.InsertDriveFile
- androidx.compose.ui.layout.ContentScale
- coil.compose.AsyncImage

ChatBubble.kt:
- androidx.compose.material.icons.filled.InsertDriveFile

* fix(#195): resolve android-lint LocalContextGetResourceValueCall error

Replace context.getString(R.string.chat_camera_error) with a
composable-level cameraErrorMsg captured via stringResource()
to avoid querying resources via LocalContext.current in a callback.

* fix(#195): migrate InsertDriveFile to AutoMirrored, fix chip double-remove

- Replace deprecated Icons.Filled.InsertDriveFile with
  Icons.AutoMirrored.Filled.InsertDriveFile in ChatBubble.kt
  and ChatScreen.kt
- Remove onClick from AttachmentChip Surface so only the
  dedicated close button triggers removal, not accidental taps
  on the chip body

---------

Co-authored-by: Hermes Agent <hermes@agent.local>
2026-06-29 16:33:12 +03:00
M57 42a67bffa9 feat(#194): add Speech-to-Text voice dictation support in chat (#413)
* feat(#194): add Speech-to-Text voice dictation support in chat

* fix(#194): resolve android lint warnings and update compose UI test

* fix(#194): remove invalid assertDoesNotExist import
2026-06-28 00:47:24 +03:00
M57andM57 42fcf2a14b fix: implement in-memory token caching to avoid redundant prefs decryptions (#320)
* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.
- Fixed merge conflicts and ensured code still tests well.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

* fix: implement in-memory token caching to avoid redundant prefs decryptions

Resolves #277

Previously, the `AuthManager.getToken()` method decrypted the entire
`EncryptedSharedPreferences` file on every call. Because this method is
used in the OkHttp interceptor on every HTTP/WS request, and frequently
during navigation, it introduced 10-30ms of redundant work per call.
This compounded to noticeable UI lag (e.g., during chat session switches).

This commit implements thread-safe in-memory caching for the auth token
inside `AuthManager`.
- Added a `@Volatile` `cachedToken` and `tokenInitialized` flag.
- Modified `getToken()` to return the cached value after initial evaluation,
  using double-checked locking (`synchronized(this)`) to avoid race conditions.
- Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate
  or update the cache.
- Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching
  behavior and proper invalidation upon profile switches.
- Fixed GitGuardian warning by removing the "my_secret_token" from the test.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 22:19:35 +03:00
M57andM57 eb71ae3a46 Fix WebSocket events being silently dropped on buffer overflow (#313)
* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix ktlint formatting issues in HermesWsClient.kt

The formatting changes adhere to ktlint rules required by the CI pipeline:
- Removed a trailing comma in MutableSharedFlow initialization to avoid multiline expression wrapping errors.
- Sorted imports to be in lexicographical order.

These formatting changes fix the failing CI check.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 22:13:55 +03:00
M57andM57 2c6e437a5b Fix: Prevent AuthManager state pollution on connection verification (#348)
* Fix: Prevent AuthManager state pollution on connection verification

Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.

Fixes #260

* Fix: Prevent AuthManager state pollution on connection verification

Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.

Fixes #260

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 20:49:58 +03:00
M57 2c52263772 Fix data exposure via Android backup by adding proper exclusion rules (#339)
This commit addresses issue #257 (SEC-04) where `allowBackup="true"` was enabled but the extraction rules files had empty templates, exposing sensitive database and shared preferences files.

Changes made:
- Updated `AndroidManifest.xml` to include `android:dataExtractionRules` and `android:fullBackupContent` on the `<application>` element.
- Updated `res/xml/backup_rules.xml` and `res/xml/data_extraction_rules.xml` to specifically exclude the Room database (`hermes_control.db`) and EncryptedSharedPreferences file (`hermes_secure_prefs.xml`) from both cloud backup and device transfer operations.

Reasoning:
This prevents extraction of these sensitive files via `adb backup` or Google cloud backups on both legacy and API 31+ devices, mitigating a high-severity security vulnerability without breaking legitimate application backup capabilities.

Impact:
This secures the sensitive app data. No functional changes to the codebase logic were made.

Fixes #257
2026-06-23 12:42:15 +03:00
M57 bfef3613fa fix(#240): parse session_id from params root (#242)
* fix(#240): parse session_id from params root

Allows correct session mapping when opening app via notification.

* fix(#240): set MainActivity launchMode to singleTop

Prevents activity destruction and WebSocket reconnection when tapping notifications.

* fix(#240): save inline notification replies to Room database

* fix(#240): refresh current session when app comes to foreground

* fix(#240): use uppercase role string to match Mapper mapping expectations

* style: format NotificationReplyReceiver.kt to satisfy ktlint
2026-06-21 19:04:54 +03:00
M57 4e8da7b90c fix(security): allow cleartext traffic globally to support LAN IP connections (#200)
* feat(skills): add category and status filters (#111)

* fix(security): allow cleartext traffic globally to support LAN IP connections
2026-06-21 01:09:28 +03:00
M57 d1f4e9eb67 fix(#67): restrict cleartext traffic to local subnets and Tailscale range (#178) 2026-06-20 23:17:00 +03:00
M57 54a195bd94 fix(auth): initialize AuthManager in custom application class (#172)
Closes #68. Redundant initialization in MainActivity has been removed.
2026-06-20 17:43:40 +03:00
M57andM57 ca21e22147 ci(#71): change FGS type from dataSync to remoteMessaging for indefinite WebSocket listener (#162)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-20 14:07:46 +03:00
Hy4ri ce5428f66b feat(#116): add direct reply action to background notifications 2026-06-19 23:44:40 +03:00
M57andM57 5130c2e308 feat(chat): message persistence + background notifications (#2)
* feat(chat): message persistence + background notifications

Two critical gaps for daily-driver use:

1. Message persistence (Room database):
   - New ChatMessageEntity / ChatMessageDao / HermesDatabase layer
   - Every user message, streaming token, tool result, and assistant
     reply is written to a local Room DB as it arrives
   - switchSession loads cached messages instantly from Room before
     the REST call returns — no more blank screen while loading
   - Session resume also loads from cache first
   - Messages survive app kills, process death, and configuration
     changes
   - ChatViewModel changed from ViewModel() to AndroidViewModel(app)
     to access the database context

2. Background notifications (foreground service):
   - ChatNotificationService keeps the WebSocket alive while the app
     is backgrounded and posts a high-priority notification when a
     MessageComplete or ClarifyRequest event arrives while the app
     is not in the foreground
   - Lifecycle wired via LifecycleEventObserver in ChatScreen:
     ON_STOP starts the service, ON_START stops it
   - Tapping the notification returns to the chat
   - New permissions: POST_NOTIFICATIONS, FOREGROUND_SERVICE,
     FOREGROUND_SERVICE_DATA_SYNC

Build changes:
   - Added Room 2.7.1 + KSP to version catalog
   - Added WorkManager 2.10.0 (reserved for future polling fallback)
   - Updated ChatViewModelTest to mock HermesDatabase + use
     AndroidViewModel constructor

* fix(build): use KSP 2.3.9 (new versioning scheme for Kotlin 2.3.x)

* fix(build): add Room schemaDirectory (required by Room Gradle plugin)

* fix(compile): add else branch to WsEvent when expression (exhaustiveness)

* ci(release): add release workflow + R8 minification + proguard rules

New .github/workflows/release.yml:
- Triggers on tag pushes (v*) and manual workflow_dispatch
- Runs full CI (ktlint + lint + test) before building
- Builds assembleRelease with version injection from tag
- Creates a GitHub Release with auto-generated changelog
- Attaches the release APK to the release

Build changes:
- Release builds now use R8 minification (isMinifyEnabled=true)
  and resource shrinking (isShrinkResources=true) for smaller APKs
- versionCode/versionName injectable via -PversionCode / -PversionName
  Gradle properties (passed by CI from the git tag)
- Local dev builds default to versionCode=1, versionName=1.0-dev

ProGuard rules:
- Keep all data model classes (Gson reflection)
- Keep Room entities and RoomDatabase subclasses
- Keep OkHttp/Retrofit interface methods
- Suppress warnings for Compose, Security Crypto, Coroutines

* feat(ci): release workflow + proguard rules + test fix

Release workflow (.github/workflows/release.yml):
  - Triggers on tag push (v*) or manual workflow_dispatch
  - Two jobs: ci (ktlint+lint+test) → build-release
  - Builds a minified, shrunk release APK with version from tag
  - Creates a GitHub Release with auto-generated release notes
  - Attaches the APK as a release asset

Build config:
  - versionCode/versionName now injected from CI via -P flags
  - Release build enables isMinifyEnabled + isShrinkResources
  - proguard-rules.pro: keep rules for Gson models, Room entities,
    OkHttp/Retrofit interfaces, Coroutines, Compose, BuildConfig

Test fix:
  - ChatViewModelTest: use coEvery for suspend DAO methods instead
    of every (compile error — suspend functions need coEvery)
  - Added stubs for all suspend DAO methods used by the ViewModel

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 03:23:38 +03:00
Hy4ri 69f36b1606 fix: resolve double-padding on chat screen when keyboard is opened 2026-06-16 00:34:45 +03:00
Hy4ri d6ff3de594 Initial commit of HermesControl Android app 2026-06-16 00:00:36 +03:00