@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,833 @@
1
+ use std::path::{Path, PathBuf};
2
+ use std::sync::Arc;
3
+
4
+ use anyhow::{Context, Result};
5
+ use clap::{Parser, Subcommand, ValueEnum};
6
+ use serde::Serialize;
7
+ use uuid::Uuid;
8
+ use vault_daemon::{DaemonError, KeyManagerDaemonApi};
9
+ use vault_domain::{AgentAction, BroadcastTx, EvmAddress};
10
+ use vault_sdk_agent::{AgentOperations, AgentSdk, AgentSdkError};
11
+ use vault_transport_unix::{assert_root_owned_daemon_socket_path, UnixDaemonClient};
12
+
13
+ mod io_utils;
14
+
15
+ use io_utils::*;
16
+
17
+ #[derive(Debug, Parser)]
18
+ #[command(name = "wlfi-agent-agent")]
19
+ #[command(about = "Agent CLI for sending signing requests through daemon policy checks")]
20
+ struct Cli {
21
+ #[arg(
22
+ long,
23
+ default_value_t = false,
24
+ help = "Do not prompt for missing secrets; require WLFI_AGENT_AUTH_TOKEN or --agent-auth-token-stdin"
25
+ )]
26
+ non_interactive: bool,
27
+ #[arg(
28
+ long,
29
+ env = "WLFI_AGENT_KEY_ID",
30
+ value_name = "UUID",
31
+ help = "Provisioned agent key id"
32
+ )]
33
+ agent_key_id: Uuid,
34
+ #[arg(
35
+ long,
36
+ value_name = "TOKEN",
37
+ conflicts_with = "agent_auth_token_stdin",
38
+ help = "Legacy insecure agent auth token flag (disabled by default); use --agent-auth-token-stdin or WLFI_AGENT_AUTH_TOKEN"
39
+ )]
40
+ agent_auth_token: Option<String>,
41
+ #[arg(
42
+ long,
43
+ default_value_t = false,
44
+ help = "Read agent auth token from stdin (trailing newlines are trimmed)"
45
+ )]
46
+ agent_auth_token_stdin: bool,
47
+ #[arg(
48
+ long,
49
+ env = "WLFI_DAEMON_SOCKET",
50
+ value_name = "PATH",
51
+ help = "Always-on daemon unix socket path (default: $WLFI_HOME/daemon.sock or ~/.wlfi_agent/daemon.sock)"
52
+ )]
53
+ daemon_socket: Option<PathBuf>,
54
+ #[arg(
55
+ long,
56
+ short = 'f',
57
+ value_enum,
58
+ value_name = "text|json",
59
+ help = "Output format"
60
+ )]
61
+ format: Option<OutputFormat>,
62
+ #[arg(
63
+ long,
64
+ short = 'j',
65
+ default_value_t = false,
66
+ help = "Shortcut for --format json"
67
+ )]
68
+ json: bool,
69
+ #[arg(
70
+ long,
71
+ short = 'q',
72
+ default_value_t = false,
73
+ help = "Suppress non-essential status messages in text mode"
74
+ )]
75
+ quiet: bool,
76
+ #[arg(
77
+ long,
78
+ short = 'o',
79
+ value_name = "PATH",
80
+ help = "Write final command output to PATH (use '-' for stdout)"
81
+ )]
82
+ output: Option<PathBuf>,
83
+ #[arg(
84
+ long,
85
+ default_value_t = false,
86
+ requires = "output",
87
+ help = "Allow replacing an existing output file"
88
+ )]
89
+ overwrite: bool,
90
+ #[command(subcommand)]
91
+ command: Commands,
92
+ }
93
+
94
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
95
+ enum OutputFormat {
96
+ Text,
97
+ Json,
98
+ }
99
+
100
+ #[derive(Debug)]
101
+ enum OutputTarget {
102
+ Stdout,
103
+ File { path: PathBuf, overwrite: bool },
104
+ }
105
+
106
+ #[derive(Debug, Subcommand)]
107
+ enum Commands {
108
+ #[command(about = "Submit an ERC-20 transfer request through policy checks")]
109
+ Transfer {
110
+ #[arg(long, value_parser = parse_positive_u64)]
111
+ network: u64,
112
+ #[arg(long)]
113
+ token: EvmAddress,
114
+ #[arg(long)]
115
+ to: EvmAddress,
116
+ #[arg(long, value_parser = parse_positive_u128)]
117
+ amount_wei: u128,
118
+ },
119
+ #[command(about = "Submit a native ETH transfer request through policy checks")]
120
+ TransferNative {
121
+ #[arg(long, value_parser = parse_positive_u64)]
122
+ network: u64,
123
+ #[arg(long)]
124
+ to: EvmAddress,
125
+ #[arg(long, value_parser = parse_positive_u128)]
126
+ amount_wei: u128,
127
+ },
128
+ #[command(about = "Submit an ERC-20 approve request through policy checks")]
129
+ Approve {
130
+ #[arg(long, value_parser = parse_positive_u64)]
131
+ network: u64,
132
+ #[arg(long)]
133
+ token: EvmAddress,
134
+ #[arg(long)]
135
+ spender: EvmAddress,
136
+ #[arg(long, value_parser = parse_positive_u128)]
137
+ amount_wei: u128,
138
+ },
139
+ #[command(about = "Submit a raw transaction broadcast request through policy checks")]
140
+ Broadcast {
141
+ #[arg(long, value_parser = parse_positive_u64)]
142
+ network: u64,
143
+ #[arg(long, default_value_t = 0u64, value_parser = parse_non_negative_u64)]
144
+ nonce: u64,
145
+ #[arg(long)]
146
+ to: EvmAddress,
147
+ #[arg(long, default_value_t = 0u128, value_parser = parse_non_negative_u128)]
148
+ value_wei: u128,
149
+ #[arg(long, default_value = "0x")]
150
+ data_hex: String,
151
+ #[arg(long, value_parser = parse_positive_u64)]
152
+ gas_limit: u64,
153
+ #[arg(long, value_parser = parse_positive_u128)]
154
+ max_fee_per_gas_wei: u128,
155
+ #[arg(
156
+ long,
157
+ default_value_t = 0u128,
158
+ value_parser = parse_non_negative_u128
159
+ )]
160
+ max_priority_fee_per_gas_wei: u128,
161
+ #[arg(long, default_value_t = 0x02u8, value_parser = parse_tx_type_u8)]
162
+ tx_type: u8,
163
+ #[arg(long, default_value_t = false)]
164
+ delegation_enabled: bool,
165
+ },
166
+ }
167
+
168
+ #[derive(Debug, Serialize)]
169
+ struct AgentCommandOutput {
170
+ command: String,
171
+ network: String,
172
+ asset: String,
173
+ counterparty: String,
174
+ amount_wei: String,
175
+ #[serde(skip_serializing_if = "Option::is_none")]
176
+ estimated_max_gas_spend_wei: Option<String>,
177
+ #[serde(skip_serializing_if = "Option::is_none")]
178
+ tx_type: Option<String>,
179
+ #[serde(skip_serializing_if = "Option::is_none")]
180
+ delegation_enabled: Option<bool>,
181
+ signature_hex: String,
182
+ #[serde(skip_serializing_if = "Option::is_none")]
183
+ r_hex: Option<String>,
184
+ #[serde(skip_serializing_if = "Option::is_none")]
185
+ s_hex: Option<String>,
186
+ #[serde(skip_serializing_if = "Option::is_none")]
187
+ v: Option<u64>,
188
+ #[serde(skip_serializing_if = "Option::is_none")]
189
+ raw_tx_hex: Option<String>,
190
+ #[serde(skip_serializing_if = "Option::is_none")]
191
+ tx_hash_hex: Option<String>,
192
+ }
193
+
194
+ #[derive(Debug, Serialize)]
195
+ struct ManualApprovalRequiredOutput {
196
+ command: String,
197
+ approval_request_id: String,
198
+ #[serde(skip_serializing_if = "Option::is_none")]
199
+ relay_url: Option<String>,
200
+ #[serde(skip_serializing_if = "Option::is_none")]
201
+ frontend_url: Option<String>,
202
+ cli_approval_command: String,
203
+ }
204
+
205
+ #[tokio::main]
206
+ async fn main() -> Result<()> {
207
+ let cli = Cli::parse();
208
+ let output_format = resolve_output_format(cli.format, cli.json)?;
209
+ let output_target = resolve_output_target(cli.output, cli.overwrite)?;
210
+ let daemon_socket = resolve_daemon_socket_path(cli.daemon_socket)?;
211
+ let agent_auth_token = resolve_agent_auth_token(
212
+ cli.agent_auth_token,
213
+ std::env::var("WLFI_AGENT_AUTH_TOKEN").ok(),
214
+ cli.agent_auth_token_stdin,
215
+ cli.non_interactive,
216
+ )?;
217
+ print_status("connecting to daemon socket", output_format, cli.quiet);
218
+ let daemon: Arc<dyn KeyManagerDaemonApi> =
219
+ Arc::new(UnixDaemonClient::new_with_expected_server_euid(
220
+ daemon_socket.clone(),
221
+ std::time::Duration::from_secs(10),
222
+ 0,
223
+ ));
224
+
225
+ let sdk = AgentSdk::new_with_key_id_and_token(daemon, cli.agent_key_id, agent_auth_token);
226
+
227
+ match cli.command {
228
+ Commands::Transfer {
229
+ network,
230
+ token,
231
+ to,
232
+ amount_wei,
233
+ } => {
234
+ let token_str = token.to_string();
235
+ let to_str = to.to_string();
236
+ print_status("submitting transfer request", output_format, cli.quiet);
237
+ let signature = match await_signature_or_handle_manual_approval(
238
+ "transfer",
239
+ &daemon_socket,
240
+ output_format,
241
+ &output_target,
242
+ sdk.transfer(network, token, to, amount_wei),
243
+ )
244
+ .await?
245
+ {
246
+ Some(signature) => signature,
247
+ None => std::process::exit(2),
248
+ };
249
+ let output = AgentCommandOutput {
250
+ command: "transfer".to_string(),
251
+ network: network.to_string(),
252
+ asset: format!("erc20:{token_str}"),
253
+ counterparty: to_str,
254
+ amount_wei: amount_wei.to_string(),
255
+ estimated_max_gas_spend_wei: None,
256
+ tx_type: None,
257
+ delegation_enabled: None,
258
+ signature_hex: format!("0x{}", hex::encode(signature.bytes)),
259
+ r_hex: None,
260
+ s_hex: None,
261
+ v: None,
262
+ raw_tx_hex: None,
263
+ tx_hash_hex: None,
264
+ };
265
+ print_status("transfer request signed", output_format, cli.quiet);
266
+ print_agent_output(&output, output_format, &output_target)?;
267
+ }
268
+ Commands::TransferNative {
269
+ network,
270
+ to,
271
+ amount_wei,
272
+ } => {
273
+ let to_str = to.to_string();
274
+ print_status(
275
+ "submitting native transfer request",
276
+ output_format,
277
+ cli.quiet,
278
+ );
279
+ let signature = match await_signature_or_handle_manual_approval(
280
+ "transfer-native",
281
+ &daemon_socket,
282
+ output_format,
283
+ &output_target,
284
+ sdk.transfer_native(network, to, amount_wei),
285
+ )
286
+ .await?
287
+ {
288
+ Some(signature) => signature,
289
+ None => std::process::exit(2),
290
+ };
291
+ let output = AgentCommandOutput {
292
+ command: "transfer-native".to_string(),
293
+ network: network.to_string(),
294
+ asset: "native_eth".to_string(),
295
+ counterparty: to_str,
296
+ amount_wei: amount_wei.to_string(),
297
+ estimated_max_gas_spend_wei: None,
298
+ tx_type: None,
299
+ delegation_enabled: None,
300
+ signature_hex: format!("0x{}", hex::encode(signature.bytes)),
301
+ r_hex: None,
302
+ s_hex: None,
303
+ v: None,
304
+ raw_tx_hex: None,
305
+ tx_hash_hex: None,
306
+ };
307
+ print_status("native transfer request signed", output_format, cli.quiet);
308
+ print_agent_output(&output, output_format, &output_target)?;
309
+ }
310
+ Commands::Approve {
311
+ network,
312
+ token,
313
+ spender,
314
+ amount_wei,
315
+ } => {
316
+ let token_str = token.to_string();
317
+ let spender_str = spender.to_string();
318
+ print_status("submitting approve request", output_format, cli.quiet);
319
+ let signature = match await_signature_or_handle_manual_approval(
320
+ "approve",
321
+ &daemon_socket,
322
+ output_format,
323
+ &output_target,
324
+ sdk.approve(network, token, spender, amount_wei),
325
+ )
326
+ .await?
327
+ {
328
+ Some(signature) => signature,
329
+ None => std::process::exit(2),
330
+ };
331
+ let output = AgentCommandOutput {
332
+ command: "approve".to_string(),
333
+ network: network.to_string(),
334
+ asset: format!("erc20:{token_str}"),
335
+ counterparty: spender_str,
336
+ amount_wei: amount_wei.to_string(),
337
+ estimated_max_gas_spend_wei: None,
338
+ tx_type: None,
339
+ delegation_enabled: None,
340
+ signature_hex: format!("0x{}", hex::encode(signature.bytes)),
341
+ r_hex: None,
342
+ s_hex: None,
343
+ v: None,
344
+ raw_tx_hex: None,
345
+ tx_hash_hex: None,
346
+ };
347
+ print_status("approve request signed", output_format, cli.quiet);
348
+ print_agent_output(&output, output_format, &output_target)?;
349
+ }
350
+ Commands::Broadcast {
351
+ network,
352
+ nonce,
353
+ to,
354
+ value_wei,
355
+ data_hex,
356
+ gas_limit,
357
+ max_fee_per_gas_wei,
358
+ max_priority_fee_per_gas_wei,
359
+ tx_type,
360
+ delegation_enabled,
361
+ } => {
362
+ let tx = BroadcastTx {
363
+ chain_id: network,
364
+ nonce,
365
+ to,
366
+ value_wei,
367
+ data_hex,
368
+ gas_limit,
369
+ max_fee_per_gas_wei,
370
+ max_priority_fee_per_gas_wei,
371
+ tx_type,
372
+ delegation_enabled,
373
+ };
374
+ let action = AgentAction::BroadcastTx { tx: tx.clone() };
375
+ action
376
+ .validate()
377
+ .context("invalid broadcast transaction payload")?;
378
+ let estimated_max_gas_spend_wei = tx.max_gas_spend_wei()?;
379
+ let asset = action.asset();
380
+ let counterparty = action.recipient();
381
+
382
+ print_status("submitting broadcast request", output_format, cli.quiet);
383
+ let signature = match await_signature_or_handle_manual_approval(
384
+ "broadcast",
385
+ &daemon_socket,
386
+ output_format,
387
+ &output_target,
388
+ sdk.broadcast_tx(tx),
389
+ )
390
+ .await?
391
+ {
392
+ Some(signature) => signature,
393
+ None => std::process::exit(2),
394
+ };
395
+ let output = AgentCommandOutput {
396
+ command: "broadcast".to_string(),
397
+ network: network.to_string(),
398
+ asset: asset.to_string(),
399
+ counterparty: counterparty.to_string(),
400
+ amount_wei: action.amount_wei().to_string(),
401
+ estimated_max_gas_spend_wei: Some(estimated_max_gas_spend_wei.to_string()),
402
+ tx_type: Some(format!("0x{tx_type:02x}")),
403
+ delegation_enabled: Some(delegation_enabled),
404
+ signature_hex: format!("0x{}", hex::encode(&signature.bytes)),
405
+ r_hex: signature.r_hex,
406
+ s_hex: signature.s_hex,
407
+ v: signature.v,
408
+ raw_tx_hex: signature.raw_tx_hex,
409
+ tx_hash_hex: signature.tx_hash_hex,
410
+ };
411
+ print_status("broadcast request signed", output_format, cli.quiet);
412
+ print_agent_output(&output, output_format, &output_target)?;
413
+ }
414
+ }
415
+
416
+ Ok(())
417
+ }
418
+
419
+ async fn await_signature_or_handle_manual_approval(
420
+ command: &str,
421
+ daemon_socket: &Path,
422
+ format: OutputFormat,
423
+ target: &OutputTarget,
424
+ future: impl std::future::Future<Output = Result<vault_domain::Signature, AgentSdkError>>,
425
+ ) -> Result<Option<vault_domain::Signature>> {
426
+ match future.await {
427
+ Ok(signature) => Ok(Some(signature)),
428
+ Err(AgentSdkError::Daemon(DaemonError::ManualApprovalRequired {
429
+ approval_request_id,
430
+ relay_url,
431
+ frontend_url,
432
+ })) => {
433
+ let output = ManualApprovalRequiredOutput {
434
+ command: command.to_string(),
435
+ approval_request_id: approval_request_id.to_string(),
436
+ relay_url,
437
+ frontend_url,
438
+ cli_approval_command: format!(
439
+ "wlfi-agent admin approve-manual-approval-request --approval-request-id {} --daemon-socket {}",
440
+ approval_request_id,
441
+ daemon_socket.display()
442
+ ),
443
+ };
444
+ print_manual_approval_required_output(&output, format, target)?;
445
+ Ok(None)
446
+ }
447
+ Err(err) => Err(err.into()),
448
+ }
449
+ }
450
+
451
+ fn print_manual_approval_required_output(
452
+ output: &ManualApprovalRequiredOutput,
453
+ format: OutputFormat,
454
+ target: &OutputTarget,
455
+ ) -> Result<()> {
456
+ let rendered = match format {
457
+ OutputFormat::Json => {
458
+ serde_json::to_string_pretty(output).context("failed to serialize output")?
459
+ }
460
+ OutputFormat::Text => {
461
+ let mut lines = vec![
462
+ format!("Command: {}", output.command),
463
+ format!("Approval Request ID: {}", output.approval_request_id),
464
+ ];
465
+ if let Some(frontend_url) = &output.frontend_url {
466
+ lines.push(format!("Frontend Approval URL: {frontend_url}"));
467
+ }
468
+ if let Some(relay_url) = &output.relay_url {
469
+ lines.push(format!("Relay URL: {relay_url}"));
470
+ }
471
+ lines.push(format!(
472
+ "CLI Approval Command: {}",
473
+ output.cli_approval_command
474
+ ));
475
+ lines.join(
476
+ "
477
+ ",
478
+ )
479
+ }
480
+ };
481
+ emit_output(&rendered, target)
482
+ }
483
+
484
+ fn resolve_daemon_socket_path(cli_value: Option<PathBuf>) -> Result<PathBuf> {
485
+ let path = match cli_value {
486
+ Some(path) => path,
487
+ None => wlfi_home_dir()?.join("daemon.sock"),
488
+ };
489
+
490
+ assert_root_owned_daemon_socket_path(&path).map_err(anyhow::Error::msg)
491
+ }
492
+
493
+ fn wlfi_home_dir() -> Result<PathBuf> {
494
+ if let Some(path) = std::env::var_os("WLFI_HOME") {
495
+ let candidate = PathBuf::from(path);
496
+ if candidate.as_os_str().is_empty() {
497
+ return Err(anyhow::anyhow!("WLFI_HOME must not be empty").into());
498
+ }
499
+ return Ok(candidate);
500
+ }
501
+
502
+ let Some(home) = std::env::var_os("HOME") else {
503
+ return Err(
504
+ anyhow::anyhow!("HOME is not set; use WLFI_HOME to choose config directory").into(),
505
+ );
506
+ };
507
+ Ok(PathBuf::from(home).join(".wlfi_agent"))
508
+ }
509
+
510
+ fn parse_positive_u128(input: &str) -> Result<u128, String> {
511
+ let parsed = input
512
+ .parse::<u128>()
513
+ .map_err(|_| "must be a valid unsigned integer".to_string())?;
514
+ if parsed == 0 {
515
+ return Err("must be greater than zero".to_string());
516
+ }
517
+ Ok(parsed)
518
+ }
519
+
520
+ fn parse_non_negative_u128(input: &str) -> Result<u128, String> {
521
+ input
522
+ .parse::<u128>()
523
+ .map_err(|_| "must be a valid unsigned integer".to_string())
524
+ }
525
+
526
+ fn parse_tx_type_u8(input: &str) -> Result<u8, String> {
527
+ let trimmed = input.trim();
528
+ let parsed = if let Some(hex_value) = trimmed
529
+ .strip_prefix("0x")
530
+ .or_else(|| trimmed.strip_prefix("0X"))
531
+ {
532
+ u8::from_str_radix(hex_value, 16)
533
+ } else {
534
+ trimmed.parse::<u8>()
535
+ };
536
+ parsed.map_err(|_| "must be a valid u8 (decimal or 0x-prefixed hex)".to_string())
537
+ }
538
+
539
+ fn parse_positive_u64(input: &str) -> Result<u64, String> {
540
+ let parsed = input
541
+ .parse::<u64>()
542
+ .map_err(|_| "must be a valid unsigned integer".to_string())?;
543
+ if parsed == 0 {
544
+ return Err("must be greater than zero".to_string());
545
+ }
546
+ Ok(parsed)
547
+ }
548
+
549
+ fn parse_non_negative_u64(input: &str) -> Result<u64, String> {
550
+ input
551
+ .parse::<u64>()
552
+ .map_err(|_| "must be a valid unsigned integer".to_string())
553
+ }
554
+
555
+ #[cfg(test)]
556
+ mod tests {
557
+ use super::{
558
+ ensure_output_parent, resolve_agent_auth_token, resolve_output_format,
559
+ resolve_output_target, should_print_status, write_output_file, Cli, Commands, OutputFormat,
560
+ OutputTarget,
561
+ };
562
+ use clap::Parser;
563
+ use std::fs;
564
+ use std::time::{SystemTime, UNIX_EPOCH};
565
+
566
+ const TEST_AGENT_KEY_ID: &str = "11111111-1111-1111-1111-111111111111";
567
+
568
+ #[test]
569
+ fn resolve_output_format_defaults_to_text() {
570
+ let format = resolve_output_format(None, false).expect("format");
571
+ assert!(matches!(format, OutputFormat::Text));
572
+ }
573
+
574
+ #[test]
575
+ fn resolve_output_format_json_shortcut() {
576
+ let format = resolve_output_format(None, true).expect("format");
577
+ assert!(matches!(format, OutputFormat::Json));
578
+ }
579
+
580
+ #[test]
581
+ fn resolve_output_format_rejects_text_conflict_with_json_shortcut() {
582
+ let err = resolve_output_format(Some(OutputFormat::Text), true).expect_err("must fail");
583
+ assert!(err.to_string().contains("--format text"));
584
+ }
585
+
586
+ #[test]
587
+ fn resolve_output_format_allows_json_redundancy() {
588
+ let format = resolve_output_format(Some(OutputFormat::Json), true).expect("format");
589
+ assert!(matches!(format, OutputFormat::Json));
590
+ }
591
+
592
+ #[test]
593
+ fn resolve_agent_auth_token_rejects_whitespace_only() {
594
+ let err = resolve_agent_auth_token(Some(" \t ".to_string()), None, false, false)
595
+ .expect_err("must fail");
596
+ assert!(err.to_string().contains("must not be empty or whitespace"));
597
+ }
598
+
599
+ #[test]
600
+ fn resolve_agent_auth_token_rejects_cli_argument_source() {
601
+ let err = resolve_agent_auth_token(Some("secret".to_string()), None, false, false)
602
+ .expect_err("must fail");
603
+ assert!(err
604
+ .to_string()
605
+ .contains("--agent-auth-token is disabled for security"));
606
+ }
607
+
608
+ #[test]
609
+ fn resolve_agent_auth_token_accepts_environment_source() {
610
+ let token = resolve_agent_auth_token(None, Some("secret".to_string()), false, false)
611
+ .expect("environment token");
612
+ assert_eq!(token, "secret");
613
+ }
614
+
615
+ #[test]
616
+ fn status_printing_only_in_text_when_not_quiet() {
617
+ assert!(should_print_status(OutputFormat::Text, false));
618
+ assert!(!should_print_status(OutputFormat::Text, true));
619
+ assert!(!should_print_status(OutputFormat::Json, false));
620
+ }
621
+
622
+ #[test]
623
+ fn resolve_output_target_maps_dash_to_stdout() {
624
+ let target = resolve_output_target(Some("-".into()), false).expect("target");
625
+ assert!(matches!(target, OutputTarget::Stdout));
626
+ }
627
+
628
+ #[test]
629
+ fn resolve_output_target_rejects_overwrite_with_stdout() {
630
+ let err = resolve_output_target(Some("-".into()), true).expect_err("must fail");
631
+ assert!(err.to_string().contains("--overwrite cannot be used"));
632
+ }
633
+
634
+ #[test]
635
+ fn output_file_write_respects_overwrite_flag() {
636
+ let unique = SystemTime::now()
637
+ .duration_since(UNIX_EPOCH)
638
+ .expect("time")
639
+ .as_nanos();
640
+ let path = std::env::temp_dir().join(format!(
641
+ "wlfi-agent-cli-output-{}-{}.txt",
642
+ std::process::id(),
643
+ unique
644
+ ));
645
+ std::fs::write(&path, "existing\n").expect("seed");
646
+
647
+ let err = write_output_file(&path, "next", false).expect_err("must fail");
648
+ assert!(err.to_string().contains("already exists"));
649
+
650
+ write_output_file(&path, "next", true).expect("overwrite");
651
+ let updated = std::fs::read_to_string(&path).expect("read");
652
+ assert_eq!(updated, "next\n");
653
+
654
+ std::fs::remove_file(&path).expect("cleanup");
655
+ }
656
+
657
+ #[cfg(unix)]
658
+ #[test]
659
+ fn output_file_write_rejects_symlink_target() {
660
+ use std::os::unix::fs::symlink;
661
+
662
+ let unique = SystemTime::now()
663
+ .duration_since(UNIX_EPOCH)
664
+ .expect("time")
665
+ .as_nanos();
666
+ let target = std::env::temp_dir().join(format!(
667
+ "wlfi-agent-cli-symlink-target-{}-{}.txt",
668
+ std::process::id(),
669
+ unique
670
+ ));
671
+ let link = std::env::temp_dir().join(format!(
672
+ "wlfi-agent-cli-symlink-link-{}-{}.txt",
673
+ std::process::id(),
674
+ unique
675
+ ));
676
+ std::fs::write(&target, "seed\n").expect("seed");
677
+ symlink(&target, &link).expect("symlink");
678
+
679
+ let err = ensure_output_parent(&link).expect_err("must fail");
680
+ assert!(err.to_string().contains("must not be a symlink"));
681
+
682
+ std::fs::remove_file(&link).expect("cleanup link");
683
+ std::fs::remove_file(&target).expect("cleanup target");
684
+ }
685
+
686
+ #[test]
687
+ #[cfg(unix)]
688
+ fn resolve_daemon_socket_path_rejects_non_socket_files() {
689
+ use std::os::unix::fs::PermissionsExt;
690
+
691
+ let unique = SystemTime::now()
692
+ .duration_since(UNIX_EPOCH)
693
+ .expect("time")
694
+ .as_nanos();
695
+ let root = std::env::temp_dir().join(format!("wg-{unique:x}"));
696
+ fs::create_dir_all(&root).expect("create root directory");
697
+ fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
698
+ .expect("secure root directory permissions");
699
+
700
+ let socket_path = root.join("daemon.sock");
701
+ fs::write(&socket_path, "not a socket").expect("write file");
702
+
703
+ let err = super::resolve_daemon_socket_path(Some(socket_path)).expect_err("must reject");
704
+ assert!(err.to_string().contains("must be a unix socket"));
705
+
706
+ fs::remove_dir_all(&root).expect("cleanup temp tree");
707
+ }
708
+
709
+ #[test]
710
+ fn canonical_transfer_command_is_accepted() {
711
+ let cli = Cli::try_parse_from([
712
+ "wlfi-agent-agent",
713
+ "--agent-key-id",
714
+ TEST_AGENT_KEY_ID,
715
+ "--agent-auth-token",
716
+ "test-auth-token",
717
+ "--daemon-socket",
718
+ "/tmp/wlfi.sock",
719
+ "transfer",
720
+ "--network",
721
+ "1",
722
+ "--token",
723
+ "0x1000000000000000000000000000000000000000",
724
+ "--to",
725
+ "0x2000000000000000000000000000000000000000",
726
+ "--amount-wei",
727
+ "1",
728
+ ])
729
+ .expect("parse");
730
+ assert!(matches!(cli.command, Commands::Transfer { .. }));
731
+ }
732
+
733
+ #[test]
734
+ fn canonical_transfer_native_command_is_accepted() {
735
+ let cli = Cli::try_parse_from([
736
+ "wlfi-agent-agent",
737
+ "--agent-key-id",
738
+ TEST_AGENT_KEY_ID,
739
+ "--agent-auth-token",
740
+ "test-auth-token",
741
+ "--daemon-socket",
742
+ "/tmp/wlfi.sock",
743
+ "transfer-native",
744
+ "--network",
745
+ "1",
746
+ "--to",
747
+ "0x2000000000000000000000000000000000000000",
748
+ "--amount-wei",
749
+ "1",
750
+ ])
751
+ .expect("parse");
752
+ assert!(matches!(cli.command, Commands::TransferNative { .. }));
753
+ }
754
+
755
+ #[test]
756
+ fn canonical_approve_command_is_accepted() {
757
+ let cli = Cli::try_parse_from([
758
+ "wlfi-agent-agent",
759
+ "--agent-key-id",
760
+ TEST_AGENT_KEY_ID,
761
+ "--agent-auth-token",
762
+ "test-auth-token",
763
+ "--daemon-socket",
764
+ "/tmp/wlfi.sock",
765
+ "approve",
766
+ "--network",
767
+ "1",
768
+ "--token",
769
+ "0x1000000000000000000000000000000000000000",
770
+ "--spender",
771
+ "0x3000000000000000000000000000000000000000",
772
+ "--amount-wei",
773
+ "1",
774
+ ])
775
+ .expect("parse");
776
+ assert!(matches!(cli.command, Commands::Approve { .. }));
777
+ }
778
+
779
+ #[test]
780
+ fn canonical_broadcast_command_is_accepted() {
781
+ let cli = Cli::try_parse_from([
782
+ "wlfi-agent-agent",
783
+ "--agent-key-id",
784
+ TEST_AGENT_KEY_ID,
785
+ "--agent-auth-token",
786
+ "test-auth-token",
787
+ "--daemon-socket",
788
+ "/tmp/wlfi.sock",
789
+ "broadcast",
790
+ "--network",
791
+ "1",
792
+ "--to",
793
+ "0x3000000000000000000000000000000000000000",
794
+ "--value-wei",
795
+ "0",
796
+ "--data-hex",
797
+ "0xdeadbeef",
798
+ "--gas-limit",
799
+ "21000",
800
+ "--max-fee-per-gas-wei",
801
+ "1000000000",
802
+ "--tx-type",
803
+ "0x02",
804
+ ])
805
+ .expect("parse");
806
+ assert!(matches!(cli.command, Commands::Broadcast { .. }));
807
+ }
808
+
809
+ #[test]
810
+ fn agent_auth_token_flag_conflicts_with_stdin_flag() {
811
+ let err = Cli::try_parse_from([
812
+ "wlfi-agent-agent",
813
+ "--agent-key-id",
814
+ TEST_AGENT_KEY_ID,
815
+ "--daemon-socket",
816
+ "/tmp/wlfi.sock",
817
+ "--agent-auth-token",
818
+ "test-auth-token",
819
+ "--agent-auth-token-stdin",
820
+ "transfer-native",
821
+ "--network",
822
+ "1",
823
+ "--to",
824
+ "0x2000000000000000000000000000000000000000",
825
+ "--amount-wei",
826
+ "1",
827
+ ])
828
+ .expect_err("must fail");
829
+ let rendered = err.to_string();
830
+ assert!(rendered.contains("--agent-auth-token"));
831
+ assert!(rendered.contains("--agent-auth-token-stdin"));
832
+ }
833
+ }