Skip to content

feat: lockdown mode — spec + protobufs bump + coordinator foundation#1779

Open
niccellular wants to merge 17 commits into
mainfrom
007-lockdown-mode
Open

feat: lockdown mode — spec + protobufs bump + coordinator foundation#1779
niccellular wants to merge 17 commits into
mainfrom
007-lockdown-mode

Conversation

@niccellular

@niccellular niccellular commented May 13, 2026

Copy link
Copy Markdown
Member

Implements MESHTASTIC_LOCKDOWN client support for Meshtastic-Apple using the typed proto wire format from meshtastic/protobufs PR #911.

Status

T001–T010 T011–T027 T028
Foundation + protos
UI + tests + a11y + i18n
Manual hardware verification ⏸ requires hardened-firmware device

xcodebuild test ... -destination 'platform=iOS Simulator,name=iPhone 15' CODE_SIGNING_ALLOWED=NO
35 tests, 6 skipped (Keychain entitlement), 0 failures.

What lands

Foundation

  • protobufs submodule bumped to 1c62540 (PR Waypointform drop maps pin #911 merge); Swift bindings regenerated. Includes the 4 unrelated portnums upstream added since the prior pin (remoteShellApp, lorawanBridge, atakPluginV2, groupalarmApp) handled as no-ops in processFromRadio.
  • LockdownState.swift — 7-case enum.
  • LockdownPassphraseStore.swift — per-peripheral Keychain cache, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, synchronizable=false. LockdownPassphraseStoring protocol so tests can inject a fake.
  • LockdownCoordinator.swift@MainActor ObservableObject. Auto-replay on LOCKED with cache hit, cache clear on auto-replay reject (backoff=0), pendingLockNow flag resolved by next LOCKED or BLE disconnect.
  • KeychainHelper.swift — extended with optional accessibility: and synchronizable: params (defaults preserve every existing callsite).
  • AccessoryManager+Lockdown.swiftLockdownSender conformance; builds the strict-gate MeshPacket (to=myNodeNum, from unset, wantAck=true, hopLimit=hopStart=7, priority=.reliable, no pkiEncrypted).
  • AccessoryManager.swiftlockdownStatus dispatch branch, lockdownCoordinator stored property, onConnect/onDisconnect lifecycle hooks.
  • MeshtasticApp.swift — instantiates coordinator, wires sender, injects into environment, auto-disconnects on .lockNowAcknowledged.

Architectural deviation from plan: the plan said "extend BluetoothManager", but in this repo that file is a 27-line scan/connect stub. Real ToRadio sends go through AccessoryManager. tasks.md T008 amended.

UI

  • LockdownSheet.swift (T011) — full-screen sheet with three content variants driven by LockdownState: passphrase entry/provisioning, unlock-failed with inline error, backoff countdown with TimelineView. Optional Boots / Hours TTL fields behind a DisclosureGroup.
  • ContentView.swift (T012) — .fullScreenCover gate with non-dismissable binding (FR-012).
  • SecurityConfig.swift (T017/T021/T024) — Lockdown section with status badge, Boots/Expires rows, destructive Lock Now button with confirmation alert, Forget Stored Passphrase button.
  • Connect.swift (T025) — region-unset banner now also checks !lockdown.isBlockingSession; non-lockdown firmware unchanged.

Tests

  • LockdownCoordinatorTests.swift (T013/T015/T020/T022) — 16 unit tests covering every state transition incl. forward-compat for STATE_UNSPECIFIED.
  • LockdownPassphraseStoreTests.swift (T023) — 6 round-trip tests against the real Keychain. XCTSkip on errSecMissingEntitlement so unsigned harnesses don't false-fail.

i18n (T016)

22 lockdown.* keys in Localizable.xcstrings, English source. crowdin will pick them up.

a11y (T027)

VoiceOver labels on the byte counter and Submit button, Dynamic Type cap at accessibility3 for Form layout integrity.

Privacy (T026)

Confirmed: no Logger / os_log / print call site touches the passphrase value. Coordinator clears pendingPassphrase on every state transition.

Reviewer notes

The xcodeproj uses explicit group membership (not fileSystemSynchronizedGroups), so the 7 new .swift files (5 app + 2 test) are added directly in Meshtastic.xcodeproj/project.pbxproj with deterministic IDs prefixed DD7E574/DD7E575/DD7E576. plutil -lint clean.

The LockdownPassphraseStoring protocol (added for tests) is the only deviation from the documented contract in specs/007-lockdown-mode/contracts/coordinator-protocol.md — the coordinator's store dep type widened from LockdownPassphraseStore to the protocol. Public method signatures, LockdownSender, and the LockdownCoordinator public surface are unchanged.

T028 (manual quickstart on a MESHTASTIC_LOCKDOWN-flashed device) remains for hardware verification. specs/007-lockdown-mode/quickstart.md has the procedure.

🤖 Generated with Claude Code

niccellular and others added 4 commits May 13, 2026 15:30
Adds the GitHub Spec Kit (https://github.com/github/spec-kit) framework
to drive spec-driven development for future features. Lays down:

  - .specify/           framework templates, scripts, hooks
  - .github/prompts/    Copilot Chat slash-command prompts
                        (/speckit.specify, /speckit.clarify, ...)
  - .github/agents/     Copilot agent definitions
  - .github/copilot-instructions.md  Copilot project context
  - .claude/skills/     Claude Code skill manifests for the same workflow
  - CLAUDE.md           project context for Claude Code sessions
  - .vscode/            workspace settings recommended by spec-kit

No source changes. Feature specs live under specs/<NNN-name>/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First feature spec under the new spec-kit framework. Ports the
Meshtastic-Android lockdown spec to Apple, with Apple-specific
divergences in storage (Keychain, not EncryptedSharedPreferences),
state ownership (single ObservableObject coordinator injected via
Environment — no KMP layer), and UI (SwiftUI .fullScreenCover gate,
SecureField passphrase entry, "Lock Now" in Settings → Security).

Wire format identical to Android: AdminMessage.lockdown_auth out,
FromRadio.lockdown_status in, per meshtastic/protobufs PR #911.

Artifacts:
  - spec.md             the feature spec
  - plan.md             implementation plan
  - research.md         resolutions for the three R-items (Keychain API,
                        ObservableObject vs Observable, verified
                        SwiftProtobuf type names)
  - data-model.md       LockdownState enum + StoredPassphrase struct
  - contracts/          LockdownCoordinator + LockdownSender contracts
  - quickstart.md       end-to-end manual test plan
  - tasks.md            T001-T028 broken out by user story
  - checklists/         requirements quality + security audit
  - analysis.md         cross-artifact consistency review

Output of /speckit.specify + /speckit.plan + /speckit.tasks +
/speckit.checklist + /speckit.analyze in this order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the full upstream proto set from meshtastic/protobufs master at
commit 1c62540 (the merge of PR #911 — LockdownAuth + LockdownStatus).
Regenerates Swift bindings under MeshtasticProtobufs/Sources/meshtastic/
via the existing scripts/gen_protos.sh invocation of protoc.

Adds LockdownAuth (admin.proto) and LockdownStatus (mesh.proto). The
new oneof cases AdminMessage.OneOf_PayloadVariant.lockdownAuth and
FromRadio.OneOf_PayloadVariant.lockdownStatus are now available. Note
that the generated Swift types are UNPREFIXED (e.g. LockdownAuth, not
Meshtastic_LockdownAuth) because the .proto files lack a swift_prefix
option.

Also picks up other upstream deltas accumulated since c8d5047,
including a new serial_hal.proto.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the foundational layer of MESHTASTIC_LOCKDOWN client support
per specs/007-lockdown-mode/. NO USER-FACING UI YET — coordinator stays
at .none on non-lockdown firmware, so this change is functionally inert
for existing devices. UI (LockdownSheet, SecurityConfig Lock Now row),
tests, and banner suppression follow in subsequent commits.

What lands:

  - Meshtastic/Model/LockdownState.swift (new)
      7-case enum mirroring the Android sealed class. Equatable for
      SwiftUI diffing.

  - Meshtastic/Helpers/LockdownPassphraseStore.swift (new)
      Per-peripheral JSON-in-Keychain cache. Keyed by CBPeripheral
      identifier (Device.id, a UUID). Uses
      kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly so silent
      auto-replay can fire on first foreground after device reboot.
      synchronizable=false — lockdown is a per-device pairing, not
      iCloud-synced.

  - Meshtastic/Helpers/LockdownCoordinator.swift (new)
      @mainactor ObservableObject driving the state machine. Owns
      auto-replay on LOCKED (silent send when cache hits), cache delete
      on auto-replay UNLOCK_FAILED with backoff=0, pendingLockNow flag
      resolved by the next inbound LOCKED status OR the BLE disconnect
      (whichever comes first). LockdownSender protocol defines the
      outbound dependency.

  - Meshtastic/Helpers/KeychainHelper.swift (modified)
      Added optional accessibility: CFString and synchronizable: Bool
      params to save/read/delete. Defaults preserve every existing
      callsite. Required so LockdownPassphraseStore can opt into
      stricter accessibility + sync=false without forking the helper.

  - Meshtastic/Accessory/Accessory Manager/AccessoryManager+Lockdown.swift (new)
      Conforms AccessoryManager to LockdownSender. Builds the
      AdminMessage.lockdown_auth ToRadio packet with the firmware-required
      MeshPacket invariants:
        to = myNodeNum, from unset, channel=0, wantAck=true,
        hopLimit=hopStart=7, priority=.reliable,
        decoded.portnum=.adminApp, NO pkiEncrypted.
      Sends via the existing send(toRadio, debugDescription:) path so
      the BLE serial queue handles it like any other admin write.

  - Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift (modified)
      - new `var lockdownCoordinator: LockdownCoordinator?` (set from MeshtasticApp)
      - processFromRadio: new `case .lockdownStatus(let status):` branch
        before the default arm; forwards to coordinator.handle(status)
      - closeConnection: calls coordinator.onDisconnect() so a pending
        Lock Now resolves to .lockNowAcknowledged on the BLE drop, and
        per-connection state is cleared

  - Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift (modified)
      Step 0 of the connect sequence now calls
      coordinator.onConnect(peripheralID: device.id) — firmware
      requires re-auth on every new connection regardless of storage state.

  - Meshtastic/MeshtasticApp.swift (modified)
      Instantiates LockdownCoordinator() in init(), wires
      accessoryManager as its LockdownSender (deferred call to avoid an
      init-time cycle), assigns accessoryManager.lockdownCoordinator,
      and exposes it as @StateObject + .environmentObject so the UI
      work that follows can observe it.

Architectural deviation from the plan: the plan said "extend
BluetoothManager", but in this repo BluetoothManager is a 27-line
scan/connect stub — actual ToRadio sends go through AccessoryManager
(@mainactor, ObservableObject, singleton-ish). The implementation
pivoted accordingly; specs/007-lockdown-mode/tasks.md T008 has been
amended to reflect reality.

Manual step required after merging: add the four new .swift files to
the Meshtastic target via Xcode's "Add Files to Project" — the
xcodeproj uses explicit group membership (not
fileSystemSynchronizedGroups), so file drops don't auto-join the target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
niccellular and others added 10 commits May 15, 2026 16:01
Full-screen sheet rendered when LockdownCoordinator.state requires the
user to act. Three content variants driven by a switch on the state:

  - PassphraseEntryContent(mode: .provision)            (.needsProvision)
  - PassphraseEntryContent(mode: .unlock(reason:))      (.locked, .unlockFailed)
  - BackoffCountdownContent(deadline:)                  (.unlockBackoff)

PassphraseEntryContent renders a Form with three sections:
  1. SecureField passphrase entry (.textContentType(.password),
     autocorrection + autocapitalisation disabled) plus a live N/32 byte
     counter that turns red outside the 1..32 range. An optional inline
     error banner is shown for .unlockFailed.
  2. DisclosureGroup (collapsed by default) with optional Boots remaining
     and Hours valid TextFields. Empty fields submit 0 so the firmware
     uses its defaults; non-numeric input disables Submit.
  3. Submit button gated on (passphrase valid) AND (TTL fields parseable)
     AND (AccessoryManager has a device.num). A "Connecting to device..."
     caption surfaces when the coordinator is not yet ready.

Hint copy for the unlock variant is selected from the firmware's
lock_reason string per the table in specs/007-lockdown-mode/spec.md
(needs_auth, token_*, auto_replay_wrong_passphrase, unknown). The
component falls back to the needs_auth string for forward-compat.

BackoffCountdownContent uses TimelineView(.periodic) to render a
deadline-relative countdown each second. It is read-only and has no
Submit. When the deadline elapses the view stays mounted until the
next inbound LockdownStatus changes the coordinator state; this
matches the strict reading of data-model.md (the coordinator owns
all state transitions; views do not call back into it on a timer).

NFR-002: passphrase wiped from local @State on submit and on
onDisappear; never logged.
FR-012: .interactiveDismissDisabled(true) on the NavigationStack so
the sheet cannot be swipe-dismissed.

Strings are LocalizedStringKey references; the corresponding entries
in Localizable.xcstrings land in T016. Until then they fall back to
the keys themselves at runtime.

Presentation of this view as a .fullScreenCover (with the dismiss
binding tied to coordinator state) is T012 in ContentView.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the 5 lockdown Swift files to Meshtastic.xcodeproj target membership
(PBXBuildFile + PBXFileReference + group children + Sources phase). New
Lockdown PBXGroup created under Views for LockdownSheet.swift. New IDs
namespaced under DD7E574/575/576 prefixes (zero collisions with existing).

Also adds switch cases for four new PortNum values pulled in by the
protobufs bump in commit 4c5e637 (remoteShellApp, lorawanBridge,
atakPluginV2, groupalarmApp). All four are no-op handlers consistent
with how other unhandled apps are logged in this switch; no behavior
change for existing functionality.

xcodebuild now compiles cleanly:
  xcodebuild -workspace Meshtastic.xcworkspace -scheme Meshtastic \
             -destination 'generic/platform=iOS' build \
             CODE_SIGNING_ALLOWED=NO
  ** BUILD SUCCEEDED **

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two build issues surfaced after pulling lockdown files into the target:

1. AccessoryManager+Lockdown.swift triggered a Swift-6 cross-actor
   warning ("conformance crosses into main actor-isolated code"). Root
   cause: LockdownSender was actor-agnostic but AccessoryManager is
   @mainactor. Fix: mark the LockdownSender protocol @mainactor since
   its only caller (LockdownCoordinator) is already @mainactor. Drops
   the Task { @mainactor in ... } wrapper inside sendLockdownAuth(...);
   only the actual `try await send(...)` call now needs a Task because
   that method is async.

2. LockdownSheet.swift line 151:
     .foregroundStyle(isPassphraseValid ? .secondary : .red)
   .secondary is HierarchicalShapeStyle, .red is Color. The ternary
   couldn't infer a common type. Fix: explicit Color.secondary /
   Color.red.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Presents LockdownSheet as a non-dismissable .fullScreenCover when the
LockdownCoordinator reports any state that demands user action:

  - .needsProvision
  - .locked(reason:)
  - .unlockFailed
  - .unlockBackoff(deadline:)

The cover dismisses automatically when the coordinator transitions to
.none, .unlocked, or .lockNowAcknowledged.

The isPresented binding's setter is intentionally a no-op so users
cannot dismiss the cover via gesture; visibility is fully driven by
coordinator.state per FR-012.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…T020, T022)

16 unit tests covering every transition in data-model.md using fake
collaborators (FakeLockdownSender, FakePassphraseStore). No BLE,
no Keychain.

Coverage:
  - Initial state is .none
  - NEEDS_PROVISION transition (T015 US-2)
  - Submit-from-NEEDS_PROVISION caches passphrase on UNLOCKED (T015 US-2)
  - LOCKED without cache transitions to .locked(reason:) (T013 US-1)
  - LOCKED with cache auto-replays silently (T013 US-4)
  - UNLOCK_FAILED user-submit, backoff=0 -> .unlockFailed (T013 US-1)
  - UNLOCK_FAILED user-submit, backoff>0 -> .unlockBackoff with deadline (T013 US-1)
  - UNLOCK_FAILED auto-replay, backoff=0 -> clears cache, .locked (T022 US-4)
  - UNLOCK_FAILED auto-replay, backoff>0 -> preserves cache, .unlockBackoff (T022 US-4)
  - Lock Now sets pending flag, sends empty-passphrase auth (T020 US-3)
  - LOCKED with pendingLockNow -> .lockNowAcknowledged (T020 US-3)
  - onDisconnect with pendingLockNow -> .lockNowAcknowledged (T020 US-3)
  - onDisconnect without pendingLockNow -> .none
  - STATE_UNSPECIFIED ignored (analysis.md G3 forward-compat)
  - forgetCachedPassphrase deletes cache for connected peripheral
  - forgetCachedPassphrase no-op when no peripheral connected

Required test-seam plumbing:
  - LockdownPassphraseStoring protocol added so tests can inject a fake.
    LockdownPassphraseStore conforms. LockdownCoordinator's `store`
    dependency switched to the protocol type. No effect on production
    behavior; this is below the level of the publicly documented contract
    (LockdownSender protocol + coordinator public methods are unchanged).
  - LockdownCoordinatorTests.swift added to Meshtastic.xcodeproj test
    target via direct pbxproj edits.

Run:
  xcodebuild test -workspace Meshtastic.xcworkspace -scheme Meshtastic \
                  -destination 'platform=iOS Simulator,name=iPhone 15' \
                  -only-testing:MeshtasticTests/LockdownCoordinatorTests
  ** TEST SUCCEEDED **

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 unit tests against the real iOS Keychain:
  - read of unknown peripheral returns nil
  - save then read round-trips passphrase + TTL fields
  - save overwrites previous entry
  - delete removes entry
  - delete of unknown peripheral returns true (treated as no-op success)
  - entries are isolated per peripheral UUID

Each test uses a fresh UUID and cleans its key in tearDown so no
cross-test interference and no production keychain entries touched.

setUpWithError probes the Keychain via KeychainHelper.standard.save
and throws XCTSkip if SecItemAdd returns errSecMissingEntitlement
(-34018), which happens in unsigned test harnesses like the project's
default `xcodebuild ... CODE_SIGNING_ALLOWED=NO` invocation. The tests
exercise the real Keychain when the test target is signed.

  xcodebuild test -workspace Meshtastic.xcworkspace -scheme Meshtastic \
                  -destination 'platform=iOS Simulator,name=iPhone 15'
  ** TEST SUCCEEDED **
  (35 tests, 6 skipped, 0 failures)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…017, T019, T021, T024, T025)

Settings → Security → Lockdown (LockdownSection):
  - Renders only when lockdown.state == .unlocked (T024 US-5).
  - Status row with green lock.open.fill glyph.
  - "Boots remaining: N" row when bootsRemaining > 0.
  - "No time limit" or "Expires <localized date/time>" row driven by
    validUntilEpoch (T024 US-5).
  - Destructive "Lock Now" button (T017 US-3) with a confirmation alert
    that explains the reboot side effect.
  - "Forget Stored Passphrase" button (T021 US-4) calls
    coordinator.forgetCachedPassphrase().
  - All other coordinator states show EmptyView so the section disappears
    cleanly; .needsProvision / .locked / .unlockFailed / .unlockBackoff
    surface via the full-screen sheet, not in Settings.

MeshtasticApp:
  - .onChange(of: lockdownCoordinator.state) tears down the BLE connection
    when state resolves to .lockNowAcknowledged (T019 US-3). This is the
    one-way exit from the Lock Now flow; ContentView's cover binding also
    dismisses since .lockNowAcknowledged is non-blocking.

Banner suppression (T025 FR-013):
  - Connect.swift's region-unset banner now also checks
    !lockdown.isBlockingSession. Non-lockdown firmware leaves
    coordinator state at .none which returns false, so existing
    behavior for non-hardened devices is preserved. Hardened-but-locked
    devices stop nagging the user with banners they can't act on.

LockdownCoordinator helper:
  - New `isBlockingSession: Bool` flag the views use; ContentView's
    gate now defers to it (single source of truth).

Build:
  xcodebuild ... -destination 'generic/platform=iOS' build
  ** BUILD SUCCEEDED **

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 new "lockdown.*" keys for LockdownSheet (passphrase entry, provisioning,
backoff countdown, lock_reason hint copy) and the Settings → Lockdown
section. English source values only; entries are state=new so the next
crowdin sync will pick them up for translation.

Keys cover:
  - lockdown.set_passphrase.* (provisioning title/hint/submit)
  - lockdown.unlock.* (unlock title/submit)
  - lockdown.passphrase.field, lockdown.passphrase.wrong
  - lockdown.session.* (TTL fields under disclosure group)
  - lockdown.connecting (Submit gating caption)
  - lockdown.backoff.title, lockdown.backoff.body %lld, lockdown.backoff.explanation
  - lockdown.locked.{needs_auth, token_expired, token_rtc, token_tamper, auto_replay_failed}
    (hint copy switched by firmware-supplied lock_reason)

The backoff key uses the %lld placeholder convention that matches the
LocalizedStringKey interpolation `"lockdown.backoff.body \(remaining)"`
in LockdownSheet.swift; SwiftUI's catalog lookup synthesizes the same key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LockdownSheet.PassphraseEntryContent:
  - byte-counter Text now exposes an explicit accessibilityLabel
    "Passphrase length N of 32 bytes" so VoiceOver reads it as English
    instead of pronouncing "N/32" as "N slash 32".
  - Submit button accessibilityHint explains the disabled state
    ("Enter a passphrase between 1 and 32 bytes.") so VoiceOver users
    know why the button is inactive.

  - dynamicTypeSize cap at ...accessibility3. The Form's three-section
    layout with passphrase + DisclosureGroup + Submit breaks visually
    beyond accessibility3 in iOS 16.4. Cap matches what the rest of the
    Settings forms do implicitly; users who need larger sizes still get
    a usable layout at .accessibility3 (roughly 2x default).

BackoffCountdownContent already had an accessibilityLabel on the
TimelineView countdown ("Retry available in N seconds"); no further
change there.

SF Symbols already in use across the sheet: lock.trianglebadge.exclamationmark
(backoff), exclamationmark.triangle.fill (inline error), clock.badge
(disclosure label). Matches the SF Symbols set called out in
specs/007-lockdown-mode/spec.md "Design Standards Compliance".

Build:
  xcodebuild ... build CODE_SIGNING_ALLOWED=NO
  ** BUILD SUCCEEDED **

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T028 (manual quickstart on hardware) remains open; requires a
MESHTASTIC_LOCKDOWN firmware-flashed device which is out of scope
for code-level work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@niccellular niccellular marked this pull request as ready for review May 15, 2026 21:58
Brings the lockdown branch up to date with origin/main (124+ commits
ahead at merge time). Resolution summary by file:

  - MeshtasticProtobufs/Sources/meshtastic/*.pb.swift (9 files):
    Both branches independently regenerated these from the same
    protobufs revision (1c62540). Took main's regenerated output;
    LockdownAuth + LockdownStatus types still present.

  - .specify/**, .github/{copilot-instructions.md,prompts,agents},
    .claude/skills/**, .vscode/settings.json, CLAUDE.md:
    Both branches added spec-kit scaffold. Took main's versions
    (scaffold is byte-identical or main has minor edits).

  - Localizable.xcstrings: took main's, re-injected the 22 lockdown.*
    keys via the same python script used in commit 1dd4abc.

  - Meshtastic.xcodeproj/project.pbxproj: took main's (major structure
    changes for the new Watch app target etc.), then re-added the 7
    lockdown PBXFileReference + PBXBuildFile + group + Sources phase
    entries (5 app sources + 2 test sources + Lockdown view group)
    with the same DD7E574/575/576 IDs. plutil -lint clean.

  - Meshtastic/Views/ContentView.swift: main rewrote body to dispatch
    via @ViewBuilder var tabContent (iOS 18+ TabView vs legacy). Moved
    the .fullScreenCover(LockdownSheet) modifier onto the new body
    structure.

  - Meshtastic/Views/Settings/Config/SecurityConfig.swift: main added
    a #Preview block; kept both my LockdownSection extension and the
    Preview. Augmented the Preview with .environmentObject(LockdownCoordinator())
    so SecurityConfig's new @EnvironmentObject dependency resolves.

  - Meshtastic/MeshtasticApp.swift: main changed persistenceController
    type to optional (test-mode support) and added a Mesh Map secondary
    WindowGroup. Kept both: lockdown coordinator construction + the new
    Watch/optional-persistence/MapWindow plumbing. Injected
    lockdownCoordinator into both WindowGroups' environment chains.

  - Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift:
    Main added `self.activeDeviceNum = nil` in closeConnection AND four
    new portnum cases (.groupalarmApp, .lorawanBridge, .remoteShellApp,
    .unknownApp) using its own copy-text. Kept main's variants;
    re-added .atakPluginV2 (which main missed) alongside. Kept my
    lockdownCoordinator?.onDisconnect() call after activeDeviceNum.

  - Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift:
    auto-merged; main's `maxAttempts:` retry refactor preserved my
    onConnect(peripheralID: device.id) call at step 0.

Build / test: cannot verify locally (this Xcode lacks watchOS 26.5 SDK
required by main's new Watch App target). CI will exercise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 17, 2026

Copy link
Copy Markdown

📄 Docs staleness warning

This PR modifies user-facing Swift source files but does not update any page under docs/user/ or docs/developer/.

Changed source files:

Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift
Meshtastic/Accessory/Accessory Manager/AccessoryManager+Lockdown.swift
Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift
Meshtastic/Model/LockdownState.swift
Meshtastic/Views/Connect/Connect.swift
Meshtastic/Views/ContentView.swift
Meshtastic/Views/Lockdown/LockdownSheet.swift
Meshtastic/Views/Settings/Config/SecurityConfig.swift

What to check:

Changed area Likely doc page
Views/Messages/ docs/user/messages.md
Views/Nodes/ docs/user/nodes.md
Views/Map/ docs/user/map.md
Views/Settings/Bluetooth/ docs/user/bluetooth.md
Views/Settings/Discovery/ docs/user/discovery.md
Views/Settings/MQTT/ docs/user/mqtt.md
Views/Settings/TAK/ docs/user/tak.md
Views/Settings/Firmware/ docs/user/firmware.md
Views/Settings/ (telemetry/sensor) docs/user/telemetry.md
Views/Settings/ (general) docs/user/settings.md
Meshtastic Watch App/ docs/user/watch.md
Model/ docs/developer/swiftdata.md or docs/developer/architecture.md
Accessory/Transports/ docs/developer/transport.md

If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the skip-docs-check label to dismiss this warning.

After updating docs/, re-run bash scripts/build-docs.sh --output Meshtastic/Resources/docs --beta locally and commit the regenerated HTML bundle.

niccellular and others added 2 commits May 18, 2026 16:23
Both branches added strings since the last merge; took main's version
and re-injected the 21 lockdown.* keys via the same Python script used
in commit 1dd4abc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

Bumps protobufs submodule from master (1c62540) to develop tip (7ffb4bb)
which includes PR #916: a new `uint32 max_session_seconds = 5` on
`LockdownAuth`. Per-boot uptime cap, in seconds. 0 = unlimited (current
behavior). Non-zero arms a firmware uptime timer at unlock; on expiry the
device revokes per-connection auth and reboots without deleting the token
so the next boot auto-unlocks via the boot-count TTL and arms a fresh
session window. Total exposure ceiling = boots_remaining * max_session_seconds.

Wiring across the app:

  Meshtastic/Helpers/LockdownPassphraseStore.swift
    StoredPassphrase gains `maxSessionSeconds: UInt32` with default 0.
    Custom Decodable init via decodeIfPresent so cached entries written
    before this commit still load cleanly (treated as unlimited = 0).

  Meshtastic/Helpers/LockdownCoordinator.swift
    LockdownSender protocol method gains `maxSessionSeconds: UInt32`.
    submitPassphrase(_:bootsRemaining:validUntilEpoch:maxSessionSeconds:)
    gains the param with default 0 for source compat with existing call
    sites. Auto-replay reuses the cached value; lockNow sends 0.
    pendingMaxSessionSeconds tracks the in-flight submit through to
    UNLOCKED so the value persists in the cache.

  Meshtastic/Accessory/Accessory Manager/AccessoryManager+Lockdown.swift
    sendLockdownAuth impl threads the value into LockdownAuth.maxSessionSeconds
    before serializing. No change to the MeshPacket envelope.

  Meshtastic/Views/Lockdown/LockdownSheet.swift
    New optional "Session cap (minutes)" TextField in the DisclosureGroup,
    alongside Boots remaining / Hours valid. Converted minutes -> seconds
    on submit; empty -> 0. areTTLFieldsValid now also checks the new
    parse so non-numeric input keeps Submit disabled.

  Localizable.xcstrings
    Two new keys: lockdown.session.cap_minutes, lockdown.session.cap_caption.

  MeshtasticTests/LockdownCoordinatorTests.swift
    FakeLockdownSender.Call records maxSessionSeconds. Two new tests:
      - testSubmitPassphrase_threadsMaxSessionSecondsToSenderAndCache
      - testHandle_locked_autoReplay_passesCachedMaxSessionSeconds
    All 18 existing tests continue to pass via the default param value.

  protobufs (submodule)
    1c62540 -> 7ffb4bb. Develop has 9 other commits since master
    (NodeDB split, XEdDSA, PositionLite precision_bits, etc.); none touch
    portnums so no exhaustive-switch impact in AccessoryManager.

Cannot test locally (Xcode here lacks watchOS 26.5 SDK required by main's
new Watch App target in the scheme). CI will build + run tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant