From f5d98c4e69a966b286bcb56823bb16d448e72bda Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 17 Aug 2026 21:03:51 +0200 Subject: [PATCH] v0.2.1-pre.006 --- .env.example | 14 +- Cargo.toml | 4 +- ROADMAP.md | 4 +- config/examples/std.transport.example.json | 64 ++++ config/schemas/std.transport.schema.json | 124 +++++++ config/std.transport.json | 69 ++++ crates/ksp-config-lib/Cargo.toml | 3 +- crates/ksp-config-lib/README.md | 22 +- crates/ksp-config-lib/TODO.md | 8 +- crates/ksp-config-lib/USAGE.md | 22 +- crates/ksp-config-lib/src/lib.rs | 17 +- crates/ksp-config-lib/src/registry.rs | 28 +- crates/ksp-config-lib/src/transport.rs | 316 ++++++++++++++++++ crates/ksp-config-lib/tests/ownership.rs | 11 +- crates/ksp-config-lib/tests/public_api.rs | 31 +- .../unit_tests/fixtures/std.transport.json | 40 +++ crates/ksp-config-lib/unit_tests/registry.rs | 51 ++- crates/ksp-config-lib/unit_tests/transport.rs | 202 +++++++++++ deltas/0.2.1/pre.006.md | 263 +++++++++++++++ docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md | 21 +- docs/rules/RULES_DEPENDENCIES.md | 2 +- 21 files changed, 1270 insertions(+), 46 deletions(-) create mode 100644 config/examples/std.transport.example.json create mode 100644 config/schemas/std.transport.schema.json create mode 100644 config/std.transport.json create mode 100644 crates/ksp-config-lib/src/transport.rs create mode 100644 crates/ksp-config-lib/unit_tests/fixtures/std.transport.json create mode 100644 crates/ksp-config-lib/unit_tests/transport.rs create mode 100644 deltas/0.2.1/pre.006.md diff --git a/.env.example b/.env.example index 6479615..b2a6d3b 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,22 @@ # file: .env.example -# version: 2 +# version: 3 # KSP Logging root directory. Used by config/std.logging.json for relative log output paths. # The current Config document fallback is "logs" when neither the process environment nor .env defines this variable. KSP_LOGS_DIRECTORY=logs +# Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json. +# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent. +KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com + +# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example. +# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent. +KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com + +# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected. +# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON. +# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me + # Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready. KSP_DESK_SPLASH_MINIMUM_MS=1200 diff --git a/Cargo.toml b/Cargo.toml index b81238c..7227123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 104 +# version: 105 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"] [workspace.package] -version = "0.2.1-pre.5.fix.1" +version = "0.2.1-pre.6" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/ROADMAP.md b/ROADMAP.md index c08cec3..ce58804 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap KSP @@ -41,7 +41,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U ### Cadrage - [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés. -- [/] `0.2.1` — HTTP foundation en cours : matrice 52+14, crate/settings/JSON-RPC/registry, client/pool/routing, résilience runtime et exécution HTTP acquises ; les 4 canaris typés (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) sont matérialisés, Config standard et clôture restent planifiés. +- [/] `0.2.1` — HTTP foundation en cours : matrice 52+14, runtime/routing/résilience/exécution HTTP et 4 canaris typés acquis ; `std.transport` et l’adapter Config -> Transport sont matérialisés, la compliance/smoke/documentation de clôture reste planifiée. ### Releases fonctionnelles décidées/pressenties diff --git a/config/examples/std.transport.example.json b/config/examples/std.transport.example.json new file mode 100644 index 0000000..d87ffdf --- /dev/null +++ b/config/examples/std.transport.example.json @@ -0,0 +1,64 @@ +{ + "format_version": 1, + "retry": { + "max_retries": 3, + "initial_backoff_ms": 150, + "max_backoff_ms": 3000 + }, + "default_profile": "mainnet_mixed", + "profiles": [ + { + "profile_id": "mainnet_mixed", + "endpoints": [ + { + "name": "mainnet_public", + "enabled": true, + "provider": "solana-public", + "cluster": "mainnet-beta", + "url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}", + "connect_timeout_ms": 5000, + "request_timeout_ms": 15000, + "max_idle_connections_per_host": 8, + "roles": [ + { + "role": "default", + "enabled": true, + "request_kinds": ["*"], + "priority": 200, + "limits": { + "requests_per_second": 5, + "burst_capacity": 10, + "max_concurrent_requests": 8, + "pause_after_rate_limit_ms": 1000 + } + } + ] + }, + { + "name": "mainnet_private", + "enabled": true, + "provider": "private-provider", + "cluster": "mainnet-beta", + "url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}", + "connect_timeout_ms": 5000, + "request_timeout_ms": 10000, + "max_idle_connections_per_host": 16, + "roles": [ + { + "role": "default", + "enabled": true, + "request_kinds": ["*"], + "priority": 100, + "limits": { + "requests_per_second": 20, + "burst_capacity": 40, + "max_concurrent_requests": 16, + "pause_after_rate_limit_ms": 750 + } + } + ] + } + ] + } + ] +} diff --git a/config/schemas/std.transport.schema.json b/config/schemas/std.transport.schema.json new file mode 100644 index 0000000..2515611 --- /dev/null +++ b/config/schemas/std.transport.schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:ksp:schema:std.transport:v1", + "title": "KSP standard HTTP Transport configuration", + "type": "object", + "additionalProperties": false, + "required": ["format_version", "retry", "default_profile", "profiles"], + "properties": { + "format_version": {"const": 1}, + "retry": {"$ref": "#/$defs/retry"}, + "default_profile": {"$ref": "#/$defs/profileId"}, + "profiles": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/profile"} + } + }, + "$defs": { + "profileId": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._-]*$" + }, + "descriptor": { + "type": "string", + "minLength": 1, + "pattern": "^\\S(?:.*\\S)?$" + }, + "positiveMs": { + "type": "integer", + "minimum": 1, + "maximum": 4294967295 + }, + "positiveU32": { + "type": "integer", + "minimum": 1, + "maximum": 4294967295 + }, + "retry": { + "type": "object", + "additionalProperties": false, + "required": ["max_retries", "initial_backoff_ms", "max_backoff_ms"], + "properties": { + "max_retries": {"type": "integer", "minimum": 0, "maximum": 100}, + "initial_backoff_ms": {"$ref": "#/$defs/positiveMs"}, + "max_backoff_ms": {"$ref": "#/$defs/positiveMs"} + } + }, + "limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "requests_per_second": {"$ref": "#/$defs/positiveU32"}, + "burst_capacity": {"$ref": "#/$defs/positiveU32"}, + "max_concurrent_requests": {"$ref": "#/$defs/positiveU32"}, + "pause_after_rate_limit_ms": {"$ref": "#/$defs/positiveMs"} + } + }, + "role": { + "type": "object", + "additionalProperties": false, + "required": ["role", "enabled", "request_kinds", "priority", "limits"], + "properties": { + "role": {"$ref": "#/$defs/descriptor"}, + "enabled": {"type": "boolean"}, + "request_kinds": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": {"$ref": "#/$defs/descriptor"}, + "allOf": [ + { + "if": {"contains": {"const": "*"}}, + "then": {"maxItems": 1} + } + ] + }, + "priority": {"type": "integer", "minimum": 0, "maximum": 4294967295}, + "limits": {"$ref": "#/$defs/limits"} + } + }, + "endpoint": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "enabled", + "provider", + "cluster", + "url", + "connect_timeout_ms", + "request_timeout_ms", + "roles" + ], + "properties": { + "name": {"$ref": "#/$defs/descriptor"}, + "enabled": {"type": "boolean"}, + "provider": {"$ref": "#/$defs/descriptor"}, + "cluster": {"$ref": "#/$defs/descriptor"}, + "url": {"type": "string", "minLength": 1}, + "connect_timeout_ms": {"$ref": "#/$defs/positiveMs"}, + "request_timeout_ms": {"$ref": "#/$defs/positiveMs"}, + "max_idle_connections_per_host": {"type": "integer", "minimum": 1}, + "roles": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/role"} + } + } + }, + "profile": { + "type": "object", + "additionalProperties": false, + "required": ["profile_id", "endpoints"], + "properties": { + "profile_id": {"$ref": "#/$defs/profileId"}, + "endpoints": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/endpoint"} + } + } + } + } +} diff --git a/config/std.transport.json b/config/std.transport.json new file mode 100644 index 0000000..4cbf365 --- /dev/null +++ b/config/std.transport.json @@ -0,0 +1,69 @@ +{ + "format_version": 1, + "retry": { + "max_retries": 2, + "initial_backoff_ms": 100, + "max_backoff_ms": 2000 + }, + "default_profile": "devnet_public", + "profiles": [ + { + "profile_id": "devnet_public", + "endpoints": [ + { + "name": "solana_devnet_public", + "enabled": true, + "provider": "solana-public", + "cluster": "devnet", + "url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}", + "connect_timeout_ms": 5000, + "request_timeout_ms": 15000, + "max_idle_connections_per_host": 8, + "roles": [ + { + "role": "default", + "enabled": true, + "request_kinds": ["*"], + "priority": 100, + "limits": { + "requests_per_second": 5, + "burst_capacity": 10, + "max_concurrent_requests": 8, + "pause_after_rate_limit_ms": 1000 + } + } + ] + } + ] + }, + { + "profile_id": "mainnet_public", + "endpoints": [ + { + "name": "solana_mainnet_public", + "enabled": true, + "provider": "solana-public", + "cluster": "mainnet-beta", + "url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}", + "connect_timeout_ms": 5000, + "request_timeout_ms": 15000, + "max_idle_connections_per_host": 8, + "roles": [ + { + "role": "default", + "enabled": true, + "request_kinds": ["*"], + "priority": 100, + "limits": { + "requests_per_second": 5, + "burst_capacity": 10, + "max_concurrent_requests": 8, + "pause_after_rate_limit_ms": 1000 + } + } + ] + } + ] + } + ] +} diff --git a/crates/ksp-config-lib/Cargo.toml b/crates/ksp-config-lib/Cargo.toml index 65d3265..c00564c 100644 --- a/crates/ksp-config-lib/Cargo.toml +++ b/crates/ksp-config-lib/Cargo.toml @@ -1,5 +1,5 @@ # file: crates/ksp-config-lib/Cargo.toml -# version: 4 +# version: 5 [package] name = "ksp-config-lib" @@ -10,6 +10,7 @@ repository.workspace = true [dependencies] ksp-core-lib = { path = "../ksp-core-lib" } ksp-logging-lib = { path = "../ksp-logging-lib" } +ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true jsonschema.workspace = true diff --git a/crates/ksp-config-lib/README.md b/crates/ksp-config-lib/README.md index 6f0a83c..dd9436d 100644 --- a/crates/ksp-config-lib/README.md +++ b/crates/ksp-config-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-config-lib @@ -23,6 +23,7 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio - la classification `Public`, `Internal`, `Secret` ; - les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ; - l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ; +- l'adapter du document HTTP Transport effectif vers `ksp_onchain_transport_lib::HttpTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ; - la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ; - les écritures atomiques JSON/`.env` et la protection des permissions `.env` ; - les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`. @@ -32,9 +33,11 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio Le registre par défaut connaît : ```text -cfg.std.logging -> config/std.logging.json -schema.std.logging -> config/schemas/std.logging.schema.json -schema.composite -> config/schemas/composite.schema.json +cfg.std.logging -> config/std.logging.json +cfg.std.transport -> config/std.transport.json +schema.std.logging -> config/schemas/std.logging.schema.json +schema.std.transport -> config/schemas/std.transport.schema.json +schema.composite -> config/schemas/composite.schema.json ``` `ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques. @@ -59,11 +62,11 @@ Les autres crates et applications KSP ne doivent pas : - parser ou écrire directement `.env` ; - ouvrir directement les documents Config connus par leur filename physique ; - réimplémenter la sélection de profils, les compositions ou les placeholders ; -- reconstruire elles-mêmes la configuration Logging depuis le JSON. +- reconstruire elles-mêmes la configuration Logging ou Transport depuis le JSON. -`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result` et de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`. +`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport. -La dépendance inverse est interdite : `ksp-core-lib` et `ksp-logging-lib` ne dépendent pas de Config. +La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib` et `ksp-onchain-transport-lib` ne dépendent pas de Config. Config ne possède pas le `LoggingGuard`. L'application ou le service qui orchestre le runtime construit la configuration effective puis possède le lifecycle `ksp_logging_lib::initialize/reinitialize`. @@ -75,12 +78,13 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées. -Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. +Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. ## Documentation - [`USAGE.md`](USAGE.md) — construction du moteur, résolution runtime et management ; - [`TODO.md`](TODO.md) — points explicitement différés après `0.1.3` ; - [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ; -- [`../../config/std.logging.json`](../../config/std.logging.json) — premier document standard concret ; +- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ; +- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ; - [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime. diff --git a/crates/ksp-config-lib/TODO.md b/crates/ksp-config-lib/TODO.md index 7de158f..77e89ba 100644 --- a/crates/ksp-config-lib/TODO.md +++ b/crates/ksp-config-lib/TODO.md @@ -1,5 +1,5 @@ - + # TODO ksp-config-lib @@ -31,11 +31,15 @@ La validation applicative desktop appartient à `ksp-app-config-desk` : Ces points ne nécessitent pas de duplication de logique dans `ksp-config-lib`; toute lacune réelle révélée par l'application ouvrira un delta Config explicite. +## Extension `0.2.1` — Transport HTTP + +`0.2.1-pre.006` introduit le premier nouveau domaine standard depuis Logging : `std.transport.json`, son schema, son exemple, son enregistrement et l'adapter Config -> `HttpTransportSettings`. Cette extension confirme que les nouveaux domaines restent ajoutés à la demande d'un consumer réel, sans transformer Config en propriétaire du runtime Transport. + ## Futur, uniquement au besoin Les capacités suivantes sont différées jusqu'à l'apparition de composants réels : -- nouveaux documents `std..json` et schemas associés ; +- documents `std..json` supplémentaires et schemas associés ; - descriptors `cfg.composite.` pour de vrais consumers ; - contrats typés de management supplémentaires pour les nouveaux documents ; - watcher filesystem/reload automatique si une application ou un service démontre le besoin ; diff --git a/crates/ksp-config-lib/USAGE.md b/crates/ksp-config-lib/USAGE.md index a11f02b..99a43f8 100644 --- a/crates/ksp-config-lib/USAGE.md +++ b/crates/ksp-config-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-config-lib @@ -29,6 +29,7 @@ Les arguments compris par Config sont : --cfgpath=/path/to/config --schemapath=/path/to/schemas --filemap=cfg.std.logging=my-logging.json +--filemap=cfg.std.transport=my-transport.json ``` `cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif. @@ -122,6 +123,23 @@ Un `logs_directory` relatif est ancré sur le current working directory du proce Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation. +### 4.1 Construire le Transport HTTP depuis Config + +Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` : + +```rust +let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), +}; + +let transport_settings = transport.into_settings(); +``` + +Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted. + +La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP. + ## 5. Profils et composites Pour un document standard profilé : @@ -213,6 +231,7 @@ Ce guide reste volontairement indépendant des numéros de release. Les contrats | Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §7–8 | | Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 | | Logging effectif | `ResolvedLoggingConfig` | §4 | +| Transport effectif | `ResolvedTransportConfig` | §4.1 | | Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §6–7 | | Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous | | Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous | @@ -324,4 +343,3 @@ if let std::result::Result::Err(error) = loaded { ``` Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`. - diff --git a/crates/ksp-config-lib/src/lib.rs b/crates/ksp-config-lib/src/lib.rs index c2890c9..07898d3 100644 --- a/crates/ksp-config-lib/src/lib.rs +++ b/crates/ksp-config-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/src/lib.rs -// version: 11 +// version: 12 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -8,8 +8,8 @@ //! //! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and //! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and -//! provenance. The standard Logging document maps explicitly to `ksp_logging_lib::LoggingSettings`, while the management surface provides typed Logging -//! mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence. +//! provenance. Standard Logging and HTTP Transport documents map explicitly to their runtime settings contracts, while the management surface provides +//! typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence. mod bootstrap; mod composite; @@ -23,6 +23,7 @@ mod persistence; mod profile; mod registry; mod sensitivity; +mod transport; pub(crate) use self::constants::TRACING_TARGET; @@ -144,12 +145,20 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME; pub use self::registry::DEFAULT_STD_LOGGING_FILENAME; /// Default physical filename for the standard Logging JSON Schema document. pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME; +/// Default physical filename for the standard HTTP Transport configuration document. +pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME; +/// Default physical filename for the standard HTTP Transport JSON Schema document. +pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME; /// Logical file identifier for the generic composite JSON Schema document. pub use self::registry::FILE_ID_SCHEMA_COMPOSITE; /// Logical file identifier for the standard Logging JSON Schema document. pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING; +/// Logical file identifier for the standard HTTP Transport JSON Schema document. +pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT; /// Logical file identifier for the standard Logging configuration document. pub use self::registry::FILE_ID_STD_LOGGING; +/// Logical file identifier for the standard HTTP Transport configuration document. +pub use self::registry::FILE_ID_STD_TRANSPORT; /// Sensitivity assigned to one Config value after environment resolution. pub use self::sensitivity::ConfigSensitivity; /// Provenance segment participating in one resolved Config value. @@ -160,3 +169,5 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE; pub use self::sensitivity::ResolvedConfigJson; /// One resolved Config string preserving real/safe representations and provenance. pub use self::sensitivity::ResolvedConfigText; +/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`. +pub use self::transport::ResolvedTransportConfig; diff --git a/crates/ksp-config-lib/src/registry.rs b/crates/ksp-config-lib/src/registry.rs index 362b036..8c65d49 100644 --- a/crates/ksp-config-lib/src/registry.rs +++ b/crates/ksp-config-lib/src/registry.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/src/registry.rs -// version: 4 +// version: 5 /// Bootstrap argument used to replace a known Config filename mapping. pub const ARG_FILE_MAP: &str = "--filemap"; @@ -7,12 +7,20 @@ pub const ARG_FILE_MAP: &str = "--filemap"; pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging"; /// Logical file identifier for the standard Logging JSON Schema document. pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging"; +/// Logical file identifier for the standard HTTP Transport configuration document. +pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport"; +/// Logical file identifier for the standard HTTP Transport JSON Schema document. +pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport"; /// Logical file identifier for the generic composite JSON Schema document. pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite"; /// Default physical filename for the standard Logging configuration document. pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json"; /// Default physical filename for the standard Logging JSON Schema document. pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json"; +/// Default physical filename for the standard HTTP Transport configuration document. +pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json"; +/// Default physical filename for the standard HTTP Transport JSON Schema document. +pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json"; /// Default physical filename for the generic composite JSON Schema document. pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json"; @@ -135,13 +143,29 @@ impl ConfigFileRegistry { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; + let transport = ConfigFileDescriptor::new( + FILE_ID_STD_TRANSPORT, + ConfigFileKind::Config, + DEFAULT_STD_TRANSPORT_FILENAME, + std::option::Option::Some(FILE_ID_SCHEMA_STD_TRANSPORT), + ); + let transport = match transport { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let transport_schema = + ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_TRANSPORT, ConfigFileKind::Schema, DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, std::option::Option::None); + let transport_schema = match transport_schema { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; let composite_schema = ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None); let composite_schema = match composite_schema { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - return build_registry([logging, logging_schema, composite_schema]); + return build_registry([logging, logging_schema, transport, transport_schema, composite_schema]); } /// Creates the default registry and applies repeatable `--filemap==` overrides from raw process arguments. diff --git a/crates/ksp-config-lib/src/transport.rs b/crates/ksp-config-lib/src/transport.rs new file mode 100644 index 0000000..0a88cf2 --- /dev/null +++ b/crates/ksp-config-lib/src/transport.rs @@ -0,0 +1,316 @@ +// file: crates/ksp-config-lib/src/transport.rs +// version: 1 + +/// Effective standard HTTP Transport configuration resolved from Config and mapped to the Transport runtime contract. +#[derive(Clone, Eq, PartialEq)] +pub struct ResolvedTransportConfig { + file_id: crate::ConfigFileId, + source_path: std::path::PathBuf, + profile_id: String, + selection_source: crate::ConfigProfileSelectionSource, + effective: crate::ResolvedConfigJson, + settings: ksp_onchain_transport_lib::HttpTransportSettings, +} + +impl ResolvedTransportConfig { + /// Returns the logical Config document identifier used by this runtime configuration. + #[must_use] + pub const fn file_id(&self) -> &crate::ConfigFileId { + return &self.file_id; + } + + /// Returns the physical source Config document path. + #[must_use] + pub fn source_path(&self) -> &std::path::Path { + return self.source_path.as_path(); + } + + /// Returns the selected standard Transport profile identifier. + #[must_use] + pub fn profile_id(&self) -> &str { + return self.profile_id.as_str(); + } + + /// Returns the source that selected the standard Transport profile. + #[must_use] + pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource { + return self.selection_source; + } + + /// Returns the detailed environment-resolved effective Config view. + /// + /// The real tree is available to legitimate runtime consumers. The safe tree redacts values originating from `KSP_SECRET_*` or `KSPB_SECRET_*` + /// placeholders and is the only representation used by this type's [`std::fmt::Debug`] implementation. + #[must_use] + pub const fn effective(&self) -> &crate::ResolvedConfigJson { + return &self.effective; + } + + /// Returns the validated runtime HTTP Transport settings. + #[must_use] + pub const fn settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings { + return &self.settings; + } + + /// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings. + #[must_use] + pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings { + return self.settings; + } +} + +impl std::fmt::Debug for ResolvedTransportConfig { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("ResolvedTransportConfig") + .field("file_id", &self.file_id) + .field("source_path", &self.source_path) + .field("profile_id", &self.profile_id) + .field("selection_source", &self.selection_source) + .field("effective", &self.effective) + .finish_non_exhaustive(); + } +} + +impl crate::ConfigDocumentEngine { + /// Loads the standard HTTP Transport document, selects a profile, resolves environment placeholders and maps it to Transport runtime settings. + /// + /// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed + /// because the Transport URL wrapper owns runtime redaction. Invalid environment-resolved values are reported as + /// [`crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID`] without copying endpoint URL values into ordinary error context. + pub fn load_resolved_transport_config( + &self, + requested_profile: std::option::Option<&str>, + environment: &crate::ConfigEnvironment, + ) -> ksp_core_lib::Result { + let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT); + let file_id = match file_id { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let profile = self.load_resolved_profile(&file_id, requested_profile); + let profile = match profile { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return resolve_transport_profile(&profile, environment); + } +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveTransportSource { + format_version: u32, + profile_id: String, + retry: EffectiveRetrySource, + endpoints: std::vec::Vec, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveRetrySource { + max_retries: u32, + initial_backoff_ms: u64, + max_backoff_ms: u64, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveEndpointSource { + name: String, + enabled: bool, + provider: String, + cluster: String, + url: String, + connect_timeout_ms: u64, + request_timeout_ms: u64, + max_idle_connections_per_host: std::option::Option, + roles: std::vec::Vec, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveRoleSource { + role: String, + enabled: bool, + request_kinds: std::vec::Vec, + priority: u32, + limits: EffectiveLimitsSource, +} + +#[derive(serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct EffectiveLimitsSource { + requests_per_second: std::option::Option, + burst_capacity: std::option::Option, + max_concurrent_requests: std::option::Option, + pause_after_rate_limit_ms: std::option::Option, +} + +fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result { + let effective = profile.resolve_effective_environment_detailed(environment); + let effective = match effective { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let source = serde_json::from_value::(effective.value().clone()); + let source = match source { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err( + effective_error(profile, "effective Transport Config cannot be decoded into the runtime adapter contract").with_source(error), + ); + }, + }; + if source.format_version != 1 { + return std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported")); + } + if source.profile_id != profile.profile_id() { + return std::result::Result::Err(effective_error(profile, "effective Transport profile_id does not match the selected profile")); + } + let retry = ksp_onchain_transport_lib::HttpRetrySettings::new( + source.retry.max_retries, + std::time::Duration::from_millis(source.retry.initial_backoff_ms), + std::time::Duration::from_millis(source.retry.max_backoff_ms), + ); + let endpoints = map_endpoints(source.endpoints, profile); + let endpoints = match endpoints { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(endpoints, retry); + let validation = settings.validate(); + if let std::result::Result::Err(error) = validation { + return std::result::Result::Err(transport_contract_error(profile, "effective Transport settings fail the Transport runtime contract", &error)); + } + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + profile_id = profile.profile_id(), + endpoint_count = settings.endpoints().len(), + "mapped standard Transport Config to runtime settings" + ); + return std::result::Result::Ok(ResolvedTransportConfig { + file_id: profile.file_id().clone(), + source_path: profile.path().to_path_buf(), + profile_id: profile.profile_id().to_owned(), + selection_source: profile.selection_source(), + effective, + settings, + }); +} + +fn map_endpoints( + sources: std::vec::Vec, + profile: &crate::ResolvedConfigProfile, +) -> ksp_core_lib::Result> { + let mut endpoints = std::vec::Vec::::with_capacity(sources.len()); + for source in sources { + let endpoint_name = source.name.clone(); + let url = ksp_onchain_transport_lib::HttpEndpointUrl::parse(source.url); + let url = match url { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err( + transport_contract_error(profile, "effective endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name), + ); + }, + }; + let roles = map_roles(source.roles, profile, endpoint_name.as_str()); + let roles = match roles { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + endpoints.push(ksp_onchain_transport_lib::HttpEndpointSettings::new( + source.name, + source.enabled, + ksp_onchain_transport_lib::HttpProviderName::new(source.provider), + ksp_onchain_transport_lib::HttpClusterName::new(source.cluster), + url, + std::time::Duration::from_millis(source.connect_timeout_ms), + std::time::Duration::from_millis(source.request_timeout_ms), + source.max_idle_connections_per_host, + roles, + )); + } + return std::result::Result::Ok(endpoints); +} + +fn map_roles( + sources: std::vec::Vec, + profile: &crate::ResolvedConfigProfile, + endpoint_name: &str, +) -> ksp_core_lib::Result> { + let mut roles = std::vec::Vec::::with_capacity(sources.len()); + for source in sources { + let requests_per_second = map_non_zero(source.limits.requests_per_second, profile, "limits.requests_per_second", endpoint_name); + let requests_per_second = match requests_per_second { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let burst_capacity = map_non_zero(source.limits.burst_capacity, profile, "limits.burst_capacity", endpoint_name); + let burst_capacity = match burst_capacity { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let max_concurrent_requests = map_non_zero(source.limits.max_concurrent_requests, profile, "limits.max_concurrent_requests", endpoint_name); + let max_concurrent_requests = match max_concurrent_requests { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let pause_after_rate_limit = match source.limits.pause_after_rate_limit_ms { + std::option::Option::Some(value) => std::option::Option::Some(std::time::Duration::from_millis(value)), + std::option::Option::None => std::option::Option::None, + }; + let limits = ksp_onchain_transport_lib::HttpRoleLimits::new(requests_per_second, burst_capacity, max_concurrent_requests, pause_after_rate_limit); + let mut request_kinds = std::vec::Vec::::with_capacity(source.request_kinds.len()); + for request_kind in source.request_kinds { + request_kinds.push(ksp_onchain_transport_lib::HttpRequestKind::new(request_kind)); + } + roles.push(ksp_onchain_transport_lib::HttpEndpointRoleSettings::new( + ksp_onchain_transport_lib::HttpRoleName::new(source.role), + source.enabled, + request_kinds, + source.priority, + limits, + )); + } + return std::result::Result::Ok(roles); +} + +fn map_non_zero( + value: std::option::Option, + profile: &crate::ResolvedConfigProfile, + field: &'static str, + endpoint_name: &str, +) -> ksp_core_lib::Result> { + let value = match value { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Ok(std::option::Option::None), + }; + let non_zero = std::num::NonZeroU32::new(value); + return match non_zero { + std::option::Option::Some(value) => std::result::Result::Ok(std::option::Option::Some(value)), + std::option::Option::None => std::result::Result::Err( + effective_error(profile, "effective Transport role limit must be greater than zero") + .with_context("field", field) + .with_context("endpoint_name", endpoint_name), + ), + }; +} + +fn transport_contract_error(profile: &crate::ResolvedConfigProfile, reason: &'static str, transport_error: &ksp_core_lib::Error) -> ksp_core_lib::Error { + return effective_error(profile, reason) + .with_context("transport_error_domain", transport_error.code().domain()) + .with_context("transport_error_code", transport_error.code().code()); +} + +fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract") + .with_context("file_id", profile.file_id().as_str()) + .with_context("profile_id", profile.profile_id()) + .with_context("reason", reason); +} + +#[cfg(test)] +#[path = "../unit_tests/transport.rs"] +mod tests; diff --git a/crates/ksp-config-lib/tests/ownership.rs b/crates/ksp-config-lib/tests/ownership.rs index 6e82149..8d18c50 100644 --- a/crates/ksp-config-lib/tests/ownership.rs +++ b/crates/ksp-config-lib/tests/ownership.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/tests/ownership.rs -// version: 2 +// version: 3 //! Workspace ownership audits for KSP application configuration boundaries. @@ -239,7 +239,14 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() { std::result::Result::Ok(value) => non_comment_source(value.as_str()), std::result::Result::Err(_) => continue, }; - for token in ["\".env\"", "\"std.logging.json\"", "\"std.logging.schema.json\"", "\"composite.schema.json\""] { + for token in [ + "\".env\"", + "\"std.logging.json\"", + "\"std.logging.schema.json\"", + "\"std.transport.json\"", + "\"std.transport.schema.json\"", + "\"composite.schema.json\"", + ] { assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display()); } } diff --git a/crates/ksp-config-lib/tests/public_api.rs b/crates/ksp-config-lib/tests/public_api.rs index 41bc4f5..dc37765 100644 --- a/crates/ksp-config-lib/tests/public_api.rs +++ b/crates/ksp-config-lib/tests/public_api.rs @@ -1,8 +1,8 @@ // file: crates/ksp-config-lib/tests/public_api.rs -// version: 16 +// version: 17 -//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, Logging-adapter and -//! management contracts. +//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, +//! Logging/Transport adapters and management contracts. #[test] fn bootstrap_contract_is_available_from_crate_root() { @@ -80,15 +80,18 @@ fn registry_descriptor_inventory_is_available_from_crate_root() { assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}"); if let std::result::Result::Ok(registry) = registry { let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect(); - assert_eq!(descriptors.len(), 3); + assert_eq!(descriptors.len(), 5); assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING); assert_eq!(descriptors[0].kind(), ksp_config_lib::ConfigFileKind::Config); - assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE); - assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING); - let schema_file_id = descriptors[0].schema_file_id(); - assert!(schema_file_id.is_some(), "public descriptor inventory should preserve schema association"); + assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT); + assert_eq!(descriptors[1].kind(), ksp_config_lib::ConfigFileKind::Config); + assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE); + assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING); + assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT); + let schema_file_id = descriptors[1].schema_file_id(); + assert!(schema_file_id.is_some(), "public Transport descriptor should preserve schema association"); if let std::option::Option::Some(schema_file_id) = schema_file_id { - assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING); + assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT); } } } @@ -241,3 +244,13 @@ fn management_contracts_are_available_from_crate_root() { let _ = (save_source_candidate, reveal_effective, reveal_dotenv); assert_ne!(ksp_config_lib::ERROR_CODE_MANAGEMENT_OPERATION_INVALID, ksp_config_lib::ERROR_CODE_PERSISTENCE_WRITE_FAILED); } + +#[test] +fn transport_adapter_contract_is_available_from_crate_root() { + let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_transport_config; + assert!(std::mem::size_of::() > 0); + assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport"); + assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT, "schema.std.transport"); + assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json"); + assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, "std.transport.schema.json"); +} diff --git a/crates/ksp-config-lib/unit_tests/fixtures/std.transport.json b/crates/ksp-config-lib/unit_tests/fixtures/std.transport.json new file mode 100644 index 0000000..f5ac990 --- /dev/null +++ b/crates/ksp-config-lib/unit_tests/fixtures/std.transport.json @@ -0,0 +1,40 @@ +{ + "format_version": 1, + "retry": { + "max_retries": 4, + "initial_backoff_ms": 125, + "max_backoff_ms": 2500 + }, + "default_profile": "secret_test", + "profiles": [ + { + "profile_id": "secret_test", + "endpoints": [ + { + "name": "fixture_private", + "enabled": true, + "provider": "fixture-provider", + "cluster": "fixture-cluster", + "url": "${KSP_SECRET_TRANSPORT_TEST_URL:-https://fallback.invalid}", + "connect_timeout_ms": 750, + "request_timeout_ms": 2500, + "max_idle_connections_per_host": 3, + "roles": [ + { + "role": "default", + "enabled": true, + "request_kinds": ["*"], + "priority": 7, + "limits": { + "requests_per_second": 9, + "burst_capacity": 12, + "max_concurrent_requests": 4, + "pause_after_rate_limit_ms": 650 + } + } + ] + } + ] + } + ] +} diff --git a/crates/ksp-config-lib/unit_tests/registry.rs b/crates/ksp-config-lib/unit_tests/registry.rs index 417b90a..f7d1efc 100644 --- a/crates/ksp-config-lib/unit_tests/registry.rs +++ b/crates/ksp-config-lib/unit_tests/registry.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/unit_tests/registry.rs -// version: 4 +// version: 5 #[test] fn descriptors_expose_complete_registry_in_deterministic_file_id_order() { @@ -7,19 +7,29 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() { assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); if let std::result::Result::Ok(registry) = registry { let descriptors: std::vec::Vec<&super::ConfigFileDescriptor> = registry.descriptors().collect(); - assert_eq!(descriptors.len(), 3); + assert_eq!(descriptors.len(), 5); assert_eq!(descriptors[0].file_id().as_str(), super::FILE_ID_STD_LOGGING); assert_eq!(descriptors[0].kind(), super::ConfigFileKind::Config); assert_eq!(descriptors[0].filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME)); - let schema_file_id = descriptors[0].schema_file_id(); - assert!(schema_file_id.is_some(), "logging descriptor should expose its validation schema"); - if let std::option::Option::Some(schema_file_id) = schema_file_id { + let logging_schema_file_id = descriptors[0].schema_file_id(); + assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema"); + if let std::option::Option::Some(schema_file_id) = logging_schema_file_id { assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); } - assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE); - assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Schema); - assert_eq!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); + assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_STD_TRANSPORT); + assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Config); + assert_eq!(descriptors[1].filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_FILENAME)); + let transport_schema_file_id = descriptors[1].schema_file_id(); + assert!(transport_schema_file_id.is_some(), "transport descriptor should expose its validation schema"); + if let std::option::Option::Some(schema_file_id) = transport_schema_file_id { + assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_TRANSPORT); + } + assert_eq!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE); assert_eq!(descriptors[2].kind(), super::ConfigFileKind::Schema); + assert_eq!(descriptors[3].file_id().as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); + assert_eq!(descriptors[3].kind(), super::ConfigFileKind::Schema); + assert_eq!(descriptors[4].file_id().as_str(), super::FILE_ID_SCHEMA_STD_TRANSPORT); + assert_eq!(descriptors[4].kind(), super::ConfigFileKind::Schema); } } @@ -83,6 +93,31 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() { } } +#[test] +fn defaults_register_transport_document_and_schema_with_distinct_roots() { + let registry = super::ConfigFileRegistry::defaults(); + assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); + if let std::result::Result::Ok(registry) = registry { + let transport_id = super::ConfigFileId::new(super::FILE_ID_STD_TRANSPORT); + let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_TRANSPORT); + assert!(transport_id.is_ok(), "transport file_id should be valid: {transport_id:?}"); + assert!(schema_id.is_ok(), "transport schema file_id should be valid: {schema_id:?}"); + if let (std::result::Result::Ok(transport_id), std::result::Result::Ok(schema_id)) = (transport_id, schema_id) { + let transport = registry.descriptor(&transport_id); + let schema = registry.descriptor(&schema_id); + assert!(transport.is_ok(), "transport descriptor should exist: {transport:?}"); + assert!(schema.is_ok(), "transport schema descriptor should exist: {schema:?}"); + if let (std::result::Result::Ok(transport), std::result::Result::Ok(schema)) = (transport, schema) { + assert_eq!(transport.kind(), super::ConfigFileKind::Config); + assert_eq!(transport.filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_FILENAME)); + assert_eq!(transport.schema_file_id(), std::option::Option::Some(&schema_id)); + assert_eq!(schema.kind(), super::ConfigFileKind::Schema); + assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME)); + } + } + } +} + #[test] fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() { let registry = super::ConfigFileRegistry::defaults(); diff --git a/crates/ksp-config-lib/unit_tests/transport.rs b/crates/ksp-config-lib/unit_tests/transport.rs new file mode 100644 index 0000000..d1dd769 --- /dev/null +++ b/crates/ksp-config-lib/unit_tests/transport.rs @@ -0,0 +1,202 @@ +// file: crates/ksp-config-lib/unit_tests/transport.rs +// version: 1 + +#[test] +fn fixture_transport_profile_maps_complete_runtime_contract() { + let engine = fixture_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); + let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "fixture Transport Config should map: {resolved:?}"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_TRANSPORT); + assert_eq!(resolved.profile_id(), "secret_test"); + assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile); + assert_eq!(resolved.settings().retry().max_retries(), 4); + assert_eq!(resolved.settings().retry().initial_backoff(), std::time::Duration::from_millis(125)); + assert_eq!(resolved.settings().retry().max_backoff(), std::time::Duration::from_millis(2500)); + assert_eq!(resolved.settings().endpoints().len(), 1); + let endpoint = &resolved.settings().endpoints()[0]; + assert_eq!(endpoint.name(), "fixture_private"); + assert_eq!(endpoint.provider().as_str(), "fixture-provider"); + assert_eq!(endpoint.cluster().as_str(), "fixture-cluster"); + assert_eq!(endpoint.url().as_str(), "https://fallback.invalid"); + assert_eq!(endpoint.connect_timeout(), std::time::Duration::from_millis(750)); + assert_eq!(endpoint.request_timeout(), std::time::Duration::from_millis(2500)); + assert_eq!(endpoint.max_idle_connections_per_host(), std::option::Option::Some(3)); + assert_eq!(endpoint.roles().len(), 1); + let role = &endpoint.roles()[0]; + assert_eq!(role.role().as_str(), "default"); + assert!(role.enabled()); + assert_eq!(role.priority(), 7); + assert_eq!(role.request_kinds().len(), 1); + assert!(role.request_kinds()[0].is_wildcard()); + assert_eq!(role.limits().requests_per_second().map(std::num::NonZeroU32::get), std::option::Option::Some(9)); + assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12)); + assert_eq!(role.limits().max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(4)); + assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650))); +} + +#[test] +fn committed_transport_document_maps_default_and_explicit_profiles() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); + let default = engine.load_resolved_transport_config(std::option::Option::None, &environment); + let mainnet = engine.load_resolved_transport_config(std::option::Option::Some("mainnet_public"), &environment); + assert!(default.is_ok(), "committed default Transport profile should map: {default:?}"); + assert!(mainnet.is_ok(), "committed explicit Transport profile should map: {mainnet:?}"); + if let std::result::Result::Ok(default) = default { + assert_eq!(default.profile_id(), "devnet_public"); + assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet"); + assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com"); + } + if let std::result::Result::Ok(mainnet) = mainnet { + assert_eq!(mainnet.profile_id(), "mainnet_public"); + assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit); + assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta"); + assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com"); + } +} + +#[test] +fn transport_profile_preserves_global_and_profile_origin() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT); + let file_id = match file_id { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let profile = engine.load_resolved_profile(&file_id, std::option::Option::None); + assert!(profile.is_ok(), "committed Transport profile should resolve: {profile:?}"); + if let std::result::Result::Ok(profile) = profile { + assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global)); + assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile)); + assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global)); + } +} + +#[test] +fn secret_transport_url_is_runtime_available_but_safe_projection_is_redacted() { + let engine = fixture_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let canary = "https://secret-provider.invalid/?api-key=transport-secret-canary"; + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), canary.to_owned()); + let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); + let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "secret Transport endpoint should map without being rejected: {resolved:?}"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(resolved.effective().sensitivity(), crate::ConfigSensitivity::Secret); + assert_eq!(resolved.settings().endpoints()[0].url().as_str(), canary); + let safe_url = resolved.effective().safe_value().pointer("/endpoints/0/url").and_then(serde_json::Value::as_str); + assert_eq!(safe_url, std::option::Option::Some(crate::REDACTED_CONFIG_VALUE)); + let debug = format!("{resolved:?}"); + assert!(!debug.contains("transport-secret-canary")); + assert!(debug.contains(crate::REDACTED_CONFIG_VALUE)); +} + +#[test] +fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() { + let engine = fixture_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), "https://process.invalid".to_owned()); + let mut dotenv = std::collections::BTreeMap::::new(); + dotenv.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), "https://dotenv.invalid".to_owned()); + let environment = crate::ConfigEnvironment::from_maps(process, dotenv); + let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment); + assert!(resolved.is_ok(), "secret Transport environment precedence should map: {resolved:?}"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://process.invalid"); + let provenance = resolved.effective().provenance_at("/endpoints/0/url"); + assert!(provenance.is_some(), "endpoint URL should retain environment provenance"); + if let std::option::Option::Some(provenance) = provenance { + assert_eq!(provenance.len(), 1); + assert_eq!(provenance[0].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process)); + assert_eq!(provenance[0].variable_name(), std::option::Option::Some("KSP_SECRET_TRANSPORT_TEST_URL")); + } +} + +#[test] +fn invalid_secret_transport_url_is_effective_config_error_without_secret_leak() { + let engine = fixture_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let canary = "transport-invalid-secret-canary"; + let mut process = std::collections::BTreeMap::::new(); + process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), canary.to_owned()); + let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); + let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment); + assert!(resolved.is_err(), "invalid secret-derived endpoint URL must fail effective mapping"); + if let std::result::Result::Err(error) = resolved { + assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID); + let debug = format!("{error:?}"); + assert!(!debug.contains(canary)); + assert!(error.context().iter().any(|item| -> bool { + return item.key() == "transport_error_code" && item.value() == "invalid_settings"; + })); + } +} + +fn fixture_engine() -> ksp_core_lib::Result { + let workspace = workspace_root(); + let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures"); + let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture_root, workspace.join("config/schemas")); + let bootstrap = match bootstrap { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let registry = crate::ConfigFileRegistry::defaults(); + let registry = match registry { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry)); +} + +fn committed_engine() -> ksp_core_lib::Result { + let workspace = workspace_root(); + let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); + let bootstrap = match bootstrap { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let registry = crate::ConfigFileRegistry::defaults(); + let registry = match registry { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry)); +} + +fn workspace_root() -> std::path::PathBuf { + return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); +} diff --git a/deltas/0.2.1/pre.006.md b/deltas/0.2.1/pre.006.md new file mode 100644 index 0000000..0b23be8 --- /dev/null +++ b/deltas/0.2.1/pre.006.md @@ -0,0 +1,263 @@ + + + +# Delta `v0.2.1-pre.006` + +## Base + +Base attendue : + +```text +v0.2.1-pre.005-fix.001 +``` + +Cette base a été validée localement par le user avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, les tests Transport, Config, Core et `cargo test --workspace`. Les canaries workspace de dépendances et de targets Logging sont également propres. + +Version Cargo cible : + +```text +0.2.1-pre.6 +``` + +## Objectif + +Matérialiser la frontière de configuration standard de `ksp-onchain-transport-lib` sans inverser l'ownership : + +```text +Config document / environment -> ksp-config-lib adapter -> HttpTransportSettings +``` + +Transport ne lit toujours ni Config, ni `.env`, ni `KSP_*` / `KSPB_*`. + +## Nouveau document standard Transport + +Nouvelles ressources gérées : + +```text +cfg.std.transport -> config/std.transport.json +schema.std.transport -> config/schemas/std.transport.schema.json +``` + +Le document standard possède : + +- `format_version = 1` ; +- `retry` au niveau global ; +- `default_profile` autonome ; +- `profiles[]` contenant les endpoints propres au profil ; +- endpoints avec identité/provider/cluster/URL/timeouts/idle-pool ; +- rôles avec capabilities/request kinds, priorité et limites RPS/burst/concurrence/cooldown. + +Le profil par défaut committé est `devnet_public`. Le document fournit également `mainnet_public`. + +Les URLs publiques utilisent : + +```text +KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL +KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL +``` + +avec fallback vers les endpoints publics Solana correspondants. + +## Schema + +`config/schemas/std.transport.schema.json` : + +- Draft 2020-12 ; +- `additionalProperties = false` aux frontières structurées ; +- `format_version` fixé à `1` ; +- invariants numériques positifs pour timeouts/limites ; +- `request_kinds` non vide et wildcard `*` exclusif ; +- profils et endpoints non vides ; +- descriptors non vides sans whitespace de bord. + +Le schema protège la forme source ; la validation finale des invariants runtime reste possédée par `HttpTransportSettings::validate()`. + +## Exemple provider-neutral + +`config/examples/std.transport.example.json` démontre un profil `mainnet_mixed` avec : + +- endpoint public de fallback ; +- endpoint privé prioritaire ; +- URL privée provenant de `KSP_SECRET_SOLANA_HTTP_URL`. + +Aucun credential réel n'est committé. + +`.env.example` inventorie les deux URLs publiques et documente l'URL privée optionnelle sous forme commentée. + +## Registry Config + +Nouveaux contrats : + +```text +FILE_ID_STD_TRANSPORT +FILE_ID_SCHEMA_STD_TRANSPORT +DEFAULT_STD_TRANSPORT_FILENAME +DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME +``` + +`ConfigFileRegistry::defaults()` contient désormais cinq descriptors ordonnés : + +```text +cfg.std.logging +cfg.std.transport +schema.composite +schema.std.logging +schema.std.transport +``` + +Le document Transport référence explicitement son schema. + +## Adapter Config -> Transport + +Nouvelle surface publique : + +```text +ResolvedTransportConfig +ConfigDocumentEngine::load_resolved_transport_config(...) +``` + +L'adapter : + +1. charge et valide `cfg.std.transport` ; +2. sélectionne le `default_profile` ou le profil explicite ; +3. résout les placeholders via `ConfigEnvironment` ; +4. préserve valeur réelle, valeur sûre, sensibilité et provenance JSON Pointer ; +5. décode le contrat effectif Config ; +6. convertit les scalaires `*_ms` en `std::time::Duration` ; +7. construit les newtypes/roles/limits/endpoints Transport ; +8. parse les URLs via `HttpEndpointUrl` ; +9. construit `HttpTransportSettings` ; +10. délègue la validation structurelle finale à Transport. + +La dépendance est donc : + +```text +ksp-config-lib -> ksp-onchain-transport-lib +``` + +Le firewall inverse reste inchangé : Transport ne dépend pas de Config. + +## Secrets, safe view et provenance + +Contrairement au document Logging, le document Transport peut légitimement consommer une valeur `Secret` pour une URL endpoint complète. + +`ResolvedTransportConfig` : + +- expose `effective()` pour conserver le réel/safe/provenance ; +- expose `settings()` / `into_settings()` au runtime légitime ; +- n'imprime dans `Debug` que la projection `ResolvedConfigJson` sûre ; +- ne copie pas l'URL réelle dans le contexte d'une erreur d'adaptation. + +Un échec Transport est projeté vers `ERROR_CODE_EFFECTIVE_CONFIG_INVALID` avec uniquement le domaine/code Transport et les identités non sensibles utiles. + +## Tests + +Nouveau module unitaire `unit_tests/transport.rs` : + +- mapping complet des scalaires Config vers le runtime Transport ; +- validation des profils committés `devnet_public` / `mainnet_public` ; +- provenance top-level `Global` pour `retry` et `Profile` pour `endpoints` ; +- URL `KSP_SECRET_*` disponible au runtime mais redacted dans la safe view et `Debug` ; +- precedence process > `.env` ; +- provenance JSON Pointer de l'URL endpoint ; +- URL secrète invalide -> `ERROR_CODE_EFFECTIVE_CONFIG_INVALID` sans fuite du canary. + +Le registry gagne un test dédié au document/schema Transport et la surface publique Config gagne un canary de disponibilité de l'adapter. + +La surface Config déclare désormais : + +```text +95 tests unitaires +18 tests d'intégration +113 tests au total +``` + +## Ownership et documentation + +`DEP-TRANSPORT-005` précise maintenant explicitement que `ksp-config-lib` peut dépendre des crates Transport pour posséder les adapters Config -> runtime settings, jamais l'inverse. + +Le README/USAGE/TODO de Config est synchronisé avec `std.transport`. Le ROADMAP reçoit uniquement la mise à jour synthétique normale de l'état `0.2.1`; il ne journalise pas les détails du delta. + +## Dépendances + +Nouvelle dépendance interne de `ksp-config-lib` : + +```toml +ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" } +``` + +Aucune nouvelle dépendance externe et aucune nouvelle feature externe. + +## Fichiers ajoutés + +```text +config/examples/std.transport.example.json +config/schemas/std.transport.schema.json +config/std.transport.json +crates/ksp-config-lib/src/transport.rs +crates/ksp-config-lib/unit_tests/fixtures/std.transport.json +crates/ksp-config-lib/unit_tests/transport.rs +deltas/0.2.1/pre.006.md +``` + +## Fichiers modifiés + +```text +.env.example +Cargo.toml +ROADMAP.md +crates/ksp-config-lib/Cargo.toml +crates/ksp-config-lib/README.md +crates/ksp-config-lib/TODO.md +crates/ksp-config-lib/USAGE.md +crates/ksp-config-lib/src/lib.rs +crates/ksp-config-lib/src/registry.rs +crates/ksp-config-lib/tests/ownership.rs +crates/ksp-config-lib/tests/public_api.rs +crates/ksp-config-lib/unit_tests/registry.rs +docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md +docs/rules/RULES_DEPENDENCIES.md +``` + +## Validation de génération + +Effectuée dans le sandbox : + +- parsing TOML des manifests modifiés ; +- parsing JSON des nouvelles ressources ; +- validation Draft 2020-12 des deux documents Transport et de la fixture contre le nouveau schema via l'implémentation Python `jsonschema` disponible ; +- contrôle des lignes Rust/TOML modifiées <= 160 colonnes ; +- contrôle des EOF et headers ; +- scan statique des interdits KSP sur le nouveau code de production ; +- contrôle de l'inventaire des variables `KSP_*` dans `.env.example` ; +- contrôle différentiel des fichiers livrés. + +Non exécutée dans le sandbox : validation Cargo/Rust, les binaires `cargo`, `rustc` et `rustfmt` n'étant pas disponibles. + +Après application : + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-config-lib +cargo test -p ksp-onchain-transport-lib +cargo test -p ksp-core-lib +cargo test --workspace +cargo tree -p ksp-config-lib +cargo tree -p ksp-config-lib -d +cargo tree -p ksp-config-lib -e features +cargo tree -p ksp-config-lib -e normal +``` + +Le `cargo tree` Config est requis cette fois : la dépendance interne Config -> Transport modifie réellement le graphe de la crate Config, même sans nouvelle dépendance externe. + +## Suite + +Tranche suivante prévue : + +```text +0.2.1-pre.007 +``` + +Périmètre : completeness/canaries finales, smoke réseau opt-in, audit `cargo tree`, README/USAGE Transport, documentation de clôture, prompt `0.2.2` et préparation de `rel.001`. diff --git a/docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md b/docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md index a110e9a..797aa12 100644 --- a/docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md +++ b/docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md @@ -1,5 +1,5 @@ - + # `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation @@ -876,7 +876,7 @@ La release peut devenir stable seulement si : | `pre.003` | **réalisé** : endpoint client + pool logique + rôles/capabilities + priorité/fairness/fallback + snapshots sûrs | | `pre.004` | **réalisé** : RPS/burst/concurrence/cooldown + deadline commune + retry/backoff + classification retry/no-resend | | `pre.005` | **réalisé** : exécution HTTP JSON-RPC + `getHealth`, `getVersion`, `getGenesisHash`, `getBalance` + fixtures déterministes + centralisation des canaries workspace dans Core | -| `pre.006` | `std.transport` schema/document/example + registry Config + adapter Config -> Transport + sensibilité/env tests | +| `pre.006` | **réalisé** : `std.transport` schema/document/example + registry Config + adapter Config -> Transport + sensibilité/provenance/env tests | | `pre.007` | completeness/canaries, smoke opt-in, `cargo tree`, README/USAGE, docs finales, prompt `0.2.2`, préparation `rel.001` | Ce découpage est révisable si une tranche dépasse le budget ; la release réduite, contrairement au scope initial, reste raisonnablement clôturable dans la session. @@ -953,6 +953,23 @@ Restent à `pre.005` : exécution HTTP JSON-RPC réelle et wrappers typés `getH Restent à `pre.006` : document/schema/exemple `std.transport`, enregistrement Config, adapter Config -> Transport et tests de sensibilité/provenance/env. +### 22.5 État après `0.2.1-pre.006` + +`pre.006` matérialise la frontière de configuration standard du Transport sans créer de dépendance inverse : + +- `workspace.package.version = 0.2.1-pre.6` ; +- `config/std.transport.json` et `config/schemas/std.transport.schema.json` deviennent des ressources Config gérées, avec `cfg.std.transport` / `schema.std.transport` dans le registry ; +- le document standard possède `retry` en global et des `endpoints` par profil ; `devnet_public` est le profil autonome par défaut et `mainnet_public` reste sélectionnable explicitement ; +- `config/examples/std.transport.example.json` illustre un pool mixte public/privé, y compris une URL complète provenant d’un `KSP_SECRET_*` ; +- `.env.example` inventorie les deux overrides publics standard et l’override secret d’exemple sans committer de credential réel ; +- `ksp-config-lib` dépend désormais de `ksp-onchain-transport-lib` dans la direction autorisée Config -> Transport, tandis que le firewall Transport -> Config reste inchangé ; +- `ConfigDocumentEngine::load_resolved_transport_config` sélectionne le profil, résout l’environnement, convertit les scalaires `*_ms` en `Duration`, construit les rôles/limites/endpoints puis délègue la validation finale à `HttpTransportSettings::validate()` ; +- `ResolvedTransportConfig` conserve le profil, la provenance détaillée et la projection safe, et expose les settings runtime réels ; son `Debug` n’expose pas les URLs secrètes ; +- les tests couvrent le document committé, les origines global/profile, la precedence process > `.env`, la provenance JSON Pointer, la redaction `KSP_SECRET_*` et l’échec d’une URL secrète invalide sans fuite de canary ; +- `DEP-TRANSPORT-005` explicite maintenant que Config peut dépendre des crates Transport pour posséder les adapters, jamais l’inverse. + +Restent à `pre.007` : completeness/canaries finales, smoke réseau opt-in, audit `cargo tree`, README/USAGE, documentation de clôture, prompt `0.2.2` et préparation de `rel.001`. + ## 23. Séquence `0.2.x` recalibrée ```text diff --git a/docs/rules/RULES_DEPENDENCIES.md b/docs/rules/RULES_DEPENDENCIES.md index 2ac6826..fcde9a0 100644 --- a/docs/rules/RULES_DEPENDENCIES.md +++ b/docs/rules/RULES_DEPENDENCIES.md @@ -1,5 +1,5 @@ - + # Règles des dépendances KSP