@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
@@ -1232,21 +1232,33 @@ impl KeyManagerDaemonApi for UnixDaemonClient {
1232
1232
  #[cfg(test)]
1233
1233
  mod tests {
1234
1234
  use super::{
1235
- assert_root_owned_daemon_socket_path, combined_allowed_peer_euids, ensure_socket_parent,
1236
- handle_connection, read_frame, rpc_access_level, socket_mode_for_allowed_peer_euids,
1235
+ assert_root_owned_daemon_socket_path, assert_trusted_daemon_socket_path,
1236
+ combined_allowed_peer_euids, ensure_socket_parent, handle_connection, read_frame,
1237
+ remove_existing_socket_file, rpc_access_level, socket_mode_for_allowed_peer_euids,
1237
1238
  write_frame, RpcAccessLevel, UnixDaemonClient, UnixDaemonServer, UnixTransportError,
1238
- WireRequest, WireResponse,
1239
+ WireDaemonError, WireRequest, WireResponse, MAX_WIRE_BODY_BYTES,
1239
1240
  };
1241
+ use serde_json::to_vec;
1240
1242
  use std::collections::BTreeSet;
1241
1243
  use std::path::Path;
1242
1244
  use std::sync::Arc;
1243
1245
  use std::time::Duration;
1244
1246
  use time::OffsetDateTime;
1247
+ use tokio::io::AsyncWriteExt;
1245
1248
  use tokio::net::UnixStream;
1246
1249
  use uuid::Uuid;
1247
- use vault_daemon::{DaemonConfig, InMemoryDaemon, KeyManagerDaemonApi};
1248
- use vault_domain::{AgentAction, SignRequest};
1250
+ use vault_daemon::{
1251
+ DaemonConfig, DaemonError, DaemonRpcRequest, DaemonRpcResponse, KeyManagerDaemonApi,
1252
+ InMemoryDaemon,
1253
+ };
1254
+ use vault_domain::{
1255
+ AdminSession, AgentAction, AgentCredentials, EntityScope, ManualApprovalStatus,
1256
+ NonceReleaseRequest, NonceReservationRequest, PolicyAttachment, PolicyType, RelayConfig,
1257
+ SignRequest, SpendingPolicy,
1258
+ };
1259
+ use vault_policy::{PolicyDecision, PolicyError};
1249
1260
  use vault_signer::SoftwareSignerBackend;
1261
+ use vault_signer::{KeyCreateRequest, SignerError};
1250
1262
 
1251
1263
  fn unique_socket_path(label: &str) -> std::path::PathBuf {
1252
1264
  std::env::temp_dir().join(format!(
@@ -1257,6 +1269,19 @@ mod tests {
1257
1269
  ))
1258
1270
  }
1259
1271
 
1272
+ fn short_test_root(label: &str) -> std::path::PathBuf {
1273
+ let base = if Path::new("/private/tmp").is_dir() {
1274
+ Path::new("/private/tmp")
1275
+ } else {
1276
+ Path::new("/tmp")
1277
+ };
1278
+ base.join(format!(
1279
+ "w-{}-{}",
1280
+ label,
1281
+ &uuid::Uuid::new_v4().simple().to_string()[..6]
1282
+ ))
1283
+ }
1284
+
1260
1285
  fn singleton_allowed_set(euid: u32) -> BTreeSet<u32> {
1261
1286
  let mut set = BTreeSet::new();
1262
1287
  set.insert(euid);
@@ -1281,6 +1306,97 @@ mod tests {
1281
1306
  }
1282
1307
  }
1283
1308
 
1309
+ fn policy_all_per_tx(max_amount_wei: u128) -> SpendingPolicy {
1310
+ SpendingPolicy::new(
1311
+ 0,
1312
+ PolicyType::PerTxMaxSpending,
1313
+ max_amount_wei,
1314
+ EntityScope::All,
1315
+ EntityScope::All,
1316
+ EntityScope::All,
1317
+ )
1318
+ .expect("policy")
1319
+ }
1320
+
1321
+ fn admin_session(lease: vault_domain::Lease) -> AdminSession {
1322
+ AdminSession {
1323
+ vault_password: "vault-password".to_string(),
1324
+ lease,
1325
+ }
1326
+ }
1327
+
1328
+ fn sign_request(credentials: &AgentCredentials, amount_wei: u128) -> SignRequest {
1329
+ let action = AgentAction::TransferNative {
1330
+ chain_id: 1,
1331
+ to: "0x2222222222222222222222222222222222222222"
1332
+ .parse()
1333
+ .expect("recipient"),
1334
+ amount_wei,
1335
+ };
1336
+ let now = OffsetDateTime::now_utc();
1337
+ SignRequest {
1338
+ request_id: Uuid::new_v4(),
1339
+ agent_key_id: credentials.agent_key.id,
1340
+ agent_auth_token: credentials.auth_token.clone(),
1341
+ payload: to_vec(&action).expect("serialize action"),
1342
+ action,
1343
+ requested_at: now,
1344
+ expires_at: now + time::Duration::minutes(2),
1345
+ }
1346
+ }
1347
+
1348
+ fn nonce_reservation_request(
1349
+ credentials: &AgentCredentials,
1350
+ min_nonce: u64,
1351
+ ) -> NonceReservationRequest {
1352
+ let now = OffsetDateTime::now_utc();
1353
+ NonceReservationRequest {
1354
+ request_id: Uuid::new_v4(),
1355
+ agent_key_id: credentials.agent_key.id,
1356
+ agent_auth_token: credentials.auth_token.clone(),
1357
+ chain_id: 1,
1358
+ min_nonce,
1359
+ exact_nonce: false,
1360
+ requested_at: now,
1361
+ expires_at: now + time::Duration::minutes(2),
1362
+ }
1363
+ }
1364
+
1365
+ fn nonce_release_request(
1366
+ credentials: &AgentCredentials,
1367
+ reservation_id: Uuid,
1368
+ ) -> NonceReleaseRequest {
1369
+ let now = OffsetDateTime::now_utc();
1370
+ NonceReleaseRequest {
1371
+ request_id: Uuid::new_v4(),
1372
+ agent_key_id: credentials.agent_key.id,
1373
+ agent_auth_token: credentials.auth_token.clone(),
1374
+ reservation_id,
1375
+ requested_at: now,
1376
+ expires_at: now + time::Duration::minutes(2),
1377
+ }
1378
+ }
1379
+
1380
+ async fn spawn_one_shot_server(
1381
+ socket_path: std::path::PathBuf,
1382
+ response: WireResponse,
1383
+ ) -> tokio::task::JoinHandle<()> {
1384
+ if let Some(parent) = socket_path.parent() {
1385
+ std::fs::create_dir_all(parent).expect("create server parent");
1386
+ }
1387
+ let _ = std::fs::remove_file(&socket_path);
1388
+ let listener = tokio::net::UnixListener::bind(&socket_path).expect("bind fake server");
1389
+ tokio::spawn(async move {
1390
+ let (mut stream, _) = listener.accept().await.expect("accept");
1391
+ let _: WireRequest = read_frame(&mut stream, Duration::from_secs(2))
1392
+ .await
1393
+ .expect("read request");
1394
+ write_frame(&mut stream, &response, Duration::from_secs(2))
1395
+ .await
1396
+ .expect("write response");
1397
+ })
1398
+ }
1399
+
1284
1400
  #[cfg(unix)]
1285
1401
  fn socket_mode(path: &Path) -> u32 {
1286
1402
  use std::os::unix::fs::PermissionsExt;
@@ -1361,6 +1477,114 @@ mod tests {
1361
1477
  std::fs::remove_dir_all(&root).expect("cleanup");
1362
1478
  }
1363
1479
 
1480
+ #[test]
1481
+ #[cfg(unix)]
1482
+ fn assert_trusted_daemon_socket_path_accepts_current_user_socket_and_rejects_bad_parents() {
1483
+ use std::os::unix::fs::symlink;
1484
+ use std::os::unix::net::UnixListener as StdUnixListener;
1485
+
1486
+ let err = assert_trusted_daemon_socket_path(Path::new(""))
1487
+ .expect_err("empty path must be rejected");
1488
+ assert!(err.contains("must not be empty"));
1489
+
1490
+ let err = assert_trusted_daemon_socket_path(Path::new("daemon.sock")).expect_err("must reject");
1491
+ assert!(err.contains("must have a parent directory"));
1492
+
1493
+ let root = short_test_root("trusted");
1494
+ let real_dir = root.join("real");
1495
+ std::fs::create_dir_all(&real_dir).expect("create real dir");
1496
+ let socket_path = real_dir.join("daemon.sock");
1497
+ let _listener = StdUnixListener::bind(&socket_path).expect("bind socket");
1498
+ assert_eq!(
1499
+ assert_trusted_daemon_socket_path(&socket_path).expect("trusted socket"),
1500
+ socket_path
1501
+ );
1502
+
1503
+ let linked_dir = root.join("linked");
1504
+ symlink(&real_dir, &linked_dir).expect("symlink dir");
1505
+ let err = assert_trusted_daemon_socket_path(&linked_dir.join("daemon.sock"))
1506
+ .expect_err("symlink parent must fail");
1507
+ assert!(err.contains("socket directory"));
1508
+ assert!(err.contains("must not be a symlink"));
1509
+
1510
+ std::fs::remove_file(root.join("linked")).expect("remove symlink");
1511
+ std::fs::remove_file(socket_path).expect("remove socket");
1512
+ std::fs::remove_dir_all(root).expect("cleanup");
1513
+ }
1514
+
1515
+ #[tokio::test(flavor = "current_thread")]
1516
+ async fn write_and_read_frame_cover_protocol_and_serialization_errors() {
1517
+ let (mut writer, mut reader) = UnixStream::pair().expect("stream pair");
1518
+ let message = WireRequest {
1519
+ body_json: "{\"ok\":true}".to_string(),
1520
+ };
1521
+ write_frame(&mut writer, &message, Duration::from_secs(2))
1522
+ .await
1523
+ .expect("write frame");
1524
+ let decoded: WireRequest = read_frame(&mut reader, Duration::from_secs(2))
1525
+ .await
1526
+ .expect("read frame");
1527
+ assert_eq!(decoded.body_json, message.body_json);
1528
+
1529
+ let oversized = WireRequest {
1530
+ body_json: "x".repeat(MAX_WIRE_BODY_BYTES + 1),
1531
+ };
1532
+ let err = write_frame(&mut writer, &oversized, Duration::from_secs(2))
1533
+ .await
1534
+ .expect_err("oversized write");
1535
+ assert!(matches!(err, UnixTransportError::Protocol(message) if message.contains("wire body exceeds max bytes")));
1536
+
1537
+ let (mut invalid_writer, mut invalid_reader) = UnixStream::pair().expect("stream pair");
1538
+ invalid_writer
1539
+ .write_all(&((MAX_WIRE_BODY_BYTES as u32) + 1).to_be_bytes())
1540
+ .await
1541
+ .expect("write length");
1542
+ let err = read_frame::<WireRequest>(&mut invalid_reader, Duration::from_secs(2))
1543
+ .await
1544
+ .expect_err("oversized read");
1545
+ assert!(matches!(err, UnixTransportError::Protocol(message) if message.contains("wire frame exceeds max bytes")));
1546
+
1547
+ let (mut malformed_writer, mut malformed_reader) = UnixStream::pair().expect("stream pair");
1548
+ malformed_writer
1549
+ .write_all(&(4u32).to_be_bytes())
1550
+ .await
1551
+ .expect("write length");
1552
+ malformed_writer
1553
+ .write_all(b"nope")
1554
+ .await
1555
+ .expect("write body");
1556
+ let err = read_frame::<WireRequest>(&mut malformed_reader, Duration::from_secs(2))
1557
+ .await
1558
+ .expect_err("malformed read");
1559
+ assert!(matches!(err, UnixTransportError::Serialization(_)));
1560
+ }
1561
+
1562
+ #[test]
1563
+ #[cfg(unix)]
1564
+ fn remove_existing_socket_file_covers_missing_socket_and_non_socket_paths() {
1565
+ use std::os::unix::net::UnixListener as StdUnixListener;
1566
+
1567
+ let root = short_test_root("rm");
1568
+ std::fs::create_dir_all(&root).expect("create root");
1569
+
1570
+ let missing = root.join("missing.sock");
1571
+ remove_existing_socket_file(&missing).expect("missing is a noop");
1572
+
1573
+ let file = root.join("file.sock");
1574
+ std::fs::write(&file, "not a socket").expect("write file");
1575
+ let err = remove_existing_socket_file(&file).expect_err("non-socket path");
1576
+ assert!(err.contains("is not a unix socket"));
1577
+ std::fs::remove_file(&file).expect("remove file");
1578
+
1579
+ let socket = root.join("daemon.sock");
1580
+ let _listener = StdUnixListener::bind(&socket).expect("bind socket");
1581
+ drop(_listener);
1582
+ remove_existing_socket_file(&socket).expect("remove socket");
1583
+ assert!(!socket.exists());
1584
+
1585
+ std::fs::remove_dir_all(root).expect("cleanup");
1586
+ }
1587
+
1364
1588
  #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1365
1589
  async fn unix_round_trip_for_issue_lease() {
1366
1590
  let socket_path = unique_socket_path("lease");
@@ -1398,6 +1622,154 @@ mod tests {
1398
1622
  let _ = server_task.await;
1399
1623
  }
1400
1624
 
1625
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1626
+ async fn unix_round_trip_for_full_key_manager_api_surface() {
1627
+ let socket_path = unique_socket_path("full-surface");
1628
+ let daemon = Arc::new(
1629
+ InMemoryDaemon::new(
1630
+ "vault-password",
1631
+ SoftwareSignerBackend::default(),
1632
+ DaemonConfig::default(),
1633
+ )
1634
+ .expect("daemon"),
1635
+ );
1636
+ let server = UnixDaemonServer::bind(
1637
+ socket_path.clone(),
1638
+ singleton_allowed_set(nix::unistd::geteuid().as_raw()),
1639
+ )
1640
+ .await
1641
+ .expect("server");
1642
+ let server_task = tokio::spawn({
1643
+ let daemon = daemon.clone();
1644
+ async move {
1645
+ server
1646
+ .run_until_shutdown(daemon, async {
1647
+ std::future::pending::<()>().await;
1648
+ })
1649
+ .await
1650
+ }
1651
+ });
1652
+
1653
+ tokio::time::sleep(Duration::from_millis(50)).await;
1654
+ let client = UnixDaemonClient::new(socket_path.clone(), Duration::from_secs(2));
1655
+
1656
+ let lease = client.issue_lease("vault-password").await.expect("lease");
1657
+ let session = admin_session(lease);
1658
+
1659
+ let policy = policy_all_per_tx(100);
1660
+ client
1661
+ .add_policy(&session, policy.clone())
1662
+ .await
1663
+ .expect("add policy");
1664
+
1665
+ let policies = client.list_policies(&session).await.expect("list policies");
1666
+ assert_eq!(policies.len(), 1);
1667
+ assert!(policies[0].enabled);
1668
+
1669
+ let vault_key = client
1670
+ .create_vault_key(&session, KeyCreateRequest::Generate)
1671
+ .await
1672
+ .expect("create vault key");
1673
+ let exported = client
1674
+ .export_vault_private_key(&session, vault_key.id)
1675
+ .await
1676
+ .expect("export key");
1677
+ assert!(exported.is_some());
1678
+
1679
+ let mut credentials = client
1680
+ .create_agent_key(&session, vault_key.id, PolicyAttachment::AllPolicies)
1681
+ .await
1682
+ .expect("create agent key");
1683
+
1684
+ let evaluation = client
1685
+ .evaluate_for_agent(sign_request(&credentials, 1))
1686
+ .await
1687
+ .expect("evaluate");
1688
+ assert_eq!(evaluation.evaluated_policy_ids, vec![policy.id]);
1689
+
1690
+ let explanation = client
1691
+ .explain_for_agent(sign_request(&credentials, 1))
1692
+ .await
1693
+ .expect("explain");
1694
+ assert!(matches!(explanation.decision, PolicyDecision::Allow));
1695
+
1696
+ let signature = client
1697
+ .sign_for_agent(sign_request(&credentials, 1))
1698
+ .await
1699
+ .expect("sign");
1700
+ assert!(!signature.bytes.is_empty());
1701
+
1702
+ let reservation = client
1703
+ .reserve_nonce(nonce_reservation_request(&credentials, 7))
1704
+ .await
1705
+ .expect("reserve nonce");
1706
+ assert_eq!(reservation.nonce, 7);
1707
+ client
1708
+ .release_nonce(nonce_release_request(&credentials, reservation.reservation_id))
1709
+ .await
1710
+ .expect("release nonce");
1711
+
1712
+ let rotated_token = client
1713
+ .rotate_agent_auth_token(&session, credentials.agent_key.id)
1714
+ .await
1715
+ .expect("rotate token");
1716
+ credentials.auth_token = rotated_token;
1717
+
1718
+ let approvals = client
1719
+ .list_manual_approval_requests(&session)
1720
+ .await
1721
+ .expect("list approvals");
1722
+ assert!(approvals.is_empty());
1723
+
1724
+ let relay_config = client
1725
+ .set_relay_config(
1726
+ &session,
1727
+ Some("http://127.0.0.1:8787".to_string()),
1728
+ Some("https://relay.example".to_string()),
1729
+ )
1730
+ .await
1731
+ .expect("set relay config");
1732
+ assert_eq!(
1733
+ relay_config,
1734
+ RelayConfig {
1735
+ relay_url: Some("http://127.0.0.1:8787".to_string()),
1736
+ frontend_url: Some("https://relay.example".to_string()),
1737
+ daemon_id_hex: relay_config.daemon_id_hex.clone(),
1738
+ daemon_public_key_hex: relay_config.daemon_public_key_hex.clone(),
1739
+ }
1740
+ );
1741
+ let current_relay_config = client
1742
+ .get_relay_config(&session)
1743
+ .await
1744
+ .expect("get relay config");
1745
+ assert_eq!(current_relay_config.relay_url, relay_config.relay_url);
1746
+ assert_eq!(current_relay_config.frontend_url, relay_config.frontend_url);
1747
+
1748
+ client
1749
+ .disable_policy(&session, policy.id)
1750
+ .await
1751
+ .expect("disable policy");
1752
+ let disabled = client.list_policies(&session).await.expect("list disabled");
1753
+ assert_eq!(disabled.len(), 1);
1754
+ assert!(!disabled[0].enabled);
1755
+
1756
+ client
1757
+ .revoke_agent_key(&session, credentials.agent_key.id)
1758
+ .await
1759
+ .expect("revoke agent");
1760
+ let err = client
1761
+ .sign_for_agent(sign_request(&credentials, 1))
1762
+ .await
1763
+ .expect_err("revoked agent must fail");
1764
+ assert!(matches!(
1765
+ err,
1766
+ DaemonError::UnknownAgentKey(id) if id == credentials.agent_key.id
1767
+ ) || matches!(err, DaemonError::AgentAuthenticationFailed));
1768
+
1769
+ server_task.abort();
1770
+ let _ = server_task.await;
1771
+ }
1772
+
1401
1773
  #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1402
1774
  async fn client_rejects_unexpected_daemon_euid() {
1403
1775
  let socket_path = unique_socket_path("peer-euid");
@@ -1445,6 +1817,74 @@ mod tests {
1445
1817
  let _ = server_task.await;
1446
1818
  }
1447
1819
 
1820
+ #[tokio::test(flavor = "current_thread")]
1821
+ async fn call_rpc_surfaces_wire_daemon_errors_and_transport_fallbacks() {
1822
+ let socket_path = unique_socket_path("wire-error");
1823
+ let client = UnixDaemonClient::new(socket_path.clone(), Duration::from_secs(2));
1824
+
1825
+ let daemon_task = spawn_one_shot_server(
1826
+ socket_path.clone(),
1827
+ WireResponse {
1828
+ ok: false,
1829
+ body_json: serde_json::to_string(&WireDaemonError::UnknownLease)
1830
+ .expect("serialize daemon error"),
1831
+ },
1832
+ )
1833
+ .await;
1834
+ let err = client
1835
+ .call_rpc(DaemonRpcRequest::IssueLease {
1836
+ vault_password: "vault-password".to_string(),
1837
+ })
1838
+ .await
1839
+ .expect_err("daemon error");
1840
+ assert!(matches!(err, UnixTransportError::Daemon(DaemonError::UnknownLease)));
1841
+ daemon_task.await.expect("daemon task");
1842
+
1843
+ let transport_task = spawn_one_shot_server(
1844
+ socket_path,
1845
+ WireResponse {
1846
+ ok: false,
1847
+ body_json: "plain transport failure".to_string(),
1848
+ },
1849
+ )
1850
+ .await;
1851
+ let err = client
1852
+ .call_rpc(DaemonRpcRequest::IssueLease {
1853
+ vault_password: "vault-password".to_string(),
1854
+ })
1855
+ .await
1856
+ .expect_err("transport fallback");
1857
+ assert!(matches!(
1858
+ err,
1859
+ UnixTransportError::Daemon(DaemonError::Transport(message))
1860
+ if message == "plain transport failure"
1861
+ ));
1862
+ transport_task.await.expect("transport task");
1863
+ }
1864
+
1865
+ #[tokio::test(flavor = "current_thread")]
1866
+ async fn issue_lease_rejects_unexpected_response_types() {
1867
+ let socket_path = unique_socket_path("unexpected-response");
1868
+ let client = UnixDaemonClient::new(socket_path.clone(), Duration::from_secs(2));
1869
+ let response_task = spawn_one_shot_server(
1870
+ socket_path,
1871
+ WireResponse {
1872
+ ok: true,
1873
+ body_json: serde_json::to_string(&DaemonRpcResponse::Unit)
1874
+ .expect("serialize response"),
1875
+ },
1876
+ )
1877
+ .await;
1878
+
1879
+ let err = client
1880
+ .issue_lease("vault-password")
1881
+ .await
1882
+ .expect_err("wrong response type");
1883
+ assert!(matches!(err, DaemonError::Transport(message) if message == "unexpected response type"));
1884
+
1885
+ response_task.await.expect("response task");
1886
+ }
1887
+
1448
1888
  #[test]
1449
1889
  fn rpc_access_level_classifies_admin_and_agent_requests() {
1450
1890
  let admin_request = vault_daemon::DaemonRpcRequest::IssueLease {
@@ -1493,6 +1933,214 @@ mod tests {
1493
1933
  );
1494
1934
  }
1495
1935
 
1936
+ #[tokio::test(flavor = "current_thread")]
1937
+ async fn bind_rejects_empty_admin_and_agent_allowlists() {
1938
+ let socket_path = unique_socket_path("empty-allowlists");
1939
+ let err = match UnixDaemonServer::bind_with_allowed_peer_euids(
1940
+ socket_path.clone(),
1941
+ BTreeSet::new(),
1942
+ singleton_allowed_set(nix::unistd::geteuid().as_raw()),
1943
+ )
1944
+ .await
1945
+ {
1946
+ Ok(_) => panic!("empty admin allowlist must fail"),
1947
+ Err(err) => err,
1948
+ };
1949
+ assert!(matches!(err, UnixTransportError::Protocol(message) if message.contains("allowed admin peer euid set must not be empty")));
1950
+
1951
+ let err = match UnixDaemonServer::bind_with_allowed_peer_euids(
1952
+ socket_path,
1953
+ singleton_allowed_set(nix::unistd::geteuid().as_raw()),
1954
+ BTreeSet::new(),
1955
+ )
1956
+ .await
1957
+ {
1958
+ Ok(_) => panic!("empty agent allowlist must fail"),
1959
+ Err(err) => err,
1960
+ };
1961
+ assert!(matches!(err, UnixTransportError::Protocol(message) if message.contains("allowed agent peer euid set must not be empty")));
1962
+ }
1963
+
1964
+ #[tokio::test(flavor = "current_thread")]
1965
+ async fn run_until_shutdown_honors_ready_shutdown_and_exposes_socket_path() {
1966
+ let socket_path = unique_socket_path("ready-shutdown");
1967
+ let server = UnixDaemonServer::bind(
1968
+ socket_path.clone(),
1969
+ singleton_allowed_set(nix::unistd::geteuid().as_raw()),
1970
+ )
1971
+ .await
1972
+ .expect("server");
1973
+ assert_eq!(server.socket_path(), socket_path.as_path());
1974
+
1975
+ let daemon = Arc::new(
1976
+ InMemoryDaemon::new(
1977
+ "vault-password",
1978
+ SoftwareSignerBackend::default(),
1979
+ DaemonConfig::default(),
1980
+ )
1981
+ .expect("daemon"),
1982
+ );
1983
+ server
1984
+ .run_until_shutdown(daemon, async {})
1985
+ .await
1986
+ .expect("shutdown must succeed");
1987
+ }
1988
+
1989
+ #[test]
1990
+ fn wire_daemon_error_roundtrip_covers_all_variants() {
1991
+ let manual_approval_id = Uuid::new_v4();
1992
+ let unknown_vault_key = Uuid::new_v4();
1993
+ let unknown_agent_key = Uuid::new_v4();
1994
+ let unknown_policy = Uuid::new_v4();
1995
+ let unknown_approval = Uuid::new_v4();
1996
+ let unknown_reservation = Uuid::new_v4();
1997
+
1998
+ let cases = vec![
1999
+ (
2000
+ DaemonError::AuthenticationFailed,
2001
+ DaemonError::AuthenticationFailed.to_string(),
2002
+ ),
2003
+ (DaemonError::UnknownLease, DaemonError::UnknownLease.to_string()),
2004
+ (DaemonError::InvalidLease, DaemonError::InvalidLease.to_string()),
2005
+ (
2006
+ DaemonError::TooManyActiveLeases,
2007
+ DaemonError::TooManyActiveLeases.to_string(),
2008
+ ),
2009
+ (
2010
+ DaemonError::UnknownVaultKey(unknown_vault_key),
2011
+ DaemonError::UnknownVaultKey(unknown_vault_key).to_string(),
2012
+ ),
2013
+ (
2014
+ DaemonError::UnknownAgentKey(unknown_agent_key),
2015
+ DaemonError::UnknownAgentKey(unknown_agent_key).to_string(),
2016
+ ),
2017
+ (
2018
+ DaemonError::UnknownPolicy(unknown_policy),
2019
+ DaemonError::UnknownPolicy(unknown_policy).to_string(),
2020
+ ),
2021
+ (
2022
+ DaemonError::UnknownManualApprovalRequest(unknown_approval),
2023
+ DaemonError::UnknownManualApprovalRequest(unknown_approval).to_string(),
2024
+ ),
2025
+ (
2026
+ DaemonError::AgentAuthenticationFailed,
2027
+ DaemonError::AgentAuthenticationFailed.to_string(),
2028
+ ),
2029
+ (
2030
+ DaemonError::PayloadActionMismatch,
2031
+ DaemonError::PayloadActionMismatch.to_string(),
2032
+ ),
2033
+ (
2034
+ DaemonError::PayloadTooLarge { max_bytes: 1024 },
2035
+ DaemonError::PayloadTooLarge { max_bytes: 1024 }.to_string(),
2036
+ ),
2037
+ (
2038
+ DaemonError::InvalidRequestTimestamps,
2039
+ DaemonError::InvalidRequestTimestamps.to_string(),
2040
+ ),
2041
+ (DaemonError::RequestExpired, DaemonError::RequestExpired.to_string()),
2042
+ (
2043
+ DaemonError::RequestReplayDetected,
2044
+ DaemonError::RequestReplayDetected.to_string(),
2045
+ ),
2046
+ (
2047
+ DaemonError::InvalidPolicyAttachment("attachment".to_string()),
2048
+ DaemonError::InvalidPolicyAttachment("attachment".to_string()).to_string(),
2049
+ ),
2050
+ (
2051
+ DaemonError::InvalidNonceReservation("nonce".to_string()),
2052
+ DaemonError::InvalidNonceReservation("nonce".to_string()).to_string(),
2053
+ ),
2054
+ (
2055
+ DaemonError::UnknownNonceReservation(unknown_reservation),
2056
+ DaemonError::UnknownNonceReservation(unknown_reservation).to_string(),
2057
+ ),
2058
+ (
2059
+ DaemonError::MissingNonceReservation {
2060
+ chain_id: 1,
2061
+ nonce: 7,
2062
+ },
2063
+ DaemonError::MissingNonceReservation {
2064
+ chain_id: 1,
2065
+ nonce: 7,
2066
+ }
2067
+ .to_string(),
2068
+ ),
2069
+ (
2070
+ DaemonError::InvalidPolicy("policy".to_string()),
2071
+ DaemonError::InvalidPolicy("policy".to_string()).to_string(),
2072
+ ),
2073
+ (
2074
+ DaemonError::InvalidRelayConfig("relay".to_string()),
2075
+ DaemonError::InvalidRelayConfig("relay".to_string()).to_string(),
2076
+ ),
2077
+ (
2078
+ DaemonError::ManualApprovalRequired {
2079
+ approval_request_id: manual_approval_id,
2080
+ relay_url: Some("https://relay.example".to_string()),
2081
+ frontend_url: Some("https://frontend.example".to_string()),
2082
+ },
2083
+ DaemonError::ManualApprovalRequired {
2084
+ approval_request_id: manual_approval_id,
2085
+ relay_url: Some("https://relay.example".to_string()),
2086
+ frontend_url: Some("https://frontend.example".to_string()),
2087
+ }
2088
+ .to_string(),
2089
+ ),
2090
+ (
2091
+ DaemonError::ManualApprovalRejected {
2092
+ approval_request_id: manual_approval_id,
2093
+ },
2094
+ DaemonError::ManualApprovalRejected {
2095
+ approval_request_id: manual_approval_id,
2096
+ }
2097
+ .to_string(),
2098
+ ),
2099
+ (
2100
+ DaemonError::ManualApprovalRequestNotPending {
2101
+ approval_request_id: manual_approval_id,
2102
+ status: ManualApprovalStatus::Approved,
2103
+ },
2104
+ DaemonError::ManualApprovalRequestNotPending {
2105
+ approval_request_id: manual_approval_id,
2106
+ status: ManualApprovalStatus::Approved,
2107
+ }
2108
+ .to_string(),
2109
+ ),
2110
+ (
2111
+ DaemonError::Policy(PolicyError::NoAttachedPolicies),
2112
+ DaemonError::Policy(PolicyError::NoAttachedPolicies).to_string(),
2113
+ ),
2114
+ (
2115
+ DaemonError::Signer(SignerError::InvalidPrivateKey),
2116
+ DaemonError::Signer(SignerError::InvalidPrivateKey).to_string(),
2117
+ ),
2118
+ (
2119
+ DaemonError::PasswordHash("hash".to_string()),
2120
+ DaemonError::PasswordHash("hash".to_string()).to_string(),
2121
+ ),
2122
+ (
2123
+ DaemonError::InvalidConfig("config".to_string()),
2124
+ DaemonError::InvalidConfig("config".to_string()).to_string(),
2125
+ ),
2126
+ (
2127
+ DaemonError::Transport("transport".to_string()),
2128
+ DaemonError::Transport("transport".to_string()).to_string(),
2129
+ ),
2130
+ (
2131
+ DaemonError::Persistence("persistence".to_string()),
2132
+ DaemonError::Persistence("persistence".to_string()).to_string(),
2133
+ ),
2134
+ (DaemonError::LockPoisoned, DaemonError::LockPoisoned.to_string()),
2135
+ ];
2136
+
2137
+ for (original, expected_message) in cases {
2138
+ let wire = WireDaemonError::from(original);
2139
+ let recovered = wire.into_daemon_error();
2140
+ assert_eq!(recovered.to_string(), expected_message);
2141
+ }
2142
+ }
2143
+
1496
2144
  #[tokio::test(flavor = "current_thread")]
1497
2145
  async fn handle_connection_rejects_admin_requests_from_agent_only_peers() {
1498
2146
  let daemon = Arc::new(