@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.
- package/Cargo.lock +5 -0
- package/README.md +61 -28
- package/crates/vault-cli-admin/src/io_utils.rs +149 -1
- package/crates/vault-cli-admin/src/main.rs +639 -16
- package/crates/vault-cli-admin/src/shared_config.rs +18 -18
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
- package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
- package/crates/vault-cli-admin/src/tui.rs +1205 -120
- package/crates/vault-cli-agent/Cargo.toml +1 -0
- package/crates/vault-cli-agent/src/io_utils.rs +163 -2
- package/crates/vault-cli-agent/src/main.rs +648 -32
- package/crates/vault-cli-daemon/Cargo.toml +4 -0
- package/crates/vault-cli-daemon/src/main.rs +617 -67
- package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
- package/crates/vault-daemon/src/persistence.rs +637 -100
- package/crates/vault-daemon/src/tests.rs +1013 -3
- package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
- package/crates/vault-domain/src/nonce.rs +4 -0
- package/crates/vault-domain/src/tests.rs +616 -0
- package/crates/vault-policy/src/engine.rs +55 -32
- package/crates/vault-policy/src/tests.rs +195 -0
- package/crates/vault-sdk-agent/src/lib.rs +415 -22
- package/crates/vault-signer/Cargo.toml +3 -0
- package/crates/vault-signer/src/lib.rs +266 -40
- package/crates/vault-transport-unix/src/lib.rs +653 -5
- package/crates/vault-transport-xpc/src/tests.rs +531 -3
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
- package/dist/cli.cjs +756 -194
- package/dist/cli.cjs.map +1 -1
- package/package.json +5 -2
- package/packages/cache/.turbo/turbo-build.log +20 -20
- package/packages/cache/coverage/clover.xml +529 -394
- package/packages/cache/coverage/coverage-final.json +2 -2
- package/packages/cache/coverage/index.html +21 -21
- package/packages/cache/coverage/src/client/index.html +1 -1
- package/packages/cache/coverage/src/client/index.ts.html +1 -1
- package/packages/cache/coverage/src/errors/index.html +1 -1
- package/packages/cache/coverage/src/errors/index.ts.html +12 -12
- package/packages/cache/coverage/src/index.html +1 -1
- package/packages/cache/coverage/src/index.ts.html +1 -1
- package/packages/cache/coverage/src/service/index.html +21 -21
- package/packages/cache/coverage/src/service/index.ts.html +769 -313
- package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
- package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
- package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
- package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
- package/packages/cache/dist/index.cjs +2 -2
- package/packages/cache/dist/index.js +1 -1
- package/packages/cache/dist/service/index.cjs +2 -2
- package/packages/cache/dist/service/index.js +1 -1
- package/packages/cache/node_modules/.bin/tsc +2 -2
- package/packages/cache/node_modules/.bin/tsserver +2 -2
- package/packages/cache/node_modules/.bin/tsup +2 -2
- package/packages/cache/node_modules/.bin/tsup-node +2 -2
- package/packages/cache/node_modules/.bin/vitest +4 -4
- package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/cache/src/service/index.test.ts +165 -19
- package/packages/cache/src/service/index.ts +38 -1
- package/packages/config/.turbo/turbo-build.log +4 -4
- package/packages/config/dist/index.cjs +0 -17
- package/packages/config/dist/index.cjs.map +1 -1
- package/packages/config/src/index.ts +0 -17
- package/packages/rpc/.turbo/turbo-build.log +11 -11
- package/packages/rpc/dist/index.cjs +0 -17
- package/packages/rpc/dist/index.cjs.map +1 -1
- package/packages/rpc/src/index.js +1 -0
- package/packages/ui/node_modules/.bin/tsc +2 -2
- package/packages/ui/node_modules/.bin/tsserver +2 -2
- package/packages/ui/node_modules/.bin/tsup +2 -2
- package/packages/ui/node_modules/.bin/tsup-node +2 -2
- package/scripts/install-cli-launcher.mjs +37 -0
- package/scripts/install-rust-binaries.mjs +47 -0
- package/scripts/run-tests-isolated.mjs +210 -0
- package/src/cli.ts +310 -50
- package/src/lib/admin-reset.ts +101 -33
- package/src/lib/admin-setup.ts +285 -55
- package/src/lib/agent-auth-migrate.ts +5 -1
- package/src/lib/asset-broadcast.ts +15 -4
- package/src/lib/config-amounts.ts +6 -4
- package/src/lib/hidden-tty-prompt.js +1 -0
- package/src/lib/hidden-tty-prompt.ts +105 -0
- package/src/lib/keychain.ts +1 -0
- package/src/lib/local-admin-access.ts +4 -29
- package/src/lib/rust.ts +129 -33
- package/src/lib/signed-tx.ts +1 -0
- package/src/lib/sudo.ts +15 -5
- package/src/lib/wallet-profile.ts +3 -0
- package/src/lib/wallet-setup.ts +52 -0
- package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
- 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
|
|
687
|
-
existing_vault_public_key
|
|
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
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
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::{
|
|
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", "
|
|
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", "
|
|
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: "
|
|
3331
|
-
symbol: "
|
|
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("
|
|
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
|
|
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
|
|
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() {
|