Commit ced218d3 authored by Lisa (AI Assistant)'s avatar Lisa (AI Assistant)

chore: scaffold Hermes Node Android project

parents
Pipeline #322 canceled with stages
# Android / Gradle
.gradle/
build/
app/build/
local.properties
*.iml
.idea/
# Kotlin / Java
*.class
# Logs
*.log
# OS
.DS_Store
Thumbs.db
# Secrets / node-specific config
config.json
*.keystore
*.jks
.env
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
This project is licensed under the GNU General Public License version 3
or (at your option) any later version.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
# Hermes Node Android
Android node agent for the Hermes Node Protocol.
This is the Android sibling of the Linux `hermes-node-agent`: an outbound-only node that connects to the Hermes Node Gateway over WSS, registers Android-safe capabilities, and executes supported actions without requiring inbound firewall rules, SSH, or rooted devices.
## Current status
Project scaffold. No remote Git repository is configured yet; the GitLab remote will be added later when provided.
## Design goals
- **Protocol-compatible** with the existing Hermes Node Gateway JSON/WebSocket protocol.
- **Outbound-only** connection from Android to gateway: `wss://gateway:8765`.
- **No root requirement** for baseline Android operation.
- **Capability-first architecture** so Android can expose what is safe/available:
- `android_status` / device info
- notifications bridge, if explicitly enabled
- media/audio controls where Android APIs permit it
- camera capture only with explicit permission and visible UX
- file operations limited to app-accessible storage / SAF grants
- exec only for non-root shell where available, and disabled by default
- **User-owned configuration** inside app storage, never hardcoded tokens.
- **Visible foreground service** for persistent connectivity, because Android kills hidden background agents.
## Non-goals for the first milestone
- Root-only device control.
- Silent spyware-style capture.
- Full Linux `exec` parity.
- Bypassing Android permission prompts or background limits.
Android is a different security model, not just Linux with a touchscreen. We'll keep this boring and explicit rather than building a brittle hack pile.
## Proposed architecture
```text
Android App
├─ MainActivity: setup/status UI
├─ Foreground HermesNodeService
│ ├─ GatewayClient: OkHttp WebSocket connection + reconnect
│ ├─ ProtocolHandler: register, heartbeat, command dispatch
│ ├─ ConfigStore: encrypted/shared preferences
│ └─ CapabilityRegistry
│ ├─ StatusCapability
│ ├─ NotificationCapability
│ ├─ MediaCapability
│ ├─ CameraCapability
│ └─ ShellCapability (disabled by default)
└─ Android permissions declared per capability
```
## Repository layout
```text
.
├── app/ Android application module
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/net/nexlab/hermesnodeandroid/
│ └── res/
├── docs/
│ ├── ANDROID_CAPABILITIES.md
│ └── PROTOCOL_COMPATIBILITY.md
├── build.gradle.kts
├── settings.gradle.kts
└── README.md
```
## Build prerequisites
- Android Studio or Android SDK command-line tools
- JDK 17+
- Gradle/Android Gradle Plugin matching the checked-in build files
A Gradle wrapper is not checked in yet. Once the local Android tooling is confirmed, generate it with:
```bash
gradle wrapper
```
## License
GPL-3.0-or-later. See `LICENSE`.
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "net.nexlab.hermesnodeandroid"
compileSdk = 35
defaultConfig {
applicationId = "net.nexlab.hermesnodeandroid"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "0.1.0"
}
}
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="false"
android:label="Hermes Node"
android:supportsRtl="true"
android:theme="@style/Theme.HermesNodeAndroid">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".HermesNodeService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>
/*
* Hermes Node Android
* Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package net.nexlab.hermesnodeandroid
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener
class GatewayClient(
private val gatewayUrl: String,
private val token: String,
private val nodeName: String,
) {
private val client = OkHttpClient()
private var socket: WebSocket? = null
fun connect(listener: WebSocketListener) {
val request = Request.Builder()
.url(gatewayUrl)
.addHeader("Authorization", "Bearer $token")
.build()
socket = client.newWebSocket(request, listener)
}
fun send(text: String): Boolean = socket?.send(text) == true
fun close() {
socket?.close(1000, "Hermes Node Android stopping")
socket = null
}
}
/*
* Hermes Node Android
* Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package net.nexlab.hermesnodeandroid
import android.app.Service
import android.content.Intent
import android.os.IBinder
class HermesNodeService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// TODO: start foreground notification, load config, connect GatewayClient.
return START_STICKY
}
}
/*
* Hermes Node Android
* Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package net.nexlab.hermesnodeandroid
import android.app.Activity
import android.os.Bundle
import android.widget.TextView
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val text = TextView(this).apply {
text = "Hermes Node Android
Scaffold ready. Configuration UI comes next."
textSize = 18f
setPadding(32, 32, 32, 32)
}
setContentView(text)
}
}
/*
* Hermes Node Android
* Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package net.nexlab.hermesnodeandroid
object ProtocolMessages {
fun registration(nodeName: String): String = """
{
"type": "register",
"node_name": "$nodeName",
"version": "0.1.0-android",
"capabilities": ["android_status"],
"capability_info": {
"platform": "android",
"enable_exec": false,
"enable_camera_control": false,
"enable_audio_control": false
}
}
""".trimIndent()
fun heartbeat(timestampSeconds: Long): String = """
{
"type": "heartbeat",
"timestamp": $timestampSeconds
}
""".trimIndent()
}
<resources>
<string name="app_name">Hermes Node</string>
</resources>
<resources>
<style name="Theme.HermesNodeAndroid" parent="android:style/Theme.Material.Light.NoActionBar" />
</resources>
plugins {
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
# Android capabilities
Android capabilities must map to Android's permission and lifecycle model. Linux parity is not the goal; protocol compatibility with honest capability advertisement is.
## Baseline capability set
- `android_status`: app version, Android version, manufacturer/model, battery/network summary.
- `android_notification`: optional notification listener bridge; requires explicit Android settings grant.
- `android_media`: media session controls where permitted.
- `android_camera`: explicit capture flow only; requires camera permission and visible UX.
- `android_files`: limited app-private files first; SAF-backed access later.
- `exec`: disabled by default; Android shell is limited and not comparable to Linux node exec.
## Security posture
- No hidden background capture.
- No hardcoded tokens.
- WSS by default.
- Foreground service notification while connected.
- Per-capability enable/disable switches.
# Protocol compatibility
Hermes Node Android should speak the same JSON/WebSocket protocol as the Linux node agent where the semantics match.
## Shared messages
- `register`
- `register_ack`
- `heartbeat`
- `heartbeat_ack`
- command messages routed by capability
## Android-specific principle
The Android agent should advertise Android-specific capabilities rather than pretending to support Linux capabilities. Gateway-side tools can route to these capabilities once implemented.
## Initial registration example
```json
{
"type": "register",
"node_name": "phone",
"version": "0.1.0-android",
"capabilities": ["android_status"],
"capability_info": {
"platform": "android",
"enable_exec": false,
"enable_camera_control": false,
"enable_audio_control": false
}
}
```
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "hermes-node-android"
include(":app")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment