Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c43da8c
feat: custom model paths + auto-download for missing models
Mar 15, 2026
8a964c4
feat: add Local Models section in Settings for updating Ollama and TT…
Mar 16, 2026
4671446
fix: resolve merge conflict in gui_bridge.py HF_HOME detection
Mar 16, 2026
7d691a0
feat: balance widget in QubeManager + sweep on card flip + TTS fix
Mar 16, 2026
ad5cf43
build: update Tauri crate versions and add pnpm lockfile for local bu…
Mar 16, 2026
3d3701a
fix: add torch distributed/futures/rpc stubs to fix TTS and GPU accel…
Mar 16, 2026
23e0006
v0.9.1: fix TTS model updates, add AI model download, balance widget …
Mar 16, 2026
4773ed4
v0.9.1: enforce wallet 2FA for all backup and restore operations
Mar 16, 2026
197aca1
fix: unpin old IPFS backups after each upload (prevent Pinata bloat)
Mar 16, 2026
a35fa0b
fix: smart wallet 2FA for restore — required only if backup is wallet…
Mar 16, 2026
89a12ee
refactor: simplify wallet 2FA — require at restore only, not backup
Mar 16, 2026
9711c1a
fix: WalletConnect copy URL + inline connect in restore modals
Mar 16, 2026
18c16f2
feat: auto-sync to IPFS after wallet import (received Qube anchors to…
Mar 16, 2026
e4420ce
feat: step-by-step progress bar for wallet import
Mar 16, 2026
6582bf4
feat: per-Qube NFT ownership check at restore time
Mar 16, 2026
a6e9c45
feat: scan wallet as third IPFS restore option + rename restore labels
Mar 16, 2026
29fe675
fix: match login WalletConnect UI to in-app style
Mar 16, 2026
6755ec4
feat: fix IPFS sync on auto-anchor + cleaner settings UI with timing
Mar 16, 2026
8544670
fix: rename BCH Anchor label to Auto-Anchor in settings
Mar 16, 2026
dbc521f
feat: P2P relay + Nostr integration + Endpoints settings (all 6 phases)
Mar 16, 2026
8db0e42
fix: merge Relay + Endpoints into single panel, remove unused service…
Mar 16, 2026
5a4efd1
feat: live X/Y endpoint connectivity — real WSS ping using websockets…
Mar 16, 2026
c2406e8
feat: relay folder at D:\Qubes\relay\ — same pattern as ollama
Mar 16, 2026
0945cf2
feat: implement relay Phases 2-6 (onion, Nostr NIP-44, BLE, bundle up…
Mar 17, 2026
4b1f624
feat: close all relay design doc gaps
Mar 17, 2026
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
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,53 @@ Qubes are AI agents you genuinely own. Each Qube has a cryptographic identity mi
│ Category: c9054d53dcc075dd7226ea319f20d43d │
│ f102371149311c9239f6c0ea1200b80f │
└─────────────────────────────────────────────┘

(parallel stack — no message data ever on-chain)
┌─────────────────────────────────────────────┐
│ Relay Layer │
│ Qubes P2P Relay Protocol │
│ Kademlia DHT · Noise encryption │
│ 2-hop onion routing · Store-and-forward │
│ Nostr fallback (Phase 3) │
└───────────────┬─────────────────────────────┘
│ transport-agnostic
┌───────────────▼─────────────────────────────┐
│ Transport Drivers (updatable bundle) │
│ TCP/WS · QUIC · BLE · LoRa · I2P │
└─────────────────────────────────────────────┘
```

The desktop app is one implementation of the protocol. The SDK is the protocol itself. The relay layer is a completely separate stack — no message content, metadata, or communication graph ever touches the BCH blockchain.

---

## P2P Relay

Qubes includes a private peer-to-peer messaging relay for cross-Qube communication. It is transport-agnostic, metadata-private, and requires no central server.

Every Qubes desktop installation automatically runs a lightweight relay node. Nodes find each other through a Kademlia DHT seeded by a built-in list of community relays — the same pattern Electron Cash uses for Fulcrum servers. Messages route through 2-hop onion encryption (Phase 2) so relay operators cannot determine who is communicating with whom.

When a recipient is offline, their nearest DHT neighbors hold the encrypted message for up to 7 days. Messages are purged immediately after delivery. Conversation history is stored locally inside the Qube's signed memory chain, not on relays.

**Transport fallback waterfall** (automatic, no user action required):

```
Priority 1 Local relay User's own desktop relay node
Priority 2 Internet DHT Kademlia routing, Noise-encrypted
Priority 3 Nostr fallback Phase 3 — ephemeral keys + NIP-44 encryption
Priority 4 LoRa / BLE mesh Phase 6 — extreme offline, no internet needed
```

**Running a dedicated relay node** (for community relay operators):

```bash
cd relay
npm run relay:start # production, port 4001
npm run relay:dev # verbose logging
npm run relay:start -- --port 4001 --max-connections 200 --retention-days 7
```

The desktop app is one implementation of the protocol. The SDK is the protocol itself.
Every Qubes desktop user already contributes relay capacity just by running the app. Dedicated relay nodes are for always-on community infrastructure, like running a BCH full node.

---

Expand All @@ -56,7 +100,7 @@ The desktop app is one implementation of the protocol. The SDK is the protocol i
npm install @qubesai/sdk
```

**8 modules:** `types` · `crypto` · `wallet` · `blocks` · `covenant` · `package` · `bcmr` · `storage`
**10 modules:** `types` · `crypto` · `wallet` · `blocks` · `covenant` · `package` · `bcmr` · `storage` · `relay` · `nostr`

### Quick example — generate an identity

Expand Down
66 changes: 66 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# mypy: allow-untyped-defs
from torch.nn.parameter import ( # usort: skip
Buffer as Buffer,
Parameter as Parameter,
UninitializedBuffer as UninitializedBuffer,
UninitializedParameter as UninitializedParameter,
)
# Pre-import functional and init so submodules can do `import torch.nn.functional as F`
# without hitting a circular import during torch.nn initialization
import torch.nn.functional # noqa: E402
import torch.nn.init # noqa: E402
from torch.nn.modules import * # usort: skip # noqa: F403
from torch.nn import (
attention as attention,
functional as functional,
init as init,
modules as modules,
parallel as parallel,
parameter as parameter,
utils as utils,
)
from torch.nn.parallel import DataParallel as DataParallel


def factory_kwargs(kwargs):
r"""Return a canonicalized dict of factory kwargs.

Given kwargs, returns a canonicalized dict of factory kwargs that can be directly passed
to factory functions like torch.empty, or errors if unrecognized kwargs are present.

This function makes it simple to write code like this::

class MyModule(nn.Module):
def __init__(self, **kwargs):
factory_kwargs = torch.nn.factory_kwargs(kwargs)
self.weight = Parameter(torch.empty(10, **factory_kwargs))

Why should you use this function instead of just passing `kwargs` along directly?

1. This function does error validation, so if there are unexpected kwargs we will
immediately report an error, instead of deferring it to the factory call
2. This function supports a special `factory_kwargs` argument, which can be used to
explicitly specify a kwarg to be used for factory functions, in the event one of the
factory kwargs conflicts with an already existing argument in the signature (e.g.
in the signature ``def f(dtype, **kwargs)``, you can specify ``dtype`` for factory
functions, as distinct from the dtype argument, by saying
``f(dtype1, factory_kwargs={"dtype": dtype2})``)
"""
if kwargs is None:
return {}
simple_keys = {"device", "dtype", "memory_format"}
expected_keys = simple_keys | {"factory_kwargs"}
if not kwargs.keys() <= expected_keys:
raise TypeError(f"unexpected kwargs {kwargs.keys() - expected_keys}")

# guarantee no input kwargs is untouched
r = dict(kwargs.get("factory_kwargs", {}))
for k in simple_keys:
if k in kwargs:
if k in r:
raise TypeError(
f"{k} specified twice, in **kwargs and in factory_kwargs"
)
r[k] = kwargs[k]

return r
31 changes: 27 additions & 4 deletions audio/audio_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ def __init__(self, config: Optional[Dict[str, Any]] = None, qube_data_dir: Optio

def _load_config_from_env(self) -> Dict[str, Any]:
"""Load configuration from environment variables"""
# Resolve base models dir: QUBES_MODELS_DIR env var or platform default
_models_base = Path(os.getenv("QUBES_MODELS_DIR", "~/.qubes/models")).expanduser()
return {
"openai_api_key": os.getenv("OPENAI_API_KEY"),
"elevenlabs_api_key": os.getenv("ELEVENLABS_API_KEY"),
Expand All @@ -216,15 +218,16 @@ def _load_config_from_env(self) -> Dict[str, Any]:
"piper_model_path": Path(
os.getenv(
"PIPER_MODEL_PATH",
"~/.qubes/models/piper/en_US-lessac-medium.onnx"
str(_models_base / "piper" / "en_US-lessac-medium.onnx")
)
),
"whisper_cpp_model_path": Path(
os.getenv(
"WHISPER_MODEL_PATH",
"~/.qubes/models/whisper/ggml-base.en.bin"
str(_models_base / "whisper" / "ggml-base.en.bin")
)
),
"qwen3_models_dir": _models_base / "qwen3-tts",
}

def _init_tts_providers(self):
Expand Down Expand Up @@ -384,9 +387,28 @@ def _get_qwen3_provider(self) -> Optional[TTSProvider]:
except Exception:
pass # Use defaults

# Use QUBES_MODELS_DIR if set, otherwise platform default
qwen3_models_dir = self.config.get("qwen3_models_dir")

# Auto-download model if not present
if qwen3_models_dir is not None:
try:
from audio.model_downloader import Qwen3ModelDownloader
_dl = Qwen3ModelDownloader(models_dir=qwen3_models_dir)
_variant_key = f"{model_variant}-Base"
if not _dl.is_model_downloaded(_variant_key):
logger.info("qwen3_auto_download_triggered", variant=_variant_key)
_dl.start_download(_variant_key)
# Also download tokenizer
if not _dl.is_model_downloaded("Tokenizer"):
_dl.start_download("Tokenizer")
except Exception as _dl_err:
logger.warning("qwen3_auto_download_failed", error=str(_dl_err))

provider = Qwen3TTSProvider(
model_variant=model_variant,
use_flash_attention=use_flash_attention
use_flash_attention=use_flash_attention,
**({"models_dir": qwen3_models_dir} if qwen3_models_dir else {})
)
self.tts_providers["qwen3"] = provider
logger.info("tts_provider_initialized", provider="qwen3")
Expand Down Expand Up @@ -489,7 +511,8 @@ def check_qwen3_status(self) -> Dict[str, Any]:
result["available"] = result["recommended_variant"] is not None

# Check downloaded models
models_dir = Path.home() / ".qubes" / "models" / "qwen3-tts"
_models_base = Path(os.getenv("QUBES_MODELS_DIR", str(Path.home() / ".qubes" / "models")))
models_dir = _models_base / "qwen3-tts"
if models_dir.exists():
for model_dir in models_dir.iterdir():
if model_dir.is_dir():
Expand Down
107 changes: 104 additions & 3 deletions config/user_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ class BlockPreferences:
group_anchor_threshold: int = 20 # Anchor threshold for group chats

# IPFS backup settings
auto_sync_ipfs_on_anchor: bool = False # Auto-sync to IPFS after auto-anchor
auto_sync_ipfs_periodic: bool = False # Periodic background sync to IPFS
auto_sync_ipfs_interval: int = 15 # Interval in minutes (15, 30, 45, 60)
auto_sync_ipfs_on_anchor: bool = True # Auto-sync to IPFS after auto-anchor
auto_sync_ipfs_periodic: bool = True # Periodic background sync to IPFS
auto_sync_ipfs_interval: int = 15 # Interval in minutes (5, 15, 30, 60)


@dataclass
Expand Down Expand Up @@ -94,6 +94,42 @@ class Qwen3Preferences:
])


@dataclass
class RelayPreferences:
"""Preferences for the P2P relay node."""

relay_enabled: bool = True
relay_listen_port: int = 0 # 0 = auto-assign
relay_max_connections: int = 50
relay_retention_days: int = 7
relay_custom_peers: List[str] = field(default_factory=list)
p2pd_binary_path: Optional[str] = None # None = use bundled binary


@dataclass
class EndpointPreferences:
"""Preferences for network endpoint configuration."""

fulcrum_nodes: List[str] = field(default_factory=lambda: [
# Well-known public Fulcrum / Electrum BCH servers (same pre-load as Electron Cash)
"wss://bch.imaginary.cash:50004",
"wss://electroncash.de:50004",
"wss://bch.loping.net:50004",
"wss://blackie.c3-soft.com:50004",
"wss://electroncash.dk:50002",
"wss://electron.jochen-hoenicke.de:51002",
"wss://bitcoincash.network:50004",
"wss://bch.soul-dev.com:50002",
])
nostr_relays: List[str] = field(default_factory=lambda: [
# Well-known public Nostr relays (used as Phase 3 transport fallback)
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band",
"wss://relay.snort.social",
])


@dataclass
class OnboardingPreferences:
"""Track tutorial completion per tab."""
Expand Down Expand Up @@ -166,6 +202,8 @@ class UserPreferences:
decision: DecisionConfig
onboarding: OnboardingPreferences
qwen3: Qwen3Preferences
relay: RelayPreferences
endpoints: EndpointPreferences

def __init__(self):
self.blocks = BlockPreferences()
Expand All @@ -174,6 +212,8 @@ def __init__(self):
self.decision = DecisionConfig()
self.onboarding = OnboardingPreferences()
self.qwen3 = Qwen3Preferences()
self.relay = RelayPreferences()
self.endpoints = EndpointPreferences()

def to_dict(self) -> Dict[str, Any]:
"""Convert preferences to dictionary."""
Expand All @@ -190,6 +230,8 @@ def to_dict(self) -> Dict[str, Any]:
'decision': asdict(self.decision),
'onboarding': asdict(self.onboarding),
'qwen3': qwen3_dict,
'relay': asdict(self.relay),
'endpoints': asdict(self.endpoints),
}

@classmethod
Expand Down Expand Up @@ -220,6 +262,24 @@ def from_dict(cls, data: Dict[str, Any]) -> 'UserPreferences':
voice_library=qwen3_data.get('voice_library', {}),
)

if 'relay' in data:
relay_data = data['relay']
prefs.relay = RelayPreferences(
relay_enabled=relay_data.get('relay_enabled', True),
relay_listen_port=relay_data.get('relay_listen_port', 0),
relay_max_connections=relay_data.get('relay_max_connections', 50),
relay_retention_days=relay_data.get('relay_retention_days', 7),
relay_custom_peers=relay_data.get('relay_custom_peers', []),
p2pd_binary_path=relay_data.get('p2pd_binary_path', None),
)

if 'endpoints' in data:
ep = data['endpoints']
prefs.endpoints = EndpointPreferences(
fulcrum_nodes=ep.get('fulcrum_nodes', EndpointPreferences().fulcrum_nodes),
nostr_relays=ep.get('nostr_relays', EndpointPreferences().nostr_relays),
)

return prefs


Expand Down Expand Up @@ -686,6 +746,47 @@ def update_qwen3_preferences(
self.save_preferences(prefs)
return prefs

# =========================================================================
# RELAY PREFERENCES
# =========================================================================

def get_relay_preferences(self) -> RelayPreferences:
"""Get relay node preferences."""
return self.load_preferences().relay

def update_relay_preferences(self, **kwargs) -> UserPreferences:
"""Update relay node preferences."""
prefs = self.load_preferences()
for key, value in kwargs.items():
if hasattr(prefs.relay, key):
setattr(prefs.relay, key, value)
self.save_preferences(prefs)
return prefs

# =========================================================================
# ENDPOINT PREFERENCES
# =========================================================================

def get_endpoint_preferences(self) -> EndpointPreferences:
"""Get network endpoint preferences."""
return self.load_preferences().endpoints

def update_endpoint_preferences(self, **kwargs) -> UserPreferences:
"""Update network endpoint preferences."""
prefs = self.load_preferences()
for key, value in kwargs.items():
if hasattr(prefs.endpoints, key):
setattr(prefs.endpoints, key, value)
self.save_preferences(prefs)
return prefs

def reset_endpoint_preferences(self) -> UserPreferences:
"""Reset endpoint preferences to defaults."""
prefs = self.load_preferences()
prefs.endpoints = EndpointPreferences()
self.save_preferences(prefs)
return prefs

def get_voice_clones_dir(self) -> Path:
"""
Get the directory for storing voice clone audio files.
Expand Down
Loading