perp-cli 0.11.0 → 0.12.1

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 (240) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bridge-engine.js +102 -49
  21. package/dist/cli-spec.js +1 -2
  22. package/dist/commands/account.js +2 -500
  23. package/dist/commands/agent.d.ts +8 -2
  24. package/dist/commands/agent.js +1903 -366
  25. package/dist/commands/alerts.js +3 -1
  26. package/dist/commands/arb/index.js +37 -7
  27. package/dist/commands/arb-auto.js +44 -11
  28. package/dist/commands/arb.js +83 -34
  29. package/dist/commands/bot.d.ts +1 -1
  30. package/dist/commands/bot.js +18 -14
  31. package/dist/commands/bridge.d.ts +1 -1
  32. package/dist/commands/bridge.js +4 -4
  33. package/dist/commands/funds.d.ts +1 -1
  34. package/dist/commands/funds.js +18 -68
  35. package/dist/commands/jobs.d.ts +1 -1
  36. package/dist/commands/jobs.js +2 -2
  37. package/dist/commands/manage.d.ts +1 -1
  38. package/dist/commands/manage.js +12 -84
  39. package/dist/commands/plan.d.ts +1 -1
  40. package/dist/commands/plan.js +2 -2
  41. package/dist/commands/portfolio.d.ts +19 -3
  42. package/dist/commands/portfolio.js +747 -163
  43. package/dist/commands/rebalance.d.ts +1 -1
  44. package/dist/commands/rebalance.js +18 -12
  45. package/dist/commands/risk.js +2 -6
  46. package/dist/commands/settings.js +30 -53
  47. package/dist/commands/trade.js +16 -15
  48. package/dist/commands/wallet.d.ts +2 -1
  49. package/dist/commands/wallet.js +756 -28
  50. package/dist/errors.d.ts +64 -0
  51. package/dist/errors.js +82 -2
  52. package/dist/event-stream.js +12 -1
  53. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  54. package/dist/exchanges/aster-typed-data.js +189 -0
  55. package/dist/exchanges/aster.d.ts +60 -11
  56. package/dist/exchanges/aster.js +278 -136
  57. package/dist/exchanges/hyperliquid.d.ts +14 -0
  58. package/dist/exchanges/hyperliquid.js +84 -34
  59. package/dist/exchanges/lighter-spot.js +14 -3
  60. package/dist/exchanges/lighter.d.ts +66 -0
  61. package/dist/exchanges/lighter.js +199 -27
  62. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  63. package/dist/exchanges/pacifica-typed-data.js +74 -0
  64. package/dist/exchanges/pacifica.d.ts +29 -1
  65. package/dist/exchanges/pacifica.js +94 -6
  66. package/dist/funding/rates.js +59 -21
  67. package/dist/guardrail/perp-guardrail.js +9 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +268 -360
  70. package/dist/jobs.js +3 -1
  71. package/dist/mcp-server.js +85 -83
  72. package/dist/position-history.js +47 -4
  73. package/dist/settings.d.ts +67 -3
  74. package/dist/settings.js +1 -2
  75. package/dist/signer/ows-evm.d.ts +14 -0
  76. package/dist/signer/ows-evm.js +28 -9
  77. package/dist/smart-order.d.ts +8 -2
  78. package/dist/smart-order.js +46 -11
  79. package/dist/strategies/funding-arb.js +29 -8
  80. package/dist/trade-validator.js +30 -5
  81. package/dist/utils.d.ts +10 -0
  82. package/dist/utils.js +19 -0
  83. package/package.json +2 -2
  84. package/skills/perp-cli/SKILL.md +23 -14
  85. package/skills/perp-cli/references/agent-operations.md +6 -4
  86. package/skills/perp-cli/references/commands.md +55 -34
  87. package/skills/perp-cli/scripts/preflight.sh +4 -4
  88. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  89. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  90. package/dist/__tests__/alert-logic.test.js +0 -107
  91. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  92. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  93. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  94. package/dist/__tests__/arb-history-stats.test.js +0 -176
  95. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  96. package/dist/__tests__/arb-logic.test.js +0 -84
  97. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  98. package/dist/__tests__/arb-manage.test.js +0 -253
  99. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  100. package/dist/__tests__/arb-new-features.test.js +0 -457
  101. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  102. package/dist/__tests__/arb-sizing.test.js +0 -48
  103. package/dist/__tests__/arb-state.test.d.ts +0 -1
  104. package/dist/__tests__/arb-state.test.js +0 -284
  105. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  106. package/dist/__tests__/arb-userflow.test.js +0 -945
  107. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  108. package/dist/__tests__/arb-utils.test.js +0 -264
  109. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  110. package/dist/__tests__/bot-conditions.test.js +0 -341
  111. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  112. package/dist/__tests__/client-id-tracker.test.js +0 -137
  113. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  114. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  115. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  116. package/dist/__tests__/commands/order-intent.test.js +0 -600
  117. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  118. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  119. package/dist/__tests__/config.test.d.ts +0 -1
  120. package/dist/__tests__/config.test.js +0 -86
  121. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  122. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  123. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  124. package/dist/__tests__/dex-asset-map.test.js +0 -191
  125. package/dist/__tests__/errors.test.d.ts +0 -1
  126. package/dist/__tests__/errors.test.js +0 -110
  127. package/dist/__tests__/event-stream.test.d.ts +0 -1
  128. package/dist/__tests__/event-stream.test.js +0 -276
  129. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  130. package/dist/__tests__/exchanges/interface.test.js +0 -132
  131. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  132. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  133. package/dist/__tests__/execution-log.test.d.ts +0 -1
  134. package/dist/__tests__/execution-log.test.js +0 -106
  135. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  136. package/dist/__tests__/funding-calc.test.js +0 -71
  137. package/dist/__tests__/funding-history.test.d.ts +0 -1
  138. package/dist/__tests__/funding-history.test.js +0 -343
  139. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  140. package/dist/__tests__/funding-rates.test.js +0 -342
  141. package/dist/__tests__/funding.test.d.ts +0 -1
  142. package/dist/__tests__/funding.test.js +0 -173
  143. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  144. package/dist/__tests__/gap-logic.test.js +0 -43
  145. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  146. package/dist/__tests__/hip3-dex.test.js +0 -234
  147. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  148. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  149. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  150. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  151. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  152. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  153. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  154. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  155. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  156. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  157. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  158. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  159. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  160. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  161. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  162. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  163. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  165. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  166. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  167. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  169. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  170. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  171. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  173. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  175. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  177. package/dist/__tests__/liquidity.test.d.ts +0 -1
  178. package/dist/__tests__/liquidity.test.js +0 -225
  179. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  180. package/dist/__tests__/plan-executor.test.js +0 -314
  181. package/dist/__tests__/position-history.test.d.ts +0 -1
  182. package/dist/__tests__/position-history.test.js +0 -367
  183. package/dist/__tests__/retry.test.d.ts +0 -1
  184. package/dist/__tests__/retry.test.js +0 -310
  185. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  186. package/dist/__tests__/risk-assessment.test.js +0 -288
  187. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  188. package/dist/__tests__/security-adversarial.test.js +0 -574
  189. package/dist/__tests__/strategies.test.d.ts +0 -1
  190. package/dist/__tests__/strategies.test.js +0 -539
  191. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  192. package/dist/__tests__/trade-execution.test.js +0 -129
  193. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  194. package/dist/__tests__/trade-validator.test.js +0 -655
  195. package/dist/__tests__/utils.test.d.ts +0 -1
  196. package/dist/__tests__/utils.test.js +0 -76
  197. package/dist/arb/reconcile.d.ts +0 -28
  198. package/dist/arb/reconcile.js +0 -162
  199. package/dist/commands/alert.d.ts +0 -2
  200. package/dist/commands/alert.js +0 -421
  201. package/dist/commands/analytics.d.ts +0 -3
  202. package/dist/commands/analytics.js +0 -311
  203. package/dist/commands/arb-manage.d.ts +0 -5
  204. package/dist/commands/arb-manage.js +0 -5
  205. package/dist/commands/dashboard.d.ts +0 -3
  206. package/dist/commands/dashboard.js +0 -176
  207. package/dist/commands/deposit.d.ts +0 -4
  208. package/dist/commands/deposit.js +0 -573
  209. package/dist/commands/dex.d.ts +0 -3
  210. package/dist/commands/dex.js +0 -120
  211. package/dist/commands/env.d.ts +0 -2
  212. package/dist/commands/env.js +0 -136
  213. package/dist/commands/funding.d.ts +0 -3
  214. package/dist/commands/funding.js +0 -554
  215. package/dist/commands/gap.d.ts +0 -2
  216. package/dist/commands/gap.js +0 -305
  217. package/dist/commands/health.d.ts +0 -2
  218. package/dist/commands/health.js +0 -67
  219. package/dist/commands/multileg.d.ts +0 -16
  220. package/dist/commands/multileg.js +0 -174
  221. package/dist/commands/ows.d.ts +0 -2
  222. package/dist/commands/ows.js +0 -887
  223. package/dist/commands/pnl.d.ts +0 -12
  224. package/dist/commands/pnl.js +0 -243
  225. package/dist/commands/run.d.ts +0 -3
  226. package/dist/commands/run.js +0 -202
  227. package/dist/commands/stream.d.ts +0 -5
  228. package/dist/commands/stream.js +0 -200
  229. package/dist/commands/withdraw.d.ts +0 -3
  230. package/dist/commands/withdraw.js +0 -187
  231. package/dist/exchanges/edgex.d.ts +0 -62
  232. package/dist/exchanges/edgex.js +0 -552
  233. package/dist/exchanges/nado.d.ts +0 -52
  234. package/dist/exchanges/nado.js +0 -223
  235. package/dist/funding/index.d.ts +0 -3
  236. package/dist/funding/index.js +0 -3
  237. package/dist/retry.d.ts +0 -74
  238. package/dist/retry.js +0 -129
  239. package/dist/ws/market-feed.d.ts +0 -74
  240. package/dist/ws/market-feed.js +0 -227
package/README.md CHANGED
@@ -19,7 +19,7 @@ npx -y perp-cli --json portfolio
19
19
  - **4 Exchanges** — trade, bridge, arbitrage across Pacifica, Hyperliquid, Lighter, Aster
20
20
  - **Funding Rate Arb** — perp-perp + spot-perp scan & one-command dual-leg execution
21
21
  - **Portfolio** — single call returns balances, positions, risk level across all exchanges
22
- - **Funds** — deposit, withdraw, CCTP bridge, internal transfer in one group
22
+ - **Funds** — deposit, withdraw, transfer, multi-provider bridge (cctp/relay/debridge), inter-exchange rebalance — all in one group
23
23
  - **Bots** — TWAP, grid, DCA, trailing-stop with background job management
24
24
  - **Agent-First Design** — `--json`, `--fields`, `--ndjson`, `--dry-run`, runtime schema introspection
25
25
  - **Safety** — pre-trade validation, response sanitization, client-id deduplication
@@ -42,7 +42,7 @@ perp wallet show
42
42
 
43
43
  Same EVM key works for both Hyperliquid and Lighter.
44
44
 
45
- > **Lighter API Key Index:** Indexes 0–3 are reserved by Lighter's frontend (web/mobile). perp-cli defaults to index `4`. Override with `LIGHTER_API_KEY_INDEX` env var or `--key-index` flag on `manage setup-api-key`. Valid range: 4–254.
45
+ > **Lighter API Key Index:** Indexes 0–3 are reserved by Lighter's frontend (web/mobile). perp-cli defaults to index `4`. Override with `LIGHTER_API_KEY_INDEX` env var, or use `perp wallet agent approve lighter --api-key-index <n>` for managed agent slots. Valid range: 4–254.
46
46
 
47
47
  ## Command Groups
48
48
 
@@ -52,9 +52,8 @@ Same EVM key works for both Hyperliquid and Lighter.
52
52
  | `account` | Balance, positions, orders, margin |
53
53
  | `trade` | Market/limit/stop orders, close, scale, split execution |
54
54
  | `arb` | Funding rate arb — scan, exec, close, monitor (perp-perp & spot-perp) |
55
- | `bot` | 19 strategies, APEX orchestrator, REFLECT analytics, presets |
56
- | `funds` | Deposit, withdraw, transfer, CCTP bridge |
57
- | `bridge` | Cross-chain USDC bridge (deBridge DLN) |
55
+ | `strategy` | 19 bot algorithms (grid, dca, twap, APEX, REFLECT, presets) + nested scripted plans |
56
+ | `funds` | Deposit, withdraw, transfer, cross-chain bridge (multi-provider), inter-exchange rebalance |
58
57
  | `risk` | Risk limits, liquidation distance, guardrails |
59
58
  | `wallet` | Multi-wallet management & on-chain balances |
60
59
  | `history` | Execution log, PnL, performance breakdown |
@@ -63,9 +62,7 @@ Same EVM key works for both Hyperliquid and Lighter.
63
62
  | `dashboard` | Live web dashboard |
64
63
  | `settings` | CLI settings (referrals, defaults) |
65
64
  | `backtest` | Strategy backtesting |
66
- | `plan` | Multi-step composite execution plans |
67
- | `rebalance` | Cross-exchange balance management |
68
- | `jobs` | Background job management (tmux) |
65
+ | `background` | Background process supervisor (tmux sessions for strategies, alerts, etc.) |
69
66
  | `alerts` | Telegram funding rate alerts with background daemon |
70
67
  | `agent` | Schema introspection, capabilities, health check |
71
68
  | `setup` | Interactive setup wizard (alias: `init`) |
@@ -117,28 +114,39 @@ perp --json arb exec <SYM> spot:<exch> <perpEx> <$> # spot+perp entry
117
114
  perp --json arb config # show arb defaults
118
115
  perp --json arb history # past arb trade performance (alias: log)
119
116
 
120
- # Funds (deposit, withdraw, transfer)
117
+ # Funds (deposit, withdraw, transfer, bridge, rebalance)
121
118
  perp --json funds deposit hyperliquid 100
122
119
  perp --json funds withdraw pacifica 50
123
- perp --json funds transfer 100 <ADDRESS> # HL internal transfer
120
+ perp --json funds transfer 100 <ADDRESS> # HL internal transfer (instant)
121
+ perp --json funds bridge quote --from solana --to arbitrum --amount 100
122
+ perp --json funds bridge send --from solana --to arbitrum --amount 100 # auto-cheapest provider
123
+ perp --json funds bridge exchange --from pacifica --to hyperliquid --amount 100
124
+ perp --json funds rebalance check # balances across exchanges
125
+ perp --json funds rebalance plan # compute optimal moves
126
+ perp --json funds rebalance execute --auto-bridge # withdraw → bridge → deposit
124
127
  perp --json funds info # all routes & limits
125
128
 
126
129
  # Risk
127
130
  perp --json risk limits --max-leverage 5
128
131
  perp --json risk liquidation-distance
129
132
 
130
- # Bots (19 strategies)
131
- perp bot list-strategies # list all available strategies
132
- perp bot run <strategy> [symbol] # run any strategy
133
- perp bot apex [symbol] # APEX autonomous orchestrator
134
- perp bot reflect # trading performance analysis
135
- perp bot preset-list # list strategy presets
136
- perp --json bot twap <SYM> buy <SIZE> 30m
137
- perp --json bot grid <SYM> --range 5 --grids 10 --size 100
138
-
139
- # Bridge (cross-chain USDC)
140
- perp --json bridge quote --from solana --to arbitrum --amount 100
141
- perp --json bridge send --from solana --to arbitrum --amount 100
133
+ # Strategies (19 bot algorithms + nested scripted plans)
134
+ perp strategy list-strategies # list all available strategies
135
+ perp strategy run <strategy> [symbol] # run any strategy
136
+ perp strategy apex [symbol] # APEX autonomous orchestrator
137
+ perp strategy reflect # trading performance analysis
138
+ perp strategy preset-list # list strategy presets
139
+ perp --json strategy twap <SYM> buy <SIZE> 30m
140
+ perp --json strategy grid <SYM> --range 5 --grids 10 --size 100
141
+ perp strategy plan example # scripted multi-step plan format
142
+ perp --json strategy plan validate <FILE> # validate scripted plan
143
+ perp --json strategy plan execute <FILE> --dry-run # dry-run scripted plan
144
+
145
+ # Background process supervisor (tmux)
146
+ perp background list # list running jobs
147
+ perp background stop <ID> # stop a job
148
+ perp background logs <ID> -f # follow logs
149
+
142
150
  ```
143
151
 
144
152
  ## Telegram Alerts
@@ -231,9 +239,6 @@ Built following [agent-first CLI principles](https://justin.poehnelt.com/posts/r
231
239
  perp --json portfolio
232
240
  # → { "ok": true, "data": {...}, "meta": { "timestamp": "..." } }
233
241
 
234
- # Runtime schema introspection (don't guess commands — query this)
235
- perp --json agent schema
236
-
237
242
  # Filter output to specific fields (saves tokens)
238
243
  perp --json --fields totalEquity,risk portfolio
239
244
 
File without changes
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ // Install a stderr filter that drops known noisy SDK warnings when --json
3
+ // or --ndjson is set. Module body runs at import time — must be the FIRST
4
+ // import in src/index.ts so it activates before SDK module bodies.
5
+ if (process.argv.includes("--json") || process.argv.includes("--ndjson")) {
6
+ const _suppressPatterns = [
7
+ /'--chain evm' is deprecated/,
8
+ /\[hyperliquid\] Failed to load asset map/,
9
+ /\[lighter\] Account index not available/,
10
+ /Failed to initialize SymbolConversion/,
11
+ /HyperliquidAPIError/,
12
+ ];
13
+ // Wrap at the lowest level (_write on the Writable) so we catch writes
14
+ // even if a downstream module replaces process.stderr.write itself.
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const stderr = process.stderr;
17
+ const _origStderrWriteRaw = stderr._write.bind(stderr);
18
+ stderr._write = function (chunk, encoding, callback) {
19
+ const s = typeof chunk === "string"
20
+ ? chunk
21
+ : (chunk instanceof Uint8Array || Buffer.isBuffer(chunk))
22
+ ? Buffer.from(chunk).toString()
23
+ : "";
24
+ if (_suppressPatterns.some(re => re.test(s))) {
25
+ callback();
26
+ return;
27
+ }
28
+ return _origStderrWriteRaw(chunk, encoding, callback);
29
+ };
30
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentMeta } from "../settings.js";
2
+ /**
3
+ * Returns true if the agent's expiry timestamp is in the past.
4
+ */
5
+ export declare function isExpired(meta: AgentMeta): boolean;
6
+ /**
7
+ * Returns the number of days (float) until the agent expires.
8
+ * Negative if already expired.
9
+ */
10
+ export declare function daysUntilExpiry(meta: AgentMeta): number;
11
+ /**
12
+ * Returns a human-readable expiry string, e.g.
13
+ * "expires in 7 days (2026-05-05)" or "expired 3 days ago (2026-04-25)".
14
+ */
15
+ export declare function formatExpiry(meta: AgentMeta): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Returns true if the agent's expiry timestamp is in the past.
3
+ */
4
+ export function isExpired(meta) {
5
+ return Date.now() >= new Date(meta.expiresAt).getTime();
6
+ }
7
+ /**
8
+ * Returns the number of days (float) until the agent expires.
9
+ * Negative if already expired.
10
+ */
11
+ export function daysUntilExpiry(meta) {
12
+ const ms = new Date(meta.expiresAt).getTime() - Date.now();
13
+ return ms / (1000 * 60 * 60 * 24);
14
+ }
15
+ /**
16
+ * Returns a human-readable expiry string, e.g.
17
+ * "expires in 7 days (2026-05-05)" or "expired 3 days ago (2026-04-25)".
18
+ */
19
+ export function formatExpiry(meta) {
20
+ const days = daysUntilExpiry(meta);
21
+ const dateStr = new Date(meta.expiresAt).toISOString().slice(0, 10);
22
+ if (days < 0) {
23
+ return `expired ${Math.abs(days).toFixed(0)} days ago (${dateStr})`;
24
+ }
25
+ return `expires in ${days.toFixed(0)} days (${dateStr})`;
26
+ }
@@ -0,0 +1,34 @@
1
+ /** Pure: deterministic absolute path for the keystore file. */
2
+ export declare function lighterKeystorePath(accountIndex: number, slot: number): string;
3
+ /**
4
+ * Encrypt a 40-byte Lighter L2 private key (hex) and write it to
5
+ * `~/.perp/lighter-agents/<accountIndex>-<slot>.json` with mode 0600.
6
+ *
7
+ * Returns the absolute path of the written file.
8
+ */
9
+ export declare function saveLighterKey(accountIndex: number, slot: number, privateKeyHex: string, passphrase: string): string;
10
+ /**
11
+ * Read + decrypt a stored Lighter L2 private key. Returns the hex string
12
+ * without `0x` prefix.
13
+ *
14
+ * Throws (no fallback):
15
+ * - LIGHTER_KEYSTORE_NOT_FOUND if the file is missing
16
+ * - LIGHTER_KEYSTORE_DECRYPT_FAILED if the passphrase is wrong / file corrupt
17
+ */
18
+ export declare function loadLighterKey(accountIndex: number, slot: number, passphrase: string): string;
19
+ /**
20
+ * Idempotent delete. Returns true if a file was removed, false if absent.
21
+ */
22
+ export declare function deleteLighterKey(accountIndex: number, slot: number): boolean;
23
+ /**
24
+ * One-time bridge from legacy env-based storage to the keystore.
25
+ *
26
+ * Triggered on a single, narrow condition: env has `LIGHTER_API_KEY` AND the
27
+ * target keystore file does not yet exist. After successful save, clears the
28
+ * three legacy env entries from `~/.perp/.env` so they cannot be re-read.
29
+ *
30
+ * NOT a fallback (SSOT Rule #2): runs at most once per install (post-clear, the
31
+ * trigger condition is permanently false). Single-direction (env → keystore);
32
+ * never reads from keystore back to env.
33
+ */
34
+ export declare function migrateFromEnvIfPresent(): void;
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Lighter L2 agent keystore (SSOT Rule #3 — encrypted at-rest).
3
+ *
4
+ * Lighter's L2 signing key is a 40-byte secp256k1 private key — a non-OWS-native
5
+ * curve, so it cannot live in `~/.ows/wallets/` like Aster/HL/PAC agent keypairs.
6
+ * Per the FIXME(2d-spike) in src/commands/agent.ts and SSOT Rule #3 in CLAUDE.md,
7
+ * we persist these keys at `~/.perp/lighter-agents/<accountIndex>-<slot>.json`
8
+ * using the same AES-256-GCM + scrypt encryption scheme OWS uses.
9
+ *
10
+ * Empty passphrase ("") is acceptable — mirrors the HL/PAC pattern. Security
11
+ * model = file mode 0600 + obfuscation, NOT cryptographic strength against a
12
+ * local attacker. A non-empty passphrase upgrades the threat model.
13
+ *
14
+ * No fallbacks (SSOT Rule #2): missing files / wrong passphrase throw with
15
+ * remediation pointing the user at `perp wallet agent approve lighter --rotate`.
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, chmodSync, } from "fs";
18
+ import { resolve } from "path";
19
+ import { randomBytes, scryptSync, createCipheriv, createDecipheriv, } from "node:crypto";
20
+ import { PerpError } from "../errors.js";
21
+ const KDF_PARAMS = { dklen: 32, n: 65536, p: 1, r: 8 };
22
+ const SCRYPT_OPTS = { N: KDF_PARAMS.n, r: KDF_PARAMS.r, p: KDF_PARAMS.p, maxmem: 128 * 1024 * 1024 };
23
+ const REMEDIATION = "perp wallet agent approve lighter --rotate";
24
+ function lighterAgentsDir() {
25
+ return resolve(process.env["HOME"] ?? "~", ".perp", "lighter-agents");
26
+ }
27
+ /** Pure: deterministic absolute path for the keystore file. */
28
+ export function lighterKeystorePath(accountIndex, slot) {
29
+ return resolve(lighterAgentsDir(), `${accountIndex}-${slot}.json`);
30
+ }
31
+ function ensureDir() {
32
+ const dir = lighterAgentsDir();
33
+ if (!existsSync(dir)) {
34
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
35
+ }
36
+ else {
37
+ // best-effort tighten permissions if dir exists with looser mode
38
+ try {
39
+ chmodSync(dir, 0o700);
40
+ }
41
+ catch { /* non-fatal */ }
42
+ }
43
+ return dir;
44
+ }
45
+ function stripHexPrefix(hex) {
46
+ return hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
47
+ }
48
+ /**
49
+ * Encrypt a 40-byte Lighter L2 private key (hex) and write it to
50
+ * `~/.perp/lighter-agents/<accountIndex>-<slot>.json` with mode 0600.
51
+ *
52
+ * Returns the absolute path of the written file.
53
+ */
54
+ export function saveLighterKey(accountIndex, slot, privateKeyHex, passphrase) {
55
+ const dir = ensureDir();
56
+ const path = resolve(dir, `${accountIndex}-${slot}.json`);
57
+ const cleanHex = stripHexPrefix(privateKeyHex).toLowerCase();
58
+ if (!/^[0-9a-f]+$/.test(cleanHex) || cleanHex.length === 0) {
59
+ throw new PerpError("INVALID_PARAMS", "Lighter L2 private key must be hex", {
60
+ remediation: REMEDIATION,
61
+ });
62
+ }
63
+ const salt = randomBytes(32);
64
+ const iv = randomBytes(12);
65
+ const dk = scryptSync(Buffer.from(passphrase, "utf-8"), salt, KDF_PARAMS.dklen, SCRYPT_OPTS);
66
+ const cipher = createCipheriv("aes-256-gcm", dk, iv);
67
+ const ciphertext = Buffer.concat([
68
+ cipher.update(Buffer.from(cleanHex, "utf-8")),
69
+ cipher.final(),
70
+ ]);
71
+ const authTag = cipher.getAuthTag();
72
+ const file = {
73
+ version: 1,
74
+ accountIndex,
75
+ slot,
76
+ crypto: {
77
+ cipher: "aes-256-gcm",
78
+ cipherparams: { iv: iv.toString("hex") },
79
+ ciphertext: ciphertext.toString("hex"),
80
+ auth_tag: authTag.toString("hex"),
81
+ kdf: "scrypt",
82
+ kdfparams: {
83
+ dklen: KDF_PARAMS.dklen,
84
+ n: KDF_PARAMS.n,
85
+ p: KDF_PARAMS.p,
86
+ r: KDF_PARAMS.r,
87
+ salt: salt.toString("hex"),
88
+ },
89
+ },
90
+ createdAt: new Date().toISOString(),
91
+ };
92
+ writeFileSync(path, JSON.stringify(file, null, 2), { mode: 0o600 });
93
+ // Re-tighten in case writeFileSync overwrote an existing file with a looser umask
94
+ try {
95
+ chmodSync(path, 0o600);
96
+ }
97
+ catch { /* non-fatal */ }
98
+ return path;
99
+ }
100
+ /**
101
+ * Read + decrypt a stored Lighter L2 private key. Returns the hex string
102
+ * without `0x` prefix.
103
+ *
104
+ * Throws (no fallback):
105
+ * - LIGHTER_KEYSTORE_NOT_FOUND if the file is missing
106
+ * - LIGHTER_KEYSTORE_DECRYPT_FAILED if the passphrase is wrong / file corrupt
107
+ */
108
+ export function loadLighterKey(accountIndex, slot, passphrase) {
109
+ // One-time data migration: if a legacy `LIGHTER_API_KEY` env var is set and
110
+ // the new keystore file does not exist yet, materialize the keystore from env
111
+ // before continuing. This is NOT a runtime fallback — it's a single-pass
112
+ // upgrade path that runs at most once per install.
113
+ migrateFromEnvIfPresent();
114
+ const path = lighterKeystorePath(accountIndex, slot);
115
+ if (!existsSync(path)) {
116
+ throw new PerpError("KEY_NOT_FOUND", `LIGHTER_KEYSTORE_NOT_FOUND: ${path}`, { remediation: REMEDIATION });
117
+ }
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
121
+ }
122
+ catch (e) {
123
+ throw new PerpError("SIGNATURE_FAILED", `LIGHTER_KEYSTORE_DECRYPT_FAILED: corrupt JSON — ${e instanceof Error ? e.message : String(e)}`, { remediation: REMEDIATION });
124
+ }
125
+ if (parsed.version !== 1 || parsed.crypto?.cipher !== "aes-256-gcm" || parsed.crypto?.kdf !== "scrypt") {
126
+ throw new PerpError("SIGNATURE_FAILED", "LIGHTER_KEYSTORE_DECRYPT_FAILED: unsupported version or cipher", { remediation: REMEDIATION });
127
+ }
128
+ const salt = Buffer.from(parsed.crypto.kdfparams.salt, "hex");
129
+ const iv = Buffer.from(parsed.crypto.cipherparams.iv, "hex");
130
+ const ciphertext = Buffer.from(parsed.crypto.ciphertext, "hex");
131
+ const authTag = Buffer.from(parsed.crypto.auth_tag, "hex");
132
+ const dk = scryptSync(Buffer.from(passphrase, "utf-8"), salt, parsed.crypto.kdfparams.dklen, {
133
+ N: parsed.crypto.kdfparams.n,
134
+ r: parsed.crypto.kdfparams.r,
135
+ p: parsed.crypto.kdfparams.p,
136
+ maxmem: 128 * 1024 * 1024,
137
+ });
138
+ try {
139
+ const decipher = createDecipheriv("aes-256-gcm", dk, iv);
140
+ decipher.setAuthTag(authTag);
141
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
142
+ return plain.toString("utf-8");
143
+ }
144
+ catch (e) {
145
+ throw new PerpError("SIGNATURE_FAILED", `LIGHTER_KEYSTORE_DECRYPT_FAILED: ${e instanceof Error ? e.message : String(e)}`, { remediation: REMEDIATION });
146
+ }
147
+ }
148
+ /**
149
+ * Idempotent delete. Returns true if a file was removed, false if absent.
150
+ */
151
+ export function deleteLighterKey(accountIndex, slot) {
152
+ const path = lighterKeystorePath(accountIndex, slot);
153
+ if (!existsSync(path))
154
+ return false;
155
+ unlinkSync(path);
156
+ return true;
157
+ }
158
+ /**
159
+ * One-time bridge from legacy env-based storage to the keystore.
160
+ *
161
+ * Triggered on a single, narrow condition: env has `LIGHTER_API_KEY` AND the
162
+ * target keystore file does not yet exist. After successful save, clears the
163
+ * three legacy env entries from `~/.perp/.env` so they cannot be re-read.
164
+ *
165
+ * NOT a fallback (SSOT Rule #2): runs at most once per install (post-clear, the
166
+ * trigger condition is permanently false). Single-direction (env → keystore);
167
+ * never reads from keystore back to env.
168
+ */
169
+ export function migrateFromEnvIfPresent() {
170
+ const envKey = process.env["LIGHTER_API_KEY"];
171
+ if (!envKey || envKey.length === 0)
172
+ return;
173
+ const accountIndexStr = process.env["LIGHTER_ACCOUNT_INDEX"];
174
+ const apiKeyIndexStr = process.env["LIGHTER_API_KEY_INDEX"];
175
+ const accountIndex = accountIndexStr ? parseInt(accountIndexStr, 10) : NaN;
176
+ const slot = apiKeyIndexStr ? parseInt(apiKeyIndexStr, 10) : NaN;
177
+ // If we lack the indices needed to address the keystore slot, bail silently.
178
+ // Migration only runs when the env trio is fully populated.
179
+ if (!Number.isFinite(accountIndex) || accountIndex < 0)
180
+ return;
181
+ if (!Number.isFinite(slot) || slot < 0)
182
+ return;
183
+ const target = lighterKeystorePath(accountIndex, slot);
184
+ if (existsSync(target))
185
+ return;
186
+ saveLighterKey(accountIndex, slot, envKey, "");
187
+ process.stderr.write(`[lighter] migrated LIGHTER_API_KEY env → ~/.perp/lighter-agents/${accountIndex}-${slot}.json (SSOT Rule #3)\n`);
188
+ // Clear the legacy env entries from ~/.perp/.env so the trigger never fires
189
+ // again. setEnvVar(name, "") rewrites the file with the empty value.
190
+ try {
191
+ // Inline lazy import to avoid a circular dep at module-load time.
192
+ // setEnvVar is sync; the dynamic import is deliberately resolved lazily.
193
+ void clearEnvLegacyKeys();
194
+ }
195
+ catch {
196
+ // best-effort — file may be locked or read-only; the in-memory delete below
197
+ // still prevents this process from re-triggering migration.
198
+ }
199
+ delete process.env["LIGHTER_API_KEY"];
200
+ delete process.env["LIGHTER_ACCOUNT_INDEX"];
201
+ delete process.env["LIGHTER_API_KEY_INDEX"];
202
+ }
203
+ // Async helper deliberately split out so migrateFromEnvIfPresent stays sync.
204
+ function clearEnvLegacyKeys() {
205
+ // Use the same .env parser/writer the rest of the CLI uses so we don't
206
+ // accidentally drop comments or other entries.
207
+ // Sync read + write via fs.
208
+ const PERP_DIR = resolve(process.env["HOME"] ?? "~", ".perp");
209
+ const ENV_FILE = resolve(PERP_DIR, ".env");
210
+ if (!existsSync(ENV_FILE))
211
+ return;
212
+ const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
213
+ const out = [];
214
+ const dropKeys = new Set([
215
+ "LIGHTER_API_KEY",
216
+ "LIGHTER_ACCOUNT_INDEX",
217
+ "LIGHTER_API_KEY_INDEX",
218
+ ]);
219
+ for (const line of lines) {
220
+ const trimmed = line.trim();
221
+ if (!trimmed || trimmed.startsWith("#")) {
222
+ out.push(line);
223
+ continue;
224
+ }
225
+ const eqIdx = trimmed.indexOf("=");
226
+ if (eqIdx > 0 && dropKeys.has(trimmed.slice(0, eqIdx))) {
227
+ continue;
228
+ }
229
+ out.push(line);
230
+ }
231
+ writeFileSync(ENV_FILE, out.join("\n"), { mode: 0o600 });
232
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Resolve master passphrase from (in precedence order):
3
+ * 1. opts.flag (--passphrase)
4
+ * 2. process.env.OWS_PASSPHRASE
5
+ * 3. stdin (only if process.stdin.isTTY === false)
6
+ *
7
+ * Returns null if no path provides one AND stdin IS TTY (caller may prompt).
8
+ * Throws PASSPHRASE_REQUIRED with remediation if no path provides one AND
9
+ * stdin is non-TTY (AC-16).
10
+ */
11
+ export declare function resolvePassphrase(opts: {
12
+ flag?: string;
13
+ }): Promise<string | null>;
@@ -0,0 +1,54 @@
1
+ import { PerpError } from "../errors.js";
2
+ const REMEDIATION = "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe (echo $PP | perp ...)";
3
+ /**
4
+ * Resolve master passphrase from (in precedence order):
5
+ * 1. opts.flag (--passphrase)
6
+ * 2. process.env.OWS_PASSPHRASE
7
+ * 3. stdin (only if process.stdin.isTTY === false)
8
+ *
9
+ * Returns null if no path provides one AND stdin IS TTY (caller may prompt).
10
+ * Throws PASSPHRASE_REQUIRED with remediation if no path provides one AND
11
+ * stdin is non-TTY (AC-16).
12
+ */
13
+ export async function resolvePassphrase(opts) {
14
+ // 1. CLI flag (allow empty string when explicitly passed via --passphrase '')
15
+ if (opts.flag !== undefined) {
16
+ return opts.flag;
17
+ }
18
+ // 2. Environment variable (allow empty string when explicitly set; treat
19
+ // unset as absent)
20
+ if ("OWS_PASSPHRASE" in process.env) {
21
+ return process.env["OWS_PASSPHRASE"] ?? "";
22
+ }
23
+ // 3. stdin pipe — only when stdin is not a TTY
24
+ if (process.stdin.isTTY === false) {
25
+ const piped = await readStdin();
26
+ if (piped !== null) {
27
+ return piped;
28
+ }
29
+ // stdin was non-TTY but empty — throw rather than falling through to prompt
30
+ throw new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY", {
31
+ remediation: REMEDIATION,
32
+ });
33
+ }
34
+ // stdin IS TTY — caller decides whether to prompt interactively
35
+ return null;
36
+ }
37
+ /**
38
+ * Read all bytes from stdin until EOF, trim trailing newlines.
39
+ * Returns null if stdin produced zero bytes.
40
+ */
41
+ async function readStdin() {
42
+ return new Promise((resolve, reject) => {
43
+ const chunks = [];
44
+ process.stdin.setEncoding("utf-8");
45
+ process.stdin.on("data", (chunk) => {
46
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf-8"));
47
+ });
48
+ process.stdin.on("end", () => {
49
+ const raw = chunks.join("").replace(/\r?\n$/, "");
50
+ resolve(raw.length > 0 ? raw : null);
51
+ });
52
+ process.stdin.on("error", reject);
53
+ });
54
+ }
@@ -0,0 +1,67 @@
1
+ import type { AgentMeta } from "../settings.js";
2
+ /**
3
+ * Interface for agent-key signing strategies.
4
+ * The AsterAdapter consumes this interface, never a concrete class.
5
+ *
6
+ * Signature shape mirrors EvmSigner.signTypedData(domain, types, message) so
7
+ * call sites can cast to `EvmSigner & AgentSigningStrategy` and dispatch
8
+ * uniformly. The return type differs from EvmSigner (which returns string):
9
+ * AgentSigningStrategy returns { signature, r, s, v } so callers can pull
10
+ * raw r/s/v if needed.
11
+ */
12
+ export interface AgentSigningStrategy {
13
+ signTypedData(domain: Record<string, unknown>, types: Record<string, Array<{
14
+ name: string;
15
+ type: string;
16
+ }>>, message: Record<string, unknown>): Promise<{
17
+ signature: string;
18
+ r: string;
19
+ s: string;
20
+ v: number;
21
+ }>;
22
+ getAddress(): string;
23
+ }
24
+ /**
25
+ * Strategy: pass the OWS api-key token in the passphrase slot.
26
+ * This is the default (Option A γ-path). Validity depends on Step 0a spike.
27
+ * Wraps OwsEvmSigner.create(walletName, owsKeyToken).
28
+ */
29
+ export declare class TokenAsPassphraseStrategy implements AgentSigningStrategy {
30
+ private _walletName;
31
+ private _owsKeyToken;
32
+ private _address;
33
+ constructor(walletName: string, owsKeyToken: string, address: string);
34
+ getAddress(): string;
35
+ signTypedData(domain: Record<string, unknown>, types: Record<string, Array<{
36
+ name: string;
37
+ type: string;
38
+ }>>, message: Record<string, unknown>): Promise<{
39
+ signature: string;
40
+ r: string;
41
+ s: string;
42
+ v: number;
43
+ }>;
44
+ }
45
+ /**
46
+ * Strategy: read the token from the OWS keyfile by api-key id and pass it
47
+ * explicitly. Stub until Step 0a determines whether this path is needed.
48
+ */
49
+ export declare class ExplicitTokenStrategy implements AgentSigningStrategy {
50
+ constructor(_walletName: string, _apiKeyId: string, _address: string);
51
+ getAddress(): string;
52
+ signTypedData(_domain: Record<string, unknown>, _types: Record<string, Array<{
53
+ name: string;
54
+ type: string;
55
+ }>>, _message: Record<string, unknown>): Promise<{
56
+ signature: string;
57
+ r: string;
58
+ s: string;
59
+ v: number;
60
+ }>;
61
+ }
62
+ /**
63
+ * Factory: returns the appropriate signing strategy for the given agent meta +
64
+ * OWS api-key token. Defaults to TokenAsPassphraseStrategy per Option A γ-path.
65
+ * Will switch based on Step 0a spike outcome.
66
+ */
67
+ export declare function agentSigningStrategyFor(meta: AgentMeta, owsKeyToken: string): AgentSigningStrategy;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Strategy: pass the OWS api-key token in the passphrase slot.
3
+ * This is the default (Option A γ-path). Validity depends on Step 0a spike.
4
+ * Wraps OwsEvmSigner.create(walletName, owsKeyToken).
5
+ */
6
+ export class TokenAsPassphraseStrategy {
7
+ _walletName;
8
+ _owsKeyToken;
9
+ _address;
10
+ constructor(walletName, owsKeyToken, address) {
11
+ this._walletName = walletName;
12
+ this._owsKeyToken = owsKeyToken;
13
+ this._address = address;
14
+ }
15
+ getAddress() {
16
+ return this._address;
17
+ }
18
+ async signTypedData(domain, types, message) {
19
+ // Lazy import to avoid loading OWS bindings unless this strategy is used
20
+ const { OwsEvmSigner } = await import("../signer/ows-evm.js");
21
+ const signer = OwsEvmSigner.create(this._walletName, this._owsKeyToken);
22
+ const hexSig = await signer.signTypedData(domain, types, message);
23
+ // Parse r, s, v from the 65-byte hex signature
24
+ const sig = hexSig.startsWith("0x") ? hexSig.slice(2) : hexSig;
25
+ const r = `0x${sig.slice(0, 64)}`;
26
+ const s = `0x${sig.slice(64, 128)}`;
27
+ const v = parseInt(sig.slice(128, 130), 16);
28
+ return { signature: hexSig, r, s, v };
29
+ }
30
+ }
31
+ /**
32
+ * Strategy: read the token from the OWS keyfile by api-key id and pass it
33
+ * explicitly. Stub until Step 0a determines whether this path is needed.
34
+ */
35
+ export class ExplicitTokenStrategy {
36
+ constructor(_walletName, _apiKeyId, _address) { }
37
+ getAddress() {
38
+ throw new Error("ExplicitTokenStrategy.getAddress(): not implemented — pending Step 0a spike outcome");
39
+ }
40
+ async signTypedData(_domain, _types, _message) {
41
+ throw new Error("ExplicitTokenStrategy.signTypedData(): not implemented — pending Step 0a spike outcome");
42
+ }
43
+ }
44
+ /**
45
+ * Factory: returns the appropriate signing strategy for the given agent meta +
46
+ * OWS api-key token. Defaults to TokenAsPassphraseStrategy per Option A γ-path.
47
+ * Will switch based on Step 0a spike outcome.
48
+ */
49
+ export function agentSigningStrategyFor(meta, owsKeyToken) {
50
+ return new TokenAsPassphraseStrategy(meta.agentWalletName, owsKeyToken, meta.agentEvmAddress);
51
+ }