Adds .gitea/workflows/ci.yml matching the worker app and the vegan-IQ convention: ubuntu-latest, the reviewed Flutter revision activated from the Build cache and verified before use, generated output under ~/Build, then resolve, format check, analyze, and test. Running that gate locally found two things the looser local checks missed. Nine files were not dart-formatted, including test support added in the last change. And formatting then reflowed a single-line if in local_auth_gate.dart onto two lines, which trips curly_braces_in_flow_control_structures; braced it. No behaviour change. 84 tests still pass.
Evaluetron Commander
Private, adaptive operator client for an Evaluetron Brain. Commander is a native Flutter application for phones, tablets, and keyboard/mouse desktops; it is not a web admin console wrapped in a WebView.
The application includes a deterministic fixture mode and a live Brain transport. Live pairing creates an Ed25519 device identity, verifies a signed profile request, and stores the private seed through the operating-system secure store.
Product surfaces
- Scoped Brain profile and secure pairing design
- Operational overview
- Model catalog and bounded job submission
- Job monitoring
- Worker fleet visibility and app-based enrollment preview
- Redacted configuration view
- Operator-safe activity history
- Optional, consented managed-Nebula foundation with narrow Brain routing
Security boundary
Pairing exchanges a short-lived enrollment for a revocable, least-privilege
device identity. Secret material is only handled through
the SecureCredentialVault abstraction backed by Keychain, Android Keystore,
or an equivalent operating-system store. It must never be put in
SharedPreferences, ordinary files, logs, analytics, or crash reports.
Several paired Brains
Commander keeps one device identity per Brain origin and can hold more than one. Pairing a second Brain therefore does not discard the first: it stays valid on its own Brain, so it is listed in the Brain profile dialog with the device id it carries, and can be switched to or removed there. Removing a pairing revokes it on that Brain first, then deletes the local seed.
A pairing that the Brain commits but that Commander cannot finish — the signed profile check after it fails — is cleaned up rather than abandoned. The device row exists on the Brain by then and the one-time enrollment is spent, so the credential that is about to be discarded is used to revoke that row first. If even that fails, the error names the device id to revoke from the console.
A removal always asks the Brain to revoke the device first, and never skips
that because it might fail. When the Brain answers, the credential is deleted
either way: a 401 means the console already revoked it, and a changed
certificate means Commander will not talk to that Brain again. When the Brain
cannot be reached at all, nothing is removed and the operator is offered a
clearly labelled local-only removal instead, which states that the device stays
active on the Brain and names the device id to revoke from the console. That
escape hatch is reachable from the profile dialog and from the failure screen,
so a Brain that is down cannot trap a device in a pairing it cannot clear.
Pairing an origin that is already paired is different: the new key replaces the old one, which would strand the device row the old key belonged to, so the replaced identity is revoked on a best-effort basis before it is overwritten. That revocation cannot be allowed to fail the pairing, because pairing again is the documented recovery path after a Brain certificate change — the one case where the old credential can no longer be used to talk to that Brain at all.
What a failed cleanup leaves behind
Every cleanup above is best effort, and best effort means it sometimes fails.
Commander cannot revoke another device — there is no commander:admin scope,
so only the Brain's own console can retire a row, and only if it is told which
id. Reporting that id once in an error the operator dismisses loses it for
good.
So a failed cleanup is written down. The device id, its Brain, and why the cleanup could not finish are stored beside the credentials and listed under Left on a Brain in the Brain profile dialog until the operator clears the entry. Clearing is local only and says so: Commander cannot check whether the row was actually retired, so it does not pretend to.
A stored record that no longer parses is listed too, marked unusable. It cannot be signed with, switched to, or revoked, and it used to be hidden — which also made it unremovable, because nothing else on the device mentioned it. It now offers local removal only, with the same warning that a device may remain live on that Brain.
Pairing an additional Brain still does not offer to revoke the one already held. Holding several is a supported workflow, and a mis-tap during pairing must not destroy access the operator never asked to give up. But keeping it is a live identity on another Brain, so pairing now reports how many Brains this device commands rather than leaving that a silent state.
Local authentication gate
Commander holds operator authority over a whole fleet, so the operating-system
lock screen is not accepted as the only barrier. LocalAuthGate adds a
device user-presence check (biometric, PIN, pattern, or password) at six
points:
| Point | Where it is enforced |
|---|---|
| Activating a stored credential at launch | CommanderBrainClient._initialCredential |
| Returning from the background | CommanderLockGate |
| Submitting a job | CommanderController.submitJob |
| Pairing a Brain | CommanderController.pair |
| Switching to another paired Brain | CommanderController.selectPairedBrain |
| Revoking a Commander pairing | CommanderController.forgetPairing, CommanderController.removePairing |
The step-up checks live in the controller rather than in widgets so no UI path can reach a privileged action without one.
The gate distinguishes two ways a presence proof can be missing, because they have different remedies.
LocalAuthOutcome.notEnrolled means the device could prove presence but the
operator has not set a screen lock or biometric. The gate fails closed and
says so; the remedy is to enrol one.
LocalAuthOutcome.unsupportedPlatform means the platform ships no local_auth
implementation at all — Linux desktop today. No operator action can produce a
proof there, so refusing would not add security: the desktop session login is
the barrier, exactly as it was before this gate existed. It would only make a
supported target unusable. The action proceeds and the shell shows a persistent
banner that the session is unprotected, so the reduced protection is visible
rather than assumed. Treat a Linux Commander as being only as protected as the
desktop login, and prefer a phone for operator authority.
A cancelled or failed prompt is always a refusal. Neither missing-proof case is ever reported as authenticated.
Fixture mode is intentionally not gated: it carries no operator authority, and prompting there would train operators to dismiss the prompt reflexively. Pairing is gated even in fixture mode, so authority can never be acquired on an unprotected device in the first place.
VaultProtection deliberately has no userPresence value. The vault stores
the credential under the operating-system store's at-rest protection and does
not itself require a biometric to read it back; user presence is the separate,
explicit concern owned by LocalAuthGate.
Known residual gaps, not closed by this gate:
- Frames stop while the app is backgrounded, so the OS app-switcher snapshot
is taken before the lock screen paints. Blocking it needs
FLAG_SECUREon Android and a privacy overlay on iOS. - The gate is an application-level check. It does not stop an attacker with code execution, a rooted or jailbroken device, or a debug build from reading the credential out of the platform store directly.
Brain certificate pinning
The Commander protocol signs client→server only; nothing in a Brain response is authenticated. Without pinning, anyone holding a certificate from any CA the device trusts — including one pushed by enterprise MDM — could fabricate the entire fleet view, hide activity, and swallow job submissions. The identity check against locally stored values proves nothing, because an interceptor simply echoes those values back.
Commander pins the Brain's TLS certificate, recorded at pairing:
- Requests run on an
HttpClientbuilt fromSecurityContext(withTrustedRoots: false)carrying only the pinned certificates. A peer that cannot present one is rejected during the handshake, before any request byte is sent. Hostname verification still applies. badCertificateCallbackis never set. It only fires after validation has already failed, so it cannot implement pinning; returningtruethere would be a TLS bypass.- Rotation. A bare leaf pin would break at every renewal — roughly every
90 days for an ACME issuer. When a pinned handshake fails, Commander
re-inspects the peer over
SecureSocket, which completes a handshake under ordinary public-CA validation and yields the leaf certificate without sending any application data. The new certificate is adopted only if it is publicly valid and carries the same subject and issuer as the one recorded at pairing. Up to three certificates stay trusted at once, which keeps a window open across a changeover and tolerates a load balancer that serves more than one certificate for the same name. - On mismatch it fails closed with a distinct error naming the old and new issuer, and keeps the credential and the pin. The recovery path is to pair again from the protected operator console, which requires console access. Commander never auto-wipes a credential over a certificate change, and never offers a one-tap "trust it anyway".
Residual gaps, stated plainly:
- Pairing is trust-on-first-use. The pin can only record whoever answered at pairing time. An interceptor already in place during pairing is pinned. Out-of-band verification of the first connection is not implemented.
- Same-issuer forgery is not stopped. An attacker who can obtain a
certificate for the same host from the same issuer — via DNS or BGP control
over an ACME challenge, or by compromising that CA — passes the continuity
rule. Closing this needs a pin on the issuing CA, which cannot be built
from Dart's API:
SecureSocket.peerCertificateandHttpClientResponse.certificateexpose the leaf only, never the chain, so the intermediate's bytes are never available to persist as an anchor. - The pin is stored in the operating-system secure store, so it inherits exactly the protections described above and no more.
BrainClient is the transport-independent application contract.
FixtureBrainClient is deterministic local data for development and tests.
CommanderBrainClient switches from fixture data only after authenticated
pairing succeeds.
Distribution direction
The application targets Android, iOS, macOS, Linux, and Windows. Android and iOS packaging is prepared for internal sideload/TestFlight and closed testing; store submission still requires production API qualification, platform secure storage device qualification, privacy disclosures, signing, accessibility review, and store assets.
Bundle/application ID: com.suchsoftware.evaluetron.commander
Android release signing
Release builds are never signed with the debug keystore. That key ships with every Flutter installation, so a debug-signed release would let anyone forge an update to a client holding operator authority over the fleet.
Signing material is supplied out of band through android/key.properties,
which is gitignored along with *.jks, *.keystore, and *.p12. No keystore
is generated or committed by this repository.
storeFile=/absolute/path/to/commander-release.jks
storePassword=…
keyAlias=commander
keyPassword=…
storeFile is resolved relative to android/app/ when it is not absolute, so
an absolute path is recommended. All four entries are required; a
key.properties that is present but incomplete fails the build with the list
of missing keys.
When android/key.properties is absent the release build emits a warning and
produces an unsigned artifact. That is deliberate: an unsigned build
cannot be installed or published by accident, whereas a debug-signed one can.
See architecture and store readiness for the production boundary and remaining release work.
The optional managed-network design and upstream license audit are documented in managed Nebula. Visual identity and reproducible platform-asset generation are documented in brand system.
Development
flutter analyze
flutter test
flutter run
Copyright © 2026 Such Software. All rights reserved.
This repository is private and proprietary. No license is granted. No
LICENSE file is intentionally present.