@wlfi-agent/cli 1.4.12 → 1.4.14

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 +49 -43
  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 -1894
  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 -1894
@@ -0,0 +1,644 @@
1
+ use std::collections::BTreeSet;
2
+ use std::io::Read;
3
+ use std::path::{Path, PathBuf};
4
+ use std::sync::Arc;
5
+
6
+ use anyhow::{anyhow, bail, Context, Result};
7
+ use clap::{Parser, ValueEnum};
8
+ use vault_daemon::{DaemonConfig, InMemoryDaemon, PersistentStoreConfig};
9
+ use vault_signer::{SecureEnclaveSignerBackend, SoftwareSignerBackend};
10
+ use vault_transport_unix::UnixDaemonServer;
11
+ use zeroize::Zeroize;
12
+
13
+ mod relay_sync;
14
+
15
+ const MAX_SECRET_STDIN_BYTES: u64 = 16 * 1024;
16
+
17
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
18
+ enum SignerBackendKind {
19
+ SecureEnclave,
20
+ Software,
21
+ }
22
+
23
+ #[derive(Debug, Parser)]
24
+ #[command(name = "wlfi-agent-daemon")]
25
+ #[command(about = "Long-running local daemon process for policy-gated signing")]
26
+ struct Cli {
27
+ #[arg(
28
+ long,
29
+ default_value_t = false,
30
+ help = "Read vault password from stdin (trailing newlines are trimmed)"
31
+ )]
32
+ vault_password_stdin: bool,
33
+ #[arg(
34
+ long,
35
+ default_value_t = false,
36
+ help = "Do not prompt for password; require --vault-password-stdin"
37
+ )]
38
+ non_interactive: bool,
39
+ #[arg(
40
+ long,
41
+ env = "WLFI_STATE_FILE",
42
+ value_name = "PATH",
43
+ help = "Encrypted persistent daemon state file path (default: $WLFI_HOME/daemon-state.enc or ~/.wlfi_agent/daemon-state.enc)"
44
+ )]
45
+ state_file: Option<PathBuf>,
46
+ #[arg(
47
+ long,
48
+ env = "WLFI_DAEMON_SOCKET",
49
+ value_name = "PATH",
50
+ help = "Unix socket path for daemon RPC (default: $WLFI_HOME/daemon.sock or ~/.wlfi_agent/daemon.sock)"
51
+ )]
52
+ daemon_socket: Option<PathBuf>,
53
+ #[arg(
54
+ long,
55
+ env = "WLFI_SECURE_ENCLAVE_LABEL_PREFIX",
56
+ default_value = "com.wlfi.vault",
57
+ value_name = "PREFIX",
58
+ help = "Secure Enclave key label prefix"
59
+ )]
60
+ secure_enclave_label_prefix: String,
61
+ #[arg(
62
+ long,
63
+ env = "WLFI_SIGNER_BACKEND",
64
+ value_enum,
65
+ default_value_t = SignerBackendKind::SecureEnclave,
66
+ value_name = "secure-enclave|software",
67
+ help = "Signer backend for daemon key creation and signing"
68
+ )]
69
+ signer_backend: SignerBackendKind,
70
+ #[arg(
71
+ long = "allow-admin-euid",
72
+ env = "WLFI_ALLOW_ADMIN_EUID",
73
+ value_name = "UID[,UID...]",
74
+ value_delimiter = ',',
75
+ num_args = 1..,
76
+ help = "Additional non-root admin client euid(s) allowed to connect for privileged RPCs. Root (0) is always allowed."
77
+ )]
78
+ allow_admin_euid: Vec<u32>,
79
+ #[arg(
80
+ long = "allow-agent-euid",
81
+ env = "WLFI_ALLOW_AGENT_EUID",
82
+ value_name = "UID[,UID...]",
83
+ value_delimiter = ',',
84
+ num_args = 1..,
85
+ help = "Additional non-root agent client euid(s) allowed to connect for signing and nonce RPCs. Root (0) is always allowed."
86
+ )]
87
+ allow_agent_euid: Vec<u32>,
88
+ #[arg(
89
+ long = "allow-client-euid",
90
+ env = "WLFI_ALLOW_CLIENT_EUID",
91
+ value_name = "UID[,UID...]",
92
+ value_delimiter = ',',
93
+ num_args = 1..,
94
+ help = "Legacy compatibility alias that grants the same non-root client euid(s) both admin and agent access. Root (0) is always allowed."
95
+ )]
96
+ allow_client_euid: Vec<u32>,
97
+ }
98
+
99
+ #[derive(Debug, Clone)]
100
+ struct AllowedPeerEuids {
101
+ admin: BTreeSet<u32>,
102
+ agent: BTreeSet<u32>,
103
+ }
104
+
105
+ struct StateFileLock {
106
+ #[cfg(unix)]
107
+ file: std::fs::File,
108
+ }
109
+
110
+ impl Drop for StateFileLock {
111
+ fn drop(&mut self) {
112
+ #[cfg(unix)]
113
+ {
114
+ use std::os::fd::AsRawFd;
115
+ // SAFETY: valid fd with best-effort unlock during drop.
116
+ unsafe {
117
+ let _ = libc::flock(self.file.as_raw_fd(), libc::LOCK_UN);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ #[tokio::main]
124
+ async fn main() -> Result<()> {
125
+ let cli = Cli::parse();
126
+ validate_signer_backend_runtime(cli.signer_backend)?;
127
+ let mut vault_password = resolve_vault_password(cli.vault_password_stdin, cli.non_interactive)?;
128
+ let state_file = resolve_state_file_path(cli.state_file)?;
129
+ let daemon_socket = resolve_socket_path(cli.daemon_socket)?;
130
+ let _state_lock = acquire_state_file_lock(&state_file)?;
131
+
132
+ let allowed_peer_euids = resolve_allowed_peer_euids(
133
+ &cli.allow_admin_euid,
134
+ &cli.allow_agent_euid,
135
+ &cli.allow_client_euid,
136
+ )?;
137
+ if !cli.allow_client_euid.is_empty() {
138
+ eprintln!(
139
+ "==> warning: --allow-client-euid grants both admin and agent access; prefer --allow-admin-euid and --allow-agent-euid"
140
+ );
141
+ }
142
+ let server = UnixDaemonServer::bind_with_allowed_peer_euids(
143
+ daemon_socket.clone(),
144
+ allowed_peer_euids.admin.clone(),
145
+ allowed_peer_euids.agent.clone(),
146
+ )
147
+ .await
148
+ .with_context(|| {
149
+ format!(
150
+ "failed to bind daemon socket at {}",
151
+ daemon_socket.display()
152
+ )
153
+ })?;
154
+
155
+ eprintln!(
156
+ "==> daemon listening on {} (allowed admin euid(s): {}; allowed agent euid(s): {})",
157
+ daemon_socket.display(),
158
+ format_allowed_euids(&allowed_peer_euids.admin),
159
+ format_allowed_euids(&allowed_peer_euids.agent)
160
+ );
161
+ eprintln!("==> press Ctrl+C to stop");
162
+
163
+ match cli.signer_backend {
164
+ SignerBackendKind::SecureEnclave => {
165
+ let daemon = Arc::new(
166
+ InMemoryDaemon::new_with_persistent_store(
167
+ &vault_password,
168
+ SecureEnclaveSignerBackend::new(cli.secure_enclave_label_prefix),
169
+ DaemonConfig::default(),
170
+ PersistentStoreConfig::new(state_file),
171
+ )
172
+ .context("failed to initialize daemon")?,
173
+ );
174
+ let relay_task =
175
+ relay_sync::spawn_relay_sync_task(Arc::clone(&daemon), "secure-enclave");
176
+ vault_password.zeroize();
177
+ server
178
+ .run_until_shutdown(daemon, async {
179
+ let _ = tokio::signal::ctrl_c().await;
180
+ relay_task.abort();
181
+ })
182
+ .await
183
+ .context("daemon server loop failed")?;
184
+ }
185
+ SignerBackendKind::Software => {
186
+ let daemon = Arc::new(
187
+ InMemoryDaemon::new_with_persistent_store(
188
+ &vault_password,
189
+ SoftwareSignerBackend::default(),
190
+ DaemonConfig::default(),
191
+ PersistentStoreConfig::new(state_file),
192
+ )
193
+ .context("failed to initialize daemon")?,
194
+ );
195
+ let relay_task =
196
+ relay_sync::spawn_relay_sync_task(Arc::clone(&daemon), "secure-enclave");
197
+ vault_password.zeroize();
198
+ server
199
+ .run_until_shutdown(daemon, async {
200
+ let _ = tokio::signal::ctrl_c().await;
201
+ relay_task.abort();
202
+ })
203
+ .await
204
+ .context("daemon server loop failed")?;
205
+ }
206
+ }
207
+
208
+ Ok(())
209
+ }
210
+
211
+ fn validate_signer_backend_runtime(backend: SignerBackendKind) -> Result<()> {
212
+ #[cfg(not(target_os = "macos"))]
213
+ {
214
+ if matches!(backend, SignerBackendKind::SecureEnclave) {
215
+ bail!("Secure Enclave daemon mode is supported only on macOS");
216
+ }
217
+ }
218
+
219
+ #[cfg(target_os = "macos")]
220
+ {
221
+ let euid = nix::unistd::geteuid().as_raw();
222
+ if matches!(backend, SignerBackendKind::SecureEnclave) && euid != 0 {
223
+ bail!("secure enclave daemon mode requires root daemon context (current euid: {euid})");
224
+ }
225
+ }
226
+
227
+ Ok(())
228
+ }
229
+
230
+ fn resolve_vault_password(from_stdin: bool, non_interactive: bool) -> Result<String> {
231
+ if from_stdin {
232
+ return read_secret_from_reader(std::io::stdin(), "vault password");
233
+ }
234
+
235
+ if non_interactive {
236
+ bail!("vault password is required in non-interactive mode; use --vault-password-stdin");
237
+ }
238
+
239
+ let prompted =
240
+ rpassword::prompt_password("Vault password: ").context("failed to read password input")?;
241
+ validate_password(prompted, "prompt")
242
+ }
243
+
244
+ fn validate_password(mut password: String, source: &str) -> Result<String> {
245
+ if password.as_bytes().len() > MAX_SECRET_STDIN_BYTES as usize {
246
+ password.zeroize();
247
+ bail!("vault password from {source} must not exceed {MAX_SECRET_STDIN_BYTES} bytes");
248
+ }
249
+ if password.trim().is_empty() {
250
+ password.zeroize();
251
+ bail!("vault password from {source} must not be empty or whitespace");
252
+ }
253
+ Ok(password)
254
+ }
255
+
256
+ fn read_secret_from_reader(mut reader: impl std::io::Read, label: &str) -> Result<String> {
257
+ let mut raw = String::new();
258
+ reader
259
+ .by_ref()
260
+ .take(MAX_SECRET_STDIN_BYTES + 1)
261
+ .read_to_string(&mut raw)
262
+ .with_context(|| format!("failed to read {label} from stdin"))?;
263
+ if raw.as_bytes().len() > MAX_SECRET_STDIN_BYTES as usize {
264
+ raw.zeroize();
265
+ bail!("{label} must not exceed {MAX_SECRET_STDIN_BYTES} bytes");
266
+ }
267
+ let secret = raw.trim_end_matches(['\r', '\n']).to_string();
268
+ raw.zeroize();
269
+ validate_password(secret, "stdin")
270
+ }
271
+
272
+ fn resolve_allowed_peer_euids(
273
+ configured_admin: &[u32],
274
+ configured_agent: &[u32],
275
+ configured_legacy: &[u32],
276
+ ) -> Result<AllowedPeerEuids> {
277
+ resolve_allowed_peer_euids_with_sudo_uid(
278
+ configured_admin,
279
+ configured_agent,
280
+ configured_legacy,
281
+ None,
282
+ )
283
+ }
284
+
285
+ fn resolve_allowed_peer_euids_with_sudo_uid(
286
+ configured_admin: &[u32],
287
+ configured_agent: &[u32],
288
+ configured_legacy: &[u32],
289
+ _sudo_uid: Option<u32>,
290
+ ) -> Result<AllowedPeerEuids> {
291
+ let mut admin = BTreeSet::from([0]);
292
+ let mut agent = BTreeSet::from([0]);
293
+
294
+ admin.extend(configured_legacy.iter().copied());
295
+ admin.extend(configured_admin.iter().copied());
296
+ agent.extend(configured_legacy.iter().copied());
297
+ agent.extend(configured_agent.iter().copied());
298
+
299
+ Ok(AllowedPeerEuids { admin, agent })
300
+ }
301
+
302
+ fn format_allowed_euids(values: &BTreeSet<u32>) -> String {
303
+ values
304
+ .iter()
305
+ .map(ToString::to_string)
306
+ .collect::<Vec<_>>()
307
+ .join(",")
308
+ }
309
+
310
+ fn resolve_state_file_path(cli_value: Option<PathBuf>) -> Result<PathBuf> {
311
+ let path = match cli_value {
312
+ Some(path) => path,
313
+ None => default_state_file_path()?,
314
+ };
315
+ ensure_file_parent(&path, "state")?;
316
+ Ok(path)
317
+ }
318
+
319
+ fn resolve_socket_path(cli_value: Option<PathBuf>) -> Result<PathBuf> {
320
+ let path = match cli_value {
321
+ Some(path) => path,
322
+ None => default_socket_path()?,
323
+ };
324
+ ensure_file_parent(&path, "socket")?;
325
+ Ok(path)
326
+ }
327
+
328
+ fn acquire_state_file_lock(path: &Path) -> Result<StateFileLock> {
329
+ let lock_path = lock_path(path);
330
+ ensure_file_parent(&lock_path, "state lock")?;
331
+ let mut options = std::fs::OpenOptions::new();
332
+ options.read(true).write(true).create(true);
333
+ #[cfg(unix)]
334
+ {
335
+ use std::os::unix::fs::OpenOptionsExt;
336
+ options.mode(0o600);
337
+ options.custom_flags(libc::O_NOFOLLOW);
338
+ }
339
+ let file = options
340
+ .open(&lock_path)
341
+ .with_context(|| format!("failed to open state lock file {}", lock_path.display()))?;
342
+ #[cfg(unix)]
343
+ {
344
+ use std::os::fd::AsRawFd;
345
+ // SAFETY: valid fd for advisory lock in-process lifetime.
346
+ unsafe {
347
+ if libc::flock(file.as_raw_fd(), libc::LOCK_EX) != 0 {
348
+ return Err(anyhow!(
349
+ "failed to acquire state lock on {}",
350
+ lock_path.display()
351
+ ));
352
+ }
353
+ }
354
+ }
355
+ Ok(StateFileLock {
356
+ #[cfg(unix)]
357
+ file,
358
+ })
359
+ }
360
+
361
+ fn default_state_file_path() -> Result<PathBuf> {
362
+ Ok(wlfi_home_dir()?.join("daemon-state.enc"))
363
+ }
364
+
365
+ fn default_socket_path() -> Result<PathBuf> {
366
+ Ok(wlfi_home_dir()?.join("daemon.sock"))
367
+ }
368
+
369
+ fn wlfi_home_dir() -> Result<PathBuf> {
370
+ if let Some(path) = std::env::var_os("WLFI_HOME") {
371
+ let candidate = PathBuf::from(path);
372
+ if candidate.as_os_str().is_empty() {
373
+ bail!("WLFI_HOME must not be empty");
374
+ }
375
+ return Ok(candidate);
376
+ }
377
+
378
+ let Some(home) = std::env::var_os("HOME") else {
379
+ bail!("HOME is not set; use WLFI_HOME to choose config directory");
380
+ };
381
+ let mut path = PathBuf::from(home);
382
+ path.push(".wlfi_agent");
383
+ Ok(path)
384
+ }
385
+
386
+ fn ensure_file_parent(path: &Path, label: &str) -> Result<()> {
387
+ if is_symlink(path)? {
388
+ bail!("{label} path '{}' must not be a symlink", path.display());
389
+ }
390
+ if let Some(parent) = path.parent() {
391
+ if !parent.as_os_str().is_empty() {
392
+ std::fs::create_dir_all(parent)
393
+ .with_context(|| format!("failed to create directory {}", parent.display()))?;
394
+ if is_symlink(parent)? {
395
+ bail!(
396
+ "{label} directory '{}' must not be a symlink",
397
+ parent.display()
398
+ );
399
+ }
400
+ ensure_secure_directory_owner(parent, label)?;
401
+ }
402
+ }
403
+ Ok(())
404
+ }
405
+
406
+ #[cfg(unix)]
407
+ fn assert_allowed_directory_owner(
408
+ path: &Path,
409
+ owner_uid: u32,
410
+ effective_uid: u32,
411
+ label: &str,
412
+ ) -> Result<()> {
413
+ if owner_uid == 0 {
414
+ return Ok(());
415
+ }
416
+
417
+ if effective_uid == 0 {
418
+ bail!(
419
+ "{label} directory '{}' must be owned by root; found uid {owner_uid}",
420
+ path.display()
421
+ );
422
+ }
423
+
424
+ if owner_uid != effective_uid {
425
+ bail!(
426
+ "{label} directory '{}' must be owned by current user or root; found uid {owner_uid}",
427
+ path.display()
428
+ );
429
+ }
430
+
431
+ Ok(())
432
+ }
433
+
434
+ #[cfg(unix)]
435
+ fn ensure_secure_directory_owner(path: &Path, label: &str) -> Result<()> {
436
+ use std::os::unix::fs::MetadataExt;
437
+
438
+ const STICKY_BIT_MODE: u32 = 0o1000;
439
+
440
+ fn validate_directory(
441
+ path: &Path,
442
+ metadata: &std::fs::Metadata,
443
+ effective_uid: u32,
444
+ label: &str,
445
+ allow_sticky_group_other_write: bool,
446
+ ) -> Result<()> {
447
+ if !metadata.is_dir() {
448
+ bail!("{label} directory '{}' is not a directory", path.display());
449
+ }
450
+ assert_allowed_directory_owner(path, metadata.uid(), effective_uid, label)?;
451
+ let mode = metadata.mode() & 0o7777;
452
+ if mode & 0o022 != 0 && !(allow_sticky_group_other_write && mode & STICKY_BIT_MODE != 0) {
453
+ bail!(
454
+ "{label} directory '{}' must not be writable by group/other (current mode {:o})",
455
+ path.display(),
456
+ mode & 0o777
457
+ );
458
+ }
459
+ Ok(())
460
+ }
461
+
462
+ let effective_uid = nix::unistd::geteuid().as_raw();
463
+ let metadata = std::fs::metadata(path)
464
+ .with_context(|| format!("failed to inspect {label} directory {}", path.display()))?;
465
+ validate_directory(path, &metadata, effective_uid, label, false)?;
466
+
467
+ let canonical = std::fs::canonicalize(path).with_context(|| {
468
+ format!(
469
+ "failed to canonicalize {label} directory {}",
470
+ path.display()
471
+ )
472
+ })?;
473
+ for ancestor in canonical.ancestors().skip(1) {
474
+ let metadata = std::fs::metadata(ancestor).with_context(|| {
475
+ format!(
476
+ "failed to inspect ancestor {label} directory {}",
477
+ ancestor.display()
478
+ )
479
+ })?;
480
+ validate_directory(ancestor, &metadata, effective_uid, label, true)?;
481
+ }
482
+ Ok(())
483
+ }
484
+
485
+ #[cfg(not(unix))]
486
+ fn ensure_secure_directory_owner(_path: &Path, _label: &str) -> Result<()> {
487
+ Ok(())
488
+ }
489
+
490
+ fn is_symlink(path: &Path) -> Result<bool> {
491
+ match std::fs::symlink_metadata(path) {
492
+ Ok(metadata) => Ok(metadata.file_type().is_symlink()),
493
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
494
+ Err(err) => {
495
+ Err(err).with_context(|| format!("failed to inspect metadata for {}", path.display()))
496
+ }
497
+ }
498
+ }
499
+
500
+ fn lock_path(path: &Path) -> PathBuf {
501
+ let mut lock = path.as_os_str().to_os_string();
502
+ lock.push(".lock");
503
+ PathBuf::from(lock)
504
+ }
505
+
506
+ #[cfg(test)]
507
+ mod tests {
508
+ use super::{
509
+ ensure_file_parent, resolve_allowed_peer_euids_with_sudo_uid, resolve_vault_password,
510
+ validate_password, Cli,
511
+ };
512
+ use clap::Parser;
513
+ use std::collections::BTreeSet;
514
+ use std::path::Path;
515
+ use std::time::{SystemTime, UNIX_EPOCH};
516
+
517
+ #[test]
518
+ fn validate_password_rejects_oversized_non_stdin_secret() {
519
+ let err = validate_password("a".repeat((16 * 1024) + 1), "argument or environment")
520
+ .expect_err("must fail");
521
+ assert!(err.to_string().contains("must not exceed"));
522
+ }
523
+
524
+ #[test]
525
+ fn cli_rejects_inline_vault_password_argument() {
526
+ let err = Cli::try_parse_from([
527
+ "wlfi-agent-daemon",
528
+ "--vault-password",
529
+ "vault-secret",
530
+ "--non-interactive",
531
+ ])
532
+ .expect_err("must reject");
533
+ assert!(err.to_string().contains("--vault-password"));
534
+ }
535
+
536
+ #[test]
537
+ fn resolve_vault_password_requires_stdin_in_non_interactive_mode() {
538
+ let err = resolve_vault_password(false, true).expect_err("must fail");
539
+ assert!(err.to_string().contains("use --vault-password-stdin"));
540
+ }
541
+
542
+ #[cfg(unix)]
543
+ #[test]
544
+ fn ensure_file_parent_accepts_current_user_owned_directory() {
545
+ let unique = SystemTime::now()
546
+ .duration_since(UNIX_EPOCH)
547
+ .expect("time")
548
+ .as_nanos();
549
+ let parent = std::env::temp_dir().join(format!(
550
+ "wlfi-daemon-parent-{}-{}",
551
+ std::process::id(),
552
+ unique
553
+ ));
554
+ std::fs::create_dir_all(&parent).expect("create parent");
555
+ let path = parent.join("daemon-state.enc");
556
+ ensure_file_parent(&path, "state").expect("current-user-owned directory should pass");
557
+ std::fs::remove_dir_all(&parent).expect("cleanup");
558
+ }
559
+
560
+ #[cfg(unix)]
561
+ #[test]
562
+ fn ensure_file_parent_rejects_group_writable_ancestor_directory() {
563
+ use std::os::unix::fs::PermissionsExt;
564
+
565
+ let unique = SystemTime::now()
566
+ .duration_since(UNIX_EPOCH)
567
+ .expect("time")
568
+ .as_nanos();
569
+ let root = std::env::temp_dir().join(format!(
570
+ "wlfi-daemon-parent-ancestor-{}-{}",
571
+ std::process::id(),
572
+ unique
573
+ ));
574
+ let shared = root.join("shared");
575
+ let nested = shared.join("nested");
576
+ std::fs::create_dir_all(&nested).expect("create nested directory");
577
+ std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o777))
578
+ .expect("set insecure ancestor permissions");
579
+ std::fs::set_permissions(&nested, std::fs::Permissions::from_mode(0o700))
580
+ .expect("set nested permissions");
581
+
582
+ let path = nested.join("daemon-state.enc");
583
+ let err = ensure_file_parent(&path, "state").expect_err("must reject");
584
+ assert!(err
585
+ .to_string()
586
+ .contains("must not be writable by group/other"));
587
+
588
+ std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o700))
589
+ .expect("restore cleanup permissions");
590
+ std::fs::remove_dir_all(&root).expect("cleanup");
591
+ }
592
+
593
+ #[test]
594
+ fn resolve_allowed_peer_euids_keeps_root_only_when_allowlists_are_omitted() {
595
+ let resolved = resolve_allowed_peer_euids_with_sudo_uid(&[], &[], &[], Some(501))
596
+ .expect("allowed peer euids");
597
+
598
+ assert_eq!(resolved.admin, BTreeSet::from([0]));
599
+ assert_eq!(resolved.agent, BTreeSet::from([0]));
600
+ }
601
+
602
+ #[test]
603
+ fn resolve_allowed_peer_euids_supports_split_admin_and_agent_allowlists() {
604
+ let resolved = resolve_allowed_peer_euids_with_sudo_uid(&[11], &[22], &[33], Some(501))
605
+ .expect("allowed peer euids");
606
+
607
+ assert_eq!(resolved.admin, BTreeSet::from([0, 11, 33]));
608
+ assert_eq!(resolved.agent, BTreeSet::from([0, 22, 33]));
609
+ }
610
+
611
+ #[cfg(unix)]
612
+ #[test]
613
+ fn assert_allowed_directory_owner_rejects_non_root_owner_for_root_runtime() {
614
+ let err = super::assert_allowed_directory_owner(Path::new("/tmp/wlfi"), 501, 0, "state")
615
+ .expect_err("root runtime must reject non-root owner");
616
+
617
+ assert!(err.to_string().contains("must be owned by root"));
618
+ }
619
+
620
+ #[cfg(unix)]
621
+ #[test]
622
+ fn assert_allowed_directory_owner_allows_root_owner_for_root_runtime() {
623
+ super::assert_allowed_directory_owner(Path::new("/tmp/wlfi"), 0, 0, "state")
624
+ .expect("root runtime should allow root-owned directories");
625
+ }
626
+
627
+ #[cfg(unix)]
628
+ #[test]
629
+ fn assert_allowed_directory_owner_allows_current_user_for_non_root_runtime() {
630
+ super::assert_allowed_directory_owner(Path::new("/tmp/wlfi"), 501, 501, "state")
631
+ .expect("non-root runtime should allow current-user-owned directories");
632
+ }
633
+
634
+ #[cfg(unix)]
635
+ #[test]
636
+ fn assert_allowed_directory_owner_rejects_other_user_for_non_root_runtime() {
637
+ let err = super::assert_allowed_directory_owner(Path::new("/tmp/wlfi"), 502, 501, "state")
638
+ .expect_err("non-root runtime must reject another user's directory");
639
+
640
+ assert!(err
641
+ .to_string()
642
+ .contains("must be owned by current user or root"));
643
+ }
644
+ }