Tier 3 of a protest-hardening review. The BLE mesh is already properly
fenced off from network reachability and needs nothing here; every gap is
on the internet side, or in how someone gets a build they can trust.
Say when Tor is blocked. The bootstrap poll loop simply ended at its
75-second deadline, leaving isStarting true with no further state, so a
network that blocks Tor was indistinguishable from a slow one and the UI
said "starting tor…" indefinitely. TorManager now exposes
bootstrapDidStall and posts .TorBootstrapDidStall, and the app reports
that mesh messaging still works while internet delivery is paused. It is
cleared on each new start or restart, so a later attempt can report again.
Let relays be added by hand. The four built-in relays are well-known
clearnet hostnames, which is four names for a censor to block and no
recourse short of a new build. NostrRelaySettings persists up to eight
additional relays, normalized, .onion accepted, with a settings editor.
They join the same target set as the built-ins and are subject to the
same activation policy. Removal reconciles against the previous set:
the teardown path iterates the current targets, so without that a removed
relay's socket and queued sends would linger — covered by a test that
fails without it. The merged list is cached rather than recomputed,
because allowedRelayList consults it once per candidate URL and would
otherwise read UserDefaults inside that loop.
Stop stranding people who denied location. The activation gate required
location permission or a mutual favorite, but teleporting into a geohash
requires neither, so someone with no permission and no favorites could
sit in a channel that never connected while Tor and the relays stayed
suppressed and nothing said why. Being in a location channel is now a
third arm of the gate, in both the activation service and the relay
manager's copy of the policy, and leaving the channel closes it again.
Stop burning the Tor timeout when Tor is off. GeoRelayDirectory awaited
Tor readiness unconditionally, but with the preference off TorManager has
been shut down, so every refresh spent the full bootstrap deadline and the
directory froze on its cached copy. It now keys on the preference, not on
live readiness: Tor wanted but unavailable must still skip the fetch
rather than fall back to clearnet.
Say what turning Tor off costs. The toggle's copy described it as
hiding your IP "for location channels", understating both scope and
consequence. It now names private messages too, and while the toggle is
off the settings screen states that every relay can see the device IP.
Make builds verifiable. There was no release verification of any kind:
no signatures, no checksums, no documented procedure. Post-takedown that
is the acute gap, because mirrors appear and people install whatever they
can find during a shutdown. source-manifest.yml publishes a per-tag
SHA-256 manifest with a provenance attestation, self-checking before it
publishes, and docs/VERIFYING-A-BUILD.md explains how to verify source and
states plainly that compiled builds from anywhere but the App Store cannot
be verified. It also records the gaps honestly: no published signing key,
no reproducible build, no non-GitHub mirror.
docs/TOR-INTEGRATION.md was substantially stale — it documented a
torrc, SOCKSPort and ControlPort that the in-process Arti client does not
use, and claimed there are no user-visible settings — so it is rewritten,
including the deferred gap below.
Deferred: no Tor bridges or pluggable transports. arti-client is built
without pt-client or bridge-client and bootstraps from stock config, so
in a country that blocks Tor outright there is still no circumvention
path — only a clear report that there isn't. Closing it needs the Rust
features, bridge config through the FFI, and an xcframework rebuild under
the pinned toolchain with a provenance update, which is its own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two CI-robustness fixes for the iOS-simulator job seen on main:
1. The destination picker no longer dies on runner images with placeholder/unavailable simulator lists (macos-26-arm64 20260720.0258): it filters error/unavailable rows, falls back to xcrun simctl's available-device list, and as a last resort creates a simulator from the newest installed iOS runtime's own supported iPhone device types. Each path logs which route was taken.
2. Deflakes NoiseEncryptionServiceTests' quarantine-restore test: the injected 20ms responder timeout also armed during the SETUP handshake, so a loaded runner tore down the half-open responder session before the scenario began (production quarantine/restore verified correct — the failure signature was a fresh msg2 to a session-less peer). Timeout raised to 1s and the fixed 100ms sleep replaced with a bounded waitUntil on the restore condition. No assertion weakened; 10/10 sequential + 3/3 under full-core CPU load.
Replaces the scheduled workflow that pushed upstream georelay CSV directly to main unreviewed with a validator-backed automation that resolves an immutable upstream commit, validates against the reviewed baseline, and opens a non-auto-merged PR (or tracking issue) instead of writing to main. Adds a mirrored strict client-side validator in GeoRelayDirectory (all-or-nothing, size/row/host/coord caps, streamed 512KiB cap, >=50% baseline overlap) that fails safe to last-known-good, and retargets runtime refresh to bitchat's reviewed copy.
Rewrites just clean/nuke to remove only ignored build artifacts (.DerivedData/.build), never git-checkout or rm tracked project files (previously could destroy uncommitted work). Adds a CI guard (check-just-clean-safety.sh) against reintroducing source-mutating cleanup, plus README/Local.xcconfig modernization.
Adds an ios-tests CI job that runs the UIKit/CoreBluetooth-conditional suite on the first available iPhone simulator (serial), and fixes the coverage-summary step ordering so the profile isn't invalidated by the serial benchmark rebuild.
Centralize PTT and voice audio-session ownership, harden courier/bridge/outbox delivery and recovery, correct location and delivery-state races, add privacy/release metadata, and ship reproducible universal Arti slices with Release CI coverage.
Validated by the full iOS suite, repeated audio/fragment/performance regressions, BitFoundation tests, strict lint and dead-code analysis, universal iOS Release builds, and iOS/macOS archives.
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so
platform-specific code is never touched), with test targets indexed and
the share extension built. 277 dead declarations removed or demoted:
dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed-
feature remnants (autocomplete command suggestions, back-swipe tuning,
MediaSendError, GeohashParticipantTracker), unused Tor dormancy
bindings, assign-only properties, unused parameters (renamed to _), and
redundant public accessibility. 13 orphaned localization keys deleted
across all 29 locales (old pre-#1392 location-notes UI, app_info
warnings).
Two real tests were flagged as unused because they never ran: Swift
Testing methods missing @Test (NostrProtocolTests.
testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests.
testAssemblesCompressedLargeFrame). Re-armed both; they pass.
Deliberately kept, now recorded in .periphery.baseline.json: iOS-only
code invisible to the CI macOS scan, C FFI signatures, keep-alive
NWPathMonitor reference, InboundEventKey.eventID (dedup semantics),
wifiBulk capability bit (reserved for Wi-Fi bulk work, used by
BitFoundation package tests), and the String secureClear cluster
(exercised by package tests).
New: .periphery.yml config and an advisory Dead Code CI job (mirrors
the SwiftLint precedent from #1361) that fails on findings not in the
committed baseline.
Verified: full macOS app suite, BitFoundation (119) and BitLogger (13)
package tests green; periphery scan --strict exits clean.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Four spurious CI failures on July 5, all loaded-runner flakiness:
- ViewSmokeTests.voiceAndMediaViews_renderAndWarmCaches asserted an exact
bin count on WaveformCache.shared for the same URL the mounted
VoiceNoteView was concurrently warming at its default 120-bin width;
whichever barrier write landed last owned the entry. Probe the cache with
a dedicated audio file no view touches, and purge both URLs. Also replace
the fixed 250ms sleep for loadDuration's background hop with a waitUntil
poll.
- sendImage_privateChatProcessesAndTransfersImage (and its sendVoiceNote /
sendImage siblings) wait on work that hops through Task.detached; the
global executor is shared with every parallel test worker, so a loaded
runner can exceed the 5s wait. Raise those positive waits to
TestConstants.longTimeout (10s) — waitUntil returns as soon as the
condition holds, so passing runs are unaffected.
- subscribeNostrEvent_addsToTimeline_ifMatchesGeohash raced concurrently
running suites (e.g. CommandProcessorTests) on the process-wide
LocationChannelManager singleton: a mid-test channel flip reroutes or
drops the event permanently, so no fixed wait recovers. The wait loop now
re-asserts the channel and redelivers the event on each poll — idempotent
because channel switches clear the processed-event set and the store
dedups by message ID — so interference heals while genuine failures still
time out.
- The performance floor gate failed on a saturated runner
(gcs.buildAndDecode at 85% of floor). check-perf-floors.sh now re-runs
the benchmark suite up to twice when a metric lands below floor,
appending to the same PERF log and keeping each benchmark's best value
across attempts: noise clears on a retry, a real algorithmic regression
fails every attempt. Floors are unchanged and never lowered by the
mechanism; missing-benchmark failures exit immediately without retrying.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add SwiftLint as an advisory CI-only lint job (no Xcode plugin dependency)
* Harden the advisory lint job and exclude build dirs from local runs
The lint job runs a third-party container image, so drop its token to
read-only, stop actions/checkout from persisting credentials into the
workspace the container can read, and pin the image by digest as well
as tag (tags are mutable). Also add an excluded: list to .swiftlint.yml
so local swiftlint runs don't drown in .build/DerivedData artifacts —
CI checkouts are fresh, so this only affects working trees.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The unpinned setup-swift action installs Swift 6.1, which refuses the
SDK on runner images that have rolled to Xcode 26.5 ("this SDK is not
supported by the compiler"). Jobs passed or failed depending on which
image they landed on. The Xcode-bundled toolchain always matches the
image's SDK, and matches local development. Cache keys now include the
toolchain version so artifacts from one compiler are never restored
into builds with another.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Exclude perf baselines from parallel CI via --skip; sample hung tests
Every app-suite CI run since the perf baselines landed (#1335) has
timed out at the 15-minute job limit — main has been red for five
consecutive runs. The job logs show the PerformanceBaselineTests
fixtures dispatched into the parallel phase despite the
BITCHAT_SKIP_PERF_BASELINES env guard from #1336, followed by ~11
minutes of silence until the timeout kills swiftpm-testing. The suite
passes locally in seconds with identical flags, so the hang is
specific to the CI toolchain/runners — consistent with the known
XCTest-measure-under-parallel-workers hang the serial step was
created to avoid.
Two changes:
- Exclude the baselines from the parallel phase with --skip at the
SPM level, which removes them from the worker processes entirely
instead of relying on the env guard reaching setUpWithError. They
still run (and gate) in the dedicated serial step.
- Wrap the parallel run in a 10-minute watchdog that samples any
still-running test processes before killing them, so if anything
else ever hangs, the run fails fast with thread stacks in the log
instead of a silent 15-minute timeout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Arm the test-hang watchdog only for the test execution phase
Codex review: swift test builds before running, so the watchdog timer
included dependency resolution and compilation — a cold-cache coverage
build on a slow runner could be killed before tests ever started.
Build the tests in their own step (bounded by the 15-minute job
timeout like any build) and run the watchdog around swift test
--skip-build, tightened to 5 minutes now that it times only test
execution.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Hammer transport thread-safety tests from the dispatch pool, not the cooperative pool
The watchdog added in this PR captured the actual CI hang twice, with
identical stacks both times: NostrTransportTests' 100-task groups park
every Swift Concurrency cooperative thread in a blocking queue.sync
(the pool has one thread per core — 3 on CI runners, 10+ on dev
machines, which is why this never reproduced locally). Blocking the
entire cooperative pool violates the forward-progress contract, and
the runners' dispatch wedges under the resulting asyncAndWait flood —
taking concurrently running tests down with it (the panic-reset test
deadlocked in a serviceQueue barrier that never got scheduled).
Run the same 100 concurrent hammer iterations via
DispatchQueue.concurrentPerform from a single global-queue hop instead:
identical thread-safety coverage, executed on dispatch worker threads
where blocking is legal, zero cooperative threads parked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The intermittent CI hang was caught by the new job timeout: the run
froze on the last remaining parallel test slot, an XCTest measure
benchmark (testNostrInboundEventHandling_freshEvents), after 4 hangs
in 5 runs - while the same suite completes in seconds locally and the
test itself is bounded. Independent of the micro-cause, benchmarks
do not belong inside the parallel suite: measuring while test processes
contend for cores is where our 2x CI variance came from. The parallel
run now skips benchmarks (BITCHAT_SKIP_PERF_BASELINES=1) and a
dedicated serial step runs them on an otherwise idle runner with a
6-minute step timeout, feeding the floor gate as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two app-test jobs hung intermittently (40+ minutes against a normal
~4-5), holding macOS runners against GitHub's 360-minute default and
starving the queue - subsequent runs sat pending, which read as "CI now
takes 10+ minutes". Jobs now time out at 15 minutes (3x the normal
duration) so a hang fails loudly instead of silently consuming the
runner pool. The intermittent hang itself is under investigation
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Package.swift's .process("Noise") resource claim silently excluded all
of bitchatTests/Noise/ from compilation since Oct 2025 - including a
complete official-vector runner (cacophony + snow XX transcripts,
transport messages, handshake hash, byte-identical to upstream).
Narrowing the resource to the JSON file and loading via Bundle.module
brings 51 Noise tests back to life, with a guard asserting each
vector's protocol name matches the app's.
CI gains a performance floor gate: perf-floors.json carries deliberately
generous floors (~25% of measured throughput) that catch algorithmic
regressions without flaking on runner variance; PERF lines reach the
gate via an O_APPEND side-channel file since swift test --parallel
swallows passing tests' stdout.
Tests are now hermetic: FavoritesPersistenceService uses an in-memory
keychain under test (fixes the securityd hang that blocked pipeline
benchmarks locally) and read-receipt persistence uses a wiped scratch
UserDefaults suite instead of .standard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
swift test runs with --enable-code-coverage and each matrix job prints
an llvm-cov per-file + total summary (informational only - no
thresholds, so coverage can never be the reason a build goes red).
Local baseline at introduction: 69.7% lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NostrRelayManager no longer strands work when Tor is slow to bootstrap:
failed readiness waits retry (bounded by nostrTorReadyMaxWaitAttempts)
instead of dropping queued relay connections, parked EOSE callbacks fire
after exhaustion so callers never hang, and sends made before Tor is
ready are queued locally (capped) instead of being dropped on a failed
wait - still strictly fail-closed.
MessageRouter now prefers a connected transport over a merely
window-reachable one, and sends made on a weak reachability signal are
retained in the outbox until a delivery/read ack confirms receipt
(receivers dedup by message ID), with resends bounded by attempt count.
GCS sync filters from the wire are bounds-checked (p in 1...32, m > 1)
at both the packet decode and filter decode layers; oversized Golomb
parameters previously decoded to garbage via silent shift overflow.
BLELinkStateStore is now explicitly pinned to bleQueue: debug builds
trap any access from another queue, enforcing the ownership discipline
the surrounding code already relied on by convention.
CI gains an iOS simulator build job (arm64 only; the vendored Arti
xcframework has no x86_64 simulator slice) so iOS-conditional code
paths are compile-checked - SPM tests only cover the macOS slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vendored arti.xcframework rebuilt from source (Rust 1.96.0, normalized
archive metadata for reproducible hashes). New ARTI-BINARY-PROVENANCE.md
records toolchain, rebuild steps, and a SHA256 manifest for every file
in the xcframework. A new CI workflow turns that policy into a gate:
PRs must keep the binary matching the manifest, and binary changes must
ship with source/lockfile/build-script evidence.
Also raises TorManager.awaitReady's default timeout from 25s to 75s to
match the bootstrap monitor deadline - a shorter wait reported "not
ready" while Arti was still legitimately bootstrapping, silently
stranding queued relay work.
Privacy policy, Tor integration doc, and privacy assessment updated to
match the current implementation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* SPM Test target + Github Action to build and test
Because the tests are XCTests `swift test` runs them first and then runs another batch of empty tests which results in "0 tests" at the end of the report - https://github.com/swiftlang/swift-package-manager/issues/8529#issuecomment-2815711345
* Fix dependency and library issues + handle mixed languages
`include` folder has to be next to the `*.c` file for it to work