v0.5.2-pre.006

This commit is contained in:
2026-08-10 23:14:17 +02:00
parent e3acb8ceb1
commit 066d969f5a
51 changed files with 1937 additions and 88 deletions

View File

@@ -1,8 +1,15 @@
<!-- file: ks-pipeline-demo-scenarios/CHANGELOG.md -->
<!-- version: 78 -->
<!-- version: 79 -->
# CHANGELOG — ks-pipeline-demo-scenarios
## `0.5.2-pre.006`
- ajoute l'inspection et l'unlock explicites du wallet natif sélectionné par `profile.wallet.wallet_alias`, avec `WalletPassword` fourni uniquement au runtime ;
- renomme le chemin historique en `load_profile_temporary_wallet` et interdit qu'un alias persistant configuré retombe silencieusement sur le wallet temporaire ;
- refuse les fichiers `.kswallet` dans le workflow CLI Token-2022 fondé sur `solana-keygen` / `spl-token`, exige explicitement un export `SolanaCliJson` et ne révèle plus le chemin complet dun wallet absent ;
- ajoute des tests de sélection persistante, unlock, refus de fallback et rejet du conteneur natif dans le workflow CLI.
## `0.5.1-pre.007`
- supprime la composition par défaut dédiée aux scénarios et charge directement les defaults partagés de `ks-config` ;

View File

@@ -1,5 +1,5 @@
# file: ks-pipeline-demo-scenarios/Cargo.toml
# version: 12
# version: 13
[package]
name = "ks-pipeline-demo-scenarios"
@@ -43,5 +43,8 @@ tokio.workspace = true
tracing.workspace = true
uuid.workspace = true
[dev-dependencies]
tempfile.workspace = true
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: ks-pipeline-demo-scenarios/README.md -->
<!-- version: 30 -->
<!-- version: 31 -->
# ks-pipeline-demo-scenarios
@@ -8,7 +8,8 @@
La crate contient :
- la bibliothèque Rust `ks_pipeline_demo_scenarios` ;
- le binaire ciblé `ks-pipeline-demo-scenarios-cli`.
- le binaire ciblé `ks-pipeline-demo-scenarios-cli` ;
- les adaptateurs de sélection wallet qui respectent `wallet_alias` sans stocker de password dans la configuration.
## Responsabilités

View File

@@ -1,5 +1,5 @@
<!-- file: ks-pipeline-demo-scenarios/USAGE.md -->
<!-- version: 31 -->
<!-- version: 32 -->
# Utilisation de ks-pipeline-demo-scenarios
@@ -7,6 +7,12 @@
La bibliothèque expose des scénarios Devnet contrôlés. Le binaire permet leur exécution hors de lapplication desktop lorsque la commande correspondante est disponible.
## Wallet sélectionné par le profil
`inspect_selected_profile_wallet()` résout l'alias persistant non sensible configuré dans `ks-config`. `unlock_selected_profile_wallet()` exige ensuite un `ks_wallet::WalletPassword` fourni explicitement par le consommateur. Les scénarios historiques qui utilisent encore le wallet temporaire refusent un fallback silencieux lorsqu'un `wallet_alias` persistant est configuré.
Le CLI `prepare-token-2022-fixture` reste un workflow externe Solana CLI : son `--wallet` doit viser un keypair JSON exporté explicitement et refuse un fichier `.kswallet`.
## Initialiser lenvironnement
```rust

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/bin/ks_pipeline_demo_scenarios_cli.rs
// version: 3
// version: 4
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -102,6 +102,13 @@ fn parse_prepare_options(
));
},
};
if wallet_path.extension().and_then(|value| return value.to_str())
== std::option::Option::Some(ks_wallet::KSWALLET_FILE_EXTENSION)
{
return std::result::Result::Err(ks_core::Error::config(
"native .kswallet files cannot be passed to Solana CLI fixture preparation; export SolanaCliJson explicitly first",
));
}
let wallet_dir = match wallet_dir {
std::option::Option::Some(value) => value,
std::option::Option::None => match wallet_path.parent() {
@@ -127,6 +134,19 @@ fn usage_error<T>(message: &str) -> ks_core::Result<T> {
#[cfg(test)]
mod tests {
#[test]
fn prepare_options_reject_native_wallet_path() {
let arguments = vec![
"prepare-token-2022-fixture".to_string(),
"--rpc-url".to_string(),
"https://api.devnet.solana.com".to_string(),
"--wallet".to_string(),
"operator.kswallet".to_string(),
];
let options = super::parse_prepare_options(arguments.as_slice());
assert!(options.is_err());
}
#[test]
fn prepare_options_accept_explicit_values() {
let arguments = vec![

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/lib.rs
// version: 29
// version: 30
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -231,10 +231,14 @@ pub(crate) use self::solana::emit;
pub(crate) use self::solana::ensure_not_cancelled;
/// Executes one bounded System Program transfer on Devnet.
pub use self::solana::execute_devnet_system_transfer;
/// Loads the persistent wallet configured by one execution profile.
pub(crate) use self::solana::load_profile_wallet;
/// Inspects the persistent native wallet alias selected by one execution profile.
pub use self::solana::inspect_selected_profile_wallet;
/// Loads the managed temporary wallet used by legacy Devnet scenarios.
pub(crate) use self::solana::load_profile_temporary_wallet;
/// Formats one failed simulation without discarding runtime diagnostics.
pub(crate) use self::solana::simulation_failure_message;
/// Unlocks the persistent native wallet alias selected by one execution profile.
pub use self::solana::unlock_selected_profile_wallet;
/// Formats safety violations for one denied execution plan.
pub(crate) use self::solana::violation_message;
/// Complete request for one Devnet Associated Token Account execution.

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs
// version: 22
// version: 23
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
@@ -839,7 +839,7 @@ where
),
));
}
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/fixture.rs
// version: 21
// version: 22
//! Native Metaplex Token Metadata fixture preparation for Devnet demos.
@@ -168,7 +168,7 @@ pub async fn prepare_metaplex_create_fixture(
return std::result::Result::Err(error);
}
}
let operator = match crate::load_profile_wallet(profile, workspace_root).await {
let operator = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/devnet_execution.rs
// version: 4
// version: 5
//! Real Devnet simulation and controlled submission for Solana Program Metadata.
@@ -355,7 +355,7 @@ where
),
));
}
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/fixture.rs
// version: 8
// version: 9
//! Native Solana Program Metadata fixture preparation for Devnet journeys.
@@ -143,7 +143,7 @@ pub async fn prepare_solana_program_metadata_fixture(
"Solana Program Metadata fixture preparation requires a Devnet endpoint",
));
}
let operator = match crate::load_profile_wallet(profile, workspace_root).await {
let operator = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/solana.rs
// version: 16
// version: 17
//! Devnet Solana execution orchestration with canonical post-validation.
@@ -305,7 +305,7 @@ where
if let std::result::Result::Err(error) = cancellation_result {
return std::result::Result::Err(error);
}
let wallet = match load_profile_wallet(profile, workspace_root).await {
let wallet = match load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(wallet) => wallet,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -983,17 +983,92 @@ fn validate_devnet_profile(
return std::result::Result::Ok(());
}
pub(crate) async fn load_profile_wallet(
/// Resolves the wallet directory selected by one profile relative to the workspace root.
fn profile_wallet_directory(
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
) -> std::path::PathBuf {
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
if configured.is_absolute() {
return configured;
}
return workspace_root.join(configured);
}
/// Inspects the persistent native wallet alias selected by one profile, when configured.
pub async fn inspect_selected_profile_wallet(
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
) -> ks_core::Result<std::option::Option<ks_wallet::WalletFileHandle>> {
let configured_alias = match profile.wallet.wallet_alias.as_deref() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let alias = match ks_wallet::WalletAlias::parse(configured_alias) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let manager =
match ks_wallet::WalletManager::new(profile_wallet_directory(profile, workspace_root)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let handle = match manager.lookup(&alias).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match handle {
std::option::Option::Some(value) => {
std::result::Result::Ok(std::option::Option::Some(value))
},
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"wallet_selected_alias_not_found",
"selected native wallet alias was not found in the configured wallet directory",
)),
};
}
/// Unlocks the persistent native wallet selected by one profile using an explicit password.
pub async fn unlock_selected_profile_wallet(
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
password: ks_wallet::WalletPassword,
) -> ks_core::Result<ks_wallet::UnlockedWallet> {
let configured_alias = match profile.wallet.wallet_alias.as_deref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_selected_alias_missing",
"profile does not select a persistent native wallet alias",
));
},
};
let alias = match ks_wallet::WalletAlias::parse(configured_alias) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let manager =
match ks_wallet::WalletManager::new(profile_wallet_directory(profile, workspace_root)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return manager.unlock(&alias, password).await;
}
pub(crate) async fn load_profile_temporary_wallet(
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
) -> ks_core::Result<ks_wallet::TemporaryWallet> {
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let directory = if configured.is_absolute() {
configured
} else {
workspace_root.join(configured)
};
let store = match ks_wallet::TemporaryWalletStore::new(directory) {
if profile.wallet.wallet_alias.is_some() {
return std::result::Result::Err(ks_core::Error::new(
"wallet_persistent_selection_requires_explicit_unlock",
"profile selects a persistent native wallet; temporary-wallet fallback is forbidden",
));
}
let store = match ks_wallet::TemporaryWalletStore::new(profile_wallet_directory(
profile,
workspace_root,
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -1223,6 +1298,18 @@ pub(crate) fn emit<O>(
#[cfg(test)]
mod tests {
#[cfg(unix)]
fn make_directory_private(path: &std::path::Path) {
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
let permissions = std::fs::Permissions::from_mode(0o700);
if let std::result::Result::Err(error) = std::fs::set_permissions(path, permissions) {
panic!("temporary wallet directory permissions failed: {error}");
}
}
#[cfg(not(unix))]
fn make_directory_private(_path: &std::path::Path) {}
fn example_devnet_profile() -> ks_config::ProfileConfig {
let config = match ks_config::parse_config_json(include_str!(
"../../test-fixtures/config/resolved.app.config.json"
@@ -1240,6 +1327,71 @@ mod tests {
return ks_lib::MdPubkey("Vote111111111111111111111111111111111111111".to_string());
}
#[tokio::test]
async fn selected_persistent_wallet_requires_explicit_password_path() {
let temporary = match tempfile::tempdir() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("temporary directory failed: {error}"),
};
make_directory_private(temporary.path());
let mut profile = example_devnet_profile();
profile.wallet.wallet_dir = temporary.path().display().to_string();
profile.wallet.wallet_alias = std::option::Option::Some("selected-operator".to_string());
let manager = match ks_wallet::WalletManager::new(temporary.path()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet manager failed: {error}"),
};
let alias = match ks_wallet::WalletAlias::parse("selected-operator") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet alias failed: {error}"),
};
let create_password = match ks_wallet::WalletPassword::new("selected-password".to_string())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet password failed: {error}"),
};
let created = match manager.create(alias, create_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet creation failed: {error}"),
};
let expected_public_key = created.public_key();
created.lock();
let inspected =
match super::inspect_selected_profile_wallet(&profile, temporary.path()).await {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
panic!("selected wallet must be present")
},
std::result::Result::Err(error) => panic!("wallet inspection failed: {error}"),
};
assert_eq!(inspected.alias().as_str(), "selected-operator");
assert_eq!(inspected.public_key(), expected_public_key.as_str());
let unlock_password = match ks_wallet::WalletPassword::new("selected-password".to_string())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet password failed: {error}"),
};
let unlocked = match super::unlock_selected_profile_wallet(
&profile,
temporary.path(),
unlock_password,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet unlock failed: {error}"),
};
assert_eq!(unlocked.public_key(), expected_public_key);
let fallback = super::load_profile_temporary_wallet(&profile, temporary.path()).await;
assert!(fallback.is_err());
}
#[test]
fn profile_without_persistent_alias_has_no_selected_native_wallet() {
let profile = example_devnet_profile();
assert!(profile.wallet.wallet_alias.is_none());
}
#[test]
fn request_and_profile_validation_are_conservative() {
let profile = example_devnet_profile();

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs
// version: 15
// version: 16
//! Devnet ATA execution with stateful and canonical post-validation.
@@ -491,7 +491,7 @@ where
),
));
}
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -1094,7 +1094,7 @@ mod tests {
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|| panic!("workspace root missing"));
let wallet = crate::load_profile_wallet(&profile, workspace_root.as_path())
let wallet = crate::load_profile_temporary_wallet(&profile, workspace_root.as_path())
.await
.unwrap_or_else(|error| panic!("profile wallet failed: {error}"));
let wallet_pubkey = wallet.public_key();

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/memo.rs
// version: 11
// version: 12
//! Devnet SPL Memo v4 execution with canonical post-validation.
@@ -155,7 +155,7 @@ where
),
));
}
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token/execution.rs
// version: 15
// version: 16
//! Devnet classic SPL Token execution with stateful and canonical post-validation.
@@ -479,7 +479,7 @@ where
failed_readiness_message(&readiness),
));
}
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs
// version: 14
// version: 15
//! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts.
@@ -319,7 +319,7 @@ where
"lifecycle account preparation endpoint is not Devnet",
));
}
let payer = match crate::load_profile_wallet(profile, workspace_root).await {
let payer = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -460,7 +460,7 @@ where
"the destructive lifecycle requires submit=true and explicit operator confirmation",
));
}
let profile_wallet = match crate::load_profile_wallet(profile, workspace_root).await {
let profile_wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/devnet_execution.rs
// version: 9
// version: 10
//! Devnet Token-2022 execution with stateful and canonical post-validation.
@@ -469,7 +469,7 @@ where
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/fixture.rs
// version: 5
// version: 6
//! Idempotent Token-2022 public scenario fixture preparation.
@@ -181,16 +181,22 @@ pub async fn prepare_token_2022_fixture(
}
fn validate_options(options: &crate::Token2022FixturePreparationOptions) -> ks_core::Result<()> {
if options.wallet_path.extension().and_then(|value| return value.to_str())
== std::option::Option::Some(ks_wallet::KSWALLET_FILE_EXTENSION)
{
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 CLI fixture preparation requires an explicit Solana CLI JSON export; native .kswallet files are never passed to external CLI tools",
));
}
if options.rpc_url.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 fixture RPC URL must not be empty",
));
}
if !options.wallet_path.is_file() {
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 fixture wallet does not exist: {}",
options.wallet_path.display()
)));
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 fixture wallet does not exist",
));
}
if options.decimals > 18 {
return std::result::Result::Err(ks_core::Error::config(
@@ -448,6 +454,18 @@ mod tests {
assert!(fixture.contains("export KS_PUBLIC_DEVNET_WALLET_ADDRESS=authority\n"));
}
#[test]
fn options_reject_native_wallet_container_for_legacy_cli_workflow() {
let options = super::Token2022FixturePreparationOptions {
rpc_url: "https://api.devnet.solana.com".to_string(),
wallet_path: std::path::PathBuf::from("operator.kswallet"),
wallet_dir: std::path::PathBuf::from("wallets"),
decimals: 9,
};
let error = super::validate_options(&options);
assert!(error.is_err());
}
#[test]
fn options_reject_excessive_decimals() {
let options = super::Token2022FixturePreparationOptions {

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/fixture.rs
// version: 5
// version: 6
//! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign.
@@ -96,7 +96,7 @@ pub async fn prepare_token_2022_metadata_fixture(
"Token-2022 metadata fixture requires enabled Devnet submission and explicit operator confirmation",
));
}
let operator = match crate::load_profile_wallet(profile, workspace_root).await {
let operator = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};