@wlfi-agent/cli 1.4.13 → 1.4.15

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 (289) hide show
  1. package/Cargo.lock +3968 -0
  2. package/Cargo.toml +50 -0
  3. package/README.md +426 -6
  4. package/crates/vault-cli-admin/Cargo.toml +26 -0
  5. package/crates/vault-cli-admin/src/io_utils.rs +500 -0
  6. package/crates/vault-cli-admin/src/main.rs +3990 -0
  7. package/crates/vault-cli-admin/src/shared_config.rs +624 -0
  8. package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
  9. package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
  10. package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
  11. package/crates/vault-cli-admin/src/tui.rs +3410 -0
  12. package/crates/vault-cli-agent/Cargo.toml +24 -0
  13. package/crates/vault-cli-agent/src/io_utils.rs +576 -0
  14. package/crates/vault-cli-agent/src/main.rs +833 -0
  15. package/crates/vault-cli-daemon/Cargo.toml +28 -0
  16. package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
  17. package/crates/vault-cli-daemon/src/main.rs +644 -0
  18. package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
  19. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
  20. package/crates/vault-daemon/Cargo.toml +32 -0
  21. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
  22. package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
  23. package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
  24. package/crates/vault-daemon/src/lib.rs +54 -0
  25. package/crates/vault-daemon/src/persistence.rs +441 -0
  26. package/crates/vault-daemon/src/tests.rs +237 -0
  27. package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
  28. package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
  29. package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
  30. package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
  31. package/crates/vault-domain/Cargo.toml +20 -0
  32. package/crates/vault-domain/src/action.rs +849 -0
  33. package/crates/vault-domain/src/address.rs +51 -0
  34. package/crates/vault-domain/src/approval.rs +90 -0
  35. package/crates/vault-domain/src/constants.rs +4 -0
  36. package/crates/vault-domain/src/error.rs +54 -0
  37. package/crates/vault-domain/src/keys.rs +71 -0
  38. package/crates/vault-domain/src/lib.rs +42 -0
  39. package/crates/vault-domain/src/nonce.rs +102 -0
  40. package/crates/vault-domain/src/policy.rs +172 -0
  41. package/crates/vault-domain/src/request.rs +53 -0
  42. package/crates/vault-domain/src/scope.rs +24 -0
  43. package/crates/vault-domain/src/session.rs +50 -0
  44. package/crates/vault-domain/src/signature.rs +34 -0
  45. package/crates/vault-domain/src/tests.rs +651 -0
  46. package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
  47. package/crates/vault-policy/Cargo.toml +17 -0
  48. package/crates/vault-policy/src/engine.rs +301 -0
  49. package/crates/vault-policy/src/error.rs +81 -0
  50. package/crates/vault-policy/src/lib.rs +17 -0
  51. package/crates/vault-policy/src/report.rs +34 -0
  52. package/crates/vault-policy/src/tests.rs +891 -0
  53. package/crates/vault-policy/src/tests_explain.rs +78 -0
  54. package/crates/vault-sdk-agent/Cargo.toml +21 -0
  55. package/crates/vault-sdk-agent/src/lib.rs +711 -0
  56. package/crates/vault-signer/Cargo.toml +25 -0
  57. package/crates/vault-signer/src/lib.rs +731 -0
  58. package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
  59. package/crates/vault-transport-unix/Cargo.toml +24 -0
  60. package/crates/vault-transport-unix/src/lib.rs +1640 -0
  61. package/crates/vault-transport-xpc/Cargo.toml +25 -0
  62. package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
  63. package/crates/vault-transport-xpc/src/lib.rs +680 -0
  64. package/crates/vault-transport-xpc/src/tests.rs +818 -0
  65. package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
  66. package/dist/cli.cjs +35088 -0
  67. package/dist/cli.cjs.map +1 -0
  68. package/package.json +45 -41
  69. package/packages/cache/.turbo/turbo-build.log +52 -0
  70. package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
  71. package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
  72. package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
  73. package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
  74. package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
  75. package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
  76. package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
  77. package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
  78. package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
  79. package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
  80. package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
  81. package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
  82. package/packages/cache/dist/client/index.cjs +11 -0
  83. package/packages/cache/dist/client/index.cjs.map +1 -0
  84. package/packages/cache/dist/client/index.d.cts +15 -0
  85. package/packages/cache/dist/client/index.d.ts +15 -0
  86. package/packages/cache/dist/client/index.js +11 -0
  87. package/packages/cache/dist/client/index.js.map +1 -0
  88. package/packages/cache/dist/errors/index.cjs +11 -0
  89. package/packages/cache/dist/errors/index.cjs.map +1 -0
  90. package/packages/cache/dist/errors/index.d.cts +26 -0
  91. package/packages/cache/dist/errors/index.d.ts +26 -0
  92. package/packages/cache/dist/errors/index.js +11 -0
  93. package/packages/cache/dist/errors/index.js.map +1 -0
  94. package/packages/cache/dist/index.cjs +29 -0
  95. package/packages/cache/dist/index.cjs.map +1 -0
  96. package/packages/cache/dist/index.d.cts +4 -0
  97. package/packages/cache/dist/index.d.ts +4 -0
  98. package/packages/cache/dist/index.js +29 -0
  99. package/packages/cache/dist/index.js.map +1 -0
  100. package/packages/cache/dist/service/index.cjs +15 -0
  101. package/packages/cache/dist/service/index.cjs.map +1 -0
  102. package/packages/cache/dist/service/index.d.cts +184 -0
  103. package/packages/cache/dist/service/index.d.ts +184 -0
  104. package/packages/cache/dist/service/index.js +15 -0
  105. package/packages/cache/dist/service/index.js.map +1 -0
  106. package/packages/cache/node_modules/.bin/jiti +17 -0
  107. package/packages/cache/node_modules/.bin/tsc +17 -0
  108. package/packages/cache/node_modules/.bin/tsserver +17 -0
  109. package/packages/cache/node_modules/.bin/tsup +17 -0
  110. package/packages/cache/node_modules/.bin/tsup-node +17 -0
  111. package/packages/cache/node_modules/.bin/tsx +17 -0
  112. package/packages/cache/node_modules/.bin/vitest +17 -0
  113. package/packages/cache/package.json +48 -0
  114. package/packages/cache/src/client/index.ts +56 -0
  115. package/packages/cache/src/errors/index.ts +53 -0
  116. package/packages/cache/src/index.ts +3 -0
  117. package/packages/cache/src/service/index.test.ts +263 -0
  118. package/packages/cache/src/service/index.ts +678 -0
  119. package/packages/cache/tsconfig.json +13 -0
  120. package/packages/cache/tsup.config.ts +13 -0
  121. package/packages/cache/vitest.config.ts +16 -0
  122. package/packages/config/.turbo/turbo-build.log +18 -0
  123. package/packages/config/dist/index.cjs +1037 -0
  124. package/packages/config/dist/index.cjs.map +1 -0
  125. package/packages/config/dist/index.d.ts +131 -0
  126. package/packages/config/node_modules/.bin/jiti +17 -0
  127. package/packages/config/node_modules/.bin/tsc +17 -0
  128. package/packages/config/node_modules/.bin/tsserver +17 -0
  129. package/packages/config/node_modules/.bin/tsup +17 -0
  130. package/packages/config/node_modules/.bin/tsup-node +17 -0
  131. package/packages/config/node_modules/.bin/tsx +17 -0
  132. package/packages/config/package.json +21 -0
  133. package/packages/config/src/index.js +1 -0
  134. package/packages/config/src/index.ts +1282 -0
  135. package/packages/config/tsconfig.json +4 -0
  136. package/packages/rpc/.turbo/turbo-build.log +32 -0
  137. package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
  138. package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
  139. package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
  140. package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
  141. package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
  142. package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
  143. package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
  144. package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
  145. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
  146. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
  147. package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
  148. package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
  149. package/packages/rpc/dist/index.cjs +7342 -0
  150. package/packages/rpc/dist/index.cjs.map +1 -0
  151. package/packages/rpc/dist/index.d.ts +3857 -0
  152. package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
  153. package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
  154. package/packages/rpc/node_modules/.bin/jiti +17 -0
  155. package/packages/rpc/node_modules/.bin/tsc +17 -0
  156. package/packages/rpc/node_modules/.bin/tsserver +17 -0
  157. package/packages/rpc/node_modules/.bin/tsup +17 -0
  158. package/packages/rpc/node_modules/.bin/tsup-node +17 -0
  159. package/packages/rpc/node_modules/.bin/tsx +17 -0
  160. package/packages/rpc/package.json +25 -0
  161. package/packages/rpc/src/index.ts +206 -0
  162. package/packages/rpc/tsconfig.json +4 -0
  163. package/packages/typescript/base.json +36 -0
  164. package/packages/typescript/nextjs.json +17 -0
  165. package/packages/typescript/package.json +10 -0
  166. package/packages/ui/.turbo/turbo-build.log +44 -0
  167. package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
  168. package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
  169. package/packages/ui/dist/components/badge.d.ts +12 -0
  170. package/packages/ui/dist/components/badge.js +31 -0
  171. package/packages/ui/dist/components/badge.js.map +1 -0
  172. package/packages/ui/dist/components/button.d.ts +13 -0
  173. package/packages/ui/dist/components/button.js +40 -0
  174. package/packages/ui/dist/components/button.js.map +1 -0
  175. package/packages/ui/dist/components/card.d.ts +10 -0
  176. package/packages/ui/dist/components/card.js +39 -0
  177. package/packages/ui/dist/components/card.js.map +1 -0
  178. package/packages/ui/dist/components/input.d.ts +5 -0
  179. package/packages/ui/dist/components/input.js +28 -0
  180. package/packages/ui/dist/components/input.js.map +1 -0
  181. package/packages/ui/dist/components/label.d.ts +5 -0
  182. package/packages/ui/dist/components/label.js +13 -0
  183. package/packages/ui/dist/components/label.js.map +1 -0
  184. package/packages/ui/dist/components/separator.d.ts +5 -0
  185. package/packages/ui/dist/components/separator.js +13 -0
  186. package/packages/ui/dist/components/separator.js.map +1 -0
  187. package/packages/ui/dist/components/textarea.d.ts +5 -0
  188. package/packages/ui/dist/components/textarea.js +27 -0
  189. package/packages/ui/dist/components/textarea.js.map +1 -0
  190. package/packages/ui/dist/tailwind.d.ts +56 -0
  191. package/packages/ui/dist/tailwind.js +60 -0
  192. package/packages/ui/dist/tailwind.js.map +1 -0
  193. package/packages/ui/dist/utils/cn.d.ts +5 -0
  194. package/packages/ui/dist/utils/cn.js +7 -0
  195. package/packages/ui/dist/utils/cn.js.map +1 -0
  196. package/packages/ui/node_modules/.bin/jiti +17 -0
  197. package/packages/ui/node_modules/.bin/tsc +17 -0
  198. package/packages/ui/node_modules/.bin/tsserver +17 -0
  199. package/packages/ui/node_modules/.bin/tsup +17 -0
  200. package/packages/ui/node_modules/.bin/tsup-node +17 -0
  201. package/packages/ui/node_modules/.bin/tsx +17 -0
  202. package/packages/ui/package.json +69 -0
  203. package/packages/ui/src/components/badge.tsx +27 -0
  204. package/packages/ui/src/components/button.tsx +40 -0
  205. package/packages/ui/src/components/card.tsx +31 -0
  206. package/packages/ui/src/components/input.tsx +21 -0
  207. package/packages/ui/src/components/label.tsx +6 -0
  208. package/packages/ui/src/components/separator.tsx +6 -0
  209. package/packages/ui/src/components/textarea.tsx +20 -0
  210. package/packages/ui/src/globals.css +70 -0
  211. package/packages/ui/src/tailwind.ts +56 -0
  212. package/packages/ui/src/utils/cn.ts +6 -0
  213. package/packages/ui/tsconfig.json +20 -0
  214. package/packages/ui/tsup.config.ts +20 -0
  215. package/pnpm-workspace.yaml +4 -0
  216. package/scripts/install-rust-binaries.mjs +84 -0
  217. package/scripts/launchd/install-user-daemon.sh +358 -0
  218. package/scripts/launchd/run-vault-daemon.sh +5 -0
  219. package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
  220. package/scripts/launchd/uninstall-user-daemon.sh +103 -0
  221. package/src/cli.ts +2121 -0
  222. package/src/lib/admin-guard.js +1 -0
  223. package/src/lib/admin-guard.ts +185 -0
  224. package/src/lib/admin-passthrough.ts +33 -0
  225. package/src/lib/admin-reset.ts +751 -0
  226. package/src/lib/admin-setup.ts +1612 -0
  227. package/src/lib/agent-auth-clear.js +1 -0
  228. package/src/lib/agent-auth-clear.ts +58 -0
  229. package/src/lib/agent-auth-forwarding.js +1 -0
  230. package/src/lib/agent-auth-forwarding.ts +149 -0
  231. package/src/lib/agent-auth-migrate.js +1 -0
  232. package/src/lib/agent-auth-migrate.ts +150 -0
  233. package/src/lib/agent-auth-revoke.ts +103 -0
  234. package/src/lib/agent-auth-rotate.ts +107 -0
  235. package/src/lib/agent-auth-token.js +1 -0
  236. package/src/lib/agent-auth-token.ts +25 -0
  237. package/src/lib/agent-auth.ts +89 -0
  238. package/src/lib/asset-broadcast.js +1 -0
  239. package/src/lib/asset-broadcast.ts +285 -0
  240. package/src/lib/bootstrap-artifacts.js +1 -0
  241. package/src/lib/bootstrap-artifacts.ts +205 -0
  242. package/src/lib/bootstrap-credentials.js +1 -0
  243. package/src/lib/bootstrap-credentials.ts +832 -0
  244. package/src/lib/config-amounts.js +1 -0
  245. package/src/lib/config-amounts.ts +189 -0
  246. package/src/lib/config-mutation.ts +27 -0
  247. package/src/lib/fs-trust.js +1 -0
  248. package/src/lib/fs-trust.ts +537 -0
  249. package/src/lib/keychain.js +1 -0
  250. package/src/lib/keychain.ts +225 -0
  251. package/src/lib/local-admin-access.ts +106 -0
  252. package/src/lib/network-selection.js +1 -0
  253. package/src/lib/network-selection.ts +71 -0
  254. package/src/lib/passthrough-security.js +1 -0
  255. package/src/lib/passthrough-security.ts +114 -0
  256. package/src/lib/rpc-guard.js +1 -0
  257. package/src/lib/rpc-guard.ts +7 -0
  258. package/src/lib/rust-spawn-options.js +1 -0
  259. package/src/lib/rust-spawn-options.ts +98 -0
  260. package/src/lib/rust.js +1 -0
  261. package/src/lib/rust.ts +143 -0
  262. package/src/lib/signed-tx.js +1 -0
  263. package/src/lib/signed-tx.ts +116 -0
  264. package/src/lib/status-repair-cli.ts +116 -0
  265. package/src/lib/sudo.js +1 -0
  266. package/src/lib/sudo.ts +172 -0
  267. package/src/lib/vault-password-forwarding.js +1 -0
  268. package/src/lib/vault-password-forwarding.ts +155 -0
  269. package/src/lib/wallet-profile.js +1 -0
  270. package/src/lib/wallet-profile.ts +332 -0
  271. package/src/lib/wallet-repair.js +1 -0
  272. package/src/lib/wallet-repair.ts +304 -0
  273. package/src/lib/wallet-setup.js +1 -0
  274. package/src/lib/wallet-setup.ts +1466 -0
  275. package/src/lib/wallet-status.js +1 -0
  276. package/src/lib/wallet-status.ts +640 -0
  277. package/tsconfig.base.json +17 -0
  278. package/tsconfig.json +10 -0
  279. package/tsup.config.ts +25 -0
  280. package/turbo.json +41 -0
  281. package/LICENSE.md +0 -1
  282. package/dist/wlfa/index.cjs +0 -250
  283. package/dist/wlfa/index.d.cts +0 -1
  284. package/dist/wlfa/index.d.ts +0 -1
  285. package/dist/wlfa/index.js +0 -250
  286. package/dist/wlfc/index.cjs +0 -1839
  287. package/dist/wlfc/index.d.cts +0 -1
  288. package/dist/wlfc/index.d.ts +0 -1
  289. package/dist/wlfc/index.js +0 -1839
@@ -0,0 +1,24 @@
1
+ [package]
2
+ name = "wlfi-agent-agent"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+ authors.workspace = true
7
+
8
+ [dependencies]
9
+ anyhow.workspace = true
10
+ clap.workspace = true
11
+ hex.workspace = true
12
+ libc.workspace = true
13
+ serde.workspace = true
14
+ serde_json.workspace = true
15
+ rpassword.workspace = true
16
+ time.workspace = true
17
+ tokio.workspace = true
18
+ uuid.workspace = true
19
+ zeroize.workspace = true
20
+ vault-daemon = { path = "../vault-daemon" }
21
+ vault-domain = { path = "../vault-domain" }
22
+ vault-sdk-agent = { path = "../vault-sdk-agent" }
23
+ vault-signer = { path = "../vault-signer" }
24
+ vault-transport-unix = { path = "../vault-transport-unix" }
@@ -0,0 +1,576 @@
1
+ use std::fs::OpenOptions;
2
+ use std::io::{Read, Write};
3
+ use std::path::{Path, PathBuf};
4
+
5
+ use anyhow::{anyhow, bail, Context, Result};
6
+ #[cfg(unix)]
7
+ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
8
+ use zeroize::Zeroize;
9
+
10
+ use crate::{AgentCommandOutput, OutputFormat, OutputTarget};
11
+
12
+ const MAX_SECRET_STDIN_BYTES: u64 = 16 * 1024;
13
+ #[cfg(unix)]
14
+ const PRIVATE_DIR_MODE: u32 = 0o700;
15
+ #[cfg(unix)]
16
+ const GROUP_OTHER_WRITE_MODE_MASK: u32 = 0o022;
17
+ #[cfg(unix)]
18
+ const STICKY_BIT_MODE: u32 = 0o1000;
19
+
20
+ pub(crate) fn resolve_agent_auth_token(
21
+ cli_value: Option<String>,
22
+ env_value: Option<String>,
23
+ from_stdin: bool,
24
+ non_interactive: bool,
25
+ ) -> Result<String> {
26
+ if from_stdin {
27
+ return read_secret_from_reader(std::io::stdin(), "agent auth token");
28
+ }
29
+
30
+ if let Some(value) = cli_value {
31
+ validate_secret(value, "argument")?;
32
+ bail!(
33
+ "--agent-auth-token is disabled for security; use --agent-auth-token-stdin or WLFI_AGENT_AUTH_TOKEN"
34
+ );
35
+ }
36
+
37
+ if let Some(value) = env_value {
38
+ return validate_secret(value, "environment");
39
+ }
40
+
41
+ if non_interactive {
42
+ bail!(
43
+ "agent auth token is required in non-interactive mode; use WLFI_AGENT_AUTH_TOKEN or --agent-auth-token-stdin"
44
+ );
45
+ }
46
+
47
+ let prompted = rpassword::prompt_password("Agent auth token: ")
48
+ .context("failed to read agent auth token input")?;
49
+ validate_secret(prompted, "prompt")
50
+ }
51
+
52
+ fn validate_secret(mut value: String, source: &str) -> Result<String> {
53
+ if value.as_bytes().len() > MAX_SECRET_STDIN_BYTES as usize {
54
+ value.zeroize();
55
+ bail!("{source} secret must not exceed {MAX_SECRET_STDIN_BYTES} bytes");
56
+ }
57
+ if value.trim().is_empty() {
58
+ value.zeroize();
59
+ bail!("{source} secret must not be empty or whitespace");
60
+ }
61
+ Ok(value)
62
+ }
63
+
64
+ fn read_secret_from_reader(mut reader: impl Read, label: &str) -> Result<String> {
65
+ let mut raw = String::new();
66
+ reader
67
+ .by_ref()
68
+ .take(MAX_SECRET_STDIN_BYTES + 1)
69
+ .read_to_string(&mut raw)
70
+ .with_context(|| format!("failed to read {label} from stdin"))?;
71
+ if raw.as_bytes().len() > MAX_SECRET_STDIN_BYTES as usize {
72
+ raw.zeroize();
73
+ bail!("{label} must not exceed {MAX_SECRET_STDIN_BYTES} bytes");
74
+ }
75
+ let secret = raw.trim_end_matches(['\r', '\n']).to_string();
76
+ raw.zeroize();
77
+ validate_secret(secret, "stdin")
78
+ }
79
+
80
+ #[cfg(test)]
81
+ mod tests {
82
+ use super::{
83
+ ensure_output_parent, read_secret_from_reader, validate_secret, write_output_file,
84
+ };
85
+ use std::fs;
86
+ use std::io::Cursor;
87
+ use std::path::PathBuf;
88
+ use std::time::{SystemTime, UNIX_EPOCH};
89
+
90
+ fn temp_path(prefix: &str) -> PathBuf {
91
+ std::env::temp_dir().join(format!(
92
+ "{prefix}-{}-{}",
93
+ std::process::id(),
94
+ SystemTime::now()
95
+ .duration_since(UNIX_EPOCH)
96
+ .expect("system time before unix epoch")
97
+ .as_nanos()
98
+ ))
99
+ }
100
+
101
+ #[test]
102
+ fn read_secret_from_reader_rejects_oversized_stdin() {
103
+ let oversized = Cursor::new(vec![b'a'; (16 * 1024) + 1]);
104
+ let err = read_secret_from_reader(oversized, "agent auth token").expect_err("must fail");
105
+ assert!(err.to_string().contains("must not exceed"));
106
+ }
107
+
108
+ #[test]
109
+ fn validate_secret_rejects_oversized_non_stdin_secret() {
110
+ let err = validate_secret("a".repeat((16 * 1024) + 1), "argument or environment")
111
+ .expect_err("must fail");
112
+ assert!(err.to_string().contains("must not exceed"));
113
+ }
114
+
115
+ #[test]
116
+ #[cfg(unix)]
117
+ fn ensure_output_parent_rejects_symlinked_parent_directory() {
118
+ let root = temp_path("vault-cli-agent-output-symlink");
119
+ let actual = root.join("actual");
120
+ let linked = root.join("linked");
121
+ fs::create_dir_all(&actual).expect("create actual directory");
122
+ std::os::unix::fs::symlink(&actual, &linked).expect("create symlink parent");
123
+
124
+ let err = ensure_output_parent(&linked.join("output.json")).expect_err("must reject");
125
+ assert!(err.to_string().contains("must not be a symlink"));
126
+
127
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
128
+ }
129
+
130
+ #[test]
131
+ #[cfg(unix)]
132
+ fn ensure_output_parent_rejects_group_writable_parent_directory() {
133
+ use std::os::unix::fs::PermissionsExt;
134
+
135
+ let root = temp_path("vault-cli-agent-output-mode");
136
+ let insecure = root.join("shared");
137
+ fs::create_dir_all(&insecure).expect("create insecure directory");
138
+ fs::set_permissions(&insecure, fs::Permissions::from_mode(0o777))
139
+ .expect("set insecure permissions");
140
+
141
+ let err = ensure_output_parent(&insecure.join("output.json")).expect_err("must reject");
142
+ assert!(err
143
+ .to_string()
144
+ .contains("must not be writable by group/other"));
145
+
146
+ fs::set_permissions(&insecure, fs::Permissions::from_mode(0o700))
147
+ .expect("restore cleanup permissions");
148
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
149
+ }
150
+
151
+ #[test]
152
+ #[cfg(unix)]
153
+ fn ensure_output_parent_rejects_group_writable_ancestor_directory() {
154
+ use std::os::unix::fs::PermissionsExt;
155
+
156
+ let root = temp_path("vault-cli-agent-output-ancestor-mode");
157
+ let insecure = root.join("shared");
158
+ let nested = insecure.join("nested");
159
+ fs::create_dir_all(&nested).expect("create nested directory");
160
+ fs::set_permissions(&insecure, fs::Permissions::from_mode(0o777))
161
+ .expect("set insecure ancestor permissions");
162
+ fs::set_permissions(&nested, fs::Permissions::from_mode(0o700))
163
+ .expect("set nested permissions");
164
+
165
+ let err = ensure_output_parent(&nested.join("output.json")).expect_err("must reject");
166
+ assert!(err
167
+ .to_string()
168
+ .contains("must not be writable by group/other"));
169
+
170
+ fs::set_permissions(&insecure, fs::Permissions::from_mode(0o700))
171
+ .expect("restore cleanup permissions");
172
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
173
+ }
174
+
175
+ #[test]
176
+ #[cfg(unix)]
177
+ fn ensure_output_parent_creates_private_missing_directories() {
178
+ use std::os::unix::fs::PermissionsExt;
179
+
180
+ let root = temp_path("vault-cli-agent-output-create");
181
+ fs::create_dir_all(&root).expect("create root directory");
182
+
183
+ let nested_output = root.join("nested").join("deeper").join("output.json");
184
+ ensure_output_parent(&nested_output).expect("must create parent directories");
185
+
186
+ let nested_mode = fs::metadata(root.join("nested"))
187
+ .expect("nested metadata")
188
+ .permissions()
189
+ .mode()
190
+ & 0o777;
191
+ let deeper_mode = fs::metadata(root.join("nested").join("deeper"))
192
+ .expect("deeper metadata")
193
+ .permissions()
194
+ .mode()
195
+ & 0o777;
196
+ assert_eq!(nested_mode, 0o700);
197
+ assert_eq!(deeper_mode, 0o700);
198
+
199
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
200
+ }
201
+
202
+ #[test]
203
+ #[cfg(unix)]
204
+ fn write_output_file_overwrite_replaces_existing_hard_link_instead_of_mutating_shared_inode() {
205
+ use std::os::unix::fs::PermissionsExt;
206
+
207
+ let root = temp_path("vault-cli-agent-output-overwrite-hardlink");
208
+ fs::create_dir_all(&root).expect("create root directory");
209
+ fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
210
+ .expect("secure root directory permissions");
211
+
212
+ let output_path = root.join("output.json");
213
+ let alias_path = root.join("output-alias.json");
214
+ fs::write(&output_path, "old\n").expect("write original output file");
215
+ fs::set_permissions(&output_path, fs::Permissions::from_mode(0o600))
216
+ .expect("secure original output file permissions");
217
+ fs::hard_link(&output_path, &alias_path).expect("create hard link alias");
218
+
219
+ write_output_file(&output_path, "new", true).expect("overwrite output file");
220
+
221
+ assert_eq!(
222
+ fs::read_to_string(&output_path).expect("read replaced output"),
223
+ "new\n"
224
+ );
225
+ assert_eq!(
226
+ fs::read_to_string(&alias_path).expect("read hard link alias"),
227
+ "old\n"
228
+ );
229
+
230
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
231
+ }
232
+ }
233
+
234
+ pub(crate) fn resolve_output_format(
235
+ format: Option<OutputFormat>,
236
+ json: bool,
237
+ ) -> Result<OutputFormat> {
238
+ if json {
239
+ if matches!(format, Some(OutputFormat::Text)) {
240
+ bail!("--json cannot be combined with --format text");
241
+ }
242
+ return Ok(OutputFormat::Json);
243
+ }
244
+ Ok(format.unwrap_or(OutputFormat::Text))
245
+ }
246
+
247
+ pub(crate) fn resolve_output_target(
248
+ target: Option<PathBuf>,
249
+ overwrite: bool,
250
+ ) -> Result<OutputTarget> {
251
+ match target {
252
+ Some(path) if path.as_os_str() == "-" => {
253
+ if overwrite {
254
+ bail!("--overwrite cannot be used with --output - (stdout)");
255
+ }
256
+ Ok(OutputTarget::Stdout)
257
+ }
258
+ Some(path) => Ok(OutputTarget::File { path, overwrite }),
259
+ None => Ok(OutputTarget::Stdout),
260
+ }
261
+ }
262
+
263
+ pub(crate) fn emit_output(output: &str, target: &OutputTarget) -> Result<()> {
264
+ match target {
265
+ OutputTarget::Stdout => {
266
+ println!("{output}");
267
+ Ok(())
268
+ }
269
+ OutputTarget::File { path, overwrite } => {
270
+ ensure_output_parent(path)?;
271
+ write_output_file(path, output, *overwrite)
272
+ }
273
+ }
274
+ }
275
+
276
+ pub(crate) fn ensure_output_parent(path: &Path) -> Result<()> {
277
+ if is_symlink_path(path)? {
278
+ bail!("output path '{}' must not be a symlink", path.display());
279
+ }
280
+ if path.is_dir() {
281
+ bail!(
282
+ "output path '{}' is a directory; provide a file path",
283
+ path.display()
284
+ );
285
+ }
286
+ if let Some(parent) = path.parent() {
287
+ if !parent.as_os_str().is_empty() {
288
+ ensure_secure_output_directory(parent)?;
289
+ }
290
+ }
291
+ Ok(())
292
+ }
293
+
294
+ fn ensure_secure_output_directory(path: &Path) -> Result<()> {
295
+ match std::fs::symlink_metadata(path) {
296
+ Ok(metadata) => {
297
+ assert_secure_output_directory(path, &metadata, false)?;
298
+ assert_secure_output_directory_ancestors(path)?;
299
+ Ok(())
300
+ }
301
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
302
+ if let Some(parent) = path.parent() {
303
+ if !parent.as_os_str().is_empty() {
304
+ ensure_secure_output_directory(parent)?;
305
+ }
306
+ }
307
+
308
+ std::fs::create_dir(path)
309
+ .with_context(|| format!("failed to create output directory {}", path.display()))?;
310
+
311
+ #[cfg(unix)]
312
+ std::fs::set_permissions(path, std::fs::Permissions::from_mode(PRIVATE_DIR_MODE))
313
+ .with_context(|| {
314
+ format!(
315
+ "failed to set output directory permissions on {}",
316
+ path.display()
317
+ )
318
+ })?;
319
+
320
+ let metadata = std::fs::symlink_metadata(path).with_context(|| {
321
+ format!(
322
+ "failed to inspect output directory metadata {}",
323
+ path.display()
324
+ )
325
+ })?;
326
+ assert_secure_output_directory(path, &metadata, false)?;
327
+ assert_secure_output_directory_ancestors(path)
328
+ }
329
+ Err(err) => Err(err).with_context(|| {
330
+ format!(
331
+ "failed to inspect output directory metadata {}",
332
+ path.display()
333
+ )
334
+ }),
335
+ }
336
+ }
337
+
338
+ #[cfg(unix)]
339
+ fn assert_secure_output_directory_ancestors(path: &Path) -> Result<()> {
340
+ let canonical = std::fs::canonicalize(path)
341
+ .with_context(|| format!("failed to canonicalize output directory {}", path.display()))?;
342
+
343
+ for ancestor in canonical.ancestors().skip(1) {
344
+ let metadata = std::fs::symlink_metadata(ancestor).with_context(|| {
345
+ format!(
346
+ "failed to inspect ancestor output directory metadata {}",
347
+ ancestor.display()
348
+ )
349
+ })?;
350
+ assert_secure_output_directory(ancestor, &metadata, true)?;
351
+ }
352
+
353
+ Ok(())
354
+ }
355
+
356
+ #[cfg(not(unix))]
357
+ fn assert_secure_output_directory_ancestors(_path: &Path) -> Result<()> {
358
+ Ok(())
359
+ }
360
+
361
+ fn assert_secure_output_directory(
362
+ path: &Path,
363
+ metadata: &std::fs::Metadata,
364
+ #[cfg(unix)] allow_sticky_group_other_write: bool,
365
+ ) -> Result<()> {
366
+ if metadata.file_type().is_symlink() {
367
+ bail!(
368
+ "output directory '{}' must not be a symlink",
369
+ path.display()
370
+ );
371
+ }
372
+ if !metadata.is_dir() {
373
+ bail!("output directory '{}' must be a directory", path.display());
374
+ }
375
+
376
+ #[cfg(unix)]
377
+ if metadata.permissions().mode() & GROUP_OTHER_WRITE_MODE_MASK != 0 {
378
+ if allow_sticky_group_other_write && metadata.permissions().mode() & STICKY_BIT_MODE != 0 {
379
+ return Ok(());
380
+ }
381
+
382
+ bail!(
383
+ "output directory '{}' must not be writable by group/other",
384
+ path.display()
385
+ );
386
+ }
387
+
388
+ Ok(())
389
+ }
390
+
391
+ pub(crate) fn write_output_file(path: &Path, output: &str, overwrite: bool) -> Result<()> {
392
+ if overwrite {
393
+ return write_output_file_atomic_replace(path, output);
394
+ }
395
+
396
+ let mut options = OpenOptions::new();
397
+ options.write(true);
398
+ options.create_new(true);
399
+ #[cfg(unix)]
400
+ {
401
+ options.mode(0o600);
402
+ options.custom_flags(libc::O_NOFOLLOW);
403
+ }
404
+
405
+ let mut file = options.open(path).map_err(|err| {
406
+ if err.kind() == std::io::ErrorKind::AlreadyExists {
407
+ anyhow!(
408
+ "output path '{}' already exists; pass --overwrite to replace it",
409
+ path.display()
410
+ )
411
+ } else {
412
+ err.into()
413
+ }
414
+ })?;
415
+ file.write_all(output.as_bytes())
416
+ .with_context(|| format!("failed to write output to {}", path.display()))?;
417
+ file.write_all(b"\n")
418
+ .with_context(|| format!("failed to write output to {}", path.display()))?;
419
+ #[cfg(unix)]
420
+ {
421
+ file.set_permissions(std::fs::Permissions::from_mode(0o600))
422
+ .with_context(|| format!("failed to set output permissions on {}", path.display()))?;
423
+ }
424
+ Ok(())
425
+ }
426
+
427
+ fn write_output_file_atomic_replace(path: &Path, output: &str) -> Result<()> {
428
+ let temp_path = temporary_output_path(path);
429
+ let result = (|| -> Result<()> {
430
+ let mut options = OpenOptions::new();
431
+ options.write(true).create_new(true);
432
+ #[cfg(unix)]
433
+ {
434
+ options.mode(0o600);
435
+ options.custom_flags(libc::O_NOFOLLOW);
436
+ }
437
+
438
+ let mut file = options.open(&temp_path).with_context(|| {
439
+ format!(
440
+ "failed to create temporary output file {}",
441
+ temp_path.display()
442
+ )
443
+ })?;
444
+ file.write_all(output.as_bytes())
445
+ .with_context(|| format!("failed to write output to {}", temp_path.display()))?;
446
+ file.write_all(b"\n")
447
+ .with_context(|| format!("failed to write output to {}", temp_path.display()))?;
448
+ #[cfg(unix)]
449
+ {
450
+ file.set_permissions(std::fs::Permissions::from_mode(0o600))
451
+ .with_context(|| {
452
+ format!(
453
+ "failed to set output permissions on {}",
454
+ temp_path.display()
455
+ )
456
+ })?;
457
+ }
458
+ drop(file);
459
+
460
+ #[cfg(windows)]
461
+ if path.exists() {
462
+ std::fs::remove_file(path)
463
+ .with_context(|| format!("failed to remove output file {}", path.display()))?;
464
+ }
465
+
466
+ std::fs::rename(&temp_path, path)
467
+ .with_context(|| format!("failed to replace output file {}", path.display()))?;
468
+ Ok(())
469
+ })();
470
+
471
+ if result.is_err() {
472
+ let _ = std::fs::remove_file(&temp_path);
473
+ }
474
+
475
+ result
476
+ }
477
+
478
+ fn temporary_output_path(path: &Path) -> PathBuf {
479
+ let parent = path.parent().unwrap_or_else(|| Path::new("."));
480
+ let file_name = path
481
+ .file_name()
482
+ .and_then(|value| value.to_str())
483
+ .unwrap_or("output");
484
+ let timestamp = std::time::SystemTime::now()
485
+ .duration_since(std::time::UNIX_EPOCH)
486
+ .unwrap_or_default()
487
+ .as_nanos();
488
+ parent.join(format!(
489
+ ".{file_name}.tmp-{}-{timestamp}",
490
+ std::process::id()
491
+ ))
492
+ }
493
+
494
+ fn is_symlink_path(path: &Path) -> Result<bool> {
495
+ match std::fs::symlink_metadata(path) {
496
+ Ok(metadata) => Ok(metadata.file_type().is_symlink()),
497
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
498
+ Err(err) => Err(err)
499
+ .with_context(|| format!("failed to inspect output path metadata {}", path.display())),
500
+ }
501
+ }
502
+
503
+ pub(crate) fn should_print_status(format: OutputFormat, quiet: bool) -> bool {
504
+ format == OutputFormat::Text && !quiet
505
+ }
506
+
507
+ pub(crate) fn print_status(message: &str, format: OutputFormat, quiet: bool) {
508
+ if should_print_status(format, quiet) {
509
+ eprintln!("==> {message}");
510
+ }
511
+ }
512
+
513
+ pub(crate) fn print_agent_output(
514
+ output: &AgentCommandOutput,
515
+ format: OutputFormat,
516
+ target: &OutputTarget,
517
+ ) -> Result<()> {
518
+ let rendered = match format {
519
+ OutputFormat::Json => {
520
+ serde_json::to_string_pretty(output).context("failed to serialize output")?
521
+ }
522
+ OutputFormat::Text => [
523
+ format!("Command: {}", output.command),
524
+ format!("Network: {}", output.network),
525
+ format!("Asset: {}", output.asset),
526
+ format!("Counterparty: {}", output.counterparty),
527
+ format!("Amount (wei): {}", output.amount_wei),
528
+ output
529
+ .estimated_max_gas_spend_wei
530
+ .as_ref()
531
+ .map(|value| format!("Estimated Max Gas Spend (wei): {value}"))
532
+ .unwrap_or_default(),
533
+ output
534
+ .tx_type
535
+ .as_ref()
536
+ .map(|value| format!("Tx Type: {value}"))
537
+ .unwrap_or_default(),
538
+ output
539
+ .delegation_enabled
540
+ .as_ref()
541
+ .map(|value| format!("Delegation Enabled: {value}"))
542
+ .unwrap_or_default(),
543
+ format!("Signature: {}", output.signature_hex),
544
+ output
545
+ .r_hex
546
+ .as_ref()
547
+ .map(|value| format!("r: {value}"))
548
+ .unwrap_or_default(),
549
+ output
550
+ .s_hex
551
+ .as_ref()
552
+ .map(|value| format!("s: {value}"))
553
+ .unwrap_or_default(),
554
+ output
555
+ .v
556
+ .as_ref()
557
+ .map(|value| format!("v: {value}"))
558
+ .unwrap_or_default(),
559
+ output
560
+ .raw_tx_hex
561
+ .as_ref()
562
+ .map(|value| format!("Raw Tx: {value}"))
563
+ .unwrap_or_default(),
564
+ output
565
+ .tx_hash_hex
566
+ .as_ref()
567
+ .map(|value| format!("Tx Hash: {value}"))
568
+ .unwrap_or_default(),
569
+ ]
570
+ .into_iter()
571
+ .filter(|line| !line.is_empty())
572
+ .collect::<Vec<_>>()
573
+ .join("\n"),
574
+ };
575
+ emit_output(&rendered, target)
576
+ }