Commit Graph
21 Commits
Author SHA1 Message Date
M57 dbd7008ee3 feat(chat): support non-Google speech-to-text providers (fixes #1038) (#1042) 2026-09-02 12:31:42 +03:00
M57andHy4ri 59bcbdfe3c build: Android 17 (API 37) + okhttp 5.5.0 + LAN local-network permission gate (#941)
* build: compileSdk 36 -> 37 + okhttp 5.4.0 -> 5.5.0

Take okhttp 5.5.0 (folds in dependabot PR #940) which hard-requires
compileSdk 37 via AAR metadata. targetSdk stays 36 for now.

* build: targetSdk 36 -> 37 (Android 17) + ACCESS_LOCAL_NETWORK gating

Opt into Android 17 runtime behavior. The app connects to a gateway
(default 127.0.0.1:9119, plus LAN + remote), so targeting 37 forces the
new ACCESS_LOCAL_NETWORK permission for LAN hosts.

- AndroidManifest: declare ACCESS_LOCAL_NETWORK (harmless below API 37)
- LocalNetwork.kt: pure detector + needsLocalNetworkPermission(baseUrl)
  (loopback + public remote exempt; RFC1918/link-local/CGNAT gated)
- ConnectViewModel: requestConnect() gates on the permission; screen
  launches the prompt; onLanPermissionResult proceeds or shows a clear error
- flake.nix: android platform 36 -> 37
- Tests: LocalNetworkTest + ConnectPermissionGateTest (SDK_INT / permission
  branching mocked, no device needed)
- Task 8 finding: assistant services (VoiceInteraction*/Recognition) and the
  remoteMessaging ChatNotificationService do NOT hit the Android 17 mic/
  while-in-use foreground hardening, so no change required.

* fix: ktlint + @Composable LocalContext hoist (was blocking CI)

- ConnectScreen: hoist LocalContext.current out of onClick lambda
  (calling a @Composable inside a non-composable lambda broke compileDebugKotlin)
- ktlint --format: multiline-expression-wrapping, function-signature,
  no-empty-first-line-in-class-body, no-unused-imports
- LocalNetwork.kt: merge consecutive KDocs (no-consecutive-comments)

flake.nix buildToolsVersion/platformToolsVersion intentionally stay 36.0.0
— they are decoupled from compileSdk; only platformVersions needs 37.

* fix: suspend mock (coEvery) + flake toolchain pinned to 37.0.0

- ConnectPermissionGateTest: getStatus() is suspend → use coEvery (was
  breaking compileDebugUnitTestKotlin)
- flake.nix: buildToolsVersion/platformToolsVersion/cmdLineToolsVersion
  pinned to 37.0.0 (verified against pinned nixpkgs 241313f — resolves to
  android-sdk-*-37.0.0 derivations; platformVersions already 37)

* fix(flake): revert cmdLineToolsVersion to 11.0 (not API-aligned)

cmdline-tools uses its own versioning (latest = 21.0, never 37.0.0) —
nixpkgs rejects 37.0.0 with 'version missing'. build-tools + platform-tools
stay 37.0.0 (verified resolve to android-sdk-*-37.0.0). flake now evals
clean against pinned nixpkgs 241313f.

* flake: track nixos-unstable + pin toolchain to latest (cmdline 22.0, platform-tools 37.0.1, build-tools 37.0.0, platform 37.1)

- inputs.nixpkgs -> nixos-unstable, flake.lock bumped to rev ffb3c9b7 (2026-08-19)
- cmdLineToolsVersion 11.0 -> 22.0 (own versioning, not API-aligned; v22_0 confirms)
- platformToolsVersion 37.0.0 -> 37.0.1
- buildToolsVersion stays 37.0.0
- platformVersions 37 -> 37.1
- verified: nix flake check --no-build -> all checks passed on hyari

* fix: unit tests — Uri stub + Build.SDK_INT final-field mock

- LocalNetwork.kt: needsLocalNetworkPermission uses java.net.URI instead of
  android.net.Uri (latter is a non-functional stub in JVM unit tests, its host
  returns null -> gate always false). Added pure requiresLocalNetworkPermission(sdkInt, baseUrl).
- ConnectViewModel: inject sdkVersion (package-private) instead of reading the
  final static Build.VERSION.SDK_INT directly; gate via requiresLocalNetworkPermission.
- ConnectPermissionGateTest: drop mockkStatic(Build.VERSION) (MockKException on
  final static field, and against repo convention); inject sdkVersion; add pure
  gate-decision matrix. Fixes 7 red tests.

* fix(test): remove illegal ':' from test function names (compile error)

* fix(test): order getString mocks so specific resources win over wildcard

* fix(flake): include android-37.0 system image so the emulator can boot

Google only publishes the android-37.0 system image (no 37.1 image exists
yet). With platformVersions=["37.1"] composeAndroidPackages found no matching
system image, leaving system-images/ empty and every AVD FATALing with
'Broken AVD system path'. Adding 37.0 pulls the published image so an AVD boots
(verified: hermes37 AVD boots Android 17 / API 37 on hyari). compileSdk/targetSdk
stay 37.

Also bump AGENTS.md SDK note 36 -> 37 (was stale).

* fix(flake): platformVersions 37.1 -> 37.0

Drop 37.1; 37.0 provides both the SDK platform for compileSdk 37 and the
published android-37.0 system image the emulator needs.

* fix(flake): remove stray tool-scratch line leaked into flake.nix (line 116)

A terminal tool's /tmp scratch rename line got appended to flake.nix, making
it invalid Nix. Removed; flake now parses + 'nix flake check' passes on hyari.

* fix(build): pin buildToolsVersion 37.0.0 to match nix SDK

AGP defaulted build-tools to 36.0.0, which isn't in the nix-managed SDK
(only 37.0.0 is provisioned). Local ./gradlew builds then tried to *install*
36.0.0 into the immutable /nix/store and failed. Pin 37.0.0 so the local
nix devShell builds. CI uses its own SDK so unaffected.

---------

Co-authored-by: Hy4ri <Hy4ri@users.noreply.github.com>
2026-08-20 14:35:42 +03:00
M57andM57 f931565c0d feat(settings): in-app update from About tab (issue #867) (#875)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-10 20:02:42 +03:00
M57andM57 ed48e10ca6 feat: register as Android default digital assistant (#757) (#758)
* feat: register as Android default digital assistant

* fix: assistant role request fallback + always-attempt click

* remove: assistant role request row from Settings

The system-level assistant registration stays (VoiceInteractionService
+ ROLE_ASSISTANT, set via system Settings -> Default apps), but the
in-app Settings row is dropped: RequestRoleActivity fails to resolve
the caller on a plain startActivity ('Package name cannot be null or
empty'), so the button was a dead end. Removes the row, the
AssistantRole helper, and the now-unused strings.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-01 18:33:33 +03:00
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