From 9ec4f26874be12267955c1cf64979a45a3b59130 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 29 Aug 2026 22:00:20 +0200 Subject: [PATCH] v0.3.2-pre.009 --- Cargo.toml | 4 +- .../tests/hardening_completeness.rs | 256 ++++++++++++++++++ .../tests/hardening_completeness.rs | 220 +++++++++++++++ deltas/0.3.2/pre.009.md | 157 +++++++++++ ...3-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md | 54 +++- .../019-V0_3_2_STORE_POSTGRES_FOUNDATION.md | 97 ++++--- 6 files changed, 753 insertions(+), 35 deletions(-) create mode 100644 crates/ksp-store-lib/tests/hardening_completeness.rs create mode 100644 crates/ksp-store-postgres-lib/tests/hardening_completeness.rs create mode 100644 deltas/0.3.2/pre.009.md diff --git a/Cargo.toml b/Cargo.toml index ff06c48..6255609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 342 +# version: 343 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.2-pre.8" +version = "0.3.2-pre.9" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-lib/tests/hardening_completeness.rs b/crates/ksp-store-lib/tests/hardening_completeness.rs new file mode 100644 index 0000000..af9a993 --- /dev/null +++ b/crates/ksp-store-lib/tests/hardening_completeness.rs @@ -0,0 +1,256 @@ +// file: crates/ksp-store-lib/tests/hardening_completeness.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Hardening, exact-surface and feature-matrix canaries for the common Store facade. + +const SECRET_CANARY: &str = "KSP-STORE-SECRET-CANARY-PRE009"; + +fn poll_ready(future: impl std::future::Future) -> T { + let mut future = std::boxed::Box::pin(future); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + return match std::future::Future::poll(future.as_mut(), &mut context) { + std::task::Poll::Ready(value) => value, + std::task::Poll::Pending => panic!("Store hardening pre-I/O rejection unexpectedly became pending"), + }; +} + +fn valid_network() -> ksp_store_lib::RawNetworkId { + return match ksp_store_lib::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid hardening network rejected: {error:?}"), + }; +} + +fn hostile_settings(connection_uri: &str) -> ksp_store_lib::StoreSettings { + let postgres = ksp_store_lib::PostgresStoreSettings::new( + connection_uri, + ksp_store_lib::PostgresPoolSettings::default(), + ksp_store_lib::PostgresTlsMode::VerifyFull, + ksp_store_lib::PostgresBootstrapSettings::default(), + ); + return ksp_store_lib::StoreSettings::with_default_shutdown(valid_network(), ksp_store_lib::StoreBackendSettings::Postgres(postgres)); +} + +fn public_reexport_names(source: &str) -> std::vec::Vec<&str> { + let mut names = std::vec::Vec::new(); + for line in source.lines() { + let trimmed = line.trim(); + if !trimmed.starts_with("pub use ") || !trimmed.ends_with(';') { + continue; + } + let without_semicolon = trimmed.trim_end_matches(';'); + let name = match without_semicolon.rsplit("::").next() { + std::option::Option::Some(value) => value, + std::option::Option::None => continue, + }; + names.push(name); + } + names.sort_unstable(); + return names; +} + +fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> { + let dependencies_tail = match source.split("[dependencies]").nth(1) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::vec::Vec::new(), + }; + let dependencies = match dependencies_tail.split("[lints]").next() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::vec::Vec::new(), + }; + let mut names = std::vec::Vec::new(); + for line in dependencies.lines() { + let content = match line.split('#').next() { + std::option::Option::Some(value) => value.trim(), + std::option::Option::None => continue, + }; + if content.is_empty() { + continue; + } + let key = match content.split('=').next() { + std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"), + std::option::Option::None => continue, + }; + if !key.is_empty() { + names.push(key); + } + } + names.sort_unstable(); + return names; +} + +#[test] +fn pre_009_facade_modules_and_crate_root_exports_are_exact() { + let crate_root = include_str!("../src/lib.rs"); + for required in ["mod constants;", "mod error;", "mod health;", "mod settings;", "mod store;"] { + assert!(crate_root.contains(required), "missing Store facade module: {required}"); + } + assert!(!crate_root.contains("pub mod ")); + let actual = public_reexport_names(crate_root); + let mut expected = [ + "ERROR_CODE_BACKEND_CLOSED", + "ERROR_CODE_BACKEND_NOT_COMPILED", + "ERROR_CODE_BACKEND_OPEN_FAILED", + "ERROR_CODE_POSTGRES_CONFIG_INVALID", + "ERROR_CODE_POSTGRES_CONNECT_FAILED", + "ERROR_CODE_POSTGRES_HEALTH_FAILED", + "ERROR_CODE_POSTGRES_MIGRATION_FAILED", + "ERROR_CODE_POSTGRES_MIGRATION_MISMATCH", + "ERROR_CODE_POSTGRES_POOL_TIMEOUT", + "ERROR_CODE_POSTGRES_SCHEMA_NEWER", + "ERROR_CODE_POSTGRES_TLS_FAILED", + "ERROR_CODE_RAW_CONFLICT", + "ERROR_CODE_RAW_MODEL_INVALID", + "ERROR_CODE_RAW_PAYLOAD_INVALID", + "ERROR_CODE_RAW_PROVENANCE_INVALID", + "ERROR_CODE_RAW_QUERY_INVALID", + "ERROR_CODE_RAW_RETENTION_INVALID", + "ERROR_CODE_SETTINGS_INVALID", + "ERROR_CODE_SHUTDOWN_TIMEOUT", + "Error", + "ErrorCode", + "ErrorContext", + "MAX_RAW_ACCOUNT_DATA_BYTES", + "MAX_RAW_CODE_BYTES", + "MAX_RAW_PAGE_CURSOR_BYTES", + "MAX_RAW_PAYLOAD_BYTES", + "MAX_RAW_SOURCE_PAYLOAD_BYTES", + "MAX_RAW_UNIX_MILLIS", + "PostgresBootstrapSettings", + "PostgresPoolSettings", + "PostgresStoreSettings", + "PostgresTlsMode", + "Pubkey", + "RawAccountObservation", + "RawAccountObservationRead", + "RawAccountObservationWrite", + "RawAccountState", + "RawAccountStateQuery", + "RawAccountStateRead", + "RawAccountStateReference", + "RawAccountStateWrite", + "RawAcquisitionOrigin", + "RawAcquisitionProvenance", + "RawAcquisitionWriteOutcome", + "RawContentHash", + "RawEntityWriteOutcome", + "RawFormatId", + "RawNetworkId", + "RawObservationKey", + "RawObservationWriteOutcome", + "RawPage", + "RawPageCursor", + "RawPageLimit", + "RawPageRequest", + "RawPayload", + "RawProvenanceCode", + "RawRetentionState", + "RawRetentionWriteOutcome", + "RawSlotRange", + "RawSortDirection", + "RawTimestamp", + "RawTransaction", + "RawTransactionAcquisitionMode", + "RawTransactionObservation", + "RawTransactionObservationRead", + "RawTransactionObservationWrite", + "RawTransactionQuery", + "RawTransactionRead", + "RawTransactionReference", + "RawTransactionRetentionRead", + "RawTransactionRetentionTransition", + "RawTransactionRetentionWrite", + "RawTransactionSignature", + "RawTransactionTombstone", + "RawTransactionWrite", + "Result", + "Store", + "StoreApiFuture", + "StoreBackendKind", + "StoreBackendSettings", + "StoreHealthSnapshot", + "StoreHealthState", + "StoreRuntimeSnapshot", + "StoreSettings", + ]; + expected.sort_unstable(); + assert_eq!(actual.as_slice(), expected.as_slice()); + assert_eq!(actual.len(), 84); + return; +} + +#[test] +fn pre_009_facade_manifest_and_feature_contract_are_exact() { + let manifest = include_str!("../Cargo.toml"); + assert!(manifest.contains("default = [\"postgres\"]")); + assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]")); + let actual = manifest_dependency_names(manifest); + let expected = ["ksp-logging-lib", "ksp-store-api", "ksp-store-postgres-lib"]; + assert_eq!(actual.as_slice(), expected.as_slice()); + for forbidden in ["tokio-postgres", "deadpool-postgres", "rustls", "sqlx", "ksp-config-lib", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib"] { + assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}"); + } + return; +} + +#[test] +fn pre_009_secret_canary_never_crosses_settings_or_pre_io_error_debug() { + let malformed = std::format!("not-a-postgresql-uri-{SECRET_CANARY}"); + let settings = hostile_settings(malformed.as_str()); + let rendered = std::format!("{settings:?}"); + assert!(!rendered.contains(SECRET_CANARY)); + assert!(rendered.contains("")); + let result = poll_ready(ksp_store_lib::Store::open(settings)); + let error = match result { + std::result::Result::Err(value) => value, + std::result::Result::Ok(_) => panic!("hostile Store settings unexpectedly opened"), + }; + #[cfg(feature = "postgres")] + assert_eq!(error.code(), ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID); + #[cfg(not(feature = "postgres"))] + assert_eq!(error.code(), ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED); + assert!(!std::format!("{error}").contains(SECRET_CANARY)); + assert!(!std::format!("{error:?}").contains(SECRET_CANARY)); + return; +} + +#[test] +fn pre_009_facade_production_sources_keep_config_env_physical_sql_and_backend_handles_out() { + let production = std::format!( + "{} +{} +{} +{}", + include_str!("../src/error.rs"), + include_str!("../src/health.rs"), + include_str!("../src/settings.rs"), + include_str!("../src/store.rs") + ); + for forbidden in [ + "ksp_config_lib", + "std::env", + "dotenv", + "PGHOST", + "PGPORT", + "PGUSER", + "PGPASSWORD", + ".pgpass", + "tokio_postgres::Client", + "tokio_postgres::Row", + "tokio_postgres::Statement", + "deadpool_postgres::Pool", + "sqlx::", + "CREATE TABLE", + "INSERT INTO", + "UPDATE ", + "DELETE FROM", + ] { + assert!(!production.contains(forbidden), "forbidden facade ownership/runtime material detected: {forbidden}"); + } + return; +} diff --git a/crates/ksp-store-postgres-lib/tests/hardening_completeness.rs b/crates/ksp-store-postgres-lib/tests/hardening_completeness.rs new file mode 100644 index 0000000..8eb7d39 --- /dev/null +++ b/crates/ksp-store-postgres-lib/tests/hardening_completeness.rs @@ -0,0 +1,220 @@ +// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Hardening, exact-surface and scope canaries for the physical PostgreSQL Store backend. + +const SECRET_CANARY: &str = "KSP-POSTGRES-SECRET-CANARY-PRE009"; + +fn poll_ready(future: impl std::future::Future) -> T { + let mut future = std::boxed::Box::pin(future); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + return match std::future::Future::poll(future.as_mut(), &mut context) { + std::task::Poll::Ready(value) => value, + std::task::Poll::Pending => panic!("PostgreSQL hardening pre-I/O rejection unexpectedly became pending"), + }; +} + +fn network() -> ksp_store_api::RawNetworkId { + return match ksp_store_api::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid backend hardening network rejected: {error:?}"), + }; +} + +fn settings(connection_uri: &str, tls_mode: ksp_store_postgres_lib::PostgresBackendTlsMode) -> ksp_store_postgres_lib::PostgresBackendSettings { + return ksp_store_postgres_lib::PostgresBackendSettings::new( + network(), + connection_uri, + 8, + std::time::Duration::from_secs(10), + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(10), + std::time::Duration::from_secs(5), + tls_mode, + true, + std::time::Duration::from_secs(30), + std::time::Duration::from_secs(10), + ); +} + +fn public_reexport_names(source: &str) -> std::vec::Vec<&str> { + let mut names = std::vec::Vec::new(); + for line in source.lines() { + let trimmed = line.trim(); + if !trimmed.starts_with("pub use ") || !trimmed.ends_with(';') { + continue; + } + let without_semicolon = trimmed.trim_end_matches(';'); + let name = match without_semicolon.rsplit("::").next() { + std::option::Option::Some(value) => value, + std::option::Option::None => continue, + }; + names.push(name); + } + names.sort_unstable(); + return names; +} + +fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> { + let dependencies_tail = match source.split("[dependencies]").nth(1) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::vec::Vec::new(), + }; + let dependencies = match dependencies_tail.split("[lints]").next() { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::vec::Vec::new(), + }; + let mut names = std::vec::Vec::new(); + for line in dependencies.lines() { + let content = match line.split('#').next() { + std::option::Option::Some(value) => value.trim(), + std::option::Option::None => continue, + }; + if content.is_empty() { + continue; + } + let key = match content.split('=').next() { + std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"), + std::option::Option::None => continue, + }; + if !key.is_empty() { + names.push(key); + } + } + names.sort_unstable(); + return names; +} + +fn assert_pre_io_rejection(connection_uri: &str, tls_mode: ksp_store_postgres_lib::PostgresBackendTlsMode, expected_phase: &str) { + let settings = settings(connection_uri, tls_mode); + let rendered_settings = std::format!("{settings:?}"); + assert!(!rendered_settings.contains(SECRET_CANARY)); + assert!(rendered_settings.contains("")); + let result = poll_ready(ksp_store_postgres_lib::PostgresBackend::open(settings)); + let error = match result { + std::result::Result::Err(value) => value, + std::result::Result::Ok(_) => panic!("hostile PostgreSQL settings unexpectedly opened"), + }; + assert_eq!(error.kind(), ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid); + assert_eq!(error.phase(), expected_phase); + assert!(!std::format!("{error:?}").contains(SECRET_CANARY)); + return; +} + +#[test] +fn pre_009_backend_modules_exports_and_manifest_dependencies_are_exact() { + let crate_root = include_str!("../src/lib.rs"); + for required in ["mod constants;", "mod error;", "mod health;", "mod migration;", "mod runtime;"] { + assert!(crate_root.contains(required), "missing PostgreSQL backend module: {required}"); + } + assert!(!crate_root.contains("pub mod ")); + let actual_exports = public_reexport_names(crate_root); + let mut expected_exports = [ + "PostgresBackend", + "PostgresBackendError", + "PostgresBackendErrorKind", + "PostgresBackendHealthSnapshot", + "PostgresBackendRuntimeSnapshot", + "PostgresBackendSettings", + "PostgresBackendTlsMode", + ]; + expected_exports.sort_unstable(); + assert_eq!(actual_exports.as_slice(), expected_exports.as_slice()); + assert_eq!(actual_exports.len(), 7); + let manifest = include_str!("../Cargo.toml"); + let actual_dependencies = manifest_dependency_names(manifest); + let expected_dependencies = [ + "deadpool-postgres", + "ksp-logging-lib", + "ksp-store-api", + "rustls", + "rustls-native-certs", + "sha2", + "tokio", + "tokio-postgres", + "tokio-postgres-rustls", + ]; + assert_eq!(actual_dependencies.as_slice(), expected_dependencies.as_slice()); + return; +} + +#[test] +fn pre_009_hostile_uri_matrix_is_rejected_before_io_without_secret_echo() { + let malformed = std::format!("not-a-postgresql-uri-{SECRET_CANARY}"); + assert_pre_io_rejection(malformed.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "connection_uri"); + let oversized = std::format!("{}{SECRET_CANARY}", "x".repeat(4_097)); + assert_pre_io_rejection(oversized.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "connection_uri"); + let hostaddr_only = std::format!("hostaddr=127.0.0.1 user=operator password={SECRET_CANARY} dbname=ksp"); + assert_pre_io_rejection(hostaddr_only.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull, "tls_server_identity"); + let server_options = std::format!("host=localhost user=operator password={SECRET_CANARY} dbname=ksp options='-c application_name={SECRET_CANARY}'"); + assert_pre_io_rejection(server_options.as_str(), ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, "server_options"); + return; +} + +#[test] +fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() { + let error_source = include_str!("../src/error.rs"); + assert!(error_source.contains("kind: PostgresBackendErrorKind")); + assert!(error_source.contains("phase: &'static str")); + for forbidden in ["String", "source:", "message:", "tokio_postgres::Error", "deadpool_postgres::PoolError"] { + assert!(!error_source.contains(forbidden), "backend error type can retain forbidden external material: {forbidden}"); + } + let runtime = include_str!("../src/runtime.rs"); + assert!(runtime.contains("deadpool_postgres::PoolError::Backend(_)")); + assert!(!runtime.contains("deadpool_postgres::PoolError::Backend(error)")); + for source in [runtime, include_str!("../src/migration.rs"), include_str!("../src/health.rs")] { + for forbidden in ["format!(\"{error", "format!(\"{error:?", "error = ?", "error = %"] { + assert!(!source.contains(forbidden), "backend source renders external error material: {forbidden}"); + } + } + return; +} + +#[test] +fn pre_009_backend_has_no_env_bypass_or_business_persistence_capability() { + let production = std::format!( + "{} +{} +{} +{} +{}", + include_str!("../src/error.rs"), + include_str!("../src/health.rs"), + include_str!("../src/lib.rs"), + include_str!("../src/migration.rs"), + include_str!("../src/runtime.rs") + ); + for forbidden in [ + "std::env", + "dotenv", + "KSP_SECRET_", + "KSPB_", + "PGHOST", + "PGPORT", + "PGUSER", + "PGPASSWORD", + ".pgpass", + ".postgresql/", + "sslrootcert", + "sslcert", + "sslkey", + "ksp_store_lib", + "ksp_config_lib", + "sqlx::", + "impl ksp_store_api::RawTransaction", + "impl ksp_store_api::RawAccount", + ] { + assert!(!production.contains(forbidden), "forbidden backend ownership/capability material detected: {forbidden}"); + } + let bootstrap_sql = include_str!("../migrations/V000__bootstrap.sql"); + assert!(bootstrap_sql.contains("ksp_store_schema_migrations")); + for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] { + assert!(!bootstrap_sql.contains(forbidden), "business schema leaked into foundation migration: {forbidden}"); + } + return; +} diff --git a/deltas/0.3.2/pre.009.md b/deltas/0.3.2/pre.009.md new file mode 100644 index 0000000..e5271a4 --- /dev/null +++ b/deltas/0.3.2/pre.009.md @@ -0,0 +1,157 @@ + + + +# Delta `0.3.2-pre.009` — hardening, completeness et dependency matrix + +## Base exacte + +Cette tranche s'applique sur `0.3.2-pre.008`. + +Le 29 août 2026, le gate `pre.008` est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Le gate PostgreSQL réel opt-in passe également sur **PostgreSQL 17** avec bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready`, fermeture bornée et cleanup contrôlé. + +## Objet + +Durcir et figer la fondation Store/PostgreSQL avant le gate technique final sans modifier le code de production, les migrations ou les dépendances runtime. + +## Canari façade + +Nouveau fichier : + +```text +crates/ksp-store-lib/tests/hardening_completeness.rs +``` + +Il vérifie : + +```text +5 modules privés exacts +84 exports crate-root exacts +manifest runtime exact +feature postgres par défaut exacte +backend PostgreSQL toujours optionnel +aucune dépendance physique PostgreSQL dans la façade +canary secret redacted dans StoreSettings/Debug +URI malformed rejetée avant I/O +code ConfigInvalid avec postgres / BackendNotCompiled sans feature +aucun canary dans Display/Debug Error +aucune lecture Config/env/PG*/.pgpass +aucun Pool/Client/Row/Statement/SQL physique dans la façade +``` + +Le même test est conçu pour passer sous le feature set par défaut et sous `--no-default-features`. + +## Canari backend PostgreSQL + +Nouveau fichier : + +```text +crates/ksp-store-postgres-lib/tests/hardening_completeness.rs +``` + +Il vérifie : + +```text +5 modules privés exacts +7 exports bridge exacts +9 dépendances physiques exactes du manifest +URI malformed rejetée avant I/O +URI > 4096 octets rejetée avant I/O +hostaddr sans host rejeté sous VerifyFull +options= libpq rejeté +canary password/URI absent de Debug/error backend +PostgresBackendError limité structurellement à kind + phase statique +PoolError::Backend(_) abandonne explicitement l'erreur remote +aucun rendu d'erreur externe dans runtime/migration/health +aucune lecture env/KSP/KSPB/PG*/.pgpass +aucun sslrootcert/sslcert/sslkey/.postgresql implicite +aucune capability PostgreSQL RawTransaction/RawAccount +V000 reste strictement metadata-only +``` + +Les cas hostiles sont tous forcés avant le premier `.await` physique et n'exigent donc aucun PostgreSQL réel. + +## Non-régression Store API + +`ksp-store-api` n'est pas modifié. Le gate `pre.009` rejoue explicitement ses tests, notamment `tests/external_backend.rs`, afin de confirmer qu'un backend externe peut toujours implémenter les dix capabilities RAW sans dépendre de `ksp-store-lib` ou de PostgreSQL. + +## Documentation de suivi + +Le plan et la matrice validation sont réconciliés avec : + +```text +gates pre.006/pre.007 verts +pre.008 live vert sur PostgreSQL 17 +scope exact des canaris pre.009 +statuts hardening matérialisés mais non encore exécutés +commande de dependency matrix ciblée +``` + +## Fichiers ajoutés + +```text +crates/ksp-store-lib/tests/hardening_completeness.rs +crates/ksp-store-postgres-lib/tests/hardening_completeness.rs +deltas/0.3.2/pre.009.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md +docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md +``` + +## Fichiers supprimés + +Aucun. + +## Version + +```text +workspace.package.version = 0.3.2-pre.9 +``` + +Aucun `src/**`, migration, Config, README/USAGE, CHANGELOG ou ROADMAP n'est modifié dans cette tranche. + +## Validations exécutées dans l'environnement de génération + +```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.2 +contrôle statique de la version et du payload +contrôle statique des exports/manifests attendus +contrôle des chaînes interdites no-env/no-business +``` + +Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement de génération. + +## Gate opérateur + +```bash +cargo fmt --all +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.2 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-store-lib +cargo test -p ksp-store-lib --no-default-features +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +cargo tree -p ksp-store-lib --edges normal +cargo tree -p ksp-store-lib -e features +cargo tree -p ksp-store-postgres-lib --edges normal +cargo tree --duplicates +``` + +Le smoke PostgreSQL réel n'a pas besoin d'être rejoué dans `pre.009` si aucun code runtime/migration n'a changé ; il sera rejoué au gate technique final `pre.010`. + +## Questions ouvertes + +Aucune question fonctionnelle nouvelle. Tout échec des nouveaux canaris doit produire un correctif `pre.009-fix.NNN` avant `pre.010`. + +## Suite + +Après gate vert : `0.3.2-pre.010` — gate technique final sans nouveau développement fonctionnel. diff --git a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md index ae03229..148337e 100644 --- a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md +++ b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.3.2` — Store/PostgreSQL runtime foundation @@ -1063,7 +1063,57 @@ Aucun hook test-only n'est ajouté au runtime de production et aucune table RAW ### `pre.009` — Hardening/completeness/dependency matrix -Inputs hostiles, redaction, no-env, exact exports/modules, external backend compatibility, `--no-default-features`, graphes/features/duplicates, non-régression API. +Base requise : `0.3.2-pre.008` avec gate déterministe vert et preuve PostgreSQL réelle verte. Le 29 août 2026, le smoke opt-in a validé la fondation sur PostgreSQL 17 : bootstrap initial, réouverture idempotente, concurrence sous advisory lock, corruption/restauration checksum, rollback transactionnel, health `Ready` et fermeture bornée. + +Cette tranche reste un hardening de preuve : aucun `src/**`, aucune migration SQL et aucune dépendance runtime ne doivent être modifiés sauf découverte d'un défaut réel par les canaris. + +Canaris ajoutés : + +```text +ksp-store-lib + modules privés exacts + 84 exports crate-root exacts + manifest exact : logging + Store API + backend optionnel + feature postgres default exacte + URI hostile/secret canary redacted dans Settings et Error + même preuve avec et sans default features + aucune lecture Config/env/PG*/.pgpass + aucun type PostgreSQL physique ou SQL dans la façade + +ksp-store-postgres-lib + modules privés exacts + 7 exports bridge exacts + manifest physique exact + URI malformed/surdimensionnée/hostaddr-only/options rejetées avant I/O + canary secret absent des Debug/errors publics + PostgresBackendError ne peut retenir que kind + phase statique + PoolError::Backend perd explicitement le texte remote + aucune lecture env/PG*/.pgpass ni fichiers TLS libpq implicites + aucune capability RawTransaction/RawAccount implémentée + V000 reste metadata-only +``` + +La compatibilité backend externe reste prouvée par `ksp-store-api/tests/external_backend.rs`; `pre.009` la rejoue sans modifier `ksp-store-api`. + +Gate ciblé attendu : + +```bash +cargo fmt --all +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.2 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-store-lib +cargo test -p ksp-store-lib --no-default-features +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +cargo tree -p ksp-store-lib --edges normal +cargo tree -p ksp-store-lib -e features +cargo tree -p ksp-store-postgres-lib --edges normal +cargo tree --duplicates +``` ### `pre.010` — Gate technique final diff --git a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md index 8594597..a1445b8 100644 --- a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md +++ b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md @@ -1,5 +1,5 @@ - + # Validation `0.3.2` — Store/PostgreSQL runtime foundation @@ -78,6 +78,12 @@ Le gate opérateur de `pre.004-fix.001`, après correction manuelle des commenta Le gate opérateur de `pre.005-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 5 tests runtime backend, canaris de dépendances/API, façade avec et sans feature PostgreSQL et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.006`. +Le gate opérateur de `pre.006`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 9 tests backend incluant la migration V000, façade avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes Cargo passent. + +Le gate opérateur de `pre.007`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 12 tests backend incluant health/readiness, façade avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes Cargo passent. + +Le gate `pre.008` du 29 août 2026 est doublement vert : le gate déterministe passe intégralement puis `postgres_foundation_live` passe sur **PostgreSQL 17**. La preuve réelle couvre bootstrap initial/idempotent/concurrent, mismatch/recovery checksum, rollback transactionnel, health `Ready`, fermeture bornée et cleanup de la metadata possédée par le test. Cette base est l'entrée effective de `pre.009`. + ## 3. Frontières Cargo ### V32-DEP-001 — Façade -> API @@ -110,7 +116,7 @@ Preuves : manifest scanner + cargo tree. Le canari source de `pre.002` interdit explicitement la dépendance inverse ; le `cargo tree` opérateur confirme l'absence de cycle. La frontière sera durcie à nouveau en `pre.009`. -Statut : `PASS pre.002 opérateur / TODO pre.009 final`. +Statut : `PASS pre.002 opérateur / MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-DEP-004 — Feature PostgreSQL @@ -140,7 +146,7 @@ consumer ordinaire -> ksp-store-postgres-lib Matérialisé par `pre.005` : les types physiques nécessaires sont publics uniquement dans la crate backend pour la frontière inter-crates et ne sont jamais réexportés par `ksp-store-lib`; `Pool/Client/Row/Statement` restent absents de sa crate-root. -Statut : `PASS pre.005-fix.001 opérateur / TODO pre.009`. +Statut : `PASS pre.005-fix.001 opérateur / MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-DEP-006 — `ksp-store-api` non régressé @@ -152,7 +158,7 @@ exports/capabilities existants conservés aucun type backend ajouté pour PostgreSQL ``` -Statut : `TODO gate final`, baseline `v0.3.1` déjà verte. +Statut : `MATÉRIALISÉ pre.009 / TODO gate opérateur puis gate final` — le canari backend externe de `ksp-store-api` est rejoué sans modification de la crate. ## 4. Public API et settings @@ -170,7 +176,7 @@ Critère : `Postgres` reste un backend connu sans feature et `Store::open` écho Matérialisé par `pre.003` : le test `feature_mismatch` appelle réellement `Store::open` sous `--no-default-features` et exige `store.backend_not_compiled`. -Statut : `PASS pre.003-fix.001 opérateur / TODO pre.009 final`. +Statut : `PASS pre.003-fix.001 opérateur / MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-API-003 — Aucun type backend physique public @@ -183,7 +189,7 @@ rustls::* PostgresBackend / Pool / Client / Row / Statement ``` -Statut : `TODO pre.009`. +Statut : `MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-API-004 — Réexports Store API @@ -191,7 +197,7 @@ Critère : un consumer de `ksp-store-lib` accède aux contrats Store API utiles `pre.003` réexporte explicitement les 60 symboles crate-root acquis de `ksp-store-api` depuis `ksp-store-lib`, sans glob et sans réexport backend. -Statut : `PASS pre.003-fix.001 opérateur / TODO pre.009 exact exports`. +Statut : `PASS pre.003-fix.001 opérateur / MATÉRIALISÉ pre.009 exact exports / TODO gate opérateur`. ### V32-API-005 — Lifecycle @@ -275,7 +281,7 @@ PG* `pre.004` renforce aussi le canari d'ownership avec les nouveaux filenames Store ; `pre.004-fix.001` conserve cette frontière tout en ajoutant le réseau au contrat `StoreSettings`. Store/backend restent sans dépendance Config et sans lecture KSP/KSPB. -Statut : `PASS pre.004-fix.001 opérateur / TODO pre.009`. +Statut : `PASS pre.004-fix.001 opérateur / MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-CONFIG-004 — Adapter Config -> Store @@ -308,7 +314,7 @@ Critère : chaque connection future tokio-postgres est pilotée par le manager r `pre.005` délègue la création/recycle des connexions au `Manager` Deadpool retenu ; aucun `tokio::spawn` KSP n'est introduit dans Store. La preuve de non-régression détaillée reste au hardening. -Statut : `PASS pre.005-fix.001 opérateur / TODO pre.009`. +Statut : `PASS pre.005-fix.001 opérateur / MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-POOL-003 — Open failure safe @@ -331,7 +337,7 @@ aucune tâche volontairement laissée orpheline `pre.005` matérialise déjà `Pool::close()` et un drain borné par `shutdown_timeout`; `Drop` ne fait qu'un `close()` best-effort. La preuve end-to-end avec backend réel reste réservée à `pre.007/pre.008`. -Statut : `PASS pre.005-fix.001 opérateur / PASS pre.007 opérateur / TODO exécution live pre.008`. +Statut : `PASS pre.005-fix.001 opérateur / PASS pre.007 opérateur / PASS live pre.008 PostgreSQL 17`. ## 7. TLS @@ -368,7 +374,7 @@ Statut : `PASS pre.005-fix.001 opérateur`. Critère : backend ne lit pas `sslrootcert`, `sslcert`, `sslkey`, `.postgresql/*` ou autre fichier implicite hors settings KSP. -Statut : `TODO pre.009`. +Statut : `MATÉRIALISÉ pre.009 / TODO gate opérateur`. ## 8. Migration/bootstrap @@ -384,7 +390,7 @@ et aucune table métier RAW/CORE/DECODE/SPECIALIZED. `pre.006` embarque exactement `migrations/V000__bootstrap.sql`, dont le seul DDL de production crée `ksp_store_schema_migrations`. Le canari source interdit les identifiants métier RAW/CORE/DECODE/SPECIALIZED dans le moteur et la ressource SQL. -Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO exécution live opérateur`. +Statut : `PASS pre.006 opérateur / PASS live pre.008 PostgreSQL 17`. ### V32-MIG-002 — Version/checksum @@ -400,7 +406,7 @@ mismatch historique terminal `pre.006` calcule explicitement SHA-256 sur les octets exacts du SQL embarqué puis encode les 32 octets en 64 caractères hex minuscules. Le sentinel `(0, bootstrap, checksum)` est inséré dans la même transaction que la création metadata. Les tests unitaires figent le checksum du SQL committed et couvrent sentinel valide, missing, nom/checksum divergents et historique plus récent. -Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO exécution live opérateur`. +Statut : `PASS pre.006 opérateur / PASS live pre.008 PostgreSQL 17`. ### V32-MIG-003 — Concurrence @@ -408,7 +414,7 @@ Critère : deux runners concurrents sont sérialisés par advisory transaction l `pre.006` utilise une clé KSP fixe et `pg_try_advisory_xact_lock($1)` dans une boucle bornée par `migration_lock_timeout`, avec polling de 25 ms maximum. Aucun lock bloquant illimité n'est utilisé. -Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO concurrence live opérateur`. +Statut : `PASS pre.006 opérateur / PASS concurrence live pre.008 PostgreSQL 17`. ### V32-MIG-004 — Atomicité/recovery @@ -416,7 +422,7 @@ Critère : échec d'une migration du run courant rollback DDL + history de ce ru `pre.006` place lock, metadata DDL, sentinel et validation dans une transaction unique ; toute sortie d'erreur avant `commit()` droppe la transaction et PostgreSQL rollback le run courant. Un timeout externe borne également l'ensemble du bootstrap. L'injection d'échec et la preuve physique du rollback restent au smoke réel. -Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.008 / TODO rollback live opérateur`. +Statut : `PASS pre.006 opérateur / PASS rollback live pre.008 PostgreSQL 17`. ### V32-MIG-005 — Newer runtime guard @@ -438,7 +444,7 @@ aucun identifier physique user-configurable en 0.3.2 `pre.006` garde le DDL versionné sous `include_str!` et toutes les values variables des requêtes de contrôle (`lock key`, `statement_timeout`, history values) passent par paramètres. Les noms physiques sont des constantes KSP, jamais des settings. -Statut : `PASS pre.006 opérateur / TODO pre.009 hardening`. +Statut : `PASS pre.006 opérateur / MATÉRIALISÉ pre.009 hardening / TODO gate opérateur`. ### V32-MIG-007 — No business capability @@ -450,7 +456,7 @@ Critères : 0 repository RAW métier ``` -Statut : `TODO pre.009/gate final`. +Statut : `MATÉRIALISÉ pre.009 / TODO gate opérateur puis gate final`. ## 9. Health/readiness @@ -468,7 +474,7 @@ Critère : `Store::open` ne retourne Ready qu'après connect + bootstrap/verify `pre.007` conserve l'ouverture stricte acquise en `pre.006` et ajoute un probe borné par le `wait_timeout` du pool : acquisition, `SELECT 1`, lecture de version migration. Le smoke réel reste nécessaire pour prouver ce chemin contre PostgreSQL. -Statut : `PASS pre.007 opérateur / TODO exécution live pre.008`. +Statut : `PASS pre.007 opérateur / PASS live pre.008 PostgreSQL 17`. ### V32-HEALTH-003 — Error redaction @@ -476,7 +482,7 @@ Critère : un health failure n'expose pas server error string, query text ou cre Le backend ne conserve que `PostgresBackendErrorKind`; la façade mappe vers un `ErrorCode` KSP optionnel. Le snapshot public ne contient aucune string serveur, URI, SQL, host, user, database ou handle. -Statut : `PASS pre.007 opérateur / TODO pre.009 hardening`. +Statut : `PASS pre.007 opérateur / MATÉRIALISÉ pre.009 hardening / TODO gate opérateur`. ## 10. PostgreSQL integration réelle @@ -493,7 +499,7 @@ URI jamais imprimée par le test `pre.008` ajoute `tests/postgres_foundation_live.rs`. Le test borne l'entrée à 4096 octets, ne conserve dans ses erreurs que des phases statiques et ne rend jamais l'erreur PostgreSQL brute. L'opérateur peut masquer la saisie côté shell puis piper une seule ligne sur stdin ; aucune variable d'environnement n'est lue par le test. -Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur`. +Statut : `PASS live pre.008 PostgreSQL 17`. ### V32-LIVE-002 — Non destructif @@ -508,7 +514,7 @@ cleanup seulement de metadata créée par le test Le test interroge `information_schema.tables` avant toute mutation. Il ne prend ownership du cleanup qu'après avoir prouvé que le bootstrap initial a créé la metadata alors que la précondition était absente. Les seuls `DROP` exécutés ciblent `ksp_store_schema_migrations` pendant les resets contrôlés et le cleanup final. -Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur`. +Statut : `PASS live pre.008 PostgreSQL 17`. ### V32-LIVE-003 — Scénario foundation @@ -528,15 +534,15 @@ cleanup metadata La preuve rollback évite tout hook public de test : une transaction `tokio-postgres` de test exécute le V000 exact par `include_str!`, insère un sentinel transitoire, provoque ensuite une erreur SQL puis est droppée. Le test vérifie que la metadata n'existe pas après le rollback implicite et que le bootstrap KSP normal peut repartir proprement. -Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur puis revalidation pre.010`. +Statut : `PASS live pre.008 PostgreSQL 17 / TODO revalidation pre.010`. ### V32-LIVE-004 — PostgreSQL support Critère : test refuse major < 15 et enregistre seulement le major safe réellement testé. -Cible release : PostgreSQL 18.6. `pre.008` interroge uniquement `SHOW server_version_num`, dérive le major et n'imprime aucune identité de serveur. +Cible release : PostgreSQL 18.6. `pre.008` interroge uniquement `SHOW server_version_num`, dérive le major et n'imprime aucune identité de serveur. Le gate opérateur réel a été exécuté avec succès sur PostgreSQL 17. -Statut : `MATÉRIALISÉ pre.008 / TODO exécution live opérateur puis revalidation pre.010`. +Statut : `PASS live pre.008 PostgreSQL 17 / TODO revalidation pre.010`. ## 11. Security/adversarial @@ -546,7 +552,9 @@ Cas : vide, surdimensionnée, malformed, paramètres conflictuels, password cont Attendu : rejet borné sans echo. -Statut : `TODO pre.009`. +`pre.009` ajoute un test d'intégration backend qui force avant I/O : URI malformed, URI > 4096 octets, `hostaddr` sans identité sous `VerifyFull` et `options=` libpq. Chaque cas contient un canary secret et exige une erreur `ConfigInvalid` avec phase statique seulement. + +Statut : `MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-SEC-002 — Secret canary @@ -560,7 +568,9 @@ tracing snapshots health snapshots ``` -Statut : `TODO pre.009`. +`pre.009` injecte des canaris distincts dans la façade et le backend. Les settings Debug doivent afficher ``; les erreurs pré-I/O publiques/backend ne doivent contenir aucun canary. Les snapshots health restent couverts par les canaris déterministes de `pre.007`. + +Statut : `MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-SEC-003 — Timeouts hostiles @@ -568,21 +578,46 @@ Cas : zéro, inversion, dépassement bornes pour pool/connect/migration/close. Les bornes backend-neutral décidées en `pre.001` sont matérialisées et couvertes par tests unitaires en `pre.003`. `pre.005` applique physiquement `connect/wait/create/recycle/shutdown`; `pre.006` applique physiquement `migration_timeout` et `migration_lock_timeout`. -Statut : `PASS pre.003-fix.001 opérateur` pour les bornes backend-neutral / `PASS pre.005-fix.001 opérateur` pour pool/connect/shutdown / `PASS pre.006 opérateur` pour migration / `TODO pre.009` hardening. +Statut : `PASS pre.003-fix.001 opérateur` pour les bornes backend-neutral / `PASS pre.005-fix.001 opérateur` pour pool/connect/shutdown / `PASS pre.006 opérateur` pour migration / `MATÉRIALISÉ pre.009` hardening / `TODO gate opérateur`. ### V32-SEC-004 — Feature mismatch avant I/O `pre.003` matérialise un canari d'intégration compilé avec et sans `postgres`. Sans feature, `Store::open` retourne le code stable `store.backend_not_compiled` avant tout chemin backend physique. -Statut : `PASS pre.003-fix.001 opérateur / TODO pre.009`. +Statut : `PASS pre.003-fix.001 opérateur / MATÉRIALISÉ pre.009 / TODO gate opérateur`. ### V32-SEC-005 — Server error sanitization Un serveur/test double qui renvoie un message contenant un canary ne doit pas le faire traverser l'erreur publique. -Statut : bridge de sanitization matérialisé en `pre.005`; `TODO pre.009` pour le canary serveur hostile. +Statut : bridge de sanitization matérialisé en `pre.005`; `pre.009` ajoute un canari structurel prouvant que `PostgresBackendError` ne peut retenir que `kind + phase` statiques et que `PoolError::Backend(_)` abandonne le texte remote. `TODO gate opérateur`. -## 12. Gates Rust/workspace +## 12. Hardening/completeness `pre.009` + +Deux nouveaux canaris d'intégration figent sans modifier la production : + +```text +crates/ksp-store-lib/tests/hardening_completeness.rs +crates/ksp-store-postgres-lib/tests/hardening_completeness.rs +``` + +Ils couvrent : + +```text +84 exports façade exacts / 5 modules privés exacts +7 exports backend exacts / 5 modules privés exacts +manifests et feature postgres exacts +URI hostiles et canaris secrets avant I/O +sanitization structurelle des erreurs serveur/pool +no-env / no-.pgpass / no-libpq-TLS-files +aucune capability PostgreSQL RawTransaction/RawAccount +V000 metadata-only +compatibilité backend externe ksp-store-api rejouée au gate +``` + +Statut global `pre.009` : `MATÉRIALISÉ / TODO gate opérateur`. + +## 13. Gates Rust/workspace À chaque tranche applicable : @@ -629,7 +664,7 @@ Les builds Tauri ne sont requis que si `pre.004` modifie réellement les resourc Statut global : `TODO` jusqu'aux preuves de chaque tranche. -## 13. Critères de fermeture +## 14. Critères de fermeture La matrice ne peut passer en finale que si tous les critères applicables sont `PASS` et que :