Skip to content

feat(android): add profile management for multi-squad support#158

Open
yuke-x68 wants to merge 2 commits into
yohey-w:mainfrom
yuke-x68:feat/multi-profile-support
Open

feat(android): add profile management for multi-squad support#158
yuke-x68 wants to merge 2 commits into
yohey-w:mainfrom
yuke-x68:feat/multi-profile-support

Conversation

@yuke-x68
Copy link
Copy Markdown

@yuke-x68 yuke-x68 commented May 28, 2026

Summary

Add profile management so a single Shogun companion app can monitor and
control multiple multi-agent setups (e.g. dev / prod, separate squads)
without reinstalling or re-entering connection details.

Motivation

The app currently stores a single SSH + session configuration. When the
same user runs multiple multi-agent setups in parallel (different hosts,
different tmux session names, different dashboard files), they have to
edit settings each time they switch context. This PR introduces named
profiles that bundle every per-setup setting and lets the user switch
between them from the bottom navigation bar.

Changes

  • Data layer
    • data/Profile.kt — Profile data class holding SSH, session and
      dashboard config per profile
    • data/ProfileRepository.kt — SharedPreferences-backed persistence
  • State layer
    • viewmodel/ProfileViewModel.kt — single-setter pattern so
      activeProfile updates always sync SharedPreferences in one place
    • viewmodel/DashboardViewModelFactory.kt — wires the repository into
      the dashboard view model
  • UI layer
    • MainActivity.kt — move profile switcher from TopAppBar to
      BottomNavigation; key(activeProfile?.id) forces clean re-init on
      profile change
    • ui/SettingsScreen.kt — profile list with create / duplicate /
      delete; active-profile deletion is blocked with a Toast
    • ui/DashboardScreen.kt — SwipeRefresh gated by WebView scroll
      position (avoids the WebView-vs-Compose nested-scroll conflict)
    • ui/ShogunScreen.kt, ui/AgentsScreen.kt — re-connect when the
      active profile changes
  • Misc
    • util/Constants.kt — add dashboard.md default file name
    • viewmodel/ShogunViewModel.kt — use tmux window index 0 instead
      of the hard-coded main, so the app works with a wider range of
      tmux layouts
    • app/build.gradle.kts — bump versionCode to 3, versionName to 4.2

Testing

  • Unit tests added/updated — N/A (Android-only change; existing
    bats unit / integration tests do not cover Android code)
  • Integration tests pass — N/A for the same reason
  • make check passes (generated instructions in sync)
  • Manually tested: built debug APK, side-loaded, exercised
    profile create / duplicate / delete / switch flows, verified
    Shogun / Agents / Dashboard tabs reflect the active profile,
    confirmed pull-to-refresh on Dashboard

Note: make lint (shellcheck) and make test (bats) were not run
locally because this PR touches only Kotlin sources; CI will run them.

Screenshots

(Lord, please attach screenshots of the new BottomNav profile chip and
the Settings profile list if desired.)

Related Issues

None.

備考

私の手元にある、某白い悪魔のいるSF世界観に魔改造されたmulti-agent-shogunは複数部隊運用を可能にしているので、アプリから全部隊へアクセスできるようにするためにアプリも魔改造しました。
Xを検索した限り、複数軍運用できるように魔改造されている人は他にもいるようなので、この機能には需要があるかなと思っています。

なお、アクセス先IPとポート単位でプロファイルを作れるようにしているので、複数PCや仮想マシンにmulti-agent-shogunを導入している方であれば、複数軍運用が可能です。

私の手元には正規Androidビルドに必要な証明書がないため、承認いただいた暁には、お手数ですがreleaseビルドをお願い致します。

初めてのコントリビュートで緊張していますが、なにとぞよろしくお願いします。
なお、この備考以外の文言は艦長(=将軍ポジのエージェント)が書きました。いい時代ですね、戦国と宇宙世紀ですけど。

- Add Profile data class storing SSH, session and dashboard config per profile
- Centralize ProfileViewModel state mutations through a single setter so
  activeProfile updates always sync SharedPreferences in one place
- Move profile switcher from TopAppBar dropdown to BottomNavigation
- Add profile list UI in Settings (create / duplicate / delete)
- Add per-profile dashboard filename (defaults to dashboard.md)
- Block deletion of the currently active profile with a Toast warning
- Add SwipeRefresh on Dashboard, gated by WebView scroll position
- Use tmux window index 0 instead of hardcoded "main" for the shogun
  session target to support a wider range of tmux layouts
- Bump versionCode to 3 and versionName to 4.2

Allows monitoring multiple multi-agent setups (e.g. dev/prod or
separate squads) from a single companion app instance.
Copy link
Copy Markdown
Owner

@yohey-w yohey-w left a comment

Choose a reason for hiding this comment

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

Good addition, @yuke-x68. The change is well scoped — all modifications stay inside android/, the core shogun system is untouched.

What's covered:

  • Profile data class + ProfileRepository for persistence (Room/DataStore pattern)
  • ProfileViewModel drives the profile picker UI
  • ShogunScreen / AgentsScreen / DashboardScreen / SettingsScreen all wired to the active profile's SSH credentials
  • Constants.kt updated for multi-profile support
  • MainActivity.kt bootstraps the profile context

The use case is real — users running dev/prod environments or separate squads genuinely need profile switching without reinstalling.

Can't build-test Android from here, but the architecture (ViewModel + Repository + profile-scoped SSH credentials) looks sound. Merging is safe from the core-system perspective.

Copy link
Copy Markdown
Owner

@yohey-w yohey-w left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution, @yuke-x68 — the profile management feature is well-structured and the motivation is clear. One security issue needs to be addressed before merge.

Required change: sshPassword plain-text storage

Profile.kt includes:

val sshPassword: String = ""

This gets serialized to JSON and saved in SharedPreferences, which is unencrypted app-local storage. On rooted devices or via ADB backup, this is readable without special privileges.

Please replace SharedPreferences with EncryptedSharedPreferences (Jetpack Security) for the profile store:

// build.gradle.kts — add dependency
implementation("androidx.security:security-crypto:1.1.0-alpha06")

// ProfileRepository.kt — use EncryptedSharedPreferences
val prefs = EncryptedSharedPreferences.create(
    context,
    "shogun_profiles",
    MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

This encrypts the entire profile store (not just the password field), which is the right scope since SSH host/user/key paths are also sensitive.

Alternatively, if SSH key auth is the primary use case and password auth is rarely needed, removing sshPassword from the data class entirely and asking users to rely on key-based auth would also be acceptable.

Everything else looks good — the BottomNav profile switcher, create/duplicate/delete flow, active-profile deletion guard, and the tmux window index fix are all solid. Happy to re-approve once the storage concern is addressed.

Address the plaintext credential storage flagged in PR review by
migrating the entire PREFS_NAME store to EncryptedSharedPreferences.
This covers both storage paths that held the SSH password in plaintext:

- PROFILES_JSON via ProfileRepository (the original review finding)
- the flat SSH_PASSWORD key written by ProfileViewModel.syncToPrefs and
  read back by AgentsScreen/ShogunScreen (an additional path found during
  investigation)

A single central EncryptedPrefsProvider (AES256_GCM / AES256_SIV) backs
all consumers, so encrypting once seals both paths.

The sshPassword field is retained: SshManager uses it as the passphrase
for passphrase-protected SSH keys (SshManager.kt L80-81), so removing it
would break key-based auth for those users.

PreferencesMigration transparently moves legacy plaintext data into the
encrypted store on first launch and securely erases the old XML
(commit() ordering hardened against OOM-kill data loss).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yuke-x68
Copy link
Copy Markdown
Author

yuke-x68 commented Jun 7, 2026

Thank you for the security review

Thank you for the thorough security review and for identifying the plaintext storage vulnerability in our SharedPreferences implementation. We take security concerns seriously and appreciate your diligent oversight.


Analysis and Additional Findings

Upon conducting a deeper investigation into the identified issue, we discovered an additional vulnerability beyond the original concern:

Original Issue (Route A)

  • Location: ProfileRepository class
  • Problem: PROFILES_JSON containing the entire Profile object (including sshPassword field) is stored in plaintext in SharedPreferences
  • Risk: The sshPassword field is directly accessible without encryption

Additional Finding (Route B - Parallel Storage)

  • Location: ProfileViewModel.syncToPrefs() method
  • Problem: An independent storage path writes the SSH_PASSWORD field as a separate flat key directly to SharedPreferences
  • Critical Issue: Even if we encrypt Route A, Route B continues to store plaintext credentials, creating a dual-storage vulnerability
  • Impact: This means two independent plaintext copies of the SSH password could exist simultaneously

This dual-path design was not immediately obvious from a static review of ProfileRepository alone, which explains why it appeared as a single-vector issue initially.


Implemented Solution: Unified Encrypted SharedPreferences Migration

To comprehensively address both Route A and Route B, we have implemented the following solution:

1. Unified Encryption Strategy

The entire PREFS_NAME store has been migrated to EncryptedSharedPreferences

  • All sensitive data, including profile objects and credential flat keys, is now encrypted at the storage layer
  • This approach eliminates the dual-storage vulnerability in a single, unified fix
  • No plaintext credentials remain across any storage path within this shared preference instance

2. Preservation of sshPassword Field

As part of the encryption migration, we have retained the sshPassword field within the Profile object. Here's why:

Technical Reason: The sshPassword is actively used in SshManager.kt as a passphrase for the asymmetric key storage mechanism.

User Impact: Users who have configured passphrase-protected SSH keys depend on this field for successful SSH connections. Removing this field would break the following user workflow:

  • Users with passphrase-protected SSH keys (common security practice)
  • SshManager retrieves the passphrase from ProfileViewModel during key negotiation
  • Without the stored passphrase, authentication fails and users cannot connect

Forward Compatibility: By retaining the field but encrypting its storage, we maintain backward compatibility while securing the data.

3. User Migration Strategy

For existing users with plaintext data in SharedPreferences:

  • Detection: On first app launch after the update, the app detects legacy plaintext data
  • Automatic Migration: A migration handler transparently moves all data from plaintext SharedPreferences to EncryptedSharedPreferences
  • Data Consistency: The migration preserves all existing user profiles, credentials, and preferences
  • Cleanup: After successful migration, plaintext residual data is securely erased
  • Transparency: Users are not disrupted; the migration occurs transparently in the background

What Was Implemented

The implementation includes:

  1. EncryptedSharedPreferences setup and initialization logic
  2. Comprehensive migration handler with data consistency validation

Verification Performed

We have validated the implementation through:

  • Debug build compiles successfully (assembleDebug → BUILD SUCCESSFUL)
  • Verified on a physical device: clean install, profile creation, and SSH connection all work without regression
  • Internal code review confirming encryption reaches both Route A(ProfileRepository) and Route B (ProfileViewModel.syncToPrefs())
  • Code-level review confirming no plaintext storage paths remain
  • must_fix verification: 3 required fixes implemented

Follow-up: Automated Testing

We are prepared to add comprehensive automated testing in a follow-up iteration:

  • Unit tests covering both new-install and upgrade paths
  • Integration tests verifying encrypted data integrity across sessions
  • Security validation confirming no plaintext traces remain

Conclusion

This comprehensive solution addresses both the original concern and the additional vulnerability we identified. By unifying all sensitive data under EncryptedSharedPreferences while respecting legitimate use cases for the sshPassword field, we achieve a robust and maintainable security posture.

We welcome your feedback on this implementation.


Status: The changes are now ready for your re-review. Please let us know if you have any questions or if additional validation is needed.

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.

2 participants