@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,1021 @@
1
+ #[tokio::test]
2
+ async fn broadcast_without_nonce_reservation_is_rejected() {
3
+ let daemon = InMemoryDaemon::new(
4
+ "vault-password",
5
+ SoftwareSignerBackend::default(),
6
+ DaemonConfig::default(),
7
+ )
8
+ .expect("daemon");
9
+
10
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
11
+ let session = AdminSession {
12
+ vault_password: "vault-password".to_string(),
13
+ lease,
14
+ };
15
+ daemon
16
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
17
+ .await
18
+ .expect("add per-tx policy");
19
+ daemon
20
+ .add_policy(&session, policy_per_chain_gas(1, 1_000_000_000_000_000))
21
+ .await
22
+ .expect("add gas policy");
23
+
24
+ let key = daemon
25
+ .create_vault_key(&session, KeyCreateRequest::Generate)
26
+ .await
27
+ .expect("key");
28
+ let agent_credentials = daemon
29
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
30
+ .await
31
+ .expect("agent");
32
+
33
+ let request = sign_request(
34
+ &agent_credentials,
35
+ AgentAction::BroadcastTx {
36
+ tx: BroadcastTx {
37
+ chain_id: 1,
38
+ nonce: 0,
39
+ to: "0x9300000000000000000000000000000000000000"
40
+ .parse()
41
+ .expect("to"),
42
+ value_wei: 0,
43
+ data_hex: "0x".to_string(),
44
+ gas_limit: 21_000,
45
+ max_fee_per_gas_wei: 1_000_000_000,
46
+ max_priority_fee_per_gas_wei: 1_000_000_000,
47
+ tx_type: 0x02,
48
+ delegation_enabled: false,
49
+ },
50
+ },
51
+ );
52
+ let err = daemon
53
+ .sign_for_agent(request)
54
+ .await
55
+ .expect_err("broadcast signing requires nonce reservation");
56
+ assert!(matches!(
57
+ err,
58
+ DaemonError::MissingNonceReservation {
59
+ chain_id: 1,
60
+ nonce: 0
61
+ }
62
+ ));
63
+ }
64
+
65
+ #[tokio::test]
66
+ async fn reserve_nonce_is_monotonic_for_same_agent_and_chain() {
67
+ let daemon = InMemoryDaemon::new(
68
+ "vault-password",
69
+ SoftwareSignerBackend::default(),
70
+ DaemonConfig::default(),
71
+ )
72
+ .expect("daemon");
73
+
74
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
75
+ let session = AdminSession {
76
+ vault_password: "vault-password".to_string(),
77
+ lease,
78
+ };
79
+ daemon
80
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
81
+ .await
82
+ .expect("add policy");
83
+ let key = daemon
84
+ .create_vault_key(&session, KeyCreateRequest::Generate)
85
+ .await
86
+ .expect("key");
87
+ let agent_credentials = daemon
88
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
89
+ .await
90
+ .expect("agent");
91
+
92
+ let now = time::OffsetDateTime::now_utc();
93
+ let first = daemon
94
+ .reserve_nonce(NonceReservationRequest {
95
+ request_id: Uuid::new_v4(),
96
+ agent_key_id: agent_credentials.agent_key.id,
97
+ agent_auth_token: agent_credentials.auth_token.clone(),
98
+ chain_id: 1,
99
+ min_nonce: 7,
100
+ requested_at: now,
101
+ expires_at: now + time::Duration::minutes(2),
102
+ })
103
+ .await
104
+ .expect("first reservation");
105
+ let second = daemon
106
+ .reserve_nonce(NonceReservationRequest {
107
+ request_id: Uuid::new_v4(),
108
+ agent_key_id: agent_credentials.agent_key.id,
109
+ agent_auth_token: agent_credentials.auth_token,
110
+ chain_id: 1,
111
+ min_nonce: 7,
112
+ requested_at: now,
113
+ expires_at: now + time::Duration::minutes(2),
114
+ })
115
+ .await
116
+ .expect("second reservation");
117
+ assert_eq!(first.nonce, 7);
118
+ assert_eq!(second.nonce, 8);
119
+ }
120
+
121
+ #[tokio::test]
122
+ async fn reserve_nonce_replayed_request_id_is_rejected() {
123
+ let daemon = InMemoryDaemon::new(
124
+ "vault-password",
125
+ SoftwareSignerBackend::default(),
126
+ DaemonConfig::default(),
127
+ )
128
+ .expect("daemon");
129
+
130
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
131
+ let session = AdminSession {
132
+ vault_password: "vault-password".to_string(),
133
+ lease,
134
+ };
135
+ daemon
136
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
137
+ .await
138
+ .expect("add policy");
139
+ let key = daemon
140
+ .create_vault_key(&session, KeyCreateRequest::Generate)
141
+ .await
142
+ .expect("key");
143
+ let agent_credentials = daemon
144
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
145
+ .await
146
+ .expect("agent");
147
+
148
+ let now = time::OffsetDateTime::now_utc();
149
+ let request = NonceReservationRequest {
150
+ request_id: Uuid::new_v4(),
151
+ agent_key_id: agent_credentials.agent_key.id,
152
+ agent_auth_token: agent_credentials.auth_token,
153
+ chain_id: 1,
154
+ min_nonce: 0,
155
+ requested_at: now,
156
+ expires_at: now + time::Duration::minutes(2),
157
+ };
158
+
159
+ daemon
160
+ .reserve_nonce(request.clone())
161
+ .await
162
+ .expect("first reserve");
163
+
164
+ let err = daemon
165
+ .reserve_nonce(request)
166
+ .await
167
+ .expect_err("replayed reserve request id must fail");
168
+ assert!(matches!(err, DaemonError::RequestReplayDetected));
169
+ }
170
+
171
+ #[tokio::test]
172
+ async fn release_nonce_reclaims_latest_unused_nonce() {
173
+ let daemon = InMemoryDaemon::new(
174
+ "vault-password",
175
+ SoftwareSignerBackend::default(),
176
+ DaemonConfig::default(),
177
+ )
178
+ .expect("daemon");
179
+
180
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
181
+ let session = AdminSession {
182
+ vault_password: "vault-password".to_string(),
183
+ lease,
184
+ };
185
+ daemon
186
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
187
+ .await
188
+ .expect("add policy");
189
+ let key = daemon
190
+ .create_vault_key(&session, KeyCreateRequest::Generate)
191
+ .await
192
+ .expect("key");
193
+ let agent_credentials = daemon
194
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
195
+ .await
196
+ .expect("agent");
197
+
198
+ let now = time::OffsetDateTime::now_utc();
199
+ let reservation = daemon
200
+ .reserve_nonce(NonceReservationRequest {
201
+ request_id: Uuid::new_v4(),
202
+ agent_key_id: agent_credentials.agent_key.id,
203
+ agent_auth_token: agent_credentials.auth_token.clone(),
204
+ chain_id: 56,
205
+ min_nonce: 0,
206
+ requested_at: now,
207
+ expires_at: now + time::Duration::minutes(2),
208
+ })
209
+ .await
210
+ .expect("reserve");
211
+ assert_eq!(reservation.nonce, 0);
212
+
213
+ daemon
214
+ .release_nonce(NonceReleaseRequest {
215
+ request_id: Uuid::new_v4(),
216
+ agent_key_id: agent_credentials.agent_key.id,
217
+ agent_auth_token: agent_credentials.auth_token.clone(),
218
+ reservation_id: reservation.reservation_id,
219
+ requested_at: now,
220
+ expires_at: now + time::Duration::minutes(2),
221
+ })
222
+ .await
223
+ .expect("release");
224
+
225
+ let next = daemon
226
+ .reserve_nonce(NonceReservationRequest {
227
+ request_id: Uuid::new_v4(),
228
+ agent_key_id: agent_credentials.agent_key.id,
229
+ agent_auth_token: agent_credentials.auth_token,
230
+ chain_id: 56,
231
+ min_nonce: 0,
232
+ requested_at: now,
233
+ expires_at: now + time::Duration::minutes(2),
234
+ })
235
+ .await
236
+ .expect("reserve after release");
237
+ assert_eq!(next.nonce, 0);
238
+ }
239
+
240
+ #[tokio::test]
241
+ async fn expired_nonce_reservation_is_reclaimed_before_next_reserve() {
242
+ let daemon = InMemoryDaemon::new(
243
+ "vault-password",
244
+ SoftwareSignerBackend::default(),
245
+ DaemonConfig {
246
+ nonce_reservation_ttl: time::Duration::milliseconds(5),
247
+ ..DaemonConfig::default()
248
+ },
249
+ )
250
+ .expect("daemon");
251
+
252
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
253
+ let session = AdminSession {
254
+ vault_password: "vault-password".to_string(),
255
+ lease,
256
+ };
257
+ daemon
258
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
259
+ .await
260
+ .expect("add policy");
261
+ let key = daemon
262
+ .create_vault_key(&session, KeyCreateRequest::Generate)
263
+ .await
264
+ .expect("key");
265
+ let agent_credentials = daemon
266
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
267
+ .await
268
+ .expect("agent");
269
+
270
+ let now = time::OffsetDateTime::now_utc();
271
+ let first = daemon
272
+ .reserve_nonce(NonceReservationRequest {
273
+ request_id: Uuid::new_v4(),
274
+ agent_key_id: agent_credentials.agent_key.id,
275
+ agent_auth_token: agent_credentials.auth_token.clone(),
276
+ chain_id: 56,
277
+ min_nonce: 0,
278
+ requested_at: now,
279
+ expires_at: now + time::Duration::minutes(2),
280
+ })
281
+ .await
282
+ .expect("first reserve");
283
+ assert_eq!(first.nonce, 0);
284
+
285
+ tokio::time::sleep(std::time::Duration::from_millis(20)).await;
286
+
287
+ let second = daemon
288
+ .reserve_nonce(NonceReservationRequest {
289
+ request_id: Uuid::new_v4(),
290
+ agent_key_id: agent_credentials.agent_key.id,
291
+ agent_auth_token: agent_credentials.auth_token,
292
+ chain_id: 56,
293
+ min_nonce: 0,
294
+ requested_at: time::OffsetDateTime::now_utc(),
295
+ expires_at: time::OffsetDateTime::now_utc() + time::Duration::minutes(2),
296
+ })
297
+ .await
298
+ .expect("second reserve");
299
+ assert_eq!(second.nonce, 0);
300
+ }
301
+
302
+ #[tokio::test]
303
+ async fn reserve_nonce_rejects_zero_chain_id() {
304
+ let daemon = InMemoryDaemon::new(
305
+ "vault-password",
306
+ SoftwareSignerBackend::default(),
307
+ DaemonConfig::default(),
308
+ )
309
+ .expect("daemon");
310
+
311
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
312
+ let session = AdminSession {
313
+ vault_password: "vault-password".to_string(),
314
+ lease,
315
+ };
316
+ daemon
317
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
318
+ .await
319
+ .expect("add policy");
320
+ let key = daemon
321
+ .create_vault_key(&session, KeyCreateRequest::Generate)
322
+ .await
323
+ .expect("key");
324
+ let agent_credentials = daemon
325
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
326
+ .await
327
+ .expect("agent");
328
+
329
+ let now = time::OffsetDateTime::now_utc();
330
+ let err = daemon
331
+ .reserve_nonce(NonceReservationRequest {
332
+ request_id: Uuid::new_v4(),
333
+ agent_key_id: agent_credentials.agent_key.id,
334
+ agent_auth_token: agent_credentials.auth_token,
335
+ chain_id: 0,
336
+ min_nonce: 0,
337
+ requested_at: now,
338
+ expires_at: now + time::Duration::minutes(2),
339
+ })
340
+ .await
341
+ .expect_err("chain_id zero must fail");
342
+
343
+ assert!(matches!(
344
+ err,
345
+ DaemonError::InvalidNonceReservation(message)
346
+ if message.contains("chain_id must be greater than zero")
347
+ ));
348
+ }
349
+
350
+ #[tokio::test]
351
+ async fn release_nonce_replayed_request_id_is_rejected() {
352
+ let daemon = InMemoryDaemon::new(
353
+ "vault-password",
354
+ SoftwareSignerBackend::default(),
355
+ DaemonConfig::default(),
356
+ )
357
+ .expect("daemon");
358
+
359
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
360
+ let session = AdminSession {
361
+ vault_password: "vault-password".to_string(),
362
+ lease,
363
+ };
364
+ daemon
365
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
366
+ .await
367
+ .expect("add policy");
368
+ let key = daemon
369
+ .create_vault_key(&session, KeyCreateRequest::Generate)
370
+ .await
371
+ .expect("key");
372
+ let agent_credentials = daemon
373
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
374
+ .await
375
+ .expect("agent");
376
+
377
+ let now = time::OffsetDateTime::now_utc();
378
+ let reservation = daemon
379
+ .reserve_nonce(NonceReservationRequest {
380
+ request_id: Uuid::new_v4(),
381
+ agent_key_id: agent_credentials.agent_key.id,
382
+ agent_auth_token: agent_credentials.auth_token.clone(),
383
+ chain_id: 1,
384
+ min_nonce: 0,
385
+ requested_at: now,
386
+ expires_at: now + time::Duration::minutes(2),
387
+ })
388
+ .await
389
+ .expect("reserve");
390
+
391
+ let release_request = NonceReleaseRequest {
392
+ request_id: Uuid::new_v4(),
393
+ agent_key_id: agent_credentials.agent_key.id,
394
+ agent_auth_token: agent_credentials.auth_token,
395
+ reservation_id: reservation.reservation_id,
396
+ requested_at: now,
397
+ expires_at: now + time::Duration::minutes(2),
398
+ };
399
+
400
+ daemon
401
+ .release_nonce(release_request.clone())
402
+ .await
403
+ .expect("first release");
404
+
405
+ let err = daemon
406
+ .release_nonce(release_request)
407
+ .await
408
+ .expect_err("replayed release request id must fail");
409
+ assert!(matches!(err, DaemonError::RequestReplayDetected));
410
+ }
411
+
412
+ #[tokio::test]
413
+ async fn release_nonce_rejects_non_owner_agent() {
414
+ let daemon = InMemoryDaemon::new(
415
+ "vault-password",
416
+ SoftwareSignerBackend::default(),
417
+ DaemonConfig::default(),
418
+ )
419
+ .expect("daemon");
420
+
421
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
422
+ let session = AdminSession {
423
+ vault_password: "vault-password".to_string(),
424
+ lease,
425
+ };
426
+ daemon
427
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
428
+ .await
429
+ .expect("add policy");
430
+ let key = daemon
431
+ .create_vault_key(&session, KeyCreateRequest::Generate)
432
+ .await
433
+ .expect("key");
434
+
435
+ let owner_credentials = daemon
436
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
437
+ .await
438
+ .expect("owner");
439
+ let attacker_credentials = daemon
440
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
441
+ .await
442
+ .expect("attacker");
443
+
444
+ let now = time::OffsetDateTime::now_utc();
445
+ let reservation = daemon
446
+ .reserve_nonce(NonceReservationRequest {
447
+ request_id: Uuid::new_v4(),
448
+ agent_key_id: owner_credentials.agent_key.id,
449
+ agent_auth_token: owner_credentials.auth_token.clone(),
450
+ chain_id: 1,
451
+ min_nonce: 0,
452
+ requested_at: now,
453
+ expires_at: now + time::Duration::minutes(2),
454
+ })
455
+ .await
456
+ .expect("reserve");
457
+
458
+ let err = daemon
459
+ .release_nonce(NonceReleaseRequest {
460
+ request_id: Uuid::new_v4(),
461
+ agent_key_id: attacker_credentials.agent_key.id,
462
+ agent_auth_token: attacker_credentials.auth_token,
463
+ reservation_id: reservation.reservation_id,
464
+ requested_at: now,
465
+ expires_at: now + time::Duration::minutes(2),
466
+ })
467
+ .await
468
+ .expect_err("non-owner should not release reservation");
469
+ assert!(matches!(err, DaemonError::AgentAuthenticationFailed));
470
+
471
+ daemon
472
+ .release_nonce(NonceReleaseRequest {
473
+ request_id: Uuid::new_v4(),
474
+ agent_key_id: owner_credentials.agent_key.id,
475
+ agent_auth_token: owner_credentials.auth_token,
476
+ reservation_id: reservation.reservation_id,
477
+ requested_at: now,
478
+ expires_at: now + time::Duration::minutes(2),
479
+ })
480
+ .await
481
+ .expect("owner must still be able to release");
482
+ }
483
+
484
+ #[tokio::test]
485
+ async fn explain_for_agent_returns_denial_without_error() {
486
+ let daemon = InMemoryDaemon::new(
487
+ "vault-password",
488
+ SoftwareSignerBackend::default(),
489
+ DaemonConfig::default(),
490
+ )
491
+ .expect("daemon");
492
+
493
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
494
+ let session = AdminSession {
495
+ vault_password: "vault-password".to_string(),
496
+ lease,
497
+ };
498
+ let policy = policy_all_per_tx(10);
499
+ daemon
500
+ .add_policy(&session, policy.clone())
501
+ .await
502
+ .expect("add policy");
503
+
504
+ let key = daemon
505
+ .create_vault_key(&session, KeyCreateRequest::Generate)
506
+ .await
507
+ .expect("key");
508
+ let agent_credentials = daemon
509
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
510
+ .await
511
+ .expect("agent");
512
+
513
+ let request = sign_request(
514
+ &agent_credentials,
515
+ AgentAction::Transfer {
516
+ chain_id: 1,
517
+ token: "0x7700000000000000000000000000000000000000"
518
+ .parse()
519
+ .expect("token"),
520
+ to: "0x8700000000000000000000000000000000000000"
521
+ .parse()
522
+ .expect("recipient"),
523
+ amount_wei: 11,
524
+ },
525
+ );
526
+ let explanation = daemon
527
+ .explain_for_agent(request)
528
+ .await
529
+ .expect("explain should return payload");
530
+ assert_eq!(explanation.applicable_policy_ids, vec![policy.id]);
531
+ assert_eq!(explanation.evaluated_policy_ids, vec![policy.id]);
532
+ match explanation.decision {
533
+ vault_policy::PolicyDecision::Deny(PolicyError::PerTxLimitExceeded { .. }) => {}
534
+ other => panic!("unexpected explanation decision: {other:?}"),
535
+ }
536
+ }
537
+
538
+ #[tokio::test]
539
+ async fn unknown_agent_key_is_not_disclosed_in_sign_api() {
540
+ let daemon = InMemoryDaemon::new(
541
+ "vault-password",
542
+ SoftwareSignerBackend::default(),
543
+ DaemonConfig::default(),
544
+ )
545
+ .expect("daemon");
546
+
547
+ let token = "0x7100000000000000000000000000000000000000"
548
+ .parse::<EvmAddress>()
549
+ .expect("token");
550
+ let recipient = "0x8100000000000000000000000000000000000000"
551
+ .parse::<EvmAddress>()
552
+ .expect("recipient");
553
+
554
+ let request = SignRequest {
555
+ request_id: Uuid::new_v4(),
556
+ agent_key_id: Uuid::new_v4(),
557
+ agent_auth_token: "random-token".to_string(),
558
+ payload: to_vec(&AgentAction::Transfer {
559
+ chain_id: 1,
560
+ token: token.clone(),
561
+ to: recipient.clone(),
562
+ amount_wei: 1,
563
+ })
564
+ .expect("payload"),
565
+ action: AgentAction::Transfer {
566
+ chain_id: 1,
567
+ token,
568
+ to: recipient,
569
+ amount_wei: 1,
570
+ },
571
+ requested_at: time::OffsetDateTime::now_utc(),
572
+ expires_at: time::OffsetDateTime::now_utc() + time::Duration::minutes(2),
573
+ };
574
+
575
+ let err = daemon
576
+ .sign_for_agent(request)
577
+ .await
578
+ .expect_err("unknown key must not leak key existence");
579
+ assert!(matches!(err, DaemonError::AgentAuthenticationFailed));
580
+ }
581
+
582
+ #[tokio::test]
583
+ async fn release_nonce_removes_reservation_from_snapshot() {
584
+ let daemon = InMemoryDaemon::new(
585
+ "vault-password",
586
+ SoftwareSignerBackend::default(),
587
+ DaemonConfig::default(),
588
+ )
589
+ .expect("daemon");
590
+
591
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
592
+ let session = AdminSession {
593
+ vault_password: "vault-password".to_string(),
594
+ lease,
595
+ };
596
+ daemon
597
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
598
+ .await
599
+ .expect("add policy");
600
+ let key = daemon
601
+ .create_vault_key(&session, KeyCreateRequest::Generate)
602
+ .await
603
+ .expect("key");
604
+ let agent_credentials = daemon
605
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
606
+ .await
607
+ .expect("agent");
608
+
609
+ let now = time::OffsetDateTime::now_utc();
610
+ let reservation = daemon
611
+ .reserve_nonce(NonceReservationRequest {
612
+ request_id: Uuid::new_v4(),
613
+ agent_key_id: agent_credentials.agent_key.id,
614
+ agent_auth_token: agent_credentials.auth_token.clone(),
615
+ chain_id: 1,
616
+ min_nonce: 0,
617
+ requested_at: now,
618
+ expires_at: now + time::Duration::minutes(2),
619
+ })
620
+ .await
621
+ .expect("reserve");
622
+
623
+ let release_request = NonceReleaseRequest {
624
+ request_id: Uuid::new_v4(),
625
+ agent_key_id: agent_credentials.agent_key.id,
626
+ agent_auth_token: agent_credentials.auth_token,
627
+ reservation_id: reservation.reservation_id,
628
+ requested_at: now,
629
+ expires_at: now + time::Duration::minutes(2),
630
+ };
631
+
632
+ daemon
633
+ .release_nonce(release_request)
634
+ .await
635
+ .expect("release");
636
+
637
+ let snapshot = daemon.snapshot_state().expect("snapshot");
638
+ assert!(
639
+ !snapshot
640
+ .nonce_reservations
641
+ .contains_key(&reservation.reservation_id),
642
+ "released reservation must be removed from persisted state snapshot"
643
+ );
644
+ }
645
+
646
+ #[tokio::test]
647
+ async fn payload_action_mismatch_is_rejected() {
648
+ let daemon = InMemoryDaemon::new(
649
+ "vault-password",
650
+ SoftwareSignerBackend::default(),
651
+ DaemonConfig::default(),
652
+ )
653
+ .expect("daemon");
654
+
655
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
656
+ let session = AdminSession {
657
+ vault_password: "vault-password".to_string(),
658
+ lease,
659
+ };
660
+ daemon
661
+ .add_policy(&session, policy_all_per_tx(100))
662
+ .await
663
+ .expect("add policy");
664
+
665
+ let key = daemon
666
+ .create_vault_key(&session, KeyCreateRequest::Generate)
667
+ .await
668
+ .expect("key");
669
+ let agent_credentials = daemon
670
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
671
+ .await
672
+ .expect("agent");
673
+
674
+ let token = "0x9000000000000000000000000000000000000000"
675
+ .parse::<EvmAddress>()
676
+ .expect("token");
677
+ let recipient = "0xa000000000000000000000000000000000000000"
678
+ .parse::<EvmAddress>()
679
+ .expect("recipient");
680
+
681
+ let declared_action = AgentAction::Transfer {
682
+ chain_id: 1,
683
+ token: token.clone(),
684
+ to: recipient.clone(),
685
+ amount_wei: 10,
686
+ };
687
+ let mut request = sign_request(&agent_credentials, declared_action.clone());
688
+ request.payload = to_vec(&AgentAction::Transfer {
689
+ chain_id: 1,
690
+ token,
691
+ to: recipient,
692
+ amount_wei: 99,
693
+ })
694
+ .expect("payload");
695
+ request.action = declared_action;
696
+
697
+ let err = daemon
698
+ .sign_for_agent(request)
699
+ .await
700
+ .expect_err("mismatched payload/action must fail");
701
+ assert!(matches!(err, DaemonError::PayloadActionMismatch));
702
+ }
703
+
704
+ #[tokio::test]
705
+ async fn malformed_payload_is_rejected() {
706
+ let daemon = InMemoryDaemon::new(
707
+ "vault-password",
708
+ SoftwareSignerBackend::default(),
709
+ DaemonConfig::default(),
710
+ )
711
+ .expect("daemon");
712
+
713
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
714
+ let session = AdminSession {
715
+ vault_password: "vault-password".to_string(),
716
+ lease,
717
+ };
718
+ daemon
719
+ .add_policy(&session, policy_all_per_tx(100))
720
+ .await
721
+ .expect("add policy");
722
+
723
+ let key = daemon
724
+ .create_vault_key(&session, KeyCreateRequest::Generate)
725
+ .await
726
+ .expect("key");
727
+ let agent_credentials = daemon
728
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
729
+ .await
730
+ .expect("agent");
731
+
732
+ let token = "0x9100000000000000000000000000000000000000"
733
+ .parse::<EvmAddress>()
734
+ .expect("token");
735
+ let recipient = "0xa100000000000000000000000000000000000000"
736
+ .parse::<EvmAddress>()
737
+ .expect("recipient");
738
+
739
+ let mut request = sign_request(
740
+ &agent_credentials,
741
+ AgentAction::Transfer {
742
+ chain_id: 1,
743
+ token,
744
+ to: recipient,
745
+ amount_wei: 1,
746
+ },
747
+ );
748
+ request.payload = b"not-json".to_vec();
749
+
750
+ let err = daemon
751
+ .sign_for_agent(request)
752
+ .await
753
+ .expect_err("malformed payload must fail");
754
+ assert!(matches!(err, DaemonError::PayloadActionMismatch));
755
+ }
756
+
757
+ #[tokio::test]
758
+ async fn payload_with_extra_fields_is_rejected() {
759
+ let daemon = InMemoryDaemon::new(
760
+ "vault-password",
761
+ SoftwareSignerBackend::default(),
762
+ DaemonConfig::default(),
763
+ )
764
+ .expect("daemon");
765
+
766
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
767
+ let session = AdminSession {
768
+ vault_password: "vault-password".to_string(),
769
+ lease,
770
+ };
771
+ daemon
772
+ .add_policy(&session, policy_all_per_tx(100))
773
+ .await
774
+ .expect("add policy");
775
+
776
+ let key = daemon
777
+ .create_vault_key(&session, KeyCreateRequest::Generate)
778
+ .await
779
+ .expect("key");
780
+ let agent_credentials = daemon
781
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
782
+ .await
783
+ .expect("agent");
784
+
785
+ let token = "0x9100000000000000000000000000000000000000"
786
+ .parse::<EvmAddress>()
787
+ .expect("token");
788
+ let recipient = "0xa100000000000000000000000000000000000000"
789
+ .parse::<EvmAddress>()
790
+ .expect("recipient");
791
+
792
+ let action = AgentAction::Transfer {
793
+ chain_id: 1,
794
+ token: token.clone(),
795
+ to: recipient.clone(),
796
+ amount_wei: 1,
797
+ };
798
+ let mut request = sign_request(&agent_credentials, action.clone());
799
+ request.payload = format!(
800
+ "{{\"kind\":\"Transfer\",\"token\":\"{}\",\"to\":\"{}\",\"amount_wei\":\"1\",\"unexpected\":\"x\"}}",
801
+ token.as_str(),
802
+ recipient.as_str()
803
+ )
804
+ .into_bytes();
805
+ request.action = action;
806
+
807
+ let err = daemon
808
+ .sign_for_agent(request)
809
+ .await
810
+ .expect_err("non-canonical payload must fail");
811
+ assert!(matches!(err, DaemonError::PayloadActionMismatch));
812
+ }
813
+
814
+ #[tokio::test]
815
+ async fn oversized_payload_is_rejected() {
816
+ let config = DaemonConfig {
817
+ max_sign_payload_bytes: 64,
818
+ ..DaemonConfig::default()
819
+ };
820
+ let daemon =
821
+ InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config)
822
+ .expect("daemon");
823
+
824
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
825
+ let session = AdminSession {
826
+ vault_password: "vault-password".to_string(),
827
+ lease,
828
+ };
829
+ daemon
830
+ .add_policy(&session, policy_all_per_tx(100))
831
+ .await
832
+ .expect("add policy");
833
+
834
+ let key = daemon
835
+ .create_vault_key(&session, KeyCreateRequest::Generate)
836
+ .await
837
+ .expect("key");
838
+ let agent_credentials = daemon
839
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
840
+ .await
841
+ .expect("agent");
842
+
843
+ let token = "0x9100000000000000000000000000000000000000"
844
+ .parse::<EvmAddress>()
845
+ .expect("token");
846
+ let recipient = "0xa100000000000000000000000000000000000000"
847
+ .parse::<EvmAddress>()
848
+ .expect("recipient");
849
+
850
+ let mut request = sign_request(
851
+ &agent_credentials,
852
+ AgentAction::Transfer {
853
+ chain_id: 1,
854
+ token,
855
+ to: recipient,
856
+ amount_wei: 1,
857
+ },
858
+ );
859
+ request.payload = vec![b'a'; 65];
860
+
861
+ let err = daemon
862
+ .sign_for_agent(request)
863
+ .await
864
+ .expect_err("oversized payload must fail");
865
+ assert!(matches!(
866
+ err,
867
+ DaemonError::PayloadTooLarge { max_bytes: 64 }
868
+ ));
869
+ }
870
+
871
+ #[tokio::test]
872
+ async fn spend_log_retention_prunes_old_entries() {
873
+ let daemon = InMemoryDaemon::new(
874
+ "vault-password",
875
+ SoftwareSignerBackend::default(),
876
+ DaemonConfig::default(),
877
+ )
878
+ .expect("daemon");
879
+
880
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
881
+ let session = AdminSession {
882
+ vault_password: "vault-password".to_string(),
883
+ lease,
884
+ };
885
+ daemon
886
+ .add_policy(&session, policy_all_per_tx(100))
887
+ .await
888
+ .expect("add policy");
889
+
890
+ let key = daemon
891
+ .create_vault_key(&session, KeyCreateRequest::Generate)
892
+ .await
893
+ .expect("key");
894
+ let agent_credentials = daemon
895
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
896
+ .await
897
+ .expect("agent");
898
+
899
+ let old_token = "0x9200000000000000000000000000000000000000"
900
+ .parse::<EvmAddress>()
901
+ .expect("token");
902
+ let old_recipient = "0xa200000000000000000000000000000000000000"
903
+ .parse::<EvmAddress>()
904
+ .expect("recipient");
905
+ daemon
906
+ .spend_log
907
+ .write()
908
+ .expect("log write")
909
+ .push(vault_domain::SpendEvent {
910
+ agent_key_id: agent_credentials.agent_key.id,
911
+ chain_id: 1,
912
+ asset: AssetId::Erc20(old_token),
913
+ recipient: old_recipient,
914
+ amount_wei: 1,
915
+ at: time::OffsetDateTime::now_utc() - time::Duration::days(30),
916
+ });
917
+
918
+ let token = "0x9300000000000000000000000000000000000000"
919
+ .parse::<EvmAddress>()
920
+ .expect("token");
921
+ let recipient = "0xa300000000000000000000000000000000000000"
922
+ .parse::<EvmAddress>()
923
+ .expect("recipient");
924
+ daemon
925
+ .sign_for_agent(sign_request(
926
+ &agent_credentials,
927
+ AgentAction::Transfer {
928
+ chain_id: 1,
929
+ token,
930
+ to: recipient,
931
+ amount_wei: 1,
932
+ },
933
+ ))
934
+ .await
935
+ .expect("sign must pass");
936
+
937
+ let log = daemon.spend_log.read().expect("log read");
938
+ assert!(
939
+ log.iter().all(|event| {
940
+ event.at >= time::OffsetDateTime::now_utc() - time::Duration::days(8)
941
+ }),
942
+ "all old entries must be pruned"
943
+ );
944
+ }
945
+
946
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
947
+ async fn concurrent_sign_requests_do_not_race_policy_windows() {
948
+ let daemon = Arc::new(
949
+ InMemoryDaemon::new(
950
+ "vault-password",
951
+ SoftwareSignerBackend::default(),
952
+ DaemonConfig::default(),
953
+ )
954
+ .expect("daemon"),
955
+ );
956
+
957
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
958
+ let session = AdminSession {
959
+ vault_password: "vault-password".to_string(),
960
+ lease,
961
+ };
962
+
963
+ let policy = SpendingPolicy::new(
964
+ 0,
965
+ PolicyType::DailyMaxSpending,
966
+ 100,
967
+ EntityScope::All,
968
+ EntityScope::All,
969
+ EntityScope::All,
970
+ )
971
+ .expect("policy");
972
+ daemon
973
+ .add_policy(&session, policy)
974
+ .await
975
+ .expect("add policy");
976
+
977
+ let key = daemon
978
+ .create_vault_key(&session, KeyCreateRequest::Generate)
979
+ .await
980
+ .expect("key");
981
+ let agent_credentials = daemon
982
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
983
+ .await
984
+ .expect("agent");
985
+
986
+ let token = "0x5000000000000000000000000000000000000000"
987
+ .parse::<EvmAddress>()
988
+ .expect("token");
989
+ let recipient = "0x6000000000000000000000000000000000000000"
990
+ .parse::<EvmAddress>()
991
+ .expect("recipient");
992
+
993
+ let req1 = sign_request(
994
+ &agent_credentials,
995
+ AgentAction::Transfer {
996
+ chain_id: 1,
997
+ token: token.clone(),
998
+ to: recipient.clone(),
999
+ amount_wei: 60,
1000
+ },
1001
+ );
1002
+ let req2 = sign_request(
1003
+ &agent_credentials,
1004
+ AgentAction::Transfer {
1005
+ chain_id: 1,
1006
+ token,
1007
+ to: recipient,
1008
+ amount_wei: 60,
1009
+ },
1010
+ );
1011
+
1012
+ let daemon_a = daemon.clone();
1013
+ let daemon_b = daemon.clone();
1014
+ let (res1, res2) =
1015
+ tokio::join!(daemon_a.sign_for_agent(req1), daemon_b.sign_for_agent(req2));
1016
+
1017
+ let success_count = usize::from(res1.is_ok()) + usize::from(res2.is_ok());
1018
+ let failure_count = usize::from(res1.is_err()) + usize::from(res2.is_err());
1019
+ assert_eq!(success_count, 1, "exactly one request must pass");
1020
+ assert_eq!(failure_count, 1, "exactly one request must fail");
1021
+ }