Platform matrix¶
What is pinned, what was built, and what was actually run. Versión española.
A platform counts as tested only where the acceptance flow ran on that system. Compiling for a target proves the toolchain, not the product; distribution is recorded separately from acceptance. Public beta packages do not establish physical-device acceptance.
Pinned toolchain¶
| Component | Version | Where it is pinned |
|---|---|---|
| Rust toolchain | 1.98.1 | core/rust-toolchain.toml |
| Flutter SDK | 3.44.1 (stable channel) | this document and the CI setup action |
| Dart | 3.12.1 | bundled with the Flutter SDK |
| flutter_rust_bridge | 2.13.0 (runtime and generator) | core/crates/arveil-flutter/Cargo.toml (=2.13.0) |
| Android NDK | 28.2.13676358, minimum API 24 | Android SDK installation |
| Android SDK | compile SDK 37, minimum API 24 | Android SDK installation |
| Xcode | 27.0 | host installation; macOS deployment target 12.0 |
openssl-src |
300.6.1+3.6.3 | core/Cargo.lock |
libsqlite3-sys |
0.38.2 (bundled-sqlcipher-vendored-openssl) |
core/Cargo.lock |
Matrix¶
| Platform | Rust target | Built | Tested | Distributed |
|---|---|---|---|---|
| macOS (Apple silicon) | aarch64-apple-darwin |
yes | yes — acceptance run on the host | beta 6 ZIP/Homebrew (0.1.0+27) |
| Android | aarch64-linux-android, x86_64-linux-android |
yes — application and bridge | Android 15/API 35 arm64 emulation; physical linking reported by the tester on build 24, full physical matrix pending | beta 6 ARM64 APK and internal Play (build 27) |
| iOS | aarch64-apple-ios |
core and application layer only | no | no |
| Linux | — | no | no | no |
| Windows | — | no | no | no |
SQLCipher and its vendored OpenSSL cross-compile for Android without the fallback ADR-009 kept in reserve: the built objects are elf64-littleaarch64 for both libcrypto and sqlite3.
Public beta 6 uses source e4011f3f2781aa48888d7da35114aa475d6ff71e. Beta readiness records hashes, signatures and channels. Source/debug checks on other revisions do not automatically certify these packages.
Protecting a profile, and what recovers what¶
Three things are deliberately kept apart, because conflating them is how a promise becomes false:
| Protects | Recovers | |
|---|---|---|
| Profile key | the local database at rest | nothing. Losing it loses local history, and that is accepted on purpose |
| Identity kit | the identity's root, exported by the user | the identity. Not conversations, not MLS group state |
| History export | an explicit encrypted archive the user asks for | read-only messages and available attachments for the same identity, including after identity recovery into a new profile with a new local key. It does not recover MLS sessions |
The profile key is 32 random bytes from the operating system's generator, made in Rust with the same call the rest of the client uses, and handed to the platform store. It is never derived from anything a person types and is not synchronized by Arveil. History export uses its own key. On macOS, a manual backup/migration of the classic login Keychain is outside the application’s control; do not claim that this backend is device-bound.
Backups are refused rather than trusted:
- Android —
android:allowBackup="false",fullBackupContent="false"and adata-extraction-rulesfile that excludes every domain from both cloud backup and device transfer. A transfer is as much a copy as a backup. The stored key is not carried into a backup either. - Apple — the profile directory is marked
isExcludedFromBackupon every start, since an attribute set once does not survive a directory being replaced. iOS uses the device-bound Data Protection Keychain. macOS uses the classic login Keychain with app access control; neither backend requests synchronization. Apple TN3137 describes the different protection models.
Exclusion is not encryption and does not replace it. It keeps an encrypted database out of an account whose protection this project does not control.
Where the key store stands¶
| Situation | How it is checked | State |
|---|---|---|
| First install: no key, no profile | integration_test/profile_key_test.dart |
verified on the Android emulator |
| Second start: the key comes back | same test | verified on the Android emulator |
| Key gone, profile present | same test | verified: reported, never replaced silently |
| A different key on an existing profile | same test | verified: refused at open |
| Reinstall | manual: uninstall, install, start | not done. An uninstall does not necessarily take secure-store entries with it, and the two platforms differ; this must be observed, not assumed |
| Restore from cloud backup or device transfer | manual, on hardware | not done |
| macOS login Keychain | profile_key_test.dart with ARVEIL_REQUIRE_SECURE_STORAGE=true, ad-hoc build |
verified on the local Apple silicon Mac: write/read, encrypted reopen, missing/wrong-key rejection; package upgrade verified below, fresh download still pending |
Reproducing it¶
The Rust workspace, on the host:
cargo fmt --all --manifest-path core/Cargo.toml -- --check
cargo clippy --manifest-path core/Cargo.toml --workspace --all-targets --locked -- -D warnings
cargo test --manifest-path core/Cargo.toml --workspace --locked
Cross-compiling the application layer for Android, with the NDK toolchain named explicitly:
NDK=$HOME/Library/Android/sdk/ndk/28.2.13676358
BIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin
ANDROID_NDK_ROOT=$NDK PATH="$BIN:$PATH" \
CC_aarch64_linux_android=$BIN/aarch64-linux-android24-clang \
AR_aarch64_linux_android=$BIN/llvm-ar \
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$BIN/aarch64-linux-android24-clang \
cargo build --manifest-path core/Cargo.toml -p arveil-app --target aarch64-linux-android --locked
Regenerating the bindings, from core/crates/arveil-flutter:
flutter_rust_bridge_codegen generate
The client, from clients/flutter:
flutter analyze
flutter test integration_test/profile_test.dart -d macos
flutter build apk --debug --target-platform android-arm64
What the acceptance run covers¶
integration_test/profile_test.dart runs on the device itself: a profile opens with an explicit 64-hexadecimal-character key, the conversation query answers with an empty list and a malformed history identifier returns a typed Domain error, a second independent open is refused with AlreadyOpen, a malformed key is refused with BadKey before anything is created, and after close the same directory opens again while a wrong key fails at open with Unusable.
Record every run against a commit, an operating system and a device. A run on an emulator is written down as an emulator run: it exercises the same binaries, not the same hardware, and M3b.5 still owes a physical device.
Invitation UI acceptance (September 15, 2026)¶
The invitation-enrollment changes are recorded in the commit accompanying
this document. integration_test/onboarding_test.dart passed on an Android
15/API 35 arm64 emulator against an ARM64 Podman staging relay. It exercises
the form, real platform key storage, native Rust/SQLCipher, unreachable
endpoint, close/reopen, same-identity retry and completed-profile reopen.
This does not cover a physical phone, an OS restart, app reinstall or cloud
restore. Pairing and recovery-kit were outside that acceptance run. Reproduction and
private fixture handling are in the Flutter README.
Experimental package acceptance (September 15, 2026)¶
Client packages 0.1.0+2 were built from clean commit
a1d954c7a13ae9a2f189ba19e49d2f52e8fd5b19. Each includes revision metadata and
checksums; ZIP/APK signatures, architecture and decompressed-content privacy
checks passed. The macOS ZIP is ad-hoc signed, with no Developer ID/notarization.
The Android APK uses a persistent private release key.
- macOS Apple silicon, Xcode 27: native login-Keychain acceptance passed with unavailable storage treated as failure. The packaged app opened its encrypted profile, reopened it after quitting, and opened the same profile after replacing build 1 with build 2 at the same location. Keychain may request authorization for a rebuilt app. A fresh download on another Mac is still unverified.
- Android 15/API 35 ARM64 emulator: the release APK installed and cold-started. The separate native upgrade harness creates a test identity with a platform key and verifies that identity after APK replacement; it never reads or replaces the normal app profile. Both create/reopen signals passed across build 1 → production APK 2 → verifier 2, without uninstalling or clearing data. Physical-phone acceptance is still pending.
These are local experimental results, not beta acceptance or a production security review. The packaging guide explains reproduction and the installation guide explains end-user installation.
Pairing and recovery acceptance (September 23, 2026)¶
integration_test/pairing_recovery_test.dart passed natively on Apple silicon
macOS with Xcode 27 and the classic login Keychain, and on a disposable
Android 15/API 35 ARM64 emulator with Keystore, against a disposable local
relay. Three encrypted profiles exercise enrollment, pairing, wrong comparison,
reopen before confirmation, encrypted kit export, wrong recovery key, restore,
repeat, reopen and empty recovered conversation history. The current source
is client 0.1.0+3; this run does not rebuild or publish the 0.1.0+2 alpha.
The 16 widget/unit tests cover presentation using a file-dialog substitute. Native save/open/cancel dialog interaction, cross-device Mac–Android pairing, a physical phone and a fresh downloaded installation remain outside this run.
KeyPackage GUI acceptance (September 23, 2026)¶
The 0.1.0+4 source accompanying this record passed
integration_test/key_packages_test.dart on Apple silicon macOS with Xcode 27
and the login Keychain, and on a disposable Android 15/API 35 ARM64 emulator
with Keystore, against a disposable local relay. The real GUI checks
exhaustion, replenishes and reopens the persisted count and timestamp. The
fixture marks the initial five packages consumed in its own relay database;
the final inventory is 15 total, five consumed, ten available. Phase 4
separately verifies actual MLS group consumption and CLI replenishment.
The Flutter README
provides a self-contained helper. Neither this run nor widget tests cover
physical hardware, cross-device pairing or native file-dialog interaction.
Conversation GUI acceptance (September 23, 2026)¶
The 0.1.0+5 source accompanying this record passed
integration_test/conversations_test.dart natively on Apple silicon macOS
with Xcode 27 and the login Keychain, and on a disposable Android 15/API 35
ARM64 emulator with Keystore. The isolated helper creates two encrypted
profiles and drives verified group creation, duplex text, relay shutdown,
offline acceptance, profile reopen, pagination and reconnect with 55 events
and no duplicates. It also checks cancellation before watcher dispatch.
The peer runs through the native bridge inside the test app. Text input is
injected by Flutter's test framework. This is not Mac–Android cross-device,
physical-phone, native keyboard/IME or clean-download acceptance. The earlier
0.1.0+2 experimental packages are unchanged. Reproduce with the
Flutter README.
Cross-platform package acceptance (September 24, 2026)¶
Normal release packages 0.1.0+5, built from clean commit
4d4362b5f27d3782d992254fc494cd9967f37417, passed a conversation test between
two separate apps: macOS 26.6.2 on Apple silicon (Xcode 27.0, build 27A266a)
and an Android 15/API 35 ARM64 emulator (Pixel 7 device definition).
Both used an ARM64 Podman staging relay over a private network. This run used
the packaged lib/main.dart applications, not integration-test entry points.
| Package | SHA-256 |
|---|---|
arveil-0.1.0-5-macos-arm64.zip |
5ac26a37b64c82c7d2a58427739de4e4a5056bf29b9215babd984a5a36997ae9 |
arveil-0.1.0-5-android-arm64.apk |
c275634f32f91d7fa25c35603ca52493d232149c1de0aaafb43ec2f3f9bc17e3 |
The packages retain ad-hoc macOS signing (no Developer ID/notarization) and
the persistent Android release certificate used for build 2. Both revision
metadata files report dirty_source: false. Signature, architecture, checksum
and decompressed-content privacy checks passed. These candidates are not a
published release; the previous 0.1.0+2 packages are unchanged.
Procedure and observed results¶
Use disposable test identities and a reachable relay. Keep invitations, connection data, contact routes and raw UI diagnostics private.
- Enroll a separate identity through each build 2 app's invitation form. Replace the Mac app at the same location and install APK 5 over APK 2, without uninstalling or clearing data. Both apps reopened their enrolled profiles after the upgrade. macOS requested login-Keychain access for the updated app; the user authorized it locally. A later restart required no additional prompt.
- Exchange contact routes through the GUI. Compare both displayed safety numbers before confirming, then create one conversation on the Mac. The numbers matched and both apps opened the same conversation.
- Send one text from each app. Both appeared on the peer. Enable airplane mode and disable Wi-Fi/mobile data only on the emulator. Send another Android text: it remained in local history with synchronization pending. Send another Mac text while Android is offline.
- Stop and reopen the Android process while it is still offline. Its profile, earlier history and pending outgoing text survived; the new Mac text had not arrived. Restore connectivity and synchronize. Both apps then showed exactly four messages. Two further completed Android syncs created no duplicates and left no pending banner.
- Quit and reopen the Mac app. The profile and all four messages remained accessible. Native Mac keyboard entry/paste worked. Tapping the Android on-screen keyboard entered and deleted a draft without sending it.
This verifies separate-app Mac ↔ Android-emulator messaging, profile retention across upgrade, offline persistence and reconnect delivery. It does not verify a physical phone, cross-device pairing, OS reboot/Doze, broad keyboard/IME coverage, native kit save/open/cancel dialogs, or Gatekeeper on a fresh download on another Mac. Those checks remain open; this is not beta acceptance or an external security review.
Saved-contact acceptance (September 24, 2026)¶
Client source 0.1.0+6 passed the native conversation integration test on:
| Platform | Source commit | Runner |
|---|---|---|
| macOS 26.6.2 Apple silicon, Xcode 27.0, login Keychain | b176ab4f55a90f8e3b6e273081f8244bb945afd6 |
flutter test |
| Android 15/API 35 ARM64 emulator, Keystore | 77206b85b4c24ff3b6abb7a652d28cba1388c23f |
flutter drive --no-dds |
The Android follow-up changes the host test runner and its documentation; the client implementation and test scenario are unchanged. The isolated helper creates a disposable local relay and two encrypted profiles. It checks saving an unverified contact, explicit safety number verification, encrypted profile reopen, conversation creation from a saved contact without pasting its route again, and renaming the contact with the updated alias shown in the conversation. Duplex text, offline queue persistence, pagination across 55 events and reconnection without duplicates also passed on both platforms.
The peer runs through the native bridge inside the same integration app;
Flutter injects text through its test channel. This record covers source
acceptance, not separate-app release-package, native keyboard/IME,
physical-phone or fresh-download acceptance for build 6. The existing
0.1.0+5 package candidates and release draft are unchanged. Native kit
file-dialog acceptance remains open. Reproduction commands and local
Android test-tool workarounds are in the
Flutter README.
Attachment GUI acceptance (September 25, 2026)¶
Source client 0.1.0+7 passed these native checks:
| Platform and scenario | Client implementation revision |
|---|---|
| macOS 26.6.2 Apple silicon, Xcode 27.0, login Keychain: attachment transfers | 234406740fac64852d7583f77530305638987100 |
| Android 15/API 35 ARM64 emulator, Keystore: attachment transfers | b943594b222c09a5c495b5c6c8fac3ab0f5303f7 |
| Same Android emulator: real attachment open/save dialogs | 6771842c387854f06abbebf5fe443e81c2403911 |
The transfer scenario uses two encrypted profiles, native Rust/SQLCipher and an isolated local relay. It verifies confirmation, offline queueing, encrypted reopen, upload resume without another event, opt-in download, verified export, two files with the same name, cancellation and repeated sync without duplicates. Selectors in that scenario are in-memory substitutes; it writes no plaintext attachment download. The Android harness waits for transfer completion instead of assuming that a settled frame means native I/O has finished.
The separate interactive Android test uses the real OS dialogs and a synthetic
37-byte document, without any profile, relay or identity kit. Selection,
cancel-open, cancel-save and explicit save passed. The exported file matched
the source bytes exactly, and no file_picker plaintext cache was created.
The two disposable documents were removed afterwards. Android reads the source
URI directly with a bounded stream; three JVM regressions cover empty/exact-limit
input, oversized unknown-length input and cancellation before reading.
All 43 Flutter tests and 99 Rust tests passed (one Rust test remains ignored),
as did static analysis and privacy checks. Reproduction commands are in the
Flutter README.
This is source acceptance: macOS native attachment dialogs, physical Android,
fresh-download acceptance and attachments between separate release apps remain
unverified. The 0.1.0+5 candidate packages and release draft are unchanged.
Device management acceptance (September 25, 2026)¶
Source client 0.1.0+8, implementation commit
a17ba9d392616e74f92f039c96fcda56c1e54ef8, passed the native devices scenario on:
| Platform | Storage and transport |
|---|---|
| macOS 26.6.2 Apple silicon, Xcode 27.0 | Login Keychain, SQLCipher, disposable local relay |
| Android 15/API 35 ARM64 emulator | Keystore, SQLCipher, disposable local relay |
Three disposable profiles exercise administrator/linked-device inventory, partial-inventory warnings, pairing, explicit confirmation and cancellation, offline revocation, encrypted reopen and resumption through the UI. The relay then refuses the revoked device's handshake; the administrator removes its MLS leaf and exchanges text with a remaining participant. Repeated sync preserves the manifest version and produces no duplicate text. No identity kit or history archive is exported. Fixtures and the emulator are cleaned up afterwards.
All 102 Rust tests and 47 Flutter tests passed (one Rust test remains ignored), as did Clippy, Flutter analysis, CLI phase 2 acceptance, strict documentation and publication-hygiene checks. Rust additionally simulates lost manifest and envelope ACKs, failed local transactions and a newer link while revocation is pending. Reproduction commands are in the Flutter README.
This is source acceptance with multiple profiles inside each native test app;
it does not establish physical-phone or separate release-app acceptance. The
existing 0.1.0+5 installer candidates and release draft remain unchanged.
After that native run, the stale-link-request guard and atomic, repeatable relay publication were verified by 103 Rust tests, the Go suite and phase 2 acceptance. The real-relay scenario clears only the disposable client publication receipt to simulate a lost ACK, then confirms retry success without a new manifest. These follow-up checks require the updated relay; native revision above is kept explicit rather than claiming another native run.
History and loss recovery acceptance (September 25, 2026)¶
Source 0.1.0+9 passed the native archives scenario on macOS 26.6.2 Apple
silicon / Xcode 27.0 and the Android 15 / API 35 ARM64 emulator. Each app used
three disposable profiles, SQLCipher, its native Keychain/Keystore and a local
relay. The scenario exported text and an available attachment, deleted the source
profile and its local key, restored the identity using its test kit, and imported
history under a newly generated profile key through the GUI.
The imported attachment matched the original bytes. Repeated import and reopen preserved exactly two records. Sync produced neither duplicate messages nor an old MLS group; a new conversation was explicitly created with the recovered route and successfully exchanged text. Kits, history archives and attachment exports stayed in memory; selectors were test substitutes, so this does not verify the native archive dialogs. No real profile or identity kit was exported.
The final bounded-parser, transactional-snapshot and shared attachment-reader changes have Rust regression coverage; the shared-reader refactor followed the native runs and additionally checks CLI configuration on a GUI-created profile. The suite has 108 passing Rust tests (one ignored), 51 Flutter tests, Clippy, Flutter analysis, strict documentation and the phase-2 CLI recovery acceptance. Reproduction commands are in the Flutter README.
Physical Android, native archive dialogs, clean installation and this recovery
flow between separate release apps remain unverified. Existing 0.1.0+5
installer candidates and the release draft have not been replaced.
Corrected package and archive-dialog acceptance (September 25, 2026)¶
Normal release packages 0.1.0+10 were built from clean commit
8b4f5ef06b810ce07adc82e69bd0e6e28d86c5c6. Both passed signature,
architecture, checksum and decompressed-content privacy checks. The macOS ZIP
is ad-hoc signed; the Android APK retains the certificate from builds 2 and 5.
These are unpublished candidates for the clients-v0.1.0-alpha.3 draft.
| Package | SHA-256 |
|---|---|
arveil-0.1.0-10-macos-arm64.zip |
80e8f93f11449c2e160030f850d96914287089cff0a57d32b28f3e2d31fcfc3d |
arveil-0.1.0-10-android-arm64.apk |
33f0accdc43ebe7c54724f27ee403fc72535ba1bb262999b5ecc7dce8fbcd030 |
The Android 15/API 35 ARM64 emulator used the normal installed application, OS keyboard and native document selectors, with its existing disposable profile. No integration-test entry point, uninstall or data clearing was used.
- Upgrade from APK 5 to 9 retained enrollment and all five fixture messages. One new test message brought the live conversation to six. APK 10 then replaced APK 9 using the same signing certificate and retained that profile.
- Build 9 exposed a real native-selector bug: saving finished before Flutter resumed, so the app discarded the archive key. That candidate is not for distribution. Build 10 keeps this result hidden until an explicit foreground reveal; another loss of focus after returning discards hidden/visible keys. Identity-kit export shares the correction and has widget regression coverage.
- In APK 10, cancelling native save revealed no key. Saving a new archive reported six records; Mostrar clave del archivo guardado displayed its key. Leaving and returning to the app cleared it without redisplaying it.
- Cancelling native open selected nothing. Opening the saved archive and entering its key imported six read-only records. Repeating the import reported zero new records and six existing records. The live conversation still contained six messages; import did not resend them. All six imported texts remained readable after stopping and restarting the app process.
The Flutter suite has 55 passing tests; analysis, formatting and strict bilingual documentation checks pass. Test archives, keys and raw UI data stay private. The macOS ZIP launched on macOS 26.6.2 Apple silicon / Xcode 27.0, but profile reopening, native dialogs and a Mac–Android messaging rerun remain pending for build 10. Physical Android, clean-download acceptance, native kit selectors and recovery between separate release apps also remain unverified. The earlier alpha drafts are unchanged.
Redesign accessibility (September 25, 2026)¶
Automated, in test/accessibility_test.dart: spoken labels for messages
("Lucía, 18:40: …" and, for what was sent, its state), the main phone screens
with touch targets of at least 48 dp and a label on everything tappable, the
system's reduced-motion setting, and the main screens at 200 % text without
overflow.
Pending, manual: walk the main flows with TalkBack on a physical Android device and with VoiceOver on macOS, and record the result here with the device, system version and commit.
Redesign candidates (September 25, 2026)¶
scripts/package_clients.py built and audited locally, without publishing,
the 0.1.0+13 candidates for macOS (arm64, ad-hoc signing) and Android
(arm64, private release key, minimum SDK 24) from the clean commit 4d5d2c6
of the redesign branch (E3). Both passed the script's checks: architecture,
signing, and no private paths, test markers or keys inside the package. The
app's diagnostic report carries version 0.1.0+13 and that commit.
Update: the profile scenario with the CLI built from 8b4f5ef, the exact
revision of the 0.1.0+10 packages, against the redesign's code keeps
identity, conversations, history and read marks, and refuses a profile from a
future schema unchanged. Repeating it at package level (install 0.1.0+10,
fill it from the app, install the candidate) on macOS and the Android
emulator is still pending, following client packages.
Package upgrade and TalkBack review (September 26, 2026)¶
0.1.0+14 candidates for macOS and Android built and audited locally,
without publishing them, from main at 0fa6f6f, with the redesign stack
already merged.
Upgrade from 0.1.0+10 on Android. A new AVD (Android 15, google_apis
arm64, emulator) and a disposable local relay were used. The 0.1.0+10 app
was installed from its APK, enrolled with an invitation, saved and verified a
test contact (a CLI profile), created the conversation and exchanged two
messages. Then adb install -r installed 0.1.0+14, signed with the same
key (versionCode 10 → 14). Result:
- The profile opens with its identity, the contact is still verified and the conversation keeps its history.
- What arrived before the update is read. A message that arrives after it shows as unread, becomes read when opened and stays so after a restart.
- A message sent after the update reaches the contact through the same MLS group.
- What was received with
0.1.0+10has no author, because that version did not store it. It shows without a name and reads as "A contact", as the design asks. - On a device set to English the app starts in English;
0.1.0+10was Spanish only.
Upgrade from 0.1.0+10 on macOS (macOS 26.6, arm64). With the real
profile moved aside and the same relay, the 0.1.0+10 app created a new
profile, enrolled, saved and verified the test contact, created the
conversation and exchanged two messages. It was then quit and the 0.1.0+14
app opened. The keychain asked for permission for the new ad-hoc signature,
as it does for every unnotarized build, and it was granted by hand. Result:
- The profile opens with its identity, the verified contact and the full history.
- What arrived before the update is read. A message that arrives after it shows as unread and becomes read when the conversation is opened.
- A message sent from
0.1.0+14reaches the contact through the same group. - The app follows the system's language and theme: English and dark on this Mac.
Afterwards the real profile went back in place unchanged.
TalkBack. TalkBack 15.0 was used on the same emulator, not on a physical Android device. Gestures were sent as real touches through the emulator console, and what was spoken was read from TalkBack's verbose log. The walk covered the welcome screen, opening the profile, the chat list, a conversation (reading, typing and sending), contacts, settings and appearance. The order is logical and every element is announced with its name and role. Bubbles read as "Bob, 08:28: …", tabs as "Chats, Tab 1 of 3", and appearance announces its headings and the chosen option. Findings:
- A settings group with a single tappable row, such as "Connection", read as one tappable heading. Fixed in #100.
- The text size control said "100%, 100%" without naming what it sizes. Fixed in #100.
- After leaving with Back from the first screen and opening the app again
in the same process, the profile said it was open in another session.
This already happened with
0.1.0+10. Fixed in #99, checked with a local0.1.0+15candidate: it failed 2 times out of 2 with0.1.0+14and none out of 3 with the fix. - Fields with
enableSuggestions: false, the composer among them, make Flutter ask Android for a visible password. Gboard then shows its password keyboard, TalkBack announces it and voice typing may disappear.enableIMEPersonalizedLearning: falsealready keeps the keyboard from learning. Whether the composer gets suggestions back is still to be decided. - A message that arrives while the conversation is open is not announced. The design does not ask for it; it is noted.
- Opening a conversation puts focus on the first list item ("Today") rather than the header. This is minor.
Later candidates: 0.1.0+15 (Android, with #99 only) and 0.1.0+16
(macOS, from main with #100) were built locally to check the fixes.
0.1.0+16 opened the upgraded profile with the conversation still read after
a restart.
VoiceOver: compatibility untested. The accessibility tree and labels are the ones TalkBack reads, but the app could not be walked with VoiceOver on macOS in this session. The tool driving the Mac could not read the caption panel or take window focus without freezing the app the test was run from. It stays pending, together with TalkBack on a physical Android device.
Read-state corrections acceptance (September 26, 2026)¶
Source e8a5c01 on codex/client-read-state-beta corrects unread messages
being cleared behind another screen and the stale kit summary after device
revocation. Local verification on macOS 26.6.2, Apple silicon, Xcode 27.0:
flutter analyze: no issues; 172 Flutter tests pass, including the screen goldens. New regressions cover hidden destinations, search, a covering route, delayed history, app inactivity and return to the conversation; kit state updates after successful/failed revocation and after leaving its screen.- The native conversation helper passes using two temporary encrypted profiles, platform-held keys and its own loopback relay. It exercises the current full application, saved/verified contacts, rename, profile reopen, an actual Rust unread count of one while Settings hides a reply and zero once the history is displayed, duplex text, offline queueing, pagination and reconnect without duplicates. The previous helper still looked for controls removed by the redesign; this run uses the current navigation and verification button.
- Strict bilingual documentation, the eight packaging/publication script tests and the Gitleaks 8.30.1 branch-history scan pass.
The local 0.1.0+17 macOS ZIP and Android APK were built from that exact clean
commit and pass the packaging audit and SHA-256 manifest checks. Android retains
the build 14 signing certificate (both actual APK signatures were verified).
The macOS release app launches and shows the welcome screen with accessible
labels; its existing user profile was not opened during this package check.
No package has been published.
| Package | SHA-256 |
|---|---|
arveil-0.1.0-17-macos-arm64.zip |
e234f8a9215cbe9f8c3f8401d81fa1596dc200444ccd78a5f08866aa4e678f3c |
arveil-0.1.0-17-android-arm64.apk |
05f4ce541522345a8e61debda2d782f86e539823a0d11429c7921cfae548f0ab |
This is local source/integration acceptance. It does not close fresh-download
installation, physical Android installation/update and Doze/reconnect,
VoiceOver, TalkBack on a physical device, or the three external-user
evaluations required by M3b.5. The package upgrade from 0.1.0+10 is recorded
in the previous section.
Android signed-updater acceptance — 2026-09-26¶
The private update_installer_acceptance.dart entry point was exercised on a
new disposable Android 15 / API 35 ARM64 emulator. These were debug test APKs,
builds 901 and 902, not distribution artifacts. The first created an actual
SQLCipher profile with its key in Android Keystore. The second was installed
through the app's PackageInstaller session and Android's visible Update
confirmation, without adb install -r, uninstalling or clearing app storage.
After relaunch, ARVEIL_TEST_UPDATER_OK:profile:after confirmed the same stored
identity and retained platform key.
Before accepting the update, the same installation also passed:
- Missing installation permission: refused before creating an installation.
- User cancellation at the Android prompt: returned
cancelled, retaining build 901 and its profile. - A candidate signed with a different disposable certificate: returned
package, removed the candidate and retained build 901. - A same-certificate candidate with a deliberately wrong expected SHA-256: refused and removed before installation.
The automated checks cover signed manifests, an independent OpenSSL signature fixture, expiry, persistent sequence protection, opt-in/daily checking, download integrity, HTTPS redirects, and native package identity/certificate validation. See the update protocol and release procedure. This emulator result does not establish physical-phone, device-policy or background-install behavior. No silent/background installation is implemented; macOS integration and automatic update-key rotation remain pending.
Source: the run above used the updater from 283467b; 049fd0d changed the
update transport afterwards, and the corrections in the same pull request
followed. The final revision was accepted the same day on emulators at API 24,
28, 29 and 35, with ADR-010
criteria 3–5; see the next record.
Android signed-updater acceptance, final revision (September 26, 2026)¶
Source: e7936a2, on disposable ARM64 emulators: AOSP images at Android 7.0
(API 24), 9 (API 28) and 10 (API 29), and a Google APIs image at Android 15
(API 35).
Method. The private update_flow_acceptance.dart entry point runs the real
app with the real update controller, transport, Rust signature check and
PackageInstaller installer. Two profile-mode test APKs, builds 1901 and 1902,
were signed with the same debug key and built with a local HTTPS feed, served
from the host (10.0.2.2), and a disposable update key. They differ from a
release build only in one extra trusted root: the feed's disposable authority.
Everything was driven from Settings → Updates on the real screens; the
update was never installed with adb install -r.
Result on every API level:
- The check verified the signed announcement and offered 0.1.0+1902 (44.0 MiB) with its release notes link. The download followed the feed's redirect and matched size and SHA-256.
- Android asked for confirmation ("Do you want to install an update to this existing application?" on Android 7.0, "Do you want to update this app?" on Android 15). Once accepted, build 1901 became 1902 with no uninstall and no data clearing.
- The new build opened the same encrypted profile with its Keystore key
(
ARVEIL_TEST_UPDATER_OK:profile:after), kept the accepted sequence and removed the installed package from its cache when it started.
Per level:
- API 24: with Unknown sources off, as on phones, the app asked for it and opened Security settings; back in the app, the hint was gone. Leaving Android's prompt with Back returned "Installation cancelled" and kept build 1901.
- API 28, 29 and 35: the app opened Android's per-app Install unknown apps page. API 28 is also the Android 9 case whose package certificates the app now reads with the legacy fallback.
- API 35: dismissing the confirmation by tapping outside it returned "Installation cancelled" without leaving the screen waiting. The release notes link opened in the browser.
Two defects found and fixed in e7936a2:
- GitHub serves release assets under Let's Encrypt, and Android 7.0 does not carry ISRG Root X1. A probe to GitHub's asset host was untrusted with the system's roots on API 24 and reached with the updater's, which add that root; both reached it on API 28, 29 and 35.
- On Android 7 the app took installation as allowed and ignored the global Unknown sources setting, which phones ship off.
ADR-010 criteria 3–5. On API
29, with the emulator's network captured (-tcpdump) and the app's profile
joined to a disposable local relay:
| Window | Relay connections | Feed connections | Anything else |
|---|---|---|---|
| Checks off, 6 minutes of use | 27 | 0 | nothing, not even DNS |
| Automatic check on, then a manual check | 5 | 2 | nothing |
| Relay hostile, then down | 24 | 2 | nothing |
- 3. With checks off, the app joined, synced, opened Settings → Updates without checking, came back to the foreground three times and was restarted; it never contacted the feed or any other address.
- 4. Every request the feed received carried only
HostandAccept-Encoding: identity: no User-Agent, version, query or cookie, though the server set one on every response. Turning automatic checks on sent nothing; the next return to the app made one check, and a second the same day none. - 5. With the relay replaced by one answering garbage, and then with no
relay, the app reported "No connection to your server". Each check still made
exactly one request to the feed and showed the same offer. The hostile relay
received only
GET /v1/channel.
Criteria 1, 2, 6 and 7 are covered by the automated tests and by these runs.
Candidates. 0.1.0+19 for Android, built with the beta update channel, and
for macOS, from e7936a2 with scripts/package_clients.py; built and audited,
not published. The APK has versionCode 19 and requests
REQUEST_INSTALL_PACKAGES because it carries a feed. It is signed with the
same release certificate as +17 and +18, and installed over +18 it opens
and shows the configured channel. The public feed was not yet published
(HTTP 404).
This does not establish physical phones, installers modified by manufacturers, device policies or Play Protect, and the public feed host was not exercised. macOS updates are not implemented.
Beta 6 packages — October 1, 2026¶
Clean source e4011f3, build 27. Extracted Mac ZIP: ad-hoc signature verified, app opened and displayed version 27. Direct APK: maintained certificate, successful installation in Android 15/API 35 arm64 emulation. AAB: upload key accepted and build available in internal Play. Homebrew: download and checksum verified. Compatible relay: backup, unchanged identity and public Noise probe passed.
This does not establish physical camera, WhatsApp installation journey, Play App Links, Gatekeeper on another Mac or suspended notification delivery. Keep those criteria in #134/#135/#140. Invitations are published; complete physical acceptance remains open.