0.3.15-pre.012

This commit is contained in:
2026-09-16 10:58:29 +02:00
parent 2f587e22be
commit 31ae38853d
21 changed files with 815 additions and 79 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 621
# version: 622
[workspace]
resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
[workspace.package]
version = "0.3.15-pre.11.fix.1"
version = "0.3.15-pre.12"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
<!-- version: 13 -->
<!-- version: 14 -->
# `ksp-app-raw-transaction-ingest-desk`
@@ -31,11 +31,15 @@ Au Start, le backend revalide une sélection logique par `profile_id + route_id
Le runtime accepte désormais plusieurs routes simultanées sur un même réseau. Chaque route possède son Worker indépendant ; deux Starts de la même identité logique `profile_id + route_id` sont refusés, et un Start d'un autre réseau est refusé tant que le Store partagé reste ouvert. Le premier Worker ouvre et valide le Store, les suivants réutilisent le même `Arc<Store>`, puis la dernière route terminale déclenche seule la fermeture explicite du Store. Un fault d'une route ne stoppe pas les Workers siblings. Stop est ciblé par route et reste idempotent lorsqu'un terminal sûr a déjà été retenu.
Les races de contrôle sont fermées côté backend : un Stop qui rencontre une route encore `Starting` marque cette réservation pour arrêt coopératif et attend son cleanup au lieu de la considérer inactive. Dès qu'une fermeture de l'application commence, tout nouveau Start est refusé, les routes actives reçoivent Stop et les Starts déjà réservés sont marqués avant activation. La fenêtre principale ne quitte le processus qu'après cleanup borné des Workers et du Store partagé, ou avec un code d'échec après expiration de la borne.
Les profils applicatifs sont réseau-centriques (`devnet`, `mainnet`, `testnet`). Un profil peut agréger plusieurs profils Transport du même réseau : le provider reste une source de capability et ne devient jamais une identité réseau ni un choix de profil applicatif.
Une route explicitement liée à un provider nest projetée pour un réseau que si le composite déclare une source Transport de ce provider. Ainsi, la route Helius Transaction existe sur Devnet/Mainnet mais nest pas présentée sur Testnet tant quHelius ne fournit pas de source Testnet configurée.
Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`.
Les requêtes IPC Start/Stop sont shape-strict : les champs inconnus sont rejetés et `profile_id` est borné/validé avant toute reconstruction. Le bridge de logging frontend applique également des bornes strictes et refuse les caractères de contrôle avant émission dans la façade Logging.
## Routes V1
```text

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
@@ -36,12 +36,16 @@ Une route provider-specific dont aucune source nest déclarée pour le résea
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only.
Les commandes Start/Stop refusent les champs IPC inconnus et les identifiants logiques non bornés ou contenant des caractères de contrôle. Le logging frontend est lui aussi borné avant l'IPC ; une entrée hostile ne doit jamais être recopiée dans les diagnostics backend.
### Start/Stop multi-route
Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `Start`. Le backend revalide la génération d'inventaire et la composition Config, ouvre le Store correspondant lorsqu'aucun Store compatible n'est encore actif, exige son état `Ready`, construit le Worker exact puis installe son handle avant de renvoyer l'accusé runtime.
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
Un Stop peut être demandé même si le Start ciblé n'a pas encore fini d'ouvrir le Store ou d'installer le Worker : la réservation est alors marquée pour arrêt et le backend attend sa résolution. Lors de la fermeture de la fenêtre principale, le Desk ferme d'abord l'admission Start, demande l'arrêt de toutes les routes actives ou encore en démarrage et attend un cleanup borné avant de quitter. Il n'est donc pas nécessaire d'arrêter manuellement chaque route avant de fermer l'application.
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Le relais backend coalesce les changements à une cadence maximale d'une émission toutes les `500 ms` par route, et les changements steady-state mettent à jour la carte en place sans recréer les boutons Start/Stop. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/frontend_log.ts
// version: 1
// version: 2
import { invoke } from "@tauri-apps/api/core";
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/frontend_logging/FrontendLogPayloadDto.ts";
@@ -9,6 +9,8 @@ export type FrontendLogTargetId = "frontend" | "main" | "splash";
type ConsoleMethod = (...items: unknown[]) => void;
const FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024;
const originalConsole = {
trace: console.trace.bind(console),
debug: console.debug.bind(console),
@@ -37,11 +39,15 @@ function formatMessage(items: unknown[]): string {
return items.map(item => stringifyItem(item)).join(" ");
}
function boundMessage(message: string): string {
return message.length <= FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS ? message : `${message.slice(0, FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS)}`;
}
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
const payload: FrontendLogPayloadDto = {
level,
targetId,
message,
message: boundMessage(message),
};
await invoke("emit_frontend_log", { payload });
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
// version: 7
// version: 8
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
@@ -117,23 +117,26 @@ impl crate::AppState {
/// Starts one independent route Worker after repeating complete Start-time revalidation, sharing Store only on an exact network match.
pub(crate) async fn start_route(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
let generation = {
let generation = self.inventory_generation.lock();
match generation {
std::result::Result::Ok(value) => *value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
}
let admission = self.route_runtime.ensure_start_admission_open();
if let std::result::Result::Err(error) = admission {
return std::result::Result::Err(error);
}
let generation = self.inventory_generation.lock();
let generation = match generation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
));
},
};
let prepared = crate::prepare_route_start(&self.config_management, generation, request);
let prepared = crate::prepare_route_start(&self.config_management, *generation, request);
let prepared = match prepared {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
std::mem::drop(generation);
return crate::start_route_runtime(std::sync::Arc::clone(&self.route_runtime), prepared).await;
}
@@ -147,6 +150,16 @@ impl crate::AppState {
return self.route_runtime.stop_and_wait(request).await;
}
/// Closes route Start admission once and requests cooperative Stop for all routes during application shutdown.
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
return self.route_runtime.begin_shutdown();
}
/// Waits for bounded cleanup of all route Workers and the shared Store during application shutdown.
pub(crate) async fn shutdown_routes(&self) -> ksp_core_lib::Result<()> {
return self.route_runtime.shutdown_and_wait().await;
}
/// Builds the safe scaffold status exposed by the shell.
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
// version: 6
// version: 7
//! Safe route-identity, Config-composability and lifecycle DTOs for Raw Transaction Ingest Desk.
@@ -99,7 +99,7 @@ impl crate::RawIngestCommitment {
/// Safe request used to revalidate and reconstruct one future route Start without accepting physical resources from the frontend.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts")]
pub(crate) struct RawIngestRouteStartRequestDto {
/// Confirmed/finalized commitment selected for the future Worker.
@@ -114,7 +114,7 @@ pub(crate) struct RawIngestRouteStartRequestDto {
/// Safe targeted Stop request containing only the logical Worker route identity.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts")]
pub(crate) struct RawIngestRouteStopRequestDto {
/// Safe logical network-profile identifier that owns the Worker route.
@@ -123,6 +123,36 @@ pub(crate) struct RawIngestRouteStopRequestDto {
pub(crate) route_id: crate::RawIngestRouteId,
}
const RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256;
impl crate::RawIngestRouteStartRequestDto {
/// Validates the only caller-controlled free-form logical identifier before Config or runtime reconstruction.
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
return validate_profile_id(self.profile_id.as_str());
}
}
impl crate::RawIngestRouteStopRequestDto {
/// Validates the only caller-controlled free-form logical identifier before runtime lookup.
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
return validate_profile_id(self.profile_id.as_str());
}
}
fn validate_profile_id(profile_id: &str) -> ksp_core_lib::Result<()> {
if profile_id.is_empty()
|| profile_id.len() > RAW_INGEST_PROFILE_ID_MAX_BYTES
|| profile_id.trim() != profile_id
|| profile_id.chars().any(char::is_control)
{
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_REQUEST_INVALID,
"Raw Transaction Ingest Desk route request contains an invalid logical profile identifier",
));
}
return std::result::Result::Ok(());
}
/// Safe acknowledgement that exact Start-time Transport resources were reconstructed and validated without Worker launch.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
// version: 5
// version: 6
//! Stable Raw Transaction Ingest Desk application error codes.
@@ -17,6 +17,9 @@ pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_target_invalid");
/// Frontend logging payload exceeds the bounded technical IPC contract.
pub(crate) const ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_payload_invalid");
/// Raw Transaction Ingest Desk could not install managed Logging or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "logging_bootstrap_failed");
@@ -46,6 +49,15 @@ pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED: ksp_core_lib::E
/// The multi-route runtime reservation/handle state changed unexpectedly.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_state_invalid");
/// Application shutdown has closed admission for new route Worker starts.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutting_down");
/// Application shutdown could not join all route Workers and shared Store cleanup within the bounded deadline.
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutdown_failed");
/// A Start/Stop IPC request contains an invalid logical identifier or unsupported shape.
pub(crate) const ERROR_CODE_ROUTE_REQUEST_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_request_invalid");
/// Start request references an inventory generation that is no longer current.
pub(crate) const ERROR_CODE_ROUTE_START_STALE_INVENTORY: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "stale_inventory");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/frontend_logging.rs
// version: 1
// version: 2
//! KSP-owned bridge for technical log events emitted by Raw Transaction Ingest Desk frontend scripts.
@@ -7,7 +7,7 @@ use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Raw Transaction Ingest Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/frontend_logging/FrontendLogPayloadDto.ts")]
pub(crate) struct FrontendLogPayloadDto {
/// Lowercase KSP log level.
@@ -34,8 +34,15 @@ enum FrontendLogTarget {
Splash,
}
const FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192;
const FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
let shape = validate_payload_shape(&payload);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
let level = parse_level(payload.level.as_str());
let level = match level {
std::result::Result::Ok(value) => value,
@@ -50,26 +57,55 @@ pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_cor
return std::result::Result::Ok(());
}
fn validate_payload_shape(payload: &FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
if payload.level.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|| payload.target_id.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|| payload.message.len() > FRONTEND_LOG_MESSAGE_MAX_BYTES
|| payload.message.chars().any(char::is_control)
{
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID,
"Frontend log payload exceeds the bounded technical IPC contract",
));
}
return std::result::Result::Ok(());
}
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
return match level.trim().to_ascii_lowercase().as_str() {
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
_ => std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported")),
};
let level = level.trim();
if level.eq_ignore_ascii_case("trace") {
return std::result::Result::Ok(FrontendLogLevel::Trace);
}
if level.eq_ignore_ascii_case("debug") {
return std::result::Result::Ok(FrontendLogLevel::Debug);
}
if level.eq_ignore_ascii_case("info") {
return std::result::Result::Ok(FrontendLogLevel::Info);
}
if level.eq_ignore_ascii_case("warn") {
return std::result::Result::Ok(FrontendLogLevel::Warn);
}
if level.eq_ignore_ascii_case("error") {
return std::result::Result::Ok(FrontendLogLevel::Error);
}
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported"));
}
fn parse_target(target_id: &str) -> ksp_core_lib::Result<FrontendLogTarget> {
return match target_id.trim().to_ascii_lowercase().as_str() {
"frontend" => std::result::Result::Ok(FrontendLogTarget::Frontend),
"main" => std::result::Result::Ok(FrontendLogTarget::Main),
"splash" => std::result::Result::Ok(FrontendLogTarget::Splash),
_ => {
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID, "Frontend log target identifier is not supported"))
},
};
let target_id = target_id.trim();
if target_id.eq_ignore_ascii_case("frontend") {
return std::result::Result::Ok(FrontendLogTarget::Frontend);
}
if target_id.eq_ignore_ascii_case("main") {
return std::result::Result::Ok(FrontendLogTarget::Main);
}
if target_id.eq_ignore_ascii_case("splash") {
return std::result::Result::Ok(FrontendLogTarget::Splash);
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID,
"Frontend log target identifier is not supported",
));
}
fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
// version: 7
// version: 8
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
@@ -113,18 +113,26 @@ pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
/// Frontend logging requested an unsupported level.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
/// Frontend logging payload exceeds the bounded technical IPC contract.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID;
/// Frontend logging requested an unsupported target.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Managed and fallback Logging initialization failed.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Config-only route inventory cannot be projected safely.
pub(crate) use self::errors::ERROR_CODE_ROUTE_INVENTORY_INVALID;
/// Start/Stop IPC request contains an invalid logical shape.
pub(crate) use self::errors::ERROR_CODE_ROUTE_REQUEST_INVALID;
/// A Worker runtime already owns the same logical route in the shared application runtime.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_ACTIVE;
/// Selected route cannot share the currently open Store because its network differs.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH;
/// No selected route Worker runtime is currently active.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE;
/// Application shutdown could not complete all bounded route/Store cleanup.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED;
/// Application shutdown has closed admission for new route Worker starts.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN;
/// Store or Worker startup failed after Start-time route revalidation.
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_START_FAILED;
/// The multi-route runtime reservation or handle state changed unexpectedly.

View File

@@ -1,9 +1,10 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
// version: 6
// version: 7
//! Multi-route Store and independent Worker lifecycle owned by Raw Transaction Ingest Desk.
const ROUTE_STOP_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
const ROUTE_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
const ROUTE_STOP_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
const STORE_RECLAIM_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(5);
const STORE_RECLAIM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
@@ -13,6 +14,7 @@ struct RouteRuntimeInner {
network: std::option::Option<String>,
next_sequence: u64,
routes: std::vec::Vec<RouteRuntimeSlot>,
shutting_down: bool,
store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>,
store_closing_token: std::option::Option<u64>,
store_opening_token: std::option::Option<u64>,
@@ -34,6 +36,7 @@ impl crate::RouteRuntimeState {
network: std::option::Option::None,
next_sequence: 0,
routes: std::vec::Vec::new(),
shutting_down: false,
store: std::option::Option::None,
store_closing_token: std::option::Option::None,
store_opening_token: std::option::Option::None,
@@ -41,12 +44,34 @@ impl crate::RouteRuntimeState {
};
}
/// Rejects new route Start work after application shutdown has closed admission.
pub(crate) fn ensure_start_admission_open(&self) -> ksp_core_lib::Result<()> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
));
}
return std::result::Result::Ok(());
}
fn reserve(&self, prepared: &crate::PreparedRouteStart) -> ksp_core_lib::Result<RouteRuntimeReservation> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
));
}
if inner.store_closing_token.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
@@ -111,7 +136,7 @@ impl crate::RouteRuntimeState {
if needs_store_open {
inner.store_opening_token = std::option::Option::Some(sequence);
}
inner.routes.push(RouteRuntimeSlot::Starting { identity: identity.clone(), token: sequence });
inner.routes.push(RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: sequence });
return std::result::Result::Ok(RouteRuntimeReservation { identity, needs_store_open, token: sequence, worker_id });
}
@@ -173,7 +198,19 @@ impl crate::RouteRuntimeState {
));
},
};
let state = project_worker_state(handle.snapshot_source().current().worker_snapshot().state());
let stop_requested = match &inner.routes[position] {
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested,
RouteRuntimeSlot::Active { .. } => false,
};
let should_stop = stop_requested || inner.shutting_down;
if should_stop {
let _accepted = handle.request_stop();
}
let state = if should_stop {
crate::RawIngestRouteState::Stopping
} else {
project_worker_state(handle.snapshot_source().current().worker_snapshot().state())
};
inner.routes[position] = RouteRuntimeSlot::Active { handle, identity: reservation.identity.clone(), token: reservation.token };
return std::result::Result::Ok(reservation.identity.dto(state));
}
@@ -184,7 +221,12 @@ impl crate::RouteRuntimeState {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let cancelled_terminal = inner.routes.iter().find(|slot| return slot.token() == token).and_then(RouteRuntimeSlot::cancelled_start_terminal);
inner.routes.retain(|slot| return slot.token() != token);
if let std::option::Option::Some(terminal) = cancelled_terminal {
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
inner.last_terminals.push(terminal);
}
if inner.store_opening_token == std::option::Option::Some(token) {
inner.store_opening_token = std::option::Option::None;
}
@@ -272,8 +314,71 @@ impl crate::RouteRuntimeState {
return std::result::Result::Ok(values);
}
/// Closes route Start admission exactly once and requests cooperative Stop for all active or still-starting routes.
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
let inner = self.inner.lock();
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
if inner.shutting_down {
return std::result::Result::Ok(false);
}
inner.shutting_down = true;
for slot in &mut inner.routes {
match slot {
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested = true,
RouteRuntimeSlot::Active { handle, .. } => {
let _accepted = handle.request_stop();
},
}
}
return std::result::Result::Ok(true);
}
/// Waits until all owned route Workers and the shared Store have completed bounded application-shutdown cleanup.
pub(crate) async fn shutdown_and_wait(&self) -> ksp_core_lib::Result<()> {
let begin = self.begin_shutdown();
if let std::result::Result::Err(error) = begin {
return std::result::Result::Err(error);
}
let started = std::time::Instant::now();
loop {
let complete = self.shutdown_complete();
let complete = match complete {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if complete {
return std::result::Result::Ok(());
}
if started.elapsed() >= ROUTE_SHUTDOWN_TIMEOUT {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED,
"Raw Transaction Ingest Desk application shutdown did not release all route Workers and shared Store state before the bounded deadline",
));
}
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
}
}
fn shutdown_complete(&self) -> ksp_core_lib::Result<bool> {
let inner = self.inner.lock();
let inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
return std::result::Result::Ok(
inner.routes.is_empty() && inner.store.is_none() && inner.store_opening_token.is_none() && inner.store_closing_token.is_none(),
);
}
/// Requests cooperative Stop for one exact logical route and waits for its Worker cleanup.
pub(crate) async fn stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
let identity = request.validate_logical_identity();
if let std::result::Result::Err(error) = identity {
return std::result::Result::Err(error);
}
let target = self.stop_target(request);
let target = match target {
std::result::Result::Ok(value) => value,
@@ -281,6 +386,7 @@ impl crate::RouteRuntimeState {
};
let (handle, identity, token) = match target {
RouteStopTarget::Active { handle, identity, token } => (handle, identity, token),
RouteStopTarget::Starting { identity, token } => return self.wait_route_cleanup(token, &identity).await,
RouteStopTarget::Terminal(value) => return std::result::Result::Ok(value),
RouteStopTarget::TerminalClosing => return self.wait_terminal_store_cleanup(request).await,
};
@@ -296,33 +402,30 @@ impl crate::RouteRuntimeState {
fn stop_target(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<RouteStopTarget> {
let inner = self.inner.lock();
let inner = match inner {
let mut inner = match inner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
};
let slot = inner.routes.iter().find(|slot| return slot.matches_identity(request.profile_id.as_str(), request.route_id));
return match slot {
std::option::Option::Some(RouteRuntimeSlot::Active { handle, identity, token }) => {
std::result::Result::Ok(RouteStopTarget::Active { handle: handle.clone(), identity: identity.clone(), token: *token })
},
std::option::Option::Some(RouteRuntimeSlot::Starting { .. }) => std::result::Result::Err(ksp_core_lib::Error::new(
let position = inner.routes.iter().position(|slot| return slot.matches_identity(request.profile_id.as_str(), request.route_id));
if let std::option::Option::Some(position) = position {
return match &mut inner.routes[position] {
RouteRuntimeSlot::Active { handle, identity, token } => {
std::result::Result::Ok(RouteStopTarget::Active { handle: handle.clone(), identity: identity.clone(), token: *token })
},
RouteRuntimeSlot::Starting { identity, stop_requested, token } => {
*stop_requested = true;
std::result::Result::Ok(RouteStopTarget::Starting { identity: identity.clone(), token: *token })
},
};
}
let terminal = inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
return match terminal {
std::option::Option::Some(value) if inner.store_closing_token.is_none() => std::result::Result::Ok(RouteStopTarget::Terminal(value.clone())),
std::option::Option::Some(_) => std::result::Result::Ok(RouteStopTarget::TerminalClosing),
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
"Raw Transaction Ingest Desk route Worker is still starting and cannot be stopped yet",
"Raw Transaction Ingest Desk has no active Worker for the selected logical route",
)),
std::option::Option::None => {
let terminal =
inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
match terminal {
std::option::Option::Some(value) if inner.store_closing_token.is_none() => {
std::result::Result::Ok(RouteStopTarget::Terminal(value.clone()))
},
std::option::Option::Some(_) => std::result::Result::Ok(RouteStopTarget::TerminalClosing),
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
"Raw Transaction Ingest Desk has no active Worker for the selected logical route",
)),
}
},
};
}
@@ -668,6 +771,10 @@ enum RouteStopTarget {
identity: RouteRuntimeIdentity,
token: u64,
},
Starting {
identity: RouteRuntimeIdentity,
token: u64,
},
Terminal(crate::RawIngestRouteRuntimeDto),
TerminalClosing,
}
@@ -682,6 +789,7 @@ struct RouteRuntimeReservation {
enum RouteRuntimeSlot {
Starting {
identity: RouteRuntimeIdentity,
stop_requested: bool,
token: u64,
},
Active {
@@ -706,6 +814,13 @@ impl RouteRuntimeSlot {
};
}
fn cancelled_start_terminal(&self) -> std::option::Option<crate::RawIngestRouteRuntimeDto> {
return match self {
Self::Starting { identity, stop_requested: true, .. } => std::option::Option::Some(identity.dto(crate::RawIngestRouteState::Stopped)),
Self::Starting { stop_requested: false, .. } | Self::Active { .. } => std::option::Option::None,
};
}
fn token(&self) -> u64 {
return match self {
Self::Starting { token, .. } | Self::Active { token, .. } => *token,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
// version: 4
// version: 5
//! Start-time Config revalidation and exact Transport/Store resource reconstruction for route validation and runtime launch.
@@ -27,6 +27,10 @@ pub(crate) fn prepare_route_start(
current_generation: u32,
request: &crate::RawIngestRouteStartRequestDto,
) -> ksp_core_lib::Result<crate::PreparedRouteStart> {
let identity = request.validate_logical_identity();
if let std::result::Result::Err(error) = identity {
return std::result::Result::Err(error);
}
if current_generation == 0 || request.inventory_generation != current_generation {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
// version: 8
// version: 9
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
@@ -23,6 +23,7 @@ pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
builder = builder.manage(app_state);
builder = builder.plugin(tauri_plugin_tracing::Builder::new().build::<tauri::Wry>());
builder = configure_commands(builder);
builder = configure_window_events(builder);
builder = builder.setup(|app| {
let splash = crate::require_splash_window(app);
if let std::result::Result::Err(error) = splash {
@@ -90,6 +91,66 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
]);
}
fn configure_window_events(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.on_window_event(|window, event| {
if window.label() != "main" {
return;
}
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let app_handle = window.app_handle().clone();
let state = app_handle.state::<crate::AppState>();
let begin = state.begin_shutdown();
let begin = match begin {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk could not enter graceful shutdown; exiting after safe error projection"
);
app_handle.exit(1);
return;
},
};
if !begin {
return;
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
"Raw Transaction Ingest Desk main-window close requested; route Start admission closed and cooperative shutdown requested"
);
tauri::async_runtime::spawn(async move {
let state = app_handle.state::<crate::AppState>();
let shutdown = state.shutdown_routes().await;
match shutdown {
std::result::Result::Ok(()) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
"Raw Transaction Ingest Desk route and shared Store shutdown completed; exiting application"
);
app_handle.exit(0);
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Raw Transaction Ingest Desk bounded route shutdown failed; exiting application after bounded cleanup attempt"
);
app_handle.exit(1);
},
}
});
}
});
}
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
// version: 9
// version: 10
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
@@ -274,3 +274,36 @@ fn pre_011_fix_001_monitoring_bridge_is_bounded_and_steady_updates_preserve_rout
assert!(main.contains(required), "missing pre.011-fix.001 stable-control marker {required}");
}
}
#[test]
fn pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned() {
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
for required in [
"configure_window_events(builder)",
"tauri::WindowEvent::CloseRequested",
"api.prevent_close()",
"state.begin_shutdown()",
"state.shutdown_routes().await",
"app_handle.exit(0)",
"app_handle.exit(1)",
] {
assert!(tauri.contains(required), "missing pre.012 bounded shutdown marker {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
for required in [
"shutting_down: bool",
"stop_requested: bool",
"ROUTE_SHUTDOWN_TIMEOUT",
"ensure_start_admission_open",
"shutdown_and_wait",
"RouteStopTarget::Starting",
"*stop_requested = true",
"handle.request_stop()",
] {
assert!(runtime.contains(required), "missing pre.012 route-race marker {required}");
}
let app_state = read_text(app_root().join("src/app_state.rs").as_path());
assert!(app_state.contains("let generation = self.inventory_generation.lock()"));
assert!(app_state.contains("std::mem::drop(generation)"));
assert!(app_state.contains("self.route_runtime.ensure_start_admission_open()"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
// version: 8
// version: 9
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
@@ -195,3 +195,31 @@ fn pre_011_frontend_monitoring_tracing_is_source_neutral_and_does_not_log_raw_ma
assert!(!main.contains("frontendTrace(\"main\", \"Raw Transaction Ingest Desk route monitoring latest value applied\", status"));
assert!(!main.contains("frontendDebug(\"main\", \"Raw Transaction Ingest Desk route monitoring resynchronization completed\", statuses"));
}
#[test]
fn pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected() {
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
assert!(dto.matches("deny_unknown_fields").count() >= 2);
assert!(dto.contains("RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256"));
assert!(dto.contains("validate_logical_identity"));
assert!(dto.contains("ERROR_CODE_ROUTE_REQUEST_INVALID"));
let frontend_logging = read_text(app_root().join("src/frontend_logging.rs").as_path());
for required in [
"deny_unknown_fields",
"FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192",
"FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16",
"payload.message.chars().any(char::is_control)",
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
"eq_ignore_ascii_case",
] {
assert!(frontend_logging.contains(required), "missing pre.012 frontend IPC guard {required}");
}
assert!(!frontend_logging.contains("to_ascii_lowercase()"));
let frontend = read_text(app_root().join("frontend/ts/frontend_log.ts").as_path());
assert!(frontend.contains("FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024"));
assert!(frontend.contains("boundMessage(message)"));
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
assert!(tauri.contains("error_domain = error.code().domain()"));
assert!(tauri.contains("error_code = error.code().code()"));
assert!(!tauri.contains("error = ?error"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
// version: 11
// version: 12
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
@@ -200,3 +200,27 @@ fn pre_011_fix_001_preserves_latest_value_contract_without_backend_module_growth
assert!(main.contains("renderRouteMonitoringCard"));
assert!(main.contains("renderSelectedProfile"));
}
#[test]
fn pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth() {
let lib = read_text(app_root().join("src/lib.rs").as_path());
assert_eq!(lib.matches("mod ").count(), 16);
for required in [
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
"ERROR_CODE_ROUTE_REQUEST_INVALID",
"ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN",
"ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED",
] {
assert!(lib.contains(required), "missing pre.012 crate-root error export {required}");
}
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
assert!(runtime.contains("stop_requested: bool"));
assert!(runtime.contains("RouteStopTarget::Starting"));
assert!(runtime.contains("cancelled_start_terminal"));
assert!(runtime.contains("inner.routes.is_empty()"));
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
assert!(route_start.contains("build_route_inventory_with_environment"));
assert!(route_start.contains("request.validate_logical_identity()"));
let worker_manifest = read_text(app_root().join("../ksp-worker-raw-transaction-ingest-lib/Cargo.toml").as_path());
assert!(!worker_manifest.contains("ksp-app-raw-transaction-ingest-desk"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
// version: 4
// version: 5
#[test]
fn pre_005_route_ids_are_exact_and_stably_ordered() {
@@ -91,3 +91,34 @@ fn pre_008_runtime_acknowledgement_serializes_only_safe_logical_identity_and_sta
}
}
}
#[test]
fn pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity() {
let start = serde_json::from_value::<crate::RawIngestRouteStartRequestDto>(serde_json::json!({
"commitment": "confirmed",
"inventoryGeneration": 1,
"profileId": "mainnet",
"routeId": "http-block-polling",
"endpointUrl": "https://forbidden.invalid"
}));
assert!(start.is_err());
let stop = serde_json::from_value::<crate::RawIngestRouteStopRequestDto>(serde_json::json!({
"profileId": "mainnet",
"routeId": "http-block-polling",
"workerId": "forbidden"
}));
assert!(stop.is_err());
let oversized = crate::RawIngestRouteStartRequestDto {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 1,
profile_id: "x".repeat(super::RAW_INGEST_PROFILE_ID_MAX_BYTES + 1),
route_id: crate::RawIngestRouteId::HttpBlockPolling,
};
let oversized = oversized.validate_logical_identity();
assert!(oversized.is_err());
if let std::result::Result::Err(error) = oversized {
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_REQUEST_INVALID);
}
let control = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet\n".to_owned(), route_id: crate::RawIngestRouteId::HttpBlockPolling };
assert!(control.validate_logical_identity().is_err());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/frontend_logging.rs
// version: 1
// version: 2
#[test]
fn frontend_log_levels_are_explicit_and_case_insensitive() {
@@ -38,3 +38,26 @@ fn arbitrary_frontend_log_target_is_rejected() {
};
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
}
#[test]
fn pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages() {
let unknown = serde_json::from_value::<crate::FrontendLogPayloadDto>(serde_json::json!({
"level": "info",
"targetId": "main",
"message": "safe",
"endpointUrl": "https://forbidden.invalid"
}));
assert!(unknown.is_err());
let oversized = crate::FrontendLogPayloadDto {
level: "info".to_owned(),
target_id: "main".to_owned(),
message: "x".repeat(super::FRONTEND_LOG_MESSAGE_MAX_BYTES + 1),
};
let result = super::validate_payload_shape(&oversized);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID);
}
let nul = crate::FrontendLogPayloadDto { level: "info".to_owned(), target_id: "main".to_owned(), message: "safe\0hostile".to_owned() };
assert!(super::validate_payload_shape(&nul).is_err());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
// version: 3
// version: 4
#[test]
fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material() {
@@ -63,3 +63,70 @@ fn pre_009_late_targeted_stop_waits_while_final_shared_store_is_closing() {
let target = state.stop_target(&request);
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::TerminalClosing)));
}
#[test]
fn pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop() {
let state = crate::RouteRuntimeState::new();
let identity = super::RouteRuntimeIdentity {
commitment: crate::RawIngestCommitment::Confirmed,
inventory_generation: 12,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::YellowstoneHydrated,
};
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.network = std::option::Option::Some("mainnet".to_owned());
inner.routes.push(super::RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: 12 });
}
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::YellowstoneHydrated };
let target = state.stop_target(&request);
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::Starting { token: 12, .. })));
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
}
let rollback = state.rollback(12);
assert!(matches!(rollback, std::result::Result::Ok(std::option::Option::None)));
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(inner.routes.is_empty());
assert!(matches!(inner.last_terminals.first(), std::option::Option::Some(value) if value.state == crate::RawIngestRouteState::Stopped));
}
}
#[test]
fn pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation() {
let state = crate::RouteRuntimeState::new();
let identity = super::RouteRuntimeIdentity {
commitment: crate::RawIngestCommitment::Finalized,
inventory_generation: 13,
network: "mainnet".to_owned(),
profile_id: "mainnet".to_owned(),
route_id: crate::RawIngestRouteId::HttpBlockPolling,
};
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(mut inner) = inner {
inner.network = std::option::Option::Some("mainnet".to_owned());
inner.routes.push(super::RouteRuntimeSlot::Starting { identity, stop_requested: false, token: 13 });
}
let first = state.begin_shutdown();
assert!(matches!(first, std::result::Result::Ok(true)));
let second = state.begin_shutdown();
assert!(matches!(second, std::result::Result::Ok(false)));
let admission = state.ensure_start_admission_open();
assert!(admission.is_err());
if let std::result::Result::Err(error) = admission {
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN);
}
let inner = state.inner.lock();
assert!(inner.is_ok());
if let std::result::Result::Ok(inner) = inner {
assert!(inner.shutting_down);
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
}
}

177
deltas/0.3.15/pre.012.md Normal file
View File

@@ -0,0 +1,177 @@
<!-- file: deltas/0.3.15/pre.012.md -->
<!-- version: 1 -->
# Delta `0.3.15-pre.012`
## Base
```text
base archive : ksp-general-0.3.15-pre.011-fix.001.zip
base SHA-256 : 5da2d7bf343d20354b941ef9158004c3dabf521251bb346ef3aaf20586342a04
base version : 0.3.15-pre.11.fix.1
```
Le gate opérateur du 16 septembre 2026 confirme la base : audits Rust/Markdown, `cargo check --workspace`, Clippy strict, tests Raw Transaction Ingest Desk/Worker et workspace all-targets/all-features passent. Le live Mainnet valide aussi plusieurs cycles Yellowstone + HTTP Block Polling simultanés avec Stop ciblé dans les deux ordres et fermeture du Store seulement après la dernière route.
## Objectif
Fermer les races Start/Stop, stale inventory, shutdown applicatif et les surfaces IPC hostiles sans modifier Config, Transport, Store, Worker, Common RAW ni les stratégies d'acquisition.
## Race Start / Stop
Une route réservée mais pas encore activée possède maintenant un marqueur `stop_requested`.
```text
Stop sur Active -> request_stop Worker puis attente terminal/cleanup
Stop sur Starting -> stop_requested=true puis attente cleanup
Start active après Stop -> request_stop avant acknowledgement
Start échoue avant activation -> rollback retient terminal Stopped
```
La route n'est donc plus faussement déclarée `not_active` pendant sa fenêtre de démarrage. Le timeout de cleanup Desk est porté à `15 s`, au-dessus du drain Worker borné, afin de ne pas transformer un arrêt coopératif encore valide en erreur Desk prématurée.
## Shutdown applicatif
`RouteRuntimeState` possède désormais un état `shutting_down` one-shot. Le `CloseRequested` de la fenêtre principale est intercepté :
```text
prevent_close
begin_shutdown
fermeture admission Start
marquage des Starting
request_stop de tous les Active
attente bornée routes + Store
exit(0) si cleanup complet
exit(1) si échec/timeout sûr
```
Un second événement de fermeture pendant ce cycle ne lance pas une seconde séquence concurrente. L'admission shutdown est vérifiée avant la préparation Start puis atomiquement lors de la réservation runtime, ce qui ferme la race entre un Start déjà en vol et la fermeture de l'application.
## Stale inventory
Le Start conserve le verrou de génération pendant toute la préparation synchrone qui revalide l'inventaire et reconstruit Config/Transport. Le verrou est explicitement libéré avant le premier `await` d'ouverture Store. Un refresh d'inventaire ne peut donc pas intercaler une nouvelle génération entre le contrôle de génération et cette reconstruction.
## Sécurité IPC
Les requêtes Start/Stop sont maintenant `deny_unknown_fields` et valident `profile_id` avant reconstruction :
```text
non vide
<= 256 octets
aucun trim implicite
aucun caractère de contrôle
```
Le bridge frontend Logging est également shape-strict et borné :
```text
level / target <= 16 octets
message <= 8192 octets
aucun caractère de contrôle
aucune allocation lowercase du selector hostile
```
Le frontend borne le message technique à `1024` code units avant IPC, ce qui laisse une marge sûre pour l'encodage UTF-8. Les erreurs Tauri restent projetées uniquement par `domain/code` ; les payloads hostiles ne sont jamais journalisés.
## Canaris
Ajouts/renforcements :
```text
pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity
pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages
pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop
pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation
pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned
pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected
pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth
```
Le canari de race Start/Stop vérifie aussi que le rollback d'une réservation déjà stoppée retient un terminal logique `Stopped`.
## Frontières inchangées
Aucune modification de :
```text
ksp-config-lib
ksp-onchain-transport-lib
ksp-store-lib / backend Store
ksp-worker-raw-transaction-ingest-lib
ksp-raw-transaction-lib
pipeline RAW / replay / repair
```
Le module Rust de production du Desk reste à `16` modules et aucun nouvel edge lower-layer n'est introduit.
## Version
```text
workspace.package.version : 0.3.15-pre.12
root Cargo header counter : 622
```
## Validation d'assemblage
Exécuté dans l'environnement d'assemblage :
```text
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.15
canaris statiques pre.012 de surface/race/IPC
TypeScript auxiliary no-emit du frontend_log avec bindings temporaires
```
Cargo/rustfmt ne sont pas disponibles dans cet environnement ; aucun gate Cargo de `pre.012` n'est déclaré PASS ici.
Le gate opérateur attendu reste :
```bash
cargo fmt --all
cargo fmt --all -- --check
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.15
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features
cargo test -p ksp-worker-raw-transaction-ingest-lib --all-targets --all-features
cargo test --workspace --all-targets --all-features
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
```
Aucun `npm run build` manuel n'est demandé.
## Inventaire exact du delta
Ajout :
```text
deltas/0.3.15/pre.012.md
```
Modifications :
```text
Cargo.toml
crates/ksp-app-raw-transaction-ingest-desk/README.md
crates/ksp-app-raw-transaction-ingest-desk/USAGE.md
crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/frontend_log.ts
crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
crates/ksp-app-raw-transaction-ingest-desk/src/frontend_logging.rs
crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/frontend_logging.rs
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md
docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md
```
Suppressions : aucune.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md -->
<!-- version: 19 -->
<!-- version: 20 -->
# Plan v0.3.15 — Raw Transaction Ingest Desk
@@ -835,7 +835,15 @@ Budget cible : une tranche frontend.
### `pre.012` — races / shutdown / sécurité IPC
Durcir stale inventory, Start/Stop concurrents, fermeture application, joins bornés, erreurs sûres, surfaces IPC et scénarios hostiles.
**État : implémenté.** Le Desk ferme désormais explicitement l'admission de nouveaux Start dès qu'une fermeture de la fenêtre principale est engagée. La fermeture Tauri est interceptée une seule fois, demande un Stop coopératif à toutes les routes actives, marque aussi les réservations encore en `Starting`, puis attend de façon bornée la disparition de tous les Workers et du Store partagé avant de quitter le processus. Un second événement de fermeture ne lance pas un second shutdown concurrent.
La race Start/Stop est désormais définie : un Stop ciblé reçu pendant `Starting` marque la réservation `stop_requested` au lieu de répondre `not_active`. Si le Worker est ensuite activé, son Stop est demandé avant publication de l'acknowledgement ; si le Start échoue avant activation, le rollback conserve un terminal logique `Stopped`, ce qui permet au Stop concurrent de terminer sans inventer de Worker. Les délais de cleanup Desk sont alignés sur les drains Worker bornés afin de ne pas signaler prématurément un échec pendant un arrêt coopératif valide.
La revalidation Start conserve la génération d'inventaire sous verrou pendant toute la reconstruction synchrone Config/Transport, puis libère explicitement ce verrou avant le premier `await`. L'admission shutdown est vérifiée avant la préparation et de nouveau atomiquement lors de la réservation runtime ; un Start déjà en vol ne peut donc pas franchir silencieusement une fermeture applicative.
Les DTO Start/Stop refusent maintenant les champs JSON inconnus et bornent le seul identifiant libre `profile_id`. Le bridge de logging frontend refuse également les champs inconnus, borne niveau/target/message avant parsing et rejette les caractères de contrôle ; le frontend borne lui-même le message technique avant IPC avec une marge sûre vis-à-vis de l'encodage UTF-8. Les erreurs Tauri continuent à journaliser uniquement `domain/code`, jamais le payload hostile ni une ressource physique.
Aucun module de production supplémentaire, aucune dépendance lower-layer et aucune seconde pipeline ne sont introduits. Les comportements Config, Transport, Store, Worker et Common RAW restent inchangés.
Budget cible : une tranche hardening.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
<!-- version: 21 -->
<!-- version: 22 -->
# Validation v0.3.15 — Raw Transaction Ingest Desk
@@ -743,3 +743,55 @@ pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth
```
Aucune modification Config, Transport, Store, Worker ou Common RAW n'est introduite par `pre.011`. Le module Rust de production du Desk reste exactement celui de `pre.010`; la tranche suivante `pre.012` reste dédiée aux races/shutdown/sécurité IPC.
### `pre.012` — races Start/Stop, shutdown applicatif et sécurité IPC
Base : `0.3.15-pre.011-fix.001`, validée par le gate opérateur du 16 septembre 2026. Les audits, `cargo check --workspace`, Clippy strict, les suites Desk/Worker/workspace et le scénario live multi-route passent ; Yellowstone et HTTP Block Polling restent manipulables simultanément sous charge et le Store partagé ferme seulement après la dernière route.
Périmètre `pre.012` : hardening du contrôle Desk uniquement, sans modification des stratégies d'acquisition.
```text
shutdown application
CloseRequested main window intercepté et empêché temporairement
admission de nouveaux Start fermée une seule fois
Stop coopératif demandé à tous les Workers actifs
réservations Starting marquées stop_requested
attente bornée routes + Store avant app_handle.exit
second CloseRequested n'engendre aucun second shutdown
race Start / Stop
Stop sur Starting => marque stop_requested puis attend le cleanup
activation ultérieure => request_stop avant acknowledgement
échec avant activation => rollback retient un terminal logique Stopped
cleanup Desk borné et compatible avec le drain Worker borné
stale inventory / Start
génération sous verrou pendant la préparation synchrone
reconstruction complète Config/Transport à chaque Start
verrou libéré explicitement avant le premier await
admission shutdown revérifiée atomiquement pendant reserve
IPC hostile
StartRequest / StopRequest : deny_unknown_fields
profile_id : non vide, <= 256 octets, sans trim implicite ni contrôle
FrontendLogPayload : deny_unknown_fields
level / target <= 16 octets
message <= 8192 octets et sans caractères de contrôle
frontend : message technique borné à 1024 code units avant IPC
erreurs Tauri : projection domain/code uniquement
```
Canaris ajoutés/étendus :
```text
pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity
pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages
pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop
pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation
pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned
pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected
pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth
```
Non-claims : `pre.012` ne modifie ni Config, ni Transport, ni Store, ni `ksp-worker-raw-transaction-ingest-lib`, ni Common RAW. Il n'ajoute aucune orchestration Backfill, aucun scheduler, aucune route et aucune nouvelle donnée frontend. `pre.013` reste propriétaire de la fermeture completeness/security cross-layer.