v0.2.1-pre.006
This commit is contained in:
14
.env.example
14
.env.example
@@ -1,10 +1,22 @@
|
||||
# file: .env.example
|
||||
# version: 2
|
||||
# version: 3
|
||||
|
||||
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
||||
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
||||
KSP_LOGS_DIRECTORY=logs
|
||||
|
||||
# Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json.
|
||||
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
|
||||
|
||||
# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example.
|
||||
# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
|
||||
|
||||
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
|
||||
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
||||
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
|
||||
|
||||
# Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready.
|
||||
KSP_DESK_SPLASH_MINIMUM_MS=1200
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 l’adapter Config -> Transport sont matérialisés, la compliance/smoke/documentation de clôture reste planifiée.
|
||||
|
||||
### Releases fonctionnelles décidées/pressenties
|
||||
|
||||
|
||||
64
config/examples/std.transport.example.json
Normal file
64
config/examples/std.transport.example.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
124
config/schemas/std.transport.schema.json
Normal file
124
config/schemas/std.transport.schema.json
Normal 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
69
config/std.transport.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ;
|
||||
|
||||
@@ -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, §7–8 |
|
||||
| Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 |
|
||||
| Logging effectif | `ResolvedLoggingConfig` | §4 |
|
||||
| Transport effectif | `ResolvedTransportConfig` | §4.1 |
|
||||
| Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §6–7 |
|
||||
| Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous |
|
||||
| Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous |
|
||||
@@ -324,4 +343,3 @@ if let std::result::Result::Err(error) = loaded {
|
||||
```
|
||||
|
||||
Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
316
crates/ksp-config-lib/src/transport.rs
Normal file
316
crates/ksp-config-lib/src/transport.rs
Normal 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;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
40
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
Normal file
40
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
202
crates/ksp-config-lib/unit_tests/transport.rs
Normal file
202
crates/ksp-config-lib/unit_tests/transport.rs
Normal 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
263
deltas/0.2.1/pre.006.md
Normal 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`.
|
||||
@@ -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 d’un `KSP_SECRET_*` ;
|
||||
- `.env.example` inventorie les deux overrides publics standard et l’override secret d’exemple sans committer de credential réel ;
|
||||
- `ksp-config-lib` dépend désormais de `ksp-onchain-transport-lib` dans la direction autorisée Config -> Transport, tandis que le firewall Transport -> Config reste inchangé ;
|
||||
- `ConfigDocumentEngine::load_resolved_transport_config` sélectionne le profil, résout l’environnement, convertit les scalaires `*_ms` en `Duration`, construit les rôles/limites/endpoints puis délègue la validation finale à `HttpTransportSettings::validate()` ;
|
||||
- `ResolvedTransportConfig` conserve le profil, la provenance détaillée et la projection safe, et expose les settings runtime réels ; son `Debug` n’expose pas les URLs secrètes ;
|
||||
- les tests couvrent le document committé, les origines global/profile, la precedence process > `.env`, la provenance JSON Pointer, la redaction `KSP_SECRET_*` et l’échec d’une URL secrète invalide sans fuite de canary ;
|
||||
- `DEP-TRANSPORT-005` explicite maintenant que Config peut dépendre des crates Transport pour posséder les adapters, jamais l’inverse.
|
||||
|
||||
Restent à `pre.007` : completeness/canaries finales, smoke réseau opt-in, audit `cargo tree`, README/USAGE, documentation de clôture, prompt `0.2.2` et préparation de `rel.001`.
|
||||
|
||||
## 23. Séquence `0.2.x` recalibrée
|
||||
|
||||
```text
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
|
||||
<!-- version: 14 -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# Règles des dépendances KSP
|
||||
|
||||
|
||||
Reference in New Issue
Block a user