v0.2.1-pre.006

This commit is contained in:
2026-08-17 21:03:51 +02:00
parent 14bcbf2cfb
commit f5d98c4e69
21 changed files with 1270 additions and 46 deletions

View File

@@ -1,10 +1,22 @@
# file: .env.example
# version: 2
# version: 3
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
KSP_LOGS_DIRECTORY=logs
# Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json.
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example.
# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
# Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready.
KSP_DESK_SPLASH_MINIMUM_MS=1200

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 104
# version: 105
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
[workspace.package]
version = "0.2.1-pre.5.fix.1"
version = "0.2.1-pre.6"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 31 -->
<!-- version: 32 -->
# Roadmap KSP
@@ -41,7 +41,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
### Cadrage
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
- [/] `0.2.1` — HTTP foundation en cours : matrice 52+14, crate/settings/JSON-RPC/registry, client/pool/routing, résilience runtime et exécution HTTP acquises ; les 4 canaris typés (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) sont matérialisés, Config standard et clôture restent planifiés.
- [/] `0.2.1` — HTTP foundation en cours : matrice 52+14, runtime/routing/résilience/exécution HTTP et 4 canaris typés acquis ; `std.transport` et ladapter Config -> Transport sont matérialisés, la compliance/smoke/documentation de clôture reste planifiée.
### Releases fonctionnelles décidées/pressenties

View File

@@ -0,0 +1,64 @@
{
"format_version": 1,
"retry": {
"max_retries": 3,
"initial_backoff_ms": 150,
"max_backoff_ms": 3000
},
"default_profile": "mainnet_mixed",
"profiles": [
{
"profile_id": "mainnet_mixed",
"endpoints": [
{
"name": "mainnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 200,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
},
{
"name": "mainnet_private",
"enabled": true,
"provider": "private-provider",
"cluster": "mainnet-beta",
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 10000,
"max_idle_connections_per_host": 16,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 100,
"limits": {
"requests_per_second": 20,
"burst_capacity": 40,
"max_concurrent_requests": 16,
"pause_after_rate_limit_ms": 750
}
}
]
}
]
}
]
}

View File

@@ -0,0 +1,124 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.transport:v1",
"title": "KSP standard HTTP Transport configuration",
"type": "object",
"additionalProperties": false,
"required": ["format_version", "retry", "default_profile", "profiles"],
"properties": {
"format_version": {"const": 1},
"retry": {"$ref": "#/$defs/retry"},
"default_profile": {"$ref": "#/$defs/profileId"},
"profiles": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/$defs/profile"}
}
},
"$defs": {
"profileId": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*$"
},
"descriptor": {
"type": "string",
"minLength": 1,
"pattern": "^\\S(?:.*\\S)?$"
},
"positiveMs": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"positiveU32": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295
},
"retry": {
"type": "object",
"additionalProperties": false,
"required": ["max_retries", "initial_backoff_ms", "max_backoff_ms"],
"properties": {
"max_retries": {"type": "integer", "minimum": 0, "maximum": 100},
"initial_backoff_ms": {"$ref": "#/$defs/positiveMs"},
"max_backoff_ms": {"$ref": "#/$defs/positiveMs"}
}
},
"limits": {
"type": "object",
"additionalProperties": false,
"properties": {
"requests_per_second": {"$ref": "#/$defs/positiveU32"},
"burst_capacity": {"$ref": "#/$defs/positiveU32"},
"max_concurrent_requests": {"$ref": "#/$defs/positiveU32"},
"pause_after_rate_limit_ms": {"$ref": "#/$defs/positiveMs"}
}
},
"role": {
"type": "object",
"additionalProperties": false,
"required": ["role", "enabled", "request_kinds", "priority", "limits"],
"properties": {
"role": {"$ref": "#/$defs/descriptor"},
"enabled": {"type": "boolean"},
"request_kinds": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"$ref": "#/$defs/descriptor"},
"allOf": [
{
"if": {"contains": {"const": "*"}},
"then": {"maxItems": 1}
}
]
},
"priority": {"type": "integer", "minimum": 0, "maximum": 4294967295},
"limits": {"$ref": "#/$defs/limits"}
}
},
"endpoint": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"enabled",
"provider",
"cluster",
"url",
"connect_timeout_ms",
"request_timeout_ms",
"roles"
],
"properties": {
"name": {"$ref": "#/$defs/descriptor"},
"enabled": {"type": "boolean"},
"provider": {"$ref": "#/$defs/descriptor"},
"cluster": {"$ref": "#/$defs/descriptor"},
"url": {"type": "string", "minLength": 1},
"connect_timeout_ms": {"$ref": "#/$defs/positiveMs"},
"request_timeout_ms": {"$ref": "#/$defs/positiveMs"},
"max_idle_connections_per_host": {"type": "integer", "minimum": 1},
"roles": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/$defs/role"}
}
}
},
"profile": {
"type": "object",
"additionalProperties": false,
"required": ["profile_id", "endpoints"],
"properties": {
"profile_id": {"$ref": "#/$defs/profileId"},
"endpoints": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/$defs/endpoint"}
}
}
}
}
}

69
config/std.transport.json Normal file
View File

@@ -0,0 +1,69 @@
{
"format_version": 1,
"retry": {
"max_retries": 2,
"initial_backoff_ms": 100,
"max_backoff_ms": 2000
},
"default_profile": "devnet_public",
"profiles": [
{
"profile_id": "devnet_public",
"endpoints": [
{
"name": "solana_devnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "devnet",
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
]
},
{
"profile_id": "mainnet_public",
"endpoints": [
{
"name": "solana_mainnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
"connect_timeout_ms": 5000,
"request_timeout_ms": 15000,
"max_idle_connections_per_host": 8,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 100,
"limits": {
"requests_per_second": 5,
"burst_capacity": 10,
"max_concurrent_requests": 8,
"pause_after_rate_limit_ms": 1000
}
}
]
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-config-lib/Cargo.toml
# version: 4
# version: 5
[package]
name = "ksp-config-lib"
@@ -10,6 +10,7 @@ repository.workspace = true
[dependencies]
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
jsonschema.workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# ksp-config-lib
@@ -23,6 +23,7 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
- la classification `Public`, `Internal`, `Secret` ;
- les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ;
- l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ;
- l'adapter du document HTTP Transport effectif vers `ksp_onchain_transport_lib::HttpTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ;
- la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ;
- les écritures atomiques JSON/`.env` et la protection des permissions `.env` ;
- les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`.
@@ -32,9 +33,11 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
Le registre par défaut connaît :
```text
cfg.std.logging -> config/std.logging.json
schema.std.logging -> config/schemas/std.logging.schema.json
schema.composite -> config/schemas/composite.schema.json
cfg.std.logging -> config/std.logging.json
cfg.std.transport -> config/std.transport.json
schema.std.logging -> config/schemas/std.logging.schema.json
schema.std.transport -> config/schemas/std.transport.schema.json
schema.composite -> config/schemas/composite.schema.json
```
`ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques.
@@ -59,11 +62,11 @@ Les autres crates et applications KSP ne doivent pas :
- parser ou écrire directement `.env` ;
- ouvrir directement les documents Config connus par leur filename physique ;
- réimplémenter la sélection de profils, les compositions ou les placeholders ;
- reconstruire elles-mêmes la configuration Logging depuis le JSON.
- reconstruire elles-mêmes la configuration Logging ou Transport depuis le JSON.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result` et de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport.
La dépendance inverse est interdite : `ksp-core-lib` et `ksp-logging-lib` ne dépendent pas de Config.
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib` et `ksp-onchain-transport-lib` ne dépendent pas de Config.
Config ne possède pas le `LoggingGuard`. L'application ou le service qui orchestre le runtime construit la configuration effective puis possède le lifecycle `ksp_logging_lib::initialize/reinitialize`.
@@ -75,12 +78,13 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted.
## Documentation
- [`USAGE.md`](USAGE.md) — construction du moteur, résolution runtime et management ;
- [`TODO.md`](TODO.md) — points explicitement différés après `0.1.3` ;
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
- [`../../config/std.logging.json`](../../config/std.logging.json) — premier document standard concret ;
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ;
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/TODO.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# TODO ksp-config-lib
@@ -31,11 +31,15 @@ La validation applicative desktop appartient à `ksp-app-config-desk` :
Ces points ne nécessitent pas de duplication de logique dans `ksp-config-lib`; toute lacune réelle révélée par l'application ouvrira un delta Config explicite.
## Extension `0.2.1` — Transport HTTP
`0.2.1-pre.006` introduit le premier nouveau domaine standard depuis Logging : `std.transport.json`, son schema, son exemple, son enregistrement et l'adapter Config -> `HttpTransportSettings`. Cette extension confirme que les nouveaux domaines restent ajoutés à la demande d'un consumer réel, sans transformer Config en propriétaire du runtime Transport.
## Futur, uniquement au besoin
Les capacités suivantes sont différées jusqu'à l'apparition de composants réels :
- nouveaux documents `std.<domain>.json` et schemas associés ;
- documents `std.<domain>.json` supplémentaires et schemas associés ;
- descriptors `cfg.composite.<consumer>` pour de vrais consumers ;
- contrats typés de management supplémentaires pour les nouveaux documents ;
- watcher filesystem/reload automatique si une application ou un service démontre le besoin ;

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Utilisation de ksp-config-lib
@@ -29,6 +29,7 @@ Les arguments compris par Config sont :
--cfgpath=/path/to/config
--schemapath=/path/to/schemas
--filemap=cfg.std.logging=my-logging.json
--filemap=cfg.std.transport=my-transport.json
```
`cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif.
@@ -122,6 +123,23 @@ Un `logs_directory` relatif est ancré sur le current working directory du proce
Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation.
### 4.1 Construire le Transport HTTP depuis Config
Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` :
```rust
let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_settings = transport.into_settings();
```
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted.
La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
## 5. Profils et composites
Pour un document standard profilé :
@@ -213,6 +231,7 @@ Ce guide reste volontairement indépendant des numéros de release. Les contrats
| Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §78 |
| Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 |
| Logging effectif | `ResolvedLoggingConfig` | §4 |
| Transport effectif | `ResolvedTransportConfig` | §4.1 |
| Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §67 |
| Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous |
| Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous |
@@ -324,4 +343,3 @@ if let std::result::Result::Err(error) = loaded {
```
Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 11
// version: 12
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -8,8 +8,8 @@
//!
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
//! provenance. The standard Logging document maps explicitly to `ksp_logging_lib::LoggingSettings`, while the management surface provides typed Logging
//! mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
//! provenance. Standard Logging and HTTP Transport documents map explicitly to their runtime settings contracts, while the management surface provides
//! typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
mod bootstrap;
mod composite;
@@ -23,6 +23,7 @@ mod persistence;
mod profile;
mod registry;
mod sensitivity;
mod transport;
pub(crate) use self::constants::TRACING_TARGET;
@@ -144,12 +145,20 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
/// Default physical filename for the standard Logging JSON Schema document.
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
/// Default physical filename for the standard HTTP Transport configuration document.
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
/// Logical file identifier for the generic composite JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
/// Logical file identifier for the standard Logging JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
/// Logical file identifier for the standard Logging configuration document.
pub use self::registry::FILE_ID_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport configuration document.
pub use self::registry::FILE_ID_STD_TRANSPORT;
/// Sensitivity assigned to one Config value after environment resolution.
pub use self::sensitivity::ConfigSensitivity;
/// Provenance segment participating in one resolved Config value.
@@ -160,3 +169,5 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
pub use self::sensitivity::ResolvedConfigJson;
/// One resolved Config string preserving real/safe representations and provenance.
pub use self::sensitivity::ResolvedConfigText;
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
pub use self::transport::ResolvedTransportConfig;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 4
// version: 5
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
@@ -7,12 +7,20 @@ pub const ARG_FILE_MAP: &str = "--filemap";
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the standard HTTP Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Default physical filename for the standard Logging configuration document.
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
/// Default physical filename for the standard Logging JSON Schema document.
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
/// Default physical filename for the standard HTTP Transport configuration document.
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Default physical filename for the generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
@@ -135,13 +143,29 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport = ConfigFileDescriptor::new(
FILE_ID_STD_TRANSPORT,
ConfigFileKind::Config,
DEFAULT_STD_TRANSPORT_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_STD_TRANSPORT),
);
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_TRANSPORT, ConfigFileKind::Schema, DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, std::option::Option::None);
let transport_schema = match transport_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
let composite_schema = match composite_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return build_registry([logging, logging_schema, composite_schema]);
return build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
}
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.

View File

@@ -0,0 +1,316 @@
// file: crates/ksp-config-lib/src/transport.rs
// version: 1
/// Effective standard HTTP Transport configuration resolved from Config and mapped to the Transport runtime contract.
#[derive(Clone, Eq, PartialEq)]
pub struct ResolvedTransportConfig {
file_id: crate::ConfigFileId,
source_path: std::path::PathBuf,
profile_id: String,
selection_source: crate::ConfigProfileSelectionSource,
effective: crate::ResolvedConfigJson,
settings: ksp_onchain_transport_lib::HttpTransportSettings,
}
impl ResolvedTransportConfig {
/// Returns the logical Config document identifier used by this runtime configuration.
#[must_use]
pub const fn file_id(&self) -> &crate::ConfigFileId {
return &self.file_id;
}
/// Returns the physical source Config document path.
#[must_use]
pub fn source_path(&self) -> &std::path::Path {
return self.source_path.as_path();
}
/// Returns the selected standard Transport profile identifier.
#[must_use]
pub fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the source that selected the standard Transport profile.
#[must_use]
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
return self.selection_source;
}
/// Returns the detailed environment-resolved effective Config view.
///
/// The real tree is available to legitimate runtime consumers. The safe tree redacts values originating from `KSP_SECRET_*` or `KSPB_SECRET_*`
/// placeholders and is the only representation used by this type's [`std::fmt::Debug`] implementation.
#[must_use]
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
return &self.effective;
}
/// Returns the validated runtime HTTP Transport settings.
#[must_use]
pub const fn settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings {
return &self.settings;
}
/// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings.
#[must_use]
pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings {
return self.settings;
}
}
impl std::fmt::Debug for ResolvedTransportConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("ResolvedTransportConfig")
.field("file_id", &self.file_id)
.field("source_path", &self.source_path)
.field("profile_id", &self.profile_id)
.field("selection_source", &self.selection_source)
.field("effective", &self.effective)
.finish_non_exhaustive();
}
}
impl crate::ConfigDocumentEngine {
/// Loads the standard HTTP Transport document, selects a profile, resolves environment placeholders and maps it to Transport runtime settings.
///
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed
/// because the Transport URL wrapper owns runtime redaction. Invalid environment-resolved values are reported as
/// [`crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID`] without copying endpoint URL values into ordinary error context.
pub fn load_resolved_transport_config(
&self,
requested_profile: std::option::Option<&str>,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedTransportConfig> {
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = self.load_resolved_profile(&file_id, requested_profile);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return resolve_transport_profile(&profile, environment);
}
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveTransportSource {
format_version: u32,
profile_id: String,
retry: EffectiveRetrySource,
endpoints: std::vec::Vec<EffectiveEndpointSource>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveRetrySource {
max_retries: u32,
initial_backoff_ms: u64,
max_backoff_ms: u64,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveEndpointSource {
name: String,
enabled: bool,
provider: String,
cluster: String,
url: String,
connect_timeout_ms: u64,
request_timeout_ms: u64,
max_idle_connections_per_host: std::option::Option<usize>,
roles: std::vec::Vec<EffectiveRoleSource>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveRoleSource {
role: String,
enabled: bool,
request_kinds: std::vec::Vec<String>,
priority: u32,
limits: EffectiveLimitsSource,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveLimitsSource {
requests_per_second: std::option::Option<u32>,
burst_capacity: std::option::Option<u32>,
max_concurrent_requests: std::option::Option<u32>,
pause_after_rate_limit_ms: std::option::Option<u64>,
}
fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> {
let effective = profile.resolve_effective_environment_detailed(environment);
let effective = match effective {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let source = serde_json::from_value::<EffectiveTransportSource>(effective.value().clone());
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
effective_error(profile, "effective Transport Config cannot be decoded into the runtime adapter contract").with_source(error),
);
},
};
if source.format_version != 1 {
return std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported"));
}
if source.profile_id != profile.profile_id() {
return std::result::Result::Err(effective_error(profile, "effective Transport profile_id does not match the selected profile"));
}
let retry = ksp_onchain_transport_lib::HttpRetrySettings::new(
source.retry.max_retries,
std::time::Duration::from_millis(source.retry.initial_backoff_ms),
std::time::Duration::from_millis(source.retry.max_backoff_ms),
);
let endpoints = map_endpoints(source.endpoints, profile);
let endpoints = match endpoints {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(endpoints, retry);
let validation = settings.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(transport_contract_error(profile, "effective Transport settings fail the Transport runtime contract", &error));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
profile_id = profile.profile_id(),
endpoint_count = settings.endpoints().len(),
"mapped standard Transport Config to runtime settings"
);
return std::result::Result::Ok(ResolvedTransportConfig {
file_id: profile.file_id().clone(),
source_path: profile.path().to_path_buf(),
profile_id: profile.profile_id().to_owned(),
selection_source: profile.selection_source(),
effective,
settings,
});
}
fn map_endpoints(
sources: std::vec::Vec<EffectiveEndpointSource>,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::HttpEndpointSettings>> {
let mut endpoints = std::vec::Vec::<ksp_onchain_transport_lib::HttpEndpointSettings>::with_capacity(sources.len());
for source in sources {
let endpoint_name = source.name.clone();
let url = ksp_onchain_transport_lib::HttpEndpointUrl::parse(source.url);
let url = match url {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
transport_contract_error(profile, "effective endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name),
);
},
};
let roles = map_roles(source.roles, profile, endpoint_name.as_str());
let roles = match roles {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
endpoints.push(ksp_onchain_transport_lib::HttpEndpointSettings::new(
source.name,
source.enabled,
ksp_onchain_transport_lib::HttpProviderName::new(source.provider),
ksp_onchain_transport_lib::HttpClusterName::new(source.cluster),
url,
std::time::Duration::from_millis(source.connect_timeout_ms),
std::time::Duration::from_millis(source.request_timeout_ms),
source.max_idle_connections_per_host,
roles,
));
}
return std::result::Result::Ok(endpoints);
}
fn map_roles(
sources: std::vec::Vec<EffectiveRoleSource>,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::HttpEndpointRoleSettings>> {
let mut roles = std::vec::Vec::<ksp_onchain_transport_lib::HttpEndpointRoleSettings>::with_capacity(sources.len());
for source in sources {
let requests_per_second = map_non_zero(source.limits.requests_per_second, profile, "limits.requests_per_second", endpoint_name);
let requests_per_second = match requests_per_second {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let burst_capacity = map_non_zero(source.limits.burst_capacity, profile, "limits.burst_capacity", endpoint_name);
let burst_capacity = match burst_capacity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_concurrent_requests = map_non_zero(source.limits.max_concurrent_requests, profile, "limits.max_concurrent_requests", endpoint_name);
let max_concurrent_requests = match max_concurrent_requests {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pause_after_rate_limit = match source.limits.pause_after_rate_limit_ms {
std::option::Option::Some(value) => std::option::Option::Some(std::time::Duration::from_millis(value)),
std::option::Option::None => std::option::Option::None,
};
let limits = ksp_onchain_transport_lib::HttpRoleLimits::new(requests_per_second, burst_capacity, max_concurrent_requests, pause_after_rate_limit);
let mut request_kinds = std::vec::Vec::<ksp_onchain_transport_lib::HttpRequestKind>::with_capacity(source.request_kinds.len());
for request_kind in source.request_kinds {
request_kinds.push(ksp_onchain_transport_lib::HttpRequestKind::new(request_kind));
}
roles.push(ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
ksp_onchain_transport_lib::HttpRoleName::new(source.role),
source.enabled,
request_kinds,
source.priority,
limits,
));
}
return std::result::Result::Ok(roles);
}
fn map_non_zero(
value: std::option::Option<u32>,
profile: &crate::ResolvedConfigProfile,
field: &'static str,
endpoint_name: &str,
) -> ksp_core_lib::Result<std::option::Option<std::num::NonZeroU32>> {
let value = match value {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let non_zero = std::num::NonZeroU32::new(value);
return match non_zero {
std::option::Option::Some(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::option::Option::None => std::result::Result::Err(
effective_error(profile, "effective Transport role limit must be greater than zero")
.with_context("field", field)
.with_context("endpoint_name", endpoint_name),
),
};
}
fn transport_contract_error(profile: &crate::ResolvedConfigProfile, reason: &'static str, transport_error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
return effective_error(profile, reason)
.with_context("transport_error_domain", transport_error.code().domain())
.with_context("transport_error_code", transport_error.code().code());
}
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
.with_context("file_id", profile.file_id().as_str())
.with_context("profile_id", profile.profile_id())
.with_context("reason", reason);
}
#[cfg(test)]
#[path = "../unit_tests/transport.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/ownership.rs
// version: 2
// version: 3
//! Workspace ownership audits for KSP application configuration boundaries.
@@ -239,7 +239,14 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
std::result::Result::Ok(value) => non_comment_source(value.as_str()),
std::result::Result::Err(_) => continue,
};
for token in ["\".env\"", "\"std.logging.json\"", "\"std.logging.schema.json\"", "\"composite.schema.json\""] {
for token in [
"\".env\"",
"\"std.logging.json\"",
"\"std.logging.schema.json\"",
"\"std.transport.json\"",
"\"std.transport.schema.json\"",
"\"composite.schema.json\"",
] {
assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display());
}
}

View File

@@ -1,8 +1,8 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 16
// version: 17
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, Logging-adapter and
//! management contracts.
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport adapters and management contracts.
#[test]
fn bootstrap_contract_is_available_from_crate_root() {
@@ -80,15 +80,18 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 3);
assert_eq!(descriptors.len(), 5);
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[0].kind(), ksp_config_lib::ConfigFileKind::Config);
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
let schema_file_id = descriptors[0].schema_file_id();
assert!(schema_file_id.is_some(), "public descriptor inventory should preserve schema association");
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[1].kind(), ksp_config_lib::ConfigFileKind::Config);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
let schema_file_id = descriptors[1].schema_file_id();
assert!(schema_file_id.is_some(), "public Transport descriptor should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id {
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
}
}
}
@@ -241,3 +244,13 @@ fn management_contracts_are_available_from_crate_root() {
let _ = (save_source_candidate, reveal_effective, reveal_dotenv);
assert_ne!(ksp_config_lib::ERROR_CODE_MANAGEMENT_OPERATION_INVALID, ksp_config_lib::ERROR_CODE_PERSISTENCE_WRITE_FAILED);
}
#[test]
fn transport_adapter_contract_is_available_from_crate_root() {
let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_transport_config;
assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0);
assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport");
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT, "schema.std.transport");
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, "std.transport.schema.json");
}

View File

@@ -0,0 +1,40 @@
{
"format_version": 1,
"retry": {
"max_retries": 4,
"initial_backoff_ms": 125,
"max_backoff_ms": 2500
},
"default_profile": "secret_test",
"profiles": [
{
"profile_id": "secret_test",
"endpoints": [
{
"name": "fixture_private",
"enabled": true,
"provider": "fixture-provider",
"cluster": "fixture-cluster",
"url": "${KSP_SECRET_TRANSPORT_TEST_URL:-https://fallback.invalid}",
"connect_timeout_ms": 750,
"request_timeout_ms": 2500,
"max_idle_connections_per_host": 3,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": ["*"],
"priority": 7,
"limits": {
"requests_per_second": 9,
"burst_capacity": 12,
"max_concurrent_requests": 4,
"pause_after_rate_limit_ms": 650
}
}
]
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 4
// version: 5
#[test]
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
@@ -7,19 +7,29 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&super::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 3);
assert_eq!(descriptors.len(), 5);
assert_eq!(descriptors[0].file_id().as_str(), super::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[0].kind(), super::ConfigFileKind::Config);
assert_eq!(descriptors[0].filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME));
let schema_file_id = descriptors[0].schema_file_id();
assert!(schema_file_id.is_some(), "logging descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = schema_file_id {
let logging_schema_file_id = descriptors[0].schema_file_id();
assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = logging_schema_file_id {
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_LOGGING);
}
assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Schema);
assert_eq!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Config);
assert_eq!(descriptors[1].filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_FILENAME));
let transport_schema_file_id = descriptors[1].schema_file_id();
assert!(transport_schema_file_id.is_some(), "transport descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = transport_schema_file_id {
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_TRANSPORT);
}
assert_eq!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[2].kind(), super::ConfigFileKind::Schema);
assert_eq!(descriptors[3].file_id().as_str(), super::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[3].kind(), super::ConfigFileKind::Schema);
assert_eq!(descriptors[4].file_id().as_str(), super::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[4].kind(), super::ConfigFileKind::Schema);
}
}
@@ -83,6 +93,31 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
}
}
#[test]
fn defaults_register_transport_document_and_schema_with_distinct_roots() {
let registry = super::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let transport_id = super::ConfigFileId::new(super::FILE_ID_STD_TRANSPORT);
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_TRANSPORT);
assert!(transport_id.is_ok(), "transport file_id should be valid: {transport_id:?}");
assert!(schema_id.is_ok(), "transport schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(transport_id), std::result::Result::Ok(schema_id)) = (transport_id, schema_id) {
let transport = registry.descriptor(&transport_id);
let schema = registry.descriptor(&schema_id);
assert!(transport.is_ok(), "transport descriptor should exist: {transport:?}");
assert!(schema.is_ok(), "transport schema descriptor should exist: {schema:?}");
if let (std::result::Result::Ok(transport), std::result::Result::Ok(schema)) = (transport, schema) {
assert_eq!(transport.kind(), super::ConfigFileKind::Config);
assert_eq!(transport.filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_FILENAME));
assert_eq!(transport.schema_file_id(), std::option::Option::Some(&schema_id));
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME));
}
}
}
}
#[test]
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
let registry = super::ConfigFileRegistry::defaults();

View File

@@ -0,0 +1,202 @@
// file: crates/ksp-config-lib/unit_tests/transport.rs
// version: 1
#[test]
fn fixture_transport_profile_maps_complete_runtime_contract() {
let engine = fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "fixture Transport Config should map: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(resolved.profile_id(), "secret_test");
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(resolved.settings().retry().max_retries(), 4);
assert_eq!(resolved.settings().retry().initial_backoff(), std::time::Duration::from_millis(125));
assert_eq!(resolved.settings().retry().max_backoff(), std::time::Duration::from_millis(2500));
assert_eq!(resolved.settings().endpoints().len(), 1);
let endpoint = &resolved.settings().endpoints()[0];
assert_eq!(endpoint.name(), "fixture_private");
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
assert_eq!(endpoint.cluster().as_str(), "fixture-cluster");
assert_eq!(endpoint.url().as_str(), "https://fallback.invalid");
assert_eq!(endpoint.connect_timeout(), std::time::Duration::from_millis(750));
assert_eq!(endpoint.request_timeout(), std::time::Duration::from_millis(2500));
assert_eq!(endpoint.max_idle_connections_per_host(), std::option::Option::Some(3));
assert_eq!(endpoint.roles().len(), 1);
let role = &endpoint.roles()[0];
assert_eq!(role.role().as_str(), "default");
assert!(role.enabled());
assert_eq!(role.priority(), 7);
assert_eq!(role.request_kinds().len(), 1);
assert!(role.request_kinds()[0].is_wildcard());
assert_eq!(role.limits().requests_per_second().map(std::num::NonZeroU32::get), std::option::Option::Some(9));
assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12));
assert_eq!(role.limits().max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(4));
assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650)));
}
#[test]
fn committed_transport_document_maps_default_and_explicit_profiles() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let default = engine.load_resolved_transport_config(std::option::Option::None, &environment);
let mainnet = engine.load_resolved_transport_config(std::option::Option::Some("mainnet_public"), &environment);
assert!(default.is_ok(), "committed default Transport profile should map: {default:?}");
assert!(mainnet.is_ok(), "committed explicit Transport profile should map: {mainnet:?}");
if let std::result::Result::Ok(default) = default {
assert_eq!(default.profile_id(), "devnet_public");
assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet");
assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
}
if let std::result::Result::Ok(mainnet) = mainnet {
assert_eq!(mainnet.profile_id(), "mainnet_public");
assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta");
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com");
}
}
#[test]
fn transport_profile_preserves_global_and_profile_origin() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let profile = engine.load_resolved_profile(&file_id, std::option::Option::None);
assert!(profile.is_ok(), "committed Transport profile should resolve: {profile:?}");
if let std::result::Result::Ok(profile) = profile {
assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
}
}
#[test]
fn secret_transport_url_is_runtime_available_but_safe_projection_is_redacted() {
let engine = fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let canary = "https://secret-provider.invalid/?api-key=transport-secret-canary";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), canary.to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "secret Transport endpoint should map without being rejected: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.effective().sensitivity(), crate::ConfigSensitivity::Secret);
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), canary);
let safe_url = resolved.effective().safe_value().pointer("/endpoints/0/url").and_then(serde_json::Value::as_str);
assert_eq!(safe_url, std::option::Option::Some(crate::REDACTED_CONFIG_VALUE));
let debug = format!("{resolved:?}");
assert!(!debug.contains("transport-secret-canary"));
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
}
#[test]
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
let engine = fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), "https://process.invalid".to_owned());
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), "https://dotenv.invalid".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
assert!(resolved.is_ok(), "secret Transport environment precedence should map: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://process.invalid");
let provenance = resolved.effective().provenance_at("/endpoints/0/url");
assert!(provenance.is_some(), "endpoint URL should retain environment provenance");
if let std::option::Option::Some(provenance) = provenance {
assert_eq!(provenance.len(), 1);
assert_eq!(provenance[0].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
assert_eq!(provenance[0].variable_name(), std::option::Option::Some("KSP_SECRET_TRANSPORT_TEST_URL"));
}
}
#[test]
fn invalid_secret_transport_url_is_effective_config_error_without_secret_leak() {
let engine = fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let canary = "transport-invalid-secret-canary";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), canary.to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
assert!(resolved.is_err(), "invalid secret-derived endpoint URL must fail effective mapping");
if let std::result::Result::Err(error) = resolved {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
let debug = format!("{error:?}");
assert!(!debug.contains(canary));
assert!(error.context().iter().any(|item| -> bool {
return item.key() == "transport_error_code" && item.value() == "invalid_settings";
}));
}
}
fn fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root();
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures");
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture_root, workspace.join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = crate::ConfigFileRegistry::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
}
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root();
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = crate::ConfigFileRegistry::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
}
fn workspace_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
}

263
deltas/0.2.1/pre.006.md Normal file
View File

@@ -0,0 +1,263 @@
<!-- file: deltas/0.2.1/pre.006.md -->
<!-- version: 1 -->
# Delta `v0.2.1-pre.006`
## Base
Base attendue :
```text
v0.2.1-pre.005-fix.001
```
Cette base a été validée localement par le user avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, les tests Transport, Config, Core et `cargo test --workspace`. Les canaries workspace de dépendances et de targets Logging sont également propres.
Version Cargo cible :
```text
0.2.1-pre.6
```
## Objectif
Matérialiser la frontière de configuration standard de `ksp-onchain-transport-lib` sans inverser l'ownership :
```text
Config document / environment -> ksp-config-lib adapter -> HttpTransportSettings
```
Transport ne lit toujours ni Config, ni `.env`, ni `KSP_*` / `KSPB_*`.
## Nouveau document standard Transport
Nouvelles ressources gérées :
```text
cfg.std.transport -> config/std.transport.json
schema.std.transport -> config/schemas/std.transport.schema.json
```
Le document standard possède :
- `format_version = 1` ;
- `retry` au niveau global ;
- `default_profile` autonome ;
- `profiles[]` contenant les endpoints propres au profil ;
- endpoints avec identité/provider/cluster/URL/timeouts/idle-pool ;
- rôles avec capabilities/request kinds, priorité et limites RPS/burst/concurrence/cooldown.
Le profil par défaut committé est `devnet_public`. Le document fournit également `mainnet_public`.
Les URLs publiques utilisent :
```text
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL
```
avec fallback vers les endpoints publics Solana correspondants.
## Schema
`config/schemas/std.transport.schema.json` :
- Draft 2020-12 ;
- `additionalProperties = false` aux frontières structurées ;
- `format_version` fixé à `1` ;
- invariants numériques positifs pour timeouts/limites ;
- `request_kinds` non vide et wildcard `*` exclusif ;
- profils et endpoints non vides ;
- descriptors non vides sans whitespace de bord.
Le schema protège la forme source ; la validation finale des invariants runtime reste possédée par `HttpTransportSettings::validate()`.
## Exemple provider-neutral
`config/examples/std.transport.example.json` démontre un profil `mainnet_mixed` avec :
- endpoint public de fallback ;
- endpoint privé prioritaire ;
- URL privée provenant de `KSP_SECRET_SOLANA_HTTP_URL`.
Aucun credential réel n'est committé.
`.env.example` inventorie les deux URLs publiques et documente l'URL privée optionnelle sous forme commentée.
## Registry Config
Nouveaux contrats :
```text
FILE_ID_STD_TRANSPORT
FILE_ID_SCHEMA_STD_TRANSPORT
DEFAULT_STD_TRANSPORT_FILENAME
DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME
```
`ConfigFileRegistry::defaults()` contient désormais cinq descriptors ordonnés :
```text
cfg.std.logging
cfg.std.transport
schema.composite
schema.std.logging
schema.std.transport
```
Le document Transport référence explicitement son schema.
## Adapter Config -> Transport
Nouvelle surface publique :
```text
ResolvedTransportConfig
ConfigDocumentEngine::load_resolved_transport_config(...)
```
L'adapter :
1. charge et valide `cfg.std.transport` ;
2. sélectionne le `default_profile` ou le profil explicite ;
3. résout les placeholders via `ConfigEnvironment` ;
4. préserve valeur réelle, valeur sûre, sensibilité et provenance JSON Pointer ;
5. décode le contrat effectif Config ;
6. convertit les scalaires `*_ms` en `std::time::Duration` ;
7. construit les newtypes/roles/limits/endpoints Transport ;
8. parse les URLs via `HttpEndpointUrl` ;
9. construit `HttpTransportSettings` ;
10. délègue la validation structurelle finale à Transport.
La dépendance est donc :
```text
ksp-config-lib -> ksp-onchain-transport-lib
```
Le firewall inverse reste inchangé : Transport ne dépend pas de Config.
## Secrets, safe view et provenance
Contrairement au document Logging, le document Transport peut légitimement consommer une valeur `Secret` pour une URL endpoint complète.
`ResolvedTransportConfig` :
- expose `effective()` pour conserver le réel/safe/provenance ;
- expose `settings()` / `into_settings()` au runtime légitime ;
- n'imprime dans `Debug` que la projection `ResolvedConfigJson` sûre ;
- ne copie pas l'URL réelle dans le contexte d'une erreur d'adaptation.
Un échec Transport est projeté vers `ERROR_CODE_EFFECTIVE_CONFIG_INVALID` avec uniquement le domaine/code Transport et les identités non sensibles utiles.
## Tests
Nouveau module unitaire `unit_tests/transport.rs` :
- mapping complet des scalaires Config vers le runtime Transport ;
- validation des profils committés `devnet_public` / `mainnet_public` ;
- provenance top-level `Global` pour `retry` et `Profile` pour `endpoints` ;
- URL `KSP_SECRET_*` disponible au runtime mais redacted dans la safe view et `Debug` ;
- precedence process > `.env` ;
- provenance JSON Pointer de l'URL endpoint ;
- URL secrète invalide -> `ERROR_CODE_EFFECTIVE_CONFIG_INVALID` sans fuite du canary.
Le registry gagne un test dédié au document/schema Transport et la surface publique Config gagne un canary de disponibilité de l'adapter.
La surface Config déclare désormais :
```text
95 tests unitaires
18 tests d'intégration
113 tests au total
```
## Ownership et documentation
`DEP-TRANSPORT-005` précise maintenant explicitement que `ksp-config-lib` peut dépendre des crates Transport pour posséder les adapters Config -> runtime settings, jamais l'inverse.
Le README/USAGE/TODO de Config est synchronisé avec `std.transport`. Le ROADMAP reçoit uniquement la mise à jour synthétique normale de l'état `0.2.1`; il ne journalise pas les détails du delta.
## Dépendances
Nouvelle dépendance interne de `ksp-config-lib` :
```toml
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
```
Aucune nouvelle dépendance externe et aucune nouvelle feature externe.
## Fichiers ajoutés
```text
config/examples/std.transport.example.json
config/schemas/std.transport.schema.json
config/std.transport.json
crates/ksp-config-lib/src/transport.rs
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
crates/ksp-config-lib/unit_tests/transport.rs
deltas/0.2.1/pre.006.md
```
## Fichiers modifiés
```text
.env.example
Cargo.toml
ROADMAP.md
crates/ksp-config-lib/Cargo.toml
crates/ksp-config-lib/README.md
crates/ksp-config-lib/TODO.md
crates/ksp-config-lib/USAGE.md
crates/ksp-config-lib/src/lib.rs
crates/ksp-config-lib/src/registry.rs
crates/ksp-config-lib/tests/ownership.rs
crates/ksp-config-lib/tests/public_api.rs
crates/ksp-config-lib/unit_tests/registry.rs
docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md
docs/rules/RULES_DEPENDENCIES.md
```
## Validation de génération
Effectuée dans le sandbox :
- parsing TOML des manifests modifiés ;
- parsing JSON des nouvelles ressources ;
- validation Draft 2020-12 des deux documents Transport et de la fixture contre le nouveau schema via l'implémentation Python `jsonschema` disponible ;
- contrôle des lignes Rust/TOML modifiées <= 160 colonnes ;
- contrôle des EOF et headers ;
- scan statique des interdits KSP sur le nouveau code de production ;
- contrôle de l'inventaire des variables `KSP_*` dans `.env.example` ;
- contrôle différentiel des fichiers livrés.
Non exécutée dans le sandbox : validation Cargo/Rust, les binaires `cargo`, `rustc` et `rustfmt` n'étant pas disponibles.
Après application :
```bash
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib
cargo test --workspace
cargo tree -p ksp-config-lib
cargo tree -p ksp-config-lib -d
cargo tree -p ksp-config-lib -e features
cargo tree -p ksp-config-lib -e normal
```
Le `cargo tree` Config est requis cette fois : la dépendance interne Config -> Transport modifie réellement le graphe de la crate Config, même sans nouvelle dépendance externe.
## Suite
Tranche suivante prévue :
```text
0.2.1-pre.007
```
Périmètre : completeness/canaries finales, smoke réseau opt-in, audit `cargo tree`, README/USAGE Transport, documentation de clôture, prompt `0.2.2` et préparation de `rel.001`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation
@@ -876,7 +876,7 @@ La release peut devenir stable seulement si :
| `pre.003` | **réalisé** : endpoint client + pool logique + rôles/capabilities + priorité/fairness/fallback + snapshots sûrs |
| `pre.004` | **réalisé** : RPS/burst/concurrence/cooldown + deadline commune + retry/backoff + classification retry/no-resend |
| `pre.005` | **réalisé** : exécution HTTP JSON-RPC + `getHealth`, `getVersion`, `getGenesisHash`, `getBalance` + fixtures déterministes + centralisation des canaries workspace dans Core |
| `pre.006` | `std.transport` schema/document/example + registry Config + adapter Config -> Transport + sensibilité/env tests |
| `pre.006` | **réalisé** : `std.transport` schema/document/example + registry Config + adapter Config -> Transport + sensibilité/provenance/env tests |
| `pre.007` | completeness/canaries, smoke opt-in, `cargo tree`, README/USAGE, docs finales, prompt `0.2.2`, préparation `rel.001` |
Ce découpage est révisable si une tranche dépasse le budget ; la release réduite, contrairement au scope initial, reste raisonnablement clôturable dans la session.
@@ -953,6 +953,23 @@ Restent à `pre.005` : exécution HTTP JSON-RPC réelle et wrappers typés `getH
Restent à `pre.006` : document/schema/exemple `std.transport`, enregistrement Config, adapter Config -> Transport et tests de sensibilité/provenance/env.
### 22.5 État après `0.2.1-pre.006`
`pre.006` matérialise la frontière de configuration standard du Transport sans créer de dépendance inverse :
- `workspace.package.version = 0.2.1-pre.6` ;
- `config/std.transport.json` et `config/schemas/std.transport.schema.json` deviennent des ressources Config gérées, avec `cfg.std.transport` / `schema.std.transport` dans le registry ;
- le document standard possède `retry` en global et des `endpoints` par profil ; `devnet_public` est le profil autonome par défaut et `mainnet_public` reste sélectionnable explicitement ;
- `config/examples/std.transport.example.json` illustre un pool mixte public/privé, y compris une URL complète provenant dun `KSP_SECRET_*` ;
- `.env.example` inventorie les deux overrides publics standard et loverride secret dexemple sans committer de credential réel ;
- `ksp-config-lib` dépend désormais de `ksp-onchain-transport-lib` dans la direction autorisée Config -> Transport, tandis que le firewall Transport -> Config reste inchangé ;
- `ConfigDocumentEngine::load_resolved_transport_config` sélectionne le profil, résout lenvironnement, convertit les scalaires `*_ms` en `Duration`, construit les rôles/limites/endpoints puis délègue la validation finale à `HttpTransportSettings::validate()` ;
- `ResolvedTransportConfig` conserve le profil, la provenance détaillée et la projection safe, et expose les settings runtime réels ; son `Debug` nexpose pas les URLs secrètes ;
- les tests couvrent le document committé, les origines global/profile, la precedence process > `.env`, la provenance JSON Pointer, la redaction `KSP_SECRET_*` et léchec dune URL secrète invalide sans fuite de canary ;
- `DEP-TRANSPORT-005` explicite maintenant que Config peut dépendre des crates Transport pour posséder les adapters, jamais linverse.
Restent à `pre.007` : completeness/canaries finales, smoke réseau opt-in, audit `cargo tree`, README/USAGE, documentation de clôture, prompt `0.2.2` et préparation de `rel.001`.
## 23. Séquence `0.2.x` recalibrée
```text

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
<!-- version: 14 -->
<!-- version: 15 -->
# Règles des dépendances KSP