jwinterm e218970cb2
CI (analyze, test) / analyze-test (push) Successful in 17s
Add Gitea Actions CI and make the tree pass its own gate
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.
2026-08-16 00:08:36 -04:00
2026-07-20 13:37:30 -04:00
2026-07-19 11:04:44 -04:00
2026-07-20 13:37:30 -04:00

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_SECURE on 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 HttpClient built from SecurityContext(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.
  • badCertificateCallback is never set. It only fires after validation has already failed, so it cannot implement pinning; returning true there 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.peerCertificate and HttpClientResponse.certificate expose 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.

S
Description
No description provided
Readme
543 KiB
Languages
Dart 83.9%
C++ 7.3%
CMake 5.7%
Kotlin 0.9%
Ruby 0.8%
Other 1.4%