Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7bc566f
feat(identity): shared keychain access-group for app<->NE identity (T…
torlando-agent[bot] Jun 2, 2026
26455df
chore(deps): pin reticulum-swift/LXMF-swift to the Model-B feature br…
torlando-agent[bot] Jun 2, 2026
448fc9e
feat(store): unify the SwiftUI layer on the GRDB message store (Track…
torlando-agent[bot] Jun 2, 2026
84530a4
feat(ne): App-Group bridge NetworkInterface + bidirectional frame que…
torlando-agent[bot] Jun 2, 2026
bb2a112
feat(backend): promote registeredDestinationHashes() to RnsCore seam …
torlando-agent[bot] Jun 2, 2026
de5f90c
feat(ne): App-Group ExtensionDiagLog observability + NE log PII redac…
torlando-agent[bot] Jun 2, 2026
ae35dbd
feat(ne): capped-exponential-backoff reconnect + NWPathMonitor (Track…
torlando-agent[bot] Jun 2, 2026
5789893
build(ne): flip ENABLE_NETWORK_EXTENSION on -Swift configs + compile …
torlando-agent[bot] Jun 2, 2026
4bcda81
feat(ne): on-demand connect for jetsam/reboot auto-relaunch (Track C2c)
torlando-agent[bot] Jun 2, 2026
1d40a6b
fix(a0): repoint stale Compat conversation reads to the unified GRDB …
torlando-agent[bot] Jun 2, 2026
8b5a2f4
build(ne): link ReticulumSwift + LXMFSwift into the NE target (Track …
torlando-agent[bot] Jun 2, 2026
1e8996a
feat(ne): in-NE Reticulum+LXMF node core, gated off (Track A5a)
torlando-agent[bot] Jun 2, 2026
731255a
feat(store): relocate GRDB store to App-Group container via shared pa…
torlando-agent[bot] Jun 2, 2026
b2bc444
feat(ne): app->NE ProxyRnsBackend IPC skeleton, flag-gated (Track A5b)
torlando-agent[bot] Jun 2, 2026
d8a50e4
feat(ne): durable App-Group outbox for NE-stopped sends (Track A5c)
torlando-agent[bot] Jun 2, 2026
015d237
feat(ne): wire Model-B node as selectable live delivery path (Track C…
torlando-agent[bot] Jun 2, 2026
2b4cecb
fix(a0): recover attachments from wire-format packed_lxmf rows (A0 fo…
torlando-agent[bot] Jun 2, 2026
0ab71b8
feat(ble): CoreBluetooth state-restoration for background BLE wake (T…
torlando-agent[bot] Jun 2, 2026
1a76777
feat(ui): background-transport explainer + enable UX (Track C6)
torlando-agent[bot] Jun 2, 2026
b10b374
build(ne): embed ColumbaNetworkExtension into the app (C2 packaging gap)
torlando-agent[bot] Jun 2, 2026
9fafd97
fix(ne): re-assert tunnel mode after interface hot-reload (announces …
torlando-agent[bot] Jun 2, 2026
b48c3d9
fix(ne): re-assert tunnel mode when an interface is established (laun…
torlando-agent[bot] Jun 2, 2026
a819a7a
test(ne): device-drivable test-announce trigger + frame-bridge diagno…
torlando-agent[bot] Jun 2, 2026
741d24c
fix(ne): bring up the in-NE Model B node — keychain group resolution …
torlando-agent[bot] Jun 2, 2026
572a496
Model B background delivery: on-device bring-up, UI + announce fixes
torlando-agent[bot] Jun 3, 2026
708be61
Model B: hardcode as sole architecture (no toggle) + reliable bring-u…
torlando-agent[bot] Jun 3, 2026
cf24387
test(interop): BUG #1 network-tab regression + [PY]->[RNS] marker compat
torlando-agent[bot] Jun 5, 2026
a57236d
feat(model-b): wire BLE across the NE<->app seam + surface native sta…
torlando-agent[bot] Jun 5, 2026
b3addc9
fix(model-b): surface NE-delivered LXMF + delivery proofs to the open…
torlando-agent[bot] Jun 5, 2026
ad915e7
chore(ble): seam-level connect/handshake logging in AppGroupBLEServer
torlando-agent[bot] Jun 5, 2026
7f86660
build(deps): bump reticulum-swift to merged BLE fixes (7f5006f)
torlando-agent[bot] Jun 7, 2026
e1cfb04
fix(ble): Model B seam survives NE restart; resolve sender name in NE…
torlando-agent[bot] Jun 7, 2026
e40ea3d
feat(ne): re-enable suspend-safe path-table persistence + push networ…
torlando-agent[bot] Jun 7, 2026
68f04b6
refactor(ui): drive NE-backed status/BLE UI off Darwin pushes, not po…
torlando-agent[bot] Jun 7, 2026
5684695
feat(rnode): Model B RNode-over-BLE — radio in app, RNS in NE
torlando-agent[bot] Jun 12, 2026
1833a7b
chore(rnode): remove the legacy Model A python RNode path
torlando-agent[bot] Jun 12, 2026
df2679e
feat(ui): Lucide icon font + RNode antenna on announce cards
torlando-agent[bot] Jun 12, 2026
6c49b27
fix(ne): keep ext-diag.log live for real-time on-device NE diagnostics
torlando-agent[bot] Jun 12, 2026
6a04256
fix(prop): re-wire manually-selected propagation node when its announ…
torlando-agent[bot] Jun 12, 2026
c81aefd
feat(ios): Model-B LXMF propagation — wire app PN + sync into the NE
torlando-agent[bot] Jun 12, 2026
30f535c
feat(ne): log a success marker when an inbound notification is posted
torlando-agent[bot] Jun 12, 2026
f0bbec3
chore(security): gate the lxma://test-* observer registrations behind…
torlando-agent[bot] Jun 13, 2026
bd7ebb7
chore(deps): bump reticulum-swift to main (b366f29) — resource disk-s…
torlando-agent[bot] Jun 18, 2026
c38d452
Merge origin/main into feat/model-b-background-ne
torlando-agent[bot] Jun 18, 2026
a118775
fix(model-b): port 3 background-hardening fixes from the #57 tunnel b…
torlando-agent[bot] Jun 18, 2026
cfed1a9
fix(ne): tear down Darwin observers + RNode seam on stop; harden conf…
torlando-agent[bot] Jun 18, 2026
971a354
fix(ne): stop logging message plaintext + sender name to ext-diag.log…
torlando-agent[bot] Jun 18, 2026
35b257e
fix(ne): close actor re-entrancy on start() + sync (greptile #90 iter 3)
torlando-agent[bot] Jun 18, 2026
fe9c2d8
fix(proxy): close ProxyRnsBackend stop/start race + poller cancellati…
torlando-agent[bot] Jun 18, 2026
2ec6bd8
fix(proxy): prevent zombie announce-poller on start/stop race (grepti…
torlando-agent[bot] Jun 18, 2026
29cb14a
fix(ne): honor stop() during start() + deinit observer safety net (gr…
torlando-agent[bot] Jun 18, 2026
81b2e46
fix(ne): track + cancel the detached RNode addInterface task (greptil…
torlando-agent[bot] Jun 18, 2026
76dd7ed
fix(ne): floor the propagation sync interval to prevent a busy-loop (…
torlando-agent[bot] Jun 18, 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
4 changes: 4 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Target / module dependency graph for the iOS app. Mirrors the role of `docs/architecture.md` in the sibling Android repo (`columba/`).

## Subsystem deep-dives

- [Model B — Background LXMF Delivery](docs/MODEL_B_BACKGROUND_DELIVERY.md) — how the Network Extension delivers LXMF messages + notifications while the app is backgrounded/suspended/locked (no APNS): the NE-canonical node, the control IPC + App-Group frame bridge, the load-bearing invariants, and the on-device-verified inbound/outbound/announce flows.

Regenerate this file from the current `Package.swift` + `Columba.xcodeproj/project.pbxproj`:

```sh
Expand Down
181 changes: 171 additions & 10 deletions Columba.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 84 additions & 7 deletions Sources/ColumbaApp/App/ColumbaApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import SwiftUI
import RNSAPI
import UserNotifications
import BackgroundTasks
import SwiftBLEBridge
import os
#if canImport(CoreBluetooth)
import CoreBluetooth
#endif

private let logger = Logger(subsystem: "network.columba.Columba", category: "ColumbaApp")

Expand Down Expand Up @@ -45,6 +49,45 @@ struct ColumbaApp: App {
logger.error("Python runtime failed: \(err.localizedDescription, privacy: .public)")
}

#if os(iOS) && canImport(CoreBluetooth)
// Track C8 — background BLE wake / CoreBluetooth state restoration.
// When iOS RELAUNCHES the app for a preserved BLE event, it sets
// UIApplication.LaunchOptionsKey.bluetoothCentrals / .bluetoothPeripherals
// and expects the app to RE-CREATE its CBCentralManager /
// CBPeripheralManager with the SAME restore identifiers EARLY in launch,
// so it can replay the preserved state via `willRestoreState`. This is a
// pure-SwiftUI app (`@main struct ColumbaApp: App`) with NO
// UIApplicationDelegate, so there is no
// `application(_:didFinishLaunchingWithOptions:)` from which to read
// `launchOptions` and branch on those keys. `App.init()` is the earliest
// app-owned hook and runs before the run loop settles, so we
// re-materialise the managers here UNCONDITIONALLY (every launch). That
// is cheap and satisfies CoreBluetooth's "re-create promptly with the
// same identifier" contract on the relaunch-for-BLE case; on a normal
// launch it just pre-creates the managers (the regular
// AppServices.startBLEInterface() path reuses them via start()).
//
// GAP / FOLLOW-ON: this re-arms the wake and re-adopts CoreBluetooth
// state, but inbound BLE bytes only become a *delivered + notified*
// message through the Python delivery path, which requires the active
// backend to be the Python backend AND its BLE bring-up
// (startBLEInterface → re-install of SwiftBLEBridge's callbackInvoker) to
// run on this relaunch. Native-Swift BLE delivery is a deliberate
// follow-on; until it lands, background-wake delivery is
// Python-backend-only. See the DELIVERY CAVEAT in SwiftBLEBridge.start().
//
// Model B follow-on (now landing): reticulum-swift's `CoreBluetoothBLEDriver`
// owns CoreBluetooth via `ModelBBLEService` (started from `AppServices` once
// the identity is ready). It must be the ONLY CB stack — `SwiftBLEBridge`
// creating its own managers would fight over the same GATT service — so we
// restore `SwiftBLEBridge` only on the Python-backend (non-Model-B) path.
// (Model B background-restore via CoreBluetoothBLEDriver's own restore
// identifier is a further follow-on: it needs the identity at launch.)
if !BackendPreference.modelB {
SwiftBLEBridge.shared.restoreAtLaunch()
}
#endif

#if os(iOS)
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "network.columba.Columba.sync",
Expand All @@ -56,6 +99,11 @@ struct ColumbaApp: App {

// Install notification delegate early so didReceive (notification tap) works
UNUserNotificationCenter.current().delegate = NotificationService.delegate

// Register app-side notification/announce defaults at launch (not lazily on
// first Settings open) so the foreground notification path isn't suppressed
// on a fresh install that never visits Settings. (ports #57 dc1024b)
SettingsViewModel.registerLocalDefaults()
}

// MARK: - App Body
Expand Down Expand Up @@ -622,6 +670,17 @@ struct RootView: View {
private func initializeServices() async {
DiagLog.log("[STARTUP] initializeServices() ENTERED")

// Surface the Network Extension's App-Group diagnostic log into Documents
// so it's retrievable via `devicectl ... copy from` alongside diag.log.
// The NE (sandboxed) writes ext-diag.log to the shared container; the host
// copies the previous background session's log out here on each launch.
DiagLog.copyExtensionDiagToDocuments()
#if DEBUG
// Keep that copy LIVE (not just this launch's snapshot) so on-device NE
// diagnostics can be tailed in real time. DEBUG-only.
DiagLog.startExtDiagLiveCopy()
#endif

// Retry the entire init up to 5 times with increasing delay —
// the Keychain, file system, or CryptoKit may not be ready
// immediately after device unlock.
Expand Down Expand Up @@ -718,13 +777,18 @@ struct RootView: View {
)
DiagLog.log("[STARTUP] Step 5: AppServices initialized OK")

// 6. Wire up database, message repo, handler
guard let db = appServices.database else {
// 6. Wire up database, message repo, handler.
// `db` is the RNSAPI Compat store IncomingMessageHandler uses for
// sender-name lookups. `repo` is the GRDB-backed canonical store
// (Track A0) the UI reads — built and held by AppServices during
// initialize(), so reuse that single instance rather than opening a
// second handle to the same `lxmf-swift.db` (and keeping the
// LXMFSwift import walled off in MessageRepository.swift).
guard let db = appServices.database,
let repo = appServices.messageRepository else {
throw AppServicesError.routerNotInitialized
}
self.database = db

let repo = MessageRepository(database: db)
self.messageRepository = repo

#if os(iOS)
Expand All @@ -751,7 +815,14 @@ struct RootView: View {
DiagLog.log("[STARTUP] Starting interface: \(iface.type) name=\(iface.name)")
switch iface.type {
case .tcpClient:
if case .tcpClient(let config) = iface.config {
if BackendPreference.modelB {
// Model B: the NE owns the single TCP relay interface. The app
// must NOT open a competing/duplicate one — doing so spawns a
// second socket to the relay and surfaces as a stray
// "enabled but disconnected" interface in the UI. The app owns
// only Auto/BLE/RNode in Model B; their frames bridge to the NE.
DiagLog.log("[STARTUP] Model B: skipping app-side TCP interface (NE owns TCP)")
} else if case .tcpClient(let config) = iface.config {
let entityId = iface.id
Task {
DiagLog.log("[STARTUP] TCP interface \(config.targetHost):\(config.targetPort) — registering")
Expand Down Expand Up @@ -817,8 +888,14 @@ struct RootView: View {
}
}

// 8. Request notification permission and install foreground delegate
await NotificationService.shared.requestPermission()
// 8. Request notification permission WITHOUT blocking init. A blocking
// `await` here holds the rest of RootView setup (and `isInitialized`)
// hostage behind the OS auth sheet until the user taps Allow/Don't Allow
// — and on a fresh-install device the smoke harness (no UI driver) can't
// tap it at all, so init never completes. The foreground UN delegate is
// already installed eagerly in `init()` (see the delegate assignment in
// ColumbaApp.init), so deferring the prompt is safe. (ports #57 fc9b0b8)
Task { _ = await NotificationService.shared.requestPermission() }

self.isInitialized = true

Expand Down
7 changes: 7 additions & 0 deletions Sources/ColumbaApp/Resources/ColumbaApp.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
<array>
<string>group.network.columba.Columba</string>
</array>
<!-- Shared keychain group so the Network Extension can load the SAME RNS
identity (Model B): the NE signs delivery proofs / decrypts inbound LXMF
while the host app is suspended. $(AppIdentifierPrefix) = team-id prefix. -->
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)network.columba.Columba.shared</string>
</array>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
Expand Down
1 change: 1 addition & 0 deletions Sources/ColumbaApp/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<string>materialdesignicons.ttf</string>
<string>JetBrainsMono-Regular.ttf</string>
<string>JetBrainsMono-Bold.ttf</string>
<string>lucide.ttf</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
Expand Down
39 changes: 39 additions & 0 deletions Sources/ColumbaApp/Resources/lucide-font-LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
ISC License

Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2025.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

---

The MIT License (MIT) (for portions derived from Feather)

Copyright (c) 2013-2023 Cole Bemis

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file added Sources/ColumbaApp/Resources/lucide.ttf
Binary file not shown.
Loading
Loading