@wlfi-agent/cli 1.4.17 → 1.4.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/Cargo.lock +5 -0
  2. package/README.md +61 -28
  3. package/crates/vault-cli-admin/src/io_utils.rs +149 -1
  4. package/crates/vault-cli-admin/src/main.rs +639 -16
  5. package/crates/vault-cli-admin/src/shared_config.rs +18 -18
  6. package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
  7. package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
  8. package/crates/vault-cli-admin/src/tui.rs +1205 -120
  9. package/crates/vault-cli-agent/Cargo.toml +1 -0
  10. package/crates/vault-cli-agent/src/io_utils.rs +163 -2
  11. package/crates/vault-cli-agent/src/main.rs +648 -32
  12. package/crates/vault-cli-daemon/Cargo.toml +4 -0
  13. package/crates/vault-cli-daemon/src/main.rs +617 -67
  14. package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
  15. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
  16. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
  17. package/crates/vault-daemon/src/persistence.rs +637 -100
  18. package/crates/vault-daemon/src/tests.rs +1013 -3
  19. package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
  20. package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
  21. package/crates/vault-domain/src/nonce.rs +4 -0
  22. package/crates/vault-domain/src/tests.rs +616 -0
  23. package/crates/vault-policy/src/engine.rs +55 -32
  24. package/crates/vault-policy/src/tests.rs +195 -0
  25. package/crates/vault-sdk-agent/src/lib.rs +415 -22
  26. package/crates/vault-signer/Cargo.toml +3 -0
  27. package/crates/vault-signer/src/lib.rs +266 -40
  28. package/crates/vault-transport-unix/src/lib.rs +653 -5
  29. package/crates/vault-transport-xpc/src/tests.rs +531 -3
  30. package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
  31. package/dist/cli.cjs +756 -194
  32. package/dist/cli.cjs.map +1 -1
  33. package/package.json +5 -2
  34. package/packages/cache/.turbo/turbo-build.log +20 -20
  35. package/packages/cache/coverage/clover.xml +529 -394
  36. package/packages/cache/coverage/coverage-final.json +2 -2
  37. package/packages/cache/coverage/index.html +21 -21
  38. package/packages/cache/coverage/src/client/index.html +1 -1
  39. package/packages/cache/coverage/src/client/index.ts.html +1 -1
  40. package/packages/cache/coverage/src/errors/index.html +1 -1
  41. package/packages/cache/coverage/src/errors/index.ts.html +12 -12
  42. package/packages/cache/coverage/src/index.html +1 -1
  43. package/packages/cache/coverage/src/index.ts.html +1 -1
  44. package/packages/cache/coverage/src/service/index.html +21 -21
  45. package/packages/cache/coverage/src/service/index.ts.html +769 -313
  46. package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
  47. package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
  48. package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
  49. package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
  50. package/packages/cache/dist/index.cjs +2 -2
  51. package/packages/cache/dist/index.js +1 -1
  52. package/packages/cache/dist/service/index.cjs +2 -2
  53. package/packages/cache/dist/service/index.js +1 -1
  54. package/packages/cache/node_modules/.bin/tsc +2 -2
  55. package/packages/cache/node_modules/.bin/tsserver +2 -2
  56. package/packages/cache/node_modules/.bin/tsup +2 -2
  57. package/packages/cache/node_modules/.bin/tsup-node +2 -2
  58. package/packages/cache/node_modules/.bin/vitest +4 -4
  59. package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  60. package/packages/cache/src/service/index.test.ts +165 -19
  61. package/packages/cache/src/service/index.ts +38 -1
  62. package/packages/config/.turbo/turbo-build.log +4 -4
  63. package/packages/config/dist/index.cjs +0 -17
  64. package/packages/config/dist/index.cjs.map +1 -1
  65. package/packages/config/src/index.ts +0 -17
  66. package/packages/rpc/.turbo/turbo-build.log +11 -11
  67. package/packages/rpc/dist/index.cjs +0 -17
  68. package/packages/rpc/dist/index.cjs.map +1 -1
  69. package/packages/rpc/src/index.js +1 -0
  70. package/packages/ui/node_modules/.bin/tsc +2 -2
  71. package/packages/ui/node_modules/.bin/tsserver +2 -2
  72. package/packages/ui/node_modules/.bin/tsup +2 -2
  73. package/packages/ui/node_modules/.bin/tsup-node +2 -2
  74. package/scripts/install-cli-launcher.mjs +37 -0
  75. package/scripts/install-rust-binaries.mjs +47 -0
  76. package/scripts/run-tests-isolated.mjs +210 -0
  77. package/src/cli.ts +310 -50
  78. package/src/lib/admin-reset.ts +101 -33
  79. package/src/lib/admin-setup.ts +285 -55
  80. package/src/lib/agent-auth-migrate.ts +5 -1
  81. package/src/lib/asset-broadcast.ts +15 -4
  82. package/src/lib/config-amounts.ts +6 -4
  83. package/src/lib/hidden-tty-prompt.js +1 -0
  84. package/src/lib/hidden-tty-prompt.ts +105 -0
  85. package/src/lib/keychain.ts +1 -0
  86. package/src/lib/local-admin-access.ts +4 -29
  87. package/src/lib/rust.ts +129 -33
  88. package/src/lib/signed-tx.ts +1 -0
  89. package/src/lib/sudo.ts +15 -5
  90. package/src/lib/wallet-profile.ts +3 -0
  91. package/src/lib/wallet-setup.ts +52 -0
  92. package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
  93. package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
@@ -157,6 +157,20 @@ struct BootstrapCommandArgs {
157
157
  help = "Deprecated compatibility flag; bootstrap-created policies are always attached"
158
158
  )]
159
159
  attach_bootstrap_policies: bool,
160
+ #[arg(
161
+ long,
162
+ value_name = "UUID",
163
+ requires = "existing_vault_public_key",
164
+ help = "Reuse an existing vault key id instead of generating a fresh wallet"
165
+ )]
166
+ existing_vault_key_id: Option<Uuid>,
167
+ #[arg(
168
+ long,
169
+ value_name = "HEX",
170
+ requires = "existing_vault_key_id",
171
+ help = "Reuse an existing vault public key instead of generating a fresh wallet"
172
+ )]
173
+ existing_vault_public_key: Option<String>,
160
174
  #[arg(long, default_value_t = false)]
161
175
  print_agent_auth_token: bool,
162
176
  #[arg(
@@ -650,6 +664,8 @@ async fn main() -> Result<()> {
650
664
  recipient,
651
665
  attach_policy_id,
652
666
  attach_bootstrap_policies: _attach_bootstrap_policies,
667
+ existing_vault_key_id,
668
+ existing_vault_public_key,
653
669
  print_agent_auth_token,
654
670
  print_vault_private_key,
655
671
  } = *args;
@@ -661,6 +677,8 @@ async fn main() -> Result<()> {
661
677
  false,
662
678
  )?;
663
679
  params.attach_policy_ids = attach_policy_id;
680
+ params.existing_vault_key_id = existing_vault_key_id;
681
+ params.existing_vault_public_key = existing_vault_public_key;
664
682
  params
665
683
  } else {
666
684
  BootstrapParams {
@@ -683,8 +701,8 @@ async fn main() -> Result<()> {
683
701
  attach_policy_ids: attach_policy_id,
684
702
  print_agent_auth_token,
685
703
  print_vault_private_key,
686
- existing_vault_key_id: None,
687
- existing_vault_public_key: None,
704
+ existing_vault_key_id,
705
+ existing_vault_public_key,
688
706
  }
689
707
  };
690
708
  let output = execute_bootstrap(
@@ -2909,21 +2927,36 @@ fn single_scope<T: Ord>(value: T) -> EntityScope<T> {
2909
2927
  mod tests {
2910
2928
  use super::{
2911
2929
  build_asset_scope, build_network_scope, describe_recipient_scope, ensure_output_parent,
2912
- execute_bootstrap, execute_revoke_agent_key, execute_rotate_agent_auth_token,
2913
- resolve_daemon_socket_path, resolve_output_format, resolve_output_target,
2914
- should_print_status, validate_existing_policy_attachments, validate_password,
2915
- validate_policy_limits, write_output_file, BootstrapParams, Cli, Commands,
2916
- DestinationPolicyOverride, OutputFormat, OutputTarget, RevokeAgentKeyParams,
2917
- RotateAgentAuthTokenParams, TokenDestinationPolicyOverride, TokenPolicyConfig,
2930
+ execute_add_manual_approval_policy, execute_bootstrap,
2931
+ execute_decide_manual_approval_request, execute_get_relay_config,
2932
+ execute_list_manual_approval_requests, execute_revoke_agent_key,
2933
+ execute_rotate_agent_auth_token, execute_set_relay_config, parse_non_negative_u128,
2934
+ parse_positive_u128, parse_positive_u64, print_bootstrap_output,
2935
+ print_manual_approval_policy_output, print_manual_approval_request_output,
2936
+ print_manual_approval_requests_output, print_relay_config_output,
2937
+ print_revoke_agent_key_output, print_rotate_agent_auth_token_output,
2938
+ resolve_bootstrap_policy_attachment, resolve_daemon_socket_path, resolve_output_format,
2939
+ resolve_output_target, should_print_status, validate_existing_policy_attachments,
2940
+ validate_password, validate_policy_limits, write_output_file,
2941
+ AddManualApprovalPolicyParams, BootstrapParams, Cli, Commands,
2942
+ DecideManualApprovalRequestParams, DestinationPolicyOverride, ManualApprovalPolicyOutput,
2943
+ OutputFormat, OutputTarget, RevokeAgentKeyOutput, RevokeAgentKeyParams,
2944
+ RotateAgentAuthTokenOutput, RotateAgentAuthTokenParams, SetRelayConfigParams,
2945
+ TokenDestinationPolicyOverride, TokenPolicyConfig,
2918
2946
  };
2919
2947
  use crate::{shared_config::WlfiConfig, tui};
2920
2948
  use clap::Parser;
2921
2949
  use serde_json::to_vec;
2950
+ use std::fs;
2922
2951
  use std::sync::Arc;
2952
+ use std::path::PathBuf;
2923
2953
  use std::time::{SystemTime, UNIX_EPOCH};
2924
2954
  use uuid::Uuid;
2925
2955
  use vault_daemon::{DaemonError, InMemoryDaemon, KeyManagerDaemonApi};
2926
- use vault_domain::{AdminSession, AgentAction, AssetId, EntityScope, EvmAddress, SignRequest};
2956
+ use vault_domain::{
2957
+ AdminSession, AgentAction, AssetId, EntityScope, EvmAddress, ManualApprovalDecision,
2958
+ ManualApprovalStatus, PolicyAttachment, RelayConfig, SignRequest, SpendingPolicy,
2959
+ };
2927
2960
  use vault_signer::{KeyCreateRequest, SoftwareSignerBackend};
2928
2961
  use zeroize::Zeroize;
2929
2962
 
@@ -3158,6 +3191,34 @@ mod tests {
3158
3191
  assert_eq!(args.attach_policy_id.len(), 2);
3159
3192
  }
3160
3193
 
3194
+ #[test]
3195
+ fn bootstrap_supports_existing_wallet_reuse_flags() {
3196
+ let cli = Cli::try_parse_from([
3197
+ "wlfi-agent-admin",
3198
+ "--daemon-socket",
3199
+ "/tmp/wlfi.sock",
3200
+ "bootstrap",
3201
+ "--from-shared-config",
3202
+ "--existing-vault-key-id",
3203
+ "00000000-0000-0000-0000-000000000003",
3204
+ "--existing-vault-public-key",
3205
+ "03abcdef",
3206
+ ])
3207
+ .expect("parse");
3208
+
3209
+ let Commands::Bootstrap(args) = cli.command else {
3210
+ panic!("expected bootstrap command");
3211
+ };
3212
+ assert_eq!(
3213
+ args.existing_vault_key_id,
3214
+ Some(
3215
+ Uuid::parse_str("00000000-0000-0000-0000-000000000003")
3216
+ .expect("valid uuid"),
3217
+ )
3218
+ );
3219
+ assert_eq!(args.existing_vault_public_key.as_deref(), Some("03abcdef"));
3220
+ }
3221
+
3161
3222
  #[test]
3162
3223
  fn bootstrap_command_accepts_explicit_vault_private_key_export_flag() {
3163
3224
  let cli = Cli::try_parse_from([
@@ -3218,12 +3279,12 @@ mod tests {
3218
3279
 
3219
3280
  #[test]
3220
3281
  fn setup_command_is_accepted() {
3221
- let cli = Cli::try_parse_from(["wlfi-agent-admin", "setup", "--network", "11155111"])
3282
+ let cli = Cli::try_parse_from(["wlfi-agent-admin", "setup", "--network", "56"])
3222
3283
  .expect("parse");
3223
3284
  let Commands::Setup(args) = cli.command else {
3224
3285
  panic!("expected setup command");
3225
3286
  };
3226
- assert_eq!(args.forwarded_args, vec!["--network", "11155111"]);
3287
+ assert_eq!(args.forwarded_args, vec!["--network", "56"]);
3227
3288
  }
3228
3289
 
3229
3290
  #[test]
@@ -3327,15 +3388,15 @@ mod tests {
3327
3388
  per_tx_max_calldata_bytes: 0,
3328
3389
  },
3329
3390
  TokenPolicyConfig {
3330
- token_key: "usdc".to_string(),
3331
- symbol: "USDC".to_string(),
3391
+ token_key: "usd1".to_string(),
3392
+ symbol: "USD1".to_string(),
3332
3393
  chain_key: "ethereum".to_string(),
3333
3394
  chain_id: 1,
3334
3395
  is_native: false,
3335
3396
  address: Some(
3336
3397
  "0x1000000000000000000000000000000000000000"
3337
3398
  .parse()
3338
- .expect("usdc address"),
3399
+ .expect("usd1 address"),
3339
3400
  ),
3340
3401
  per_tx_max_wei: 250,
3341
3402
  daily_max_wei: 1_000,
@@ -3402,6 +3463,75 @@ mod tests {
3402
3463
  (vault_key.id, vault_key.public_key_hex)
3403
3464
  }
3404
3465
 
3466
+ fn unique_temp_path(label: &str) -> PathBuf {
3467
+ let unique = SystemTime::now()
3468
+ .duration_since(UNIX_EPOCH)
3469
+ .expect("time")
3470
+ .as_nanos();
3471
+ std::env::temp_dir().join(format!("wlfi-admin-{label}-{unique:x}.txt"))
3472
+ }
3473
+
3474
+ fn read_output(path: &PathBuf) -> String {
3475
+ fs::read_to_string(path).expect("read output file")
3476
+ }
3477
+
3478
+ async fn seed_manual_approval_request(
3479
+ daemon: Arc<dyn KeyManagerDaemonApi>,
3480
+ ) -> (Uuid, AdminSession) {
3481
+ let lease = daemon
3482
+ .issue_lease("vault-password")
3483
+ .await
3484
+ .expect("issue lease");
3485
+ let session = AdminSession {
3486
+ vault_password: "vault-password".to_string(),
3487
+ lease,
3488
+ };
3489
+ daemon
3490
+ .add_policy(
3491
+ &session,
3492
+ SpendingPolicy::new_manual_approval(
3493
+ 0,
3494
+ 1,
3495
+ 100,
3496
+ EntityScope::All,
3497
+ EntityScope::All,
3498
+ EntityScope::All,
3499
+ )
3500
+ .expect("manual approval policy"),
3501
+ )
3502
+ .await
3503
+ .expect("add policy");
3504
+ let vault_key = daemon
3505
+ .create_vault_key(&session, KeyCreateRequest::Generate)
3506
+ .await
3507
+ .expect("vault key");
3508
+ let agent_credentials = daemon
3509
+ .create_agent_key(&session, vault_key.id, PolicyAttachment::AllPolicies)
3510
+ .await
3511
+ .expect("agent");
3512
+ let request = build_sign_request(
3513
+ &agent_credentials.agent_key.id.to_string(),
3514
+ &agent_credentials.auth_token,
3515
+ AgentAction::Transfer {
3516
+ chain_id: 1,
3517
+ token: "0x1000000000000000000000000000000000000000"
3518
+ .parse()
3519
+ .expect("token"),
3520
+ to: "0x2000000000000000000000000000000000000000"
3521
+ .parse()
3522
+ .expect("recipient"),
3523
+ amount_wei: 42,
3524
+ },
3525
+ );
3526
+ let approval_request_id = match daemon.sign_for_agent(request).await {
3527
+ Err(DaemonError::ManualApprovalRequired {
3528
+ approval_request_id, ..
3529
+ }) => approval_request_id,
3530
+ other => panic!("expected manual approval request, got {other:?}"),
3531
+ };
3532
+ (approval_request_id, session)
3533
+ }
3534
+
3405
3535
  #[tokio::test]
3406
3536
  async fn execute_bootstrap_creates_destination_override_policy_sets() {
3407
3537
  let daemon = test_daemon();
@@ -3506,7 +3636,7 @@ mod tests {
3506
3636
  },
3507
3637
  ))
3508
3638
  .await
3509
- .expect("erc20 transfer should use the USDC token policy");
3639
+ .expect("erc20 transfer should use the USD1 token policy");
3510
3640
  assert!(!erc20_signature.bytes.is_empty());
3511
3641
  }
3512
3642
 
@@ -3642,7 +3772,7 @@ mod tests {
3642
3772
  },
3643
3773
  ))
3644
3774
  .await
3645
- .expect("non-overridden recipient should keep the base ETH limit");
3775
+ .expect("non-overridden recipient should keep the default ETH limit");
3646
3776
  assert!(!allowed_elsewhere.bytes.is_empty());
3647
3777
  }
3648
3778
 
@@ -3971,6 +4101,499 @@ mod tests {
3971
4101
  .contains("bootstrap-created policy id(s)"));
3972
4102
  }
3973
4103
 
4104
+ #[test]
4105
+ fn manual_approval_and_relay_commands_are_accepted() {
4106
+ let list_cli = Cli::try_parse_from([
4107
+ "wlfi-agent-admin",
4108
+ "--daemon-socket",
4109
+ "/tmp/wlfi.sock",
4110
+ "list-manual-approval-requests",
4111
+ ])
4112
+ .expect("parse list");
4113
+ assert!(matches!(
4114
+ list_cli.command,
4115
+ Commands::ListManualApprovalRequests
4116
+ ));
4117
+
4118
+ let approve_cli = Cli::try_parse_from([
4119
+ "wlfi-agent-admin",
4120
+ "--daemon-socket",
4121
+ "/tmp/wlfi.sock",
4122
+ "approve-manual-approval-request",
4123
+ "--approval-request-id",
4124
+ "00000000-0000-0000-0000-000000000123",
4125
+ ])
4126
+ .expect("parse approve");
4127
+ let Commands::ApproveManualApprovalRequest(args) = approve_cli.command else {
4128
+ panic!("expected approve-manual-approval-request");
4129
+ };
4130
+ assert_eq!(
4131
+ args.approval_request_id,
4132
+ Uuid::parse_str("00000000-0000-0000-0000-000000000123").expect("uuid")
4133
+ );
4134
+
4135
+ let reject_cli = Cli::try_parse_from([
4136
+ "wlfi-agent-admin",
4137
+ "--daemon-socket",
4138
+ "/tmp/wlfi.sock",
4139
+ "reject-manual-approval-request",
4140
+ "--approval-request-id",
4141
+ "00000000-0000-0000-0000-000000000124",
4142
+ "--rejection-reason",
4143
+ "too risky",
4144
+ ])
4145
+ .expect("parse reject");
4146
+ let Commands::RejectManualApprovalRequest(args) = reject_cli.command else {
4147
+ panic!("expected reject-manual-approval-request");
4148
+ };
4149
+ assert_eq!(args.rejection_reason.as_deref(), Some("too risky"));
4150
+
4151
+ let add_cli = Cli::try_parse_from([
4152
+ "wlfi-agent-admin",
4153
+ "--daemon-socket",
4154
+ "/tmp/wlfi.sock",
4155
+ "add-manual-approval-policy",
4156
+ "--priority",
4157
+ "7",
4158
+ "--min-amount-wei",
4159
+ "10",
4160
+ "--max-amount-wei",
4161
+ "20",
4162
+ "--allow-native-eth",
4163
+ "--network",
4164
+ "1",
4165
+ ])
4166
+ .expect("parse add manual approval policy");
4167
+ let Commands::AddManualApprovalPolicy(args) = add_cli.command else {
4168
+ panic!("expected add-manual-approval-policy");
4169
+ };
4170
+ assert_eq!(args.priority, 7);
4171
+ assert!(args.allow_native_eth);
4172
+
4173
+ let set_cli = Cli::try_parse_from([
4174
+ "wlfi-agent-admin",
4175
+ "--daemon-socket",
4176
+ "/tmp/wlfi.sock",
4177
+ "set-relay-config",
4178
+ "--relay-url",
4179
+ "https://relay.example",
4180
+ "--frontend-url",
4181
+ "https://frontend.example",
4182
+ ])
4183
+ .expect("parse set relay config");
4184
+ let Commands::SetRelayConfig(args) = set_cli.command else {
4185
+ panic!("expected set-relay-config");
4186
+ };
4187
+ assert_eq!(args.relay_url.as_deref(), Some("https://relay.example"));
4188
+ assert_eq!(args.frontend_url.as_deref(), Some("https://frontend.example"));
4189
+
4190
+ let get_cli = Cli::try_parse_from([
4191
+ "wlfi-agent-admin",
4192
+ "--daemon-socket",
4193
+ "/tmp/wlfi.sock",
4194
+ "get-relay-config",
4195
+ ])
4196
+ .expect("parse get relay config");
4197
+ assert!(matches!(get_cli.command, Commands::GetRelayConfig));
4198
+ }
4199
+
4200
+ #[tokio::test]
4201
+ async fn manual_approval_execute_helpers_roundtrip() {
4202
+ let daemon = test_daemon();
4203
+ let (approval_request_id, mut session) = seed_manual_approval_request(daemon.clone()).await;
4204
+
4205
+ let mut list_statuses = Vec::new();
4206
+ let requests = execute_list_manual_approval_requests(daemon.clone(), "vault-password", |msg| {
4207
+ list_statuses.push(msg.to_string());
4208
+ })
4209
+ .await
4210
+ .expect("list requests");
4211
+ assert!(requests.iter().any(|request| request.id == approval_request_id));
4212
+ assert_eq!(
4213
+ list_statuses,
4214
+ vec![
4215
+ "issuing admin lease".to_string(),
4216
+ "listing manual approval requests".to_string()
4217
+ ]
4218
+ );
4219
+
4220
+ let mut approve_statuses = Vec::new();
4221
+ let approved = execute_decide_manual_approval_request(
4222
+ daemon.clone(),
4223
+ "vault-password",
4224
+ DecideManualApprovalRequestParams {
4225
+ approval_request_id,
4226
+ decision: ManualApprovalDecision::Approve,
4227
+ rejection_reason: None,
4228
+ },
4229
+ |msg| approve_statuses.push(msg.to_string()),
4230
+ )
4231
+ .await
4232
+ .expect("approve request");
4233
+ assert_eq!(approved.id, approval_request_id);
4234
+ assert_eq!(approved.status, ManualApprovalStatus::Approved);
4235
+ assert_eq!(
4236
+ approve_statuses,
4237
+ vec![
4238
+ "issuing admin lease".to_string(),
4239
+ "updating manual approval request".to_string()
4240
+ ]
4241
+ );
4242
+
4243
+ session.vault_password.zeroize();
4244
+ }
4245
+
4246
+ #[tokio::test]
4247
+ async fn add_manual_approval_policy_and_relay_config_helpers_roundtrip() {
4248
+ let daemon = test_daemon();
4249
+
4250
+ let mut policy_statuses = Vec::new();
4251
+ let policy_output = execute_add_manual_approval_policy(
4252
+ daemon.clone(),
4253
+ "vault-password",
4254
+ AddManualApprovalPolicyParams {
4255
+ priority: 7,
4256
+ min_amount_wei: 10,
4257
+ max_amount_wei: 20,
4258
+ tokens: vec![],
4259
+ allow_native_eth: true,
4260
+ network: Some(1),
4261
+ recipient: Some(
4262
+ "0x2000000000000000000000000000000000000000"
4263
+ .parse()
4264
+ .expect("recipient"),
4265
+ ),
4266
+ },
4267
+ |msg| policy_statuses.push(msg.to_string()),
4268
+ )
4269
+ .await
4270
+ .expect("manual approval policy");
4271
+ assert_eq!(policy_output.priority, 7);
4272
+ assert_eq!(policy_output.min_amount_wei, "10");
4273
+ assert_eq!(policy_output.max_amount_wei, "20");
4274
+ assert!(policy_output.asset_scope.contains("native_eth"));
4275
+ assert_eq!(
4276
+ policy_statuses,
4277
+ vec![
4278
+ "issuing admin lease".to_string(),
4279
+ "creating manual approval policy".to_string()
4280
+ ]
4281
+ );
4282
+
4283
+ let lease = daemon
4284
+ .issue_lease("vault-password")
4285
+ .await
4286
+ .expect("issue lease");
4287
+ let mut session = AdminSession {
4288
+ vault_password: "vault-password".to_string(),
4289
+ lease,
4290
+ };
4291
+ daemon
4292
+ .set_relay_config(
4293
+ &session,
4294
+ Some("https://relay.example".to_string()),
4295
+ None,
4296
+ )
4297
+ .await
4298
+ .expect("seed relay config");
4299
+ session.vault_password.zeroize();
4300
+
4301
+ let mut merge_statuses = Vec::new();
4302
+ let merged = execute_set_relay_config(
4303
+ daemon.clone(),
4304
+ "vault-password",
4305
+ SetRelayConfigParams {
4306
+ relay_url: None,
4307
+ frontend_url: Some("https://frontend.example".to_string()),
4308
+ clear: false,
4309
+ },
4310
+ |msg| merge_statuses.push(msg.to_string()),
4311
+ )
4312
+ .await
4313
+ .expect("merge relay config");
4314
+ assert_eq!(merged.relay_url.as_deref(), Some("https://relay.example"));
4315
+ assert_eq!(merged.frontend_url.as_deref(), Some("https://frontend.example"));
4316
+ assert_eq!(
4317
+ merge_statuses,
4318
+ vec![
4319
+ "issuing admin lease".to_string(),
4320
+ "reading existing relay configuration".to_string(),
4321
+ "updating relay configuration".to_string()
4322
+ ]
4323
+ );
4324
+
4325
+ let current = execute_get_relay_config(daemon.clone(), "vault-password", |_| {})
4326
+ .await
4327
+ .expect("get relay config");
4328
+ assert_eq!(current, merged);
4329
+
4330
+ let cleared = execute_set_relay_config(
4331
+ daemon,
4332
+ "vault-password",
4333
+ SetRelayConfigParams {
4334
+ relay_url: Some("https://ignored.example".to_string()),
4335
+ frontend_url: Some("https://ignored-frontend.example".to_string()),
4336
+ clear: true,
4337
+ },
4338
+ |_| {},
4339
+ )
4340
+ .await
4341
+ .expect("clear relay config");
4342
+ assert!(cleared.relay_url.is_none());
4343
+ assert!(cleared.frontend_url.is_none());
4344
+ }
4345
+
4346
+ #[tokio::test]
4347
+ async fn output_renderers_cover_text_sections() {
4348
+ let global_daemon = test_daemon();
4349
+ let mut global_params = test_bootstrap_params(false);
4350
+ global_params.print_vault_private_key = true;
4351
+ global_params
4352
+ .destination_overrides
4353
+ .push(DestinationPolicyOverride {
4354
+ recipient: "0x3000000000000000000000000000000000000003"
4355
+ .parse()
4356
+ .expect("recipient"),
4357
+ per_tx_max_wei: 100,
4358
+ daily_max_wei: 200,
4359
+ weekly_max_wei: 300,
4360
+ max_gas_per_chain_wei: 400,
4361
+ daily_max_tx_count: 2,
4362
+ per_tx_max_fee_per_gas_wei: 3,
4363
+ per_tx_max_priority_fee_per_gas_wei: 4,
4364
+ per_tx_max_calldata_bytes: 5,
4365
+ });
4366
+ let global_output = execute_bootstrap(
4367
+ global_daemon.clone(),
4368
+ "vault-password",
4369
+ "daemon_socket:/tmp/wlfi.sock",
4370
+ global_params,
4371
+ |_| {},
4372
+ )
4373
+ .await
4374
+ .expect("global bootstrap");
4375
+ let global_path = unique_temp_path("bootstrap-global");
4376
+ let global_target = OutputTarget::File {
4377
+ path: global_path.clone(),
4378
+ overwrite: true,
4379
+ };
4380
+ print_bootstrap_output(&global_output, OutputFormat::Text, &global_target)
4381
+ .expect("render global bootstrap");
4382
+ let global_text = read_output(&global_path);
4383
+ assert!(global_text.contains("Policies"));
4384
+ assert!(global_text.contains("Vault Private Key:"));
4385
+ assert!(global_text.contains("Destination Overrides"));
4386
+ assert!(global_text.contains("Note: pass --print-agent-auth-token"));
4387
+ fs::remove_file(&global_path).expect("cleanup global output");
4388
+
4389
+ let per_token_daemon = test_daemon();
4390
+ let mut per_token_params = test_per_token_bootstrap_params(true);
4391
+ per_token_params
4392
+ .token_destination_overrides
4393
+ .push(TokenDestinationPolicyOverride {
4394
+ token_key: "eth".to_string(),
4395
+ chain_key: "ethereum".to_string(),
4396
+ recipient: "0x3000000000000000000000000000000000000003"
4397
+ .parse()
4398
+ .expect("recipient"),
4399
+ per_tx_max_wei: 50,
4400
+ daily_max_wei: 250,
4401
+ weekly_max_wei: 500,
4402
+ max_gas_per_chain_wei: 1_000_000,
4403
+ daily_max_tx_count: 0,
4404
+ per_tx_max_fee_per_gas_wei: 0,
4405
+ per_tx_max_priority_fee_per_gas_wei: 0,
4406
+ per_tx_max_calldata_bytes: 0,
4407
+ });
4408
+ per_token_params
4409
+ .token_manual_approval_policies
4410
+ .push(super::TokenManualApprovalPolicyConfig {
4411
+ token_key: "eth".to_string(),
4412
+ symbol: "ETH".to_string(),
4413
+ chain_key: "ethereum".to_string(),
4414
+ chain_id: 1,
4415
+ is_native: true,
4416
+ address: None,
4417
+ priority: 9,
4418
+ recipient: Some(
4419
+ "0x4000000000000000000000000000000000000004"
4420
+ .parse()
4421
+ .expect("recipient"),
4422
+ ),
4423
+ min_amount_wei: 10,
4424
+ max_amount_wei: 20,
4425
+ });
4426
+ let per_token_output = execute_bootstrap(
4427
+ per_token_daemon,
4428
+ "vault-password",
4429
+ "daemon_socket:/tmp/wlfi.sock",
4430
+ per_token_params,
4431
+ |_| {},
4432
+ )
4433
+ .await
4434
+ .expect("per-token bootstrap");
4435
+ let per_token_path = unique_temp_path("bootstrap-per-token");
4436
+ let per_token_target = OutputTarget::File {
4437
+ path: per_token_path.clone(),
4438
+ overwrite: true,
4439
+ };
4440
+ print_bootstrap_output(&per_token_output, OutputFormat::Text, &per_token_target)
4441
+ .expect("render per-token bootstrap");
4442
+ let per_token_text = read_output(&per_token_path);
4443
+ assert!(per_token_text.contains("Per-Token Policies"));
4444
+ assert!(per_token_text.contains("Per-Token Destination Overrides"));
4445
+ assert!(per_token_text.contains("Per-Token Manual Approval Policies"));
4446
+ assert!(per_token_text.contains("Attached Policy IDs"));
4447
+ assert!(per_token_text.contains("Warning: keep the agent auth token"));
4448
+ fs::remove_file(&per_token_path).expect("cleanup per-token output");
4449
+ }
4450
+
4451
+ #[tokio::test]
4452
+ async fn output_renderers_cover_manual_approval_and_relay_text() {
4453
+ let daemon = test_daemon();
4454
+ let (approval_request_id, mut session) = seed_manual_approval_request(daemon.clone()).await;
4455
+ let request = daemon
4456
+ .list_manual_approval_requests(&session)
4457
+ .await
4458
+ .expect("list manual approvals")
4459
+ .into_iter()
4460
+ .find(|item| item.id == approval_request_id)
4461
+ .expect("request");
4462
+
4463
+ let requests_path = unique_temp_path("manual-requests");
4464
+ let requests_target = OutputTarget::File {
4465
+ path: requests_path.clone(),
4466
+ overwrite: true,
4467
+ };
4468
+ print_manual_approval_requests_output(&[], OutputFormat::Text, &requests_target)
4469
+ .expect("render empty requests");
4470
+ assert_eq!(read_output(&requests_path).trim_end(), "No manual approval requests");
4471
+
4472
+ print_manual_approval_request_output(&request, OutputFormat::Text, &requests_target)
4473
+ .expect("render single request");
4474
+ let single_text = read_output(&requests_path);
4475
+ assert!(single_text.contains("Request ID:"));
4476
+ assert!(single_text.contains("Triggered By Policies:"));
4477
+
4478
+ print_manual_approval_requests_output(
4479
+ std::slice::from_ref(&request),
4480
+ OutputFormat::Text,
4481
+ &requests_target,
4482
+ )
4483
+ .expect("render request list");
4484
+ let list_text = read_output(&requests_path);
4485
+ assert!(list_text.contains("Status: Pending"));
4486
+ fs::remove_file(&requests_path).expect("cleanup manual approval output");
4487
+
4488
+ let relay_output = RelayConfig {
4489
+ relay_url: Some("https://relay.example".to_string()),
4490
+ frontend_url: None,
4491
+ daemon_id_hex: "daemon-id".to_string(),
4492
+ daemon_public_key_hex: "daemon-pub".to_string(),
4493
+ };
4494
+ let relay_path = unique_temp_path("relay-config");
4495
+ let relay_target = OutputTarget::File {
4496
+ path: relay_path.clone(),
4497
+ overwrite: true,
4498
+ };
4499
+ print_relay_config_output(&relay_output, OutputFormat::Text, &relay_target)
4500
+ .expect("render relay config");
4501
+ let relay_text = read_output(&relay_path);
4502
+ assert!(relay_text.contains("Relay URL: https://relay.example"));
4503
+ assert!(relay_text.contains("Frontend URL: <unset>"));
4504
+ fs::remove_file(&relay_path).expect("cleanup relay output");
4505
+
4506
+ let rotate_path = unique_temp_path("rotate-output");
4507
+ let rotate_target = OutputTarget::File {
4508
+ path: rotate_path.clone(),
4509
+ overwrite: true,
4510
+ };
4511
+ print_rotate_agent_auth_token_output(
4512
+ &RotateAgentAuthTokenOutput {
4513
+ agent_key_id: "agent-key".to_string(),
4514
+ agent_auth_token: "<redacted>".to_string(),
4515
+ agent_auth_token_redacted: true,
4516
+ },
4517
+ OutputFormat::Text,
4518
+ &rotate_target,
4519
+ )
4520
+ .expect("render rotate output");
4521
+ assert!(read_output(&rotate_path).contains("Note: pass --print-agent-auth-token"));
4522
+ fs::remove_file(&rotate_path).expect("cleanup rotate output");
4523
+
4524
+ let revoke_path = unique_temp_path("revoke-output");
4525
+ let revoke_target = OutputTarget::File {
4526
+ path: revoke_path.clone(),
4527
+ overwrite: true,
4528
+ };
4529
+ print_revoke_agent_key_output(
4530
+ &RevokeAgentKeyOutput {
4531
+ agent_key_id: "agent-key".to_string(),
4532
+ revoked: true,
4533
+ },
4534
+ OutputFormat::Text,
4535
+ &revoke_target,
4536
+ )
4537
+ .expect("render revoke output");
4538
+ assert!(read_output(&revoke_path).contains("Revoked: true"));
4539
+ fs::remove_file(&revoke_path).expect("cleanup revoke output");
4540
+
4541
+ let policy_path = unique_temp_path("policy-output");
4542
+ let policy_target = OutputTarget::File {
4543
+ path: policy_path.clone(),
4544
+ overwrite: true,
4545
+ };
4546
+ print_manual_approval_policy_output(
4547
+ &ManualApprovalPolicyOutput {
4548
+ policy_id: "policy-id".to_string(),
4549
+ priority: 7,
4550
+ min_amount_wei: "10".to_string(),
4551
+ max_amount_wei: "20".to_string(),
4552
+ network_scope: "1".to_string(),
4553
+ asset_scope: "native_eth".to_string(),
4554
+ recipient_scope: "all recipients".to_string(),
4555
+ },
4556
+ OutputFormat::Text,
4557
+ &policy_target,
4558
+ )
4559
+ .expect("render manual approval policy");
4560
+ assert!(read_output(&policy_path).contains("Amount Range (wei): 10..=20"));
4561
+ fs::remove_file(&policy_path).expect("cleanup policy output");
4562
+
4563
+ session.vault_password.zeroize();
4564
+ }
4565
+
4566
+ #[test]
4567
+ fn helper_parsers_and_policy_attachment_resolution_cover_remaining_paths() {
4568
+ assert_eq!(parse_positive_u128("10").expect("parse"), 10);
4569
+ assert_eq!(parse_non_negative_u128("0").expect("parse"), 0);
4570
+ assert_eq!(parse_positive_u64("12").expect("parse"), 12);
4571
+ assert!(parse_positive_u128("0").is_err());
4572
+ assert!(parse_positive_u64("0").is_err());
4573
+ assert!(parse_non_negative_u128("nope").is_err());
4574
+
4575
+ let created = Uuid::parse_str("00000000-0000-0000-0000-000000000101").expect("uuid");
4576
+ let explicit = Uuid::parse_str("00000000-0000-0000-0000-000000000202").expect("uuid");
4577
+
4578
+ let created_only =
4579
+ resolve_bootstrap_policy_attachment([created], &[]).expect("created-only attachment");
4580
+ assert_eq!(created_only.1, "policy_set");
4581
+ assert_eq!(created_only.2.len(), 1);
4582
+ assert!(created_only.3.contains("bootstrap-created policy"));
4583
+
4584
+ let explicit_only = resolve_bootstrap_policy_attachment([], &[explicit])
4585
+ .expect("explicit-only attachment");
4586
+ assert_eq!(explicit_only.2, vec![explicit.to_string()]);
4587
+ assert!(explicit_only.3.contains("explicit policy"));
4588
+
4589
+ let mixed = resolve_bootstrap_policy_attachment([created], &[explicit])
4590
+ .expect("mixed attachment");
4591
+ assert_eq!(mixed.2.len(), 2);
4592
+ assert!(mixed.3.contains("bootstrap-created policy id(s) and 1 explicit policy id(s)"));
4593
+
4594
+ assert!(resolve_bootstrap_policy_attachment([], &[]).is_err());
4595
+ }
4596
+
3974
4597
  #[test]
3975
4598
  #[cfg(unix)]
3976
4599
  fn resolve_daemon_socket_path_rejects_non_root_owned_socket() {