Add Android multi-ABI build (arm64-v8a, armeabi-v7a, x86_64) with
cargo-ndk. Add Windows and macOS cargo-check jobs. Add xcodebuild
archive verification to iOS release build. Make clippy a blocking
PR gate by removing continue-on-error.
* build: add silero-coreml as git submodule
Replaces sibling-directory local package with in-repo submodule.
Updates Xcode relative paths and CI checkout to fetch submodules.
* build: add silero-coreml submodule
Constraint: SRS-118 and SRS-119 require a Linux release package and an Android release AAB, and the current workspace also needs the sibling oboe-rs checkout for Cargo manifest loading.
Rejected: Keep release packaging as ad-hoc local knowledge | CI and contributors would still miss the required artifacts and hit the missing oboe-rs prerequisite.
Confidence: medium
Scope-risk: moderate
Directive: If the oboe-rs fork path changes or is vendored, update the helper scripts and workflow checkout steps together.
Tested: bash -n tools/build-linux-deb.sh tools/build-android-aab.sh; python3 YAML parse for .github/workflows/ci.yml, .github/workflows/bench-advisory.yml, .github/workflows/bench-baseline-update.yml; git diff --check
Not-tested: End-to-end flutter build linux --release; end-to-end flutter build appbundle --release; GitHub Actions runtime execution
Closes engineering deliverables 1–3 from the open-work table in
`docs/governance/legal-review-readiness.md` so the DEC-012 legal
review can actually run. With this commit, the only remaining
engineering item blocking sign-off is signed Windows / macOS / iOS
build artefacts, deferrable per the DEC-002 staged release plan.
Tooling
-------
* `about.toml` + `about.hbs` + `about-md.hbs` configure cargo-about
with the DEC-020 license posture and the five-target matrix
(Linux, Android, Windows, macOS, iOS). One per-crate clarification
for `allo-isolate` (`flutter_rust_bridge` transitive that ships
Apache-2.0 via `license-file` rather than an SPDX `license`
field). `cargo about generate` runs with zero warnings.
* `deny.toml` mirrors the cargo-about allow-list and adds minimal
bans / sources / advisories config. `cargo deny check` reports
`advisories ok, bans ok, licenses ok, sources ok` for the
workspace; multiple-versions of `windows_x86_64_msvc` produce
advisory `warn` (no fail) because three windows-targets versions
reach the graph via `jni`, `cpal`, and `keyring` respectively.
* `tools/dump_flutter_licenses.sh` + `tools/dump_flutter_licenses.dart`
walk `apps/chanora_flutter/pubspec.lock`, resolve each dependency
to its local pub-cache directory, read the LICENSE file, and emit
`docs/security/flutter-license-inventory.md`. SDK-sourced
packages (`flutter`, `flutter_localizations`, `flutter_test`,
`flutter_web_plugins`, `sky_engine`) resolve to the Flutter
framework BSD-3-Clause LICENSE under `$FLUTTER_ROOT` (or
`$HOME/sdks/flutter`).
Artefacts
---------
* `docs/security/license-inventory.md` — 364 transitive Rust
crates with full license texts. Apache-2.0 (276), MIT (55),
Unicode-3.0 (19), BSD-3-Clause (7), ISC (7). Zero copyleft.
* `docs/security/license-inventory.html` — same data rendered as
styled HTML for reviewer convenience.
* `docs/security/flutter-license-inventory.md` — 94 Dart / Flutter
packages with their LICENSE texts. Zero packages without a
resolvable LICENSE in this RC.
CI
--
* New `supply-chain` job runs `cargo deny check --workspace
--all-features` via `EmbarkStudios/cargo-deny-action@v2`. Fails
the build on any GPL / LGPL / AGPL / commercial-source license
surfacing transitively.
* New `license-inventory` job installs `cargo-about --features cli`
and regenerates `docs/security/license-inventory.md`; diffs
against the committed copy and fails on drift. Forces
contributors who touch the Cargo.lock to refresh the inventory.
* New `flutter-license-inventory` job runs
`tools/dump_flutter_licenses.sh` against the just-resolved pub
cache; same diff-on-drift semantics.
Governance
----------
* `docs/governance/legal-review-readiness.md` §5 cross-links the
three new artefacts in a "Reviewer artefacts" subsection.
* The open-work table at the bottom of the doc is rewritten as a
status grid: items 1–3 now read **Done**; item 4 (signed iOS /
macOS builds) remains the only open engineering blocker, with a
pointer back to `staged-release-plan.md`.
Verification
------------
* `CHANORA_DISABLE_KEYRING=1 cargo test --workspace`: all 49 unit
+ integration tests green (unchanged from v1.0.0-rc.1).
* `cargo deny check`: advisories ok, bans ok, licenses ok,
sources ok.
* `cargo about generate --output-file …`: zero warnings.
* `tools/dump_flutter_licenses.sh`: 94 packages, 0 without LICENSE.
* `flutter analyze`: clean.
No code changes touch the runtime; this is governance-tooling only.
Closes the v0.4 dual-file weakness in identity-at-rest and turns the
release into an MVP public release candidate. The remaining work
before `v1.0.0` is DEC-012 legal sign-off — see
`docs/governance/legal-review-readiness.md` — and the staged
platform promotions in `docs/governance/staged-release-plan.md`.
No decision rows in `product-decision-register.md` change; the
register's change-history advances to 0.9.8.
`chanora_storage`
-----------------
* New public `Crypto` trait + `IdentityFileStore::crypto()` give
callers an encrypt / decrypt pair anchored on the per-install
32-byte DEK without exposing the key material.
* `IdentityFileStore` keyring-first DEK retrieval (Linux Secret
Service via D-Bus, macOS Keychain, Windows Credential Manager,
iOS Keychain via the `keyring` crate). Pre-existing
`identity.dek` files are opportunistically migrated into the
keyring on first run; the on-disk DEK copy is removed once the
keyring acknowledges. `CHANORA_DISABLE_KEYRING=1` forces the
file-fallback path for tests and headless / CI hosts where a
real keyring call would prompt the user or block on a missing
D-Bus session.
* `BookmarkRepository::with_crypto(dir, crypto)` encrypts the
server password into a new `password_blob` BLOB column under
the same per-install DEK. Schema v2 migration is idempotent —
legacy v0.4 rows with a plain `password TEXT` are read
transparently and lifted into `password_blob` on the next
`update()`. `BookmarkRepository::new` (no crypto) is preserved
for tests and as a documented fallback when the DEK is
unreachable.
* Storage tests rise from 8 to 10: encrypted bookmark password
round-trip + legacy-plaintext-bookmark upgrade.
`chanora_core`
--------------
* `ChanoraSession::init_storage(dir)` wires the bookmark
repository with crypto by default. On any crypto-derivation
failure it falls back to the plain-password repository and
logs the gap — better than hard-failing init.
* `supervisor_loop` now tracks a 64-bit `snapshot_signature` over
channels (id + parent + order + name) and clients (id + channel
+ name) instead of the old `(channel_count, client_count)`
tuple. Any in-channel client move, channel rename, or reorder
now fires `SessionEvent::SnapshotChanged`. The signature sorts
by id before hashing so it's stable under input-vector
reordering.
* Two new unit tests cover the signature behaviour; new
`tests/mvp_storage.rs` integration test drives
`ChanoraSession::init_storage` end-to-end and verifies the
bookmark `password_blob` does not contain the plaintext.
* Re-export `ChannelId` + `ClientId` from `chanora_protocol` so
downstream callers and tests can construct DTOs directly.
Flutter
-------
* New About dialog (info icon in the AppBar) surfaces DEC-018
(public name "Chanora"), DEC-019 (non-affiliation statement),
and DEC-020 (Apache-2.0 OR MIT dual license). New ARB keys in
`app_en.arb` and `app_zh.arb`: `aboutAction`, `aboutVersion`,
`aboutNonAffiliation`, `aboutLicenseHeading`, `aboutLicenseBody`,
`aboutThirdPartyHeading`, `aboutThirdPartyBody`.
* `pubspec.yaml` version bumps to `1.0.0-rc.1+5`.
Governance
----------
* `docs/governance/legal-review-readiness.md` — DEC-012 handoff
package. Enumerates trademark / non-affiliation / license-text
/ third-party-attribution / `tsclientlib`-posture / crypto-
export / data-handling items the legal reviewer must confirm,
and lists the concrete engineering deliverables they block on
(`cargo about generate`, `cargo deny check licenses`,
Flutter `LicenseRegistry` dump).
* `docs/governance/staged-release-plan.md` — DEC-002 channel
schedule. Linux + Android sideload promote to GA on DEC-012
sign-off; Play Store / Windows / macOS / iOS gate on per-
platform signed-build availability. Rollback policy included.
* `product-decision-register.md` change-history advances to
0.9.8 with a single entry summarising v0.3, v0.4, and v1.0-rc.1
progress against DEC-001. No decision rows mutate.
Build + ops
-----------
* `NOTICE` refreshed for the MVP product-code dependency set:
adds `chacha20poly1305`, `rand`, `zeroize`, `base64`,
`keyring`, `connectivity_plus`, `path_provider`,
`freezed_annotation`; drops PoC-only entries.
* `CHANGELOG.md` restructured: explicit version sections for
v0.3.0-beta.1, v0.4.0-beta.2, v1.0.0-rc.1. Previous "Unreleased"
contents migrated into their respective milestone sections.
* `.github/workflows/ci.yml` exports `CHANORA_DISABLE_KEYRING=1`
for the cargo-test job — CI runners have no D-Bus session and
the keyring crate would otherwise block.
* `run-chanora.sh` reads `CHANORA_BUNDLE_FLAVOUR` (default
`release`) and self-copies the latest cdylib into the bundle's
`lib/` if missing.
Verification
------------
* `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`: all
green (49 unit tests across the workspace; up from 36 at
v0.4.0-beta.2).
* `cargo test -p chanora_core --release -- --ignored alpha_smoke`
passes against the live `cn.teamspeak.app` (DNS → connect →
snapshot → disconnect in ~2.5 s).
* `flutter analyze`: clean.
* `cargo build -p chanora_bridge --release` + `flutter build
linux --release` produce a working Linux x86_64 bundle.
No Android live test in this commit per the user's note that the
physical device was removed; the Android arm64-v8a build path is
mechanically identical to v0.4.0-beta.2.
The v0.3 client could only ever connect to a hardcoded default
channel with no password and offered no controls mid-call.
External Beta closes those gaps and tightens identity-at-rest.
User-facing additions
---------------------
* **Server password** on the connect form. Plumbed through
`BridgeError`-aware `connect(host, nickname, password)`. Empty
string means "no password" — no behaviour change for open
servers.
* **Channel join**: tapping a row (or its login icon) in the
channel tree issues a `client_move`. Names containing "🔒" or
"password" prompt for a channel password first.
* **Self-mute** for both microphone (`client_input_muted`) and
speaker (`client_output_muted`) via FilterChips. Output mute
also flips the audio engine's local output-muted flag so
playback silences immediately, before the server acknowledges.
* **Master output gain** slider (0–200%). Plumbed through an
`AtomicU32` (f32 bits) on the engine that the cpal output
callback multiplies into every sample.
* **Bookmarks**: SQLite-backed list with Save / Connect / Delete
actions. Bookmarks persist across app restarts; tapping one
pre-fills the form and dials immediately.
Hardening
---------
* **Encrypted identity at rest** (RISK-PoC-002 closure for the
file-only threat model). ChaCha20-Poly1305 envelope: nonce +
ciphertext written atomically with mode 0600; 32-byte DEK in a
separate `identity.dek` file. Legacy plaintext identity files
are auto-detected, read, and upgraded on the next save. Full OS-
keyring integration is still v0.4 work — documented in the
store's doc comment.
* **Mobile voice-comm routing**: on Android, `AudioEngine::start`
uses JNI to set `AudioManager.setMode(MODE_IN_COMMUNICATION)`
when `cfg.mobile_voice_preset` is true (default). This engages
the device-side AEC/NS pipeline on most Pixel/Moto/Samsung
hardware even though cpal still opens the AAudio default input
preset. Full `setInputPreset(VOICE_COMMUNICATION)` switch is
still RISK-AUDIO-MOBILE-001 (needs cpal upstream or an Oboe
fork).
* **Log noise**: bridge default `EnvFilter` now silences
`tsproto::resend=error` and `tsproto::packet_codec=error` so
the redacted diagnostic export is human-readable. Still
overridable via `RUST_LOG=...`.
Engineering
-----------
* **`chanora_storage`** gains `BookmarkRepository` (rusqlite
bundled) with `add` / `update` / `delete` / `list`. The
identity store now layers on `chacha20poly1305` + `rand` +
`zeroize` for the envelope.
* **`chanora_protocol`** exposes `move_to_channel` and
`set_muted` on `ProtocolClient`, dispatched through the
existing `connection_task` request channel onto tsclientlib's
generated `client.client_move(...)` and
`state.client_update().set_input_muted/set_output_muted(...)`
paths.
* **`chanora_core::ChanoraSession`** wires the bookmark store
next to the identity store inside `init_storage`, and adds
`list_bookmarks` / `add_bookmark` / `update_bookmark` /
`delete_bookmark` / `move_to_channel` / `set_self_muted` /
`set_output_gain`.
* **`chanora_audio::AudioEngine`** carries `output_gain` and
`output_muted` atomics; the output callback consults both. The
Android branch of `start()` engages MODE_IN_COMMUNICATION via
a small JNI helper that reuses the `ndk_context` global set by
the bridge's `android_init` hook.
* **`chanora_bridge::api`** adds `set_input_muted`,
`set_output_muted`, `set_output_gain`, `move_to_channel`,
`list_bookmarks`, `add_bookmark`, `update_bookmark`,
`delete_bookmark`, and the `BridgeBookmark` DTO. FRB v2.12
codegen regenerated.
Tests + CI
----------
* `chanora_storage` test count rises from 3 to 8 — bookmark CRUD
round-trip, missing-row → `NotFound`, encrypted round-trip
(verifies ciphertext is not the plaintext on disk), and the
legacy plaintext upgrade path.
* New `.github/workflows/ci.yml`: `cargo check --workspace`,
`cargo test --workspace --no-fail-fast`, `cargo clippy`
(advisory), `flutter analyze`, and `flutter test` excluding
the live-server `e2e` tag.
Live-verified on Moto G Stylus 5G against cn.teamspeak.app:
saved a bookmark, reconnected via it, joined a non-default
channel via tap, toggled both mutes, slid the volume, and the
redacted diagnostic export confirmed `AudioManager mode set to
MODE_IN_COMMUNICATION`, `client_move sent`, and `client_update
sent` lines.