feat: remote-admin correctness, Android BLE MTU, inbound coverage, and storage lockdown for the Meshtastic-Android hard cutover#6
Conversation
…id hard cutover Remote admin (correctness — previously rejected by firmware 2.5+): - Route remote ADMIN_APP packets over PKC when both nodes have published keys (pki_encrypted + target public_key, channel 0), falling back to a channel named "admin"; priority defaults to RELIABLE. One choke point (prepareOutboundAdminPacket) shared by the RPC, ACK'd-send, and fire-and-forget paths. - Cache session passkeys per node (each node issues its own in every admin response) instead of one global slot that cross-contaminated concurrent admin sessions and stamped the local passkey onto remote targets. The SessionKeyExpired single-shot retry now re-seeds against the target node, whose passkey is the one the replay needs. Inbound coverage: - Surface MQTT client-proxy, XModem, and FileInfo frames as typed MeshEvents instead of dropping them with a ProtocolWarning (outbound already works via sendRaw). - Buffer mesh packets that arrive mid-handshake (drop-oldest at 64, observable via PacketsDropped) and flush them through the normal packet pipeline at Ready — live traffic interleaved with the config drain was silently lost. - Fast-fail QueueStatus res != 0 as SendFailure.QueueRejected (and fail any pending RPC sharing the wire id) instead of waiting out the full ACK timer for a packet the firmware already rejected. Ergonomics: - sendText(replyId) for threaded replies. - Document that DeviceStorage.loadNodes() is host-facing (the engine reseeds the node DB from the handshake and never calls it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…on Android Android keeps the ATT MTU at the BLE minimum (23) unless the central requests otherwise, capping write-without-response payloads at 20 bytes — nearly every real ToRadio frame (setConfig, sendText) exceeds that, so the transport was effectively broken on Android (conformance had only ever run on JVM/macOS, where CoreBluetooth auto-negotiates). The BleTransport(address) factory now installs a best-effort post-connect hook that requests MTU 517 and CONNECTION_PRIORITY_HIGH for the 30-second handshake window before downgrading to Balanced (mirrors the Android reference client). The hook is internal to the transport and runs once per link establishment, including auto-reconnect cycles. Also bumps Kable 0.42.0 -> 0.43.0 to align with Meshtastic-Android. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…e audit Audited the engine against firmware main (PhoneAPI.cpp, AdminModule.cpp, MeshService.cpp @ 4e7181c79) and protobufs v2.7.25. Two real defects: - Stage 1 accumulators now replace by key (channel index / config section) instead of appending: a want_config_id retry restarts the firmware's config drain from scratch (PhoneAPI handleStartConfig resets its read index), which duplicated channels and config sections in the committed ConfigBundle and channels state. - FromRadio.lockdown_status (protobufs 2.7.25+) fell through every routing arm and was silently dropped, violating the no-silent-loss rule. It now surfaces as a structured ProtocolWarning until typed lockdown support lands alongside AdminMessage.lockdown_auth. Audit confirmations baked into tests/comments: firmware forces from=0 on all phone packets (MeshService.cpp:188), so local admin authorizes via the from==0 branch and never needs a passkey; every phone send gets a per-packet QueueStatus(res, mesh_packet_id) from sendToMesh, so the QueueRejected fast-fail is live; the 69420/69421 special nonces are the firmware-sanctioned config-only/nodes-only drains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Kotlin library best-practices pass:
- Remove RadioClient's AutoCloseable/close() — the runBlocking bridge
was an ANR trap on Android main and a deadlock risk on iOS main, and
a radio session has no safe non-suspending teardown. Lifecycle is
suspend-only: try { ... } finally { client.disconnect() }. Docs
(threading-model.md, RadioClient KDoc) updated to match.
- Bridge the two byte-string vocabularies in the public surface:
okio.ByteString (Wire proto types, per ADR-001) <->
kotlinx.io.bytestring.ByteString (Frame/SessionPasskey) via
toKotlinxByteString()/toOkioByteString(), so consumers never
round-trip raw arrays.
- Codify the API conventions in docs/api-reference.md: proto exposure,
byte vocabularies, no blocking bridges, the data-class trade
(deliberate, ABI-dump-gated; copy()/destructuring are conveniences
not contract), and the Kotlin/Swift-first interop stance.
- Track Poko adoption for event/result types in the roadmap: 0.23.1
fails against Kotlin 2.3.21 (NoClassDefFoundError:
ExtensionPointDescriptor), so @poko migration waits on upstream.
ABI dumps regenerated (close()/AutoCloseable removal).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…s the API Wire-generated proto types force okio.ByteString into every proto-typed call for both this SDK and its consumers (Meshtastic-Android consumes the same protobufs artifact), so the kotlinx-io ByteString on Frame / SessionPasskey was a gratuitous second vocabulary. Standardize: - Frame.bytes and SessionPasskey.bytes are okio.ByteString. - send(portnum, payload: kotlinx.io.Buffer) is replaced by send(portnum, payload: okio.ByteString) — the type proto payloads already arrive in, so payloads pass through without array copies. - Internal frame assembly (WireCodec.FrameDecoder, TcpTransport, SerialFrameAssembler) moves to okio.Buffer. - kotlinx-io is dropped from every published module (and the version catalog); okio is promoted to api in :core since it now appears in core's own public signatures. - The toKotlinxByteString()/toOkioByteString() adapters added earlier this cycle are deleted — nothing left to bridge. Consumers (and the app cutover) now use exactly one byte type end-to-end: proto payloads, frames, and storage. ABI dumps regenerated; docs/api-reference.md API-conventions section updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…ode, leaner accumulators Four-angle cleanup review (reuse / simplification / efficiency / altitude) over the branch diff, fixes applied: - testing: add FromRadio.toFrame() — the device-side wire framing that 8 test files (and FakeRadioTransport) each re-implemented now lives in one helper; the local copies delegate to it. - engine: decode AdminMessage once per inbound ADMIN_APP packet — the passkey latch and external-config-change detection each ran their own decode of the same payload on a hot path. - engine: pendingConfigs/pendingModuleConfigs become immutable-list vars reassigned via mergeConfigs/mergeModuleConfigs, dropping the clear()+addAll() churn per config frame during the handshake. - engine: drain handshakePacketBuffer in place instead of copying the backlog to a list at Ready. - transport-ble: source START1/START2 from WireFraming (TCP and serial already did). Reviewed and deliberately NOT changed: prepareOutboundAdminPacket's decode+re-encode passkey stamp (binary-patching Wire payloads is fragile; remote admin is low-frequency and the single engine choke point is worth keeping); postConnectHook as an internal var (constructor plumbing isn't worth it for one platform hook); the queueStatus arm's bookkeeping (pre-transmit rejection is semantically distinct from routing NAKs); moving ScriptedTransport to :testing (900-line virtual-time fixture — tracked as follow-up, too churny for this PR). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
… factory, one decoder family
API-shape review for consumer convenience:
- RadioClient.withConnection { } — connect, run the block, always
disconnect (success, exception, AND cancellation via NonCancellable
teardown). The structured-concurrency replacement for the blocking
use{} idiom this API deliberately dropped.
- Flow<NodeChange>.asNodeMap() + RadioClient.nodeMap() — folds the node
delta stream into a live Map<NodeId, NodeInfo>. This is the exact
scan accumulator every consumer (including the Android migration
guide) was hand-writing; now it ships with stateIn-ready semantics.
- RadioClient { } builder-lambda factory as Kotlin-idiomatic sugar over
Builder (which stays, for Swift and step-wise construction).
- Removed the duplicate typed-decoder family (decodeAsText/Position/
User/NodeInfo/Telemetry/Routing/Admin) that shadowed the asText()/
asPosition()/… accessors in PayloadAccessors.kt — one accessor family
remains, plus the generic decodeAs(adapter) escape hatch for portnums
without a typed accessor (Paxcount, StoreAndForward, …).
ABI dumps refreshed; new RadioClientSugarTest covers all three sugars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…e admin, ERRNO queue results
Findings from a four-angle adversarial review of this branch
(correctness / concurrency / API surface / tests), all fixed:
HIGH — nodes flow late-subscriber contract (new in this branch):
Every subscription now receives a fresh NodeChange.Snapshot first,
seeded via onSubscription from the engine's current node map; the
engine SharedFlow drops its replay slot. Previously a late subscriber
got whatever DELTA was emitted last instead of the Snapshot, leaving
nodeMap()-style folds near-empty until the next reconnect.
HIGH — off-actor mutation of sessionPasskeys (regression in this
branch): the fire-and-forget admin path (enterDfuMode, setTimeOnly)
ran prepareOutboundAdminPacket — which prunes the actor-owned per-node
passkey map — on the caller's coroutine. sendAdmin now posts an
EngineMessage.AdminFireAndForget through the inbox (ADR-002).
Firmware conformance — QueueStatus.res is the ERRNO namespace, not
Routing.Error: 35 (ERRNO_SHOULD_RELEASE) is success and counts as
Sent; ERRNO rejections (>= 32) fail pending RPCs as NodeUnreachable
via the new CommandDispatcher.tryFail; 1..31 map through the normal
routing taxonomy. Previously 32 read as BAD_REQUEST, 33 as
NOT_AUTHORIZED, and the success code 35 produced a false failure.
Remote-admin correctness:
- Passkeys latch only from response-shaped admin messages; a remote
node administering US no longer poisons our passkey cache with the
key WE issued it.
- Managed-mode client gate applies to local targets only (firmware
only rejects local admin on managed devices; remote targets
authorize via their own admin keys).
- Session passkeys are no longer persisted (local ones are never
required — firmware rewrites phone packets to from=0; remote ones
expire in ~4 min). Storage methods remain, documented host-facing.
Pre-existing engine defects (surfaced by the same review):
- dispatchSend guards caller-supplied wire-id collisions at the real
key — the second send no longer strands the first handle forever.
- Stage-1 settle replay re-buffers frames behind a duplicate
completion instead of silently dropping them.
- The seeding window routes non-packet variants (node_info, client
notifications, MQTT proxy, ...) instead of silently dropping them.
- Stage-2 commit snapshots actor-owned collections before its async
storage flush (CME risk mis-reported as StorageDegraded).
API shape (pre-publication, nothing on Central):
- MeshEvent.FileInfo -> FileInfoReceived (no longer shadows
org.meshtastic.proto.FileInfo verbatim).
- sendText is (text, to, channel, replyId) — aligned with
sendReaction; all callers were named-arg or text-only.
- withConnection gains teardownTimeout (default 10s): the
NonCancellable disconnect is bounded so a wedged transport cannot
pin a cancelled caller forever.
- detekt ForbiddenImport now bans kotlinx.io.* (one byte vocabulary).
Tests: 16 new tests pin the riskiest logic — PKC asymmetric-key
fallbacks, caller-built PKC passthrough, caller priority + broadcast
guards, two-remote passkey isolation, passkey TTL pruning, identity-
rebind passkey clearing, queue rejection of in-flight RPCs, ERRNO vs
routing-error mapping, res=35 success, unmapped res, buffer-overflow
PacketsDropped + drop-oldest order, seeding-window buffering, settle-
replay preservation, post-Ready dedupe of flushed packets, seeded
late-subscriber snapshots, withConnection cancellation teardown, and
direct decodeAs escape-hatch coverage. ABI dumps regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…de can't leak across sessions The Android factory's delayed CONNECTION_PRIORITY downgrade was launched on the transport-lifetime scope; disconnect() didn't cancel it, and the transport supports reuse-after-disconnect (the engine's auto-reconnect path), so a 30-second timer from session N could fire mid-handshake of session N+1 and downgrade the connection priority during exactly the boost window it exists to protect. postConnectHook now receives a per-connect-cycle scope (child of the transport scope), cancelled in cancelJobs()/disconnect(). New BleTransportHookTest drives a fake Kable Peripheral through connect/disconnect/reconnect and proves: the hook runs once per connect and failures are non-fatal; hook-scheduled work is cancelled at disconnect; and prior-session work never fires into the next session (each test fails against the pre-fix code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
The review found the documentation tree contradicting the shipped
surface on every axis this branch changed. Fixed across 16 files:
- CONTRIBUTING/AGENTS/GEMINI/.specify templates: the byte-payload house
rule inverted to okio.ByteString (kotlinx-io is deliberately not a
dependency; now also detekt-banned).
- SPEC.md v2.2 -> v2.3: AutoCloseable/close() removed, okio vocabulary,
send overload, sendText signature, new MeshEvent/SendFailure variants,
nodes-flow onSubscription seeding, storage passkey methods marked
host-facing, decision-log reversal entries appended with rationale.
- ADR-003 + ADR-000/001/006: superseded-by notes appended (history
preserved, not rewritten).
- api-reference.md: Frame signature, new sendText row, RadioClient {}
factory, withConnection, nodeMap()/asNodeMap(), the three new
MeshEvent rows, AutoReconnectConfig since-tag corrected to 0.1.0.
- error-taxonomy.md: QueueRejected + the QueueStatus/ERRNO namespace
section (32/33/34 rejections, 35 = success, 1..31 Routing.Error).
- transport-isolation.md: close() -> disconnect() throughout.
- README/bom README: transport-tcp is Ktor (not kotlinx-io); proto row
is Wire-generated (not kotlinx.serialization); 233-byte payload limit.
- security.md: storage at-rest guidance references SQLDelight.
- CHANGELOG: restructured per versioning.md with a consolidated
'### Breaking' section; adds the testing toFrame() entry and all of
this branch's review fixes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Adversarial review → all findings fixedRan a four-angle review (correctness / concurrency / API surface / tests) over this branch; every finding is resolved in High severity (both introduced on this branch, both fixed + regression-tested):
Firmware conformance: Remote admin: passkeys latch only from response-shaped messages (inbound requests can't poison the cache); managed-mode gate is local-target-only; passkey persistence removed (dead by firmware's Pre-existing engine defects fixed: wire-id collision stranding in API shape (pre-publication): BLE: the delayed priority downgrade lives in a per-connect scope — a stale timer can no longer fire into the next session's handshake (each new test fails against the pre-fix code). Tests: +19 across the suites, prioritized at the riskiest logic (PKC key-conjunction fallbacks, two-remote passkey isolation, TTL pruning, rebind clearing, queue-rejection of in-flight RPCs, buffer-overflow All gates green locally: 600+ tests across modules, detekt, spotless, refreshed ABI dumps. 🤖 Generated with Claude Code |
…rgets FakePeripheral overrides Peripheral.identifier, which is kotlin.uuid.Uuid on Kable's iOS targets (experimental API) — the override compiled on JVM/Android (String typealias) but failed compileTestKotlinIosX64 / IosSimulatorArm64 in CI's full-check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
androidDeviceTest source set running the cs1-cs6 conformance envelope against a real radio: scan by Meshtastic service UUID (bonded-first, RSSI-sorted, 3-candidate retry), connect through the production BleTransport(address) factory (MTU-517 + connection-priority hook), two-stage handshake, read-only admin RPC round-trips, a 177-byte send to the radio's own node num as MTU proof (no LoRa TX), SQLDelight persistence, and same-transport reconnect. Assume-skips when no radio is advertising so CI never requires hardware. Run with: ./gradlew :transport-ble:connectedAndroidDeviceTest Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Five ship-blocking bugs caught by MeshtasticBleConformanceTest on a
Pixel 6a against live radios (fw 2.7.24), none reachable by JVM/fake
tests:
- transport-ble: TORADIO is CHR_PROPS_WRITE only on real firmware
(NRF52Bluetooth.cpp:213) — write-without-response is refused by
Android outright; use WriteType.WithResponse. Docs updated to match.
- engine: seed the wire packet-id counter randomly per instance.
Starting at 1 every session collided with the firmware's ~10-minute
packet-history dedup across reconnects, silently swallowing every
RPC of the new session (Timeout with no response on the air).
nextMessageId() now also skips 0 ("unset" on the wire).
- engine: publish configBundle/channels synchronously at the Stage-2
commit, before connect() resumes — visibility was previously gated
on the async storage flush and lost the race on real disk latency.
- storage-sqldelight (Android): enable WAL via
enableWriteAheadLogging(); execSQL("PRAGMA journal_mode=WAL")
returns a result row and throws on re-activation.
- transport-ble: frames() is re-collectable after disconnect()
(per-cycle frame channel + collector guard reset), honouring the
documented reuse-after-disconnect contract exercised by cs6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Completes AdminMessage coverage with the hardened-build (MESHTASTIC_LOCKDOWN) lockdown surface, and migrates the protobuf dependency model + all docs from the vendored-submodule scheme to the published org.meshtastic:protobufs artifact. Lockdown: - AdminApi.lockdown(LockdownAuth): local-only (rejects forNode targeting; the firmware consumes the passphrase inline on the phone link), fire-and-forget; the device replies with a fresh lockdown_status. - MeshEvent.LockdownStatusChanged: replaces the prior ProtocolWarning stub; handled identically mid- and post-handshake. - Tests (outbound + inbound), updated CapturingAdminApi fake, regenerated ABI. - Builds on the pinned 2.7.25 proto; the newer LockdownAuth.disable / max_session_seconds and LockdownStatus.State.DISABLED arrive in a follow-up once a stable proto release ships them. Protobufs: - Stays pinned at 2.7.25. Audit found zero field changes across the 46 SDK-consumed messages vs develop-SNAPSHOT; the only delta is the device-only NodeDatabase restructure, not consumed here. Docs: - protocol.md: correct the AdminMessage field numbers and add a lockdown section. - ADR-015 records the submodule + :proto-module -> published-artifact migration; superseded-by notes added to ADR-000/001/003/006/008/013. - Sweep guidance/architecture docs (AGENTS, GEMINI, CONTRIBUTING, README, versioning, ci-cd, module-graph, enforcement, transport-*, migration guides, wasm roadmap) to the artifact model; drop the dead git-submodules Renovate manager; fix Dokka V1 -> V2 (dokkaGenerate) command references. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Summary
Prerequisite fixes for cutting Meshtastic-Android over to this SDK (targeting the 0.2.0 release): remote admin now matches what modern firmware actually requires (PKC routing + per-node session passkeys), Android BLE negotiates an MTU so writes over 20 bytes work at all, and inbound frames the engine used to drop (MQTT client-proxy, XModem, FileInfo, mid-handshake mesh packets, queue rejections) are now surfaced.
It also completes
AdminMessagecoverage with hardened-build storage-lockdown support, and syncs every doc/ADR to the published-artifact proto model — the code swap landed in #5, but the guidance/architecture docs still described the old vendored-submodule scheme.Type of change
(
sendTextgained a defaultedreplyIdparameter;SendFailure/MeshEventgained sealed variants — includingMeshEvent.LockdownStatusChanged— andAdminApigainedlockdown(...). Source-compatible additions, binary-breaking pre-1.0; ABI dumps regenerated. Remote-admin wire behavior changed deliberately, see below.)What changed
Remote admin correctness (core)
ADMIN_APPpackets are built through a single choke point (prepareOutboundAdminPacket, shared by the RPC / ACK'd-send / fire-and-forget paths). When both nodes have published keys:pki_encrypted = true, target'spublic_key, channel 0. Otherwise: fall back to a channel namedadmin(legacy secondary-channel admin). Priority defaults toRELIABLE. Previously remote admin went out on channel 0 in the clear, which firmware 2.5+ rejects — verified against the Android reference client'sCommandSenderImpl/buildMeshPacket.SessionKeyExpiredsingle-shot retry now re-seeds against the target node (admin reads don't require a passkey; the response carries a fresh one, latched by responder).Storage lockdown (core)
AdminApi.lockdown(LockdownAuth)completesAdminMessagecoverage — everypayload_variantis now exposed. It drives the hardened-build (MESHTASTIC_LOCKDOWN) lockdown flow: provision / unlock /lock_now. Local-only (aforNode(...)-scoped call returnsUnauthorized; firmware consumes the passphrase inline on the phone link viaPhoneAPI::handleLockdownAuthInlineand never routes it over the mesh) and fire-and-forget (the device replies with a freshlockdown_status, not a routing ACK).MeshEvent.LockdownStatusChangedreplaces thelockdown_statusProtocolWarningstub with a typed event, handled identically mid- and post-handshake (firmware emits it right afterconfig_complete_id).LockdownAuth.disable/max_session_seconds,LockdownStatus.State.DISABLED) are split into the stacked draft feat(core): develop-only lockdown fields (disable, max_session_seconds, DISABLED) #7 so this PR stays on the stableorg.meshtastic:protobufs2.7.25 pin and remains mergeable. A field-level diff confirmed develop-SNAPSHOT has zero changes across the 46 SDK-consumed messages (the only delta is the device-onlyNodeDatabaserestructure, not consumed here).Android BLE (transport-ble)
BleTransport(address)installs a best-effort post-connect hook:requestMtu(517)andCONNECTION_PRIORITY_HIGHfor the 30 s handshake window, then downgrade to Balanced. Android keeps the ATT MTU at 23 unless the central negotiates, capping write-without-response at 20 bytes — nearly every real ToRadio frame exceeds that, so the transport was effectively broken on Android (conformance had only run on JVM/macOS where CoreBluetooth auto-negotiates). Runs once per link establishment, including auto-reconnect cycles.:transport-ble:connectedAndroidDeviceTest, cs1–cs6) and the real-hardware fixes it surfaced have since landed (acknowledged writes, randomized packet-id seeding, synchronous config publication, re-collectableframes()). It skips (does not fail) when no radio is advertising, so CI never needs hardware.Inbound coverage (core)
MeshEvent.MqttProxyMessage/MeshEvent.XmodemPacket/MeshEvent.FileInfoReceivedreplace thewarnUnhandledVariantdrops — unblocks the app's MQTT client-proxy and firmware-update (XModem) features. Outbound already works viasendRaw(ToRadio(...)).PacketsDropped) and flushed through the normal Ready pipeline — previously silently lost.QueueStatus.res != 0fast-fails the handle asSendFailure.QueueRejected(res)(and fails any pending RPC sharing the wire id) instead of letting callers wait out the full ACK timeout for a packet the firmware already rejected.Proto-artifact doc/ADR sync
:protomodule → publishedorg.meshtastic:protobufsartifact) landed in build: replace local Wire codegen with org.meshtastic:protobufs SDK #5, but the guidance/architecture docs still described the old model. ADR-015 now records the migration with superseded-by notes on ADR-000/001/003/006/008/013, and the sweep updatesAGENTS.md,GEMINI.md,CONTRIBUTING.md,README.md,versioning.md,ci-cd.md,module-graph.md,enforcement.md, the transport deep-dives, the migration guides, and the wasm roadmap. The deadgit-submodulesRenovate manager is removed (thegradlemanager already covers the artifact), and stale Dokka V1 → V2 (dokkaGenerate) command references are fixed.protocol.md§13: corrected theAdminMessagefield numbers (the listed tags were wrong) and added a lockdown subsection.Ergonomics / docs
sendText(..., replyId)for threaded replies (decoded.reply_idwithout the emoji flag).DeviceStorage.loadNodes()documented as host-facing (the engine reseeds the node DB from the handshake and never calls it).[Unreleased].Tests
AndroidCutoverPrereqsTest: PKC routing (both-keys →pki_encrypted/key/channel-0/RELIABLE; no-keys → named admin channel), QueueStatus fast-fail, typed aux events (incl.LockdownStatusChanged),sendTextreplyId.AdminApiRemainingTest(outboundlockdown, incl. the local-only remote-targeting rejection) +LockdownStatusEventTest(inboundlockdown_status→ typed event).HandshakeAndReconnectTest: mid-handshake buffering; the three session-passkey tests rewritten for per-node semantics (local passkey must not leak onto remote targets; reseed targets the remote; retry stays single-shot).spotlessCheck,detekt,:core:jvmTest(602 tests),dokkaGenerate(KDoc coverage),checkKotlinAbiall green locally. iOS targets ride on CI.Related issue / discussion
Part of the Meshtastic-Android → meshtastic-sdk hard-cutover track (see
docs/architecture/meshtastic-android-migration.md). Follows #5 (protobufs artifact swap). Stacked follow-up: #7 (draft) re-adds the develop-only lockdown proto fields once a stableorg.meshtastic:protobufsrelease ships them.Affirmations
git commit -s).CONTRIBUTING.md../gradlew updateKotlinAbiand committed the regeneratedapi/*.apifiles.CommandSenderImpl, firmwareAdminModulePKC requirements,PhoneAPI::handleLockdownAuthInlinefor lockdown).🤖 Generated with Claude Code