@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
@@ -1,14 +1,92 @@
1
1
  use super::{
2
- parse_code_sign_requirement, recv_matching_response, validate_wire_lengths,
3
- IncomingWireMessage, XpcDaemonClient, XpcDaemonServer, XpcTransportError, MAX_WIRE_BODY_BYTES,
4
- MAX_WIRE_REQUEST_ID_BYTES,
2
+ decode_wire_request, decode_wire_response, encode_wire_request, encode_wire_response,
3
+ enforce_wire_response_limits, extract_safe_request_id, get_dict_bool, get_dict_string,
4
+ parse_code_sign_requirement, recv_matching_response, serialize_wire_daemon_error,
5
+ set_dict_bool, set_dict_string, validate_wire_lengths, IncomingWireMessage, WireDaemonError,
6
+ WireRequest, WireResponse, XpcDaemonClient, XpcDaemonServer, XpcTransportError,
7
+ MAX_WIRE_BODY_BYTES, MAX_WIRE_REQUEST_ID_BYTES,
5
8
  };
6
9
  use std::collections::BTreeSet;
10
+ use std::ptr;
7
11
  use std::sync::Arc;
8
12
  use std::time::Duration;
13
+ use time::OffsetDateTime;
14
+ use uuid::Uuid;
9
15
  use vault_daemon::{DaemonConfig, DaemonError, InMemoryDaemon, KeyManagerDaemonApi};
16
+ use vault_domain::{
17
+ AdminSession, AgentAction, AgentCredentials, EntityScope, ManualApprovalDecision,
18
+ ManualApprovalStatus, NonceReleaseRequest, NonceReservationRequest, PolicyAttachment,
19
+ SignRequest, SpendingPolicy,
20
+ };
21
+ use vault_policy::PolicyError;
10
22
  use vault_signer::{KeyCreateRequest, SignerError, SoftwareSignerBackend};
11
23
 
24
+ fn test_daemon() -> Arc<InMemoryDaemon<SoftwareSignerBackend>> {
25
+ Arc::new(
26
+ InMemoryDaemon::new(
27
+ "vault-password",
28
+ SoftwareSignerBackend::default(),
29
+ DaemonConfig::default(),
30
+ )
31
+ .expect("daemon"),
32
+ )
33
+ }
34
+
35
+ fn start_test_server(
36
+ daemon: Arc<InMemoryDaemon<SoftwareSignerBackend>>,
37
+ ) -> XpcDaemonServer {
38
+ #[cfg(debug_assertions)]
39
+ {
40
+ XpcDaemonServer::start_inmemory_with_allowed_euid(
41
+ daemon,
42
+ tokio::runtime::Handle::current(),
43
+ unsafe { libc::geteuid() },
44
+ )
45
+ .expect("server")
46
+ }
47
+ #[cfg(not(debug_assertions))]
48
+ {
49
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server")
50
+ }
51
+ }
52
+
53
+ fn connect_test_client(server: &XpcDaemonServer) -> XpcDaemonClient {
54
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client")
55
+ }
56
+
57
+ async fn admin_session(client: &XpcDaemonClient) -> AdminSession {
58
+ let lease = client.issue_lease("vault-password").await.expect("lease");
59
+ AdminSession {
60
+ vault_password: "vault-password".to_string(),
61
+ lease,
62
+ }
63
+ }
64
+
65
+ fn transfer_action(amount_wei: u128) -> AgentAction {
66
+ AgentAction::Transfer {
67
+ chain_id: 1,
68
+ token: "0x7100000000000000000000000000000000000000"
69
+ .parse()
70
+ .expect("token"),
71
+ to: "0x8100000000000000000000000000000000000000"
72
+ .parse()
73
+ .expect("recipient"),
74
+ amount_wei,
75
+ }
76
+ }
77
+
78
+ fn sign_request(agent_credentials: &AgentCredentials, action: AgentAction) -> SignRequest {
79
+ SignRequest {
80
+ request_id: Uuid::new_v4(),
81
+ agent_key_id: agent_credentials.agent_key.id,
82
+ agent_auth_token: agent_credentials.auth_token.clone(),
83
+ payload: serde_json::to_vec(&action).expect("payload"),
84
+ action,
85
+ requested_at: OffsetDateTime::now_utc(),
86
+ expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
87
+ }
88
+ }
89
+
12
90
  #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
13
91
  async fn xpc_round_trip_for_issue_lease() {
14
92
  #[cfg(not(debug_assertions))]
@@ -816,3 +894,453 @@ fn receive_loop_times_out_when_only_mismatched_responses_arrive() {
816
894
  .expect_err("mismatched responses must eventually timeout");
817
895
  assert!(matches!(err, XpcTransportError::Timeout));
818
896
  }
897
+
898
+ #[test]
899
+ fn wire_daemon_error_roundtrip_covers_all_variants() {
900
+ let vault_key_id = Uuid::new_v4();
901
+ let agent_key_id = Uuid::new_v4();
902
+ let policy_id = Uuid::new_v4();
903
+ let approval_request_id = Uuid::new_v4();
904
+ let reservation_id = Uuid::new_v4();
905
+
906
+ macro_rules! assert_roundtrip {
907
+ ($err:expr, $pat:pat $(if $guard:expr)? ) => {{
908
+ let roundtrip = WireDaemonError::from($err).into_daemon_error();
909
+ assert!(matches!(roundtrip, $pat $(if $guard)?));
910
+ }};
911
+ }
912
+
913
+ assert_roundtrip!(DaemonError::AuthenticationFailed, DaemonError::AuthenticationFailed);
914
+ assert_roundtrip!(DaemonError::UnknownLease, DaemonError::UnknownLease);
915
+ assert_roundtrip!(DaemonError::InvalidLease, DaemonError::InvalidLease);
916
+ assert_roundtrip!(DaemonError::TooManyActiveLeases, DaemonError::TooManyActiveLeases);
917
+ assert_roundtrip!(
918
+ DaemonError::UnknownVaultKey(vault_key_id),
919
+ DaemonError::UnknownVaultKey(id) if id == vault_key_id
920
+ );
921
+ assert_roundtrip!(
922
+ DaemonError::UnknownAgentKey(agent_key_id),
923
+ DaemonError::UnknownAgentKey(id) if id == agent_key_id
924
+ );
925
+ assert_roundtrip!(
926
+ DaemonError::UnknownPolicy(policy_id),
927
+ DaemonError::UnknownPolicy(id) if id == policy_id
928
+ );
929
+ assert_roundtrip!(
930
+ DaemonError::UnknownManualApprovalRequest(approval_request_id),
931
+ DaemonError::UnknownManualApprovalRequest(id) if id == approval_request_id
932
+ );
933
+ assert_roundtrip!(
934
+ DaemonError::AgentAuthenticationFailed,
935
+ DaemonError::AgentAuthenticationFailed
936
+ );
937
+ assert_roundtrip!(
938
+ DaemonError::PayloadActionMismatch,
939
+ DaemonError::PayloadActionMismatch
940
+ );
941
+ assert_roundtrip!(
942
+ DaemonError::PayloadTooLarge { max_bytes: 4096 },
943
+ DaemonError::PayloadTooLarge { max_bytes } if max_bytes == 4096
944
+ );
945
+ assert_roundtrip!(
946
+ DaemonError::InvalidRequestTimestamps,
947
+ DaemonError::InvalidRequestTimestamps
948
+ );
949
+ assert_roundtrip!(DaemonError::RequestExpired, DaemonError::RequestExpired);
950
+ assert_roundtrip!(
951
+ DaemonError::RequestReplayDetected,
952
+ DaemonError::RequestReplayDetected
953
+ );
954
+ assert_roundtrip!(
955
+ DaemonError::InvalidPolicyAttachment("bad attachment".to_string()),
956
+ DaemonError::InvalidPolicyAttachment(ref msg) if msg == "bad attachment"
957
+ );
958
+ assert_roundtrip!(
959
+ DaemonError::InvalidNonceReservation("bad reservation".to_string()),
960
+ DaemonError::InvalidNonceReservation(ref msg) if msg == "bad reservation"
961
+ );
962
+ assert_roundtrip!(
963
+ DaemonError::UnknownNonceReservation(reservation_id),
964
+ DaemonError::UnknownNonceReservation(id) if id == reservation_id
965
+ );
966
+ assert_roundtrip!(
967
+ DaemonError::MissingNonceReservation {
968
+ chain_id: 1,
969
+ nonce: 7
970
+ },
971
+ DaemonError::MissingNonceReservation { chain_id, nonce } if chain_id == 1 && nonce == 7
972
+ );
973
+ assert_roundtrip!(
974
+ DaemonError::InvalidPolicy("bad policy".to_string()),
975
+ DaemonError::InvalidPolicy(ref msg) if msg == "bad policy"
976
+ );
977
+ assert_roundtrip!(
978
+ DaemonError::InvalidRelayConfig("bad relay".to_string()),
979
+ DaemonError::InvalidRelayConfig(ref msg) if msg == "bad relay"
980
+ );
981
+ assert_roundtrip!(
982
+ DaemonError::ManualApprovalRequired {
983
+ approval_request_id,
984
+ relay_url: Some("https://relay.example".to_string()),
985
+ frontend_url: Some("https://frontend.example".to_string()),
986
+ },
987
+ DaemonError::ManualApprovalRequired {
988
+ approval_request_id: id,
989
+ ref relay_url,
990
+ ref frontend_url,
991
+ } if id == approval_request_id
992
+ && relay_url.as_deref() == Some("https://relay.example")
993
+ && frontend_url.as_deref() == Some("https://frontend.example")
994
+ );
995
+ assert_roundtrip!(
996
+ DaemonError::ManualApprovalRejected { approval_request_id },
997
+ DaemonError::ManualApprovalRejected { approval_request_id: id } if id == approval_request_id
998
+ );
999
+ assert_roundtrip!(
1000
+ DaemonError::ManualApprovalRequestNotPending {
1001
+ approval_request_id,
1002
+ status: ManualApprovalStatus::Completed,
1003
+ },
1004
+ DaemonError::ManualApprovalRequestNotPending {
1005
+ approval_request_id: id,
1006
+ status,
1007
+ } if id == approval_request_id && status == ManualApprovalStatus::Completed
1008
+ );
1009
+ assert_roundtrip!(
1010
+ DaemonError::Policy(PolicyError::NoAttachedPolicies),
1011
+ DaemonError::Policy(PolicyError::NoAttachedPolicies)
1012
+ );
1013
+ assert_roundtrip!(
1014
+ DaemonError::Signer(SignerError::PermissionDenied("no root".to_string())),
1015
+ DaemonError::Signer(SignerError::PermissionDenied(ref msg)) if msg == "no root"
1016
+ );
1017
+ assert_roundtrip!(
1018
+ DaemonError::PasswordHash("bad hash".to_string()),
1019
+ DaemonError::PasswordHash(ref msg) if msg == "bad hash"
1020
+ );
1021
+ assert_roundtrip!(
1022
+ DaemonError::InvalidConfig("bad config".to_string()),
1023
+ DaemonError::InvalidConfig(ref msg) if msg == "bad config"
1024
+ );
1025
+ assert_roundtrip!(DaemonError::LockPoisoned, DaemonError::LockPoisoned);
1026
+ assert_roundtrip!(
1027
+ DaemonError::Transport("wire broken".to_string()),
1028
+ DaemonError::Transport(ref msg) if msg == "wire broken"
1029
+ );
1030
+ assert_roundtrip!(
1031
+ DaemonError::Persistence("disk full".to_string()),
1032
+ DaemonError::Persistence(ref msg) if msg == "disk full"
1033
+ );
1034
+ }
1035
+
1036
+ #[test]
1037
+ fn serialize_wire_daemon_error_and_response_limit_helpers_cover_remaining_paths() {
1038
+ let serialized = serialize_wire_daemon_error(DaemonError::Transport("transport".to_string()));
1039
+ let parsed: WireDaemonError = serde_json::from_str(&serialized).expect("deserialize");
1040
+ assert!(matches!(parsed, WireDaemonError::Transport(msg) if msg == "transport"));
1041
+
1042
+ let passthrough = WireResponse {
1043
+ request_id: "ok-id".to_string(),
1044
+ ok: true,
1045
+ body_json: "{}".to_string(),
1046
+ };
1047
+ let passthrough = enforce_wire_response_limits(passthrough.clone());
1048
+ assert_eq!(passthrough.request_id, "ok-id");
1049
+ assert!(passthrough.ok);
1050
+ assert_eq!(passthrough.body_json, "{}");
1051
+
1052
+ let oversized_body = WireResponse {
1053
+ request_id: "ok-id".to_string(),
1054
+ ok: true,
1055
+ body_json: "x".repeat(MAX_WIRE_BODY_BYTES + 1),
1056
+ };
1057
+ let oversized_body = enforce_wire_response_limits(oversized_body);
1058
+ assert_eq!(oversized_body.request_id, "ok-id");
1059
+ assert!(!oversized_body.ok);
1060
+ assert!(oversized_body.body_json.contains("wire body exceeds max bytes"));
1061
+
1062
+ let oversized_id = WireResponse {
1063
+ request_id: "r".repeat(MAX_WIRE_REQUEST_ID_BYTES + 1),
1064
+ ok: true,
1065
+ body_json: "{}".to_string(),
1066
+ };
1067
+ let oversized_id = enforce_wire_response_limits(oversized_id);
1068
+ assert_eq!(oversized_id.request_id, Uuid::nil().to_string());
1069
+ assert!(!oversized_id.ok);
1070
+ assert!(oversized_id.body_json.contains("wire request id exceeds max bytes"));
1071
+ }
1072
+
1073
+ #[test]
1074
+ fn xpc_dictionary_helpers_roundtrip_and_guard_invalid_fields() {
1075
+ let wire_request = WireRequest {
1076
+ request_id: "request-1".to_string(),
1077
+ body_json: "{\"hello\":true}".to_string(),
1078
+ };
1079
+ let encoded_request = encode_wire_request(&wire_request).expect("encode request");
1080
+ let decoded_request = decode_wire_request(encoded_request).expect("decode request");
1081
+ assert_eq!(decoded_request.request_id, wire_request.request_id);
1082
+ assert_eq!(decoded_request.body_json, wire_request.body_json);
1083
+ unsafe {
1084
+ super::xpc_release(encoded_request);
1085
+ }
1086
+
1087
+ let wire_response = WireResponse {
1088
+ request_id: "response-1".to_string(),
1089
+ ok: true,
1090
+ body_json: "{\"ok\":true}".to_string(),
1091
+ };
1092
+ let encoded_response = encode_wire_response(&wire_response).expect("encode response");
1093
+ let decoded_response = decode_wire_response(encoded_response).expect("decode response");
1094
+ assert_eq!(decoded_response.request_id, wire_response.request_id);
1095
+ assert!(decoded_response.ok);
1096
+ assert_eq!(decoded_response.body_json, wire_response.body_json);
1097
+ unsafe {
1098
+ super::xpc_release(encoded_response);
1099
+ }
1100
+
1101
+ let dict = unsafe { super::xpc_dictionary_create(ptr::null(), ptr::null(), 0) };
1102
+ assert!(!dict.is_null());
1103
+ set_dict_string(dict, "wlfi_request_id", "request-2").expect("request id");
1104
+ set_dict_string(dict, "wlfi_kind", "response").expect("kind");
1105
+ set_dict_bool(dict, "wlfi_ok", false).expect("bool");
1106
+ assert_eq!(
1107
+ get_dict_string(dict, "wlfi_request_id").expect("read string"),
1108
+ "request-2"
1109
+ );
1110
+ assert!(!get_dict_bool(dict, "wlfi_ok").expect("read bool"));
1111
+ assert!(matches!(
1112
+ get_dict_string(dict, "missing"),
1113
+ Err(XpcTransportError::Protocol(_))
1114
+ ));
1115
+ unsafe {
1116
+ super::xpc_release(dict);
1117
+ }
1118
+
1119
+ let wrong_kind = unsafe { super::xpc_dictionary_create(ptr::null(), ptr::null(), 0) };
1120
+ assert!(!wrong_kind.is_null());
1121
+ set_dict_string(wrong_kind, "wlfi_kind", "wrong").expect("kind");
1122
+ set_dict_string(wrong_kind, "wlfi_request_id", "request-3").expect("request id");
1123
+ set_dict_string(wrong_kind, "wlfi_body", "{}").expect("body");
1124
+ assert!(matches!(
1125
+ decode_wire_request(wrong_kind),
1126
+ Err(XpcTransportError::Protocol(_))
1127
+ ));
1128
+ unsafe {
1129
+ super::xpc_release(wrong_kind);
1130
+ }
1131
+
1132
+ let wrong_response_kind = unsafe { super::xpc_dictionary_create(ptr::null(), ptr::null(), 0) };
1133
+ assert!(!wrong_response_kind.is_null());
1134
+ set_dict_string(wrong_response_kind, "wlfi_kind", "request").expect("kind");
1135
+ set_dict_string(wrong_response_kind, "wlfi_request_id", "request-4").expect("request id");
1136
+ set_dict_string(wrong_response_kind, "wlfi_body", "{}").expect("body");
1137
+ set_dict_bool(wrong_response_kind, "wlfi_ok", true).expect("bool");
1138
+ assert!(matches!(
1139
+ decode_wire_response(wrong_response_kind),
1140
+ Err(XpcTransportError::Protocol(_))
1141
+ ));
1142
+ unsafe {
1143
+ super::xpc_release(wrong_response_kind);
1144
+ }
1145
+
1146
+ let missing_id = unsafe { super::xpc_dictionary_create(ptr::null(), ptr::null(), 0) };
1147
+ assert_eq!(extract_safe_request_id(missing_id), Uuid::nil().to_string());
1148
+ unsafe {
1149
+ super::xpc_release(missing_id);
1150
+ }
1151
+
1152
+ let oversized_id = unsafe { super::xpc_dictionary_create(ptr::null(), ptr::null(), 0) };
1153
+ set_dict_string(
1154
+ oversized_id,
1155
+ "wlfi_request_id",
1156
+ &"r".repeat(MAX_WIRE_REQUEST_ID_BYTES + 1),
1157
+ )
1158
+ .expect("oversized id");
1159
+ assert_eq!(extract_safe_request_id(oversized_id), Uuid::nil().to_string());
1160
+ unsafe {
1161
+ super::xpc_release(oversized_id);
1162
+ }
1163
+
1164
+ let valid_id = unsafe { super::xpc_dictionary_create(ptr::null(), ptr::null(), 0) };
1165
+ set_dict_string(valid_id, "wlfi_request_id", "request-5").expect("request id");
1166
+ assert_eq!(extract_safe_request_id(valid_id), "request-5");
1167
+ unsafe {
1168
+ super::xpc_release(valid_id);
1169
+ }
1170
+
1171
+ let dict = unsafe { super::xpc_dictionary_create(ptr::null(), ptr::null(), 0) };
1172
+ assert!(matches!(
1173
+ set_dict_string(dict, "bad\0key", "value"),
1174
+ Err(XpcTransportError::Protocol(_))
1175
+ ));
1176
+ assert!(matches!(
1177
+ set_dict_string(dict, "ok-key", "bad\0value"),
1178
+ Err(XpcTransportError::Protocol(_))
1179
+ ));
1180
+ assert!(matches!(
1181
+ set_dict_bool(dict, "bad\0key", true),
1182
+ Err(XpcTransportError::Protocol(_))
1183
+ ));
1184
+ assert!(matches!(
1185
+ get_dict_bool(dict, "bad\0key"),
1186
+ Err(XpcTransportError::Protocol(_))
1187
+ ));
1188
+ unsafe {
1189
+ super::xpc_release(dict);
1190
+ }
1191
+ }
1192
+
1193
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1194
+ async fn remaining_admin_methods_roundtrip_over_xpc() {
1195
+ #[cfg(not(debug_assertions))]
1196
+ if unsafe { libc::geteuid() } != 0 {
1197
+ return;
1198
+ }
1199
+
1200
+ let daemon = test_daemon();
1201
+ let server = start_test_server(daemon);
1202
+ let client = connect_test_client(&server);
1203
+ let admin = admin_session(&client).await;
1204
+
1205
+ let imported_key = client
1206
+ .create_vault_key(
1207
+ &admin,
1208
+ KeyCreateRequest::Import {
1209
+ private_key_hex:
1210
+ "0x1111111111111111111111111111111111111111111111111111111111111111"
1211
+ .to_string(),
1212
+ },
1213
+ )
1214
+ .await
1215
+ .expect("imported key");
1216
+ let exported = client
1217
+ .export_vault_private_key(&admin, imported_key.id)
1218
+ .await
1219
+ .expect("export key");
1220
+ assert!(exported.is_some());
1221
+
1222
+ let relay_config = client
1223
+ .set_relay_config(
1224
+ &admin,
1225
+ Some("https://relay.example".to_string()),
1226
+ Some("https://frontend.example".to_string()),
1227
+ )
1228
+ .await
1229
+ .expect("set relay config");
1230
+ assert_eq!(relay_config.relay_url.as_deref(), Some("https://relay.example"));
1231
+ assert_eq!(
1232
+ relay_config.frontend_url.as_deref(),
1233
+ Some("https://frontend.example")
1234
+ );
1235
+
1236
+ let fetched_relay_config = client
1237
+ .get_relay_config(&admin)
1238
+ .await
1239
+ .expect("get relay config");
1240
+ assert_eq!(fetched_relay_config, relay_config);
1241
+
1242
+ let manual_policy = SpendingPolicy::new_manual_approval(
1243
+ 0,
1244
+ 1,
1245
+ 100,
1246
+ EntityScope::All,
1247
+ EntityScope::All,
1248
+ EntityScope::All,
1249
+ )
1250
+ .expect("manual policy");
1251
+ client
1252
+ .add_policy(&admin, manual_policy.clone())
1253
+ .await
1254
+ .expect("add manual approval policy");
1255
+
1256
+ let vault_key = client
1257
+ .create_vault_key(&admin, KeyCreateRequest::Generate)
1258
+ .await
1259
+ .expect("vault key");
1260
+ let agent_credentials = client
1261
+ .create_agent_key(&admin, vault_key.id, PolicyAttachment::AllPolicies)
1262
+ .await
1263
+ .expect("agent credentials");
1264
+
1265
+ let request = sign_request(&agent_credentials, transfer_action(42));
1266
+ let explanation = client
1267
+ .explain_for_agent(request.clone())
1268
+ .await
1269
+ .expect("explain");
1270
+ assert!(
1271
+ explanation
1272
+ .evaluated_policy_ids
1273
+ .iter()
1274
+ .any(|entry| *entry == manual_policy.id),
1275
+ "manual approval policy must be part of explanation"
1276
+ );
1277
+
1278
+ let reservation = client
1279
+ .reserve_nonce(NonceReservationRequest {
1280
+ request_id: Uuid::new_v4(),
1281
+ agent_key_id: agent_credentials.agent_key.id,
1282
+ agent_auth_token: agent_credentials.auth_token.clone(),
1283
+ chain_id: 1,
1284
+ min_nonce: 0,
1285
+ exact_nonce: false,
1286
+ requested_at: OffsetDateTime::now_utc(),
1287
+ expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
1288
+ })
1289
+ .await
1290
+ .expect("reserve nonce");
1291
+ client
1292
+ .release_nonce(NonceReleaseRequest {
1293
+ request_id: Uuid::new_v4(),
1294
+ agent_key_id: agent_credentials.agent_key.id,
1295
+ agent_auth_token: agent_credentials.auth_token.clone(),
1296
+ reservation_id: reservation.reservation_id,
1297
+ requested_at: OffsetDateTime::now_utc(),
1298
+ expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
1299
+ })
1300
+ .await
1301
+ .expect("release nonce");
1302
+
1303
+ let approval_request_id = match client.sign_for_agent(request.clone()).await {
1304
+ Err(DaemonError::ManualApprovalRequired {
1305
+ approval_request_id, ..
1306
+ }) => approval_request_id,
1307
+ other => panic!("expected manual approval request, got {other:?}"),
1308
+ };
1309
+
1310
+ let approval_requests = client
1311
+ .list_manual_approval_requests(&admin)
1312
+ .await
1313
+ .expect("list manual approvals");
1314
+ assert!(approval_requests.iter().any(|item| item.id == approval_request_id));
1315
+
1316
+ let approved = client
1317
+ .decide_manual_approval_request(
1318
+ &admin,
1319
+ approval_request_id,
1320
+ ManualApprovalDecision::Approve,
1321
+ None,
1322
+ )
1323
+ .await
1324
+ .expect("approve manual approval");
1325
+ assert_eq!(approved.id, approval_request_id);
1326
+ assert_eq!(approved.status, ManualApprovalStatus::Approved);
1327
+ }
1328
+
1329
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1330
+ async fn xpc_endpoint_debug_and_code_sign_server_paths_are_exercised() {
1331
+ let daemon = test_daemon();
1332
+
1333
+ let root_gated = XpcDaemonServer::start_inmemory_with_code_sign_requirement(
1334
+ daemon.clone(),
1335
+ tokio::runtime::Handle::current(),
1336
+ "anchor apple",
1337
+ );
1338
+ if unsafe { libc::geteuid() } != 0 {
1339
+ assert!(matches!(root_gated, Err(XpcTransportError::RequiresRoot)));
1340
+ return;
1341
+ }
1342
+
1343
+ let server = root_gated.expect("root should start code-sign-gated server");
1344
+ let endpoint_debug = format!("{:?}", server.endpoint());
1345
+ assert!(endpoint_debug.contains("XpcEndpoint"));
1346
+ }
@@ -104,6 +104,7 @@ async fn e2e_policy_enforced_over_xpc() {
104
104
  agent_auth_token: agent_credentials.auth_token.clone(),
105
105
  chain_id: 1,
106
106
  min_nonce: 0,
107
+ exact_nonce: false,
107
108
  requested_at: OffsetDateTime::now_utc(),
108
109
  expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
109
110
  })
@@ -338,6 +339,7 @@ async fn e2e_broadcast_gas_policy_enforced_over_xpc() {
338
339
  agent_auth_token: agent_credentials.auth_token.clone(),
339
340
  chain_id: 1,
340
341
  min_nonce: 0,
342
+ exact_nonce: false,
341
343
  requested_at: OffsetDateTime::now_utc(),
342
344
  expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
343
345
  })
@@ -384,6 +386,7 @@ async fn e2e_broadcast_gas_policy_enforced_over_xpc() {
384
386
  agent_auth_token: agent_credentials.auth_token.clone(),
385
387
  chain_id: 1,
386
388
  min_nonce: 1,
389
+ exact_nonce: false,
387
390
  requested_at: OffsetDateTime::now_utc(),
388
391
  expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
389
392
  })