v0.2.7-pre.003

This commit is contained in:
2026-08-22 17:56:05 +02:00
parent ab29dc51bb
commit d5df0fe9af
17 changed files with 1288 additions and 103 deletions

View File

@@ -1,5 +1,5 @@
# file: .env.example
# version: 4
# version: 5
# 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.
@@ -22,10 +22,22 @@ KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
# 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 public Solana Devnet WebSocket endpoint override used by config/std.transport.json.
# The committed Transport document falls back to wss://api.devnet.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_DEVNET_WS_URL=wss://api.devnet.solana.com
# Optional public Solana Mainnet WebSocket endpoint override used by config/std.transport.json and its example.
# The committed Transport document falls back to wss://api.mainnet-beta.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://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
# Optional complete private-provider WebSocket 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_WS_URL=wss://provider.example/?api-key=replace-me
# Fade-in duration in milliseconds used by the common KSP desk splash lifecycle.
KSP_DESK_SPLASH_FADE_IN_MS=300

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 194
# version: 195
[workspace]
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.7-pre.2.fix.1"
version = "0.2.7-pre.3"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,10 +1,27 @@
{
"format_version": 1,
"format_version": 2,
"retry": {
"max_retries": 3,
"initial_backoff_ms": 150,
"max_backoff_ms": 3000
},
"ws_defaults": {
"command_timeout_ms": 10000,
"close_timeout_ms": 5000,
"reconnect": {
"max_retries": 5,
"initial_backoff_ms": 250,
"max_backoff_ms": 5000
},
"resubscribe": "active_subscriptions",
"command_queue_capacity": 128,
"notification_queue_capacity": 256,
"max_active_subscriptions": 1024,
"max_pending_requests": 128,
"max_message_size_bytes": 67108864,
"max_frame_size_bytes": 16777216,
"max_write_buffer_size_bytes": 1048576
},
"default_profile": "mainnet_mixed",
"profiles": [
{
@@ -23,7 +40,9 @@
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"request_kinds": [
"*"
],
"priority": 200,
"limits": {
"requests_per_second": 5,
@@ -47,7 +66,9 @@
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 20,
@@ -58,6 +79,28 @@
}
]
}
],
"ws_endpoints": [
{
"name": "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}"
},
{
"name": "mainnet_private_ws",
"enabled": true,
"provider": "private-provider",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "${KSP_SECRET_SOLANA_WS_URL:-wss://example.invalid}",
"session": {
"notification_queue_capacity": 512,
"max_active_subscriptions": 2048
}
}
]
}
]

View File

@@ -1,20 +1,15 @@
{
"$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"}
}
"$id": "urn:ksp:schema:std.transport:v2",
"title": "KSP standard HTTP + WebSocket Transport configuration",
"oneOf": [
{
"$ref": "#/$defs/documentV1"
},
{
"$ref": "#/$defs/documentV2"
}
],
"$defs": {
"profileId": {
"type": "string",
@@ -35,50 +30,99 @@
"minimum": 1,
"maximum": 4294967295
},
"positiveUsize": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"retry": {
"type": "object",
"additionalProperties": false,
"required": ["max_retries", "initial_backoff_ms", "max_backoff_ms"],
"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"}
"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"}
"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"],
"required": [
"role",
"enabled",
"request_kinds",
"priority",
"limits"
],
"properties": {
"role": {"$ref": "#/$defs/descriptor"},
"enabled": {"type": "boolean"},
"role": {
"$ref": "#/$defs/descriptor"
},
"enabled": {
"type": "boolean"
},
"request_kinds": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"$ref": "#/$defs/descriptor"},
"items": {
"$ref": "#/$defs/descriptor"
},
"allOf": [
{
"if": {"contains": {"const": "*"}},
"then": {"maxItems": 1}
"if": {
"contains": {
"const": "*"
}
},
"then": {
"maxItems": 1
}
}
]
},
"priority": {"type": "integer", "minimum": 0, "maximum": 4294967295},
"limits": {"$ref": "#/$defs/limits"}
"priority": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"limits": {
"$ref": "#/$defs/limits"
}
}
},
"endpoint": {
"httpEndpoint": {
"type": "object",
"additionalProperties": false,
"required": [
@@ -92,31 +136,322 @@
"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},
"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"}
"items": {
"$ref": "#/$defs/role"
}
}
}
},
"profile": {
"wsReconnect": {
"type": "object",
"additionalProperties": false,
"required": ["profile_id", "endpoints"],
"required": [
"max_retries",
"initial_backoff_ms",
"max_backoff_ms"
],
"properties": {
"profile_id": {"$ref": "#/$defs/profileId"},
"max_retries": {
"type": "integer",
"minimum": 0,
"maximum": 100
},
"initial_backoff_ms": {
"$ref": "#/$defs/positiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/positiveMs"
}
}
},
"wsReconnectOverride": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"max_retries": {
"type": "integer",
"minimum": 0,
"maximum": 100
},
"initial_backoff_ms": {
"$ref": "#/$defs/positiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/positiveMs"
}
}
},
"wsSession": {
"type": "object",
"additionalProperties": false,
"required": [
"command_timeout_ms",
"close_timeout_ms",
"reconnect",
"resubscribe",
"command_queue_capacity",
"notification_queue_capacity",
"max_active_subscriptions",
"max_pending_requests",
"max_message_size_bytes",
"max_frame_size_bytes",
"max_write_buffer_size_bytes"
],
"properties": {
"command_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"close_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"reconnect": {
"$ref": "#/$defs/wsReconnect"
},
"resubscribe": {
"enum": [
"never",
"active_subscriptions"
]
},
"command_queue_capacity": {
"$ref": "#/$defs/positiveUsize"
},
"notification_queue_capacity": {
"$ref": "#/$defs/positiveUsize"
},
"max_active_subscriptions": {
"$ref": "#/$defs/positiveUsize"
},
"max_pending_requests": {
"$ref": "#/$defs/positiveUsize"
},
"max_message_size_bytes": {
"$ref": "#/$defs/positiveUsize"
},
"max_frame_size_bytes": {
"$ref": "#/$defs/positiveUsize"
},
"max_write_buffer_size_bytes": {
"$ref": "#/$defs/positiveUsize"
}
}
},
"wsSessionOverride": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"command_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"close_timeout_ms": {
"$ref": "#/$defs/positiveMs"
},
"reconnect": {
"$ref": "#/$defs/wsReconnectOverride"
},
"resubscribe": {
"enum": [
"never",
"active_subscriptions"
]
},
"command_queue_capacity": {
"$ref": "#/$defs/positiveUsize"
},
"notification_queue_capacity": {
"$ref": "#/$defs/positiveUsize"
},
"max_active_subscriptions": {
"$ref": "#/$defs/positiveUsize"
},
"max_pending_requests": {
"$ref": "#/$defs/positiveUsize"
},
"max_message_size_bytes": {
"$ref": "#/$defs/positiveUsize"
},
"max_frame_size_bytes": {
"$ref": "#/$defs/positiveUsize"
},
"max_write_buffer_size_bytes": {
"$ref": "#/$defs/positiveUsize"
}
}
},
"wsEndpoint": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"enabled",
"provider",
"cluster",
"kind",
"url"
],
"properties": {
"name": {
"$ref": "#/$defs/descriptor"
},
"enabled": {
"type": "boolean"
},
"provider": {
"$ref": "#/$defs/descriptor"
},
"cluster": {
"$ref": "#/$defs/descriptor"
},
"kind": {
"enum": [
"solana_standard"
]
},
"url": {
"type": "string",
"minLength": 1
},
"session": {
"$ref": "#/$defs/wsSessionOverride"
}
}
},
"profileV1": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id",
"endpoints"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"endpoints": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/$defs/endpoint"}
"items": {
"$ref": "#/$defs/httpEndpoint"
}
}
}
},
"profileV2": {
"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"
}
}
}
},
"documentV1": {
"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/profileV1"
}
}
}
},
"documentV2": {
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"retry",
"ws_defaults",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 2
},
"retry": {
"$ref": "#/$defs/retry"
},
"ws_defaults": {
"$ref": "#/$defs/wsSession"
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profileV2"
}
}
}
}

View File

@@ -1,10 +1,27 @@
{
"format_version": 1,
"format_version": 2,
"retry": {
"max_retries": 2,
"initial_backoff_ms": 100,
"max_backoff_ms": 2000
},
"ws_defaults": {
"command_timeout_ms": 10000,
"close_timeout_ms": 5000,
"reconnect": {
"max_retries": 5,
"initial_backoff_ms": 250,
"max_backoff_ms": 5000
},
"resubscribe": "active_subscriptions",
"command_queue_capacity": 128,
"notification_queue_capacity": 256,
"max_active_subscriptions": 1024,
"max_pending_requests": 128,
"max_message_size_bytes": 67108864,
"max_frame_size_bytes": 16777216,
"max_write_buffer_size_bytes": 1048576
},
"default_profile": "devnet_public",
"profiles": [
{
@@ -23,7 +40,9 @@
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
@@ -34,6 +53,16 @@
}
]
}
],
"ws_endpoints": [
{
"name": "solana_devnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "devnet",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
}
]
},
{
@@ -52,7 +81,9 @@
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"request_kinds": [
"*"
],
"priority": 100,
"limits": {
"requests_per_second": 5,
@@ -63,6 +94,16 @@
}
]
}
],
"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}"
}
]
}
]

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# ksp-config-lib
@@ -23,7 +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_*` ;
- l'adapter du document Transport V1/V2 vers `HttpTransportSettings` et, en V2, `WsTransportSettings`, 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`.
@@ -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 endpoint : 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 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.
## 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 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) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V2 HTTP + WebSocket, avec lecture backward du V1 HTTP-only ;
- [`../../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: 6 -->
<!-- version: 7 -->
# Utilisation de ksp-config-lib
@@ -125,7 +125,7 @@ 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
### 4.1 Construire le Transport HTTP + WebSocket depuis Config
Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` :
@@ -135,12 +135,18 @@ let transport = match engine.load_resolved_transport_config(std::option::Option:
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_settings = transport.into_settings();
let http_settings = transport.http_settings();
let ws_settings = transport.ws_settings();
let _ = (http_settings, ws_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.
`std.transport` V2 conserve `retry` et `profiles[].endpoints[]` pour HTTP, ajoute `ws_defaults` et `profiles[].ws_endpoints[]`, puis exige `kind = "solana_standard"` en `0.2.7`. Un `ws_endpoints[].session` optionnel surcharge seulement les paramètres génériques de `WsSessionSettings`.
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.
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.
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.
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: 16
// version: 17
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -154,9 +154,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 Transport configuration document.
/// Default physical filename for the standard HTTP + WebSocket Transport configuration document.
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP Transport JSON Schema document.
/// Default physical filename for the standard HTTP + WebSocket 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 +168,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 Transport JSON Schema document.
/// Logical file identifier for the standard HTTP + WebSocket 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 Transport configuration document.
/// Logical file identifier for the standard HTTP + WebSocket 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 +188,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 HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
/// Effective standard Transport configuration mapped to HTTP and optional WebSocket 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: 8
// version: 9
/// 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 Transport configuration document.
/// Default physical filename for the standard HTTP + WebSocket Transport configuration document.
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP Transport JSON Schema document.
/// Default physical filename for the standard HTTP + WebSocket 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 Transport JSON Schema document.
/// Logical file identifier for the standard HTTP + WebSocket 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 Transport configuration document.
/// Logical file identifier for the standard HTTP + WebSocket 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: 2
// version: 3
/// Effective standard HTTP Transport configuration resolved from Config and mapped to the Transport runtime contract.
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP and optional WebSocket runtime contracts.
#[derive(Clone, Eq, PartialEq)]
pub struct ResolvedTransportConfig {
file_id: crate::ConfigFileId,
@@ -10,6 +10,7 @@ pub struct ResolvedTransportConfig {
selection_source: crate::ConfigProfileSelectionSource,
effective: crate::ResolvedConfigJson,
settings: ksp_onchain_transport_lib::HttpTransportSettings,
ws_settings: std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
}
impl ResolvedTransportConfig {
@@ -47,16 +48,43 @@ impl ResolvedTransportConfig {
}
/// Returns the validated runtime HTTP Transport settings.
///
/// This compatibility accessor keeps the HTTP contract introduced before Transport V2.
#[must_use]
pub const fn settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings {
return &self.settings;
}
/// Returns the validated runtime HTTP Transport settings.
#[must_use]
pub const fn http_settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings {
return &self.settings;
}
/// Returns validated WebSocket Transport settings when the selected document uses format V2.
///
/// Backward-compatible V1 HTTP-only documents return [`std::option::Option::None`].
#[must_use]
pub fn ws_settings(&self) -> std::option::Option<&ksp_onchain_transport_lib::WsTransportSettings> {
return self.ws_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.
#[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);
}
}
impl std::fmt::Debug for ResolvedTransportConfig {
@@ -68,16 +96,16 @@ impl std::fmt::Debug for ResolvedTransportConfig {
.field("profile_id", &self.profile_id)
.field("selection_source", &self.selection_source)
.field("effective", &self.effective)
.field("has_ws_settings", &self.ws_settings.is_some())
.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.
/// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket 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.
/// because Transport URL wrappers own runtime redaction. V1 documents remain HTTP-only; V2 documents require WebSocket defaults and endpoints.
pub fn load_resolved_transport_config(
&self,
requested_profile: std::option::Option<&str>,
@@ -96,7 +124,7 @@ impl crate::ConfigDocumentEngine {
return resolve_transport_profile(&profile, environment);
}
/// Maps an already resolved standard Transport profile to the runtime HTTP Transport adapter while preserving its selection provenance.
/// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket 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(
@@ -121,7 +149,11 @@ struct EffectiveTransportSource {
format_version: u32,
profile_id: String,
retry: EffectiveRetrySource,
#[serde(default)]
ws_defaults: std::option::Option<EffectiveWsSessionSource>,
endpoints: std::vec::Vec<EffectiveEndpointSource>,
#[serde(default)]
ws_endpoints: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
}
#[derive(serde::Deserialize)]
@@ -165,7 +197,69 @@ struct EffectiveLimitsSource {
pause_after_rate_limit_ms: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveWsSessionSource {
command_timeout_ms: u64,
close_timeout_ms: u64,
reconnect: EffectiveWsReconnectSource,
resubscribe: String,
command_queue_capacity: usize,
notification_queue_capacity: usize,
max_active_subscriptions: usize,
max_pending_requests: usize,
max_message_size_bytes: usize,
max_frame_size_bytes: usize,
max_write_buffer_size_bytes: usize,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveWsReconnectSource {
max_retries: u32,
initial_backoff_ms: u64,
max_backoff_ms: u64,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveWsEndpointSource {
name: String,
enabled: bool,
provider: String,
cluster: String,
kind: String,
url: String,
#[serde(default)]
session: std::option::Option<EffectiveWsSessionOverrideSource>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveWsSessionOverrideSource {
command_timeout_ms: std::option::Option<u64>,
close_timeout_ms: std::option::Option<u64>,
reconnect: std::option::Option<EffectiveWsReconnectOverrideSource>,
resubscribe: std::option::Option<String>,
command_queue_capacity: std::option::Option<usize>,
notification_queue_capacity: std::option::Option<usize>,
max_active_subscriptions: std::option::Option<usize>,
max_pending_requests: std::option::Option<usize>,
max_message_size_bytes: std::option::Option<usize>,
max_frame_size_bytes: std::option::Option<usize>,
max_write_buffer_size_bytes: std::option::Option<usize>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveWsReconnectOverrideSource {
max_retries: std::option::Option<u32>,
initial_backoff_ms: std::option::Option<u64>,
max_backoff_ms: std::option::Option<u64>,
}
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);
let effective = match effective {
std::result::Result::Ok(value) => value,
@@ -180,12 +274,10 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
);
},
};
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 format_version = source.format_version;
let retry = ksp_onchain_transport_lib::HttpRetrySettings::new(
source.retry.max_retries,
std::time::Duration::from_millis(source.retry.initial_backoff_ms),
@@ -197,14 +289,24 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
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));
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(transport_contract_error(profile, "effective HTTP Transport settings fail the Transport runtime contract", &error));
}
let ws_settings = map_optional_ws_settings(format_version, source.ws_defaults, source.ws_endpoints, profile);
let ws_settings = match ws_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,
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
profile_id = profile.profile_id(),
endpoint_count = settings.endpoints().len(),
format_version,
http_endpoint_count = settings.endpoints().len(),
ws_endpoint_count,
"mapped standard Transport Config to runtime settings"
);
return std::result::Result::Ok(ResolvedTransportConfig {
@@ -214,9 +316,49 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
selection_source: profile.selection_source(),
effective,
settings,
ws_settings,
});
}
fn map_optional_ws_settings(
format_version: u32,
defaults: std::option::Option<EffectiveWsSessionSource>,
sources: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>> {
return match format_version {
1 => {
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");
std::result::Result::Ok(std::option::Option::None)
}
},
2 => {
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")),
};
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")),
};
let endpoints = map_ws_endpoints(sources, &defaults, 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::WsTransportSettings::new(endpoints);
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(transport_contract_error(profile, "effective WebSocket 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_endpoints(
sources: std::vec::Vec<EffectiveEndpointSource>,
profile: &crate::ResolvedConfigProfile,
@@ -229,7 +371,7 @@ fn map_endpoints(
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),
transport_contract_error(profile, "effective HTTP endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name),
);
},
};
@@ -253,6 +395,171 @@ fn map_endpoints(
return std::result::Result::Ok(endpoints);
}
fn map_ws_endpoints(
sources: std::vec::Vec<EffectiveWsEndpointSource>,
defaults: &EffectiveWsSessionSource,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::WsEndpointSettings>> {
let mut endpoints = std::vec::Vec::<ksp_onchain_transport_lib::WsEndpointSettings>::with_capacity(sources.len());
for source in sources {
let endpoint_name = source.name.clone();
let protocol = map_ws_protocol_kind(source.kind.as_str(), profile, endpoint_name.as_str());
let protocol = match protocol {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let url = ksp_onchain_transport_lib::WsEndpointUrl::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 WebSocket endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name),
);
},
};
let session = map_ws_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),
};
endpoints.push(ksp_onchain_transport_lib::WsEndpointSettings::new(
source.name,
source.enabled,
ksp_onchain_transport_lib::WsProviderName::new(source.provider),
ksp_onchain_transport_lib::WsClusterName::new(source.cluster),
protocol,
url,
session,
));
}
return std::result::Result::Ok(endpoints);
}
fn map_ws_protocol_kind(
value: &str,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsProtocolKind> {
return match value {
"solana_standard" => std::result::Result::Ok(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard),
_ => std::result::Result::Err(
effective_error(profile, "effective WebSocket protocol kind is unsupported")
.with_context("endpoint_name", endpoint_name)
.with_context("ws_kind", value),
),
};
}
fn map_ws_session_settings(
defaults: &EffectiveWsSessionSource,
overrides: std::option::Option<&EffectiveWsSessionOverrideSource>,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsSessionSettings> {
let mut command_timeout_ms = defaults.command_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 resubscribe_text = defaults.resubscribe.clone();
let mut command_queue_capacity = defaults.command_queue_capacity;
let mut notification_queue_capacity = defaults.notification_queue_capacity;
let mut max_active_subscriptions = defaults.max_active_subscriptions;
let mut max_pending_requests = defaults.max_pending_requests;
let mut max_message_size_bytes = defaults.max_message_size_bytes;
let mut max_frame_size_bytes = defaults.max_frame_size_bytes;
let mut max_write_buffer_size_bytes = defaults.max_write_buffer_size_bytes;
if let std::option::Option::Some(overrides) = overrides {
if let std::option::Option::Some(value) = overrides.command_timeout_ms {
command_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.resubscribe.as_ref() {
resubscribe_text = value.clone();
}
if let std::option::Option::Some(value) = overrides.command_queue_capacity {
command_queue_capacity = value;
}
if let std::option::Option::Some(value) = overrides.notification_queue_capacity {
notification_queue_capacity = value;
}
if let std::option::Option::Some(value) = overrides.max_active_subscriptions {
max_active_subscriptions = value;
}
if let std::option::Option::Some(value) = overrides.max_pending_requests {
max_pending_requests = value;
}
if let std::option::Option::Some(value) = overrides.max_message_size_bytes {
max_message_size_bytes = value;
}
if let std::option::Option::Some(value) = overrides.max_frame_size_bytes {
max_frame_size_bytes = value;
}
if let std::option::Option::Some(value) = overrides.max_write_buffer_size_bytes {
max_write_buffer_size_bytes = value;
}
}
let reconnect = ksp_onchain_transport_lib::WsReconnectSettings::new(
reconnect_max_retries,
std::time::Duration::from_millis(reconnect_initial_backoff_ms),
std::time::Duration::from_millis(reconnect_max_backoff_ms),
);
let resubscribe = map_ws_resubscribe_policy(resubscribe_text.as_str(), profile, endpoint_name);
let resubscribe = match resubscribe {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let settings = ksp_onchain_transport_lib::WsSessionSettings::new(
std::time::Duration::from_millis(command_timeout_ms),
std::time::Duration::from_millis(close_timeout_ms),
reconnect,
resubscribe,
command_queue_capacity,
notification_queue_capacity,
max_active_subscriptions,
max_pending_requests,
max_message_size_bytes,
max_frame_size_bytes,
max_write_buffer_size_bytes,
);
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(
transport_contract_error(profile, "effective WebSocket session settings fail the Transport runtime contract", &error)
.with_context("endpoint_name", endpoint_name),
);
}
return std::result::Result::Ok(settings);
}
fn map_ws_resubscribe_policy(
value: &str,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsResubscribePolicy> {
return match value {
"never" => std::result::Result::Ok(ksp_onchain_transport_lib::WsResubscribePolicy::Never),
"active_subscriptions" => std::result::Result::Ok(ksp_onchain_transport_lib::WsResubscribePolicy::ActiveSubscriptions),
_ => std::result::Result::Err(
effective_error(profile, "effective WebSocket resubscribe policy is unsupported")
.with_context("endpoint_name", endpoint_name)
.with_context("resubscribe", value),
),
};
}
fn map_roles(
sources: std::vec::Vec<EffectiveRoleSource>,
profile: &crate::ResolvedConfigProfile,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 21
// version: 22
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport adapters and management contracts.
@@ -252,6 +252,9 @@ fn transport_adapter_contract_is_available_from_crate_root() {
let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_transport_config;
let _composite_loader = ksp_config_lib::ConfigDocumentEngine::resolve_transport_config_profile;
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 _into_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_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

@@ -1,10 +1,27 @@
{
"format_version": 1,
"format_version": 2,
"retry": {
"max_retries": 4,
"initial_backoff_ms": 125,
"max_backoff_ms": 2500
},
"ws_defaults": {
"command_timeout_ms": 8000,
"close_timeout_ms": 4000,
"reconnect": {
"max_retries": 6,
"initial_backoff_ms": 200,
"max_backoff_ms": 4000
},
"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
},
"default_profile": "secret_test",
"profiles": [
{
@@ -23,7 +40,9 @@
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"request_kinds": [
"*"
],
"priority": 7,
"limits": {
"requests_per_second": 9,
@@ -34,6 +53,25 @@
}
]
}
],
"ws_endpoints": [
{
"name": "fixture_private_ws",
"enabled": true,
"provider": "fixture-provider",
"cluster": "fixture-cluster",
"kind": "solana_standard",
"url": "${KSP_SECRET_TRANSPORT_TEST_WS_URL:-wss://fallback.invalid}",
"session": {
"command_timeout_ms": 4500,
"reconnect": {
"max_retries": 7
},
"resubscribe": "never",
"notification_queue_capacity": 48,
"max_pending_requests": 24
}
}
]
}
]

View File

@@ -0,0 +1,42 @@
{
"format_version": 1,
"retry": {
"max_retries": 1,
"initial_backoff_ms": 90,
"max_backoff_ms": 900
},
"default_profile": "legacy_http",
"profiles": [
{
"profile_id": "legacy_http",
"endpoints": [
{
"name": "legacy_http",
"enabled": true,
"provider": "legacy-provider",
"cluster": "devnet",
"url": "https://legacy.invalid",
"connect_timeout_ms": 5000,
"request_timeout_ms": 2200,
"max_idle_connections_per_host": 2,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 10,
"limits": {
"requests_per_second": 2,
"burst_capacity": 3,
"max_concurrent_requests": 2,
"pause_after_rate_limit_ms": 250
}
}
]
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/transport.rs
// version: 2
// version: 3
#[test]
fn fixture_transport_profile_maps_complete_runtime_contract() {
@@ -41,6 +41,49 @@ 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)));
let ws = resolved.ws_settings();
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints().len(), 1);
let endpoint = &ws.endpoints()[0];
assert_eq!(endpoint.name(), "fixture_private_ws");
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
assert_eq!(endpoint.cluster().as_str(), "fixture-cluster");
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
assert_eq!(endpoint.url().as_str(), "wss://fallback.invalid");
assert_eq!(endpoint.session().command_timeout(), std::time::Duration::from_millis(4500));
assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(4000));
assert_eq!(endpoint.session().reconnect().max_retries(), 7);
assert_eq!(endpoint.session().reconnect().initial_backoff(), std::time::Duration::from_millis(200));
assert_eq!(endpoint.session().reconnect().max_backoff(), std::time::Duration::from_millis(4000));
assert_eq!(endpoint.session().resubscribe(), ksp_onchain_transport_lib::WsResubscribePolicy::Never);
assert_eq!(endpoint.session().command_queue_capacity(), 64);
assert_eq!(endpoint.session().notification_queue_capacity(), 48);
assert_eq!(endpoint.session().max_active_subscriptions(), 256);
assert_eq!(endpoint.session().max_pending_requests(), 24);
assert_eq!(endpoint.session().max_message_size_bytes(), 33_554_432);
assert_eq!(endpoint.session().max_frame_size_bytes(), 8_388_608);
assert_eq!(endpoint.session().max_write_buffer_size_bytes(), 524_288);
}
}
#[test]
fn v1_transport_fixture_remains_backward_readable_and_http_only() {
let engine = v1_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(), "strict Transport V1 fixture should remain readable: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "legacy_http");
assert_eq!(resolved.settings().retry().max_retries(), 1);
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");
}
}
#[test]
@@ -59,12 +102,23 @@ 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");
let ws = default.ws_settings();
assert!(ws.is_some(), "committed V2 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);
}
}
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");
let ws = mainnet.ws_settings();
assert!(ws.is_some(), "committed V2 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");
}
}
}
@@ -84,7 +138,9 @@ fn transport_profile_preserves_global_and_profile_origin() {
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("ws_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));
}
}
@@ -149,6 +205,36 @@ fn secret_transport_url_is_runtime_available_but_safe_projection_is_redacted() {
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
}
#[test]
fn secret_websocket_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 = "wss://user:pass@secret-provider.invalid/path?api-key=transport-ws-secret-canary";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_TRANSPORT_TEST_WS_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 WebSocket endpoint should map without being exposed: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let ws = resolved.ws_settings();
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints()[0].url().as_str(), canary);
}
let safe_url = resolved.effective().safe_value().pointer("/ws_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-ws-secret-canary"));
assert!(!debug.contains("user:pass"));
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
}
#[test]
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
let engine = fixture_engine();
@@ -216,6 +302,22 @@ fn fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
}
fn v1_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_v1");
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"));

225
deltas/0.2.7/pre.003.md Normal file
View File

@@ -0,0 +1,225 @@
<!-- file: deltas/0.2.7/pre.003.md -->
<!-- version: 1 -->
# Delta `0.2.7-pre.003` — `std.transport` V2 HTTP + WebSocket
## 1. Base requise
```text
0.2.7-pre.002-fix.001 appliquée
workspace.package.version = 0.2.7-pre.2.fix.1
```
Le checkpoint opérateur reçu avant cette tranche est entièrement vert : `cargo fmt`, audit Python, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, tests Transport ciblés et `cargo test --workspace`.
## 2. Signal technique
Cette prerelease non-fix modifie configuration exécutable, Rust et tests. Conformément à `VER-ID-009` :
```text
livraison = 0.2.7-pre.003
workspace.package.version = 0.2.7-pre.3
commit = v0.2.7-pre.003
```
Aucun tag prerelease.
## 3. `std.transport` V2
Le document standard livré devient :
```text
format_version = 2
retry
ws_defaults
default_profile
profiles[]
profile_id
endpoints[]
ws_endpoints[]
kind
session?
```
Le HTTP existant reste inchangé dans `endpoints[]`. `ws_defaults` contient les defaults génériques de `WsSessionSettings`; `ws_endpoints[].session` peut surcharger seulement les paramètres génériques nécessaires à un endpoint.
Le discriminateur est obligatoire :
```text
kind = solana_standard
```
Toute autre famille est rejetée en `0.2.7`. Aucun paramètre Helius, LaserStream ou autre provider-specific n'est pré-implémenté.
## 4. Backward V1 strict
Le schema enregistré passe à :
```text
urn:ksp:schema:std.transport:v2
```
Il conserve deux branches strictes discriminées par `format_version` :
```text
V1 -> HTTP-only historique
V2 -> HTTP + ws_defaults + ws_endpoints
```
Le V1 n'est pas rendu compatible par un relâchement de `additionalProperties`. Une fixture V1 dédiée prouve le chemin historique.
Dans l'adapter :
```text
V1 -> HttpTransportSettings + ws_settings = None
V2 -> HttpTransportSettings + Some(WsTransportSettings)
```
`WsTransportSettings` conserve donc son invariant `pre.002` : il n'existe jamais comme faux conteneur vide.
## 5. Adapter Config -> Transport
`ResolvedTransportConfig` conserve `settings()` pour compatibilité HTTP et ajoute :
```text
http_settings()
ws_settings() -> Option<&WsTransportSettings>
into_transport_settings() -> (HttpTransportSettings, Option<WsTransportSettings>)
```
Le mapper V2 construit :
```text
WsProviderName
WsClusterName
WsProtocolKind::SolanaStandard
WsEndpointUrl
WsReconnectSettings
WsResubscribePolicy
WsSessionSettings
WsEndpointSettings
WsTransportSettings
```
La direction reste strictement `Config -> Transport`. Aucun import de Config n'est ajouté à `ksp-onchain-transport-lib`.
## 6. Overrides session
Les valeurs de `ws_defaults` correspondent aux defaults KSP matérialisés en `pre.002`. Un endpoint peut surcharger indépendamment :
```text
command_timeout_ms
close_timeout_ms
reconnect.{max_retries, initial_backoff_ms, max_backoff_ms}
resubscribe
command_queue_capacity
notification_queue_capacity
max_active_subscriptions
max_pending_requests
max_message_size_bytes
max_frame_size_bytes
max_write_buffer_size_bytes
```
La session résultante est toujours validée par `WsSessionSettings::validate()`.
## 7. Secrets et logging
Les URLs HTTP et WebSocket peuvent provenir de `KSP_SECRET_*`; les valeurs réelles restent disponibles au runtime mais la projection Config safe les redacted. Les erreurs d'adaptation ne recopient pas l'URL.
Le mapping Config utilise exclusivement `ksp-logging-lib` avec le `TRACING_TARGET` existant de `ksp-config-lib` :
```text
trace -> début mapping et chemin backward V1
debug -> version, compteurs HTTP/WS après validation
```
Aucun `tracing` direct n'est ajouté.
## 8. Environment inventory
Ajouts `.env.example` :
```text
KSP_PUBLIC_SOLANA_DEVNET_WS_URL
KSP_PUBLIC_SOLANA_MAINNET_WS_URL
# KSP_SECRET_SOLANA_WS_URL
```
Les URLs provider privées restent des valeurs complètes gérées via `KSP_SECRET_*`.
## 9. Tests ajoutés/étendus
```text
V2 fixture HTTP + WS complète
ws_defaults + endpoint overrides
V1 strict toujours chargeable
V1 n'invente pas de WsTransportSettings vide
committed Devnet/Mainnet V2 mappe HTTP + WS
provenance ws_defaults globale et ws_endpoints profil
secret WebSocket URL disponible au runtime mais redacted en safe/Debug
public API canary pour les nouveaux accessors
```
## 10. Fichiers principaux modifiés
```text
Cargo.toml
.env.example
config/std.transport.json
config/examples/std.transport.example.json
config/schemas/std.transport.schema.json
crates/ksp-config-lib/src/transport.rs
crates/ksp-config-lib/src/lib.rs
crates/ksp-config-lib/src/registry.rs
crates/ksp-config-lib/unit_tests/transport.rs
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
crates/ksp-config-lib/tests/public_api.rs
crates/ksp-config-lib/README.md
crates/ksp-config-lib/USAGE.md
docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md
docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md
```
Nouveau :
```text
crates/ksp-config-lib/unit_tests/fixtures_v1/std.transport.json
deltas/0.2.7/pre.003.md
```
Aucune dépendance externe n'est ajoutée.
## 11. Validation de préparation
Le sandbox de génération ne fournit pas Cargo. Sont exécutés ici :
```text
python3 scripts/audit_rust_workspace_rules.py
validation JSON des documents/schema
inspection absence de tracing direct
inspection version workspace
inspection archive delta
```
Les gates compilés restent opérateur-only.
## 12. Gates opérateur avant commit
```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-onchain-transport-lib
cargo test --workspace
```
Si le checkpoint est vert :
```text
commit = v0.2.7-pre.003
```
La tranche suivante est `0.2.7-pre.004` : dépendances WebSocket, actor physique, handshake/read/write, pending JSON-RPC et serveur local déterministe.

View File

@@ -1,9 +1,9 @@
<!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Plan `0.2.7` — WebSocket Solana standard
> **Statut : actif, `0.2.7-pre.002`.** Le gate `pre.001` est clos. `pre.002` matérialise les settings WebSocket Transport, la redaction URL, le discriminateur de protocole, les identités locales, les états lifecycle et les snapshots sûrs. Le socket physique reste différé à `pre.004`.
> **Statut : actif, `0.2.7-pre.003`.** Les gates `pre.001` et `pre.002` sont clos. `pre.003` matérialise `std.transport` V2 HTTP + WebSocket, la lecture backward V1 HTTP-only et l'adapter `Config -> WsTransportSettings`. Le socket physique reste différé à `pre.004`.
## 1. Objet et base vérifiée
@@ -110,9 +110,9 @@ Aucun gate Cargo n'est déclaré réussi. Une validation opérateur est obligato
La surface WebSocket doit être additive : aucune renormalisation invasive des types `Http*` n'est requise dans `0.2.7`.
### 4.2 Config actuel est explicitement HTTP V1
### 4.2 Config hérité et évolution V2
`config/std.transport.json`, son schema et `ksp-config-lib/src/transport.rs` décrivent un document V1 HTTP :
À louverture de `0.2.7`, `config/std.transport.json`, son schema et `ksp-config-lib/src/transport.rs` décrivaient un document V1 HTTP :
```text
format_version = 1
@@ -124,7 +124,7 @@ format_version != 1 rejeté
Décision : **ne pas ajouter silencieusement des clés WebSocket au V1**.
`0.2.7` fera évoluer le même composant standard `std.transport` vers un **format V2 explicitement HTTP + WebSocket**, tout en conservant la lecture du V1 HTTP-only pour compatibilité. Config reste propriétaire du parsing/résolution et adapte ensuite vers les settings publics Transport.
`pre.003` fait évoluer le même composant standard `std.transport` vers un **format V2 explicitement HTTP + WebSocket**, tout en conservant la lecture du V1 HTTP-only pour compatibilité. Config reste propriétaire du parsing/résolution et adapte ensuite vers les settings publics Transport.
Shape cible minimale :
@@ -146,7 +146,21 @@ Décision d'extensibilité : **le conteneur V2 ne doit pas supposer qu'il n'exis
Cette préparation ne promet aucune option Helius dans `0.2.7`. L'audit provider-specific de la release suivante décidera la relation exacte entre Helius Enhanced WebSocket, LaserStream et le moteur standard avant d'ajouter des variantes/configurations concrètes.
Le V1 chargé sous `0.2.7` produit une configuration HTTP valide et une collection WebSocket vide. Le V2 devient le format livré par les fixtures Config dès la tranche qui matérialise le WebSocket.
Le V1 chargé sous `0.2.7` produit une configuration HTTP valide et aucune instance `WsTransportSettings`. Le V2 devient le format livré par les fixtures Config dès la tranche qui matérialise le WebSocket.
Checkpoint `pre.003` matérialisé :
```text
std.transport schema id = urn:ksp:schema:std.transport:v2
V1 strict HTTP-only -> toujours accepté
V2 strict HTTP + WS -> format livré
ws_defaults -> session defaults génériques
ws_endpoints[].session -> overrides génériques optionnels
ws_endpoints[].kind -> solana_standard uniquement en 0.2.7
ResolvedTransportConfig -> HTTP toujours présent + WS Option pour backward V1
```
Le schema V2 utilise deux branches strictes discriminées par `format_version`; la compatibilité V1 n'est donc pas obtenue en relâchant `additionalProperties`. Les overrides endpoint ne contiennent que des paramètres génériques de `WsSessionSettings`, jamais des options Helius/provider-specific.
## 5. Audit historique bot3
@@ -853,7 +867,7 @@ L'inventaire officiel n'impose que 9 familles de subscriptions, mais le lifecycl
```text
pre.001 audit interne/externe + matrice 18 méthodes + bot3 + dependencies + threat model + plan/sizing
pre.002 DONE — settings WS Transport + URL redaction + IDs/states/snapshots + tests de settings
pre.003 std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings
pre.003 DONE — std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings
pre.004 deps tokio-tungstenite/futures-util + actor physique + handshake/read/write + pending JSON-RPC + serveur local
pre.005 limites frame/message/request + control frames + cancellation/close/shutdown + adversarial socket tests
pre.006 registry subscriptions + IDs locaux + generic subscribe/unsubscribe engine + channels typed bounded

View File

@@ -1,9 +1,9 @@
<!-- file: docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Validation `0.2.7` — WebSocket Solana standard
> **Statut : matrice active, `0.2.7-pre.002`.** Le gate normatif `pre.001` est clos ; les settings, identités, états lifecycle et snapshots sûrs sont maintenant matérialisés. Les preuves runtime socket/subscription restent ouvertes.
> **Statut : matrice active, `0.2.7-pre.003`.** Les settings/lifecycle `pre.002` et la composition Config V2 `pre.003` sont matérialisés. Les preuves runtime socket/subscription restent ouvertes.
## 1. Baseline normative
@@ -253,7 +253,7 @@ Aucune de ces dependencies n'est ajoutée par `pre.001`; le graphe Cargo stable
## 9. Config compliance initiale
V1 actuel : HTTP-only strict. Décision :
V1 hérité : HTTP-only strict. Contrat de compatibilité :
```text
V1 -> support de lecture conservé, WS vide
@@ -266,7 +266,24 @@ Transport -X-> Config
Le type Transport correspondant au discriminateur est prévu `#[non_exhaustive]`. L'objectif est de pouvoir ajouter ultérieurement une famille telle que Helius Enhanced WebSocket sans créer un nouveau conteneur Config ni injecter des options provider-specific dans les settings Solana standard. Toute valeur inconnue reste explicitement rejetée en `0.2.7`; aucun paramètre Helius n'est implémenté par anticipation.
La schema V1 n'est pas assouplie. Une schema V2 explicite remplace la fixture standard au moment où l'adapter est matérialisé.
`pre.003` remplace le schema enregistré par `urn:ksp:schema:std.transport:v2`, avec deux branches strictes : V1 HTTP-only et V2 HTTP + WebSocket. La compatibilité V1 ne relâche donc ni `additionalProperties`, ni la shape historique.
Preuves `pre.003` :
```text
config/std.transport.json format_version = 2
config/examples/std.transport.example.json format_version = 2
V1 fixture dédiée load + HTTP mapping OK attendu
V1 ws_settings = None
V2 ws_settings = Some(validated)
ws_defaults globals
profiles[].ws_endpoints profile-local
ws_endpoints[].kind enum schema solana_standard
ws_endpoints[].session overrides génériques optionnels
Config -> Transport seule direction de dépendance
```
`ResolvedTransportConfig::settings()` reste l'accesseur HTTP historique. `http_settings()` l'explicite et `ws_settings()` expose `Option<&WsTransportSettings>` afin que le V1 backward ne force jamais un faux `WsTransportSettings` vide. `into_transport_settings()` permet de consommer les deux contrats ensemble.
## 9.1 Checkpoint settings/lifecycle `pre.002`
@@ -359,7 +376,7 @@ N subscriptions same session prouvé
reconnect/resubscribe/backpressure/shutdown gates verts
unstable warnings centralisés
HTTP 52+14 non régressé
Config V1 backward + V2 WS validés
Config V1 backward + V2 WS validés (**Done `pre.003`**, compilation opérateur requise)
smoke live opt-in documenté
cargo tree inspecté
cargo test --workspace vert