Last Update: 2026-08-14 Last Submit: 51f7f7fd Last modified model: MiMoCode
SecRandom is a GPLv3 C#/.NET desktop app for fair random drawing in education scenarios. Stack: .NET solution, Avalonia + FluentAvalonia UI, Microsoft.Extensions.Hosting DI, xUnit v3 tests.
SecRandom-C/
├── SecRandom/ # Avalonia app layer: App host, views, viewmodels, app services, localization, assets
├── SecRandom.Core/ # Core/domain + reusable UI controls/styles + config/logging/draw services
├── SecRandom.Shared/ # Cross-project contracts, base config/model types, IPC/profile models
├── SecRandom.PluginSdk/ # In-process plugin base types and SRPX packaging target
├── SecRandom.ExamplePlugin/# Minimal plugin template
├── SecRandom4Ci.Interface/ # Shared ClassIsland v2 IPC contract for the SecRandom4Ci plugin
├── SecRandom.Desktop/ # Tiny executable launcher; Program.cs bootstraps Avalonia and UiAccessStartup.cs prepares Windows UIAccess
├── SecRandom.Launcher/ # Minimal portable-package version selector; starts an activated app-* payload only
├── SecRandom.Platforms.Abstractions/ # Platform-neutral window capability contracts and result DTOs
├── SecRandom.Platforms/ # Startup context, DI registration, and unsupported-platform stub
├── SecRandom.Platforms.Windows/ # Windows-native window feature implementation
├── SecRandom.Platforms.Linux/ # Linux-native window feature implementation boundary
├── SecRandom.Platforms.MacOs/ # macOS-native window feature implementation boundary
├── SecRandom.Mobile.Tests/ # Avalonia Headless tests for mobile styles, native controls, and phone-size layout
├── SecRandom.Android/ # Android entry head: net10.0-android Exe with BuildMobile=true, otherwise empty neutral library
├── SecRandom.iOS/ # iOS entry head: net10.0-ios Exe with BuildMobile=true, otherwise empty neutral library
├── SecRandom.Core.Tests/ # xUnit v3 test project; currently covers legacy privacy/telemetry migration
├── scripts/ # Standalone tooling and verification scripts, including fairness audits
├── docs/ # Project rules, localization, namespace boundaries
├── CHANGELOG/ # Versioned release notes, mostly v3 tree
├── resources/ # README mirrors, screenshots, banners, root static assets
├── vendors/EdgeTtsSharp/ # Edge TTS synthesis submodule; app supplies a cross-platform transport seam
├── Global.props # Main shared MSBuild policy; imported by projects
├── Directory.Build.props # Avalonia version pin only
└── SecRandom.sln # Build/test solution entrypoint
Nested instruction files:
SecRandom/AGENTS.md: app layer, DI composition, views/viewmodels, app services, localization.SecRandom/Views/SettingsPages/AGENTS.md: settings page subtree, page IDs, restart semantics, grouped localization expectations.SecRandom.Core/AGENTS.md: Core draw/config/logging services and shared controls/styles.SecRandom.Shared/AGENTS.md: UI-free shared contracts and persistence model boundaries.
| Task | Location | Notes |
|---|---|---|
| Run/build/test | SecRandom.sln, .github/workflows/Build.yml |
Use solution commands; no Makefile/CMake. CodeQL builds the desktop project with BuildInParallel=false because the desktop asset override can otherwise schedule duplicate Core project nodes that race on Avalonia's generated resource file. |
| Desktop startup | SecRandom.Desktop/Program.cs |
Process entry → Avalonia lifetime. |
| Platform capability contracts | SecRandom.Platforms.Abstractions/, SecRandom.Platforms/ |
App-internal platform root, window feature requests/results, startup context, and DI bridge. |
| Native window features | SecRandom.Platforms.Windows/, SecRandom.Platforms.Linux/, SecRandom.Platforms.MacOs/ |
Each platform owns native feature handling; views must not add platform API calls. |
| Mobile startup | SecRandom/App.axaml.cs, SecRandom/Mobile/, SecRandom/Views/Mobile/, SecRandom.Android/, SecRandom.iOS/ |
The shared App owns the one Host and branches by Avalonia lifetime; SecRandom owns the mobile root, routes, services, and platform-neutral seams while heads own entry points. |
| Mobile UI tests | SecRandom.Mobile.Tests/ |
Avalonia Headless smoke tests load mobile styles and lay out native shell controls at phone dimensions. |
| Mobile point-call orchestration | SecRandom/Services/Mobile/MobileRollCallService.cs |
Mobile-only list/scope/count orchestration over existing Core filtering, sampling, and transactional commit services. |
| App composition / DI | SecRandom/App.axaml.cs |
BuildHost() is the registration source of truth. |
| Plugins | SecRandom.PluginSdk/, SecRandom/Services/Plugins/, SecRandom.ExamplePlugin/, docs/plugins.md |
Desktop SRPX plugins are loaded before Host build; plugin pages/services register through Core APIs. Stable plugin-facing contracts live in SecRandom.Core/Abstraction/Services/ (views under Abstraction/Services/Views/). The plugin market index and signed generation workflow live in the separate SECTL/SecRandom-PluginIndex repository. |
| Main navigation | SecRandom/Views/MainView.axaml.cs |
Default page main.rollCall; keyed DI page factory. Built-in draw pages are main.rollCall and main.lottery; quick draw opens from the floating window instead of the main sidebar. |
| Settings navigation | SecRandom/Views/SettingsView.axaml.cs |
Default page settings.overview; has back stack + restart dialog. General group now includes settings.general.basic, settings.general.privacy, and settings.general.backup. |
| Page registration helpers | SecRandom.Core/Extensions/Registry/ |
AddMainPage, AddSettingsPage, group, and separator helpers. |
| ClassIsland notifications | SecRandom/Services/Notification/, SecRandom4Ci.Interface/ |
Typed v2 IPC client for the installed SecRandom4Ci ClassIsland plugin. |
| Crash recovery | SecRandom/Services/CrashRecovery/, SecRandom/Views/CrashRecoveryWindow.axaml.cs |
Fatal/dispatcher crash report prompt, guarded auto-restart, and shared desktop relaunch logic. |
| Page registry state | SecRandom.Core/Services/PagesRegistryService.cs |
Main/settings/group collections. |
| Cross-platform view engine | SecRandom.Core/Views/ |
Logical view/session contracts; desktop and mobile shells provide DI-registered physical hosts. |
| Fair draw logic | SecRandom.Core/Services/Draw/ |
Partial DrawEngine, weighted draw, filters, crypto RNG, plus DrawCommitCoordinator (IDrawCommitService) transactional commits and shared DrawRepeatPolicy/DrawCandidateFilter. |
| Config persistence | SecRandom.Core/Services/Config/ |
FileConfigService and handlers are host-internal Core runtime services; desktop keeps its existing package-root data path. v3 backup/archive transfer lives in SecRandom.Core/Services/Archive/ (DataArchiveService). |
| Audit tooling | scripts/FairnessAudit/ |
Standalone fairness/performance validation script and HTML report generator. |
| Release update signing | scripts/ReleaseManifest/, .github/workflows/build_publish.yml |
Ed25519 key-generation helper and CI manifest signer; private key is Actions-secret-only. Release intermediates and final artifacts are grouped under artifacts/release/. |
| Reusable controls/styles | SecRandom.Core/Controls/, SecRandom.Core/Styles/, SecRandom.Core/StylesBase.axaml |
App style entrypoint includes Core bundle. |
| Localization rules | SecRandom/Langs/, SecRandom.Core/Langs/, docs/localization.md |
Per-page resource folders; .csproj registers base resx/designer only. Privacy page resources live under SecRandom/Langs/SettingsPages/General/Privacy/. |
| Shared contracts | SecRandom.Shared/ |
Keep UI/runtime dependencies out. Profile list items use hidden stable RecordId keys; visible Id/student number/prize number is optional metadata. |
| Project rules | docs/project_rules.md |
Strongest local convention source. |
Keep this map short and stable. When code moves, AI agents should re-read the moved files and update this map in the same task.
| Symbol | Type | Location | Role |
|---|---|---|---|
Program.Main |
entry | SecRandom.Desktop/Program.cs |
Starts Avalonia desktop lifetime. |
UiAccessStartup |
startup helper | SecRandom.Desktop/UiAccessStartup.cs |
When UIAccess topmost is configured on Windows, elevates a bootstrap process and starts a replacement process with a UIAccess token before Avalonia initializes. |
Program.BuildAvaloniaApp |
entry helper | SecRandom.Desktop/Program.cs |
Platform detect, MiSans default font, trace logging. |
App |
Avalonia app | SecRandom/App.axaml.cs, App.Consts.cs |
Culture, XAML load, Host/DI, windows, restart/stop, theme/font refresh. |
IAppHost |
static service access | SecRandom.Core/Abstraction/IAppHost.cs |
Holds Host and exposes GetService<T>() / TryGetService<T>(). |
MainView |
shell view | SecRandom/Views/MainView.axaml.cs |
Main NavigationView, drawer, default page, settings window bridge. |
SettingsView |
shell view | SecRandom/Views/SettingsView.axaml.cs |
Settings NavigationView, history/back, restart prompt. |
PagesRegistryService |
registry | SecRandom.Core/Services/PagesRegistryService.cs |
Static collections backing generated navigation menus. |
DrawEngine |
domain service | SecRandom.Core/Services/Draw/DrawEngine*.cs |
Student/prize drawing, fairness weights, repeat/avg-gap filtering. |
WeightedDrawEngine<T> |
algorithm | SecRandom.Core/Services/Draw/WeightedDrawEngine.cs |
Validates weights and samples without replacement. |
MainConfigHandler |
config handler | SecRandom.Core/Services/Config/MainConfigHandler.cs |
Main config wrapper over ConfigHandlerBase<MainConfigModel>; persists the canonical General subtree and still loads legacy root basic/backup JSON. |
ProfileService |
runtime service | SecRandom.Core/Services/Profiles/ProfileService.cs |
Current profile runtime state, active student-list/history switching, and persistence for desktop and mobile hosts. |
IProfileService |
service contract | SecRandom.Core/Abstraction/Services/IProfileService.cs |
Current lists/history + student profile switch + profile save boundary. |
SettingsSearchService |
app service | SecRandom/Services/Settings/SettingsSearchService.cs |
Indexes settings pages via reflected localization resources. |
CrashRecoveryRuntime |
app service helper | SecRandom/Services/CrashRecovery/CrashRecoveryRuntime.cs |
Reads crash recovery mode, writes bounded crash reports, and builds restart process plans. |
ISecurityService |
app service contract | SecRandom/Services/Security/ |
Owns credential verification, lockout policy, selected-factor authorization, and protected-operation gating. |
ProtocolCommandRouter |
app service | SecRandom/Services/Ipc/ProtocolCommandRouter.cs |
Normalizes URL/IPC routes, routes protected commands, and returns structured IPC results. |
DeviceUuidStore |
app service | SecRandom/Services/Config/DeviceUuidStore.cs |
Persists the pseudo-anonymous device UUID separately in data/config/device-uuid.json and migrates legacy settings values. |
AttachedSettingsRegistryService |
registry | SecRandom.Core/Services/AttachedSettingsRegistryService.cs |
Static collections for attached-settings controls. |
ViewModelBase |
base VM | SecRandom/ViewModels/ViewModelBase.cs |
Base VM exposing MainConfig; inherits ObservableRecipient. |
GlobalConstants |
constants | SecRandom.Core/GlobalConstants.cs |
Version, platform, and development-mode constants. |
DrawCommitCoordinator |
domain service | SecRandom.Core/Services/Draw/DrawCommitCoordinator.cs |
IDrawCommitService implementation: single DrawRoundId, temp→history commit order, snapshot compensation, serialized gate. |
DrawRepeatPolicy / DrawCandidateFilter |
draw rules | SecRandom.Core/Services/Draw/ |
Shared repeat-threshold and candidate-filter rules; replaces formerly duplicated copies. |
DataArchiveService |
domain service | SecRandom.Core/Services/Archive/DataArchiveService.cs |
Platform-neutral v3 backup/archive engine: validation, staging commit/rollback, snapshots. |
IArchivePostImportHooks |
seam | SecRandom.Core/Services/Archive/IArchivePostImportHooks.cs |
Platform follow-up after archive import; Core registers Null hooks, desktop overrides them. |
ProfileCatalogManager |
domain service | SecRandom.Core/Services/Profiles/ProfileCatalogManager.cs |
List/profile CRUD and student/prize history clearing behind IProfileCatalogManager. |
RosterImportParser |
parser | SecRandom.Core/Services/Profiles/RosterImportParser.cs |
Shared roster spreadsheet parsing and column mapping for desktop/mobile imports. |
IRosterQrCameraCapture / IRosterQrCameraCaptureFactory |
app service seam | SecRandom/Services/RosterTransfer/IRosterQrCameraCapture.cs |
Common in-memory frame, permission, error, and lifetime boundary for list-import QR cameras. It resolves named IPlatformCameraDeviceCatalog devices from Host: Windows uses DirectShow/OpenCV for physical and virtual cameras, while macOS/Linux use AVFoundation/V4L2 indexes and Android maps Camera2 camera IDs to CameraView facing. |
IPlatformCameraDeviceCatalog |
platform contract | SecRandom.Platforms.Abstractions/IPlatformCameraDeviceCatalog.cs |
Supplies the active platform's camera device ID, display name, capture index, and facing hint without leaking native APIs into the app layer. |
MobileRollCallService |
mobile service | SecRandom/Services/Mobile/MobileRollCallService.cs |
Mobile list/scope/count snapshots, multi-member draws, remaining list, and scoped temporary reset without changing the Core session contract. |
MobileMediaLibraryService / MobileDrawMediaService |
mobile services | SecRandom/Services/Mobile/ |
Mobile-private media import/reference cleanup and draw-time per-record image/music/voice orchestration through head-injected native playback. |
docs/project_rules.mdoverrides inference when adding features.- General settings now live under
MainConfigModel.General;MainConfigModel.Basic/Backupremain compatibility bridges for existing callers while new config splits belong underSecRandom.Core/Models/SubConfigs/General/. - Basic settings are functional runtime controls:
ShowStartupWindow, primary-window topmost mode, and background residency apply only to the primaryMainWindow;AutoSaveWindowSizepreserves independent geometry/maximized state for the primary and settings windows. Cross-platform autostart andsecrandom://protocol registration belong to app-layerDesktopIntegrationService, which must use user-level Windows/Linux/macOS mechanisms and not silently persist a failed integration request. - Selecting
TopmostMode.UiAccessfor the primary or floating window persists the setting and requests a restart.SecRandom.Desktop/UiAccessStartupis the Windows-only pre-Avalonia launcher boundary: followingkilltimer0/uiaccess, it elevates a bootstrap process, impersonates same-sessionwinlogon.exe, setsTokenUIAccesson a duplicate of the bootstrap's current token, and starts the UIAccess replacement before Avalonia initializes. The original process waits only for the bootstrap to create the replacement, never for Avalonia or Host initialization; denied elevation, preparation failure, or timeout keeps the original process in ordinary topmost and preserves the configured mode for a later retry. - Windows UIAccess topmost builds may use
SecRandom.Desktop/app.uiaccess.manifestwhenEnableUiAccess=true; that manifest requires Authenticode signing and installation under Program Files. Independently, the runtimeUiAccessStartuptoken preparation followskilltimer0/uiaccessand works from the ordinary manifest after UAC authorization, so debug and portable Windows builds can exercise UIAccess as well. The release workflow enables the manifest only when Windows code-signing secrets are available. - Point-call students and lottery prizes use
RecordIdas the internal stable identity for history/fairness. The visibleIdfield is optional display metadata only and must not be required by import, draw, or history logic. - A roll-call student or lottery prize candidate must be enabled and have a nonblank
IdorName. Import may map either column and excludes rows where both are blank. - Picking
ClearRecordcontrols temporary draw records only; do not clear persistent profile histories from that setting. RollCall and QuickDraw share the same student temporary record store, while Lottery uses prize temporary records. - Privacy settings split Sentry upload from online status reporting:
SentryTelemetryEnabledonly controlsSecRandom/Services/Telemetry/, whileOnlineStatusModeonly controlsSecRandom/Services/OnlineStatusService.cs. - Telemetry access is seam-based: runtime code depends on
ITelemetryTransaction/ITelemetrySdkAdapterand must not reference Sentry types directly (the DSN lives inGlobalConstants.SentryDsn).TelemetryRuntimeServiceis Sentry-free; the desktop-onlyTelemetryTransactionSentryExtensionsshim is excluded from mobile builds, while mobile links the same telemetry sources, addsMobileProfilingIntegrationStub, and owns its Sentry DI/init/shutdown plus Android/iOS unhandled-exception hooks. - The pseudo-anonymous device UUID belongs in
data/config/device-uuid.json; legacy settings values are migration-only and must not be serialized back intosettings.json. Full-data and configuration backups include this file, while settings-only exports do not overwrite a device identity. - ClassIsland notifications require the installed
SecRandom4Civ2 plugin.NotificationServiceuses the localSecRandom4Ci.Interfacecontract andClassIsland.IPC.v2.Server; it must not use loopback TCP. SecRandom's built-in notification channel is the existing QuickDraw result window, shared by roll-call, QuickDraw, and lottery notifications. Each draw channel only controls whether it is enabled;NotificationSettings.Defaultowns the one global service, duration, threshold, fallback, and built-in auto-close configuration.UseBuiltInOnServiceFailureis the backend-neutral fallback policy for a selected external notification service and defaults to enabled; current ClassIsland-only delivery uses it when v2 IPC fails. For automatic QuickDraw delivery, the built-in or dual service opens this window before the draw to show the rolling animation and then its result; external-only delivery must not open the QuickDraw window unless the fallback runs. Notification settings must not duplicate the QuickDraw window's placement, opacity, or draw-animation controls. - Crash recovery mode lives under
MainConfigModel.General.CrashRecovery; prompt startup handling must run before single-instance acquisition, while normal restart must release the single-instance service before relaunch.CrashRecoverySettingsConfig.DisableCrashedPlugin(default enabled) gates the plugin crash auto-disable path: when a dispatcher unhandled exception stack belongs to a plugin load context, that plugin is marked.disabledfor the next startup; the toggle lives beside the crash-recovery mode in the basic settings page. - First-run OOBE is a top-level window shown immediately after
BuildHost()and before floating-window/main-app initialization. Its carousel begins with a non-blocking, centered welcome screen with the application icon; it is outside the seven configuration pages, whose first item places the collapsed-by-default verifiable-draw notice before the privacy-policy and GPLv3 acknowledgement controls. Each requires explicit acknowledgement. The welcome page owns the lower-left language selector; changing it persists the language, refreshes ViewModel-derived localized values, then recreates only the OOBE visual tree into localized resources without restarting or leaving the first-run flow. An active import drawer defers the visual replacement until its preview/mapping flow closes.GuideCompleted,AcceptedVerificationNoticeVersion,AcceptedPrivacyPolicyVersion, andAcceptedGplVersiondetermine whether an OOBE acknowledgement is required. After the full guide has completed, a verifiable-draw notice, privacy-policy, or GPL version update opens only the acknowledgement page and its privacy controls, not unrelated setup pages. OOBE class/prize spreadsheet imports reuse the list-management import controls in its own right-side drawer, so their file mapping, preview, duplicate handling, and cancel behavior stay aligned with the settings list pages. Completing OOBE continues into normal startup and explicitly opens the primary main window; it must not exit merely because the OOBE window closes. Verifiable-draw notice, privacy-policy, and GPL changes increment their app-layer OOBE versions independently. - Settings JSON, full-data ZIP, and backup restore only accept manifest/envelope exports produced by SecRandom v3. Validate
producer_versionbefore taking an import snapshot or changing data; unsupported sources must be explained in a modal dialog. - Security credentials must never be stored in
MainConfigModelorsettings.json. Keep passwords, TOTP seeds, USB binding tokens, and lockout state inSecRandom/Services/Security's separate credential store; ordinary settings only select factors and protected operations. The sole credential file isdata/config/security/credentials.json: its internalFormatVersionis authoritative, uses Argon2id-derived key material plus AES-256-GCM for portable storage, and must not depend on DPAPI, Keychain,secret-tool, or a versioned filename. Do not read or migrate older credential files. - OmniTTS cloud speech API keys are credentials too: they live in
data/config/voice/omnitts-keys.jsonviaOmniTtsCredentialStore(app-layer, atomic replace), never inVoiceSettingsConfig/settings.json, IPC payloads, logs, telemetry, backups, or the Android DocumentsProvider. OmniTTS is voice engine 2 (OmniTtsSpeechProvider.OmniEngine), sits beside System SAPI (0) and Edge TTS (1), and is a unified cloud TTS engine whose providers are OpenAI, Gemini, FishAudio, MiMo, and Custom OpenAI-compatible. OpenAI, Gemini, and MiMo voice choices use documented preset sets; Gemini uses native/v1beta/interactionssynthesis and wraps its 24 kHz PCM response as WAV. MiMo voice-design and voice-clone models use model-specific chat-completions payloads; clone references are WAV files kept privately underdata/config/voice/mimo-voice-reference.wav, identified by a hash, and excluded from settings, archives, IPC, and logs. Models are fetched from provider APIs or typed manually. The voice settings page shows OmniTTS-specific rows (provider, base URL, key, model, voice, batch cache) only whileVoiceEngine == 2.data/config/voice/is excluded from archives and protected from the Android provider/directory launcher. iOS keeps the voice settings page unregistered (!OperatingSystem.IsIOS()); OmniTTS targets Windows, Linux, macOS, and Android. - Security authorization always flows through
ISecurityService; do not add direct validation checks to tray handlers, windows, ViewModels, or linkage code. Passwords require at least 6 characters, with no artificial character-class rule. - Full IPC/URL compatibility is app-layer routed through
ProtocolCommandRouter: structured current-user named-pipe IPC is additive to legacyShowMainWindow/Restart/Url:delivery, and all external mutations must useISecurityService.data/*queries must use non-mutating profile snapshots, never active-profile loading APIs. HistoryItem.DrawRoundIdidentifies every record committed by one logical draw. Populate it for new history writes; IPC history projections group by it and must never expose internalRecordId.- Draw commits must go through
IDrawCommitService(DrawCommitCoordinator): one logical draw gets exactly oneDrawRoundId(a caller may supply one), temporary records commit before persistent history, a mid-commit failure rolls back through snapshot compensation, and commits serialize behind the coordinator gate. Never reintroduce bare two-step writes ofIProfileService.Record*Historyplus temporary-record calls; the optionaldrawRoundIdparameter (anddrawMethodonRecordPrizeHistory) exists for coordinator use. - File persistence saves are atomic replacements:
FileConfigService.SaveConfigandDrawTemporaryRecordService.SaveStatewrite a temporary file and atomically replace the target; new file-persistence code must follow the same pattern instead of overwriting files in place. - Draw proofs use the SecRandom v3 algorithm ID and an
algorithmEngineVersion. Engine v3.1 commits the sampling mode into the request: student draws use history-balanced weighted sampling, count-based lottery inventory uses equal-probability partial permutation when no internal rule applies, and pan or internal-rule lottery draws use weighted sampling without replacement. Keep the verifier's replay logic byte-compatible with each supported proof engine version. - Ordinary draws immediately save an
OfflineReproducibleproof, then submit that newly generated proof once tofair.sectl.cnfor background replay/signing. Startup, mode changes, and failed requests must not scan or retry historical proof files. A current server receipt signs a canonical hash of all proof fields except the receipt itself, plus the proof ID, input hash, payload hash, anonymous audit-payload hash, and mode; public sharing requires that receipt. This detects changes after the signed submission, but it does not attest to an unmodified local executable, an authentic real-world candidate pool, or absence of pre-draw result selection. Do not call it a pre-draw server witness. Local proof retention applies both the configured age and total storage limits; after every new save, delete oldest.srproof.jsonfiles until the configured storage cap is met. - Formal notarization is the explicit alternative to ordinary replay attestation: the client sends a zero-seed anonymous request,
fair.sectl.cndurably locks it, persists server random material after the lock, calculates the result, and returns an immutableOnlineWitnessedproof. Formal mode waits for that response and must never substitute a local draw after an error; its ledger is independent of retention-limited public proof sharing. It protects the locked flow against local code/seed/proof replacement, but cannot establish real-world roster authenticity, completeness, or pre-submission pool integrity. - Proof ordering TODO: attestation is still enqueued when a draw completes rather than strictly after the commit boundary; deferring proof finalization/attestation until after the
IDrawCommitServicecommit remains an open task. Keep this note until that ordering lands. - Settings preview is a security-prompt outcome, not URL authorization bypass. When enabled, it freezes page content while preserving settings navigation and must not mutate configuration.
- ViewModels must be registered in
SecRandom/App.axaml.csBuildHost(); reusable services also go through Host. The mobile branch directly registers its mobile runtime services, navigation, pages, and single-view host rather than delegating to a separate application-service extension. - Desktop plugins use
.srpxZIP packages with a rootmanifest.yml; the entry assembly contains one non-abstractPluginBaseimplementation. Plugins are discovered fromdata/plugins, pending packages are staged indata/cache/plugin-packages, and plugin initialization runs before Host construction. Plugin page IDs follow theplugin.<id>.*namespace, are owned by the plugin, and must be unique at the application registry level. External dynamic plugins are desktop-only for now; mobile hosts do not load them. - Plugin API surface:
PluginBaseimplementsIAsyncDisposable; the host disposes each entrance (DisposePluginsAsync) before Host shutdown, and forwards application start/stop to thePluginBase.OnAppStarted/OnAppStoppingoverrides through thePluginLifecycleBridgehosted service (no plugin-registered hosted service needed just for lifecycle). Stable plugin-facing contracts resolve through DI fromSecRandom.Core/Abstraction/Services/:IMainView/ISettingsView/IDrawerView(underViews/, wrappingMainView.Current/SettingsView.Currentwith UI-thread marshaling and empty-shell no-ops),IAppNavigationService(wrapsApp.ShowMainWindow/ShowSettingsWindow/ShowQuickDrawWindowand preserves the settings authorization gate),IAppLifecycleService(forwardsApp.AppStarted/AppStopping; primary use is plugin lifecycle hooks),IFloatingWindowButtonRegistry+FloatingWindowButtonDescriptor(runtime-only floating-window button registration; visibility is picked in the floating-window settings MultiComboBox and persisted asFloatingWindowSettingsConfig.VisiblePluginButtonIds, pruned at refresh when a registration disappears), andIPluginDrawService(controlled draw facade). Plugin draws must go throughIPluginDrawService, which authorizes viaLinkageDrawCoordinatorand reusesRollCallDrawService/LotteryDrawService; plugins must never callIDrawCommitService,IRollCallSession/ILotterySession, orIProfileService.Record*Historydirectly. Plugin management supports uninstall markers (.uninstall, config dir preserved), crash auto-disable (.disabledwritten when an unhandled exception stack belongs to a plugin load context), and external dev plugin directories via repeatable--epp/--externalPluginPathstartup args.PluginManageris registered as bothIPluginManagerand its concrete type. The srpx packaging target excludes host-provided assemblies (SecRandom/Avalonia/FluentAvalonia/Microsoft.Extensions/runtimes/resources) so plugin packages stay host-free even with the local SDK reference. The full guide isdocs/plugins.md. - Plugin SDK packaging and release:
SecRandom.PluginSdktracks the application version.PluginApiVersions.Currentin the SDK defines the minimum host API version and its major must equal the application major version (enforced by thebuild_pluginsdkCI job).SecRandom.Shared,SecRandom.Core, andSecRandom.PluginSdkare packed together on every CI run (-p:PackageVersion=<app version>) and uploaded as thepluginsdkartifact; only the releasepublishjob pushes them to NuGet.org usingNuGet/login@v1(OIDC Trusted Publishing,user=${{ secrets.NUGET_USER }}). The host rejects plugins whosemanifest.ymlapiVersionmajor is belowPluginApiVersions.Current.Major. Local pack workflow:scripts/publish-pluginsdk.ps1. - Plugin market: the signed index lives in
SECTL/SecRandom-PluginIndex, where oneplugins/<id>.yamlper plugin declaresrepoOwner/repoNameplus metadata. A workflow rebuildsindex.json+index.json.sigon push and nightly, then uploads them to the fixed release taggenerated.PluginMarketService(app layer, desktop only) downloads mirror-first with a GitHub fallback, verifies the whole index with the embedded Ed25519 key (avares://SecRandom/Assets/Plugins/plugin-market-public-key.txt, excluded from the desktop overlay), resolves the required dependency closure in topological order with cycle detection, verifies each package's SHA-256, then stages throughIPluginManager.StagePackage. The market public key is independent of the update signing key; the private key exists only as thePLUGIN_MARKET_PRIVATE_KEY_PEM_BASE64Actions secret. Plugin authors usescripts/publish-plugin.ps1to build metadata and embed<!-- SECRANDOM_SHA256: <hex> -->in their release note; the workflow requires exactly one.srpxasset per release. Index/entry DTOs live inSecRandom.Shared/Models/Plugins/PluginCatalog.cs. - The cross-platform view engine lives in
SecRandom.Core/Views/. It separates logical AvaloniaControlsessions, presentation intent, close/result handling, factories, and hosts. Core contracts must not exposeWindow, platform lifetimes, native APIs, or rawIServiceProvider; desktop and mobile shells register their physical presenters through DI. - MVE is reserved for independent logical views with their own lifecycle, including
MainView,SettingsView, crash recovery, and the modalmain.remainingListview. Desktop shell child pages remain keyedUserControlinstances hosted only byFAFrame;desktop.mainanddesktop.settingsname the physical windows that host the two shell views, never hidden regions inside those shells. Settings read-only preview freezes theFAFramecontent while leaving navigation available. - Platform feature callers must resolve narrow platform contracts from Host.
PlatformStartupContextis startup-only: desktopProgramand Android/iOS heads set it before Avalonia starts, then sharedAppreads it once to select desktop or mobile registrations. Do not use it from views, ViewModels, Core, or business services. Removable-storage discovery follows the same boundary throughIRemovableStorageCatalog: Windows, Linux, and macOS enumerate ready removable volumes in their matching platform projects, while the app security service consumes opaque device IDs and user-facing display locations and resolves mount paths internally.IPlatformCameraDeviceCatalogfollows the same boundary for named QR camera choices: only platform projects may enumerate native devices; the app layer receives opaque IDs, display names, capture indexes, and facing hints. A catalog mount root is internal implementation data and must not cross intoISecurityServicepublic binding APIs, UI models, dialogs, or IPC payloads. Platform discovery commands must read output asynchronously and enforce a bounded timeout before returning an empty catalog. USB binding markers use the siblingIRemovableStorageBindingMarkercontract and always use.SecRandom.safety.key; binding presence requires both the platform-hidden marker token and the matching stored device record. Binding records persist only the stable device ID and token hash; mount roots are never persisted or used as identity. - Window feature requests use
IWindowFeatureServicewith a neutralPlatformWindowHandle,WindowFeatureRequest, and explicitApplied/Unsupported/Failedresult. Keep Win32/X11/AppKit operations in the matchingSecRandom.Platforms.<OS>project. - Mobile
MobileViewHostuses AvaloniaIInputPaneto keep a focused text input above the system keyboard by translating the stable page viewport, including keyboard animation timing and cleanup on host detach. AndroidMainActivitymust useSoftInput.AdjustNothing; do not combine system resize with the host translation. Windows touch input initializesSecRandom.Platforms.Windows.WindowsTouchKeyboardIntegrationbefore Avalonia startup; it openstabtip.exethrough the system COM bridge and usesIInputPaneto shift an occluded focusedTextBox. Its window-closing guard skips Avalonia Win32 input-pane disposal on shutdown because some Windows builds crash in the nativeUnadvisecall; this is process-exit cleanup only and must stay outside Core. TopmostMode.UiAccessremains a Windows process-token capability controlled bySecRandom.Desktop/UiAccessStartup.cs; it is not a generic window feature or a responsibility of platform window services.- Mobile front-end code is part of
SecRandom: composition/platform contracts live inSecRandom/Mobile/, views inSecRandom/Views/Mobile/, controls inSecRandom/Controls/Mobile/, services inSecRandom/Services/Mobile/, and resources inSecRandom/Langs/Mobile/.SecRandom.Android/SecRandom.iOSremain the only platform entry heads.BuildMobile=trueenables their platform TFMs (net10.0-android/net10.0-iosExe); build the relevant head project directly for platform CI. Platform-specific code stays in the heads behindIMobileUpdateInstaller,IMobileMediaPlayer, andMobilePlatformServiceRoot.StartupErrorLogger. iPadOS and Android tablets setMobilePlatformServiceRoot.UsesDesktopMainViewbefore Avalonia startup, so the shared app presentsMainViewand desktop main-page registrations; iPhone and Android phones retainMobileRootView. SharedSecRandom.AppcallsUtils.ConfigureMobileDataRoot()before it builds the mobile Host, registers Core/mobile services, assigns/clearsIAppHost.Host, mountsMobileViewHostasISingleViewApplicationLifetime.MainView, then shows the selected initial view through itsNavigationPage. The mobile branch must not register desktop-only services, windows, tray, shortcuts, OOBE, or protocol startup paths. Android additionally exposes onlydata/throughSecRandomDocumentsProvider;data/config/security/and descendants are never provider-visible. iOS has no provider and hides open-directory actions. The provider virtual root shows a singledatachild and Android path launching targets that child document viaACTION_VIEW(directory picker is only the fallback); all path containment and protection checks must canonicalize throughJavaFile.CanonicalPathbecause app-private storage resolves through the/data/user/0symlink to/data/data, never compare rawAbsolutePathstrings. Android settings "更多" shows the log/data/app directory entries plus their separator throughCanOpenFileManagerDirectories; path launching mapsdata/logsanddatato their provider documents and falls back to the provider root for non-browsable paths such as the package root. - Mobile UI uses native Avalonia controls with FluentAvalonia settings expanders and one
NavigationPageinsideMobileViewHost;MobileRootViewowns the ordinary bottom-barUserControlroutes whileMobileRootViewandSettingsVieware the only mobile MVE pages. Do not addNavigationView,InfoBar, a third-party mobile component library,MobileStyles.axaml, or a mobile token dictionary. Stable mobile layouts are AXAML-first; code-behind is limited to runtime data projection, StorageProvider operations, DataGrid construction, dialogs, media, and service calls. ViewBasederives fromContentPageso the same logical view can be hosted by desktop windows or the mobileNavigationPage. Every independent view must set a pageHeader; it may decide its own navigation-bar and back-button visibility throughNavigationPageattached properties.- Mobile settings open the independent
SettingsViewMVE throughMobileViewHost's oneNavigationPage, rather than becoming a normal bottom-bar route. Keep the mobile settings catalog as the navigation shell, but have it route to registered settings pages, including the desktopUpdateSettingsPageforsettings.update. Do not register or restore the deprecated mobile general, personalization, list-management, draw, backup, or about settings content pages. - Phone
MobileDrawPageis a layout shell over the singleton desktopRollCallPageViewModelandLotteryPageViewModel. Its result area uses the sameRollCallResultPresenter/LotteryResultPresentercontrols as desktop, so result models, display settings, animation revisions, reset semantics, and operation text remain shared. After mobile settings closes, refresh these shared sessions throughRefreshAfterProfileChange()rather than adding mobile file watchers or another draw ViewModel. IFeatureAvailabilityServiceis the sole live source for the mobile lottery tab and history tab. When lottery is disabled, hide both immediately, stop only the lottery preview, and return the draw shell to roll-call without interrupting an in-flight transactional commit.- Resolve shared services via
IAppHost.GetService<T>()/TryGetService<T>()unless constructor injection is already the local style. - Navigation pages need
[PageInfo(...)]plusservices.AddMainPage<T>()orservices.AddSettingsPage<T>()inBuildHost().AddMainPage/AddSettingsPagenow carry navigation metadata only; every built-in main/settings page is a CoreViewBaseadditionally registered withAddView<T>(pageId)so the view engine can host it. - Built-in main navigation entries may use
PageLocation.Bottomfor bottom-pinned sidebar items; roll-call (main.rollCall) and lottery (main.lottery) are bottom-pinned and full-width/title-hidden. Quick draw is not a main navigation page and opens from the floating window. - Page IDs:
main.xxx,settings.xxx,settings.group.xxx. - Picking animation style is unified: settings expose it as
AnimationStyle/ “动画样式”, and RollCall, QuickDraw, and Lottery use the same style for both rolling preview/process animation and final result reveal. Do not split process/result animation style settings. - Managed draw music uses a platform-specific runtime boundary: desktop
settings.personalized.musicimports/deletes/previews MP3/WAV/FLAC files throughMusicLibraryService/private SoundFlow playback, while mobileMobileMediaLibraryServiceuses StorageProvider streams intodata/audio/musicandIMobileMediaPlayersupplied by Android/iOS heads. Draw-settings and student/prize attached settings use the same managed IDs, no-music, or random-play options; the first drawn record supplies both overrides for a multi-record result. Deleting a managed track clears global and per-record references. Mobile display images likewise import through StorageProvider streams intodata/images; image/music/voice failures must not invalidate a committed draw. - Course linkage uses fixed v2 data-source values:
0=Off,1=CSES,2=ClassIsland. CSES schedules live atdata/CSES/cses_schedule.yml; ClassIsland is accessed only through the app-layer official IPC adapter. Only a confirmed course break restricts local draw/reset or hides the floating window. Missing/invalid CSES data, ClassIsland connection loss, or an unknown state must permit normal operation. - CSES schedule errors follow the “
InvalidDataException+Dataerror code” convention:CsesScheduleExceptionis a static factory that throwsInvalidDataException(sealed, so it cannot be subclassed) with aCsesScheduleErrorcode and optional argument stored inException.Data; UI retrieves them throughCsesScheduleException.TryGetErrorand localizes from resources. LinkageSettings.VerificationRequiredgoverns the course-time bypass prompt and is distinct fromSecuritySettings.ProtectLinkage, which continues to protect only external SecRandom URL/IPC mutations. Student course history usesHistoryItem.CourseNamewithRecordIdidentity; empty legacy course values remain global history.- File/data paths should go through
Utils.GetFilePath(...). Desktop and portable deployments keep mutable data under the package root'sdata/directory. The sharedAppmobile branch is the controlled exception: before any path is read, it setsUtilsonce to its app-private local-data root; no Core service, view, or desktop code may redirect it later.data/configkeeps its stable name for compatibility but is hidden at the filesystem level where supported (WindowsHidden|System; Unix-like hosts use the parent.hiddenconvention as a fallback), and hiding failures must not block config I/O. - Desktop startup calls
Utils.PrepareDesktopDataRoot()before reading settings. Installed packages use<PackageRoot>/datawhen it passes a real write probe and otherwise useLocalApplicationData/SecRandom/data; portable ZIP packages never fall back to a per-user directory, and a failed write probe blocks startup with the transientSecRandomTmpRootWindow/FATaskDialogwarning host. - Desktop builds copy
SecRandom/Assetsbeside the executable and bindOverlayAssetLoaderso existingavares://SecRandom/Assets/...URIs load physical files first, including Debug builds. Android and iOS keep the same assets embedded.Assets/Updates/release-public-key.txtis embedded on every target and must never be resolved from the physical overlay. - Portable ZIP updates use
SecRandom.Launcherat the stable package root and activatedapp-*payload directories. The application, not Launcher, validates/downloads/extracts/activates a complete ZIP;Utils.PackageRoot/DataRootkeep user data stable across payload versions. - Update discovery uses a fixed GitHub mirror first, with GitHub direct access as fallback. A detached Ed25519-signed release manifest and each artifact's length/SHA-512 must verify before deployment or a system installer is started; signing keys remain outside the repository. Desktop artifacts, the signed Android arm64 APK, and the unsigned iOS arm64 IPA share this manifest. Android downloads its verified APK then delegates installation to the system package installer; iOS device distribution and in-app update delivery remain deferred.
- Localization is per page folder:
Resources.resx,Resources.Designer.cs,Resources.en-US.resx, andResources.ja-JP.resx; every new or changed user-facing string must be supplied in Simplified Chinese, English, and Japanese, preserving exact filename casing used on disk. - In
.csproj, register onlyResources.resxandResources.Designer.cs; do not register every language variant. - Resource designer generator must be
PublicResXFileCodeGenerator. - Localization keys:
S_settings,S_xxx_Ddescription,S_xxx_Rreal key,O_options,M_messages,C_controls. - Chinese i18n values must not use the Chinese full stop (
。). - Settings-page explanation values (
*_D, includingS_*_DandC_*_D) must not use sentence-ending or sentence-separating full stops (。or.); preserve technical dots in file names, domains, process names, and version identifiers. - Views commonly set
DataContext = thisand exposeViewModel; bindings useViewModel.*. - ViewModels use CommunityToolkit MVVM (
ObservableRecipient,[ObservableProperty]); app VMs inheritSecRandom.ViewModels.ViewModelBase. Global.propscarries the main MSBuild behavior: unsafe enabled, Windows targeting enabled, SourceLink, full debug symbols, and default exclusion of project-localartifacts//publish/output trees from SDK item globbing. The Git-info analyzer remains available to all projects, while the sharedAssemblyInfo.csis enabled only by projects that explicitly setEnableGitInfoGenerator; current versioned assemblies areSecRandom.Core,SecRandom.Desktop,SecRandom.Android, andSecRandom.iOS.Directory.Build.propsonly pinsAvaloniaVersion.- Standalone verification scripts live under
scripts/; keep them self-contained and write outputs underartifacts/. - Release CI keeps generated material under
artifacts/release/: RID publish trees underpublish/, portable ZIP assembly underportable/, Windows installer staging underinstaller/andsetup/, platform package workspaces underlinux//macos/, upload candidates underdist/, and release-job downloads/final signed assets underdownloaded//output/. Every Android build must receive the same configured release keystore secrets, rejects missing signing configuration, and verifies the produced APK certificate against that keystore before upload. The Android job stages signed arm64 and x64 APKs separately; the release job includes the supported APK artifacts inoutput/, the signed manifest, and the GitHub release. Portable ZIP contents must remain a rootSecRandomLauncherplus one validapp-*payload directory; do not rearrange that runtime package contract. - More settings owns built-in draw page chrome options such as roll-call/lottery control panel position and control visibility; do not hard-code those controls outside the page/config binding.
- Timer recent presets are persisted as integer seconds in
MainConfigModel.RecentTimerPresetSeconds; the timer ViewModel removes duplicates, moves selected presets to the front, and caps the list at six entries. MoreSettings.LotteryEnabledis the single capability switch for lottery. It must consistently gate main navigation, floating-window buttons, shortcuts, and URL/IPC routes.- Application-owned Fluent System Icons use the
Filledvariant by default across navigation, settings, buttons, menus, floating windows, and empty states. Use the closest semanticFilledicon when no same-name variant exists. - New application icon references must use
FluentIcon,FluentIconSource,sr:Fi, orFluentIcons.*; do not add raw Fluent Unicode glyphs. When migrating a raw glyph, reverse-map its code point throughSecRandom.Core/Assets/FluentSystemIcons-Resizable.jsonbefore choosing the Filled replacement. - Brand images, author/organization images, window icons, and taskbar icons are separate resources and are not part of Fluent glyph style migration. FluentAvalonia template glyphs remain framework-owned unless an application-owned style explicitly overrides them.
- Keep comments that explain non-obvious project constraints, platform quirks, or AI-prone rules.
- Do not add comments that merely restate a method name or obvious assignment.
- Existing emphatic comments in
BuildHost()about ViewModel registration reflect a real project invariant; preserve the rule even if wording is later cleaned up.
AI agents must update AGENTS files when they:
- Add, remove, or rename top-level folders or projects.
- Change
TargetFramework, package family,AvaloniaVersion, CI SDK versions, or publish RIDs. - Change Host/DI registration, navigation page registration, page IDs, or default pages.
- Change localization folder layout, resource designer registration, or culture filename casing.
- Move draw/config/profile services or alter Core/Shared namespace boundaries.
- Discover a durable convention while fixing code that is not yet documented here.
- Remove stale facts encountered during work instead of leaving them for a later cleanup.
- Do not add ViewModels without Host registration in
BuildHost(). - Do not hardwire navigation menu items; register pages/groups and let registry/services build menus.
- Do not
newreusable services from pages; put reusable/singleton/testable services in Host. - Do not merge page localization into a shared resource bucket.
- Do not assume dictionary mutations auto-save config; call save after collection mutations or on unload.
- Do not use
Student.Id/Prize.Idas a required identity key; useProfileRecordIdentity/RecordIdand keep legacyId/Namehistory fallback ambiguity-safe. - Do not treat
SecRandom.Desktopas the app logic layer; it is only the launcher. - Do not edit
bin/,obj/,artifacts/,publish/, packaging scratch dirs, or generated build output. - Do not encode temporary local environment facts in AGENTS files; prefer durable rules that future AI agents can maintain.
- Avalonia compiled bindings are enabled in
SecRandom/SecRandom.csproj. SecRandom/Styles.axamlis only the app style entrypoint; shared styles live underSecRandom.Core/StylesBase.axamlandSecRandom.Core/Styles/.- Shared UI controls are in Core, not only app:
DrawerHost,Field,IconText,AppToastAdorner,AttachedSettingsControlPresenter,FluentIcon,FluentIconSource,StickyScrollViewer,TouchDragThumb,Empty,Emptiable,DevelopmentBuildAdorner. - UI uses FluentAvalonia
NavigationView,SettingsExpander,FluentIconSource, and custom selector classes (compact,nav-back,drawer-left/right,FullWidth). - Default font is MiSans from Avalonia resource URI
avares://SecRandom/Assets/Fonts/MiSans/#MiSans. - README/docs are Chinese-first with English/ZH-TW mirrors under
resources/.
dotnet restore SecRandom.sln
dotnet build SecRandom.sln -c Release --no-restore
dotnet test SecRandom.sln -c Release --no-build
dotnet run --project SecRandom.Desktop/SecRandom.Desktop.csproj
dotnet run --project scripts/FairnessAudit/SecRandom.FairnessAudit.csproj -c Release
dotnet publish SecRandom.Desktop/SecRandom.Desktop.csproj -c Release -r <rid> --self-contained true -o artifacts/SecRandom-<rid> /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=trueCI RIDs: win-x64, win-x86, win-arm64, linux-x64, linux-arm64, osx-x64, osx-arm64.
- Build workflow and CodeQL use .NET SDK
10.0.x. The iOS job onmacos-26pins Xcode26.6throughmaxim-lobanov/setup-xcode@v1; do not override that selection withDEVELOPER_DIR, and retain itsxcode-select/xcrun --find stripchecks to catch mixed toolchains. - In PowerShell release-workflow regex strings, delimit a variable immediately followed by a colon as
${variable}:;$variable:is parsed as a scoped-variable reference and makes the whole script fail to parse. - The desktop
Publish Desktopand UIAccess publish steps must rundotnet publishwith-p:BuildInParallel=false -p:UseSharedCompilation=falseand must check$LASTEXITCODEafter every publish. The two full/light publishes share the sameobj/tree, so parallel builds and the sharedVBCSCompilerrace intermittently withCS2012file-lock failures on Windows; a masked (unchecked) failure leavespublish/fullincomplete and breaks downstream portable/installer packaging with confusing "file does not exist" errors. Keep theVerify bundled audio runtimestep enabled so an incomplete self-contained publish is caught at its source instead of silently producing broken artifacts. - The mobile
Build Android APKandPublish unsigned iOS arm64 IPAsteps must also pass-p:BuildInParallel=false -p:UseSharedCompilation=false(and the iOS step runs withset -euo pipefail). The sharedSecRandom.Core/app graph is otherwise scheduled twice in parallel within a single build, racing onobj/Release/net10.0resource outputs withMSB3554/CS1566/MSB4018"being used by another process" failures on any platform. - Android release signing decodes
ANDROID_SIGNING_KEYSTORE_BASE64by trimming whitespace and translating URL-safe base64 (-→+,_→/) beforeConvert.FromBase64String, then validates the bytes withkeytool -listbefore invokingdotnet build. Do not strip non-base64 characters blindly: a junk character (beyond whitespace/URL-safe chars) means the secret value itself is malformed, and deleting it silently corrupts the keystore so apksigner later fails with the opaqueMSB6006: "java" exited with code 2. A decode failure must report the offending character's code point and require re-provisioning the secret, never a lossy strip.keytool -J-*options must be passed through array splatting (& keytool @args) — as bare-J-Duser.language=entokens PowerShell splits them into-J-Duser+.language=en. - Release workflow triggers on tags
v*, manual dispatch, PR/push build, or commit message containing开始构建. - Test project currently includes
UnitTest1.cscoverage for legacy privacy/telemetry migration behavior. vendors/EdgeTtsSharp/is the Edge TTS synthesis submodule. Its embedded voice list and synthesis source are compiled by the app;Services/Voice/EdgeTtsSharpCompatibility.cssupplies the cross-platform transport seam, and all playback remains in ClassIsland'sSoundFlowMiniAudio package from the repository-wide MyGet source.- README mentions
vendors/pythonnet-stub-generator/; treat it as third-party if present in future snapshots. - CodeQL workflow (
codeQL.yml) runs on push, PR, and weekly schedule; C# scans use manual build mode with .NET SDK10.0.x. - Keep
SecRandom.Corepublic contracts stable.