@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
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
4
4
  use std::sync::Arc;
5
5
 
6
6
  use anyhow::{anyhow, bail, Context, Result};
7
+ use async_trait::async_trait;
7
8
  use clap::{Parser, ValueEnum};
8
9
  use vault_daemon::{DaemonConfig, InMemoryDaemon, PersistentStoreConfig};
9
10
  use vault_signer::{SecureEnclaveSignerBackend, SoftwareSignerBackend};
@@ -120,13 +121,47 @@ impl Drop for StateFileLock {
120
121
  }
121
122
  }
122
123
 
124
+ #[async_trait]
125
+ trait DaemonRuntime {
126
+ async fn run_secure_enclave(
127
+ &self,
128
+ daemon_socket: PathBuf,
129
+ allowed_peer_euids: AllowedPeerEuids,
130
+ vault_password: String,
131
+ state_file: PathBuf,
132
+ secure_enclave_label_prefix: String,
133
+ signer_backend_label: &'static str,
134
+ ) -> Result<()>;
135
+
136
+ async fn run_software(
137
+ &self,
138
+ daemon_socket: PathBuf,
139
+ allowed_peer_euids: AllowedPeerEuids,
140
+ vault_password: String,
141
+ state_file: PathBuf,
142
+ signer_backend_label: &'static str,
143
+ ) -> Result<()>;
144
+ }
145
+
146
+ struct RealDaemonRuntime;
147
+
123
148
  #[tokio::main]
124
149
  async fn main() -> Result<()> {
125
150
  let cli = Cli::parse();
126
- validate_signer_backend_runtime(cli.signer_backend)?;
127
151
  let mut vault_password = resolve_vault_password(cli.vault_password_stdin, cli.non_interactive)?;
128
- let state_file = resolve_state_file_path(cli.state_file)?;
129
- let daemon_socket = resolve_socket_path(cli.daemon_socket)?;
152
+ let runtime = RealDaemonRuntime;
153
+ let result = run_cli_with_runtime(cli, vault_password.clone(), &runtime).await;
154
+ vault_password.zeroize();
155
+ result
156
+ }
157
+
158
+ async fn run_cli_with_runtime<R>(cli: Cli, vault_password: String, runtime: &R) -> Result<()>
159
+ where
160
+ R: DaemonRuntime + ?Sized,
161
+ {
162
+ validate_signer_backend_runtime(cli.signer_backend)?;
163
+ let state_file = resolve_state_file_path(cli.state_file.clone())?;
164
+ let daemon_socket = resolve_socket_path(cli.daemon_socket.clone())?;
130
165
  let _state_lock = acquire_state_file_lock(&state_file)?;
131
166
 
132
167
  let allowed_peer_euids = resolve_allowed_peer_euids(
@@ -139,6 +174,71 @@ async fn main() -> Result<()> {
139
174
  "==> warning: --allow-client-euid grants both admin and agent access; prefer --allow-admin-euid and --allow-agent-euid"
140
175
  );
141
176
  }
177
+
178
+ dispatch_runtime(
179
+ cli,
180
+ vault_password,
181
+ state_file,
182
+ daemon_socket,
183
+ allowed_peer_euids,
184
+ runtime,
185
+ )
186
+ .await
187
+ }
188
+
189
+ async fn dispatch_runtime<R>(
190
+ cli: Cli,
191
+ vault_password: String,
192
+ state_file: PathBuf,
193
+ daemon_socket: PathBuf,
194
+ allowed_peer_euids: AllowedPeerEuids,
195
+ runtime: &R,
196
+ ) -> Result<()>
197
+ where
198
+ R: DaemonRuntime + ?Sized,
199
+ {
200
+ let signer_backend_label = relay_signer_backend_label(cli.signer_backend);
201
+ match cli.signer_backend {
202
+ SignerBackendKind::SecureEnclave => {
203
+ runtime
204
+ .run_secure_enclave(
205
+ daemon_socket,
206
+ allowed_peer_euids,
207
+ vault_password,
208
+ state_file,
209
+ cli.secure_enclave_label_prefix,
210
+ signer_backend_label,
211
+ )
212
+ .await
213
+ }
214
+ SignerBackendKind::Software => {
215
+ runtime
216
+ .run_software(
217
+ daemon_socket,
218
+ allowed_peer_euids,
219
+ vault_password,
220
+ state_file,
221
+ signer_backend_label,
222
+ )
223
+ .await
224
+ }
225
+ }
226
+ }
227
+
228
+ fn print_server_banner(daemon_socket: &Path, allowed_peer_euids: &AllowedPeerEuids) {
229
+ eprintln!(
230
+ "==> daemon listening on {} (allowed admin euid(s): {}; allowed agent euid(s): {})",
231
+ daemon_socket.display(),
232
+ format_allowed_euids(&allowed_peer_euids.admin),
233
+ format_allowed_euids(&allowed_peer_euids.agent)
234
+ );
235
+ eprintln!("==> press Ctrl+C to stop");
236
+ }
237
+
238
+ async fn bind_server(
239
+ daemon_socket: PathBuf,
240
+ allowed_peer_euids: &AllowedPeerEuids,
241
+ ) -> Result<UnixDaemonServer> {
142
242
  let server = UnixDaemonServer::bind_with_allowed_peer_euids(
143
243
  daemon_socket.clone(),
144
244
  allowed_peer_euids.admin.clone(),
@@ -151,62 +251,70 @@ async fn main() -> Result<()> {
151
251
  daemon_socket.display()
152
252
  )
153
253
  })?;
254
+ print_server_banner(&daemon_socket, allowed_peer_euids);
255
+ Ok(server)
256
+ }
154
257
 
155
- eprintln!(
156
- "==> daemon listening on {} (allowed admin euid(s): {}; allowed agent euid(s): {})",
157
- daemon_socket.display(),
158
- format_allowed_euids(&allowed_peer_euids.admin),
159
- format_allowed_euids(&allowed_peer_euids.agent)
160
- );
161
- eprintln!("==> press Ctrl+C to stop");
258
+ async fn run_bound_daemon<B>(
259
+ server: UnixDaemonServer,
260
+ daemon: Arc<InMemoryDaemon<B>>,
261
+ signer_backend_label: &'static str,
262
+ ) -> Result<()>
263
+ where
264
+ B: vault_signer::VaultSignerBackend + Send + Sync + 'static,
265
+ {
266
+ let relay_task = relay_sync::spawn_relay_sync_task(Arc::clone(&daemon), signer_backend_label);
267
+ server
268
+ .run_until_shutdown(daemon, async {
269
+ let _ = tokio::signal::ctrl_c().await;
270
+ relay_task.abort();
271
+ })
272
+ .await
273
+ .context("daemon server loop failed")
274
+ }
162
275
 
163
- let signer_backend_label = relay_signer_backend_label(cli.signer_backend);
164
- match cli.signer_backend {
165
- SignerBackendKind::SecureEnclave => {
166
- let daemon = Arc::new(
167
- InMemoryDaemon::new_with_persistent_store(
168
- &vault_password,
169
- SecureEnclaveSignerBackend::new(cli.secure_enclave_label_prefix),
170
- DaemonConfig::default(),
171
- PersistentStoreConfig::new(state_file),
172
- )
173
- .context("failed to initialize daemon")?,
174
- );
175
- let relay_task =
176
- relay_sync::spawn_relay_sync_task(Arc::clone(&daemon), signer_backend_label);
177
- vault_password.zeroize();
178
- server
179
- .run_until_shutdown(daemon, async {
180
- let _ = tokio::signal::ctrl_c().await;
181
- relay_task.abort();
182
- })
183
- .await
184
- .context("daemon server loop failed")?;
185
- }
186
- SignerBackendKind::Software => {
187
- let daemon = Arc::new(
188
- InMemoryDaemon::new_with_persistent_store(
189
- &vault_password,
190
- SoftwareSignerBackend::default(),
191
- DaemonConfig::default(),
192
- PersistentStoreConfig::new(state_file),
193
- )
194
- .context("failed to initialize daemon")?,
195
- );
196
- let relay_task =
197
- relay_sync::spawn_relay_sync_task(Arc::clone(&daemon), signer_backend_label);
198
- vault_password.zeroize();
199
- server
200
- .run_until_shutdown(daemon, async {
201
- let _ = tokio::signal::ctrl_c().await;
202
- relay_task.abort();
203
- })
204
- .await
205
- .context("daemon server loop failed")?;
206
- }
276
+ #[async_trait]
277
+ impl DaemonRuntime for RealDaemonRuntime {
278
+ async fn run_secure_enclave(
279
+ &self,
280
+ daemon_socket: PathBuf,
281
+ allowed_peer_euids: AllowedPeerEuids,
282
+ mut vault_password: String,
283
+ state_file: PathBuf,
284
+ secure_enclave_label_prefix: String,
285
+ signer_backend_label: &'static str,
286
+ ) -> Result<()> {
287
+ let server = bind_server(daemon_socket, &allowed_peer_euids).await?;
288
+ let daemon = InMemoryDaemon::new_with_persistent_store(
289
+ &vault_password,
290
+ SecureEnclaveSignerBackend::new(secure_enclave_label_prefix),
291
+ DaemonConfig::default(),
292
+ PersistentStoreConfig::new(state_file),
293
+ );
294
+ vault_password.zeroize();
295
+ let daemon = Arc::new(daemon.context("failed to initialize daemon")?);
296
+ run_bound_daemon(server, daemon, signer_backend_label).await
207
297
  }
208
298
 
209
- Ok(())
299
+ async fn run_software(
300
+ &self,
301
+ daemon_socket: PathBuf,
302
+ allowed_peer_euids: AllowedPeerEuids,
303
+ mut vault_password: String,
304
+ state_file: PathBuf,
305
+ signer_backend_label: &'static str,
306
+ ) -> Result<()> {
307
+ let server = bind_server(daemon_socket, &allowed_peer_euids).await?;
308
+ let daemon = InMemoryDaemon::new_with_persistent_store(
309
+ &vault_password,
310
+ SoftwareSignerBackend::default(),
311
+ DaemonConfig::default(),
312
+ PersistentStoreConfig::new(state_file),
313
+ );
314
+ vault_password.zeroize();
315
+ let daemon = Arc::new(daemon.context("failed to initialize daemon")?);
316
+ run_bound_daemon(server, daemon, signer_backend_label).await
317
+ }
210
318
  }
211
319
 
212
320
  fn relay_signer_backend_label(backend: SignerBackendKind) -> &'static str {
@@ -514,14 +622,148 @@ fn lock_path(path: &Path) -> PathBuf {
514
622
  #[cfg(test)]
515
623
  mod tests {
516
624
  use super::{
517
- ensure_file_parent, relay_signer_backend_label, resolve_allowed_peer_euids_with_sudo_uid,
518
- resolve_vault_password, validate_password, Cli, SignerBackendKind,
625
+ acquire_state_file_lock, default_socket_path, default_state_file_path, dispatch_runtime,
626
+ ensure_file_parent, format_allowed_euids, is_symlink, lock_path, read_secret_from_reader,
627
+ relay_signer_backend_label, resolve_allowed_peer_euids, resolve_allowed_peer_euids_with_sudo_uid,
628
+ resolve_socket_path, resolve_state_file_path, resolve_vault_password, run_cli_with_runtime,
629
+ validate_password, validate_signer_backend_runtime, wlfi_home_dir, AllowedPeerEuids, Cli,
630
+ DaemonRuntime, SignerBackendKind,
519
631
  };
632
+ use anyhow::{anyhow, Result};
633
+ use async_trait::async_trait;
520
634
  use clap::Parser;
521
635
  use std::collections::BTreeSet;
522
- use std::path::Path;
636
+ use std::io::{Cursor, Read};
637
+ use std::path::{Path, PathBuf};
638
+ use std::sync::{Mutex, OnceLock};
523
639
  use std::time::{SystemTime, UNIX_EPOCH};
524
640
 
641
+ fn env_lock() -> &'static Mutex<()> {
642
+ static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
643
+ ENV_LOCK.get_or_init(|| Mutex::new(()))
644
+ }
645
+
646
+ fn temp_path(prefix: &str) -> PathBuf {
647
+ std::env::temp_dir().join(format!(
648
+ "{prefix}-{}-{}",
649
+ std::process::id(),
650
+ SystemTime::now()
651
+ .duration_since(UNIX_EPOCH)
652
+ .expect("time")
653
+ .as_nanos()
654
+ ))
655
+ }
656
+
657
+ struct FailingReader;
658
+
659
+ impl Read for FailingReader {
660
+ fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
661
+ Err(std::io::Error::other("boom"))
662
+ }
663
+ }
664
+
665
+ #[derive(Debug, Clone, PartialEq, Eq)]
666
+ enum RuntimeCall {
667
+ SecureEnclave {
668
+ daemon_socket: PathBuf,
669
+ allowed_admin: BTreeSet<u32>,
670
+ allowed_agent: BTreeSet<u32>,
671
+ vault_password: String,
672
+ state_file: PathBuf,
673
+ secure_enclave_label_prefix: String,
674
+ signer_backend_label: &'static str,
675
+ },
676
+ Software {
677
+ daemon_socket: PathBuf,
678
+ allowed_admin: BTreeSet<u32>,
679
+ allowed_agent: BTreeSet<u32>,
680
+ vault_password: String,
681
+ state_file: PathBuf,
682
+ signer_backend_label: &'static str,
683
+ },
684
+ }
685
+
686
+ struct FakeRuntime {
687
+ calls: Mutex<Vec<RuntimeCall>>,
688
+ fail_message: Option<&'static str>,
689
+ }
690
+
691
+ #[async_trait]
692
+ impl DaemonRuntime for FakeRuntime {
693
+ async fn run_secure_enclave(
694
+ &self,
695
+ daemon_socket: PathBuf,
696
+ allowed_peer_euids: AllowedPeerEuids,
697
+ vault_password: String,
698
+ state_file: PathBuf,
699
+ secure_enclave_label_prefix: String,
700
+ signer_backend_label: &'static str,
701
+ ) -> Result<()> {
702
+ self.calls
703
+ .lock()
704
+ .expect("lock")
705
+ .push(RuntimeCall::SecureEnclave {
706
+ daemon_socket,
707
+ allowed_admin: allowed_peer_euids.admin,
708
+ allowed_agent: allowed_peer_euids.agent,
709
+ vault_password,
710
+ state_file,
711
+ secure_enclave_label_prefix,
712
+ signer_backend_label,
713
+ });
714
+ match self.fail_message {
715
+ Some(message) => Err(anyhow!(message)),
716
+ None => Ok(()),
717
+ }
718
+ }
719
+
720
+ async fn run_software(
721
+ &self,
722
+ daemon_socket: PathBuf,
723
+ allowed_peer_euids: AllowedPeerEuids,
724
+ vault_password: String,
725
+ state_file: PathBuf,
726
+ signer_backend_label: &'static str,
727
+ ) -> Result<()> {
728
+ self.calls
729
+ .lock()
730
+ .expect("lock")
731
+ .push(RuntimeCall::Software {
732
+ daemon_socket,
733
+ allowed_admin: allowed_peer_euids.admin,
734
+ allowed_agent: allowed_peer_euids.agent,
735
+ vault_password,
736
+ state_file,
737
+ signer_backend_label,
738
+ });
739
+ match self.fail_message {
740
+ Some(message) => Err(anyhow!(message)),
741
+ None => Ok(()),
742
+ }
743
+ }
744
+ }
745
+
746
+ fn sample_cli(root: &Path, signer_backend: SignerBackendKind) -> Cli {
747
+ Cli {
748
+ vault_password_stdin: false,
749
+ non_interactive: false,
750
+ state_file: Some(root.join("state").join("daemon-state.enc")),
751
+ daemon_socket: Some(root.join("socket").join("daemon.sock")),
752
+ secure_enclave_label_prefix: "com.wlfi.test".to_string(),
753
+ signer_backend,
754
+ allow_admin_euid: vec![11],
755
+ allow_agent_euid: vec![22],
756
+ allow_client_euid: vec![33],
757
+ }
758
+ }
759
+
760
+ fn test_runtime() -> tokio::runtime::Runtime {
761
+ tokio::runtime::Builder::new_current_thread()
762
+ .enable_all()
763
+ .build()
764
+ .expect("runtime")
765
+ }
766
+
525
767
  #[test]
526
768
  fn validate_password_rejects_oversized_non_stdin_secret() {
527
769
  let err = validate_password("a".repeat((16 * 1024) + 1), "argument or environment")
@@ -529,6 +771,12 @@ mod tests {
529
771
  assert!(err.to_string().contains("must not exceed"));
530
772
  }
531
773
 
774
+ #[test]
775
+ fn validate_password_rejects_whitespace_only() {
776
+ let err = validate_password(" \n\t ".to_string(), "stdin").expect_err("must fail");
777
+ assert!(err.to_string().contains("must not be empty or whitespace"));
778
+ }
779
+
532
780
  #[test]
533
781
  fn cli_rejects_inline_vault_password_argument() {
534
782
  let err = Cli::try_parse_from([
@@ -547,18 +795,30 @@ mod tests {
547
795
  assert!(err.to_string().contains("use --vault-password-stdin"));
548
796
  }
549
797
 
798
+ #[test]
799
+ fn read_secret_from_reader_trims_trailing_newlines() {
800
+ let secret = read_secret_from_reader(Cursor::new("vault-secret\r\n"), "vault password")
801
+ .expect("trimmed secret");
802
+ assert_eq!(secret, "vault-secret");
803
+ }
804
+
805
+ #[test]
806
+ fn read_secret_from_reader_rejects_blank_after_trimming() {
807
+ let err = read_secret_from_reader(Cursor::new(" \n"), "vault password")
808
+ .expect_err("must fail");
809
+ assert!(err.to_string().contains("must not be empty or whitespace"));
810
+ }
811
+
812
+ #[test]
813
+ fn read_secret_from_reader_propagates_io_errors() {
814
+ let err = read_secret_from_reader(FailingReader, "vault password").expect_err("must fail");
815
+ assert!(err.to_string().contains("failed to read vault password from stdin"));
816
+ }
817
+
550
818
  #[cfg(unix)]
551
819
  #[test]
552
820
  fn ensure_file_parent_accepts_current_user_owned_directory() {
553
- let unique = SystemTime::now()
554
- .duration_since(UNIX_EPOCH)
555
- .expect("time")
556
- .as_nanos();
557
- let parent = std::env::temp_dir().join(format!(
558
- "wlfi-daemon-parent-{}-{}",
559
- std::process::id(),
560
- unique
561
- ));
821
+ let parent = temp_path("wlfi-daemon-parent");
562
822
  std::fs::create_dir_all(&parent).expect("create parent");
563
823
  let path = parent.join("daemon-state.enc");
564
824
  ensure_file_parent(&path, "state").expect("current-user-owned directory should pass");
@@ -616,6 +876,13 @@ mod tests {
616
876
  assert_eq!(resolved.agent, BTreeSet::from([0, 22, 33]));
617
877
  }
618
878
 
879
+ #[test]
880
+ fn resolve_allowed_peer_euids_wrapper_matches_internal_helper() {
881
+ let resolved = resolve_allowed_peer_euids(&[7], &[8], &[9]).expect("allowed peer euids");
882
+ assert_eq!(resolved.admin, BTreeSet::from([0, 7, 9]));
883
+ assert_eq!(resolved.agent, BTreeSet::from([0, 8, 9]));
884
+ }
885
+
619
886
  #[test]
620
887
  fn relay_signer_backend_label_matches_runtime_backend() {
621
888
  assert_eq!(
@@ -628,6 +895,289 @@ mod tests {
628
895
  );
629
896
  }
630
897
 
898
+ #[test]
899
+ fn validate_signer_backend_runtime_accepts_software_everywhere() {
900
+ validate_signer_backend_runtime(SignerBackendKind::Software).expect("software backend");
901
+ }
902
+
903
+ #[cfg(not(target_os = "macos"))]
904
+ #[test]
905
+ fn validate_signer_backend_runtime_rejects_secure_enclave_off_macos() {
906
+ let err = validate_signer_backend_runtime(SignerBackendKind::SecureEnclave)
907
+ .expect_err("must fail");
908
+ assert!(err
909
+ .to_string()
910
+ .contains("Secure Enclave daemon mode is supported only on macOS"));
911
+ }
912
+
913
+ #[test]
914
+ fn format_allowed_euids_renders_sorted_csv() {
915
+ assert_eq!(format_allowed_euids(&BTreeSet::from([0, 11, 33])), "0,11,33");
916
+ }
917
+
918
+ #[test]
919
+ fn lock_path_appends_lock_suffix() {
920
+ assert_eq!(
921
+ lock_path(Path::new("/tmp/wlfi/daemon-state.enc")),
922
+ PathBuf::from("/tmp/wlfi/daemon-state.enc.lock")
923
+ );
924
+ }
925
+
926
+ #[test]
927
+ fn wlfi_home_dir_prefers_wlfi_home_and_falls_back_to_home() {
928
+ let _guard = env_lock().lock().expect("env lock");
929
+ let wlfi_home = temp_path("wlfi-home");
930
+ let home = temp_path("home-dir");
931
+
932
+ std::env::set_var("WLFI_HOME", &wlfi_home);
933
+ std::env::set_var("HOME", &home);
934
+ assert_eq!(wlfi_home_dir().expect("wlfi home"), wlfi_home);
935
+
936
+ std::env::remove_var("WLFI_HOME");
937
+ assert_eq!(
938
+ wlfi_home_dir().expect("home fallback"),
939
+ home.join(".wlfi_agent")
940
+ );
941
+
942
+ std::env::remove_var("HOME");
943
+ }
944
+
945
+ #[test]
946
+ fn wlfi_home_dir_rejects_empty_and_missing_env() {
947
+ let _guard = env_lock().lock().expect("env lock");
948
+
949
+ std::env::set_var("WLFI_HOME", "");
950
+ let err = wlfi_home_dir().expect_err("must reject empty WLFI_HOME");
951
+ assert!(err.to_string().contains("WLFI_HOME must not be empty"));
952
+
953
+ std::env::remove_var("WLFI_HOME");
954
+ std::env::remove_var("HOME");
955
+ let err = wlfi_home_dir().expect_err("must reject missing HOME");
956
+ assert!(err
957
+ .to_string()
958
+ .contains("HOME is not set; use WLFI_HOME to choose config directory"));
959
+ }
960
+
961
+ #[test]
962
+ fn default_paths_and_resolvers_use_wlfi_home() {
963
+ let _guard = env_lock().lock().expect("env lock");
964
+ let wlfi_home = temp_path("wlfi-daemon-defaults");
965
+ std::env::set_var("WLFI_HOME", &wlfi_home);
966
+
967
+ assert_eq!(
968
+ default_state_file_path().expect("default state"),
969
+ wlfi_home.join("daemon-state.enc")
970
+ );
971
+ assert_eq!(
972
+ default_socket_path().expect("default socket"),
973
+ wlfi_home.join("daemon.sock")
974
+ );
975
+ assert_eq!(
976
+ resolve_state_file_path(None).expect("resolved state"),
977
+ wlfi_home.join("daemon-state.enc")
978
+ );
979
+ assert_eq!(
980
+ resolve_socket_path(None).expect("resolved socket"),
981
+ wlfi_home.join("daemon.sock")
982
+ );
983
+
984
+ std::env::remove_var("WLFI_HOME");
985
+ }
986
+
987
+ #[test]
988
+ fn dispatch_runtime_routes_to_expected_backend() {
989
+ let runtime = test_runtime();
990
+ let root = temp_path("wlfi-daemon-dispatch");
991
+ let state_file = root.join("state.enc");
992
+ let daemon_socket = root.join("daemon.sock");
993
+ let allowed = AllowedPeerEuids {
994
+ admin: BTreeSet::from([0, 11, 33]),
995
+ agent: BTreeSet::from([0, 22, 33]),
996
+ };
997
+
998
+ let secure_runtime = FakeRuntime {
999
+ calls: Mutex::new(Vec::new()),
1000
+ fail_message: None,
1001
+ };
1002
+ runtime
1003
+ .block_on(dispatch_runtime(
1004
+ sample_cli(&root, SignerBackendKind::SecureEnclave),
1005
+ "vault-secret".to_string(),
1006
+ state_file.clone(),
1007
+ daemon_socket.clone(),
1008
+ allowed.clone(),
1009
+ &secure_runtime,
1010
+ ))
1011
+ .expect("secure enclave dispatch");
1012
+ assert_eq!(
1013
+ secure_runtime.calls.lock().expect("lock").as_slice(),
1014
+ &[RuntimeCall::SecureEnclave {
1015
+ daemon_socket: daemon_socket.clone(),
1016
+ allowed_admin: BTreeSet::from([0, 11, 33]),
1017
+ allowed_agent: BTreeSet::from([0, 22, 33]),
1018
+ vault_password: "vault-secret".to_string(),
1019
+ state_file: state_file.clone(),
1020
+ secure_enclave_label_prefix: "com.wlfi.test".to_string(),
1021
+ signer_backend_label: "secure-enclave",
1022
+ }]
1023
+ );
1024
+
1025
+ let software_runtime = FakeRuntime {
1026
+ calls: Mutex::new(Vec::new()),
1027
+ fail_message: None,
1028
+ };
1029
+ runtime
1030
+ .block_on(dispatch_runtime(
1031
+ sample_cli(&root, SignerBackendKind::Software),
1032
+ "vault-secret".to_string(),
1033
+ state_file.clone(),
1034
+ daemon_socket.clone(),
1035
+ allowed,
1036
+ &software_runtime,
1037
+ ))
1038
+ .expect("software dispatch");
1039
+ assert_eq!(
1040
+ software_runtime.calls.lock().expect("lock").as_slice(),
1041
+ &[RuntimeCall::Software {
1042
+ daemon_socket,
1043
+ allowed_admin: BTreeSet::from([0, 11, 33]),
1044
+ allowed_agent: BTreeSet::from([0, 22, 33]),
1045
+ vault_password: "vault-secret".to_string(),
1046
+ state_file,
1047
+ signer_backend_label: "software",
1048
+ }]
1049
+ );
1050
+ }
1051
+
1052
+ #[test]
1053
+ fn run_cli_with_runtime_resolves_paths_and_lock_before_invoking_runtime() {
1054
+ let runtime = test_runtime();
1055
+ let root = temp_path("wlfi-daemon-run-runtime");
1056
+ let cli = sample_cli(&root, SignerBackendKind::Software);
1057
+ let fake_runtime = FakeRuntime {
1058
+ calls: Mutex::new(Vec::new()),
1059
+ fail_message: None,
1060
+ };
1061
+
1062
+ runtime
1063
+ .block_on(run_cli_with_runtime(
1064
+ cli,
1065
+ "vault-secret".to_string(),
1066
+ &fake_runtime,
1067
+ ))
1068
+ .expect("runtime dispatch");
1069
+
1070
+ assert!(root.join("state").exists());
1071
+ assert!(root.join("socket").exists());
1072
+ assert!(root.join("state").join("daemon-state.enc.lock").exists());
1073
+ assert_eq!(fake_runtime.calls.lock().expect("lock").len(), 1);
1074
+
1075
+ std::fs::remove_dir_all(&root).expect("cleanup");
1076
+ }
1077
+
1078
+ #[cfg(not(target_os = "macos"))]
1079
+ #[test]
1080
+ fn run_cli_with_runtime_rejects_secure_enclave_before_runtime_invocation() {
1081
+ let runtime = test_runtime();
1082
+ let root = temp_path("wlfi-daemon-secure-enclave");
1083
+ let fake_runtime = FakeRuntime {
1084
+ calls: Mutex::new(Vec::new()),
1085
+ fail_message: None,
1086
+ };
1087
+
1088
+ let err = runtime
1089
+ .block_on(run_cli_with_runtime(
1090
+ sample_cli(&root, SignerBackendKind::SecureEnclave),
1091
+ "vault-secret".to_string(),
1092
+ &fake_runtime,
1093
+ ))
1094
+ .expect_err("secure enclave must fail");
1095
+ assert!(err
1096
+ .to_string()
1097
+ .contains("Secure Enclave daemon mode is supported only on macOS"));
1098
+ assert!(fake_runtime.calls.lock().expect("lock").is_empty());
1099
+ }
1100
+
1101
+ #[test]
1102
+ fn dispatch_runtime_bubbles_runtime_failures() {
1103
+ let runtime = test_runtime();
1104
+ let root = temp_path("wlfi-daemon-runtime-error");
1105
+ let err_runtime = FakeRuntime {
1106
+ calls: Mutex::new(Vec::new()),
1107
+ fail_message: Some("runtime boom"),
1108
+ };
1109
+
1110
+ let err = runtime
1111
+ .block_on(dispatch_runtime(
1112
+ sample_cli(&root, SignerBackendKind::Software),
1113
+ "vault-secret".to_string(),
1114
+ root.join("daemon-state.enc"),
1115
+ root.join("daemon.sock"),
1116
+ AllowedPeerEuids {
1117
+ admin: BTreeSet::from([0]),
1118
+ agent: BTreeSet::from([0]),
1119
+ },
1120
+ &err_runtime,
1121
+ ))
1122
+ .expect_err("runtime failure must bubble");
1123
+ assert!(err.to_string().contains("runtime boom"));
1124
+ }
1125
+
1126
+ #[test]
1127
+ fn explicit_state_and_socket_paths_are_preserved() {
1128
+ let root = temp_path("wlfi-daemon-explicit");
1129
+ let state = root.join("state").join("daemon-state.enc");
1130
+ let socket = root.join("sock").join("daemon.sock");
1131
+
1132
+ let resolved_state = resolve_state_file_path(Some(state.clone())).expect("state path");
1133
+ let resolved_socket = resolve_socket_path(Some(socket.clone())).expect("socket path");
1134
+ assert_eq!(resolved_state, state);
1135
+ assert_eq!(resolved_socket, socket);
1136
+ assert!(root.join("state").exists());
1137
+ assert!(root.join("sock").exists());
1138
+
1139
+ std::fs::remove_dir_all(&root).expect("cleanup");
1140
+ }
1141
+
1142
+ #[cfg(unix)]
1143
+ #[test]
1144
+ fn acquire_state_file_lock_creates_lock_file() {
1145
+ let root = temp_path("wlfi-daemon-lock");
1146
+ let state_path = root.join("daemon-state.enc");
1147
+
1148
+ let lock = acquire_state_file_lock(&state_path).expect("lock file");
1149
+ let lock_file = lock_path(&state_path);
1150
+ assert!(lock_file.exists());
1151
+ drop(lock);
1152
+
1153
+ std::fs::remove_dir_all(&root).expect("cleanup");
1154
+ }
1155
+
1156
+ #[cfg(unix)]
1157
+ #[test]
1158
+ fn ensure_file_parent_rejects_symlink_path_and_is_symlink_reports_it() {
1159
+ use std::os::unix::fs::symlink;
1160
+
1161
+ let root = temp_path("wlfi-daemon-symlink");
1162
+ std::fs::create_dir_all(&root).expect("create root");
1163
+ let target = root.join("real-state.enc");
1164
+ let link = root.join("linked-state.enc");
1165
+ std::fs::write(&target, "seed").expect("seed");
1166
+ symlink(&target, &link).expect("symlink");
1167
+
1168
+ assert!(is_symlink(&link).expect("symlink metadata"));
1169
+ let err = ensure_file_parent(&link, "state").expect_err("must reject symlink file");
1170
+ assert!(err.to_string().contains("must not be a symlink"));
1171
+
1172
+ std::fs::remove_dir_all(&root).expect("cleanup");
1173
+ }
1174
+
1175
+ #[test]
1176
+ fn is_symlink_returns_false_for_missing_path() {
1177
+ let missing = temp_path("wlfi-daemon-missing");
1178
+ assert!(!is_symlink(&missing).expect("missing path is not symlink"));
1179
+ }
1180
+
631
1181
  #[cfg(unix)]
632
1182
  #[test]
633
1183
  fn assert_allowed_directory_owner_rejects_non_root_owner_for_root_runtime() {