v0.2.9-pre.011

This commit is contained in:
2026-08-24 19:53:45 +02:00
parent 3198cc5666
commit 21ed5f88da
14 changed files with 2285 additions and 2121 deletions

View File

@@ -6,7 +6,7 @@ resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.2.9-pre.10.fix.1"
version = "0.2.9-pre.11"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,13 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.transport:v2",
"title": "KSP standard HTTP + WebSocket Transport configuration",
"$id": "urn:ksp:schema:std.transport:v3",
"title": "KSP standard HTTP + WebSocket + Yellowstone gRPC Transport configuration",
"oneOf": [
{
"$ref": "#/$defs/documentV1"
},
{
"$ref": "#/$defs/documentV2"
},
{
"$ref": "#/$defs/documentV3"
}
],
"$defs": {
@@ -455,6 +458,275 @@
}
}
}
},
"grpcPositiveMs": {
"type": "integer",
"minimum": 1,
"maximum": 300000
},
"grpcCapacity": {
"type": "integer",
"minimum": 1,
"maximum": 65536
},
"grpcMessageSize": {
"type": "integer",
"minimum": 1,
"maximum": 536870912
},
"grpcReconnect": {
"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/grpcPositiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/grpcPositiveMs"
}
}
},
"grpcReconnectOverride": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"max_retries": {
"type": "integer",
"minimum": 0,
"maximum": 100
},
"initial_backoff_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/grpcPositiveMs"
}
}
},
"grpcSession": {
"type": "object",
"additionalProperties": false,
"required": [
"connect_timeout_ms",
"unary_timeout_ms",
"close_timeout_ms",
"reconnect",
"request_channel_capacity",
"update_channel_capacity",
"max_inbound_message_size_bytes",
"max_outbound_message_size_bytes"
],
"properties": {
"connect_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"unary_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"close_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"reconnect": {
"$ref": "#/$defs/grpcReconnect"
},
"request_channel_capacity": {
"$ref": "#/$defs/grpcCapacity"
},
"update_channel_capacity": {
"$ref": "#/$defs/grpcCapacity"
},
"max_inbound_message_size_bytes": {
"$ref": "#/$defs/grpcMessageSize"
},
"max_outbound_message_size_bytes": {
"$ref": "#/$defs/grpcMessageSize"
}
}
},
"grpcSessionOverride": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"connect_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"unary_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"close_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"reconnect": {
"$ref": "#/$defs/grpcReconnectOverride"
},
"request_channel_capacity": {
"$ref": "#/$defs/grpcCapacity"
},
"update_channel_capacity": {
"$ref": "#/$defs/grpcCapacity"
},
"max_inbound_message_size_bytes": {
"$ref": "#/$defs/grpcMessageSize"
},
"max_outbound_message_size_bytes": {
"$ref": "#/$defs/grpcMessageSize"
}
}
},
"grpcMetadata": {
"type": "object",
"additionalProperties": false,
"required": [
"key",
"value"
],
"properties": {
"key": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[0-9a-z_.-]+$"
},
"value": {
"type": "string",
"maxLength": 8192
}
}
},
"grpcEndpoint": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"enabled",
"provider",
"cluster",
"protocol",
"url"
],
"properties": {
"name": {
"$ref": "#/$defs/descriptor"
},
"enabled": {
"type": "boolean"
},
"provider": {
"$ref": "#/$defs/descriptor"
},
"cluster": {
"$ref": "#/$defs/descriptor"
},
"protocol": {
"const": "solana_yellowstone"
},
"url": {
"type": "string",
"minLength": 1,
"maxLength": 8192
},
"metadata": {
"type": "array",
"maxItems": 64,
"items": {
"$ref": "#/$defs/grpcMetadata"
}
},
"secret_metadata": {
"type": "array",
"maxItems": 64,
"items": {
"$ref": "#/$defs/grpcMetadata"
}
},
"session": {
"$ref": "#/$defs/grpcSessionOverride"
}
}
},
"profileV3": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id",
"endpoints",
"ws_endpoints"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"endpoints": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/httpEndpoint"
}
},
"ws_endpoints": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/wsEndpoint"
}
},
"grpc_endpoints": {
"type": "array",
"minItems": 1,
"maxItems": 128,
"items": {
"$ref": "#/$defs/grpcEndpoint"
}
}
}
},
"documentV3": {
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"retry",
"ws_defaults",
"grpc_defaults",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 3
},
"retry": {
"$ref": "#/$defs/retry"
},
"ws_defaults": {
"$ref": "#/$defs/wsSession"
},
"grpc_defaults": {
"$ref": "#/$defs/grpcSession"
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profileV3"
}
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
{
"format_version": 2,
"format_version": 3,
"retry": {
"max_retries": 2,
"initial_backoff_ms": 100,
@@ -22,6 +22,20 @@
"max_frame_size_bytes": 16777216,
"max_write_buffer_size_bytes": 1048576
},
"grpc_defaults": {
"connect_timeout_ms": 10000,
"unary_timeout_ms": 10000,
"close_timeout_ms": 5000,
"reconnect": {
"max_retries": 5,
"initial_backoff_ms": 250,
"max_backoff_ms": 5000
},
"request_channel_capacity": 128,
"update_channel_capacity": 256,
"max_inbound_message_size_bytes": 67108864,
"max_outbound_message_size_bytes": 67108864
},
"default_profile": "devnet_public",
"profiles": [
{
@@ -105,6 +119,57 @@
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
}
]
},
{
"profile_id": "publicnode_mainnet",
"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
}
}
]
}
],
"ws_endpoints": [
{
"name": "solana_mainnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
}
],
"grpc_endpoints": [
{
"name": "publicnode_solana_mainnet_yellowstone",
"enabled": true,
"provider": "publicnode",
"cluster": "mainnet-beta",
"protocol": "solana_yellowstone",
"url": "https://solana-yellowstone-grpc.publicnode.com:443"
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# ksp-config-lib
@@ -81,7 +81,7 @@ 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 Transport les accepte pour les URLs HTTP et WebSocket : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
## Documentation
@@ -89,7 +89,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
- [`TODO.md`](TODO.md) — points explicitement différés ;
- [`../../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) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V2 HTTP + WebSocket, avec lecture backward du V1 HTTP-only ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V3 HTTP + WebSocket + Yellowstone gRPC, avec lecture backward des V1/V2 ;
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Utilisation de ksp-config-lib
@@ -125,9 +125,9 @@ 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 + WebSocket depuis Config
### 4.1 Construire le Transport HTTP + WebSocket + Yellowstone gRPC depuis Config
Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` :
Config possède l'adapter du document `std.transport` vers les contrats runtime de `ksp-onchain-transport-lib` :
```rust
let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) {
@@ -137,18 +137,30 @@ let transport = match engine.load_resolved_transport_config(std::option::Option:
let http_settings = transport.http_settings();
let ws_settings = transport.ws_settings();
let _ = (http_settings, ws_settings);
let grpc_settings = transport.grpc_settings();
let _ = (http_settings, ws_settings, grpc_settings);
```
`std.transport` V2 conserve `retry` et `profiles[].endpoints[]` pour HTTP, ajoute `ws_defaults` et `profiles[].ws_endpoints[]`, puis accepte les protocoles WebSocket `kind = "solana_standard"` et `kind = "helius_laserstream"`. Ce second discriminateur appartient exclusivement au namespace WebSocket et mappe vers `WsProtocolKind::HeliusLaserStream`; il ne préfigure aucun contrat LaserStream gRPC. Un `ws_endpoints[].session` optionnel surcharge seulement les paramètres génériques de `WsSessionSettings`.
`std.transport` V3 conserve intégralement les shapes V1/V2, ajoute `grpc_defaults` au niveau global et permet `profiles[].grpc_endpoints[]`. Les profils V3 peuvent rester HTTP + WebSocket seulement : l'absence de `grpc_endpoints` mappe vers `None` et n'invente aucun endpoint. La lecture V1 HTTP-only et V2 HTTP + WebSocket reste stricte et backward-compatible.
Pour Helius LaserStream WebSocket, l'exemple versionné couvre explicitement les deux réseaux supportés par ce contrat : mainnet via `wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}` et devnet via `wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}`. Les deux réseaux vivent dans des profils Config distincts afin de ne pas mélanger des clusters dans un même profil logique. Config reste l'unique propriétaire de `KSP_SECRET_HELIUS_API_KEY` : il résout la clé dans l'URL effective et transmet au Transport un `WsEndpointUrl` utilisable au runtime. Dans `safe_value`, Config conserve les segments littéraux non sensibles d'une chaîne composée et remplace uniquement chaque segment secret par `********` ; les projections deviennent donc respectivement `wss://mainnet.helius-rpc.com/?api-key=********` et `wss://devnet.helius-rpc.com/?api-key=********`. Les représentations `Debug` restent sûres et n'exposent jamais la clé réelle. Transport ne lit jamais directement l'environnement.
Un endpoint gRPC V3 sépare explicitement :
Le même schema enregistré conserve la lecture stricte du V1 historique : dans ce cas `http_settings()` reste disponible et `ws_settings()` retourne `None`. Aucun `WsTransportSettings` vide n'est inventé pour simuler l'absence de WebSocket.
```text
provider = description de l'opérateur/exécution, par exemple publicnode
protocol = solana_yellowstone
metadata = metadata non secrète
secret_metadata = metadata dont la valeur doit avoir une provenance KSP_SECRET_*/KSPB_SECRET_*
```
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs HTTP et WebSocket 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.
`protocol` n'est pas un nouveau `WsProtocolKind` et ne transforme pas PublicNode en protocole. Config résout les placeholders, vérifie la classe de sensibilité des metadata puis construit `YellowstoneGrpcTransportSettings`. Transport ne lit jamais l'environnement. Les valeurs de `secret_metadata` sont disponibles au runtime mais redacted dans `safe_value` et dans les représentations `Debug`.
La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
Le provider affiche `solana-yellowstone-grpc.publicnode.com:443` ; le profil versionné `publicnode_mainnet` le représente sous la forme URL TLS requise par Transport, `https://solana-yellowstone-grpc.publicnode.com:443`, sans credential. Aucun hostname Testnet n'est versionné tant que sa valeur exacte n'est pas confirmée depuis une source opérateur/officielle.
L'accesseur historique `into_transport_settings()` conserve volontairement son tuple `(HTTP, Option<WS>)`. Un consumer ayant besoin des trois backends utilise `into_all_transport_settings()` ou les accesseurs séparés afin de ne pas casser silencieusement les consumers V2.
Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC.
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
### 4.2 Résoudre le répertoire Wallet depuis Config

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 17
// version: 18
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -9,8 +9,9 @@
//!
//! 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. Standard Logging, HTTP Transport and Wallet documents map explicitly to their runtime consumer contracts, while the management surface
//! provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC) and Wallet documents map explicitly to their runtime consumer
//! 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;
@@ -154,9 +155,9 @@ 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 + WebSocket Transport configuration document.
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document.
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
/// Default physical filename for the standard Wallet configuration document.
pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
@@ -168,13 +169,13 @@ pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
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 + WebSocket Transport JSON Schema document.
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
/// Logical file identifier for the standard Logging configuration document.
pub use self::registry::FILE_ID_STD_LOGGING;
/// Logical file identifier for the standard HTTP + WebSocket Transport configuration document.
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub use self::registry::FILE_ID_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet configuration document.
pub use self::registry::FILE_ID_STD_WALLET;
@@ -188,7 +189,7 @@ 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 Transport configuration mapped to HTTP and optional WebSocket runtime settings.
/// Effective standard Transport configuration mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
pub use self::transport::ResolvedTransportConfig;
/// Effective standard Wallet configuration resolved to validated filesystem roots.
pub use self::wallet::ResolvedWalletConfig;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 9
// version: 10
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
@@ -11,9 +11,9 @@ pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
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 + WebSocket Transport configuration document.
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document.
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Default physical filename for the standard Wallet configuration document.
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
@@ -25,13 +25,13 @@ pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-w
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// 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 + WebSocket Transport JSON Schema document.
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the standard Wallet JSON Schema document.
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
/// Logical file identifier for the standard Logging configuration document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard HTTP + WebSocket Transport configuration document.
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Logical file identifier for the standard Wallet configuration document.
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-config-lib/src/transport.rs
// version: 4
// version: 5
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP and optional WebSocket runtime contracts.
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime contracts.
#[derive(Clone, Eq, PartialEq)]
pub struct ResolvedTransportConfig {
file_id: crate::ConfigFileId,
@@ -11,6 +11,7 @@ pub struct ResolvedTransportConfig {
effective: crate::ResolvedConfigJson,
settings: ksp_onchain_transport_lib::HttpTransportSettings,
ws_settings: std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
grpc_settings: std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>,
}
impl ResolvedTransportConfig {
@@ -61,7 +62,7 @@ impl ResolvedTransportConfig {
return &self.settings;
}
/// Returns validated WebSocket Transport settings when the selected document uses format V2.
/// Returns validated WebSocket Transport settings when the selected document uses format V2 or V3.
///
/// Backward-compatible V1 HTTP-only documents return [`std::option::Option::None`].
#[must_use]
@@ -69,19 +70,42 @@ impl ResolvedTransportConfig {
return self.ws_settings.as_ref();
}
/// Returns validated Yellowstone gRPC Transport settings when the selected V3 profile declares gRPC endpoints.
///
/// V1/V2 documents and V3 profiles without `grpc_endpoints` return [`std::option::Option::None`].
#[must_use]
pub fn grpc_settings(&self) -> std::option::Option<&ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings> {
return self.grpc_settings.as_ref();
}
/// 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;
}
/// Consumes this resolved Config and returns both HTTP and optional WebSocket runtime settings.
/// Consumes this resolved Config and returns HTTP plus optional WebSocket runtime settings.
///
/// This compatibility accessor intentionally preserves the V2 tuple shape. Use [`Self::into_all_transport_settings`] when Yellowstone gRPC settings are
/// also required.
#[must_use]
pub fn into_transport_settings(
self,
) -> (ksp_onchain_transport_lib::HttpTransportSettings, std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>) {
return (self.settings, self.ws_settings);
}
/// Consumes this resolved Config and returns HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
#[must_use]
pub fn into_all_transport_settings(
self,
) -> (
ksp_onchain_transport_lib::HttpTransportSettings,
std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>,
) {
return (self.settings, self.ws_settings, self.grpc_settings);
}
}
impl std::fmt::Debug for ResolvedTransportConfig {
@@ -94,15 +118,17 @@ impl std::fmt::Debug for ResolvedTransportConfig {
.field("selection_source", &self.selection_source)
.field("effective", &self.effective)
.field("has_ws_settings", &self.ws_settings.is_some())
.field("has_grpc_settings", &self.grpc_settings.is_some())
.finish_non_exhaustive();
}
}
impl crate::ConfigDocumentEngine {
/// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket runtime settings.
/// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket and Yellowstone
/// gRPC runtime settings.
///
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed
/// because Transport URL wrappers own runtime redaction. V1 documents remain HTTP-only; V2 documents require WebSocket defaults and endpoints.
/// because Transport URL wrappers own runtime redaction. V1 remains HTTP-only, V2 adds WebSocket and V3 can additionally map Yellowstone gRPC.
pub fn load_resolved_transport_config(
&self,
requested_profile: std::option::Option<&str>,
@@ -121,7 +147,8 @@ impl crate::ConfigDocumentEngine {
return resolve_transport_profile(&profile, environment);
}
/// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket runtime adapters while preserving selection provenance.
/// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket and Yellowstone gRPC runtime adapters while preserving
/// selection provenance.
///
/// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.transport`.
pub fn resolve_transport_config_profile(
@@ -148,9 +175,13 @@ struct EffectiveTransportSource {
retry: EffectiveRetrySource,
#[serde(default)]
ws_defaults: std::option::Option<EffectiveWsSessionSource>,
#[serde(default)]
grpc_defaults: std::option::Option<EffectiveGrpcSessionSource>,
endpoints: std::vec::Vec<EffectiveEndpointSource>,
#[serde(default)]
ws_endpoints: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
#[serde(default)]
grpc_endpoints: std::option::Option<std::vec::Vec<EffectiveGrpcEndpointSource>>,
}
#[derive(serde::Deserialize)]
@@ -255,6 +286,72 @@ struct EffectiveWsReconnectOverrideSource {
max_backoff_ms: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcReconnectSource {
max_retries: u32,
initial_backoff_ms: u64,
max_backoff_ms: u64,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcReconnectOverrideSource {
max_retries: std::option::Option<u32>,
initial_backoff_ms: std::option::Option<u64>,
max_backoff_ms: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcSessionSource {
connect_timeout_ms: u64,
unary_timeout_ms: u64,
close_timeout_ms: u64,
reconnect: EffectiveGrpcReconnectSource,
request_channel_capacity: usize,
update_channel_capacity: usize,
max_inbound_message_size_bytes: usize,
max_outbound_message_size_bytes: usize,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcSessionOverrideSource {
connect_timeout_ms: std::option::Option<u64>,
unary_timeout_ms: std::option::Option<u64>,
close_timeout_ms: std::option::Option<u64>,
reconnect: std::option::Option<EffectiveGrpcReconnectOverrideSource>,
request_channel_capacity: std::option::Option<usize>,
update_channel_capacity: std::option::Option<usize>,
max_inbound_message_size_bytes: std::option::Option<usize>,
max_outbound_message_size_bytes: std::option::Option<usize>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcMetadataSource {
key: String,
value: String,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcEndpointSource {
name: String,
enabled: bool,
provider: String,
cluster: String,
protocol: String,
url: String,
#[serde(default)]
metadata: std::vec::Vec<EffectiveGrpcMetadataSource>,
#[serde(default)]
secret_metadata: std::vec::Vec<EffectiveGrpcMetadataSource>,
#[serde(default)]
session: std::option::Option<EffectiveGrpcSessionOverrideSource>,
}
fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Transport Config profile");
let effective = profile.resolve_effective_environment_detailed(environment);
@@ -294,16 +391,26 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let grpc_settings = map_optional_grpc_settings(format_version, source.grpc_defaults, source.grpc_endpoints, &effective, profile);
let grpc_settings = match grpc_settings {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let ws_endpoint_count = match ws_settings.as_ref() {
std::option::Option::Some(value) => value.endpoints().len(),
std::option::Option::None => 0_usize,
};
let grpc_endpoint_count = match grpc_settings.as_ref() {
std::option::Option::Some(value) => value.endpoints().len(),
std::option::Option::None => 0_usize,
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
profile_id = profile.profile_id(),
format_version,
http_endpoint_count = settings.endpoints().len(),
ws_endpoint_count,
grpc_endpoint_count,
"mapped standard Transport Config to runtime settings"
);
return std::result::Result::Ok(ResolvedTransportConfig {
@@ -314,6 +421,7 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
effective,
settings,
ws_settings,
grpc_settings,
});
}
@@ -328,18 +436,22 @@ fn map_optional_ws_settings(
if defaults.is_some() || sources.is_some() {
std::result::Result::Err(effective_error(profile, "Transport V1 must remain HTTP-only"))
} else {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapped backward-compatible Transport V1 without WebSocket settings");
ksp_logging_lib::trace!(
target: crate::TRACING_TARGET,
profile_id = profile.profile_id(),
"mapped backward-compatible Transport V1 without WebSocket settings"
);
std::result::Result::Ok(std::option::Option::None)
}
},
2 => {
2 | 3 => {
let defaults = match defaults {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 requires ws_defaults")),
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2/V3 requires ws_defaults")),
};
let sources = match sources {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 profile requires ws_endpoints")),
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2/V3 profile requires ws_endpoints")),
};
let endpoints = map_ws_endpoints(sources, &defaults, profile);
let endpoints = match endpoints {
@@ -360,6 +472,283 @@ fn map_optional_ws_settings(
};
}
fn map_optional_grpc_settings(
format_version: u32,
defaults: std::option::Option<EffectiveGrpcSessionSource>,
sources: std::option::Option<std::vec::Vec<EffectiveGrpcEndpointSource>>,
effective: &crate::ResolvedConfigJson,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>> {
return match format_version {
1 | 2 => {
if defaults.is_some() || sources.is_some() {
std::result::Result::Err(effective_error(profile, "Transport V1/V2 must not define Yellowstone gRPC settings"))
} else {
std::result::Result::Ok(std::option::Option::None)
}
},
3 => {
let defaults = match defaults {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V3 requires grpc_defaults")),
};
let sources = match sources {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let endpoints = map_grpc_endpoints(sources, &defaults, effective, 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::YellowstoneGrpcTransportSettings::new(endpoints);
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(transport_contract_error(
profile,
"effective Yellowstone gRPC Transport settings fail the Transport runtime contract",
&error,
));
}
std::result::Result::Ok(std::option::Option::Some(settings))
},
_ => std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported")),
};
}
fn map_grpc_endpoints(
sources: std::vec::Vec<EffectiveGrpcEndpointSource>,
defaults: &EffectiveGrpcSessionSource,
effective: &crate::ResolvedConfigJson,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings>> {
let mut endpoints = std::vec::Vec::<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings>::with_capacity(sources.len());
for (endpoint_index, source) in sources.into_iter().enumerate() {
let endpoint_name = source.name.clone();
if source.protocol != "solana_yellowstone" {
return std::result::Result::Err(
effective_error(profile, "effective gRPC protocol is unsupported")
.with_context("endpoint_name", endpoint_name)
.with_context("grpc_protocol", source.protocol),
);
}
let url = ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::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 Yellowstone gRPC endpoint URL is invalid", &error)
.with_context("endpoint_name", endpoint_name),
);
},
};
let session = map_grpc_session_settings(defaults, source.session.as_ref(), profile, endpoint_name.as_str());
let session = match session {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata = map_grpc_metadata(source.metadata, source.secret_metadata, effective, profile, endpoint_name.as_str(), endpoint_index);
let metadata = match metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
source.name,
source.enabled,
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new(source.provider),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new(source.cluster),
url,
session,
);
let endpoint = endpoint.with_metadata(metadata);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
transport_contract_error(profile, "effective Yellowstone gRPC metadata fail the Transport runtime contract", &error)
.with_context("endpoint_name", endpoint_name),
);
},
};
endpoints.push(endpoint);
}
return std::result::Result::Ok(endpoints);
}
fn map_grpc_session_settings(
defaults: &EffectiveGrpcSessionSource,
overrides: std::option::Option<&EffectiveGrpcSessionOverrideSource>,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings> {
let mut connect_timeout_ms = defaults.connect_timeout_ms;
let mut unary_timeout_ms = defaults.unary_timeout_ms;
let mut close_timeout_ms = defaults.close_timeout_ms;
let mut reconnect_max_retries = defaults.reconnect.max_retries;
let mut reconnect_initial_backoff_ms = defaults.reconnect.initial_backoff_ms;
let mut reconnect_max_backoff_ms = defaults.reconnect.max_backoff_ms;
let mut request_channel_capacity = defaults.request_channel_capacity;
let mut update_channel_capacity = defaults.update_channel_capacity;
let mut max_inbound_message_size_bytes = defaults.max_inbound_message_size_bytes;
let mut max_outbound_message_size_bytes = defaults.max_outbound_message_size_bytes;
if let std::option::Option::Some(overrides) = overrides {
if let std::option::Option::Some(value) = overrides.connect_timeout_ms {
connect_timeout_ms = value;
}
if let std::option::Option::Some(value) = overrides.unary_timeout_ms {
unary_timeout_ms = value;
}
if let std::option::Option::Some(value) = overrides.close_timeout_ms {
close_timeout_ms = value;
}
if let std::option::Option::Some(reconnect) = overrides.reconnect.as_ref() {
if let std::option::Option::Some(value) = reconnect.max_retries {
reconnect_max_retries = value;
}
if let std::option::Option::Some(value) = reconnect.initial_backoff_ms {
reconnect_initial_backoff_ms = value;
}
if let std::option::Option::Some(value) = reconnect.max_backoff_ms {
reconnect_max_backoff_ms = value;
}
}
if let std::option::Option::Some(value) = overrides.request_channel_capacity {
request_channel_capacity = value;
}
if let std::option::Option::Some(value) = overrides.update_channel_capacity {
update_channel_capacity = value;
}
if let std::option::Option::Some(value) = overrides.max_inbound_message_size_bytes {
max_inbound_message_size_bytes = value;
}
if let std::option::Option::Some(value) = overrides.max_outbound_message_size_bytes {
max_outbound_message_size_bytes = value;
}
}
let reconnect = ksp_onchain_transport_lib::YellowstoneGrpcReconnectSettings::new(
reconnect_max_retries,
std::time::Duration::from_millis(reconnect_initial_backoff_ms),
std::time::Duration::from_millis(reconnect_max_backoff_ms),
);
let settings = ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::new(
std::time::Duration::from_millis(connect_timeout_ms),
std::time::Duration::from_millis(unary_timeout_ms),
std::time::Duration::from_millis(close_timeout_ms),
reconnect,
request_channel_capacity,
update_channel_capacity,
max_inbound_message_size_bytes,
max_outbound_message_size_bytes,
);
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(
transport_contract_error(profile, "effective Yellowstone gRPC session settings fail the Transport runtime contract", &error)
.with_context("endpoint_name", endpoint_name),
);
}
return std::result::Result::Ok(settings);
}
fn map_grpc_metadata(
public_sources: std::vec::Vec<EffectiveGrpcMetadataSource>,
secret_sources: std::vec::Vec<EffectiveGrpcMetadataSource>,
effective: &crate::ResolvedConfigJson,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
endpoint_index: usize,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry>> {
let mut metadata = std::vec::Vec::<ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry>::with_capacity(public_sources.len() + secret_sources.len());
for (metadata_index, source) in public_sources.into_iter().enumerate() {
let pointer = format!("/grpc_endpoints/{endpoint_index}/metadata/{metadata_index}/value");
if let std::result::Result::Err(error) = validate_grpc_metadata_provenance(effective, pointer.as_str(), false, profile, endpoint_name) {
return std::result::Result::Err(error);
}
let entry = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::public(source.key, source.value);
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
transport_contract_error(profile, "effective public Yellowstone gRPC metadata are invalid", &error)
.with_context("endpoint_name", endpoint_name),
);
},
};
metadata.push(entry);
}
for (metadata_index, source) in secret_sources.into_iter().enumerate() {
let pointer = format!("/grpc_endpoints/{endpoint_index}/secret_metadata/{metadata_index}/value");
if let std::result::Result::Err(error) = validate_grpc_metadata_provenance(effective, pointer.as_str(), true, profile, endpoint_name) {
return std::result::Result::Err(error);
}
let entry = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret(source.key, source.value);
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
transport_contract_error(profile, "effective secret Yellowstone gRPC metadata are invalid", &error)
.with_context("endpoint_name", endpoint_name),
);
},
};
metadata.push(entry);
}
return std::result::Result::Ok(metadata);
}
fn validate_grpc_metadata_provenance(
effective: &crate::ResolvedConfigJson,
pointer: &str,
secret_class: bool,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<()> {
let provenance = match effective.provenance_at(pointer) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
effective_error(profile, "Yellowstone gRPC metadata provenance is unavailable")
.with_context("endpoint_name", endpoint_name)
.with_context("field", pointer),
);
},
};
let mut has_secret_environment = false;
for item in provenance {
let variable_name = match item.variable_name() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
let sensitivity = match sensitivity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if sensitivity.is_secret() {
has_secret_environment = true;
if !secret_class {
return std::result::Result::Err(
effective_error(profile, "public Yellowstone gRPC metadata must not derive from a secret environment variable")
.with_context("endpoint_name", endpoint_name)
.with_context("field", pointer),
);
}
} else if secret_class {
return std::result::Result::Err(
effective_error(profile, "secret Yellowstone gRPC metadata may reference only secret environment variables")
.with_context("endpoint_name", endpoint_name)
.with_context("field", pointer),
);
}
}
if secret_class && !has_secret_environment {
return std::result::Result::Err(
effective_error(profile, "secret Yellowstone gRPC metadata require secret environment provenance")
.with_context("endpoint_name", endpoint_name)
.with_context("field", pointer),
);
}
return std::result::Result::Ok(());
}
fn map_endpoints(
sources: std::vec::Vec<EffectiveEndpointSource>,
profile: &crate::ResolvedConfigProfile,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 22
// version: 23
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport adapters and management contracts.
@@ -254,7 +254,9 @@ fn transport_adapter_contract_is_available_from_crate_root() {
assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0);
let _http_settings = ksp_config_lib::ResolvedTransportConfig::http_settings;
let _ws_settings = ksp_config_lib::ResolvedTransportConfig::ws_settings;
let _grpc_settings = ksp_config_lib::ResolvedTransportConfig::grpc_settings;
let _into_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_transport_settings;
let _into_all_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_all_transport_settings;
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");

View File

@@ -0,0 +1,242 @@
{
"format_version": 3,
"retry": {
"max_retries": 2,
"initial_backoff_ms": 100,
"max_backoff_ms": 2000
},
"ws_defaults": {
"command_timeout_ms": 8000,
"close_timeout_ms": 4000,
"reconnect": {
"max_retries": 5,
"initial_backoff_ms": 250,
"max_backoff_ms": 5000
},
"resubscribe": "active_subscriptions",
"command_queue_capacity": 64,
"notification_queue_capacity": 96,
"max_active_subscriptions": 256,
"max_pending_requests": 48,
"max_message_size_bytes": 33554432,
"max_frame_size_bytes": 8388608,
"max_write_buffer_size_bytes": 524288
},
"grpc_defaults": {
"connect_timeout_ms": 6000,
"unary_timeout_ms": 7000,
"close_timeout_ms": 4000,
"reconnect": {
"max_retries": 4,
"initial_backoff_ms": 200,
"max_backoff_ms": 3000
},
"request_channel_capacity": 64,
"update_channel_capacity": 96,
"max_inbound_message_size_bytes": 33554432,
"max_outbound_message_size_bytes": 16777216
},
"default_profile": "grpc_secret_ok",
"profiles": [
{
"profile_id": "grpc_secret_ok",
"endpoints": [
{
"name": "fixture_http",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"url": "https://fallback.invalid",
"connect_timeout_ms": 1000,
"request_timeout_ms": 3000,
"max_idle_connections_per_host": 2,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 1,
"limits": {
"requests_per_second": 5,
"burst_capacity": 5,
"max_concurrent_requests": 2,
"pause_after_rate_limit_ms": 500
}
}
]
}
],
"ws_endpoints": [
{
"name": "fixture_ws",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "wss://fallback.invalid"
}
],
"grpc_endpoints": [
{
"name": "fixture_grpc",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"protocol": "solana_yellowstone",
"url": "https://grpc.invalid",
"metadata": [
{
"key": "x-ksp-label",
"value": "${KSP_PUBLIC_GRPC_LABEL:-fixture-public}"
}
],
"secret_metadata": [
{
"key": "x-ksp-token",
"value": "Bearer ${KSP_SECRET_GRPC_TOKEN:-fixture-secret}"
}
],
"session": {
"connect_timeout_ms": 4500,
"reconnect": {
"max_retries": 7
},
"update_channel_capacity": 32
}
}
]
},
{
"profile_id": "grpc_public_from_secret_invalid",
"endpoints": [
{
"name": "fixture_http",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"url": "https://fallback.invalid",
"connect_timeout_ms": 1000,
"request_timeout_ms": 3000,
"max_idle_connections_per_host": 2,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 1,
"limits": {
"requests_per_second": 5,
"burst_capacity": 5,
"max_concurrent_requests": 2,
"pause_after_rate_limit_ms": 500
}
}
]
}
],
"ws_endpoints": [
{
"name": "fixture_ws",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "wss://fallback.invalid"
}
],
"grpc_endpoints": [
{
"name": "fixture_grpc",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"protocol": "solana_yellowstone",
"url": "https://grpc.invalid",
"metadata": [
{
"key": "x-ksp-public",
"value": "${KSP_SECRET_GRPC_TOKEN:-fixture-secret}"
}
],
"secret_metadata": [],
"session": {
"connect_timeout_ms": 4500,
"reconnect": {
"max_retries": 7
},
"update_channel_capacity": 32
}
}
]
},
{
"profile_id": "grpc_secret_from_public_invalid",
"endpoints": [
{
"name": "fixture_http",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"url": "https://fallback.invalid",
"connect_timeout_ms": 1000,
"request_timeout_ms": 3000,
"max_idle_connections_per_host": 2,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 1,
"limits": {
"requests_per_second": 5,
"burst_capacity": 5,
"max_concurrent_requests": 2,
"pause_after_rate_limit_ms": 500
}
}
]
}
],
"ws_endpoints": [
{
"name": "fixture_ws",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "wss://fallback.invalid"
}
],
"grpc_endpoints": [
{
"name": "fixture_grpc",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"protocol": "solana_yellowstone",
"url": "https://grpc.invalid",
"metadata": [],
"secret_metadata": [
{
"key": "x-ksp-secret",
"value": "${KSP_PUBLIC_GRPC_LABEL:-fixture-public}"
}
],
"session": {
"connect_timeout_ms": 4500,
"reconnect": {
"max_retries": 7
},
"update_channel_capacity": 32
}
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/transport.rs
// version: 6
// version: 7
#[test]
fn fixture_transport_profile_maps_complete_runtime_contract() {
@@ -41,6 +41,7 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
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)));
assert!(resolved.grpc_settings().is_none(), "V2 fixture must remain Yellowstone gRPC-free");
let ws = resolved.ws_settings();
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
@@ -90,6 +91,7 @@ fn v1_transport_fixture_remains_backward_readable_and_http_only() {
assert_eq!(resolved.settings().endpoints().len(), 1);
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://legacy.invalid");
assert!(resolved.ws_settings().is_none(), "V1 must not invent WebSocket runtime settings");
assert!(resolved.grpc_settings().is_none(), "V1 must not invent Yellowstone gRPC runtime settings");
}
}
@@ -109,8 +111,9 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
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");
assert!(default.grpc_settings().is_none(), "Devnet profile must not invent a Yellowstone gRPC endpoint");
let ws = default.ws_settings();
assert!(ws.is_some(), "committed V2 Devnet profile should expose WebSocket settings");
assert!(ws.is_some(), "committed V3 Devnet profile should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com");
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
@@ -121,8 +124,9 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
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");
assert!(mainnet.grpc_settings().is_none(), "generic Mainnet profile must stay distinct from the PublicNode gRPC profile");
let ws = mainnet.ws_settings();
assert!(ws.is_some(), "committed V2 Mainnet profile should expose WebSocket settings");
assert!(ws.is_some(), "committed V3 Mainnet profile should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com");
}
@@ -130,7 +134,121 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
}
#[test]
fn committed_v2_websocket_endpoint_composes_with_public_session_constructor_without_polling() {
fn committed_v3_publicnode_mainnet_maps_provider_neutral_yellowstone_grpc() {
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 resolved = engine.load_resolved_transport_config(std::option::Option::Some("publicnode_mainnet"), &environment);
assert!(resolved.is_ok(), "committed PublicNode Mainnet V3 profile should map: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.profile_id(), "publicnode_mainnet");
assert!(resolved.ws_settings().is_some());
let grpc = resolved.grpc_settings();
assert!(grpc.is_some(), "PublicNode Mainnet profile should expose Yellowstone gRPC settings");
if let std::option::Option::Some(grpc) = grpc {
assert_eq!(grpc.endpoints().len(), 1);
let endpoint = &grpc.endpoints()[0];
assert_eq!(endpoint.name(), "publicnode_solana_mainnet_yellowstone");
assert_eq!(endpoint.provider().as_str(), "publicnode");
assert_eq!(endpoint.cluster().as_str(), "mainnet-beta");
assert_eq!(endpoint.url().as_str(), "https://solana-yellowstone-grpc.publicnode.com:443");
assert!(endpoint.url().uses_tls());
assert!(endpoint.metadata().is_empty());
assert_eq!(endpoint.session().connect_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(endpoint.session().unary_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(endpoint.session().reconnect().max_retries(), 5);
assert!(grpc.validate().is_ok(), "Config-produced Yellowstone gRPC settings should satisfy Transport validation");
let debug = format!("{grpc:?}");
assert!(!debug.contains("solana-yellowstone-grpc.publicnode.com"), "Transport settings Debug must not expose gRPC endpoint URLs");
}
}
#[test]
fn v3_grpc_metadata_separates_public_and_secret_provenance() {
let engine = v3_fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let canary = "grpc-secret-canary";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_GRPC_TOKEN".to_owned(), canary.to_owned());
process.insert("KSP_PUBLIC_GRPC_LABEL".to_owned(), "operator-public".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(), "V3 gRPC metadata fixture should map: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let grpc = resolved.grpc_settings();
assert!(grpc.is_some());
if let std::option::Option::Some(grpc) = grpc {
let endpoint = &grpc.endpoints()[0];
assert_eq!(endpoint.metadata().len(), 2);
assert_eq!(endpoint.metadata()[0].key(), "x-ksp-label");
assert!(!endpoint.metadata()[0].is_secret());
assert_eq!(endpoint.metadata()[1].key(), "x-ksp-token");
assert!(endpoint.metadata()[1].is_secret());
assert_eq!(endpoint.session().connect_timeout(), std::time::Duration::from_millis(4_500));
assert_eq!(endpoint.session().unary_timeout(), std::time::Duration::from_millis(7_000));
assert_eq!(endpoint.session().reconnect().max_retries(), 7);
assert_eq!(endpoint.session().update_channel_capacity(), 32);
}
assert_eq!(
resolved.effective().safe_value().pointer("/grpc_endpoints/0/metadata/0/value").and_then(serde_json::Value::as_str),
std::option::Option::Some("operator-public")
);
assert_eq!(
resolved.effective().safe_value().pointer("/grpc_endpoints/0/secret_metadata/0/value").and_then(serde_json::Value::as_str),
std::option::Option::Some("Bearer ********")
);
let secret_provenance = resolved.effective().provenance_at("/grpc_endpoints/0/secret_metadata/0/value");
assert!(secret_provenance.is_some());
if let std::option::Option::Some(provenance) = secret_provenance {
assert_eq!(provenance.len(), 2);
assert_eq!(provenance[0], crate::ConfigValueProvenance::DocumentLiteral);
assert_eq!(provenance[1].variable_name(), std::option::Option::Some("KSP_SECRET_GRPC_TOKEN"));
}
let debug = format!("{resolved:?}");
assert!(!debug.contains(canary));
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
}
#[test]
fn v3_grpc_metadata_rejects_crossed_sensitivity_classes() {
let engine = v3_fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_GRPC_TOKEN".to_owned(), "secret-canary".to_owned());
process.insert("KSP_PUBLIC_GRPC_LABEL".to_owned(), "public-canary".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let public_from_secret = engine.load_resolved_transport_config(std::option::Option::Some("grpc_public_from_secret_invalid"), &environment);
let secret_from_public = engine.load_resolved_transport_config(std::option::Option::Some("grpc_secret_from_public_invalid"), &environment);
assert!(public_from_secret.is_err(), "public metadata must reject KSP_SECRET provenance");
assert!(secret_from_public.is_err(), "secret_metadata must reject non-secret environment provenance");
if let std::result::Result::Err(error) = public_from_secret {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
assert!(!format!("{error:?}").contains("secret-canary"));
}
if let std::result::Result::Err(error) = secret_from_public {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
assert!(!format!("{error:?}").contains("public-canary"));
}
}
#[test]
fn committed_v3_websocket_endpoint_preserves_v2_compatibility_constructor_without_polling() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
@@ -138,11 +256,11 @@ fn committed_v2_websocket_endpoint_composes_with_public_session_constructor_with
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_transport_config(std::option::Option::Some("devnet_public"), &environment);
assert!(resolved.is_ok(), "committed V2 Transport profile should map: {resolved:?}");
assert!(resolved.is_ok(), "committed V3 Transport profile should map: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
let (http, ws) = resolved.into_transport_settings();
assert_eq!(http.endpoints().len(), 1);
assert!(ws.is_some(), "committed V2 Transport profile should expose WebSocket settings");
assert!(ws.is_some(), "committed V3 Transport profile should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
assert!(ws.validate().is_ok(), "Config-produced WebSocket settings should satisfy Transport validation");
assert_eq!(ws.endpoints().len(), 1);
@@ -170,6 +288,7 @@ fn transport_profile_preserves_global_and_profile_origin() {
if let std::result::Result::Ok(profile) = profile {
assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(profile.origin("ws_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(profile.origin("grpc_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
assert_eq!(profile.origin("ws_endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
@@ -421,6 +540,22 @@ fn v1_fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
}
fn v3_fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root();
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures_v3");
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<crate::ConfigDocumentEngine> {
let workspace = workspace_root();
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));

284
deltas/0.2.9/pre.011.md Normal file
View File

@@ -0,0 +1,284 @@
<!-- file: deltas/0.2.9/pre.011.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.011` — Config Transport V3 + PublicNode Mainnet + consolidation documentaire
## 1. Base et version
Base immédiate :
```text
0.2.9-pre.010-fix.001
Cargo workspace = 0.2.9-pre.10.fix.1
```
Version candidate :
```text
0.2.9-pre.011
Cargo workspace = 0.2.9-pre.11
```
Le gate opérateur fourni pour `pre.010-fix.001` est entièrement vert : fmt/audit/check/Clippy sans warning, Transport 383 unit + 49 public API + 43 release-completeness + 4 doctests, dependency canary 3/3 et `cargo test --workspace` PASS.
## 2. Objet
Cette tranche matérialise la frontière Config décidée par `pre.001` puis différée jusqu'après le lifecycle/reconnect Yellowstone :
```text
std.transport V3
Config -> YellowstoneGrpcTransportSettings
provider et protocol distincts
metadata publique et secrète avec provenance contrôlée
premier profil PublicNode Mainnet standard
backward V1/V2 strict
```
Elle profite aussi du changement de phase de la release pour remettre le plan `016` et la validation `012` dans un état maintenable. Les détails historiques ne sont pas supprimés : ils restent dans les deltas immuables `deltas/0.2.9/`.
## 3. Config Transport V3
### 3.1 Schema
`config/schemas/std.transport.schema.json` devient `urn:ksp:schema:std.transport:v3` et conserve trois branches strictes :
```text
V1 = HTTP-only
V2 = HTTP + WebSocket
V3 = HTTP + WebSocket + Yellowstone gRPC optionnel par profil
```
V3 ajoute :
```text
grpc_defaults
grpc_endpoints[]?
```
`grpc_defaults` mappe uniquement des knobs déjà possédés par `YellowstoneGrpcSessionSettings` : timeouts, reconnect, channel capacities et max message sizes. Les bounds des filtres Subscribe restent un contrat Transport et ne sont pas transformés en configuration arbitraire.
Chaque endpoint gRPC contient :
```text
name
enabled
provider
cluster
protocol = solana_yellowstone
url
metadata[]?
secret_metadata[]?
session?
```
### 3.2 Mapping Config -> Transport
`ResolvedTransportConfig` ajoute :
```text
grpc_settings()
into_all_transport_settings()
```
L'accesseur historique suivant est volontairement inchangé :
```text
into_transport_settings() -> (HttpTransportSettings, Option<WsTransportSettings>)
```
Cela évite une rupture source silencieuse des consumers V2.
V1 et V2 retournent toujours `None` pour gRPC. Un profil V3 qui n'a pas de `grpc_endpoints` retourne également `None` au lieu d'inventer un Transport gRPC vide.
### 3.3 Protocol vs provider
Config exige actuellement :
```text
protocol = solana_yellowstone
```
Le champ `provider` reste descriptif et mappe vers `YellowstoneGrpcProviderName`.
Donc :
```text
PublicNode != protocole
PublicNode != nouvelle façade Transport
PublicNode = environnement d'exécution du standard Yellowstone
```
Toute future divergence provider doit rester un delta N3 explicite.
### 3.4 Metadata et provenance
Deux classes sont matérialisées :
```text
metadata
secret_metadata
```
Règles avant mapping :
```text
metadata + provenance KSP_SECRET_*/KSPB_SECRET_* -> reject
secret_metadata sans provenance secret -> reject
secret_metadata + provenance non-secret env -> reject
secret_metadata + provenance secret -> accept
```
Les segments littéraux autour d'un placeholder secret restent permis. Config possède la provenance et la projection sûre ; Transport reçoit ensuite seulement les `YellowstoneGrpcMetadataEntry` déjà classifiées.
Aucun nom d'environnement n'est lu dans Transport.
## 4. Profil PublicNode Mainnet
La surface publique réauditée le 2026-08-24 confirme Yellowstone gRPC Solana Mainnet et affiche :
```text
solana-yellowstone-grpc.publicnode.com:443
```
`YellowstoneGrpcEndpointUrl` exige une URL `http/https`; le document Config représente donc ce même endpoint TLS comme `https://solana-yellowstone-grpc.publicnode.com:443`.
Le document committé ajoute :
```text
profile_id = publicnode_mainnet
provider = publicnode
cluster = mainnet-beta
protocol = solana_yellowstone
metadata = aucune
```
Le profil générique `mainnet_public` reste distinct et ne reçoit pas implicitement un endpoint PublicNode.
PublicNode expose également Solana Testnet gRPC, mais le hostname exact n'a pas été obtenu depuis une source suffisamment autoritative/indexable pendant cette tranche. Aucun endpoint supposé n'est versionné. `pre.012` réaudite cette valeur avant tout profil/smoke Testnet.
## 5. Tests ajoutés/étendus
La fixture V3 couvre :
```text
mapping gRPC valide
merge grpc_defaults + session overrides
metadata publique issue de KSP_PUBLIC_*
secret_metadata issue de KSP_SECRET_*
safe_value avec segment secret redacted
rejet public <- secret
rejet secret <- public
```
Le config committé couvre le profil `publicnode_mainnet` et vérifie la construction d'un `YellowstoneGrpcTransportSettings` valide sans URL dans son `Debug`.
Le canari public API inclut les nouveaux accessors sans supprimer les anciens.
## 6. Consolidation plan/validation
`docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md` et `docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md` sont réorganisés autour de :
```text
état courant
scope/architecture stabilisés
matrices protocole encore normatives
Config V3
PublicNode
lifecycle/replay
historique compact des gates fermés
forecast restant
critères de clôture
```
Les longues sections append-only propres à chaque prerelease sont remplacées par une table compacte renvoyant aux deltas immuables. Aucun delta historique n'est modifié.
Les tableaux Markdown sont reformattés selon la convention JetBrains RustRover utilisée dans le workspace : largeur calculée sur la cellule la plus large et un espace de padding autour du contenu.
## 7. Documentation Config synchronisée
La README et l'USAGE de `ksp-config-lib`, ainsi que les commentaires du registre, sont mis à jour pour ne plus présenter `std.transport` comme un document uniquement V2 HTTP + WebSocket.
La documentation finale spécifique Transport/Yellowstone reste un gate de `pre.012`.
## 8. Fichiers modifiés/ajoutés
```text
Cargo.toml
config/std.transport.json
config/schemas/std.transport.schema.json
crates/ksp-config-lib/README.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/src/transport.rs
crates/ksp-config-lib/tests/public_api.rs
crates/ksp-config-lib/unit_tests/transport.rs
crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
deltas/0.2.9/pre.011.md
```
Aucune dépendance Cargo n'est ajoutée ou modifiée.
## 9. Validation source disponible avant livraison
Exécuté dans l'environnement de préparation :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
JSON Schema Draft 2020-12 meta-validation: PASS
config/std.transport.json against V3 schema: PASS
fixture V2 against V3 schema: PASS
fixture V1 against V3 schema: PASS
fixture V3 against V3 schema: PASS
```
Cargo/Rustfmt ne sont pas disponibles dans l'environnement de préparation. Aucune compilation ou test Cargo n'est donc déclaré réussi pour `pre.011` avant gate opérateur.
## 10. Gate opérateur requis
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-config-lib --test public_api
cargo test -p ksp-config-lib --test ownership
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Attentes de candidate :
```text
Config unit 113 attendus (110 baseline + 3 V3)
Config public API 15 attendus
Config ownership 5 attendus
Transport unit >= 383
Transport public API >= 49
Transport completeness >= 43
Transport doctests 4
workspace dependencies 3
Clippy aucun warning
workspace PASS
```
Ces valeurs ne deviennent des résultats qu'après exécution opérateur.
## 11. Suite
Après fermeture de `pre.011` :
```text
pre.012 PublicNode live + Testnet endpoint re-audit + compliance finale + cargo graphs + docs/prompt suivant
rel.001 publication stable stricte
```
Si `pre.012` ne peut pas fermer proprement tous les gates dans une seule tranche, continuer avec `pre.013+` plutôt que compacter artificiellement la clôture.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff