Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions hives/wifi-p2p/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Gradle build artifacts
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar

# IDE files
.idea/
*.iml
*.iws
*.ipr
out/

# Android local
local.properties
*.apk
*.aab
*.apk.unsigned
*.aab.unsigned

# Kotlin metadata
*.kotlin_module
*.kjsm
3 changes: 3 additions & 0 deletions hives/wifi-p2p/Orchfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SERVICE wifi-p2p
RUN com.hum.hive.wifip2p/.WifiP2pBeeService
RESTART always
150 changes: 150 additions & 0 deletions hives/wifi-p2p/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
title: "wifi-p2p"
description: "hum-over-WiFi-Direct — peer-to-peer agent mesh on Android's WifiP2p radio"
---

# wifi-p2p

> _hum-over-WiFi-Direct — peer-to-peer agent mesh on Android's WifiP2p radio_

A **forager** bee that bridges Android WiFi Direct (WPA2-PSK P2P groups)
into thrum. Two phones in the same room discover each other by service
scan, form a P2P group, and exchange hum tones over TCP — no router,
no cellular, no internet.

Each phone runs its own local humd (in Termux or on a companion
machine). The bee translates incoming P2P messages into `chi:"prompt"`
to humd and routes `chunk`/`finish` replies back over the P2P link.
Conversations are continuous per peer — the sid is keyed off the
peer's Ed25519 identity (its hid), stable across link drops and
reconnects.

## Propensity

| statefulness | richness | wire shape | hides |
|---|---|---|---|
| convention-stateful (per-peer sid) | lean | WiFi Direct P2P TCP (`p2p0` interface) | tools, system prompts, perf, drone, breath |

## Wire

```
┌─ Phone A (Group Owner) ─────────────────┐
│ │
│ Peer ── TCP :4377 ───► wifi-p2p bee │
│ (Phone B) p2p0 │ │
│ │ chi:"prompt" │
│ ▼ │
│ humd │
│ │ │
│ │ chunk/finish │
│ ▼ │
│ Peer ◄── TCP :4377 ──── wifi-p2p bee │
│ (Phone B) p2p0 │ │
└──────────────────────────────────────────┘
```

## How it works

1. **Service discovery**. Phone A registers `_hum._tcp` on WiFi Direct
and starts scanning. Phone B's bee sees the service and initiates a
connection. The TXT record carries the phone's humd capabilities.

2. **Group formation**. One phone becomes the Group Owner (GO); the
other connects as a client. The GO opens a TCP server on
`p2p0:4377`; the client connects to the GO's P2P IP.

3. **Tone exchange**. Both sides speak NDJSON over the P2P TCP
socket — the same framing as thrum. Each tone carries a `sid`
keyed off the peer's hid, so the conversation survives
disconnect-reconnect cycles.

4. **Local humd bridge**. Each bee connects to its local humd via
Unix socket (Termux) or TCP bridge and translates inbound P2P
tones into thrum prompts. Chunks are collected and the final
reply is sent back over the P2P link.

## Configure

| env (Android, via `setprop` or config) | default | what |
|---|---|---|
| `HUM_P2P_PORT` | `4377` | TCP port on `p2p0` interface |
| `HUM_P2P_SERVICE_TYPE` | `_hum._tcp` | Bonjour-style service type for discovery |
| `HUM_P2P_MODEL` | `claude-haiku-4.5` | model humd spawns |
| `HUM_P2P_SYSTEM` | default system prompt | system instruction |
| `HUM_P2P_REPLY_LIMIT` | `4096` | hard cap on reply length |
| `HUM_THRUM_SOCK` | Unix socket or TCP bridge | thrum connection to humd |
| `HUM_P2P_GO_INTENT` | `8` | group owner intent (higher = more likely to be GO) |

## Permissions

Android manifest requires:

```xml
<uses-permission android:name="android.permission.NEARBY_WIFI_PEERS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
```

## Build

```bash
cd hives/wifi-p2p
gradle build
# produces app/build/outputs/apk/debug/app-debug.apk
```

## Install (via hum managed service)

If humd and orchd are running on the Android device (Termux):

```bash
hum hive install ./hives/wifi-p2p
```

Or sideload the APK and start the service manually:

```bash
adb install app/build/outputs/apk/debug/app-debug.apk
adb shell am start-foreground-service \
-n hum.hive.wifip2p/.WifiP2pBeeService \
-a start
```

## What flows where

| P2P tone | hum chi |
|---|---|
| `{"chi":"prompt","sid":"<peer-hid>","text":"..."}` | `chi:"prompt"` to humd (sid keyed off peer hid) |
| humd's `chi:"chunk"` text parts | collected into one reply buffer |
| humd's `chi:"finish"` | `{"chi":"finish","sid":"<peer-hid>","reply":"..."}` over P2P TCP |

The P2P wire uses the same NDJSON framing as thrum, so a peer that
also runs humd can forward tones directly. The bee is the translator
between the P2P radio and the local thrum socket.

## What it doesn't do

- **No mesh routing.** WiFi Direct groups are star topologies (one GO,
multiple clients). Cross-group routing requires a humd with multiple
P2P interfaces or an ensemble gossip layer over an alternative
transport.
- **No background scanning.** Android 13+ restricts background WiFi
scanning; the bee must be a foreground service with a notification.
- **No encryption beyond WPA2.** The P2P link is WPA2-PSK protected;
no application-layer encryption. For production, pair with the
ensemble's Ed25519 handshake.
- **No STA concurrency.** Many phones can't do P2P + STA (normal WiFi)
simultaneously. The bee detects this and falls back gracefully.
- **No cross-device group persistence.** Groups are ephemeral; the bee
re-forms on each discovery cycle.

## See also

- [`gsm-modem`](../gsm-modem) — same forager pattern over GSM AT-command serial
- [`bp7`](../bp7) — same forager pattern over Bundle Protocol v7 (DTN)
- [`twilio-sms`](../twilio-sms) — same forager pattern over Twilio webhook
- [WIRE.md](../../WIRE.md) — the thrum protocol spec
- [Android WifiP2pManager docs](https://developer.android.com/guide/topics/connectivity/wifip2p)
42 changes: 42 additions & 0 deletions hives/wifi-p2p/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization")
}

android {
namespace = "hum.hive.wifip2p"
compileSdk = 34

defaultConfig {
applicationId = "hum.hive.wifip2p"
minSdk = 29
targetSdk = 34
versionCode = 1
versionName = "0.1.0"
}

buildTypes {
release {
isMinifyEnabled = false
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}
}

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.lifecycle:lifecycle-service:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
}
45 changes: 45 additions & 0 deletions hives/wifi-p2p/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- WiFi Direct discovery + group formation -->
<uses-permission android:name="android.permission.NEARBY_WIFI_PEERS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Foreground service (required for ongoing P2P scanning + socket) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- Internet for TCP sockets on P2P interface -->
<uses-permission android:name="android.permission.INTERNET" />

<application
android:label="Hum P2P Bee"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoTitleBar">

<service
android:name=".WifiP2pBeeService"
android:foregroundServiceType="dataSync"
android:exported="true"
android:label="WiFi Direct Bee" />

<receiver
android:name=".P2pManagerBroadcastReceiver"
android:exported="true"
android:label="P2P Broadcast Receiver">
<intent-filter>
<action android:name="android.net.wifi.p2p.STATE_CHANGED" />
<action android:name="android.net.wifi.p2p.CONNECT_STATE_CHANGE" />
<action android:name="android.net.wifi.p2p.PEERS_CHANGED" />
<action android:name="android.net.wifi.p2p.CONNECTION_INFO_CHANGED" />
<action android:name="android.net.wifi.p2p.GROUP_INFO_CHANGED" />
<action android:name="android.net.wifi.p2p.DISCOVERY_CHANGE" />
<action android:name="android.net.wifi.p2p.GROUP_REMOVED" />
</intent-filter>
</receiver>

</application>

</manifest>
80 changes: 80 additions & 0 deletions hives/wifi-p2p/app/src/main/java/hum/hive/wifip2p/IdentityStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package hum.hive.wifip2p

import java.io.File
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.security.KeyFactory
import java.security.KeyPair
import java.security.Security
import javax.crypto.Cipher

/**
* Persisted Ed25519 identity for this bee.
*
* Mirrors hives/common/src/identity.rs and hives/openai-server/src/identity.ts
* so the hid is byte-identical across all hive languages.
*
* The 32-byte seed is stored at:
* $XDG_STATE_HOME/hum/bees/wifi-p2p.key
* (fallback: ~/.local/state/hum/bees/wifi-p2p.key)
*
* On Android, this resolves to the app's internal data directory.
*/
object IdentityStore {
private val PKCS8_ED25519_PREFIX = byteArrayOf(
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
)

private var cachedHid: String? = null
private var cachedKeyPair: KeyPair? = null

/**
* Load or mint the Ed25519 key pair and return the canonical hid.
*
* hid format: `fbee_<sha256(pubkey)>` — same prefix as Rust/TS hives.
*/
fun getHid(dataDir: String): String {
cachedHid?.let { return it }

val keyFile = File(dataDir, "hum/bees/wifi-p2p.key")
val seed: ByteArray = if (keyFile.exists() && keyFile.length() == 32L) {
keyFile.readBytes()
} else {
val newSeed = generateEd25519Seed()
keyFile.parentFile.mkdirs()
keyFile.writeBytes(newSeed)
newSeed
}

// Rebuild key pair from seed
val keyFactory = KeyFactory.getInstance("Ed25519")
val privKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(PKCS8_ED25519_PREFIX + seed))
val pubKey = keyFactory.generatePublic(X509EncodedKeySpec(seed.copyOfRange(0, 32)))
cachedKeyPair = KeyPair(pubKey, privKey)

val pubRaw = pubKey.encoded
val digest = MessageDigest.getInstance("SHA-256")
digest.update(pubRaw)
val hex = digest.digest().joinToString("") { "%02x".format(it) }
cachedHid = "fbee_$hex"
return cachedHid!!
}

fun getKeyPair(dataDir: String): KeyPair {
cachedKeyPair?.let { return it }
getHid(dataDir) // ensures key is loaded
return cachedKeyPair!!
}

private fun generateEd25519Seed(): ByteArray {
// Use Android's built-in Ed25519 keygen
val kpg = KeyPairGenerator.getInstance("Ed25519")
val kp = kpg.generateKeyPair()
// Extract 32-byte seed from PKCS#8 private key
val encoded = kp.private.encoded
return encoded.copyOfRange(encoded.size - 32, encoded.size)
}
}
Loading