@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
package/Cargo.lock CHANGED
@@ -3777,6 +3777,7 @@ name = "wlfi-agent-agent"
3777
3777
  version = "0.1.0"
3778
3778
  dependencies = [
3779
3779
  "anyhow",
3780
+ "async-trait",
3780
3781
  "clap",
3781
3782
  "hex",
3782
3783
  "libc",
@@ -3799,8 +3800,11 @@ name = "wlfi-agent-daemon"
3799
3800
  version = "0.1.0"
3800
3801
  dependencies = [
3801
3802
  "anyhow",
3803
+ "async-trait",
3804
+ "chacha20poly1305",
3802
3805
  "clap",
3803
3806
  "core-foundation",
3807
+ "hex",
3804
3808
  "libc",
3805
3809
  "nix",
3806
3810
  "reqwest",
@@ -3816,6 +3820,7 @@ dependencies = [
3816
3820
  "vault-domain",
3817
3821
  "vault-signer",
3818
3822
  "vault-transport-unix",
3823
+ "x25519-dalek",
3819
3824
  "zeroize",
3820
3825
  ]
3821
3826
 
package/README.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  WLFI Agentic SDK is a root-managed local signing daemon with policy enforcement, a single `wlfi-agent` CLI, and an optional relay + web approval flow.
4
4
 
5
+ ## Install
6
+
7
+ ### Prerequisites
8
+
9
+ - macOS
10
+ - Rust toolchain on `PATH` (`cargo`, `rustc`) with Rust `1.87.0` or newer
11
+ - Xcode Command Line Tools (`xcode-select --install`)
12
+
13
+ ### Install from npm
14
+
15
+ ```bash
16
+ npm i -g @wlfi-agent/cli
17
+ ```
18
+
19
+ `npm i -g @wlfi-agent/cli` builds the local Rust runtime during `postinstall`. If the prerequisites above are already installed, this is the normal one-step install path. If `cargo` or the macOS Command Line Tools are missing, installation fails immediately and tells you how to install the missing prerequisite before retrying.
20
+
21
+ ### Work from this repo
22
+
23
+ ```bash
24
+ pnpm install
25
+ npm run build
26
+ npm run install:cli-launcher
27
+ npm run install:rust-binaries
28
+ ```
29
+
30
+ On macOS, add `export PATH="$HOME/.wlfi_agent/bin:$PATH"` to `~/.zshrc`, then reload your shell with `source ~/.zshrc`.
31
+
32
+ On Linux, add `export PATH="$HOME/.wlfi_agent/bin:$PATH"` to your shell startup file such as `~/.bashrc`, `~/.zshrc`, or `~/.profile`, then reload that file or open a new shell.
33
+
34
+ `npm run install:cli-launcher` installs the `wlfi-agent` launcher into `~/.wlfi_agent/bin`, and `npm run install:rust-binaries` installs the Rust runtime into the same directory.
35
+
36
+ ### Reinstall Rust daemon
37
+
38
+ If you update Rust daemon code, rerun `npm run install:rust-binaries` so the root-managed daemon uses the new installed binaries under `~/.wlfi_agent/bin`.
39
+
40
+ ## Usage
41
+
5
42
  The main user path is:
6
43
 
7
44
  1. run `wlfi-agent admin setup`
@@ -15,6 +52,7 @@ User-facing examples below avoid shell env vars on purpose. Prefer prompts, conf
15
52
 
16
53
  - `wlfi-agent admin setup`
17
54
  - first-run setup
55
+ - `--reuse-existing-wallet` reattaches the current local vault when you need to recover the daemon or refresh local credentials without creating a fresh wallet
18
56
  - stores the vault password in macOS System Keychain
19
57
  - installs the root LaunchDaemon
20
58
  - creates a vault key + agent key
@@ -41,49 +79,36 @@ User-facing examples below avoid shell env vars on purpose. Prefer prompts, conf
41
79
  - `wlfi-agent daemon`
42
80
  - not a user entrypoint; daemon lifecycle is managed by `wlfi-agent admin setup`
43
81
 
44
- ## Install
45
-
46
- ### Prerequisites
47
-
48
- - macOS
49
- - Rust toolchain on `PATH` (`cargo`, `rustc`) with Rust `1.87.0` or newer
50
- - Xcode Command Line Tools (`xcode-select --install`)
82
+ ## Easiest wallet setup
51
83
 
52
- ### Install from npm
84
+ Run this once:
53
85
 
54
86
  ```bash
55
- npm i -g @wlfi-agent/cli
87
+ wlfi-agent admin setup
56
88
  ```
57
89
 
58
- `npm i -g @wlfi-agent/cli` builds the local Rust runtime during `postinstall`. If the prerequisites above are already installed, this is the normal one-step install path. If `cargo` or the macOS Command Line Tools are missing, installation fails immediately and tells you how to install the missing prerequisite before retrying.
59
-
60
- ### Work from this repo
90
+ Preview the exact sanitized setup plan first:
61
91
 
62
92
  ```bash
63
- pnpm install
64
- npm run build
65
- npm run install:rust-binaries
93
+ wlfi-agent admin setup --plan
66
94
  ```
67
95
 
68
- If you update Rust daemon code, rerun `npm run install:rust-binaries` so the root-managed daemon uses the new installed binaries under `~/.wlfi_agent/bin`.
69
-
70
- ## Easiest wallet setup
96
+ The preview is read-only. It does not prompt for the vault password, does not touch sudo, and does not mutate wallet or policy state. It prints the planned Rust command, trust preflight results, overwrite risk, and the password transport mode that would be used for the real setup.
71
97
 
72
- Run this once:
98
+ During a real `wlfi-agent admin setup`, you may be prompted for two different secrets:
73
99
 
74
- ```bash
75
- wlfi-agent admin setup
76
- ```
100
+ - `Vault password`: the wallet password you choose for encrypted local state
101
+ - `macOS admin password for sudo`: your macOS login/admin password, used only when setup needs elevated privileges to install or recover the root LaunchDaemon
77
102
 
78
- Preview the exact sanitized setup plan first:
103
+ If the local vault already exists and you only need to recover the managed daemon or refresh local setup state, reuse the current wallet instead of creating a fresh one:
79
104
 
80
105
  ```bash
81
- wlfi-agent admin setup --plan
106
+ wlfi-agent admin setup --reuse-existing-wallet
82
107
  ```
83
108
 
84
- The preview is read-only. It does not prompt for the vault password, does not touch sudo, and does not mutate wallet or policy state. It prints the planned Rust command, trust preflight results, overwrite risk, and the password transport mode that would be used for the real setup.
109
+ This reuse path keeps the current vault address, prompts for `REUSE` in interactive mode, and still requires `--yes` in non-interactive mode.
85
110
 
86
- You will be prompted for the vault password. The command:
111
+ After that, the command:
87
112
 
88
113
  - installs or refreshes the root daemon
89
114
  - waits for the daemon to come up
@@ -372,10 +397,18 @@ When relay is configured and a request requires manual approval:
372
397
  3. the frontend encrypts the vault password + decision to the daemon’s advertised X25519 public key
373
398
  4. the relay queues the encrypted update
374
399
  5. the daemon polls, decrypts, applies the decision, and reports status back
375
- 6. the original request can be retried and signed
400
+ 6. for `wlfi-agent transfer --broadcast`, `wlfi-agent transfer-native --broadcast`, and `wlfi-agent approve --broadcast`, the original CLI command keeps waiting on that same approval request and continues automatically after approval
401
+ 7. commands outside those broadcast flows still print the approval details and exit, so operators can approve or reject the request separately
376
402
 
377
403
  If the frontend link is unavailable, operators can always fall back to the local admin CLI approval command printed by the agent CLI.
378
404
 
405
+ For the auto-waiting broadcast flows above:
406
+
407
+ - do not rerun the original command after approving in the browser
408
+ - the CLI polls every 2 seconds for up to 5 minutes
409
+ - if the daemon returns a different approval request id while waiting, the CLI stops and tells you to inspect the approval status before rerunning
410
+ - approval details and waiting events go to `stderr`; the final successful `--json` result still goes to `stdout`
411
+
379
412
  ## Operational notes
380
413
 
381
414
  - The daemon state file lives at `/var/db/wlfi-agent/daemon-state.enc` and is intended to be root-only.
@@ -438,7 +471,7 @@ wlfi-agent admin tui
438
471
  wlfi-agent admin uninstall
439
472
  wlfi-agent admin get-relay-config
440
473
  wlfi-agent admin list-manual-approval-requests
441
- wlfi-agent wallet status
474
+ wlfi-agent wallet
442
475
  npm run install:rust-binaries
443
476
  pnpm build
444
477
  pnpm typecheck
@@ -62,9 +62,11 @@ fn read_secret_from_reader(mut reader: impl Read, label: &str) -> Result<String>
62
62
  #[cfg(test)]
63
63
  mod tests {
64
64
  use super::{
65
- ensure_output_parent, read_secret_from_reader, resolve_vault_password, validate_password,
65
+ emit_output, ensure_output_parent, print_status, read_secret_from_reader,
66
+ resolve_output_target, resolve_vault_password, should_print_status, validate_password,
66
67
  write_output_file,
67
68
  };
69
+ use crate::{OutputFormat, OutputTarget};
68
70
  use std::fs;
69
71
  use std::io::Cursor;
70
72
  use std::path::PathBuf;
@@ -95,12 +97,57 @@ mod tests {
95
97
  assert!(err.to_string().contains("must not exceed"));
96
98
  }
97
99
 
100
+ #[test]
101
+ fn read_secret_from_reader_trims_newlines_and_accepts_valid_secret() {
102
+ let secret =
103
+ read_secret_from_reader(Cursor::new(b"vault-secret\r\n".to_vec()), "vault password")
104
+ .expect("valid stdin secret");
105
+ assert_eq!(secret, "vault-secret");
106
+ }
107
+
108
+ #[test]
109
+ fn validate_password_accepts_non_empty_secret() {
110
+ let password = validate_password("vault-secret".to_string(), "prompt")
111
+ .expect("valid password");
112
+ assert_eq!(password, "vault-secret");
113
+ }
114
+
98
115
  #[test]
99
116
  fn resolve_vault_password_requires_stdin_in_non_interactive_mode() {
100
117
  let err = resolve_vault_password(false, true).expect_err("must fail");
101
118
  assert!(err.to_string().contains("use --vault-password-stdin"));
102
119
  }
103
120
 
121
+ #[test]
122
+ fn resolve_output_target_covers_stdout_and_file_paths() {
123
+ let stdout_target = resolve_output_target(None, false).expect("default stdout");
124
+ assert!(matches!(stdout_target, OutputTarget::Stdout));
125
+
126
+ let file_target = resolve_output_target(Some(PathBuf::from("out.json")), false)
127
+ .expect("file output target");
128
+ assert!(matches!(
129
+ file_target,
130
+ OutputTarget::File {
131
+ path,
132
+ overwrite: false
133
+ } if path == PathBuf::from("out.json")
134
+ ));
135
+
136
+ let err = resolve_output_target(Some(PathBuf::from("-")), true).expect_err("must fail");
137
+ assert!(err.to_string().contains("--overwrite cannot be used with --output -"));
138
+ }
139
+
140
+ #[test]
141
+ fn emit_output_to_stdout_and_print_status_cover_text_paths() {
142
+ emit_output("stdout output", &OutputTarget::Stdout).expect("stdout output");
143
+ assert!(should_print_status(OutputFormat::Text, false));
144
+ assert!(!should_print_status(OutputFormat::Json, false));
145
+ assert!(!should_print_status(OutputFormat::Text, true));
146
+ print_status("status output", OutputFormat::Text, false);
147
+ print_status("quiet output", OutputFormat::Text, true);
148
+ print_status("json output", OutputFormat::Json, false);
149
+ }
150
+
104
151
  #[test]
105
152
  #[cfg(unix)]
106
153
  fn ensure_output_parent_rejects_symlinked_parent_directory() {
@@ -188,6 +235,46 @@ mod tests {
188
235
  fs::remove_dir_all(&root).expect("cleanup temp tree");
189
236
  }
190
237
 
238
+ #[test]
239
+ #[cfg(unix)]
240
+ fn ensure_output_parent_rejects_directory_path() {
241
+ use std::os::unix::fs::PermissionsExt;
242
+
243
+ let root = temp_path("vault-cli-admin-output-dir-path");
244
+ fs::create_dir_all(&root).expect("create root");
245
+ fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
246
+ .expect("secure root permissions");
247
+
248
+ let err = ensure_output_parent(&root).expect_err("directory path must fail");
249
+ assert!(err
250
+ .to_string()
251
+ .contains("is a directory; provide a file path"));
252
+
253
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
254
+ }
255
+
256
+ #[test]
257
+ #[cfg(unix)]
258
+ fn ensure_output_parent_allows_sticky_world_writable_ancestor() {
259
+ use std::os::unix::fs::PermissionsExt;
260
+
261
+ let root = temp_path("vault-cli-admin-output-sticky-ancestor");
262
+ let shared = root.join("shared");
263
+ let nested = shared.join("nested");
264
+ fs::create_dir_all(&nested).expect("create nested directory");
265
+ fs::set_permissions(&shared, fs::Permissions::from_mode(0o1777))
266
+ .expect("set sticky shared permissions");
267
+ fs::set_permissions(&nested, fs::Permissions::from_mode(0o700))
268
+ .expect("set secure nested permissions");
269
+
270
+ ensure_output_parent(&nested.join("output.json"))
271
+ .expect("sticky world-writable ancestor should be allowed");
272
+
273
+ fs::set_permissions(&shared, fs::Permissions::from_mode(0o700))
274
+ .expect("restore cleanup permissions");
275
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
276
+ }
277
+
191
278
  #[test]
192
279
  #[cfg(unix)]
193
280
  fn write_output_file_overwrite_replaces_existing_hard_link_instead_of_mutating_shared_inode() {
@@ -218,6 +305,67 @@ mod tests {
218
305
 
219
306
  fs::remove_dir_all(&root).expect("cleanup temp tree");
220
307
  }
308
+
309
+ #[test]
310
+ #[cfg(unix)]
311
+ fn write_output_file_creates_new_file_and_rejects_existing_without_overwrite() {
312
+ use std::os::unix::fs::PermissionsExt;
313
+
314
+ let root = temp_path("vault-cli-admin-output-create-file");
315
+ fs::create_dir_all(&root).expect("create root directory");
316
+ fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
317
+ .expect("secure root directory permissions");
318
+
319
+ let output_path = root.join("output.json");
320
+ write_output_file(&output_path, "created", false).expect("write new output file");
321
+ assert_eq!(
322
+ fs::read_to_string(&output_path).expect("read created output"),
323
+ "created\n"
324
+ );
325
+ assert_eq!(
326
+ fs::metadata(&output_path)
327
+ .expect("metadata")
328
+ .permissions()
329
+ .mode()
330
+ & 0o777,
331
+ 0o600
332
+ );
333
+
334
+ let err = write_output_file(&output_path, "second", false)
335
+ .expect_err("existing file without overwrite must fail");
336
+ assert!(err
337
+ .to_string()
338
+ .contains("already exists; pass --overwrite to replace it"));
339
+
340
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
341
+ }
342
+
343
+ #[test]
344
+ #[cfg(unix)]
345
+ fn emit_output_to_file_target_writes_expected_content() {
346
+ use std::os::unix::fs::PermissionsExt;
347
+
348
+ let root = temp_path("vault-cli-admin-emit-output-file");
349
+ fs::create_dir_all(&root).expect("create root directory");
350
+ fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
351
+ .expect("secure root directory permissions");
352
+
353
+ let output_path = root.join("output.json");
354
+ emit_output(
355
+ "file output",
356
+ &OutputTarget::File {
357
+ path: output_path.clone(),
358
+ overwrite: false,
359
+ },
360
+ )
361
+ .expect("emit output to file");
362
+ assert_eq!(
363
+ fs::read_to_string(&output_path).expect("read emitted file"),
364
+ "file output\n"
365
+ );
366
+
367
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
368
+ }
221
369
  }
222
370
 
223
371
  pub(crate) fn resolve_output_format(