main.rs (25391B)
1 #![forbid(unsafe_code)] 2 3 mod cli; 4 mod ops; 5 mod out; 6 mod registry; 7 mod runtime; 8 mod view; 9 10 use std::io::Write; 11 use std::process::ExitCode; 12 use std::sync::atomic::{AtomicU64, Ordering}; 13 use std::time::{SystemTime, UNIX_EPOCH}; 14 15 use clap::Parser; 16 use serde_json::Value; 17 18 use crate::cli::input::runtime_invocation_args_from_target; 19 use crate::cli::{TargetCliArgs, TargetOutputFormat}; 20 use crate::ops::exec::{ 21 BasketOperationService, CoreOperationService, FarmOperationService, ListingOperationService, 22 MarketOperationService, OrderOperationService, RuntimeOperationService, 23 ValidationOperationService, 24 }; 25 use crate::ops::{ 26 OperationAdapter, OperationAdapterError, OperationNetworkMode, OperationOutputFormat, 27 OperationRequest, OperationRequestPayload, OperationResultPayload, OperationService, 28 TargetOperationRequest, 29 }; 30 use crate::out::envelope::OutputEnvelope; 31 use crate::registry::{NetworkRequirement, network_requirement, requires_local_signer_mode}; 32 use crate::runtime::config::{RuntimeConfig, SignerBackend}; 33 use crate::runtime::logging::initialize_logging; 34 35 static REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); 36 37 fn main() -> ExitCode { 38 match run() { 39 Ok(exit_code) => exit_code, 40 Err(error) => { 41 let _ = writeln!(std::io::stderr(), "{error}"); 42 error.exit_code() 43 } 44 } 45 } 46 47 fn run() -> Result<ExitCode, runtime::RuntimeError> { 48 debug_assert!(registry::registry_linkage_is_valid()); 49 debug_assert!(ops::adapter_registry_linkage_is_valid()); 50 let args = TargetCliArgs::parse(); 51 let request = 52 TargetOperationRequest::from_target_args(&args).map_err(operation_config_error)?; 53 if let Err(error) = validate_pre_runtime_request_contract(&request) { 54 let envelope = failure_envelope(&request, error); 55 render_envelope(&envelope, args.format)?; 56 return Ok(envelope_exit_code(&envelope)); 57 } 58 let config = RuntimeConfig::from_system(&runtime_invocation_args_from_target(&args))?; 59 let logging = initialize_logging(&config.logging)?; 60 let envelope = match validate_request_contract(&request, &config) { 61 Ok(()) => execute_request(request, &config, &logging), 62 Err(error) => failure_envelope(&request, error), 63 }; 64 render_envelope(&envelope, args.format)?; 65 Ok(envelope_exit_code(&envelope)) 66 } 67 68 fn execute_request( 69 request: TargetOperationRequest, 70 config: &RuntimeConfig, 71 logging: &runtime::logging::LoggingState, 72 ) -> OutputEnvelope { 73 match request { 74 TargetOperationRequest::WorkspaceInit(request) => { 75 execute_with(CoreOperationService::new(config, logging), request) 76 } 77 TargetOperationRequest::WorkspaceGet(request) => { 78 execute_with(CoreOperationService::new(config, logging), request) 79 } 80 TargetOperationRequest::HealthStatusGet(request) => { 81 execute_with(CoreOperationService::new(config, logging), request) 82 } 83 TargetOperationRequest::HealthCheckRun(request) => { 84 execute_with(CoreOperationService::new(config, logging), request) 85 } 86 TargetOperationRequest::ConfigGet(request) => { 87 execute_with(CoreOperationService::new(config, logging), request) 88 } 89 TargetOperationRequest::AccountCreate(request) => { 90 execute_with(CoreOperationService::new(config, logging), request) 91 } 92 TargetOperationRequest::AccountImport(request) => { 93 execute_with(CoreOperationService::new(config, logging), request) 94 } 95 TargetOperationRequest::AccountAttachSecret(request) => { 96 execute_with(CoreOperationService::new(config, logging), request) 97 } 98 TargetOperationRequest::AccountGet(request) => { 99 execute_with(CoreOperationService::new(config, logging), request) 100 } 101 TargetOperationRequest::AccountList(request) => { 102 execute_with(CoreOperationService::new(config, logging), request) 103 } 104 TargetOperationRequest::AccountRemove(request) => { 105 execute_with(CoreOperationService::new(config, logging), request) 106 } 107 TargetOperationRequest::AccountSelectionGet(request) => { 108 execute_with(CoreOperationService::new(config, logging), request) 109 } 110 TargetOperationRequest::AccountSelectionUpdate(request) => { 111 execute_with(CoreOperationService::new(config, logging), request) 112 } 113 TargetOperationRequest::AccountSelectionClear(request) => { 114 execute_with(CoreOperationService::new(config, logging), request) 115 } 116 TargetOperationRequest::StoreInit(request) => { 117 execute_with(CoreOperationService::new(config, logging), request) 118 } 119 TargetOperationRequest::StoreStatusGet(request) => { 120 execute_with(CoreOperationService::new(config, logging), request) 121 } 122 TargetOperationRequest::StoreExport(request) => { 123 execute_with(CoreOperationService::new(config, logging), request) 124 } 125 TargetOperationRequest::StoreBackupCreate(request) => { 126 execute_with(CoreOperationService::new(config, logging), request) 127 } 128 TargetOperationRequest::StoreBackupRestore(request) => { 129 execute_with(CoreOperationService::new(config, logging), request) 130 } 131 TargetOperationRequest::SignerStatusGet(request) => { 132 execute_with(RuntimeOperationService::new(config), request) 133 } 134 TargetOperationRequest::RelayList(request) => { 135 execute_with(RuntimeOperationService::new(config), request) 136 } 137 TargetOperationRequest::SyncStatusGet(request) => { 138 execute_with(RuntimeOperationService::new(config), request) 139 } 140 TargetOperationRequest::SyncPull(request) => { 141 execute_with(RuntimeOperationService::new(config), request) 142 } 143 TargetOperationRequest::SyncPush(request) => { 144 execute_with(RuntimeOperationService::new(config), request) 145 } 146 TargetOperationRequest::SyncWatch(request) => { 147 execute_with(RuntimeOperationService::new(config), request) 148 } 149 TargetOperationRequest::FarmCreate(request) => { 150 execute_with(FarmOperationService::new(config), request) 151 } 152 TargetOperationRequest::FarmGet(request) => { 153 execute_with(FarmOperationService::new(config), request) 154 } 155 TargetOperationRequest::FarmRebind(request) => { 156 execute_with(FarmOperationService::new(config), request) 157 } 158 TargetOperationRequest::FarmProfileUpdate(request) => { 159 execute_with(FarmOperationService::new(config), request) 160 } 161 TargetOperationRequest::FarmLocationUpdate(request) => { 162 execute_with(FarmOperationService::new(config), request) 163 } 164 TargetOperationRequest::FarmFulfillmentUpdate(request) => { 165 execute_with(FarmOperationService::new(config), request) 166 } 167 TargetOperationRequest::FarmReadinessCheck(request) => { 168 execute_with(FarmOperationService::new(config), request) 169 } 170 TargetOperationRequest::FarmPublish(request) => { 171 execute_with(FarmOperationService::new(config), request) 172 } 173 TargetOperationRequest::ListingCreate(request) => { 174 execute_with(ListingOperationService::new(config), request) 175 } 176 TargetOperationRequest::ListingGet(request) => { 177 execute_with(ListingOperationService::new(config), request) 178 } 179 TargetOperationRequest::ListingList(request) => { 180 execute_with(ListingOperationService::new(config), request) 181 } 182 TargetOperationRequest::ListingAppList(request) => { 183 execute_with(ListingOperationService::new(config), request) 184 } 185 TargetOperationRequest::ListingAppExport(request) => { 186 execute_with(ListingOperationService::new(config), request) 187 } 188 TargetOperationRequest::ListingUpdate(request) => { 189 execute_with(ListingOperationService::new(config), request) 190 } 191 TargetOperationRequest::ListingValidate(request) => { 192 execute_with(ListingOperationService::new(config), request) 193 } 194 TargetOperationRequest::ListingRebind(request) => { 195 execute_with(ListingOperationService::new(config), request) 196 } 197 TargetOperationRequest::ListingPublish(request) => { 198 execute_with(ListingOperationService::new(config), request) 199 } 200 TargetOperationRequest::ListingArchive(request) => { 201 execute_with(ListingOperationService::new(config), request) 202 } 203 TargetOperationRequest::MarketRefresh(request) => { 204 execute_with(MarketOperationService::new(config), request) 205 } 206 TargetOperationRequest::MarketProductSearch(request) => { 207 execute_with(MarketOperationService::new(config), request) 208 } 209 TargetOperationRequest::MarketListingGet(request) => { 210 execute_with(MarketOperationService::new(config), request) 211 } 212 TargetOperationRequest::BasketCreate(request) => { 213 execute_with(BasketOperationService::new(config), request) 214 } 215 TargetOperationRequest::BasketGet(request) => { 216 execute_with(BasketOperationService::new(config), request) 217 } 218 TargetOperationRequest::BasketList(request) => { 219 execute_with(BasketOperationService::new(config), request) 220 } 221 TargetOperationRequest::BasketItemAdd(request) => { 222 execute_with(BasketOperationService::new(config), request) 223 } 224 TargetOperationRequest::BasketItemUpdate(request) => { 225 execute_with(BasketOperationService::new(config), request) 226 } 227 TargetOperationRequest::BasketItemRemove(request) => { 228 execute_with(BasketOperationService::new(config), request) 229 } 230 TargetOperationRequest::BasketAdjustmentAdd(request) => { 231 execute_with(BasketOperationService::new(config), request) 232 } 233 TargetOperationRequest::BasketAdjustmentRemove(request) => { 234 execute_with(BasketOperationService::new(config), request) 235 } 236 TargetOperationRequest::BasketValidate(request) => { 237 execute_with(BasketOperationService::new(config), request) 238 } 239 TargetOperationRequest::BasketQuoteCreate(request) => { 240 execute_with(BasketOperationService::new(config), request) 241 } 242 TargetOperationRequest::OrderSubmit(request) => { 243 execute_with(OrderOperationService::new(config), request) 244 } 245 TargetOperationRequest::OrderGet(request) => { 246 execute_with(OrderOperationService::new(config), request) 247 } 248 TargetOperationRequest::OrderList(request) => { 249 execute_with(OrderOperationService::new(config), request) 250 } 251 TargetOperationRequest::OrderAppList(request) => { 252 execute_with(OrderOperationService::new(config), request) 253 } 254 TargetOperationRequest::OrderAppExport(request) => { 255 execute_with(OrderOperationService::new(config), request) 256 } 257 TargetOperationRequest::OrderRebind(request) => { 258 execute_with(OrderOperationService::new(config), request) 259 } 260 TargetOperationRequest::OrderAccept(request) => { 261 execute_with(OrderOperationService::new(config), request) 262 } 263 TargetOperationRequest::OrderDecline(request) => { 264 execute_with(OrderOperationService::new(config), request) 265 } 266 TargetOperationRequest::OrderCancel(request) => { 267 execute_with(OrderOperationService::new(config), request) 268 } 269 TargetOperationRequest::OrderRevisionPropose(request) => { 270 execute_with(OrderOperationService::new(config), request) 271 } 272 TargetOperationRequest::OrderRevisionAccept(request) => { 273 execute_with(OrderOperationService::new(config), request) 274 } 275 TargetOperationRequest::OrderRevisionDecline(request) => { 276 execute_with(OrderOperationService::new(config), request) 277 } 278 TargetOperationRequest::OrderStatusGet(request) => { 279 execute_with(OrderOperationService::new(config), request) 280 } 281 TargetOperationRequest::OrderEventList(request) => { 282 execute_with(OrderOperationService::new(config), request) 283 } 284 TargetOperationRequest::OrderEventWatch(request) => { 285 execute_with(OrderOperationService::new(config), request) 286 } 287 TargetOperationRequest::ValidationReceiptGet(request) => { 288 execute_with(ValidationOperationService::new(config), request) 289 } 290 TargetOperationRequest::ValidationReceiptList(request) => { 291 execute_with(ValidationOperationService::new(config), request) 292 } 293 TargetOperationRequest::ValidationReceiptVerify(request) => { 294 execute_with(ValidationOperationService::new(config), request) 295 } 296 } 297 } 298 299 fn execute_with<S, P>(service: S, request: OperationRequest<P>) -> OutputEnvelope 300 where 301 S: OperationService<P>, 302 P: OperationRequestPayload, 303 S::Result: OperationResultPayload, 304 { 305 let operation_id = request.operation_id().to_owned(); 306 let envelope_context = request 307 .context 308 .envelope_context(next_request_id(&operation_id)); 309 match OperationAdapter::new(service) 310 .execute(request) 311 .and_then(|result| result.to_envelope(envelope_context.clone())) 312 { 313 Ok(envelope) => envelope, 314 Err(error) => { 315 OutputEnvelope::failure(operation_id, error.to_output_error(), envelope_context) 316 } 317 } 318 } 319 320 fn validate_request_contract( 321 request: &TargetOperationRequest, 322 config: &RuntimeConfig, 323 ) -> Result<(), OperationAdapterError> { 324 validate_pre_runtime_request_contract(request)?; 325 validate_publish_transport_contract(request, config)?; 326 validate_signer_mode_contract(request, config)?; 327 validate_network_contract(request, config)?; 328 Ok(()) 329 } 330 331 fn validate_pre_runtime_request_contract( 332 request: &TargetOperationRequest, 333 ) -> Result<(), OperationAdapterError> { 334 let spec = request.spec(); 335 if matches!( 336 request.context().output_format, 337 OperationOutputFormat::Ndjson 338 ) && !spec.supports_ndjson 339 { 340 return Err(OperationAdapterError::InvalidInput { 341 operation_id: spec.operation_id.to_owned(), 342 message: format!("`{}` does not support --format ndjson", spec.cli_path), 343 }); 344 } 345 if request.context().dry_run && !spec.supports_dry_run { 346 return Err(OperationAdapterError::InvalidInput { 347 operation_id: spec.operation_id.to_owned(), 348 message: format!("`{}` does not support --dry-run", spec.cli_path), 349 }); 350 } 351 Ok(()) 352 } 353 354 fn validate_signer_mode_contract( 355 request: &TargetOperationRequest, 356 config: &RuntimeConfig, 357 ) -> Result<(), OperationAdapterError> { 358 let spec = request.spec(); 359 if matches!(config.signer.backend, SignerBackend::Myc) 360 && requires_local_signer_mode_for_publish_transport(spec.operation_id, config) 361 { 362 return Err(OperationAdapterError::SignerModeDeferred { 363 operation_id: spec.operation_id.to_owned(), 364 message: format!( 365 "`{}` cannot run with signer mode `myc`; use signer mode `local`", 366 spec.cli_path 367 ), 368 }); 369 } 370 Ok(()) 371 } 372 373 fn validate_network_contract( 374 request: &TargetOperationRequest, 375 config: &RuntimeConfig, 376 ) -> Result<(), OperationAdapterError> { 377 let spec = request.spec(); 378 let requirement = network_requirement(spec.operation_id); 379 match request.context().network_mode { 380 OperationNetworkMode::Default => Ok(()), 381 OperationNetworkMode::Offline => { 382 if allows_offline_local_mutation(spec.operation_id) { 383 return Ok(()); 384 } 385 if let NetworkRequirement::External { 386 dry_run_requires_network, 387 } = requirement 388 && (!request.context().dry_run || dry_run_requires_network) 389 { 390 return Err(OperationAdapterError::OfflineForbidden { 391 operation_id: spec.operation_id.to_owned(), 392 message: format!( 393 "`{}` requires relay, provider, or workflow network access", 394 spec.cli_path 395 ), 396 }); 397 } 398 Ok(()) 399 } 400 OperationNetworkMode::Online => { 401 if let NetworkRequirement::External { 402 dry_run_requires_network, 403 } = requirement 404 && (!request.context().dry_run || dry_run_requires_network) 405 && requires_pre_runtime_relay_target(spec.operation_id) 406 && config.relay.urls.is_empty() 407 { 408 return Err(OperationAdapterError::NetworkUnavailable { 409 operation_id: spec.operation_id.to_owned(), 410 message: format!( 411 "`{}` requires at least one configured relay for online execution", 412 spec.cli_path 413 ), 414 }); 415 } 416 Ok(()) 417 } 418 } 419 } 420 421 fn requires_local_signer_mode_for_publish_transport( 422 operation_id: &str, 423 config: &RuntimeConfig, 424 ) -> bool { 425 let _ = config; 426 requires_local_signer_mode(operation_id) 427 } 428 429 fn requires_pre_runtime_relay_target(operation_id: &str) -> bool { 430 !is_publish_transport_routed_operation(operation_id) 431 } 432 433 fn allows_offline_local_mutation(operation_id: &str) -> bool { 434 matches!(operation_id, "listing.publish") 435 } 436 437 fn validate_publish_transport_contract( 438 request: &TargetOperationRequest, 439 config: &RuntimeConfig, 440 ) -> Result<(), OperationAdapterError> { 441 let _ = request; 442 let _ = config; 443 Ok(()) 444 } 445 446 fn is_publish_transport_routed_operation(operation_id: &str) -> bool { 447 matches!( 448 operation_id, 449 "farm.publish" | "listing.publish" | "listing.update" | "listing.archive" 450 ) 451 } 452 453 fn failure_envelope( 454 request: &TargetOperationRequest, 455 error: OperationAdapterError, 456 ) -> OutputEnvelope { 457 OutputEnvelope::failure( 458 request.operation_id(), 459 error.to_output_error(), 460 request 461 .context() 462 .envelope_context(next_request_id(request.operation_id())), 463 ) 464 } 465 466 fn next_request_id(operation_id: &str) -> String { 467 let sequence = REQUEST_SEQUENCE.fetch_add(1, Ordering::Relaxed); 468 let timestamp = SystemTime::now() 469 .duration_since(UNIX_EPOCH) 470 .map(|duration| duration.as_nanos()) 471 .unwrap_or_default(); 472 format!( 473 "req_{}_{}_{}_{}", 474 operation_id.replace('.', "_"), 475 std::process::id(), 476 timestamp, 477 sequence 478 ) 479 } 480 481 fn render_envelope( 482 envelope: &OutputEnvelope, 483 format: TargetOutputFormat, 484 ) -> Result<(), runtime::RuntimeError> { 485 let stdout = std::io::stdout(); 486 let mut handle = stdout.lock(); 487 match format { 488 TargetOutputFormat::Human => { 489 render_human_envelope(&mut handle, envelope)?; 490 } 491 TargetOutputFormat::Json => { 492 serde_json::to_writer_pretty(&mut handle, envelope)?; 493 } 494 TargetOutputFormat::Ndjson => { 495 for frame in envelope.to_ndjson_frames() { 496 serde_json::to_writer(&mut handle, &frame)?; 497 writeln!(handle)?; 498 } 499 return Ok(()); 500 } 501 } 502 writeln!(handle)?; 503 Ok(()) 504 } 505 506 fn render_human_envelope( 507 handle: &mut impl Write, 508 envelope: &OutputEnvelope, 509 ) -> Result<(), runtime::RuntimeError> { 510 writeln!( 511 handle, 512 "{}: {}", 513 envelope.operation_id, 514 human_envelope_status(envelope) 515 )?; 516 writeln!(handle, "request_id: {}", envelope.request_id)?; 517 if let Some(error) = envelope.errors.first() { 518 writeln!(handle, "error: {}", error.code)?; 519 writeln!(handle, "message: {}", error.message)?; 520 } 521 let display = human_display_source(envelope); 522 if !envelope.errors.is_empty() 523 && let Some(state) = human_state(display) 524 { 525 writeln!(handle, "state: {state}")?; 526 } 527 if let Some(mode) = human_publish_transport(display) { 528 writeln!(handle, "publish_transport: {mode}")?; 529 } 530 if let Some(state) = human_publish_state(display) { 531 writeln!(handle, "publish_state: {state}")?; 532 } 533 if let Some(state) = human_proof_state(display) { 534 writeln!(handle, "proof_state: {state}")?; 535 } 536 if let Some(system) = human_proof_system(display) { 537 writeln!(handle, "proof_system: {system}")?; 538 } 539 if let Some(verified) = human_cryptographic_proof_verified(display) { 540 writeln!(handle, "cryptographic_proof_verified: {verified}")?; 541 } 542 if let Some(reason) = human_reason(display) { 543 writeln!(handle, "reason: {reason}")?; 544 } 545 let actions = human_actions(envelope, display); 546 if !actions.is_empty() { 547 writeln!(handle, "next:")?; 548 for action in actions { 549 writeln!(handle, "- {action}")?; 550 } 551 } 552 Ok(()) 553 } 554 555 fn human_display_source(envelope: &OutputEnvelope) -> &Value { 556 if !envelope.result.is_null() { 557 return &envelope.result; 558 } 559 envelope 560 .errors 561 .first() 562 .and_then(|error| error.detail.as_ref()) 563 .unwrap_or(&envelope.result) 564 } 565 566 fn human_state(result: &Value) -> Option<&str> { 567 human_string_path(result, &["state"]) 568 } 569 570 fn human_publish_transport(result: &Value) -> Option<&str> { 571 human_string_path(result, &["publish", "mode"]) 572 .or_else(|| human_string_path(result, &["checks", "publish", "mode"])) 573 .or_else(|| human_string_path(result, &["publish_transport"])) 574 } 575 576 fn human_publish_state(result: &Value) -> Option<&str> { 577 human_string_path(result, &["publish", "state"]) 578 .or_else(|| human_string_path(result, &["checks", "publish", "state"])) 579 .or_else(|| human_string_path(result, &["publish_state"])) 580 } 581 582 fn human_proof_state(result: &Value) -> Option<&str> { 583 human_string_path(result, &["proof_verification", "state"]) 584 .or_else(|| human_string_path(result, &["proof_verification_state"])) 585 } 586 587 fn human_proof_system(result: &Value) -> Option<&str> { 588 human_string_path(result, &["proof_verification", "proof_system"]) 589 .or_else(|| human_string_path(result, &["receipt", "proof", "system"])) 590 .or_else(|| human_string_path(result, &["proof_system"])) 591 } 592 593 fn human_cryptographic_proof_verified(result: &Value) -> Option<bool> { 594 human_bool_path( 595 result, 596 &["proof_verification", "cryptographic_proof_verified"], 597 ) 598 } 599 600 fn human_reason(result: &Value) -> Option<&str> { 601 human_string_path(result, &["reason"]) 602 .or_else(|| human_string_path(result, &["publish", "reason"])) 603 .or_else(|| human_string_path(result, &["checks", "publish", "reason"])) 604 .or_else(|| human_string_path(result, &["store", "reason"])) 605 .or_else(|| human_string_path(result, &["checks", "store", "reason"])) 606 .or_else(|| human_string_path(result, &["checks", "account", "reason"])) 607 } 608 609 fn human_actions(envelope: &OutputEnvelope, display: &Value) -> Vec<String> { 610 let mut actions = display 611 .get("actions") 612 .and_then(Value::as_array) 613 .into_iter() 614 .flatten() 615 .filter_map(Value::as_str) 616 .map(str::to_owned) 617 .collect::<Vec<_>>(); 618 if actions.is_empty() { 619 actions = envelope 620 .next_actions 621 .iter() 622 .map(|action| { 623 action 624 .command 625 .clone() 626 .or_else(|| action.description.clone()) 627 .unwrap_or_else(|| action.label.clone()) 628 }) 629 .collect(); 630 } 631 actions.into_iter().fold(Vec::new(), |mut unique, action| { 632 if !unique.contains(&action) { 633 unique.push(action); 634 } 635 unique 636 }) 637 } 638 639 fn human_string_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> { 640 let mut current = value; 641 for segment in path { 642 current = current.get(*segment)?; 643 } 644 current.as_str().filter(|value| !value.trim().is_empty()) 645 } 646 647 fn human_bool_path(value: &Value, path: &[&str]) -> Option<bool> { 648 let mut current = value; 649 for segment in path { 650 current = current.get(*segment)?; 651 } 652 current.as_bool() 653 } 654 655 fn human_envelope_status(envelope: &OutputEnvelope) -> &str { 656 if !envelope.errors.is_empty() { 657 return "error"; 658 } 659 if let Some(state) = envelope 660 .result 661 .get("state") 662 .and_then(|value| value.as_str()) 663 { 664 return state; 665 } 666 if envelope.dry_run { 667 return "dry_run"; 668 } 669 "ok" 670 } 671 672 fn envelope_exit_code(envelope: &OutputEnvelope) -> ExitCode { 673 envelope 674 .errors 675 .first() 676 .map(|error| ExitCode::from(error.exit_code)) 677 .unwrap_or_else(|| ExitCode::from(0)) 678 } 679 680 fn operation_config_error(error: OperationAdapterError) -> runtime::RuntimeError { 681 runtime::RuntimeError::Config(error.to_string()) 682 }