@dev.sail.money/sailor 0.0.2-21 → 0.0.2-22

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 (102) hide show
  1. package/package.json +1 -1
  2. package/packages/cli/dist/index.cjs +2347 -2037
  3. package/packages/cli/dist/server.cjs +110 -140
  4. package/packages/sdk/dist/deployments.d.ts +14 -7
  5. package/packages/sdk/dist/deployments.d.ts.map +1 -1
  6. package/packages/sdk/dist/deployments.js +132 -141
  7. package/packages/sdk/dist/deployments.js.map +1 -1
  8. package/packages/sdk/dist/intelligence.d.ts +1 -1
  9. package/packages/sdk/dist/intelligence.js +1 -1
  10. package/packages/ui/dist/assets/{add-BzRDG6go.js → add-C--RBwJe.js} +1 -1
  11. package/packages/ui/dist/assets/{all-wallets-C6juL2cm.js → all-wallets-_xwd_eso.js} +1 -1
  12. package/packages/ui/dist/assets/{app-store-DSJ1ow5G.js → app-store-CIQsK1zU.js} +1 -1
  13. package/packages/ui/dist/assets/{apple-CUEgIX9k.js → apple-BdlAnnmO.js} +1 -1
  14. package/packages/ui/dist/assets/{arrow-bottom-BOhBj1Je.js → arrow-bottom-B5p_6Dat.js} +1 -1
  15. package/packages/ui/dist/assets/{arrow-bottom-circle-C4CzuHiR.js → arrow-bottom-circle-D7c6JPTF.js} +1 -1
  16. package/packages/ui/dist/assets/{arrow-left-BTD8zZ12.js → arrow-left-SA4NpEnP.js} +1 -1
  17. package/packages/ui/dist/assets/{arrow-right-qcOukPwm.js → arrow-right-mOJNWujS.js} +1 -1
  18. package/packages/ui/dist/assets/{arrow-top-C0f2945G.js → arrow-top-CvPVVpHl.js} +1 -1
  19. package/packages/ui/dist/assets/{bank-BSWzLP3R.js → bank-B2j2rPm9.js} +1 -1
  20. package/packages/ui/dist/assets/{basic-DGTBsmnG.js → basic-Bw6cXOlk.js} +1 -1
  21. package/packages/ui/dist/assets/{browser-DJNepafc.js → browser-CUSNF__N.js} +1 -1
  22. package/packages/ui/dist/assets/{card-u08LJx43.js → card-CpKLox49.js} +1 -1
  23. package/packages/ui/dist/assets/{ccip-DcWZjG37.js → ccip-XB9iQjXB.js} +1 -1
  24. package/packages/ui/dist/assets/{checkmark-DVD-obDl.js → checkmark-BRpXeSCK.js} +1 -1
  25. package/packages/ui/dist/assets/{checkmark-bold-CSXDqIAx.js → checkmark-bold-BkPvoqxo.js} +1 -1
  26. package/packages/ui/dist/assets/{chevron-bottom-Be7f0gi2.js → chevron-bottom-CtK0W2av.js} +1 -1
  27. package/packages/ui/dist/assets/{chevron-left-v8cgKRhQ.js → chevron-left-NayfPMDy.js} +1 -1
  28. package/packages/ui/dist/assets/{chevron-right-B0ux2X-3.js → chevron-right-BPU2hCfA.js} +1 -1
  29. package/packages/ui/dist/assets/{chevron-top-De-a8tmA.js → chevron-top-CTXwC4nM.js} +1 -1
  30. package/packages/ui/dist/assets/{chrome-store-BIIIRGPA.js → chrome-store-eWIk0-YZ.js} +1 -1
  31. package/packages/ui/dist/assets/{clock-8d5kvRPQ.js → clock-VmYiq5jB.js} +1 -1
  32. package/packages/ui/dist/assets/{close-BSSJkFv0.js → close-NfBukMzW.js} +1 -1
  33. package/packages/ui/dist/assets/{coinPlaceholder-CLJaQiUO.js → coinPlaceholder-BWOeJc6j.js} +1 -1
  34. package/packages/ui/dist/assets/{compass-CR1zP0b-.js → compass-oRk8W3iM.js} +1 -1
  35. package/packages/ui/dist/assets/{copy-CGkuIFo6.js → copy-GcYQZOsF.js} +1 -1
  36. package/packages/ui/dist/assets/{core-ea860JM2.js → core-B_rvnvkC.js} +3 -3
  37. package/packages/ui/dist/assets/cursor-BAViuJWh.js +3 -0
  38. package/packages/ui/dist/assets/{cursor-transparent-C8s5LY_P.js → cursor-transparent-CGox3wZ-.js} +1 -1
  39. package/packages/ui/dist/assets/{desktop-CQyixryE.js → desktop-DU4yyiV4.js} +1 -1
  40. package/packages/ui/dist/assets/{disconnect-Ct0234l0.js → disconnect-CJm9NnxK.js} +1 -1
  41. package/packages/ui/dist/assets/{discord-haYPGMDl.js → discord-MxDL8Eq6.js} +1 -1
  42. package/packages/ui/dist/assets/{etherscan-BTLrS1KK.js → etherscan-CkCvlZiA.js} +1 -1
  43. package/packages/ui/dist/assets/{events-wdo_D3Zy.js → events-CkyJn32_.js} +1 -1
  44. package/packages/ui/dist/assets/{exclamation-triangle-CMlYpOat.js → exclamation-triangle-hH1JdYAZ.js} +1 -1
  45. package/packages/ui/dist/assets/{extension-CNGBCbo8.js → extension-DTMrXG5m.js} +1 -1
  46. package/packages/ui/dist/assets/{external-link-DmYJSKcL.js → external-link-GSwn5MzD.js} +1 -1
  47. package/packages/ui/dist/assets/{facebook-F0iMVTem.js → facebook-Vw_uyzaE.js} +1 -1
  48. package/packages/ui/dist/assets/{fallback-BqeFDEuW.js → fallback-BL3U4ZRT.js} +1 -1
  49. package/packages/ui/dist/assets/{farcaster-CYr9I6UA.js → farcaster-F-_di36M.js} +1 -1
  50. package/packages/ui/dist/assets/{filters-CxE97nqU.js → filters-DQzcstDl.js} +1 -1
  51. package/packages/ui/dist/assets/{github-CjRht-Wv.js → github-BSq3_rEd.js} +1 -1
  52. package/packages/ui/dist/assets/{google-3G0o4pR9.js → google-BU4QXiDS.js} +1 -1
  53. package/packages/ui/dist/assets/{help-circle-C5gNChqZ.js → help-circle-CuF4iPyF.js} +1 -1
  54. package/packages/ui/dist/assets/{id-C6_zK0Tb.js → id-BQWlv0a_.js} +1 -1
  55. package/packages/ui/dist/assets/{image-DsU8Irlu.js → image-BPNySDPo.js} +1 -1
  56. package/packages/ui/dist/assets/{index-D1lgDFZV.js → index-BMPQOOgv.js} +1 -1
  57. package/packages/ui/dist/assets/{index-DQ44LBvq.js → index-CMyY4FOR.js} +3 -3
  58. package/packages/ui/dist/assets/{index-Drc17uEc.js → index-CsbiKM3b.js} +1 -1
  59. package/packages/ui/dist/assets/{index-CjvcQefO.js → index-D0SPxlSM.js} +1 -1
  60. package/packages/ui/dist/assets/{index-c8ZmMTds.js → index-D2wgBslE.js} +1 -1
  61. package/packages/ui/dist/assets/{index-BCb0Nju4.js → index-Dc9_WV0G.js} +76 -76
  62. package/packages/ui/dist/assets/{index.es-BhDQmlR4.js → index.es-CvyDIsY4.js} +4 -4
  63. package/packages/ui/dist/assets/{info-CAKKH6T2.js → info-D20yslek.js} +1 -1
  64. package/packages/ui/dist/assets/{info-circle-CHV1idfy.js → info-circle-BEjvYTHa.js} +1 -1
  65. package/packages/ui/dist/assets/{lightbulb-ew10LUMl.js → lightbulb-DfvLi5mQ.js} +1 -1
  66. package/packages/ui/dist/assets/{mail-CACYeWXj.js → mail-CkgaIJAd.js} +1 -1
  67. package/packages/ui/dist/assets/{metamask-sdk-BUYu4RDE.js → metamask-sdk-O-IBvvGq.js} +1 -1
  68. package/packages/ui/dist/assets/{mobile-DX601q1y.js → mobile-CGc88WfG.js} +1 -1
  69. package/packages/ui/dist/assets/{more-CQGeX45N.js → more-DnX8wlTn.js} +1 -1
  70. package/packages/ui/dist/assets/{network-placeholder-BQw8E4X-.js → network-placeholder-DDrgA4a3.js} +1 -1
  71. package/packages/ui/dist/assets/{nftPlaceholder-CifJ2CzA.js → nftPlaceholder-DhHWPuD3.js} +1 -1
  72. package/packages/ui/dist/assets/{off-B6oUArCZ.js → off-D1CsYvPQ.js} +1 -1
  73. package/packages/ui/dist/assets/{parseSignature-DjWIdHkx.js → parseSignature-BlZUbtEc.js} +1 -1
  74. package/packages/ui/dist/assets/{play-store-Kq51oh3r.js → play-store-Dbkk8PTZ.js} +1 -1
  75. package/packages/ui/dist/assets/{plus-BxZxVpff.js → plus-B8jXpls3.js} +1 -1
  76. package/packages/ui/dist/assets/{qr-code-CSBFpyhP.js → qr-code-CDuJ3ftj.js} +1 -1
  77. package/packages/ui/dist/assets/{recycle-horizontal-D0HyLkot.js → recycle-horizontal-ZFGjaHsZ.js} +1 -1
  78. package/packages/ui/dist/assets/{refresh-BAQo228i.js → refresh-D0rMEDtF.js} +1 -1
  79. package/packages/ui/dist/assets/{reown-logo-DeUbwRp6.js → reown-logo-NlCNVmgd.js} +1 -1
  80. package/packages/ui/dist/assets/{search-C8Sd0Mpz.js → search-CrJAA2qW.js} +1 -1
  81. package/packages/ui/dist/assets/{secp256k1-BujG3JoP.js → secp256k1-mJj6W2AI.js} +1 -1
  82. package/packages/ui/dist/assets/{send-B0JwUp6Q.js → send-C7CoRziM.js} +1 -1
  83. package/packages/ui/dist/assets/{swapHorizontal-B2k5yDqc.js → swapHorizontal-fD3wbCGJ.js} +1 -1
  84. package/packages/ui/dist/assets/{swapHorizontalBold-yQqq0yPi.js → swapHorizontalBold-Cc-jQ6as.js} +1 -1
  85. package/packages/ui/dist/assets/{swapHorizontalMedium-NafYmdDj.js → swapHorizontalMedium-DlJW6uX1.js} +1 -1
  86. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-BUn0GwEK.js → swapHorizontalRoundedBold-1VHOerLO.js} +1 -1
  87. package/packages/ui/dist/assets/{swapVertical-Be1m6suD.js → swapVertical-CKaRlkZK.js} +1 -1
  88. package/packages/ui/dist/assets/{telegram-D-u7QHT5.js → telegram-DnCYed4D.js} +1 -1
  89. package/packages/ui/dist/assets/{three-dots-B6Y4DFOR.js → three-dots-BFluoxma.js} +1 -1
  90. package/packages/ui/dist/assets/{twitch-DSy1rhWQ.js → twitch-BXGv98S9.js} +1 -1
  91. package/packages/ui/dist/assets/{twitterIcon-tac1plSa.js → twitterIcon-C6IdXEe5.js} +1 -1
  92. package/packages/ui/dist/assets/{verify-6MylluBY.js → verify-D_QGyiLQ.js} +1 -1
  93. package/packages/ui/dist/assets/{verify-filled-CZc0otb8.js → verify-filled-DIW8QKL9.js} +1 -1
  94. package/packages/ui/dist/assets/{w3m-modal-DbT03Pyz.js → w3m-modal-Do9U160p.js} +1 -1
  95. package/packages/ui/dist/assets/{wallet-473-ObZE.js → wallet-CcARZnOx.js} +1 -1
  96. package/packages/ui/dist/assets/{wallet-placeholder-B9h3WvTk.js → wallet-placeholder-X1coFzQa.js} +1 -1
  97. package/packages/ui/dist/assets/{walletconnect-5GHIf5FR.js → walletconnect-Glte9ia7.js} +1 -1
  98. package/packages/ui/dist/assets/{warning-circle-8A009Dx3.js → warning-circle-j-3V4KTo.js} +1 -1
  99. package/packages/ui/dist/assets/{x-B4jK8e8X.js → x-Bcc52c_T.js} +1 -1
  100. package/packages/ui/dist/index.html +1 -1
  101. package/templates/default/AGENTS.md +3 -1
  102. package/packages/ui/dist/assets/cursor-DCRoxgSY.js +0 -3
@@ -35208,65 +35208,74 @@ var {
35208
35208
 
35209
35209
  // ../chains/dist/index.js
35210
35210
  var chains = {
35211
- // Base Sepolia (testnet)
35212
- 84532: {
35213
- chainId: 84532,
35214
- name: "Base Sepolia",
35215
- // SAIL-405 redeploy (2026-06-04, gitCommit 6d872e6) — adds owner-gated
35216
- // setManager(newManager) to rotate the delegated signer. Genesis allowlist
35217
- // bootstrap + createAccount fix carried over; allowlistBootstrapped=true,
35218
- // zero fees. Supersedes 0xcC50009115DAaBCB40513e03a1a0Cc2Fdf6Be558.
35219
- kernel: "0xf1D0F4C9893612627409948BAa9d82a01a373799",
35220
- mandateFactory: "0xdfF6a2272F667cDf78Af4681b9c88A219998db95",
35221
- governance: "0xEaD44bC6999E7b00b9b2E11c1660248DC2a30993",
35211
+ // Ethereum mainnet
35212
+ 1: {
35213
+ chainId: 1,
35214
+ name: "Ethereum",
35215
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
35216
+ kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
35217
+ mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
35218
+ governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
35222
35219
  dispatchModel: "selective",
35223
- // selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
35224
35220
  protocols: {}
35225
35221
  },
35226
35222
  // Base mainnet
35227
35223
  8453: {
35228
35224
  chainId: 8453,
35229
35225
  name: "Base",
35230
- // SAIL-405 redeploy (2026-06-04, gitCommit 0ed0561) — adds owner-gated
35231
- // setManager(newManager) to rotate the delegated signer. Genesis allowlist
35232
- // bootstrap carried over; allowlistBootstrapped=true, zero fees.
35233
- // Supersedes 0x20eff0DbE752e22655A6dAA5A94521FA06CDdE06.
35234
- kernel: "0x6319d3dfDDe3804ba93D65752b00c52bFb05a1ab",
35235
- mandateFactory: "0x7724EACd97C8601d5AC244Aadbf76ad87353Ff31",
35236
- governance: "0x7E897D919872b1587577617ffFC42113679d0C50",
35226
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
35227
+ // Supersedes 0x6319d3dfDDe3804ba93D65752b00c52bFb05a1ab (SAIL-405).
35228
+ kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
35229
+ mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
35230
+ governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
35237
35231
  dispatchModel: "selective",
35238
- // selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
35239
35232
  protocols: {}
35240
35233
  },
35241
35234
  // Arbitrum mainnet
35242
35235
  42161: {
35243
35236
  chainId: 42161,
35244
35237
  name: "Arbitrum",
35245
- // SAIL-405 redeploy (2026-06-04, gitCommit 0ed0561) — adds owner-gated
35246
- // setManager(newManager) to rotate the delegated signer. Genesis allowlist
35247
- // bootstrap carried over (bootstrap sent as a standalone tx post-core-deploy);
35248
- // allowlistBootstrapped=true, zero fees.
35249
- // Supersedes 0x9AF32E0C395fb31f5cA28994351F8fAE3003e125.
35250
- kernel: "0x2716B12832DED0EF5688519c5Fe069EFc0374E02",
35251
- mandateFactory: "0x23681A8A4C9819D8EaB37E46B858da6F3c85E683",
35252
- governance: "0xd6AbB7A1036ADc7958Abffec9Da03450c5a2Ec8e",
35238
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
35239
+ // Supersedes 0x2716B12832DED0EF5688519c5Fe069EFc0374E02 (SAIL-405).
35240
+ kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
35241
+ mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
35242
+ governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
35253
35243
  dispatchModel: "selective",
35254
- // selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
35255
35244
  protocols: {}
35256
35245
  },
35257
35246
  // Unichain mainnet
35258
35247
  130: {
35259
35248
  chainId: 130,
35260
35249
  name: "Unichain",
35261
- // SAIL-406 deploy (2026-06-05, gitCommit 2c9e325) — full protocol deploy:
35262
- // core + the complete template suite (7 shared + 12 standalone), all
35263
- // source-verified on uniscan.xyz. Genesis allowlist bootstrap
35264
- // (allowlistBootstrapped=true), zero fees, onboarding live.
35265
- kernel: "0xD985029960a9B7C2E7E38e102C448b8b8539B156",
35266
- mandateFactory: "0x8edDb62Aa49CeB837abf2653be2d93Ad9Fe6777D",
35267
- governance: "0xAb5C90ECfF2763f6f20f8E553E3b8778dD9C349A",
35250
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
35251
+ // Supersedes 0xD985029960a9B7C2E7E38e102C448b8b8539B156 (SAIL-406).
35252
+ kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
35253
+ mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
35254
+ governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
35255
+ dispatchModel: "selective",
35256
+ protocols: {}
35257
+ },
35258
+ // Base Sepolia (testnet)
35259
+ 84532: {
35260
+ chainId: 84532,
35261
+ name: "Base Sepolia",
35262
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
35263
+ // Supersedes 0xf1D0F4C9893612627409948BAa9d82a01a373799 (SAIL-405).
35264
+ kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
35265
+ mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
35266
+ governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
35267
+ dispatchModel: "selective",
35268
+ protocols: {}
35269
+ },
35270
+ // Eth Sepolia (testnet)
35271
+ 11155111: {
35272
+ chainId: 11155111,
35273
+ name: "Eth Sepolia",
35274
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
35275
+ kernel: "0x02ABC18B65A328de2e749F56ba79ACF2718a6659",
35276
+ mandateFactory: "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2",
35277
+ governance: "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC",
35268
35278
  dispatchModel: "selective",
35269
- // selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
35270
35279
  protocols: {}
35271
35280
  }
35272
35281
  };
@@ -35690,178 +35699,148 @@ async function detectKernelCapabilities(publicClient, kernel, opts) {
35690
35699
  }
35691
35700
 
35692
35701
  // ../sdk/dist/deployments.js
35702
+ var CREATE2_KERNEL = "0x02ABC18B65A328de2e749F56ba79ACF2718a6659";
35703
+ var CREATE2_GOVERNANCE = "0x7A478118715791728BDE3bc7A4D7ECfdEB89C6EC";
35704
+ var CREATE2_TIMELOCK = "0xE48Ba8DB6d748adafD13155c3590f62e58a77f56";
35705
+ var CREATE2_SAFE_MODULE_ENABLER = "0x7897Cb53a4be4a2eaAf46D60573C4Fd83b33fE1F";
35706
+ var CREATE2_MANDATE_FACTORY = "0x14EDd6c2a56EfC0d71E215ab13094B9AF90543d2";
35707
+ var CREATE2_STANDARD_FEE_POLICY = "0xe7B5901b839cFFDEd9D4108A22712C8BfdA1D80D";
35708
+ var CREATE2_TREASURY = "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6";
35693
35709
  var sailDeployments = {
35694
- 84532: {
35695
- // SAIL-405 redeploy (2026-06-04, gitCommit 6d872e6): adds owner-gated
35696
- // setManager(newManager) to rotate the SMA's delegated signer (clears the
35697
- // permission set + bumps the nonce epoch). Genesis allowlist bootstrap +
35698
- // local CREATE2 proxy prediction carried over; allowlistBootstrapped=true,
35699
- // createAccount verified working, zero fees, onboarding live. Supersedes
35700
- // 0xcC50009115DAaBCB40513e03a1a0Cc2Fdf6Be558. Only `core` was redeployed;
35701
- // shared/standalone permission templates are NOT yet deployed against this
35702
- // kernel (run the templates targets + refill the template maps before clones).
35703
- chainId: 84532,
35704
- blockNumber: 42400417,
35705
- deployer: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
35706
- governance: "0xEaD44bC6999E7b00b9b2E11c1660248DC2a30993",
35707
- timelock: "0x97B863e392C9859336788D5Ec454527d33C95B74",
35708
- kernel: "0xf1D0F4C9893612627409948BAa9d82a01a373799",
35709
- permissionFactory: "0xdfF6a2272F667cDf78Af4681b9c88A219998db95",
35710
- standardFeePolicy: "0x05570F7973b46Eb9Ed4518422891EFC26BD58b97",
35711
- safeModuleEnabler: "0xB2C2B52d94412e3472C9fb2B52186eA12a935869",
35712
- treasury: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
35710
+ // ── Ethereum mainnet ─────────────────────────────────────────────────────────
35711
+ 1: {
35712
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
35713
+ // allowlistBootstrapped=true (genesis bootstrap), zero fees, 48h timelock.
35714
+ chainId: 1,
35715
+ blockNumber: 25280925,
35716
+ deployer: CREATE2_TREASURY,
35717
+ governance: CREATE2_GOVERNANCE,
35718
+ timelock: CREATE2_TIMELOCK,
35719
+ kernel: CREATE2_KERNEL,
35720
+ mandateFactory: CREATE2_MANDATE_FACTORY,
35721
+ standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
35722
+ safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
35723
+ treasury: CREATE2_TREASURY,
35713
35724
  maxPermissionFeeWei: 1000000000000000n,
35714
35725
  initialBaseFee: 0n,
35715
35726
  initialComplexityRate: 0n,
35716
35727
  dispatchModel: "selective",
35717
- // selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
35718
35728
  knownTemplates: [],
35719
35729
  standaloneTemplates: {}
35720
35730
  },
35731
+ // ── Base mainnet ─────────────────────────────────────────────────────────────
35721
35732
  8453: {
35722
- // SAIL-405 redeploy (2026-06-04, gitCommit 0ed0561): adds owner-gated
35723
- // setManager(newManager) to rotate the SMA's delegated signer (clears the
35724
- // permission set + bumps the nonce epoch). Genesis allowlist bootstrap +
35725
- // local CREATE2 proxy prediction carried over; allowlistBootstrapped=true,
35726
- // zero fees, onboarding live. Supersedes 0x20eff0DbE752e22655A6dAA5A94521FA06CDdE06.
35727
- // Only `core` was redeployed; shared/standalone permission templates are NOT yet
35728
- // deployed against this kernel (run the templates targets + refill the maps first).
35733
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33). Supersedes
35734
+ // 0x6319d3dfDDe3804ba93D65752b00c52bFb05a1ab (SAIL-405 redeploy).
35735
+ // allowlistBootstrapped=true, zero fees, 48h timelock.
35729
35736
  chainId: 8453,
35730
- blockNumber: 46898030,
35731
- deployer: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
35732
- governance: "0x7E897D919872b1587577617ffFC42113679d0C50",
35733
- timelock: "0x8eC3Ca951E193C6E3713A70022454d7A1f083281",
35734
- kernel: "0x6319d3dfDDe3804ba93D65752b00c52bFb05a1ab",
35735
- permissionFactory: "0x7724EACd97C8601d5AC244Aadbf76ad87353Ff31",
35736
- standardFeePolicy: "0x65850a8D5050aeAade68289ff96c4F119a24B82e",
35737
- safeModuleEnabler: "0xC84EdE78f93291A1fab19F51c4c7e938AB302Edf",
35738
- treasury: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
35737
+ blockNumber: 47115338,
35738
+ deployer: CREATE2_TREASURY,
35739
+ governance: CREATE2_GOVERNANCE,
35740
+ timelock: CREATE2_TIMELOCK,
35741
+ kernel: CREATE2_KERNEL,
35742
+ mandateFactory: CREATE2_MANDATE_FACTORY,
35743
+ standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
35744
+ safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
35745
+ treasury: CREATE2_TREASURY,
35739
35746
  maxPermissionFeeWei: 1000000000000000n,
35740
35747
  initialBaseFee: 0n,
35741
35748
  initialComplexityRate: 0n,
35742
35749
  dispatchModel: "selective",
35743
- // selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
35744
35750
  knownTemplates: [],
35745
35751
  standaloneTemplates: {}
35746
35752
  },
35753
+ // ── Arbitrum mainnet ─────────────────────────────────────────────────────────
35747
35754
  42161: {
35748
- // SAIL-405 redeploy (2026-06-04, gitCommit 0ed0561): adds owner-gated
35749
- // setManager(newManager) to rotate the SMA's delegated signer (clears the
35750
- // permission set + bumps the nonce epoch). Genesis allowlist bootstrap +
35751
- // local CREATE2 proxy prediction carried over; allowlistBootstrapped=true,
35752
- // zero fees, onboarding live. Supersedes 0x9AF32E0C395fb31f5cA28994351F8fAE3003e125.
35753
- // Bootstrap was sent as a standalone tx post-core-deploy; identical end state to Base.
35754
- // Only `core` was redeployed; shared/standalone permission templates are NOT yet
35755
- // deployed against this kernel (run the templates targets + refill the maps first).
35755
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33). Supersedes
35756
+ // 0x2716B12832DED0EF5688519c5Fe069EFc0374E02 (SAIL-405 redeploy).
35757
+ // allowlistBootstrapped=true, zero fees, 48h timelock.
35756
35758
  chainId: 42161,
35757
- blockNumber: 25244824,
35758
- deployer: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
35759
- governance: "0xd6AbB7A1036ADc7958Abffec9Da03450c5a2Ec8e",
35760
- timelock: "0x114CB7110C780f7E3a6093AfE0B52463a569857C",
35761
- kernel: "0x2716B12832DED0EF5688519c5Fe069EFc0374E02",
35762
- permissionFactory: "0x23681A8A4C9819D8EaB37E46B858da6F3c85E683",
35763
- standardFeePolicy: "0xAdfB986D48480bC67a7cF3751d30599161632e0D",
35764
- safeModuleEnabler: "0xabe2a6D03F592BC602cA1dBDCD885ba2493274f9",
35765
- treasury: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
35759
+ blockNumber: 471736462,
35760
+ deployer: CREATE2_TREASURY,
35761
+ governance: CREATE2_GOVERNANCE,
35762
+ timelock: CREATE2_TIMELOCK,
35763
+ kernel: CREATE2_KERNEL,
35764
+ mandateFactory: CREATE2_MANDATE_FACTORY,
35765
+ standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
35766
+ safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
35767
+ treasury: CREATE2_TREASURY,
35766
35768
  maxPermissionFeeWei: 1000000000000000n,
35767
35769
  initialBaseFee: 0n,
35768
35770
  initialComplexityRate: 0n,
35769
35771
  dispatchModel: "selective",
35770
- // selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
35771
35772
  knownTemplates: [],
35772
35773
  standaloneTemplates: {}
35773
35774
  },
35775
+ // ── Unichain mainnet ─────────────────────────────────────────────────────────
35774
35776
  130: {
35775
- // SAIL-406 deploy (2026-06-05, gitCommit 2c9e325): full protocol deploy on
35776
- // Unichain mainnet — core + the complete template suite (7 shared + 12
35777
- // standalone), all source-verified on uniscan.xyz. Genesis allowlist
35778
- // bootstrap (allowlistBootstrapped=true: Safe v1.4.1 factory, both
35779
- // singletons, SafeModuleEnabler, StandardFeePolicy, SafeProxy codehash
35780
- // 0xd7d408eb…fb4c), zero fees, onboarding live without the 48h timelock.
35781
- // First chain to ship permission templates against the kernel.
35777
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33). Supersedes
35778
+ // 0xD985029960a9B7C2E7E38e102C448b8b8539B156 (SAIL-406 deploy).
35779
+ // NOTE: knownTemplates and standaloneTemplates from SAIL-406 were deployed
35780
+ // against the old kernel 0xD985029... and are now invalid. They must be
35781
+ // redeployed against the new kernel 0x02ABC1... and re-populated here.
35782
35782
  chainId: 130,
35783
- blockNumber: 49897206,
35784
- deployer: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
35785
- governance: "0xAb5C90ECfF2763f6f20f8E553E3b8778dD9C349A",
35786
- timelock: "0xd44FbBB37f01e235E0EE5386948F216d36D0CEf2",
35787
- kernel: "0xD985029960a9B7C2E7E38e102C448b8b8539B156",
35788
- permissionFactory: "0x8edDb62Aa49CeB837abf2653be2d93Ad9Fe6777D",
35789
- standardFeePolicy: "0x7bBA8BE3c01c972757aA4a230A00D58aB600A1F1",
35790
- safeModuleEnabler: "0xFE9227A9F2baf704060c604466df354a5A137b9B",
35791
- treasury: "0xB01dCE443d052e44b7D13726c0EC9fFB7f5815B6",
35783
+ blockNumber: 50271704,
35784
+ deployer: CREATE2_TREASURY,
35785
+ governance: CREATE2_GOVERNANCE,
35786
+ timelock: CREATE2_TIMELOCK,
35787
+ kernel: CREATE2_KERNEL,
35788
+ mandateFactory: CREATE2_MANDATE_FACTORY,
35789
+ standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
35790
+ safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
35791
+ treasury: CREATE2_TREASURY,
35792
35792
  maxPermissionFeeWei: 1000000000000000n,
35793
35793
  initialBaseFee: 0n,
35794
35794
  initialComplexityRate: 0n,
35795
35795
  dispatchModel: "selective",
35796
- // selective: verified on-chain DISPATCH_TYPEHASH 0xbe50c5391dcf9e08d11d2c30dbee822c14ad07af2ceb503c778d265801fb0e5c
35797
- knownTemplates: [
35798
- {
35799
- address: "0xbD624eC67e2685872A60c0aF8F020727e20D096e",
35800
- kind: "SharedAMMLiquidityPermission",
35801
- chainId: 130,
35802
- label: "Shared AMM Liquidity",
35803
- description: "Bounded AMM liquidity provision/removal \u2014 enforces allowed pools and bounds."
35804
- },
35805
- {
35806
- address: "0x9d386605518FA81ff536b351ff055d26203229A9",
35807
- kind: "SharedApproveAndCallBatchPermission",
35808
- chainId: 130,
35809
- label: "Shared Approve-and-Call Batch",
35810
- description: "Bounded approve-then-call batch \u2014 enforces token, spender, and selector allowlists."
35811
- },
35812
- {
35813
- address: "0x948a9F9a6f2828E50f7e71bd569ba75A69da2BEb",
35814
- kind: "SharedBoundedBorrowPermission",
35815
- chainId: 130,
35816
- label: "Shared Bounded Borrow",
35817
- description: "Bounded borrow \u2014 enforces allowed markets and max borrow size."
35818
- },
35819
- {
35820
- address: "0xfD19fad56Ca3d6FaCd4279a2F84f09bef8967f6a",
35821
- kind: "SharedBoundedSwapPermission",
35822
- chainId: 130,
35823
- label: "Shared Uniswap V3 Swap",
35824
- description: "Bounded swap via Uniswap V3 \u2014 enforces allowed tokens, max trade size, and slippage."
35825
- },
35826
- {
35827
- address: "0x900cd03ee15e629bC4e94F6344d5529F4862071c",
35828
- kind: "SharedDeFiBundlePermission",
35829
- chainId: 130,
35830
- label: "Shared DeFi Bundle",
35831
- description: "Bounded multi-step DeFi bundle within a single permission."
35832
- },
35833
- {
35834
- address: "0x1dF90a2484bCF3c6Da2FB035aa0C9f523e77Cd62",
35835
- kind: "SharedPendlePermission",
35836
- chainId: 130,
35837
- label: "Shared Pendle",
35838
- description: "Bounded Pendle interactions \u2014 enforces allowed markets and bounds."
35839
- },
35840
- {
35841
- address: "0x851Ad196b7DC6c05eaf0B9420f2a72dc336D7739",
35842
- kind: "SharedTransferTargetPermission",
35843
- chainId: 130,
35844
- label: "Shared Transfer Target",
35845
- description: "Allows transfers only to a pre-approved target address."
35846
- }
35847
- ],
35848
- standaloneTemplates: {
35849
- // EIP-1167 clone LOGIC addresses — the `impl` argument to
35850
- // PermissionFactory.deployAndAttach(account, impl, salt, initData). A clone
35851
- // is created and configured per account via its initialize(...).
35852
- azuroPrediction: "0xd48cdBB25bF0A214dEffECac3c9431650834b046",
35853
- boundedApprove: "0xbF7089A905081054c9dA628707f2e1EF70A7F300",
35854
- boundedBorrow: "0x17D466309C7E0237960f68126Cc4A109D194ac28",
35855
- boundedDeposit: "0xf49E304EDf806AF46E8f17740e56C1CBFad5d264",
35856
- boundedLiFi: "0x6a0171013FeD6B2Eda16A4dd4DB33Fa34b7F3e3f",
35857
- boundedSwap: "0x06696F9dd4bD0994f55b075600627Dc6E54635c9",
35858
- boundedWithdraw: "0xE207CfC8c2204b15ee5fD22B79472929706c7E4b",
35859
- gmxPerp: "0xB1bb967aC11D61C0599c8458D9B950461db5D4E9",
35860
- gainsNetworkPerp: "0x1297673f71A9be02bc876Dbd0ceaB3c96D268bE3",
35861
- limitlessPrediction: "0x2bE4280d8816626e1dea4E94A83d9334A971AF90",
35862
- synthetixPerp: "0x711a70B16D013a9B96Bd6733F4b3097e5787f860",
35863
- transferTarget: "0x8428155b6b9eea4E78b9a52c2312752eD04Baf16"
35864
- }
35796
+ // Templates cleared: the SAIL-406 shared + standalone templates were deployed
35797
+ // against the old kernel (0xD985029...) and are invalid against the new one.
35798
+ // Re-populate after redeploying templates against 0x02ABC1...
35799
+ knownTemplates: [],
35800
+ standaloneTemplates: {}
35801
+ },
35802
+ // ── Base Sepolia (testnet) ───────────────────────────────────────────────────
35803
+ 84532: {
35804
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33). Supersedes
35805
+ // 0xf1D0F4C9893612627409948BAa9d82a01a373799 (SAIL-405 redeploy).
35806
+ // allowlistBootstrapped=true, zero fees, 48h timelock.
35807
+ chainId: 84532,
35808
+ blockNumber: 42625843,
35809
+ deployer: CREATE2_TREASURY,
35810
+ governance: CREATE2_GOVERNANCE,
35811
+ timelock: CREATE2_TIMELOCK,
35812
+ kernel: CREATE2_KERNEL,
35813
+ mandateFactory: CREATE2_MANDATE_FACTORY,
35814
+ standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
35815
+ safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
35816
+ treasury: CREATE2_TREASURY,
35817
+ maxPermissionFeeWei: 1000000000000000n,
35818
+ initialBaseFee: 0n,
35819
+ initialComplexityRate: 0n,
35820
+ dispatchModel: "selective",
35821
+ knownTemplates: [],
35822
+ standaloneTemplates: {}
35823
+ },
35824
+ // ── Eth Sepolia (testnet) ────────────────────────────────────────────────────
35825
+ 11155111: {
35826
+ // CREATE2 deterministic deploy (2026-06-09, gitCommit 1199b33).
35827
+ // allowlistBootstrapped=true, zero fees, 48h timelock.
35828
+ chainId: 11155111,
35829
+ blockNumber: 11023571,
35830
+ deployer: CREATE2_TREASURY,
35831
+ governance: CREATE2_GOVERNANCE,
35832
+ timelock: CREATE2_TIMELOCK,
35833
+ kernel: CREATE2_KERNEL,
35834
+ mandateFactory: CREATE2_MANDATE_FACTORY,
35835
+ standardFeePolicy: CREATE2_STANDARD_FEE_POLICY,
35836
+ safeModuleEnabler: CREATE2_SAFE_MODULE_ENABLER,
35837
+ treasury: CREATE2_TREASURY,
35838
+ maxPermissionFeeWei: 1000000000000000n,
35839
+ initialBaseFee: 0n,
35840
+ initialComplexityRate: 0n,
35841
+ dispatchModel: "selective",
35842
+ knownTemplates: [],
35843
+ standaloneTemplates: {}
35865
35844
  }
35866
35845
  };
35867
35846
  function getSailDeployment(chainId) {
@@ -38141,6 +38120,68 @@ var baseSepoliaPreconf = /* @__PURE__ */ defineChain({
38141
38120
  }
38142
38121
  });
38143
38122
 
38123
+ // ../../node_modules/.pnpm/viem@2.51.3_bufferutil@4.1.0_typescript@5.9.3_utf-8-validate@5.0.10_zod@4.4.3/node_modules/viem/_esm/chains/definitions/mainnet.js
38124
+ init_defineChain();
38125
+ var mainnet = /* @__PURE__ */ defineChain({
38126
+ id: 1,
38127
+ name: "Ethereum",
38128
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
38129
+ blockTime: 12e3,
38130
+ rpcUrls: {
38131
+ default: {
38132
+ http: ["https://eth.merkle.io"]
38133
+ }
38134
+ },
38135
+ blockExplorers: {
38136
+ default: {
38137
+ name: "Etherscan",
38138
+ url: "https://etherscan.io",
38139
+ apiUrl: "https://api.etherscan.io/api"
38140
+ }
38141
+ },
38142
+ contracts: {
38143
+ ensUniversalResolver: {
38144
+ address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee",
38145
+ blockCreated: 23085558
38146
+ },
38147
+ multicall3: {
38148
+ address: "0xca11bde05977b3631167028862be2a173976ca11",
38149
+ blockCreated: 14353601
38150
+ }
38151
+ }
38152
+ });
38153
+
38154
+ // ../../node_modules/.pnpm/viem@2.51.3_bufferutil@4.1.0_typescript@5.9.3_utf-8-validate@5.0.10_zod@4.4.3/node_modules/viem/_esm/chains/definitions/sepolia.js
38155
+ init_defineChain();
38156
+ var sepolia = /* @__PURE__ */ defineChain({
38157
+ id: 11155111,
38158
+ name: "Sepolia",
38159
+ nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 },
38160
+ rpcUrls: {
38161
+ default: {
38162
+ http: ["https://11155111.rpc.thirdweb.com"]
38163
+ }
38164
+ },
38165
+ blockExplorers: {
38166
+ default: {
38167
+ name: "Etherscan",
38168
+ url: "https://sepolia.etherscan.io",
38169
+ apiUrl: "https://api-sepolia.etherscan.io/api"
38170
+ }
38171
+ },
38172
+ contracts: {
38173
+ multicall3: {
38174
+ address: "0xca11bde05977b3631167028862be2a173976ca11",
38175
+ blockCreated: 751532
38176
+ },
38177
+ ensUniversalResolver: {
38178
+ address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee",
38179
+ blockCreated: 8928790
38180
+ }
38181
+ },
38182
+ testnet: true
38183
+ });
38184
+
38144
38185
  // ../../node_modules/.pnpm/viem@2.51.3_bufferutil@4.1.0_typescript@5.9.3_utf-8-validate@5.0.10_zod@4.4.3/node_modules/viem/_esm/chains/definitions/unichain.js
38145
38186
  init_defineChain();
38146
38187
  var sourceId3 = 1;
@@ -38339,25 +38380,29 @@ async function promptHidden(question) {
38339
38380
 
38340
38381
  // src/lib/chain.ts
38341
38382
  var CHAINS = {
38383
+ 1: mainnet,
38342
38384
  8453: base,
38343
- 84532: baseSepolia,
38344
38385
  42161: arbitrum,
38345
- 130: unichain
38386
+ 130: unichain,
38387
+ 84532: baseSepolia,
38388
+ 11155111: sepolia
38346
38389
  };
38347
38390
  function getChainById(chainId) {
38348
38391
  const chain2 = CHAINS[chainId];
38349
38392
  if (!chain2) {
38350
38393
  throw new Error(
38351
- `Unsupported chainId: ${chainId}. Supported: 8453 (Base), 84532 (Base Sepolia), 42161 (Arbitrum), 130 (Unichain)`
38394
+ `Unsupported chainId: ${chainId}. Supported: 1 (Ethereum), 8453 (Base), 42161 (Arbitrum), 130 (Unichain), 84532 (Base Sepolia), 11155111 (Eth Sepolia)`
38352
38395
  );
38353
38396
  }
38354
38397
  return chain2;
38355
38398
  }
38356
38399
  var RPC_ENV_VARS = {
38400
+ 1: "ETH_MAINNET_RPC_URL",
38357
38401
  8453: "BASE_RPC_URL",
38358
- 84532: "BASE_SEPOLIA_RPC_URL",
38359
38402
  42161: "ARBITRUM_RPC_URL",
38360
- 130: "UNICHAIN_RPC_URL"
38403
+ 130: "UNICHAIN_RPC_URL",
38404
+ 84532: "BASE_SEPOLIA_RPC_URL",
38405
+ 11155111: "SEPOLIA_RPC_URL"
38361
38406
  };
38362
38407
  function getRpcUrl(chainId) {
38363
38408
  const env = parseEnvFile(sailPath(".env.local"));
@@ -38443,2054 +38488,2312 @@ async function loadAnySigner() {
38443
38488
  throw new Error('No signing key found.\nRun "sailor keys generate" first.');
38444
38489
  }
38445
38490
 
38446
- // src/lib/state.ts
38491
+ // src/lib/packagePaths.ts
38447
38492
  var import_node_fs3 = __toESM(require("node:fs"), 1);
38448
38493
  var import_node_path2 = __toESM(require("node:path"), 1);
38494
+ function cliDistDir() {
38495
+ try {
38496
+ return import_node_path2.default.dirname(import_node_fs3.default.realpathSync(process.argv[1]));
38497
+ } catch {
38498
+ return import_node_path2.default.dirname(import_node_path2.default.resolve(process.argv[1]));
38499
+ }
38500
+ }
38501
+ function packageRoot() {
38502
+ let dir = cliDistDir();
38503
+ let firstBinMatch = null;
38504
+ for (let depth = 0; depth < 6; depth++) {
38505
+ const pkgFile = import_node_path2.default.join(dir, "package.json");
38506
+ if (import_node_fs3.default.existsSync(pkgFile)) {
38507
+ try {
38508
+ const pkg = JSON.parse(import_node_fs3.default.readFileSync(pkgFile, "utf-8"));
38509
+ if (pkg.bin?.sailor) {
38510
+ if (firstBinMatch === null) firstBinMatch = dir;
38511
+ if (import_node_fs3.default.existsSync(import_node_path2.default.join(dir, "templates"))) return dir;
38512
+ }
38513
+ } catch {
38514
+ }
38515
+ }
38516
+ const parent = import_node_path2.default.dirname(dir);
38517
+ if (parent === dir) break;
38518
+ dir = parent;
38519
+ }
38520
+ return firstBinMatch ?? import_node_path2.default.resolve(cliDistDir(), "../../..");
38521
+ }
38522
+ function projectPort(projectRoot) {
38523
+ const hash3 = [...projectRoot].reduce((h, c) => (h << 5) - h + c.charCodeAt(0) >>> 0, 0);
38524
+ return 3333 + hash3 % 667;
38525
+ }
38526
+
38527
+ // src/lib/state.ts
38528
+ var import_node_fs4 = __toESM(require("node:fs"), 1);
38529
+ var import_node_path3 = __toESM(require("node:path"), 1);
38449
38530
  function upsertAccountInList(account2, name, baseSailDir = sailDir()) {
38450
- const accountsPath = import_node_path2.default.join(baseSailDir, "state", "accounts.json");
38531
+ const accountsPath = import_node_path3.default.join(baseSailDir, "state", "accounts.json");
38451
38532
  let accounts = [];
38452
38533
  try {
38453
- accounts = JSON.parse(import_node_fs3.default.readFileSync(accountsPath, "utf-8"));
38534
+ accounts = JSON.parse(import_node_fs4.default.readFileSync(accountsPath, "utf-8"));
38454
38535
  } catch {
38455
38536
  try {
38456
38537
  const prev = JSON.parse(
38457
- import_node_fs3.default.readFileSync(import_node_path2.default.join(baseSailDir, "account.json"), "utf-8")
38538
+ import_node_fs4.default.readFileSync(import_node_path3.default.join(baseSailDir, "account.json"), "utf-8")
38458
38539
  );
38459
38540
  if (prev?.safe) accounts.push({ ...prev, name: "SMA 1", addedAt: null });
38460
38541
  } catch {
38461
38542
  }
38462
38543
  }
38463
- if (!accounts.find((a) => a.safe.toLowerCase() === account2.safe.toLowerCase())) {
38544
+ const idx = accounts.findIndex((a) => a.safe.toLowerCase() === account2.safe.toLowerCase());
38545
+ if (idx === -1) {
38464
38546
  accounts.push({
38465
38547
  ...account2,
38466
38548
  name: name ?? `SMA ${accounts.length + 1}`,
38467
38549
  addedAt: nowIso()
38468
38550
  });
38551
+ } else {
38552
+ accounts[idx] = { ...accounts[idx], ...account2 };
38469
38553
  }
38470
- import_node_fs3.default.mkdirSync(import_node_path2.default.join(baseSailDir, "state"), { recursive: true });
38471
- import_node_fs3.default.writeFileSync(accountsPath, `${JSON.stringify(accounts, null, 2)}
38554
+ import_node_fs4.default.mkdirSync(import_node_path3.default.join(baseSailDir, "state"), { recursive: true });
38555
+ import_node_fs4.default.writeFileSync(accountsPath, `${JSON.stringify(accounts, null, 2)}
38472
38556
  `);
38473
38557
  }
38474
38558
 
38475
- // src/commands/account.ts
38476
- function resolveChain(chainId) {
38477
- try {
38478
- return getChain(chainId);
38479
- } catch {
38480
- throw new Error(
38481
- `Chain ${chainId} is not yet configured in @sail/chains.
38482
- The SailKernel and mandate-factory addresses for this chain are unknown,
38483
- so an account cannot be created yet. Add the chain to @sail/chains once
38484
- SailKernel is deployed there.`
38485
- );
38559
+ // src/signing/client.ts
38560
+ var import_node_fs6 = require("node:fs");
38561
+ var import_node_path5 = require("node:path");
38562
+
38563
+ // src/signing/server.ts
38564
+ var import_node_crypto2 = require("node:crypto");
38565
+ var import_node_fs5 = require("node:fs");
38566
+ var import_node_http = require("node:http");
38567
+ var import_node_net = require("node:net");
38568
+ var import_node_path4 = require("node:path");
38569
+
38570
+ // ../../node_modules/.pnpm/ws@8.21.0_bufferutil@4.1.0_utf-8-validate@5.0.10/node_modules/ws/wrapper.mjs
38571
+ var import_stream2 = __toESM(require_stream2(), 1);
38572
+ var import_extension2 = __toESM(require_extension2(), 1);
38573
+ var import_permessage_deflate2 = __toESM(require_permessage_deflate2(), 1);
38574
+ var import_receiver2 = __toESM(require_receiver2(), 1);
38575
+ var import_sender2 = __toESM(require_sender2(), 1);
38576
+ var import_subprotocol2 = __toESM(require_subprotocol2(), 1);
38577
+ var import_websocket2 = __toESM(require_websocket2(), 1);
38578
+ var import_websocket_server2 = __toESM(require_websocket_server2(), 1);
38579
+
38580
+ // src/signing/server.ts
38581
+ var DEFAULT_SIGNING_PORT = 3141;
38582
+ var RUNTIME_SUBDIR = (0, import_node_path4.join)(".sail", "runtime");
38583
+ var SERVER_STATE_FILE = "server.json";
38584
+ var REQUEST_SECRET_HEADER = "x-sailor-secret";
38585
+ var MIME = {
38586
+ ".html": "text/html; charset=utf-8",
38587
+ ".js": "application/javascript",
38588
+ ".mjs": "application/javascript",
38589
+ ".css": "text/css",
38590
+ ".svg": "image/svg+xml",
38591
+ ".png": "image/png",
38592
+ ".ico": "image/x-icon",
38593
+ ".json": "application/json",
38594
+ ".woff": "font/woff",
38595
+ ".woff2": "font/woff2"
38596
+ };
38597
+ function findUiDist() {
38598
+ const candidates = [
38599
+ // Installed package (any scope): walk up to package root via bin.sailor marker
38600
+ (0, import_node_path4.join)(packageRoot(), "packages", "ui", "dist"),
38601
+ // Monorepo dev via tsx run from the repo root
38602
+ (0, import_node_path4.join)(process.cwd(), "packages", "ui", "dist"),
38603
+ (0, import_node_path4.join)(process.cwd(), "..", "ui", "dist")
38604
+ ];
38605
+ for (const c of candidates) {
38606
+ if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(c, "index.html"))) return c;
38486
38607
  }
38608
+ return null;
38487
38609
  }
38488
- async function accountCreate() {
38489
- if (!keyExists("manager")) {
38490
- throw new Error(
38491
- 'No agent wallet found.\nRun "sailor keys generate" and choose "agent wallet" first.'
38492
- );
38610
+ var RESULT_LONGPOLL_MS = 25e3;
38611
+ var SigningServer = class {
38612
+ /** This channel owns the server in-process (see SigningChannel). */
38613
+ remote = false;
38614
+ projectRoot;
38615
+ runtimeDir;
38616
+ port;
38617
+ _url = "";
38618
+ pending = /* @__PURE__ */ new Map();
38619
+ results = /* @__PURE__ */ new Map();
38620
+ resultWaiters = /* @__PURE__ */ new Map();
38621
+ clients = /* @__PURE__ */ new Set();
38622
+ httpServer = null;
38623
+ wss = null;
38624
+ _connectedWallet;
38625
+ walletListeners = [];
38626
+ uiDist;
38627
+ /**
38628
+ * Whether to publish .sail/runtime/server.json (the daemon-discovery hint).
38629
+ * The persistent daemon (`sailor station start`) advertises; ephemeral
38630
+ * per-command servers do not, so they never clobber a running daemon's state
38631
+ * on a discovery race. The browser UI finds servers by port-probing anyway.
38632
+ */
38633
+ advertise;
38634
+ /** Random secret generated at startup. Required on POST /requests to prevent
38635
+ * cross-origin pages from injecting signing requests. */
38636
+ requestSecret = "";
38637
+ constructor(opts = {}) {
38638
+ this.projectRoot = opts.projectRoot ?? process.cwd();
38639
+ this.runtimeDir = (0, import_node_path4.join)(this.projectRoot, RUNTIME_SUBDIR);
38640
+ this.port = opts.port ?? DEFAULT_SIGNING_PORT;
38641
+ this.uiDist = opts.uiDist ?? findUiDist();
38642
+ this.advertise = opts.advertise ?? true;
38493
38643
  }
38494
- const env = parseEnvFile(sailPath(".env.local"));
38495
- const rpcUrl = env["RPC_URL"] ?? process.env["RPC_URL"];
38496
- const chainIdRaw = env["CHAIN_ID"] ?? process.env["CHAIN_ID"];
38497
- if (!rpcUrl || !chainIdRaw) {
38498
- throw new Error(
38499
- "RPC_URL and CHAIN_ID must be set in .sail/.env.local.\nCreate that file with, for example:\n RPC_URL=https://your-rpc-endpoint\n CHAIN_ID=8453"
38500
- );
38644
+ get url() {
38645
+ return this._url;
38501
38646
  }
38502
- const chainId = Number(chainIdRaw);
38503
- if (Number.isNaN(chainId)) {
38504
- throw new Error(`Invalid CHAIN_ID: "${chainIdRaw}" \u2014 must be a number.`);
38647
+ get wsUrl() {
38648
+ return this._url.replace("http://", "ws://");
38505
38649
  }
38506
- const chain2 = resolveChain(chainId);
38507
- console.log(`Chain ${chainId} (${chain2.name})`);
38508
- console.log(` SailKernel: ${checksum4(chain2.kernel)}`);
38509
- console.log(` Mandate factory: ${checksum4(chain2.mandateFactory)}
38510
- `);
38511
- const manager = await loadKeyring("manager");
38512
- const managerAddr = checksum4(manager.address);
38513
- const safeFactory = await promptAddress("Safe factory address");
38514
- const safeSingleton = await promptAddress("Safe singleton address");
38515
- const owner2 = await promptAddress("Owner (EOA) address", managerAddr);
38516
- const permissionSigner = await promptAddress("Mandate signer address", managerAddr);
38517
- const feePolicy = await prompt("Fee policy", "none");
38518
- console.log("\nCreating SMA with:");
38519
- console.log(` Owner: ${owner2}`);
38520
- console.log(` Agent wallet: ${managerAddr}`);
38521
- console.log(` Mandate signer: ${permissionSigner}`);
38522
- console.log(` Safe factory: ${safeFactory}`);
38523
- console.log(` Safe singleton: ${safeSingleton}`);
38524
- console.log(` Fee policy: ${feePolicy}`);
38525
- const client = makeClient(chainId);
38526
- try {
38527
- const account2 = await client.account.create({
38528
- owner: owner2,
38529
- permissionSigner,
38530
- manager: managerAddr,
38531
- chainId
38532
- });
38533
- const stored = {
38534
- safe: checksum4(account2.safe),
38535
- owner: checksum4(account2.owner),
38536
- permissionSigner: checksum4(account2.permissionSigner),
38537
- manager: checksum4(account2.manager),
38538
- chainId: account2.chainId,
38539
- createdAtBlock: account2.createdAtBlock.toString()
38540
- };
38541
- upsertAccountInList(stored);
38542
- writeJsonFile(sailPath("account.json"), stored);
38543
- console.log(`
38544
- SMA created. Address: ${stored.safe}`);
38545
- console.log("Saved to .sail/account.json");
38546
- } catch (err) {
38547
- if (err.message === "not implemented") {
38548
- console.log(
38549
- "\nOn-chain account creation is not wired up in this build yet \u2014\nclient.account.create is a stub until SailKernel is deployed and the\nSDK is connected. Nothing was created on-chain."
38550
- );
38551
- return;
38552
- }
38553
- throw err;
38650
+ get isRunning() {
38651
+ return this.httpServer?.listening ?? false;
38554
38652
  }
38555
- }
38556
- var SAIL_MAINNET_CHAINS = [8453, 42161, 130];
38557
- async function fetchProxyCreationCode(preferredChainId) {
38558
- const rpcUrl = getRpcUrl(preferredChainId) ?? void 0;
38559
- const publicClient = createPublicClient({
38560
- chain: getChainById(preferredChainId),
38561
- transport: http(rpcUrl)
38562
- });
38563
- return await publicClient.readContract({
38564
- address: SAFE_V141.proxyFactory,
38565
- abi: safeProxyFactoryAbi,
38566
- functionName: "proxyCreationCode"
38567
- });
38568
- }
38569
- async function accountPredict(options) {
38570
- const stored = readJsonFile(sailPath("account.json"));
38571
- let ownerAddr;
38572
- if (options.owner) {
38573
- if (!isAddress(options.owner, { strict: false })) {
38574
- throw new Error(`Invalid --owner address: ${options.owner}`);
38575
- }
38576
- ownerAddr = getAddress(options.owner);
38577
- } else {
38578
- if (!stored?.owner) {
38579
- throw new Error(
38580
- "No owner found in .sail/account.json. Pass --owner <address> or run sailor onboard first."
38653
+ async start() {
38654
+ this.port = await findAvailablePort(this.port);
38655
+ this._url = `http://localhost:${this.port}`;
38656
+ this.requestSecret = (0, import_node_crypto2.randomBytes)(16).toString("hex");
38657
+ const http2 = (0, import_node_http.createServer)((req, res) => this.handleHttp(req, res));
38658
+ this.wss = new import_websocket_server2.default({ server: http2 });
38659
+ this.wss.on("connection", (ws, req) => {
38660
+ const params = new URL(req.url ?? "/", this._url).searchParams;
38661
+ if (params.get("secret") !== this.requestSecret) {
38662
+ ws.close(1008, "Unauthorized");
38663
+ return;
38664
+ }
38665
+ this.handleConnection(ws);
38666
+ });
38667
+ await new Promise((res, rej) => {
38668
+ http2.listen(this.port, "127.0.0.1", res);
38669
+ http2.once("error", rej);
38670
+ });
38671
+ this.httpServer = http2;
38672
+ if (this.advertise) this.writeRuntimeState();
38673
+ process.once("SIGINT", () => this.stop());
38674
+ process.once("SIGTERM", () => this.stop());
38675
+ }
38676
+ stop() {
38677
+ for (const [id, entry] of this.pending) {
38678
+ clearTimeout(entry.timer);
38679
+ this.recordResult(
38680
+ { status: "rejected", requestId: id, reason: "Signing server stopped" },
38681
+ entry.request
38581
38682
  );
38582
38683
  }
38583
- ownerAddr = getAddress(stored.owner);
38584
- }
38585
- let managerAddr;
38586
- if (options.manager) {
38587
- if (!isAddress(options.manager, { strict: false })) {
38588
- throw new Error(`Invalid --manager address: ${options.manager}`);
38684
+ this.pending.clear();
38685
+ for (const ws of this.clients) {
38686
+ try {
38687
+ ws.close();
38688
+ } catch {
38689
+ }
38589
38690
  }
38590
- managerAddr = getAddress(options.manager);
38591
- } else if (stored?.manager) {
38592
- managerAddr = getAddress(stored.manager);
38593
- } else {
38594
- throw new Error(
38595
- "The predicted address depends on the agent (manager) wallet, which is mixed into the kernel's CREATE2 salt.\nPass --manager <agent address> (create one first with `sailor keys`), or run after onboarding so it can be read from .sail/account.json."
38596
- );
38691
+ this.clients.clear();
38692
+ this.wss?.close();
38693
+ this.httpServer?.close();
38694
+ this.httpServer = null;
38695
+ if (this.advertise) this.removeRuntimeState();
38597
38696
  }
38598
- const saltNonce = options.salt != null ? BigInt(options.salt) : 0n;
38599
- let chainIds;
38600
- if (options.chain) {
38601
- const chainId = Number(options.chain);
38602
- if (!(chainId in sailDeployments)) {
38603
- throw new Error(
38604
- `Chain ${chainId} has no Sail Protocol deployment. Supported: ${Object.keys(sailDeployments).join(", ")}`
38605
- );
38606
- }
38607
- chainIds = [chainId];
38608
- } else {
38609
- chainIds = SAIL_MAINNET_CHAINS;
38697
+ get connectedWallet() {
38698
+ return this._connectedWallet;
38610
38699
  }
38611
- const firstChain = chainIds[0];
38612
- const proxyCreationCode = await fetchProxyCreationCode(firstChain);
38613
- const results = chainIds.map((chainId) => {
38614
- const deployment = sailDeployments[chainId];
38615
- const viemChain = getChainById(chainId);
38616
- const initializer = buildSafeSetupInitializer({
38617
- owners: [ownerAddr],
38618
- threshold: 1n,
38619
- kernel: deployment.kernel,
38620
- safeModuleEnabler: deployment.safeModuleEnabler
38621
- });
38622
- const predictedAddress = computeSailSmaAddress({
38623
- initializer,
38624
- saltNonce,
38625
- deployer: ownerAddr,
38626
- permissionSigner: ownerAddr,
38627
- manager: managerAddr,
38628
- feePolicy: deployment.standardFeePolicy,
38629
- proxyCreationCode
38700
+ /**
38701
+ * Resolves as soon as a wallet connects (or immediately if one already is).
38702
+ * The CLI calls this before building calldata that needs the owner's address.
38703
+ */
38704
+ waitForWallet(timeoutMs = 5 * 60 * 1e3) {
38705
+ if (this._connectedWallet) return Promise.resolve(this._connectedWallet);
38706
+ return new Promise((res, rej) => {
38707
+ const timer = setTimeout(
38708
+ () => rej(new Error("Timed out waiting for wallet connection in the signing UI")),
38709
+ timeoutMs
38710
+ );
38711
+ this.walletListeners.push((addr) => {
38712
+ clearTimeout(timer);
38713
+ res(addr);
38714
+ });
38630
38715
  });
38631
- return {
38632
- chainId,
38633
- name: viemChain.name,
38634
- predictedAddress,
38635
- kernel: deployment.kernel,
38636
- safeModuleEnabler: deployment.safeModuleEnabler
38637
- };
38638
- });
38639
- const uniqueAddresses = new Set(results.map((r) => r.predictedAddress.toLowerCase()));
38640
- const allSame = uniqueAddresses.size === 1;
38641
- if (options.json) {
38642
- console.log(
38643
- JSON.stringify(
38644
- {
38645
- salt: saltNonce.toString(),
38646
- owner: ownerAddr,
38647
- manager: managerAddr,
38648
- chains: results.map(({ chainId, name, predictedAddress }) => ({
38649
- chainId,
38650
- name,
38651
- predictedAddress
38652
- })),
38653
- allSame,
38654
- note: allSame ? "All chains produce the same address with this salt, owner, and manager." : "Addresses differ per chain because the kernel salt binds the chain-specific fee policy and the Safe initializer encodes chain-specific contract addresses (kernel, safeModuleEnabler). Cross-chain same-address requires deterministic protocol deployment or a registerExisting() flow."
38655
- },
38656
- null,
38657
- 2
38658
- )
38659
- );
38660
- return;
38661
- }
38662
- console.log("\nPredicted Safe addresses");
38663
- console.log(` Owner: ${ownerAddr}`);
38664
- console.log(` Manager: ${managerAddr}`);
38665
- console.log(` Salt: ${saltNonce}`);
38666
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
38667
- for (const { chainId, name, predictedAddress } of results) {
38668
- console.log(` ${name.padEnd(14)} (${String(chainId).padEnd(5)}): ${predictedAddress}`);
38669
38716
  }
38670
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
38671
- if (allSame) {
38672
- console.log("\u2713 All chains produce the same address.");
38673
- } else {
38674
- console.log("\n\u26A0 Addresses differ across chains.");
38675
- console.log(
38676
- " Root cause: SailKernel.createAccount binds the CREATE2 salt as\n keccak256(saltNonce, deployer, permissionSigner, manager, feePolicy),\n then SafeProxyFactory derives the address from that bound salt and the\n Safe initializer. Both the fee policy and the initializer (kernel,\n safeModuleEnabler) are chain-specific, so each chain yields a different\n address even with the same owner, manager, and salt.\n\n For cross-chain same-address the Sail Protocol needs one of:\n A) Deterministic (CREATE2) deployment of kernel + safeModuleEnabler +\n fee policy so they land at the same address on every chain.\n B) A registerExisting() path allowing a plain Safe (deployed with a\n chain-agnostic initializer) to be registered with the kernel."
38677
- );
38717
+ /**
38718
+ * Enqueue a signing request and broadcast it to the UI. Returns the full
38719
+ * request (with generated id). Used by both the in-process path and the HTTP
38720
+ * control plane (POST /requests).
38721
+ */
38722
+ enqueue(req, timeoutMs = 10 * 60 * 1e3) {
38723
+ const id = `req_${Date.now()}_${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
38724
+ const request = { ...req, id, createdAt: Date.now() };
38725
+ const timer = setTimeout(() => {
38726
+ if (this.pending.has(id)) {
38727
+ this.pending.delete(id);
38728
+ this.recordResult(
38729
+ {
38730
+ status: "rejected",
38731
+ requestId: id,
38732
+ reason: `timed out after ${timeoutMs / 1e3}s`
38733
+ },
38734
+ request
38735
+ );
38736
+ }
38737
+ }, timeoutMs);
38738
+ this.pending.set(id, { request, timer });
38739
+ this.broadcast({ type: "request", request });
38740
+ return request;
38678
38741
  }
38679
- console.log(`
38680
- To deploy on this chain: sailor onboard --new-sma --salt ${saltNonce}`);
38681
- }
38682
-
38683
- // src/lib/output.ts
38684
- function emit(json, human, payload) {
38685
- if (json) {
38686
- console.log(JSON.stringify(payload));
38687
- } else {
38688
- human();
38742
+ /**
38743
+ * Resolve once a result for `id` is available (immediately if already
38744
+ * resolved). Resolves to `null` if `timeoutMs` elapses first.
38745
+ */
38746
+ waitForResult(id, timeoutMs) {
38747
+ const existing = this.results.get(id);
38748
+ if (existing) return Promise.resolve(existing);
38749
+ return new Promise((res) => {
38750
+ const timer = setTimeout(() => {
38751
+ this.resultWaiters.get(id)?.delete(waiter);
38752
+ res(null);
38753
+ }, timeoutMs);
38754
+ const waiter = (r) => {
38755
+ clearTimeout(timer);
38756
+ res(r);
38757
+ };
38758
+ if (!this.resultWaiters.has(id)) this.resultWaiters.set(id, /* @__PURE__ */ new Set());
38759
+ this.resultWaiters.get(id)?.add(waiter);
38760
+ });
38689
38761
  }
38690
- }
38691
-
38692
- // src/lib/project.ts
38693
- init_esm2();
38694
- function nonEmpty(value) {
38695
- return typeof value === "string" && value.trim().length > 0;
38696
- }
38697
- var ProjectContext = class {
38698
- config;
38699
- chainId;
38700
- deployment;
38701
- contracts;
38702
- constructor() {
38703
- const cfg = readJsonFile(sailPath("config.json"));
38704
- if (!cfg) {
38705
- throw new Error('No Sailor project found here. Run "sailor init" first.');
38762
+ /** Push a signing request to the UI and await the user's response (in-process). */
38763
+ async requestSignature(req, timeoutMs = 10 * 60 * 1e3) {
38764
+ const request = this.enqueue(req, timeoutMs);
38765
+ const result = await this.waitForResult(request.id, timeoutMs + 2e3);
38766
+ if (!result) {
38767
+ throw new Error(`Signing request "${request.title}" timed out after ${timeoutMs / 1e3}s`);
38706
38768
  }
38707
- this.config = cfg;
38708
- this.chainId = cfg.chainId ?? 8453;
38709
- this.deployment = getSailDeployment(this.chainId);
38710
- const overrides = cfg.contracts ?? {};
38711
- this.contracts = {
38712
- chainId: this.chainId,
38713
- kernel: getAddress(nonEmpty(overrides.kernel) ? overrides.kernel : this.deployment.kernel),
38714
- governance: getAddress(
38715
- nonEmpty(overrides.governance) ? overrides.governance : this.deployment.governance
38716
- ),
38717
- standardFeePolicy: getAddress(
38718
- nonEmpty(overrides.standardFeePolicy) ? overrides.standardFeePolicy : this.deployment.standardFeePolicy
38719
- ),
38720
- safeModuleEnabler: getAddress(
38721
- nonEmpty(overrides.safeModuleEnabler) ? overrides.safeModuleEnabler : this.deployment.safeModuleEnabler
38722
- ),
38723
- permissionFactory: getAddress(
38724
- nonEmpty(overrides.permissionFactory) ? overrides.permissionFactory : this.deployment.permissionFactory
38725
- )
38726
- };
38727
- }
38728
- static exists() {
38729
- return fileExists(sailPath("config.json"));
38730
- }
38731
- get name() {
38732
- return this.config.name ?? "sailor-agent";
38733
- }
38734
- // ── Owner persistence (.sail/state/owner.json) ──────────────────────────────
38735
- getOwner() {
38736
- const state = readJsonFile(sailPath("state", "owner.json"));
38737
- if (state?.owner) return getAddress(state.owner);
38738
- const account2 = readJsonFile(sailPath("account.json"));
38739
- return account2?.owner ? getAddress(account2.owner) : null;
38769
+ return result;
38740
38770
  }
38741
- setOwner(owner2) {
38742
- writeJsonFile(sailPath("state", "owner.json"), {
38743
- owner: getAddress(owner2),
38744
- chainId: this.chainId,
38745
- connectedAt: (/* @__PURE__ */ new Date()).toISOString()
38746
- });
38771
+ recordResult(response, request) {
38772
+ const id = response.requestId;
38773
+ if (this.results.has(id)) return;
38774
+ this.results.set(id, response);
38775
+ const waiters2 = this.resultWaiters.get(id);
38776
+ if (waiters2) {
38777
+ for (const w of waiters2) w(response);
38778
+ this.resultWaiters.delete(id);
38779
+ }
38780
+ this.broadcast({ type: "request-resolved", requestId: id });
38781
+ setTimeout(() => this.results.delete(id), 10 * 60 * 1e3).unref?.();
38782
+ this.logOwnerActivity(response, request);
38747
38783
  }
38748
- };
38749
- async function loadManagerSigner2() {
38750
- if (!process.env.SAIL_PASSPHRASE) {
38784
+ /**
38785
+ * Append the owner's signing decision to the unified activity log. This is
38786
+ * the single place every owner action lands — whether the request was
38787
+ * approved (a signed tx or an off-chain EIP-712 signature) or rejected — so
38788
+ * the dashboard's Recent Activity can show what the owner did, alongside the
38789
+ * agent's dispatches. We only log when the originating request is known
38790
+ * (its `kind`/`title` give the event meaning); a bare result with no request
38791
+ * carries nothing worth showing.
38792
+ */
38793
+ logOwnerActivity(response, request) {
38794
+ if (!request) return;
38795
+ const base2 = {
38796
+ ts: nowIso(),
38797
+ actor: "owner",
38798
+ kind: request.kind,
38799
+ title: request.title,
38800
+ chainId: request.chainId
38801
+ };
38802
+ let event;
38803
+ if (response.status === "signed") {
38804
+ event = { ...base2, type: "owner_signed", txHash: response.txHash };
38805
+ } else if (response.status === "signature") {
38806
+ event = { ...base2, type: "owner_signed", offchain: true };
38807
+ } else {
38808
+ event = { ...base2, type: "owner_rejected", reason: response.reason };
38809
+ }
38751
38810
  try {
38752
- const env = parseEnvFile(sailPath(".env.local"));
38753
- if (env.SAIL_PASSPHRASE) process.env.SAIL_PASSPHRASE = env.SAIL_PASSPHRASE;
38811
+ appendActivity(event, (0, import_node_path4.join)(this.projectRoot, ".sail"));
38754
38812
  } catch {
38755
38813
  }
38756
38814
  }
38757
- const passphrase = process.env.SAIL_PASSPHRASE;
38758
- if (passphrase) {
38759
- const keystore = readJsonFile(keyPath("manager"));
38760
- if (!keystore) {
38761
- throw new Error('No manager key found.\nRun "sailor keys generate" and choose "manager".');
38762
- }
38763
- return LocalKeyring.fromKeystore(keystore, passphrase);
38815
+ /** Path to `<projectRoot>/.sail/<...segments>`. */
38816
+ sailFile(...segments) {
38817
+ return (0, import_node_path4.join)(this.projectRoot, ".sail", ...segments);
38764
38818
  }
38765
- return loadKeyring("manager");
38766
- }
38767
-
38768
- // src/commands/capabilities.ts
38769
- function chainName(chainId) {
38770
- try {
38771
- return getChainById(chainId).name;
38772
- } catch {
38773
- return `Chain ${chainId}`;
38819
+ /** Stream a JSON file back, or a fallback body when it is missing/invalid. */
38820
+ sendJsonFile(res, filePath, fallback2) {
38821
+ try {
38822
+ const raw = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
38823
+ JSON.parse(raw);
38824
+ res.writeHead(200, { "Content-Type": "application/json" });
38825
+ res.end(raw);
38826
+ } catch {
38827
+ res.writeHead(fallback2.status, { "Content-Type": "application/json" });
38828
+ res.end(JSON.stringify(fallback2.body));
38829
+ }
38774
38830
  }
38775
- }
38776
- async function capabilities(options = {}) {
38777
- const project = new ProjectContext();
38778
- const chainId = project.chainId;
38779
- const kernel = project.contracts.kernel;
38780
- const deployment = getSailDeployment(chainId);
38781
- const rpcUrl = getRpcUrl(chainId) ?? getChainById(chainId).rpcUrls.default.http[0];
38782
- let dispatchModel = deployment.dispatchModel;
38783
- let modelSource = "static-hint";
38784
- try {
38785
- const caps = await new SailorClient({ chainId, rpcUrl, kernel }).capabilities();
38786
- dispatchModel = caps.dispatchModel;
38787
- modelSource = caps.source;
38788
- } catch {
38831
+ /**
38832
+ * Persist a Safe deployed/imported from the dashboard. Mirrors the UI data
38833
+ * server's `POST /api/account` (packages/ui/server.js): upsert the SMA into
38834
+ * `state/accounts.json` (so the account switcher and the agent see it) BEFORE
38835
+ * overwriting `account.json` with the new active SMA — the upsert backfills
38836
+ * from the previously-active account.json, so writing it first would drop the
38837
+ * prior SMA.
38838
+ */
38839
+ handleSaveAccount(req, res) {
38840
+ this.readBody(req).then((body) => {
38841
+ const parsed = body ? JSON.parse(body) : {};
38842
+ const { safe, owner: owner2, permissionSigner, manager, chainId, createdAtBlock } = parsed;
38843
+ if (!safe || !owner2 || !chainId) {
38844
+ res.writeHead(400, { "Content-Type": "application/json" });
38845
+ res.end(JSON.stringify({ error: "safe, owner, and chainId are required" }));
38846
+ return;
38847
+ }
38848
+ const record = {
38849
+ safe,
38850
+ owner: owner2,
38851
+ permissionSigner: permissionSigner ?? owner2,
38852
+ manager: manager ?? owner2,
38853
+ chainId,
38854
+ createdAtBlock: createdAtBlock ?? "0"
38855
+ };
38856
+ const baseSailDir = this.sailFile();
38857
+ upsertAccountInList(record, void 0, baseSailDir);
38858
+ (0, import_node_fs5.mkdirSync)(baseSailDir, { recursive: true });
38859
+ (0, import_node_fs5.writeFileSync)(this.sailFile("account.json"), `${JSON.stringify(record, null, 2)}
38860
+ `);
38861
+ res.writeHead(200, { "Content-Type": "application/json" });
38862
+ res.end(JSON.stringify({ ok: true }));
38863
+ }).catch((err) => {
38864
+ res.writeHead(500, { "Content-Type": "application/json" });
38865
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
38866
+ });
38789
38867
  }
38790
- const cloneTemplates = (deployment.cloneTemplates ?? []).map((t) => ({
38791
- key: t.key,
38792
- kind: t.kind,
38793
- label: t.label,
38794
- description: t.description,
38795
- address: t.address,
38796
- initParams: t.initParams
38797
- }));
38798
- const knownTemplates = (deployment.knownTemplates ?? []).map((t) => ({
38799
- kind: t.kind,
38800
- label: t.label,
38801
- description: t.description,
38802
- address: t.address
38803
- }));
38804
- const bareTemplates = Object.keys(deployment.standaloneTemplates ?? {}).filter(
38805
- (k) => !cloneTemplates.some((c) => c.key === k)
38806
- );
38807
- const strategyPrimitives = [
38808
- "strategy.swap \u2014 bounded swap (one-off, or looped on a schedule for DCA/rebalance)",
38809
- "dispatch.single \u2014 a single permitted call through the kernel",
38810
- dispatchModel === "selective" ? "dispatch.batch / dispatch.preview \u2014 multi-call (selective kernels only)" : "dispatch.batch / dispatch.preview \u2014 UNAVAILABLE on this conjunctive kernel"
38811
- ];
38812
- const payload = {
38813
- chainId,
38814
- chainName: chainName(chainId),
38815
- supported: true,
38816
- dispatchModel,
38817
- dispatchModelSource: modelSource,
38818
- contracts: {
38819
- kernel,
38820
- permissionFactory: project.contracts.permissionFactory
38821
- },
38822
- supportedChains: Object.keys(sailDeployments).map((id) => ({
38823
- chainId: Number(id),
38824
- name: chainName(Number(id)),
38825
- dispatchModel: sailDeployments[id].dispatchModel
38826
- })),
38827
- mandateTemplates: {
38828
- // No-Solidity, self-describing clone templates (deployAndAttach + initialize).
38829
- cloneTemplates,
38830
- // Pre-deployed shared permissions.
38831
- knownTemplates,
38832
- // Deployable clone logic without rich wizard metadata yet.
38833
- otherStandaloneTemplates: bareTemplates
38834
- },
38835
- strategyPrimitives,
38836
- customMandates: "Author a Foundry IPermission contract under mandates/ when no template fits; keep all policy parameters constructor-configured.",
38837
- intelligence: {
38838
- baseUrl: SAIL_INTELLIGENCE_BASE_URL,
38839
- docsUrl: SAIL_INTELLIGENCE_DOCS_URL,
38840
- use: "Vault screening, allocation, and rebalance advice for yield strategies."
38868
+ /** All known SMAs, annotating the currently-active one (mirrors the UI server). */
38869
+ handleListAccounts(res) {
38870
+ res.writeHead(200, { "Content-Type": "application/json" });
38871
+ let active = null;
38872
+ try {
38873
+ active = JSON.parse((0, import_node_fs5.readFileSync)(this.sailFile("account.json"), "utf-8")).safe;
38874
+ } catch {
38841
38875
  }
38842
- };
38843
- emit(
38844
- options.json,
38845
- () => {
38846
- console.log("Sailor capabilities");
38847
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
38848
- console.log(`Chain: ${payload.chainName} (${chainId})`);
38849
- console.log(`Dispatch model: ${dispatchModel ?? "unknown"} (${modelSource})`);
38850
- console.log(
38851
- `Supported chains: ${payload.supportedChains.map((c) => `${c.name} [${c.dispatchModel}]`).join(", ")}`
38876
+ try {
38877
+ const accounts = JSON.parse(
38878
+ (0, import_node_fs5.readFileSync)(this.sailFile("state", "accounts.json"), "utf-8")
38852
38879
  );
38853
- console.log("\nNo-Solidity mandate templates on this chain:");
38854
- if (cloneTemplates.length === 0 && knownTemplates.length === 0 && bareTemplates.length === 0) {
38855
- console.log(" (none registered for this chain yet \u2014 author a custom mandate)");
38856
- }
38857
- for (const t of cloneTemplates) {
38858
- console.log(` \u2022 ${t.label} (${t.kind}) \u2014 ${t.description ?? ""}`);
38859
- console.log(` params: ${t.initParams.map((p) => `${p.name}: ${p.type}`).join(", ")}`);
38860
- }
38861
- for (const t of knownTemplates) {
38862
- console.log(` \u2022 ${t.label} (${t.kind}, shared) \u2014 ${t.description ?? ""}`);
38863
- }
38864
- if (bareTemplates.length > 0) {
38865
- console.log(` \u2022 also deployable: ${bareTemplates.join(", ")}`);
38866
- }
38867
- console.log("\nStrategy primitives:");
38868
- for (const p of strategyPrimitives) console.log(` \u2022 ${p}`);
38869
- console.log("\nCustom mandates:");
38870
- console.log(` ${payload.customMandates}`);
38871
- console.log("\nIntelligence API (yield/allocation advice):");
38872
- console.log(` ${SAIL_INTELLIGENCE_BASE_URL} (docs: ${SAIL_INTELLIGENCE_DOCS_URL})`);
38873
- console.log(
38874
- "\nUse this to decide if a request is buildable. If it can't be expressed as a template, a strategy primitive, or a custom mandate, say so \u2014 don't scaffold a revert."
38880
+ res.end(
38881
+ JSON.stringify(
38882
+ accounts.map((a) => ({
38883
+ ...a,
38884
+ active: a.safe.toLowerCase() === active?.toLowerCase()
38885
+ }))
38886
+ )
38875
38887
  );
38876
- },
38877
- payload
38878
- );
38879
- }
38880
-
38881
- // src/commands/doctor.ts
38882
- init_esm2();
38883
-
38884
- // src/lib/contract-check.ts
38885
- async function checkContractExists(pc, address) {
38886
- try {
38887
- const code = await pc.getCode({ address });
38888
- return { address, hasCode: !!code && code !== "0x" };
38889
- } catch (err) {
38890
- return { address, hasCode: false, error: err.message.split("\n")[0] };
38891
- }
38892
- }
38893
-
38894
- // src/lib/permission-resolver.ts
38895
- var IPERMISSION_ABI = [
38896
- {
38897
- type: "function",
38898
- name: "evaluate",
38899
- stateMutability: "view",
38900
- inputs: [
38901
- { name: "txData", type: "bytes" },
38902
- {
38903
- name: "ctx",
38904
- type: "tuple",
38905
- components: [
38906
- { name: "account", type: "address" },
38907
- { name: "manager", type: "address" },
38908
- { name: "submitter", type: "address" },
38909
- { name: "target", type: "address" },
38910
- { name: "selector", type: "bytes4" },
38911
- { name: "value", type: "uint256" },
38912
- { name: "blockTimestamp", type: "uint256" },
38913
- { name: "blockNumber", type: "uint256" }
38914
- ]
38888
+ } catch {
38889
+ try {
38890
+ const a = JSON.parse(
38891
+ (0, import_node_fs5.readFileSync)(this.sailFile("account.json"), "utf-8")
38892
+ );
38893
+ res.end(JSON.stringify([{ ...a, name: "My SMA", active: true, addedAt: null }]));
38894
+ } catch {
38895
+ res.end("[]");
38915
38896
  }
38916
- ],
38917
- outputs: [{ type: "bool" }]
38918
- }
38919
- ];
38920
- function buildPermissionContext(params) {
38921
- const { account: account2, manager, call: call2, blockInfo } = params;
38922
- const selector = call2.data.length >= 10 ? call2.data.slice(0, 10) : "0x00000000";
38923
- return {
38924
- account: account2,
38925
- manager,
38926
- submitter: manager,
38927
- // runner submits dispatches from the manager (agent) wallet
38928
- target: call2.target,
38929
- selector,
38930
- value: call2.value,
38931
- blockTimestamp: blockInfo.timestamp,
38932
- blockNumber: blockInfo.number
38933
- };
38934
- }
38935
- async function probePermissionForCall(params) {
38936
- const { publicClient, permission, account: account2, manager, call: call2, blockInfo } = params;
38937
- const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
38938
- try {
38939
- const accepted = await publicClient.readContract({
38940
- address: permission,
38941
- abi: IPERMISSION_ABI,
38942
- functionName: "evaluate",
38943
- args: [call2.data, ctx]
38944
- });
38945
- return { accepted: Boolean(accepted), reverted: false };
38946
- } catch (err) {
38947
- return { accepted: false, reverted: true, error: err.message.split("\n")[0] };
38897
+ }
38948
38898
  }
38949
- }
38950
- async function resolvePermissionForCall(params) {
38951
- const { publicClient, account: account2, manager, call: call2, registeredPermissions, blockInfo } = params;
38952
- const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
38953
- for (const permission of registeredPermissions) {
38954
- try {
38955
- const accepted = await publicClient.readContract({
38956
- address: permission,
38957
- abi: IPERMISSION_ABI,
38958
- functionName: "evaluate",
38959
- args: [call2.data, ctx]
38899
+ handleHttp(req, res) {
38900
+ const origin = req.headers.origin;
38901
+ const url0 = (req.url ?? "/").split("?")[0];
38902
+ const isDiscoveryEndpoint = url0 === "/config";
38903
+ const allowedOrigin = isDiscoveryEndpoint && origin?.startsWith("http://localhost:") ? origin : origin === this._url ? origin : this._url;
38904
+ res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
38905
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
38906
+ res.setHeader("Access-Control-Allow-Headers", `Content-Type, ${REQUEST_SECRET_HEADER}`);
38907
+ res.setHeader("Vary", "Origin");
38908
+ if (req.method === "OPTIONS") {
38909
+ res.writeHead(204);
38910
+ res.end();
38911
+ return;
38912
+ }
38913
+ const url = (req.url ?? "/").split("?")[0];
38914
+ if (url === "/config") {
38915
+ const isTrustedOrigin = !origin || origin === this._url;
38916
+ const wsUrlForClient = isTrustedOrigin ? `${this.wsUrl}?secret=${encodeURIComponent(this.requestSecret)}` : this.wsUrl;
38917
+ res.writeHead(200, { "Content-Type": "application/json" });
38918
+ res.end(
38919
+ JSON.stringify({
38920
+ url: this._url,
38921
+ wsUrl: wsUrlForClient,
38922
+ port: this.port,
38923
+ pid: process.pid,
38924
+ pendingCount: this.pending.size
38925
+ })
38926
+ );
38927
+ return;
38928
+ }
38929
+ const secretHeader = req.headers[REQUEST_SECRET_HEADER];
38930
+ const isAuthenticated = secretHeader === this.requestSecret;
38931
+ if (url === "/pending") {
38932
+ if (!isAuthenticated) {
38933
+ res.writeHead(403, { "Content-Type": "application/json" });
38934
+ res.end(JSON.stringify({ error: "forbidden" }));
38935
+ return;
38936
+ }
38937
+ res.writeHead(200, { "Content-Type": "application/json" });
38938
+ res.end(JSON.stringify(Array.from(this.pending.values()).map((e) => e.request)));
38939
+ return;
38940
+ }
38941
+ if (url === "/wallet") {
38942
+ if (!isAuthenticated) {
38943
+ res.writeHead(403, { "Content-Type": "application/json" });
38944
+ res.end(JSON.stringify({ error: "forbidden" }));
38945
+ return;
38946
+ }
38947
+ res.writeHead(200, { "Content-Type": "application/json" });
38948
+ res.end(JSON.stringify({ address: this._connectedWallet ?? null }));
38949
+ return;
38950
+ }
38951
+ if (url === "/requests" && req.method === "POST") {
38952
+ const supplied = req.headers[REQUEST_SECRET_HEADER];
38953
+ if (supplied !== this.requestSecret) {
38954
+ res.writeHead(403, { "Content-Type": "application/json" });
38955
+ res.end(JSON.stringify({ error: "forbidden" }));
38956
+ return;
38957
+ }
38958
+ this.readBody(req).then((body) => {
38959
+ const parsed = JSON.parse(body);
38960
+ if (!parsed.kind || !["create-sma", "deploy-mandate", "register-permission", "attach-mandate", "revoke-permissions", "set-delegate", "arbitrary-tx"].includes(parsed.kind)) {
38961
+ throw new Error(`Unknown signing request kind: ${String(parsed.kind)}`);
38962
+ }
38963
+ const request = this.enqueue(parsed);
38964
+ res.writeHead(200, { "Content-Type": "application/json" });
38965
+ res.end(JSON.stringify({ id: request.id }));
38966
+ }).catch((err) => {
38967
+ res.writeHead(400, { "Content-Type": "application/json" });
38968
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
38960
38969
  });
38961
- if (accepted) return permission;
38962
- } catch {
38970
+ return;
38963
38971
  }
38964
- }
38965
- return void 0;
38966
- }
38967
- async function resolvePermissionForBatch(params) {
38968
- const { publicClient, kernel, account: account2, calls, registeredPermissions } = params;
38969
- for (const permission of registeredPermissions) {
38970
- try {
38971
- const [approved] = await publicClient.readContract({
38972
- address: kernel,
38973
- abi: SailKernelAbi,
38974
- functionName: "previewBatch",
38975
- args: [account2, permission, calls]
38972
+ if (url === "/api/account" && req.method === "POST") {
38973
+ this.handleSaveAccount(req, res);
38974
+ return;
38975
+ }
38976
+ if (url === "/api/account" && (req.method === "GET" || req.method == null)) {
38977
+ this.sendJsonFile(res, (0, import_node_path4.join)(this.projectRoot, ".sail", "account.json"), {
38978
+ status: 404,
38979
+ body: { error: "account not found" }
38976
38980
  });
38977
- if (approved) return permission;
38978
- } catch {
38981
+ return;
38979
38982
  }
38980
- }
38981
- return void 0;
38982
- }
38983
-
38984
- // src/commands/doctor.ts
38985
- var LOW_GAS_THRESHOLD_WEI = 500000000000000n;
38986
- async function nativeBalance(pc, address) {
38987
- const wei = await pc.getBalance({ address });
38988
- return {
38989
- address,
38990
- wei: wei.toString(),
38991
- eth: formatEther(wei),
38992
- funded: wei > 0n,
38993
- low: wei > 0n && wei < LOW_GAS_THRESHOLD_WEI
38994
- };
38995
- }
38996
- function keystoreAddress(role, safe) {
38997
- const ks = readJsonFile(resolveKeyPath(role, safe));
38998
- return ks?.address ? getAddress(`0x${ks.address.replace(/^0x/, "")}`) : null;
38999
- }
39000
- var PROBE_TARGET = "0x000000000000000000000000000000000000dEaD";
39001
- var PROBE_SELECTOR = "0xffffffff";
39002
- var PROBE_DATA = "0xffffffff";
39003
- async function probePassThrough(pc, permission, account2) {
39004
- try {
39005
- const ok = await pc.readContract({
39006
- address: permission,
39007
- abi: IPERMISSION_ABI,
39008
- functionName: "evaluate",
39009
- args: [
39010
- PROBE_DATA,
39011
- {
39012
- account: account2,
39013
- manager: account2,
39014
- submitter: account2,
39015
- target: PROBE_TARGET,
39016
- selector: PROBE_SELECTOR,
39017
- value: 0n,
39018
- blockTimestamp: 0n,
39019
- blockNumber: 0n
38983
+ if (url === "/api/accounts" && (req.method === "GET" || req.method == null)) {
38984
+ this.handleListAccounts(res);
38985
+ return;
38986
+ }
38987
+ const resultMatch = url.match(/^\/requests\/([^/]+)\/result$/);
38988
+ if (resultMatch && (req.method === "GET" || req.method == null)) {
38989
+ if (!isAuthenticated) {
38990
+ res.writeHead(403, { "Content-Type": "application/json" });
38991
+ res.end(JSON.stringify({ error: "forbidden" }));
38992
+ return;
38993
+ }
38994
+ const id = decodeURIComponent(resultMatch[1]);
38995
+ this.waitForResult(id, RESULT_LONGPOLL_MS).then((result) => {
38996
+ if (result) {
38997
+ res.writeHead(200, { "Content-Type": "application/json" });
38998
+ res.end(JSON.stringify(result));
38999
+ } else {
39000
+ res.writeHead(204);
39001
+ res.end();
39020
39002
  }
39021
- ]
39022
- });
39023
- return { permission, passesThrough: ok };
39024
- } catch (err) {
39025
- return {
39026
- permission,
39027
- passesThrough: false,
39028
- note: `evaluate reverted (${err.message.split("\n")[0]})`
39003
+ });
39004
+ return;
39005
+ }
39006
+ if (this.uiDist) {
39007
+ const rawPath = (req.url ?? "/").split("?")[0];
39008
+ const filePath = (0, import_node_path4.resolve)((0, import_node_path4.join)(this.uiDist, rawPath === "/" ? "index.html" : rawPath));
39009
+ if (!filePath.startsWith((0, import_node_path4.resolve)(this.uiDist))) {
39010
+ res.writeHead(403);
39011
+ res.end();
39012
+ return;
39013
+ }
39014
+ if ((0, import_node_fs5.existsSync)(filePath)) {
39015
+ const mime = MIME[(0, import_node_path4.extname)(filePath)] ?? "application/octet-stream";
39016
+ res.writeHead(200, { "Content-Type": mime });
39017
+ res.end((0, import_node_fs5.readFileSync)(filePath));
39018
+ return;
39019
+ }
39020
+ const indexHtml = (0, import_node_path4.join)(this.uiDist, "index.html");
39021
+ if ((0, import_node_fs5.existsSync)(indexHtml)) {
39022
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
39023
+ res.end((0, import_node_fs5.readFileSync)(indexHtml));
39024
+ return;
39025
+ }
39026
+ }
39027
+ res.writeHead(404);
39028
+ res.end();
39029
+ }
39030
+ handleConnection(ws) {
39031
+ this.clients.add(ws);
39032
+ const msg = {
39033
+ type: "pending",
39034
+ requests: Array.from(this.pending.values()).map((e) => e.request)
39029
39035
  };
39036
+ ws.send(JSON.stringify(msg));
39037
+ ws.on("message", (data) => {
39038
+ try {
39039
+ const parsed = JSON.parse(data.toString());
39040
+ this.handleClientMessage(ws, parsed);
39041
+ } catch {
39042
+ }
39043
+ });
39044
+ ws.on("close", () => this.clients.delete(ws));
39045
+ ws.on("error", () => this.clients.delete(ws));
39030
39046
  }
39031
- }
39032
- async function doctor(options = {}) {
39033
- const project = new ProjectContext();
39034
- const chainId = project.chainId;
39035
- const kernel = project.contracts.kernel;
39036
- const rpcUrl = getRpcUrl(chainId) ?? getChainById(chainId).rpcUrls.default.http[0];
39037
- const client = new SailorClient({ chainId, rpcUrl, kernel });
39038
- const pc = createPublicClient({ chain: getChainById(chainId), transport: http(rpcUrl) });
39039
- const caps = await client.capabilities();
39040
- const stored = readJsonFile(sailPath("account.json"));
39041
- const safe = options.account ? getAddress(options.account) : stored?.safe ? getAddress(stored.safe) : null;
39042
- let permissions = [];
39043
- let checks = [];
39044
- let permsNoCode = [];
39045
- if (safe) {
39046
- const mandates = await client.mandate.list(safe);
39047
- permissions = mandates.map((m) => getAddress(m.permission));
39048
- if (permissions.length > 0) {
39049
- const codeChecks = await Promise.all(permissions.map((p) => checkContractExists(pc, p)));
39050
- permsNoCode = codeChecks.filter((c) => !c.hasCode && !c.error).map((c) => c.address);
39047
+ handleClientMessage(_ws, msg) {
39048
+ if (msg.type === "wallet-connected") {
39049
+ if (typeof msg.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(msg.address)) return;
39050
+ this._connectedWallet = msg.address;
39051
+ for (const listener of this.walletListeners) listener(msg.address);
39052
+ this.walletListeners = [];
39053
+ return;
39051
39054
  }
39052
- if (caps.dispatchModel === "conjunctive" && permissions.length > 0) {
39053
- checks = await Promise.all(permissions.map((p) => probePassThrough(pc, p, safe)));
39055
+ if (msg.type === "wallet-disconnected") {
39056
+ this._connectedWallet = void 0;
39057
+ return;
39058
+ }
39059
+ const entry = this.pending.get(msg.requestId);
39060
+ if (!entry) return;
39061
+ clearTimeout(entry.timer);
39062
+ this.pending.delete(msg.requestId);
39063
+ const { request } = entry;
39064
+ if (msg.type === "signed") {
39065
+ this.recordResult(
39066
+ { status: "signed", requestId: msg.requestId, txHash: msg.txHash },
39067
+ request
39068
+ );
39069
+ } else if (msg.type === "signature") {
39070
+ this.recordResult(
39071
+ { status: "signature", requestId: msg.requestId, signature: msg.signature },
39072
+ request
39073
+ );
39074
+ } else {
39075
+ this.recordResult(
39076
+ {
39077
+ status: "rejected",
39078
+ requestId: msg.requestId,
39079
+ reason: msg.reason
39080
+ },
39081
+ request
39082
+ );
39054
39083
  }
39055
39084
  }
39056
- const bricking = checks.filter((c) => c.passesThrough === false);
39057
- const healthy = safe !== null && bricking.length === 0;
39058
- const ownerAddr = stored?.owner ? getAddress(stored.owner) : project.getOwner();
39059
- const managerAddr = stored?.manager ? getAddress(stored.manager) : keystoreAddress("manager", stored?.safe);
39060
- let chainIdOnChain = null;
39061
- try {
39062
- chainIdOnChain = await pc.getChainId();
39063
- } catch {
39085
+ readBody(req, maxBytes = 1e6) {
39086
+ return new Promise((res, rej) => {
39087
+ let size5 = 0;
39088
+ const chunks = [];
39089
+ req.on("data", (c) => {
39090
+ size5 += c.length;
39091
+ if (size5 > maxBytes) {
39092
+ rej(new Error("Request body too large"));
39093
+ req.destroy();
39094
+ return;
39095
+ }
39096
+ chunks.push(c);
39097
+ });
39098
+ req.on("end", () => res(Buffer.concat(chunks).toString("utf8")));
39099
+ req.on("error", rej);
39100
+ });
39064
39101
  }
39065
- const chainIdMatches = chainIdOnChain === null ? null : chainIdOnChain === chainId;
39066
- let ownerBal = null;
39067
- let managerBal = null;
39068
- try {
39069
- if (ownerAddr) ownerBal = await nativeBalance(pc, ownerAddr);
39070
- if (managerAddr) managerBal = await nativeBalance(pc, managerAddr);
39071
- } catch {
39102
+ broadcast(msg) {
39103
+ const data = JSON.stringify(msg);
39104
+ for (const ws of this.clients) {
39105
+ if (ws.readyState === import_websocket2.default.OPEN) ws.send(data);
39106
+ }
39072
39107
  }
39073
- if (options.json) {
39074
- console.log(
39108
+ writeRuntimeState() {
39109
+ if (!(0, import_node_fs5.existsSync)(this.runtimeDir)) (0, import_node_fs5.mkdirSync)(this.runtimeDir, { recursive: true });
39110
+ (0, import_node_fs5.writeFileSync)(
39111
+ (0, import_node_path4.join)(this.runtimeDir, SERVER_STATE_FILE),
39075
39112
  JSON.stringify(
39076
39113
  {
39077
- chainId,
39078
- kernel,
39079
- dispatchModel: caps.dispatchModel,
39080
- dispatchTypehash: caps.dispatchTypehash,
39081
- capabilitySource: caps.source,
39082
- rpc: { chainIdOnChain, chainIdMatches },
39083
- wallet: {
39084
- owner: ownerAddr ? { address: ownerAddr, ...ownerBal ?? {} } : null,
39085
- manager: managerAddr ? { address: managerAddr, ...managerBal ?? {} } : null
39086
- },
39087
- account: safe,
39088
- saltNonce: stored?.saltNonce ?? null,
39089
- permissions,
39090
- permissionsWithoutCode: permsNoCode,
39091
- conjunctivePassThrough: caps.dispatchModel === "conjunctive" ? checks.map((c) => ({
39092
- permission: c.permission,
39093
- passesThrough: c.passesThrough,
39094
- note: c.note
39095
- })) : "n/a (selective kernel)",
39096
- healthy
39114
+ url: this._url,
39115
+ wsUrl: this.wsUrl,
39116
+ port: this.port,
39117
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
39118
+ pid: process.pid,
39119
+ requestSecret: this.requestSecret
39097
39120
  },
39098
39121
  null,
39099
39122
  2
39100
39123
  )
39101
39124
  );
39102
- return;
39103
39125
  }
39104
- const multiBricking = bricking.length > 0 && permissions.length > 1;
39105
- if (!safe) {
39106
- console.log('\u2717 Setup incomplete \u2014 no SMA found. Run "sailor onboard --new-sma" to deploy one.');
39107
- } else if (permissions.length === 0) {
39108
- console.log("\u2717 Setup incomplete \u2014 no permissions registered. Your agent cannot dispatch until at least one permission is attached.");
39109
- } else if (multiBricking) {
39110
- console.log(`\u2717 Setup issue \u2014 ${bricking.length} of ${permissions.length} permissions block unrelated calls (see below).`);
39111
- } else {
39112
- console.log(
39113
- "\u2713 Everything looks good \u2014 your SMA is deployed, your permission is registered,\n and your agent is authorized to dispatch."
39114
- );
39126
+ removeRuntimeState() {
39127
+ const path8 = (0, import_node_path4.join)(this.runtimeDir, SERVER_STATE_FILE);
39128
+ try {
39129
+ if ((0, import_node_fs5.existsSync)(path8)) (0, import_node_fs5.unlinkSync)(path8);
39130
+ } catch {
39131
+ }
39115
39132
  }
39116
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
39117
- console.log(`Chain: ${chainId}`);
39118
- console.log(`Kernel: ${kernel}`);
39119
- console.log(` dispatch model: ${caps.dispatchModel} (detected via ${caps.source})`);
39120
- console.log(` DISPATCH_TYPEHASH: ${caps.dispatchTypehash}`);
39121
- console.log("\nWallet & gas:");
39122
- if (chainIdMatches === false) {
39123
- console.log(
39124
- ` \u2717 RPC serves chain ${chainIdOnChain}, but the project is configured for ${chainId}. Fix RPC_URL in .sail/.env.local before doing anything.`
39125
- );
39126
- } else if (chainIdMatches === true) {
39127
- console.log(` \u2713 RPC serves the configured chain (${chainId}).`);
39133
+ };
39134
+ async function findAvailablePort(startPort) {
39135
+ return new Promise((res) => {
39136
+ const probe = (0, import_node_net.createServer)();
39137
+ probe.listen(startPort, "127.0.0.1", () => {
39138
+ const addr = probe.address();
39139
+ probe.close(() => res(addr.port));
39140
+ });
39141
+ probe.on("error", () => res(findAvailablePort(startPort + 1)));
39142
+ });
39143
+ }
39144
+
39145
+ // src/signing/client.ts
39146
+ var RUNTIME_SERVER_FILE = (0, import_node_path5.join)(".sail", "runtime", "server.json");
39147
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
39148
+ var SigningClient = class {
39149
+ constructor(baseUrl, requestSecret = "") {
39150
+ this.baseUrl = baseUrl;
39151
+ this.requestSecret = requestSecret;
39128
39152
  }
39129
- const showBalance = (label, addr, bal) => {
39130
- if (!addr) {
39131
- console.log(` ${label}: not set`);
39132
- return;
39133
- }
39134
- if (!bal) {
39135
- console.log(` ${label}: ${addr} (balance unavailable)`);
39136
- return;
39153
+ remote = true;
39154
+ get url() {
39155
+ return this.baseUrl;
39156
+ }
39157
+ async start() {
39158
+ if (!await this.ping()) {
39159
+ throw new Error(`Signing station not reachable at ${this.baseUrl}`);
39137
39160
  }
39138
- const flag = !bal.funded ? "\u2717 unfunded" : bal.low ? "\u26A0 low" : "\u2713";
39139
- console.log(` ${label}: ${addr} ${bal.eth} ETH ${flag}`);
39140
- };
39141
- showBalance("owner ", ownerAddr, ownerBal);
39142
- showBalance("manager", managerAddr, managerBal);
39143
- if (managerBal && !managerBal.funded) {
39144
- console.log(
39145
- ' \u2192 The manager (agent) pays gas. Fund it before "sailor run" or dispatches fail.'
39146
- );
39147
39161
  }
39148
- if (!safe) {
39149
- console.log('\nAccount: none found. Run "sailor onboard --new-sma", or pass --account <addr>.');
39150
- console.log("Skipping permission checks.");
39151
- return;
39162
+ /** No-op: never tear down a daemon the user is running in another process. */
39163
+ stop() {
39152
39164
  }
39153
- console.log(`Account: ${safe}`);
39154
- if (stored?.saltNonce != null) {
39155
- const saltNonce = BigInt(stored.saltNonce);
39156
- const MAINNET_CHAINS = [8453, 42161, 130];
39165
+ async ping() {
39157
39166
  try {
39158
- const proxyCreationCode = await pc.readContract({
39159
- address: SAFE_V141.proxyFactory,
39160
- abi: safeProxyFactoryAbi,
39161
- functionName: "proxyCreationCode"
39167
+ const r = await fetch(`${this.baseUrl}/config`, { signal: AbortSignal.timeout(1500) });
39168
+ return r.ok;
39169
+ } catch {
39170
+ return false;
39171
+ }
39172
+ }
39173
+ async requestSignature(req, timeoutMs = 10 * 60 * 1e3) {
39174
+ const enqueueRes = await fetch(`${this.baseUrl}/requests`, {
39175
+ method: "POST",
39176
+ headers: { "Content-Type": "application/json", "x-sailor-secret": this.requestSecret },
39177
+ body: JSON.stringify(req)
39178
+ });
39179
+ if (!enqueueRes.ok) {
39180
+ throw new Error(`Failed to enqueue signing request (HTTP ${enqueueRes.status})`);
39181
+ }
39182
+ const { id } = await enqueueRes.json();
39183
+ const deadline = Date.now() + timeoutMs;
39184
+ while (Date.now() < deadline) {
39185
+ const res = await fetch(`${this.baseUrl}/requests/${encodeURIComponent(id)}/result`, {
39186
+ headers: { "x-sailor-secret": this.requestSecret }
39162
39187
  });
39163
- const ownerAddr2 = stored.owner ? getAddress(stored.owner) : null;
39164
- const managerAddr2 = stored.manager ? getAddress(stored.manager) : null;
39165
- if (ownerAddr2 && managerAddr2) {
39166
- console.log(
39167
- `
39168
- Multi-chain addresses (salt ${saltNonce}, owner ${ownerAddr2}, manager ${managerAddr2}):`
39169
- );
39170
- const CHAIN_NAMES = { 8453: "Base", 42161: "Arbitrum", 130: "Unichain" };
39171
- for (const cid of MAINNET_CHAINS) {
39172
- const dep = sailDeployments[cid];
39173
- const initializer = buildSafeSetupInitializer({
39174
- owners: [ownerAddr2],
39175
- threshold: 1n,
39176
- kernel: dep.kernel,
39177
- safeModuleEnabler: dep.safeModuleEnabler
39178
- });
39179
- const predicted = computeSailSmaAddress({
39180
- initializer,
39181
- saltNonce,
39182
- deployer: ownerAddr2,
39183
- permissionSigner: ownerAddr2,
39184
- manager: managerAddr2,
39185
- feePolicy: dep.standardFeePolicy,
39186
- proxyCreationCode
39187
- });
39188
- const isDeployed = predicted.toLowerCase() === safe.toLowerCase() && cid === chainId;
39189
- const label = isDeployed ? "deployed (this account)" : predicted;
39190
- console.log(` ${CHAIN_NAMES[cid].padEnd(12)} (${cid}): ${label}`);
39191
- }
39192
- console.log(
39193
- ' \u26A0 Addresses differ per chain (chain-specific kernel salt + initializer). Run "sailor account predict" for details.'
39194
- );
39188
+ if (res.status === 200) return await res.json();
39189
+ if (res.status !== 204) {
39190
+ throw new Error(`Unexpected result status ${res.status} from signing station`);
39195
39191
  }
39196
- } catch {
39197
39192
  }
39198
- } else if (stored) {
39199
- console.log(
39200
- "\nMulti-chain addresses: saltNonce not stored (deployed before salt tracking)."
39201
- );
39202
- console.log(" To enable: re-deploy with sailor onboard --new-sma --salt 0");
39203
- }
39204
- if (permissions.length === 0) {
39205
- console.log(
39206
- "\n\u26A0 No permissions registered \u2014 every dispatch will be denied (NoPermissionsRegistered)."
39207
- );
39208
- console.log(' Register at least one with "sailor mandate attach".');
39209
- return;
39210
- }
39211
- console.log(`
39212
- Registered permissions (${permissions.length}):`);
39213
- if (permsNoCode.length > 0) {
39214
- console.log(
39215
- `
39216
- \u26A0 ${permsNoCode.length} registered permission(s) have NO contract code on chain ${chainId} \u2014 dispatches naming them will fail. Verify the address (wrong chain?) or revoke:`
39217
- );
39218
- permsNoCode.forEach((p) => console.log(` ${p}`));
39219
- }
39220
- if (caps.dispatchModel === "selective") {
39221
- permissions.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
39222
- console.log("\nEach dispatch names one permission, so pass-through is not required.");
39223
- return;
39193
+ throw new Error(`Signing request "${req.title}" timed out after ${timeoutMs / 1e3}s`);
39224
39194
  }
39225
- for (let i = 0; i < checks.length; i++) {
39226
- const c = checks[i];
39227
- const mark = c.passesThrough ? "\u2713 pass-through" : "\u2717 NOT pass-through";
39228
- console.log(` ${i + 1}. ${c.permission} ${mark}${c.note ? ` (${c.note})` : ""}`);
39195
+ async waitForWallet(timeoutMs = 5 * 60 * 1e3) {
39196
+ const deadline = Date.now() + timeoutMs;
39197
+ while (Date.now() < deadline) {
39198
+ try {
39199
+ const r = await fetch(`${this.baseUrl}/wallet`, {
39200
+ headers: { "x-sailor-secret": this.requestSecret },
39201
+ signal: AbortSignal.timeout(2e3)
39202
+ });
39203
+ if (r.ok) {
39204
+ const { address } = await r.json();
39205
+ if (address) return address;
39206
+ }
39207
+ } catch {
39208
+ }
39209
+ await sleep(1e3);
39210
+ }
39211
+ throw new Error("Timed out waiting for wallet connection in the signing UI");
39229
39212
  }
39230
- if (multiBricking) {
39231
- console.log(
39232
- `
39233
- \u2717 ${bricking.length} permission(s) return false for unrelated calls. On this kernel EVERY registered permission must approve EVERY call, so these BRICK all dispatches (they surface as PermissionDenied). Revoke or replace them with pass-through versions:`
39234
- );
39235
- bricking.forEach((c) => console.log(` ${c.permission}`));
39236
- } else if (permissions.length > 1) {
39237
- console.log("\n\u2713 All permissions pass through unrelated calls \u2014 dispatch will not be bricked.");
39213
+ };
39214
+ function readRuntimeServerState(projectRoot) {
39215
+ const file = (0, import_node_path5.join)(projectRoot, RUNTIME_SERVER_FILE);
39216
+ if (!(0, import_node_fs6.existsSync)(file)) return null;
39217
+ try {
39218
+ return JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8"));
39219
+ } catch {
39220
+ return null;
39238
39221
  }
39239
- console.log(`
39240
- Probe is heuristic: an unknown selector (${PROBE_SELECTOR}) to a neutral target (${PROBE_TARGET}).`);
39241
39222
  }
39242
-
39243
- // src/commands/init.ts
39244
- var import_node_fs6 = __toESM(require("node:fs"), 1);
39245
- var import_node_path5 = __toESM(require("node:path"), 1);
39246
-
39247
- // src/lib/foundry.ts
39248
- var import_node_fs4 = require("node:fs");
39249
- var import_node_path3 = require("node:path");
39250
- var FOUNDRY_TOML = `[profile.default]
39251
- src = "mandates"
39252
- out = "out"
39253
- libs = ["lib"]
39254
- remappings = ["@sail/=.sail/contracts/"]
39255
- solc = "0.8.26"
39256
- optimizer = true
39257
- optimizer_runs = 200
39258
- # Mandates are deployed as standalone contracts and configured via their
39259
- # constructor, then attached to a Safe with \`sailor mandate attach\`.
39260
- `;
39261
- var IPERMISSION_SOL = `// SPDX-License-Identifier: MIT
39262
- pragma solidity 0.8.26;
39263
-
39264
- /// @notice Execution context passed to every permission on each dispatch call.
39265
- /// @dev Read-only snapshot of the transaction environment (staticcall).
39266
- struct Context {
39267
- address account; // the Safe whose assets are being moved
39268
- address manager; // the delegated signer who submitted the dispatch
39269
- address submitter; // msg.sender of the dispatch (may be a relayer)
39270
- address target; // the call target
39271
- bytes4 selector; // leading 4 bytes of calldata
39272
- uint256 value; // native ETH forwarded (wei)
39273
- uint256 blockTimestamp; // block.timestamp at dispatch
39274
- uint256 blockNumber; // block.number at dispatch
39223
+ async function discoverDaemon(projectRoot = process.cwd()) {
39224
+ const state = readRuntimeServerState(projectRoot);
39225
+ if (!state?.url) return null;
39226
+ const client = new SigningClient(state.url, state.requestSecret ?? "");
39227
+ return await client.ping() ? client : null;
39275
39228
  }
39276
-
39277
- /// @title IPermission
39278
- /// @notice Interface every Sail permission (mandate) contract must implement.
39279
- /// @dev Evaluated via staticcall with a fixed gas cap; a revert or gas
39280
- /// exhaustion is treated as \`false\`. Must not mutate state.
39281
- interface IPermission {
39282
- /// @notice Decide whether a manager-submitted transaction is permitted.
39283
- function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
39284
-
39285
- /// @notice Optional stable identifier for off-chain indexing/deduplication.
39286
- function discriminator() external view returns (bytes32);
39229
+ async function createSigningChannel(projectRoot = process.cwd()) {
39230
+ const daemon = await discoverDaemon(projectRoot);
39231
+ if (daemon) return daemon;
39232
+ return new SigningServer({ projectRoot, advertise: false });
39287
39233
  }
39288
- `;
39289
- var EXAMPLE_MANDATE_SOL = `// SPDX-License-Identifier: MIT
39290
- pragma solidity 0.8.26;
39291
-
39292
- import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
39293
-
39294
- /// @title BoundedCallPermission
39295
- /// @notice General-purpose IPermission primitive. Bounds the universal properties of any call:
39296
- /// allowed targets, allowed selectors, and max ETH value. Protocol-agnostic.
39297
- /// For calldata-parameter bounds (amount caps, recipient checks, slippage), write a
39298
- /// protocol-specific permission \u2014 see examples/permissions/ for the pattern per protocol.
39299
- /// @dev Deploy one instance per SMA with constructor-configured parameters.
39300
- contract BoundedCallPermission is IPermission {
39301
- bytes32 private constant DISCRIMINATOR = keccak256("BoundedCallPermission");
39302
-
39303
- mapping(address => bool) public isAllowedTarget;
39304
- mapping(bytes4 => bool) public isAllowedSelector;
39305
- bool public immutable SELECTOR_FILTERING;
39306
- uint256 public immutable MAX_VALUE;
39307
-
39308
- constructor(address[] memory allowedTargets, bytes4[] memory allowedSelectors, uint256 maxValue) {
39309
- for (uint256 i = 0; i < allowedTargets.length; i++) isAllowedTarget[allowedTargets[i]] = true;
39310
- SELECTOR_FILTERING = allowedSelectors.length > 0;
39311
- for (uint256 i = 0; i < allowedSelectors.length; i++) isAllowedSelector[allowedSelectors[i]] = true;
39312
- MAX_VALUE = maxValue;
39313
- }
39314
-
39315
- function evaluate(bytes calldata, Context calldata ctx) external view returns (bool) {
39316
- if (!isAllowedTarget[ctx.target]) return false;
39317
- if (SELECTOR_FILTERING && !isAllowedSelector[ctx.selector]) return false;
39318
- if (ctx.value > MAX_VALUE) return false;
39319
- return true;
39320
- }
39321
39234
 
39322
- function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
39235
+ // src/commands/account.ts
39236
+ function resolveChain(chainId) {
39237
+ try {
39238
+ return getChain(chainId);
39239
+ } catch {
39240
+ throw new Error(
39241
+ `Chain ${chainId} is not yet configured in @sail/chains.
39242
+ The SailKernel and mandate-factory addresses for this chain are unknown,
39243
+ so an account cannot be created yet. Add the chain to @sail/chains once
39244
+ SailKernel is deployed there.`
39245
+ );
39246
+ }
39323
39247
  }
39324
- `;
39325
- var MANDATES_README = `# Mandates
39326
-
39327
- Solidity permission contracts for this Sailor project live here.
39328
-
39329
- A permission implements \`@sail/interfaces/IPermission.sol\` \u2014 \`evaluate(txData, ctx)\`
39330
- returns \`true\` to permit a manager-submitted dispatch, \`false\` to block it.
39331
-
39332
- ## Authoring + deploying
39333
-
39334
- 1. Start from \`BoundedCallPermission.sol\` for target/selector/value gating.
39335
- For calldata-parameter bounds (amount caps, slippage, recipient checks),
39336
- decode \`txData\` with the target protocol's ABI and add bounds to \`evaluate()\`.
39337
- Configure all parameters in the **constructor** \u2014 the deploy flow expects a
39338
- single creation transaction to fully set up the permission.
39339
- 2. Compile:
39340
- \`\`\`bash
39341
- forge build
39342
- \`\`\`
39343
- 3. Deploy it (the owner signs the creation tx in the browser signing UI):
39344
- \`\`\`bash
39345
- sailor mandate deploy --contract BoundedCallPermission \\
39346
- --args '[["0xTarget1", "0xTarget2"], [], 0]'
39347
- \`\`\`
39348
- Args: (allowedTargets[], allowedSelectors[], maxValue).
39349
- Pass an empty selector array [] to skip selector filtering.
39350
- Pass 0 for maxValue to block all ETH transfers.
39351
- 4. Attach it to a Safe:
39352
- \`\`\`bash
39353
- sailor mandate attach --address 0xDeployed --sma 0xSafe
39354
- \`\`\`
39355
- (or pass \`--attach --sma 0xSafe\` to \`deploy\` to do both at once.)
39356
-
39357
- Compiled artifacts are written to \`out/\` and the deployed address is tracked in
39358
- \`.sail/state/mandates.json\`.
39359
- `;
39360
- function scaffoldFoundryWorkspace(root) {
39361
- const dirs = [(0, import_node_path3.join)(root, "mandates"), (0, import_node_path3.join)(root, ".sail", "contracts", "interfaces")];
39362
- for (const d of dirs) {
39363
- if (!(0, import_node_fs4.existsSync)(d)) (0, import_node_fs4.mkdirSync)(d, { recursive: true });
39248
+ async function accountCreate() {
39249
+ if (!keyExists("manager")) {
39250
+ throw new Error(
39251
+ 'No agent wallet found.\nRun "sailor keys generate" and choose "agent wallet" first.'
39252
+ );
39253
+ }
39254
+ const env = parseEnvFile(sailPath(".env.local"));
39255
+ const rpcUrl = env["RPC_URL"] ?? process.env["RPC_URL"];
39256
+ const chainIdRaw = env["CHAIN_ID"] ?? process.env["CHAIN_ID"];
39257
+ if (!rpcUrl || !chainIdRaw) {
39258
+ throw new Error(
39259
+ "RPC_URL and CHAIN_ID must be set in .sail/.env.local.\nCreate that file with, for example:\n RPC_URL=https://your-rpc-endpoint\n CHAIN_ID=8453"
39260
+ );
39261
+ }
39262
+ const chainId = Number(chainIdRaw);
39263
+ if (Number.isNaN(chainId)) {
39264
+ throw new Error(`Invalid CHAIN_ID: "${chainIdRaw}" \u2014 must be a number.`);
39265
+ }
39266
+ const chain2 = resolveChain(chainId);
39267
+ console.log(`Chain ${chainId} (${chain2.name})`);
39268
+ console.log(` SailKernel: ${checksum4(chain2.kernel)}`);
39269
+ console.log(` Mandate factory: ${checksum4(chain2.mandateFactory)}
39270
+ `);
39271
+ const manager = await loadKeyring("manager");
39272
+ const managerAddr = checksum4(manager.address);
39273
+ const safeFactory = await promptAddress("Safe factory address");
39274
+ const safeSingleton = await promptAddress("Safe singleton address");
39275
+ const owner2 = await promptAddress("Owner (EOA) address", managerAddr);
39276
+ const permissionSigner = await promptAddress("Mandate signer address", managerAddr);
39277
+ const feePolicy = await prompt("Fee policy", "none");
39278
+ console.log("\nCreating SMA with:");
39279
+ console.log(` Owner: ${owner2}`);
39280
+ console.log(` Agent wallet: ${managerAddr}`);
39281
+ console.log(` Mandate signer: ${permissionSigner}`);
39282
+ console.log(` Safe factory: ${safeFactory}`);
39283
+ console.log(` Safe singleton: ${safeSingleton}`);
39284
+ console.log(` Fee policy: ${feePolicy}`);
39285
+ const client = makeClient(chainId);
39286
+ try {
39287
+ const account2 = await client.account.create({
39288
+ owner: owner2,
39289
+ permissionSigner,
39290
+ manager: managerAddr,
39291
+ chainId
39292
+ });
39293
+ const stored = {
39294
+ safe: checksum4(account2.safe),
39295
+ owner: checksum4(account2.owner),
39296
+ permissionSigner: checksum4(account2.permissionSigner),
39297
+ manager: checksum4(account2.manager),
39298
+ chainId: account2.chainId,
39299
+ createdAtBlock: account2.createdAtBlock.toString()
39300
+ };
39301
+ upsertAccountInList(stored);
39302
+ writeJsonFile(sailPath("account.json"), stored);
39303
+ console.log(`
39304
+ SMA created. Address: ${stored.safe}`);
39305
+ console.log("Saved to .sail/account.json");
39306
+ } catch (err) {
39307
+ if (err.message === "not implemented") {
39308
+ console.log(
39309
+ "\nOn-chain account creation is not wired up in this build yet \u2014\nclient.account.create is a stub until SailKernel is deployed and the\nSDK is connected. Nothing was created on-chain."
39310
+ );
39311
+ return;
39312
+ }
39313
+ throw err;
39364
39314
  }
39365
- writeIfMissing((0, import_node_path3.join)(root, "foundry.toml"), FOUNDRY_TOML);
39366
- writeIfMissing(
39367
- (0, import_node_path3.join)(root, ".sail", "contracts", "interfaces", "IPermission.sol"),
39368
- IPERMISSION_SOL
39369
- );
39370
- writeIfMissing((0, import_node_path3.join)(root, "mandates", "BoundedCallPermission.sol"), EXAMPLE_MANDATE_SOL);
39371
- writeIfMissing((0, import_node_path3.join)(root, "mandates", "README.md"), MANDATES_README);
39372
39315
  }
39373
- function writeIfMissing(path8, content) {
39374
- if (!(0, import_node_fs4.existsSync)(path8)) (0, import_node_fs4.writeFileSync)(path8, content, "utf8");
39316
+ var SAIL_MAINNET_CHAINS = [1, 8453, 42161, 130];
39317
+ async function fetchProxyCreationCode(preferredChainId) {
39318
+ const rpcUrl = getRpcUrl(preferredChainId) ?? void 0;
39319
+ const publicClient = createPublicClient({
39320
+ chain: getChainById(preferredChainId),
39321
+ transport: http(rpcUrl)
39322
+ });
39323
+ return await publicClient.readContract({
39324
+ address: SAFE_V141.proxyFactory,
39325
+ abi: safeProxyFactoryAbi,
39326
+ functionName: "proxyCreationCode"
39327
+ });
39375
39328
  }
39376
-
39377
- // src/lib/packagePaths.ts
39378
- var import_node_fs5 = __toESM(require("node:fs"), 1);
39379
- var import_node_path4 = __toESM(require("node:path"), 1);
39380
- function cliDistDir() {
39381
- try {
39382
- return import_node_path4.default.dirname(import_node_fs5.default.realpathSync(process.argv[1]));
39383
- } catch {
39384
- return import_node_path4.default.dirname(import_node_path4.default.resolve(process.argv[1]));
39329
+ async function accountPredict(options) {
39330
+ const stored = readJsonFile(sailPath("account.json"));
39331
+ let ownerAddr;
39332
+ if (options.owner) {
39333
+ if (!isAddress(options.owner, { strict: false })) {
39334
+ throw new Error(`Invalid --owner address: ${options.owner}`);
39335
+ }
39336
+ ownerAddr = getAddress(options.owner);
39337
+ } else {
39338
+ if (!stored?.owner) {
39339
+ throw new Error(
39340
+ "No owner found in .sail/account.json. Pass --owner <address> or run sailor onboard first."
39341
+ );
39342
+ }
39343
+ ownerAddr = getAddress(stored.owner);
39385
39344
  }
39386
- }
39387
- function packageRoot() {
39388
- let dir = cliDistDir();
39389
- let firstBinMatch = null;
39390
- for (let depth = 0; depth < 6; depth++) {
39391
- const pkgFile = import_node_path4.default.join(dir, "package.json");
39392
- if (import_node_fs5.default.existsSync(pkgFile)) {
39393
- try {
39394
- const pkg = JSON.parse(import_node_fs5.default.readFileSync(pkgFile, "utf-8"));
39395
- if (pkg.bin?.sailor) {
39396
- if (firstBinMatch === null) firstBinMatch = dir;
39397
- if (import_node_fs5.default.existsSync(import_node_path4.default.join(dir, "templates"))) return dir;
39398
- }
39399
- } catch {
39400
- }
39345
+ let managerAddr;
39346
+ if (options.manager) {
39347
+ if (!isAddress(options.manager, { strict: false })) {
39348
+ throw new Error(`Invalid --manager address: ${options.manager}`);
39401
39349
  }
39402
- const parent = import_node_path4.default.dirname(dir);
39403
- if (parent === dir) break;
39404
- dir = parent;
39350
+ managerAddr = getAddress(options.manager);
39351
+ } else if (stored?.manager) {
39352
+ managerAddr = getAddress(stored.manager);
39353
+ } else {
39354
+ throw new Error(
39355
+ "The predicted address depends on the agent (manager) wallet, which is mixed into the kernel's CREATE2 salt.\nPass --manager <agent address> (create one first with `sailor keys`), or run after onboarding so it can be read from .sail/account.json."
39356
+ );
39405
39357
  }
39406
- return firstBinMatch ?? import_node_path4.default.resolve(cliDistDir(), "../../..");
39407
- }
39408
- function projectPort(projectRoot) {
39409
- const hash3 = [...projectRoot].reduce((h, c) => (h << 5) - h + c.charCodeAt(0) >>> 0, 0);
39410
- return 3333 + hash3 % 667;
39411
- }
39412
-
39413
- // src/commands/init.ts
39414
- var TEMPLATE_COPY_EXCLUDES = /* @__PURE__ */ new Set([
39415
- "node_modules",
39416
- "dist",
39417
- "out",
39418
- "cache",
39419
- "broadcast",
39420
- ".git"
39421
- ]);
39422
- function copyDirSync(src, dest) {
39423
- import_node_fs6.default.mkdirSync(dest, { recursive: true });
39424
- for (const entry of import_node_fs6.default.readdirSync(src, { withFileTypes: true })) {
39425
- if (TEMPLATE_COPY_EXCLUDES.has(entry.name)) continue;
39426
- const srcPath = import_node_path5.default.join(src, entry.name);
39427
- const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
39428
- const destPath = import_node_path5.default.join(dest, destName);
39429
- if (entry.isDirectory()) {
39430
- copyDirSync(srcPath, destPath);
39431
- } else {
39432
- import_node_fs6.default.copyFileSync(srcPath, destPath);
39358
+ if (options.salt != null && !/^\d+$/.test(options.salt)) {
39359
+ throw new Error(`Invalid --salt value: "${options.salt}" \u2014 must be a non-negative integer.`);
39360
+ }
39361
+ const saltNonce = options.salt != null ? BigInt(options.salt) : 0n;
39362
+ let chainIds;
39363
+ if (options.chain) {
39364
+ const chainId = Number(options.chain);
39365
+ if (!(chainId in sailDeployments)) {
39366
+ throw new Error(
39367
+ `Chain ${chainId} has no Sail Protocol deployment. Supported: ${Object.keys(sailDeployments).join(", ")}`
39368
+ );
39433
39369
  }
39370
+ chainIds = [chainId];
39371
+ } else {
39372
+ chainIds = SAIL_MAINNET_CHAINS;
39434
39373
  }
39374
+ const preferredChain = chainIds.find((cid) => getRpcUrl(cid) != null) ?? chainIds[0];
39375
+ const proxyCreationCode = await fetchProxyCreationCode(preferredChain);
39376
+ const results = chainIds.map((chainId) => {
39377
+ const deployment = sailDeployments[chainId];
39378
+ const viemChain = getChainById(chainId);
39379
+ const { predicted: predictedAddress } = buildSmaAddressPrediction(
39380
+ deployment,
39381
+ ownerAddr,
39382
+ managerAddr,
39383
+ saltNonce,
39384
+ proxyCreationCode
39385
+ );
39386
+ return {
39387
+ chainId,
39388
+ name: viemChain.name,
39389
+ predictedAddress,
39390
+ kernel: deployment.kernel,
39391
+ safeModuleEnabler: deployment.safeModuleEnabler
39392
+ };
39393
+ });
39394
+ const uniqueAddresses = new Set(results.map((r) => r.predictedAddress.toLowerCase()));
39395
+ const allSame = uniqueAddresses.size === 1;
39396
+ if (options.json) {
39397
+ console.log(
39398
+ JSON.stringify(
39399
+ {
39400
+ salt: saltNonce.toString(),
39401
+ owner: ownerAddr,
39402
+ manager: managerAddr,
39403
+ chains: results.map(({ chainId, name, predictedAddress }) => ({
39404
+ chainId,
39405
+ name,
39406
+ predictedAddress
39407
+ })),
39408
+ allSame,
39409
+ note: allSame ? "All chains produce the same address with this salt, owner, and manager." : "Addresses differ per chain because the kernel salt binds the chain-specific fee policy and the Safe initializer encodes chain-specific contract addresses (kernel, safeModuleEnabler). Cross-chain same-address requires deterministic protocol deployment or a registerExisting() flow."
39410
+ },
39411
+ null,
39412
+ 2
39413
+ )
39414
+ );
39415
+ return;
39416
+ }
39417
+ console.log("\nPredicted Safe addresses");
39418
+ console.log(` Owner: ${ownerAddr}`);
39419
+ console.log(` Manager: ${managerAddr}`);
39420
+ console.log(` Salt: ${saltNonce}`);
39421
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
39422
+ for (const { chainId, name, predictedAddress } of results) {
39423
+ console.log(` ${name.padEnd(14)} (${String(chainId).padEnd(5)}): ${predictedAddress}`);
39424
+ }
39425
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
39426
+ if (allSame) {
39427
+ console.log("\u2713 All chains produce the same address.");
39428
+ } else {
39429
+ console.log("\n\u26A0 Addresses differ across chains.");
39430
+ console.log(
39431
+ " Root cause: SailKernel.createAccount binds the CREATE2 salt as\n keccak256(saltNonce, deployer, permissionSigner, manager, feePolicy),\n then SafeProxyFactory derives the address from that bound salt and the\n Safe initializer. Both the fee policy and the initializer (kernel,\n safeModuleEnabler) are chain-specific, so each chain yields a different\n address even with the same owner, manager, and salt.\n\n For cross-chain same-address the Sail Protocol needs one of:\n A) Deterministic (CREATE2) deployment of kernel + safeModuleEnabler +\n fee policy so they land at the same address on every chain.\n B) A registerExisting() path allowing a plain Safe (deployed with a\n chain-agnostic initializer) to be registered with the kernel."
39432
+ );
39433
+ }
39434
+ console.log(`
39435
+ To deploy on this chain: sailor onboard --new-sma --salt ${saltNonce}`);
39435
39436
  }
39436
- var SAIL_WORKSPACE_README = `# Sailor Project Workspace
39437
-
39438
- This folder is the local workspace for one Sailor agent deployment.
39439
-
39440
- ## Layout
39441
-
39442
- - \`config.json\` is the project manifest: name, chain, and state location.
39443
- - \`keys/\` stores encrypted local signing keys. Never commit these files.
39444
- - \`runtime/\` is for local UI and signing handoff state.
39445
- - \`state/\` is for persistent agent state, audit logs, and tx history.
39446
-
39447
- AI coding agents should read the project's \`AGENTS.md\` and this folder's \`config.json\`
39448
- before changing strategy code or running commands that touch funds.
39449
- `;
39450
- function writeIfMissing2(file, content) {
39451
- if (!import_node_fs6.default.existsSync(file)) import_node_fs6.default.writeFileSync(file, content, "utf-8");
39452
- }
39453
- function scaffoldProjectWorkspace(dest, name, options) {
39454
- const chainId = options.chain ? (() => {
39455
- const n = Number(options.chain);
39456
- if (!Number.isInteger(n) || n <= 0) throw new Error(`Invalid chain id: "${options.chain}"`);
39457
- return n;
39458
- })() : null;
39459
- const sailDir2 = import_node_path5.default.join(dest, ".sail");
39460
- import_node_fs6.default.mkdirSync(import_node_path5.default.join(sailDir2, "keys"), { recursive: true });
39461
- import_node_fs6.default.mkdirSync(import_node_path5.default.join(sailDir2, "runtime"), { recursive: true });
39462
- import_node_fs6.default.mkdirSync(import_node_path5.default.join(sailDir2, "state"), { recursive: true });
39463
- import_node_fs6.default.writeFileSync(
39464
- import_node_path5.default.join(sailDir2, "config.json"),
39465
- `${JSON.stringify(
39466
- {
39467
- version: 1,
39468
- name,
39469
- chainId,
39470
- // null = chain not yet chosen; Stage 1 will set this
39471
- stateDir: ".sail/state",
39472
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
39473
- contracts: {
39474
- kernel: "",
39475
- mandateFactory: ""
39476
- }
39477
- },
39478
- null,
39479
- 2
39480
- )}
39481
- `,
39482
- "utf-8"
39483
- );
39484
- writeIfMissing2(import_node_path5.default.join(sailDir2, "README.md"), SAIL_WORKSPACE_README);
39485
- const chainIdLine = chainId != null ? `CHAIN_ID=${chainId}
39486
- ` : `# CHAIN_ID=8453 # set after choosing your chain in Stage 1
39487
- `;
39488
- import_node_fs6.default.writeFileSync(
39489
- import_node_path5.default.join(dest, ".env.example"),
39490
- `# Sailor agent environment
39491
- RPC_URL=https://your-rpc-endpoint
39492
- ${chainIdLine}
39493
- # Optional for non-interactive runs
39494
- # SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
39495
- `,
39496
- "utf-8"
39497
- );
39498
- const rpcLine = options.rpcUrl ? `RPC_URL=${options.rpcUrl}` : `# Paste your RPC endpoint here (Alchemy, Infura, or any HTTPS endpoint)
39499
- # RPC_URL=https://your-rpc-endpoint`;
39500
- const chainLine = chainId != null ? `
39501
- CHAIN_ID=${chainId}` : ``;
39502
- writeIfMissing2(
39503
- import_node_path5.default.join(sailDir2, ".env.local"),
39504
- `${rpcLine}${chainLine}
39505
-
39506
- # Optional for non-interactive runs (CI, GitHub Actions, launchd, systemd)
39507
- # SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
39508
- `
39509
- );
39510
- }
39511
- async function initCommand(dir, options = {}) {
39512
- const inPlace = !dir || dir === ".";
39513
- const dest = inPlace ? process.cwd() : import_node_path5.default.resolve(process.cwd(), dir);
39514
- const name = import_node_path5.default.basename(dest);
39515
- const templatesDir = import_node_path5.default.join(packageRoot(), "templates");
39516
- const templateName = options.template ?? "default";
39517
- if (/[/\\.]/.test(templateName) || templateName.includes("..")) {
39518
- throw new Error(`Invalid template name: "${templateName}"`);
39437
+ async function accountDeployChain(options) {
39438
+ const json = !!options.json;
39439
+ const say = (fn) => {
39440
+ if (!json) fn();
39441
+ };
39442
+ const stored = readJsonFile(sailPath("account.json"));
39443
+ if (!stored?.safe || !stored?.owner || !stored?.manager) {
39444
+ throw new Error(
39445
+ "No SMA found in .sail/account.json. Run `sailor onboard --new-sma` first."
39446
+ );
39519
39447
  }
39520
- const templateSrc = import_node_path5.default.join(templatesDir, templateName);
39521
- const availableTemplates = () => import_node_fs6.default.existsSync(templatesDir) ? import_node_fs6.default.readdirSync(templatesDir).filter((e) => import_node_fs6.default.existsSync(import_node_path5.default.join(templatesDir, e, "package.json"))).join(", ") || "none" : "none";
39522
- if (!import_node_fs6.default.existsSync(templateSrc) || !import_node_fs6.default.existsSync(import_node_path5.default.join(templateSrc, "package.json"))) {
39523
- const available = availableTemplates();
39524
- const hint = available === "none" ? `
39525
- No templates found under ${templatesDir}.
39526
- If you're running the in-tree CLI bundle from a monorepo checkout, the scaffolder
39527
- couldn't locate the repo's templates/ directory. Install the published package, or
39528
- run from the repo root.` : ` Available: ${available}`;
39529
- throw new Error(`Template "${templateName}" not found.${hint}`);
39448
+ if (stored.saltNonce == null && options.salt == null) {
39449
+ throw new Error(
39450
+ "No saltNonce stored in .sail/account.json.\nPass --salt <n> explicitly, or re-deploy your SMA with `sailor onboard --new-sma --salt <n>`\nso the salt is recorded for cross-chain use."
39451
+ );
39530
39452
  }
39531
- const cwd = process.cwd();
39532
- if (!inPlace && !dest.startsWith(cwd + import_node_path5.default.sep) && dest !== cwd) {
39533
- throw new Error(`Directory must be inside the current working directory`);
39453
+ if (options.salt != null && !/^\d+$/.test(options.salt)) {
39454
+ throw new Error(`Invalid --salt value: "${options.salt}" \u2014 must be a non-negative integer.`);
39534
39455
  }
39535
- if (!inPlace && import_node_fs6.default.existsSync(dest)) {
39536
- throw new Error(`Directory already exists: ${dest}`);
39456
+ const ownerAddr = getAddress(stored.owner);
39457
+ const managerAddr = getAddress(stored.manager);
39458
+ const storedSafe = getAddress(stored.safe);
39459
+ const saltNonce = options.salt != null ? BigInt(options.salt) : BigInt(stored.saltNonce);
39460
+ if (!/^\d+$/.test(options.chain)) {
39461
+ throw new Error(`Invalid --chain value: "${options.chain}" \u2014 must be a numeric chain ID.`);
39537
39462
  }
39538
- if (inPlace && import_node_fs6.default.existsSync(import_node_path5.default.join(dest, ".sail", "config.json"))) {
39539
- throw new Error(`Already initialized \u2014 .sail/config.json exists`);
39463
+ const targetChainId = Number(options.chain);
39464
+ if (!(targetChainId in sailDeployments)) {
39465
+ throw new Error(
39466
+ `Chain ${targetChainId} has no Sail Protocol deployment.
39467
+ Supported: ${Object.keys(sailDeployments).join(", ")}`
39468
+ );
39469
+ }
39470
+ if (targetChainId === stored.chainId) {
39471
+ throw new Error(
39472
+ `Chain ${targetChainId} is already the primary chain for this SMA.
39473
+ Use a different chain ID.`
39474
+ );
39475
+ }
39476
+ const allChainIds = Object.keys(sailDeployments).map(Number);
39477
+ const rpcPreferred = allChainIds.find((cid) => getRpcUrl(cid) != null);
39478
+ if (rpcPreferred == null) {
39479
+ throw new Error(
39480
+ "No RPC URL configured for any supported chain.\nSet RPC_URL or RPC_URL_<CHAIN_ID> in .sail/.env.local."
39481
+ );
39482
+ }
39483
+ const proxyCreationCode = await fetchProxyCreationCode(rpcPreferred);
39484
+ const deployment = sailDeployments[targetChainId];
39485
+ const { initializer, predicted } = buildSmaAddressPrediction(
39486
+ deployment,
39487
+ ownerAddr,
39488
+ managerAddr,
39489
+ saltNonce,
39490
+ proxyCreationCode
39491
+ );
39492
+ if (predicted.toLowerCase() !== storedSafe.toLowerCase()) {
39493
+ const msg = `Your existing SMA (${storedSafe}) cannot be reproduced at the same address on
39494
+ chain ${targetChainId}. Predicted address: ${predicted}.
39495
+
39496
+ Two possible causes:
39497
+ a) Wrong --salt value. The stored deployment salt is ${stored.saltNonce ?? "unknown"}. Re-run without --salt to use it automatically.
39498
+ b) SMA was deployed against the old per-chain contracts (pre-deterministic
39499
+ kernel deployment). The current contracts are identical across all chains.
39500
+
39501
+ If it is (b), deploy a new SMA with the current contracts:
39502
+ sailor onboard --new-sma --salt ${stored.saltNonce ?? saltNonce}
39503
+ Then run deploy-chain from that account.`;
39504
+ if (json) {
39505
+ console.log(
39506
+ JSON.stringify(
39507
+ {
39508
+ status: "error",
39509
+ error: "old-contracts",
39510
+ stored: storedSafe,
39511
+ predicted,
39512
+ targetChainId,
39513
+ message: msg
39514
+ },
39515
+ null,
39516
+ 2
39517
+ )
39518
+ );
39519
+ process.exit(1);
39520
+ }
39521
+ throw new Error(msg);
39540
39522
  }
39541
- copyDirSync(templateSrc, dest);
39542
- const pkgRoot = packageRoot();
39543
- const examplesPermSrc = import_node_path5.default.join(pkgRoot, "examples", "permissions");
39544
- if (import_node_fs6.default.existsSync(examplesPermSrc)) {
39545
- copyDirSync(examplesPermSrc, import_node_path5.default.join(dest, "examples", "permissions"));
39546
- }
39547
- const permModelSrc = import_node_path5.default.join(pkgRoot, "docs", "PERMISSION_MODEL.md");
39548
- if (import_node_fs6.default.existsSync(permModelSrc)) {
39549
- import_node_fs6.default.mkdirSync(import_node_path5.default.join(dest, "docs"), { recursive: true });
39550
- writeIfMissing2(import_node_path5.default.join(dest, "docs", "PERMISSION_MODEL.md"), import_node_fs6.default.readFileSync(permModelSrc, "utf-8"));
39551
- }
39552
- const pkgPath = import_node_path5.default.join(dest, "package.json");
39553
- if (import_node_fs6.default.existsSync(pkgPath)) {
39554
- const pkg = JSON.parse(import_node_fs6.default.readFileSync(pkgPath, "utf-8"));
39555
- pkg.name = name;
39556
- const deps = pkg.dependencies ?? {};
39557
- if (deps["@sail/sdk"] === "workspace:*") {
39558
- const sdkPath = import_node_path5.default.join(pkgRoot, "packages", "sdk");
39559
- deps["@sail/sdk"] = import_node_fs6.default.existsSync(sdkPath) ? `file:${sdkPath}` : (
39560
- // Fallback: SDK not bundled — user must install it manually.
39561
- "0.1.0"
39523
+ const targetClient = createPublicClient({
39524
+ chain: getChainById(targetChainId),
39525
+ transport: http(getRpcUrl(targetChainId) ?? void 0)
39526
+ });
39527
+ const alreadyRecorded = (stored.deployedChains ?? []).includes(targetChainId);
39528
+ if (alreadyRecorded) {
39529
+ say(() => console.log(`
39530
+ Chain ${targetChainId} is already recorded as deployed \u2014 verifying on-chain\u2026`));
39531
+ }
39532
+ const existingCode = await targetClient.getCode({ address: predicted });
39533
+ if (existingCode && existingCode !== "0x") {
39534
+ say(() => console.log(`SMA confirmed at ${predicted} on chain ${targetChainId}.`));
39535
+ if (!alreadyRecorded) recordDeployedChain(stored, targetChainId);
39536
+ if (json) {
39537
+ console.log(
39538
+ JSON.stringify({ status: "ok", alreadyDeployed: true, address: predicted, chainId: targetChainId }, null, 2)
39562
39539
  );
39563
39540
  }
39564
- pkg.dependencies = deps;
39565
- import_node_fs6.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
39566
- `);
39541
+ return;
39567
39542
  }
39568
- scaffoldProjectWorkspace(dest, name, options);
39569
- scaffoldFoundryWorkspace(dest);
39570
- printWelcome(
39571
- dest,
39572
- name,
39573
- inPlace,
39574
- !!options.rpcUrl,
39575
- /* freshInit */
39576
- true
39543
+ say(
39544
+ () => console.log(
39545
+ `
39546
+ Deploying SMA on ${getChainById(targetChainId).name} (chain ${targetChainId})\u2026`
39547
+ )
39577
39548
  );
39578
- }
39579
- function chainLabel(chainId) {
39580
- const labels = {
39581
- 8453: "Base",
39582
- 42161: "Arbitrum",
39583
- 84532: "Base Sepolia",
39584
- 130: "Unichain"
39585
- };
39586
- return labels[chainId] ?? `Chain ${chainId}`;
39587
- }
39588
- function detectState(dest) {
39549
+ say(() => console.log(` Predicted address: ${predicted}`));
39550
+ const createAccountData = encodeFunctionData({
39551
+ abi: SailKernelAbi,
39552
+ functionName: "createAccount",
39553
+ args: [
39554
+ SAFE_V141.proxyFactory,
39555
+ SAFE_V141.singletonL2,
39556
+ initializer,
39557
+ saltNonce,
39558
+ ownerAddr,
39559
+ // permissionSigner = owner (same as original deployment)
39560
+ managerAddr,
39561
+ // manager = agent wallet
39562
+ deployment.standardFeePolicy,
39563
+ zeroAddress
39564
+ // feeAsset (native)
39565
+ ]
39566
+ });
39567
+ const channel = await createSigningChannel(process.cwd());
39589
39568
  try {
39590
- const configRaw = import_node_fs6.default.readFileSync(import_node_path5.default.join(dest, ".sail", "config.json"), "utf-8");
39591
- const config = JSON.parse(configRaw);
39592
- const projectName = config.name ?? import_node_path5.default.basename(dest);
39593
- const accountPath = import_node_path5.default.join(dest, ".sail", "account.json");
39594
- if (!import_node_fs6.default.existsSync(accountPath)) {
39595
- return { kind: "B", projectName, chain: chainLabel(config.chainId ?? 0) };
39569
+ await channel.start();
39570
+ const stationUrl = `http://localhost:${projectPort(process.cwd())}/#/station`;
39571
+ if (json) {
39572
+ console.log(
39573
+ JSON.stringify(
39574
+ { status: "waiting_for_signature", url: stationUrl, chainId: targetChainId },
39575
+ null,
39576
+ 2
39577
+ )
39578
+ );
39579
+ } else {
39580
+ console.log(
39581
+ `
39582
+ \u2192 Open the Sailor dashboard and switch your wallet to ${getChainById(targetChainId).name}:
39583
+ ${stationUrl}
39584
+ `
39585
+ );
39596
39586
  }
39597
- const accountRaw = import_node_fs6.default.readFileSync(accountPath, "utf-8");
39598
- const account2 = JSON.parse(accountRaw);
39599
- const sma = account2.safe ?? "";
39600
- const chain2 = chainLabel(account2.chainId ?? config.chainId ?? 0);
39601
- let permissionCount = 0;
39602
- try {
39603
- const mandatesRaw = import_node_fs6.default.readFileSync(
39604
- import_node_path5.default.join(dest, ".sail", "state", "mandates.json"),
39605
- "utf-8"
39587
+ say(() => console.log("Pushing signing request\u2026"));
39588
+ const response = await channel.requestSignature({
39589
+ type: "transaction",
39590
+ kind: "create-sma",
39591
+ title: `Deploy SMA on ${getChainById(targetChainId).name}`,
39592
+ description: `Deploy the same SMA at ${predicted} on ${getChainById(targetChainId).name}. Switch your wallet to chain ${targetChainId} before signing.`,
39593
+ chainId: targetChainId,
39594
+ to: deployment.kernel,
39595
+ data: createAccountData,
39596
+ details: [
39597
+ { label: "Owner", value: ownerAddr },
39598
+ { label: "Agent wallet", value: managerAddr },
39599
+ { label: "Predicted address", value: predicted },
39600
+ { label: "Fee policy", value: deployment.standardFeePolicy },
39601
+ { label: "Salt", value: saltNonce.toString() }
39602
+ ]
39603
+ });
39604
+ if (response.status === "rejected") {
39605
+ throw new Error(`User rejected deployment: ${response.reason ?? "no reason given"}`);
39606
+ }
39607
+ if (response.status !== "signed") {
39608
+ throw new Error("Unexpected response from signing UI");
39609
+ }
39610
+ say(() => console.log("Waiting for transaction confirmation\u2026"));
39611
+ const receipt = await targetClient.waitForTransactionReceipt({ hash: response.txHash });
39612
+ const logs = parseEventLogs({ abi: SailKernelAbi, logs: receipt.logs });
39613
+ const registered = logs.find(
39614
+ (l) => l.eventName === "AccountRegistered"
39615
+ );
39616
+ if (!registered) {
39617
+ throw new Error(
39618
+ `AccountRegistered event not found in receipt (tx ${response.txHash}) \u2014 transaction may have failed or was sent to the wrong contract.`
39606
39619
  );
39607
- const mandates = JSON.parse(mandatesRaw);
39608
- permissionCount = Array.isArray(mandates.mandates) ? mandates.mandates.length : 0;
39609
- } catch {
39610
39620
  }
39611
- if (permissionCount > 0) {
39612
- return { kind: "D", projectName, chain: chain2, sma, permissionCount };
39621
+ const deployedAddress = registered.args.account;
39622
+ if (deployedAddress.toLowerCase() !== predicted.toLowerCase()) {
39623
+ throw new Error(
39624
+ `Deployed address mismatch: predicted ${predicted}, got ${deployedAddress}.
39625
+ Please report this as a bug \u2014 this should not happen with deterministic contracts.`
39626
+ );
39613
39627
  }
39614
- return { kind: "C", projectName, chain: chain2, sma };
39615
- } catch {
39616
- return { kind: "A" };
39628
+ recordDeployedChain(stored, targetChainId);
39629
+ appendActivity({
39630
+ ts: nowIso(),
39631
+ actor: "owner",
39632
+ type: "sma_deployed_chain",
39633
+ sma: deployedAddress,
39634
+ owner: ownerAddr,
39635
+ manager: managerAddr,
39636
+ txHash: response.txHash,
39637
+ chainId: targetChainId,
39638
+ saltNonce: saltNonce.toString()
39639
+ });
39640
+ say(() => {
39641
+ console.log(`
39642
+ ${"\u2500".repeat(56)}`);
39643
+ console.log("\u2713 SMA deployed on additional chain!");
39644
+ console.log(` Address: ${deployedAddress}`);
39645
+ console.log(` Chain: ${getChainById(targetChainId).name} (${targetChainId})`);
39646
+ console.log(` Tx: ${response.txHash}`);
39647
+ console.log("\u2500".repeat(56));
39648
+ });
39649
+ if (json) {
39650
+ console.log(
39651
+ JSON.stringify(
39652
+ {
39653
+ status: "ok",
39654
+ address: deployedAddress,
39655
+ chainId: targetChainId,
39656
+ txHash: response.txHash
39657
+ },
39658
+ null,
39659
+ 2
39660
+ )
39661
+ );
39662
+ }
39663
+ } finally {
39664
+ channel.stop();
39617
39665
  }
39618
39666
  }
39619
- function printWelcome(dest, name, inPlace, hasRpc, freshInit = false) {
39620
- const state = freshInit ? { kind: "A" } : detectState(dest);
39621
- if (state.kind === "B") {
39622
- console.log("\nWelcome back.\n");
39623
- console.log(`Project: ${state.projectName} | Network: ${state.chain}`);
39624
- console.log("Status: SMA not yet deployed.\n");
39625
- console.log("Next:");
39626
- console.log(" sailor ui start");
39627
- console.log(" Connect your wallet and deploy your SMA in the browser.\n");
39628
- console.log('Or open this folder in your AI coding assistant and say: "continue"');
39629
- return;
39630
- }
39631
- if (state.kind === "C") {
39632
- console.log("\nWelcome back.\n");
39633
- console.log(`Project: ${state.projectName}`);
39634
- console.log(`SMA: ${state.sma} on ${state.chain}`);
39635
- console.log(
39636
- "Permissions: none registered yet \u2014 your agent has no mandate to execute against.\n"
39637
- );
39638
- console.log("Next:");
39639
- console.log(
39640
- " Write your permission contract in mandates/ (start from BoundedCallPermission.sol)"
39641
- );
39642
- console.log(" forge build");
39643
- console.log(` sailor mandate deploy --contract <Name> --attach --sma ${state.sma}
39644
- `);
39645
- console.log('Or open this folder in your AI coding assistant and say: "continue"');
39646
- return;
39647
- }
39648
- if (state.kind === "D") {
39649
- console.log("\nWelcome back.\n");
39650
- console.log(`Project: ${state.projectName}`);
39651
- console.log(`SMA: ${state.sma} on ${state.chain}`);
39652
- console.log(`Permissions: ${state.permissionCount} registered
39653
- `);
39654
- console.log('Open this folder in your AI coding assistant and say: "continue"');
39655
- return;
39667
+ function buildSmaAddressPrediction(deployment, ownerAddr, managerAddr, saltNonce, proxyCreationCode) {
39668
+ const initializer = buildSafeSetupInitializer({
39669
+ owners: [ownerAddr],
39670
+ threshold: 1n,
39671
+ kernel: deployment.kernel,
39672
+ safeModuleEnabler: deployment.safeModuleEnabler
39673
+ });
39674
+ const predicted = computeSailSmaAddress({
39675
+ initializer,
39676
+ saltNonce,
39677
+ deployer: ownerAddr,
39678
+ permissionSigner: ownerAddr,
39679
+ manager: managerAddr,
39680
+ feePolicy: deployment.standardFeePolicy,
39681
+ proxyCreationCode
39682
+ });
39683
+ return { initializer, predicted };
39684
+ }
39685
+ function recordDeployedChain(stored, chainId) {
39686
+ const existing = Array.from(/* @__PURE__ */ new Set([stored.chainId, ...stored.deployedChains ?? []]));
39687
+ if (!existing.includes(chainId)) {
39688
+ existing.push(chainId);
39689
+ existing.sort((a, b) => a - b);
39656
39690
  }
39657
- if (!inPlace) console.log(`
39658
- Created ${name}/`);
39659
- console.log("\nYour Sail agent project is ready. Open your AI coding assistant in this folder and say start.");
39691
+ const updated = { ...stored, deployedChains: existing };
39692
+ upsertAccountInList(updated);
39693
+ writeJsonFile(sailPath("account.json"), updated);
39660
39694
  }
39661
39695
 
39662
- // src/commands/keys.ts
39663
- var import_node_fs7 = __toESM(require("node:fs"), 1);
39664
- var import_node_path6 = __toESM(require("node:path"), 1);
39665
- async function keysGenerate() {
39666
- const roleInput = await prompt("Which key? (agent wallet / mandate signer)", "agent wallet");
39667
- const role = normalizeRole(roleInput);
39668
- if (!role) {
39669
- throw new Error(`Unknown key role: "${roleInput}". Choose "agent wallet" or "mandate signer".`);
39696
+ // src/lib/output.ts
39697
+ function emit(json, human, payload) {
39698
+ if (json) {
39699
+ console.log(JSON.stringify(payload));
39700
+ } else {
39701
+ human();
39670
39702
  }
39671
- if (fileExists(keyPath(role))) {
39672
- const overwrite = await confirm(
39673
- `A ${roleLabel(role)} key already exists at .sail/keys/${role}.json. Overwrite it?`
39674
- );
39675
- if (!overwrite) {
39676
- console.log("Aborted \u2014 existing key left untouched.");
39677
- return;
39703
+ }
39704
+
39705
+ // src/lib/project.ts
39706
+ init_esm2();
39707
+ function nonEmpty(value) {
39708
+ return typeof value === "string" && value.trim().length > 0;
39709
+ }
39710
+ var ProjectContext = class {
39711
+ config;
39712
+ chainId;
39713
+ deployment;
39714
+ contracts;
39715
+ constructor() {
39716
+ const cfg = readJsonFile(sailPath("config.json"));
39717
+ if (!cfg) {
39718
+ throw new Error('No Sailor project found here. Run "sailor init" first.');
39678
39719
  }
39720
+ this.config = cfg;
39721
+ this.chainId = cfg.chainId ?? 8453;
39722
+ this.deployment = getSailDeployment(this.chainId);
39723
+ const overrides = cfg.contracts ?? {};
39724
+ this.contracts = {
39725
+ chainId: this.chainId,
39726
+ kernel: getAddress(nonEmpty(overrides.kernel) ? overrides.kernel : this.deployment.kernel),
39727
+ governance: getAddress(
39728
+ nonEmpty(overrides.governance) ? overrides.governance : this.deployment.governance
39729
+ ),
39730
+ standardFeePolicy: getAddress(
39731
+ nonEmpty(overrides.standardFeePolicy) ? overrides.standardFeePolicy : this.deployment.standardFeePolicy
39732
+ ),
39733
+ safeModuleEnabler: getAddress(
39734
+ nonEmpty(overrides.safeModuleEnabler) ? overrides.safeModuleEnabler : this.deployment.safeModuleEnabler
39735
+ ),
39736
+ // Accept both override names: mandateFactory (new) and permissionFactory (legacy).
39737
+ mandateFactory: getAddress(
39738
+ nonEmpty(overrides.mandateFactory) ? overrides.mandateFactory : nonEmpty(overrides.permissionFactory) ? overrides.permissionFactory : this.deployment.mandateFactory
39739
+ )
39740
+ };
39679
39741
  }
39680
- const password = await promptHidden("Set a password to encrypt the key");
39681
- if (password.length < 8) {
39682
- throw new Error("Password must be at least 8 characters.");
39742
+ static exists() {
39743
+ return fileExists(sailPath("config.json"));
39683
39744
  }
39684
- const confirmation = await promptHidden("Confirm password");
39685
- if (password !== confirmation) {
39686
- throw new Error("Passwords do not match.");
39745
+ get name() {
39746
+ return this.config.name ?? "sailor-agent";
39687
39747
  }
39688
- const keyring = LocalKeyring.generate();
39689
- const keystore = await keyring.exportKeystore(password);
39690
- writeJsonFile(keyPath(role), keystore);
39691
- const label = role === "manager" ? "Agent wallet" : "Mandate signer";
39692
- console.log(`
39693
- ${label} key saved. Address: ${checksum4(keyring.address)}`);
39694
- console.log(`Encrypted keystore written to .sail/keys/${role}.json`);
39695
- if (role === "manager") {
39696
- const save = await confirm(
39697
- "\nSave passphrase to .sail/.env.local for non-interactive use? (required for CI/GitHub Actions)"
39698
- );
39699
- if (save) {
39700
- const envPath = sailPath(".env.local");
39701
- let content = "";
39702
- if (import_node_fs7.default.existsSync(envPath)) {
39703
- content = import_node_fs7.default.readFileSync(envPath, "utf-8");
39704
- content = content.replace(/^SAIL_PASSPHRASE=.*\n?/m, "");
39705
- }
39706
- content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `SAIL_PASSPHRASE=${password}
39707
- `;
39708
- import_node_fs7.default.mkdirSync(import_node_path6.default.dirname(envPath), { recursive: true });
39709
- import_node_fs7.default.writeFileSync(envPath, content, { mode: 384 });
39710
- console.log("\u2713 SAIL_PASSPHRASE saved to .sail/.env.local (mode 0600)");
39711
- console.log(" sailor run will now work non-interactively.");
39712
- } else {
39713
- console.log("\nTo run non-interactively, add this to .sail/.env.local:");
39714
- console.log(` SAIL_PASSPHRASE=<your-passphrase>`);
39748
+ // ── Owner persistence (.sail/state/owner.json) ──────────────────────────────
39749
+ getOwner() {
39750
+ const state = readJsonFile(sailPath("state", "owner.json"));
39751
+ if (state?.owner) return getAddress(state.owner);
39752
+ const account2 = readJsonFile(sailPath("account.json"));
39753
+ return account2?.owner ? getAddress(account2.owner) : null;
39754
+ }
39755
+ setOwner(owner2) {
39756
+ writeJsonFile(sailPath("state", "owner.json"), {
39757
+ owner: getAddress(owner2),
39758
+ chainId: this.chainId,
39759
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
39760
+ });
39761
+ }
39762
+ };
39763
+ async function loadManagerSigner2() {
39764
+ if (!process.env.SAIL_PASSPHRASE) {
39765
+ try {
39766
+ const env = parseEnvFile(sailPath(".env.local"));
39767
+ if (env.SAIL_PASSPHRASE) process.env.SAIL_PASSPHRASE = env.SAIL_PASSPHRASE;
39768
+ } catch {
39715
39769
  }
39716
39770
  }
39717
- }
39718
- async function keysExportCi() {
39719
- const account2 = readJsonFile(sailPath("account.json"));
39720
- const src = resolveKeyPath("manager", account2?.safe);
39721
- if (!fileExists(src)) {
39722
- throw new Error(
39723
- 'No agent wallet keystore found.\nComplete Stage 1 (browser UI) to generate your agent wallet, or run\n"sailor keys generate" and choose "agent wallet" to create one manually.'
39724
- );
39725
- }
39726
- const dest = import_node_path6.default.resolve(process.cwd(), "ci-keystore.json");
39727
- import_node_fs7.default.copyFileSync(src, dest);
39728
- console.log(`\u2713 Keystore copied to ci-keystore.json`);
39729
- console.log(` Source: ${src}`);
39730
- const gitignorePath = import_node_path6.default.resolve(process.cwd(), ".gitignore");
39731
- if (import_node_fs7.default.existsSync(gitignorePath)) {
39732
- const content = import_node_fs7.default.readFileSync(gitignorePath, "utf-8");
39733
- if (!content.includes("ci-keystore.json")) {
39734
- import_node_fs7.default.appendFileSync(
39735
- gitignorePath,
39736
- "\n# CI keystore \u2014 encrypted agent wallet, safe to commit\n!ci-keystore.json\n"
39737
- );
39738
- console.log("\u2713 Added !ci-keystore.json allowlist entry to .gitignore");
39739
- } else {
39740
- console.log("\u2713 .gitignore already tracks ci-keystore.json");
39771
+ const passphrase = process.env.SAIL_PASSPHRASE;
39772
+ if (passphrase) {
39773
+ const keystore = readJsonFile(keyPath("manager"));
39774
+ if (!keystore) {
39775
+ throw new Error('No manager key found.\nRun "sailor keys generate" and choose "manager".');
39741
39776
  }
39777
+ return LocalKeyring.fromKeystore(keystore, passphrase);
39742
39778
  }
39743
- console.log("\nNext steps:");
39744
- console.log(" 1. Add two GitHub Actions secrets (Settings \u2192 Secrets \u2192 Actions):");
39745
- console.log(" SAIL_PASSPHRASE \u2014 the passphrase that encrypts your agent wallet");
39746
- console.log(" RPC_URL \u2014 your RPC endpoint");
39747
- console.log(" 2. Commit and push ci-keystore.json:");
39748
- console.log(' git add ci-keystore.json && git commit -m "chore: add CI keystore" && git push');
39749
- console.log("\n The keystore is encrypted \u2014 the raw private key is never exposed.");
39750
- console.log(" The workflow at .github/workflows/agent-tick.yml unlocks it with SAIL_PASSPHRASE.");
39779
+ return loadKeyring("manager");
39751
39780
  }
39752
- async function keysShow() {
39753
- const present = ROLES.filter((role) => keyExists(role));
39754
- if (present.length === 0) {
39755
- console.log("No keys found in .sail/keys/.");
39756
- console.log('Run "sailor keys generate" to create one.');
39757
- return;
39781
+
39782
+ // src/commands/capabilities.ts
39783
+ function chainName(chainId) {
39784
+ try {
39785
+ return getChainById(chainId).name;
39786
+ } catch {
39787
+ return `Chain ${chainId}`;
39758
39788
  }
39759
- console.log("Keys in .sail/keys/:\n");
39760
- for (const role of present) {
39761
- try {
39762
- const keyring = await loadKeyring(role);
39763
- console.log(` ${roleLabel(role)}: ${checksum4(keyring.address)}`);
39764
- } catch (err) {
39765
- console.log(` ${role}: ${err.message}`);
39766
- }
39789
+ }
39790
+ async function capabilities(options = {}) {
39791
+ const project = new ProjectContext();
39792
+ const chainId = project.chainId;
39793
+ const kernel = project.contracts.kernel;
39794
+ const deployment = getSailDeployment(chainId);
39795
+ const rpcUrl = getRpcUrl(chainId) ?? getChainById(chainId).rpcUrls.default.http[0];
39796
+ let dispatchModel = deployment.dispatchModel;
39797
+ let modelSource = "static-hint";
39798
+ try {
39799
+ const caps = await new SailorClient({ chainId, rpcUrl, kernel }).capabilities();
39800
+ dispatchModel = caps.dispatchModel;
39801
+ modelSource = caps.source;
39802
+ } catch {
39767
39803
  }
39804
+ const cloneTemplates = (deployment.cloneTemplates ?? []).map((t) => ({
39805
+ key: t.key,
39806
+ kind: t.kind,
39807
+ label: t.label,
39808
+ description: t.description,
39809
+ address: t.address,
39810
+ initParams: t.initParams
39811
+ }));
39812
+ const knownTemplates = (deployment.knownTemplates ?? []).map((t) => ({
39813
+ kind: t.kind,
39814
+ label: t.label,
39815
+ description: t.description,
39816
+ address: t.address
39817
+ }));
39818
+ const bareTemplates = Object.keys(deployment.standaloneTemplates ?? {}).filter(
39819
+ (k) => !cloneTemplates.some((c) => c.key === k)
39820
+ );
39821
+ const strategyPrimitives = [
39822
+ "strategy.swap \u2014 bounded swap (one-off, or looped on a schedule for DCA/rebalance)",
39823
+ "dispatch.single \u2014 a single permitted call through the kernel",
39824
+ dispatchModel === "selective" ? "dispatch.batch / dispatch.preview \u2014 multi-call (selective kernels only)" : "dispatch.batch / dispatch.preview \u2014 UNAVAILABLE on this conjunctive kernel"
39825
+ ];
39826
+ const payload = {
39827
+ chainId,
39828
+ chainName: chainName(chainId),
39829
+ supported: true,
39830
+ dispatchModel,
39831
+ dispatchModelSource: modelSource,
39832
+ contracts: {
39833
+ kernel,
39834
+ mandateFactory: project.contracts.mandateFactory
39835
+ },
39836
+ supportedChains: Object.keys(sailDeployments).map((id) => ({
39837
+ chainId: Number(id),
39838
+ name: chainName(Number(id)),
39839
+ dispatchModel: sailDeployments[id].dispatchModel
39840
+ })),
39841
+ mandateTemplates: {
39842
+ // No-Solidity, self-describing clone templates (deployAndAttach + initialize).
39843
+ cloneTemplates,
39844
+ // Pre-deployed shared permissions.
39845
+ knownTemplates,
39846
+ // Deployable clone logic without rich wizard metadata yet.
39847
+ otherStandaloneTemplates: bareTemplates
39848
+ },
39849
+ strategyPrimitives,
39850
+ customMandates: "Author a Foundry IPermission contract under mandates/ when no template fits; keep all policy parameters constructor-configured.",
39851
+ intelligence: {
39852
+ baseUrl: SAIL_INTELLIGENCE_BASE_URL,
39853
+ docsUrl: SAIL_INTELLIGENCE_DOCS_URL,
39854
+ use: "Vault screening, allocation, and rebalance advice for yield strategies."
39855
+ }
39856
+ };
39857
+ emit(
39858
+ options.json,
39859
+ () => {
39860
+ console.log("Sailor capabilities");
39861
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
39862
+ console.log(`Chain: ${payload.chainName} (${chainId})`);
39863
+ console.log(`Dispatch model: ${dispatchModel ?? "unknown"} (${modelSource})`);
39864
+ console.log(
39865
+ `Supported chains: ${payload.supportedChains.map((c) => `${c.name} [${c.dispatchModel}]`).join(", ")}`
39866
+ );
39867
+ console.log("\nNo-Solidity mandate templates on this chain:");
39868
+ if (cloneTemplates.length === 0 && knownTemplates.length === 0 && bareTemplates.length === 0) {
39869
+ console.log(" (none registered for this chain yet \u2014 author a custom mandate)");
39870
+ }
39871
+ for (const t of cloneTemplates) {
39872
+ console.log(` \u2022 ${t.label} (${t.kind}) \u2014 ${t.description ?? ""}`);
39873
+ console.log(` params: ${t.initParams.map((p) => `${p.name}: ${p.type}`).join(", ")}`);
39874
+ }
39875
+ for (const t of knownTemplates) {
39876
+ console.log(` \u2022 ${t.label} (${t.kind}, shared) \u2014 ${t.description ?? ""}`);
39877
+ }
39878
+ if (bareTemplates.length > 0) {
39879
+ console.log(` \u2022 also deployable: ${bareTemplates.join(", ")}`);
39880
+ }
39881
+ console.log("\nStrategy primitives:");
39882
+ for (const p of strategyPrimitives) console.log(` \u2022 ${p}`);
39883
+ console.log("\nCustom mandates:");
39884
+ console.log(` ${payload.customMandates}`);
39885
+ console.log("\nIntelligence API (yield/allocation advice):");
39886
+ console.log(` ${SAIL_INTELLIGENCE_BASE_URL} (docs: ${SAIL_INTELLIGENCE_DOCS_URL})`);
39887
+ console.log(
39888
+ "\nUse this to decide if a request is buildable. If it can't be expressed as a template, a strategy primitive, or a custom mandate, say so \u2014 don't scaffold a revert."
39889
+ );
39890
+ },
39891
+ payload
39892
+ );
39768
39893
  }
39769
39894
 
39770
- // src/commands/mandate-contracts.ts
39771
- var import_node_child_process = require("node:child_process");
39772
- var import_node_fs10 = require("node:fs");
39773
- var import_node_path9 = require("node:path");
39895
+ // src/commands/doctor.ts
39774
39896
  init_esm2();
39775
39897
 
39776
- // src/lib/mandates.ts
39777
- var MandateStore = class {
39778
- filePath;
39779
- constructor(filePath = sailPath("state", "mandates.json")) {
39780
- this.filePath = filePath;
39781
- }
39782
- read() {
39783
- const parsed = readJsonFile(this.filePath);
39784
- return { version: 1, mandates: parsed?.mandates ?? [] };
39785
- }
39786
- write(data) {
39787
- writeJsonFile(this.filePath, data);
39898
+ // src/lib/contract-check.ts
39899
+ async function checkContractExists(pc, address) {
39900
+ try {
39901
+ const code = await pc.getCode({ address });
39902
+ return { address, hasCode: !!code && code !== "0x" };
39903
+ } catch (err) {
39904
+ return { address, hasCode: false, error: err.message.split("\n")[0] };
39788
39905
  }
39789
- list() {
39790
- return this.read().mandates;
39906
+ }
39907
+
39908
+ // src/lib/permission-resolver.ts
39909
+ var IPERMISSION_ABI = [
39910
+ {
39911
+ type: "function",
39912
+ name: "evaluate",
39913
+ stateMutability: "view",
39914
+ inputs: [
39915
+ { name: "txData", type: "bytes" },
39916
+ {
39917
+ name: "ctx",
39918
+ type: "tuple",
39919
+ components: [
39920
+ { name: "account", type: "address" },
39921
+ { name: "manager", type: "address" },
39922
+ { name: "submitter", type: "address" },
39923
+ { name: "target", type: "address" },
39924
+ { name: "selector", type: "bytes4" },
39925
+ { name: "value", type: "uint256" },
39926
+ { name: "blockTimestamp", type: "uint256" },
39927
+ { name: "blockNumber", type: "uint256" }
39928
+ ]
39929
+ }
39930
+ ],
39931
+ outputs: [{ type: "bool" }]
39791
39932
  }
39792
- /** Find a tracked mandate by address (case-insensitive) or by exact name. */
39793
- find(addressOrName) {
39794
- const needle = addressOrName.toLowerCase();
39795
- return this.read().mandates.find(
39796
- (m) => m.address.toLowerCase() === needle || m.name === addressOrName
39797
- );
39933
+ ];
39934
+ function buildPermissionContext(params) {
39935
+ const { account: account2, manager, call: call2, blockInfo } = params;
39936
+ const selector = call2.data.length >= 10 ? call2.data.slice(0, 10) : "0x00000000";
39937
+ return {
39938
+ account: account2,
39939
+ manager,
39940
+ submitter: manager,
39941
+ // runner submits dispatches from the manager (agent) wallet
39942
+ target: call2.target,
39943
+ selector,
39944
+ value: call2.value,
39945
+ blockTimestamp: blockInfo.timestamp,
39946
+ blockNumber: blockInfo.number
39947
+ };
39948
+ }
39949
+ async function probePermissionForCall(params) {
39950
+ const { publicClient, permission, account: account2, manager, call: call2, blockInfo } = params;
39951
+ const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
39952
+ try {
39953
+ const accepted = await publicClient.readContract({
39954
+ address: permission,
39955
+ abi: IPERMISSION_ABI,
39956
+ functionName: "evaluate",
39957
+ args: [call2.data, ctx]
39958
+ });
39959
+ return { accepted: Boolean(accepted), reverted: false };
39960
+ } catch (err) {
39961
+ return { accepted: false, reverted: true, error: err.message.split("\n")[0] };
39798
39962
  }
39799
- /** Append a newly deployed mandate (replacing any prior record at the same address). */
39800
- add(mandate2) {
39801
- const data = this.read();
39802
- data.mandates = data.mandates.filter(
39803
- (m) => m.address.toLowerCase() !== mandate2.address.toLowerCase()
39804
- );
39805
- data.mandates.push(mandate2);
39806
- this.write(data);
39963
+ }
39964
+ async function resolvePermissionForCall(params) {
39965
+ const { publicClient, account: account2, manager, call: call2, registeredPermissions, blockInfo } = params;
39966
+ const ctx = buildPermissionContext({ account: account2, manager, call: call2, blockInfo });
39967
+ for (const permission of registeredPermissions) {
39968
+ try {
39969
+ const accepted = await publicClient.readContract({
39970
+ address: permission,
39971
+ abi: IPERMISSION_ABI,
39972
+ functionName: "evaluate",
39973
+ args: [call2.data, ctx]
39974
+ });
39975
+ if (accepted) return permission;
39976
+ } catch {
39977
+ }
39807
39978
  }
39808
- /** Record that a tracked mandate was attached to an SMA. */
39809
- recordAttachment(address, attachment) {
39810
- const data = this.read();
39811
- const mandate2 = data.mandates.find((m) => m.address.toLowerCase() === address.toLowerCase());
39812
- if (!mandate2) return;
39813
- mandate2.attachments ??= [];
39814
- mandate2.attachments.push({ ...attachment, at: (/* @__PURE__ */ new Date()).toISOString() });
39815
- this.write(data);
39979
+ return void 0;
39980
+ }
39981
+ async function resolvePermissionForBatch(params) {
39982
+ const { publicClient, kernel, account: account2, calls, registeredPermissions } = params;
39983
+ for (const permission of registeredPermissions) {
39984
+ try {
39985
+ const [approved] = await publicClient.readContract({
39986
+ address: kernel,
39987
+ abi: SailKernelAbi,
39988
+ functionName: "previewBatch",
39989
+ args: [account2, permission, calls]
39990
+ });
39991
+ if (approved) return permission;
39992
+ } catch {
39993
+ }
39816
39994
  }
39817
- };
39818
-
39819
- // src/signing/client.ts
39820
- var import_node_fs9 = require("node:fs");
39821
- var import_node_path8 = require("node:path");
39822
-
39823
- // src/signing/server.ts
39824
- var import_node_crypto2 = require("node:crypto");
39825
- var import_node_fs8 = require("node:fs");
39826
- var import_node_http = require("node:http");
39827
- var import_node_net = require("node:net");
39828
- var import_node_path7 = require("node:path");
39829
-
39830
- // ../../node_modules/.pnpm/ws@8.21.0_bufferutil@4.1.0_utf-8-validate@5.0.10/node_modules/ws/wrapper.mjs
39831
- var import_stream2 = __toESM(require_stream2(), 1);
39832
- var import_extension2 = __toESM(require_extension2(), 1);
39833
- var import_permessage_deflate2 = __toESM(require_permessage_deflate2(), 1);
39834
- var import_receiver2 = __toESM(require_receiver2(), 1);
39835
- var import_sender2 = __toESM(require_sender2(), 1);
39836
- var import_subprotocol2 = __toESM(require_subprotocol2(), 1);
39837
- var import_websocket2 = __toESM(require_websocket2(), 1);
39838
- var import_websocket_server2 = __toESM(require_websocket_server2(), 1);
39995
+ return void 0;
39996
+ }
39839
39997
 
39840
- // src/signing/server.ts
39841
- var DEFAULT_SIGNING_PORT = 3141;
39842
- var RUNTIME_SUBDIR = (0, import_node_path7.join)(".sail", "runtime");
39843
- var SERVER_STATE_FILE = "server.json";
39844
- var REQUEST_SECRET_HEADER = "x-sailor-secret";
39845
- var MIME = {
39846
- ".html": "text/html; charset=utf-8",
39847
- ".js": "application/javascript",
39848
- ".mjs": "application/javascript",
39849
- ".css": "text/css",
39850
- ".svg": "image/svg+xml",
39851
- ".png": "image/png",
39852
- ".ico": "image/x-icon",
39853
- ".json": "application/json",
39854
- ".woff": "font/woff",
39855
- ".woff2": "font/woff2"
39856
- };
39857
- function findUiDist() {
39858
- const candidates = [
39859
- // Installed package (any scope): walk up to package root via bin.sailor marker
39860
- (0, import_node_path7.join)(packageRoot(), "packages", "ui", "dist"),
39861
- // Monorepo dev via tsx run from the repo root
39862
- (0, import_node_path7.join)(process.cwd(), "packages", "ui", "dist"),
39863
- (0, import_node_path7.join)(process.cwd(), "..", "ui", "dist")
39864
- ];
39865
- for (const c of candidates) {
39866
- if ((0, import_node_fs8.existsSync)((0, import_node_path7.join)(c, "index.html"))) return c;
39998
+ // src/commands/doctor.ts
39999
+ var LOW_GAS_THRESHOLD_WEI = 500000000000000n;
40000
+ async function nativeBalance(pc, address) {
40001
+ const wei = await pc.getBalance({ address });
40002
+ return {
40003
+ address,
40004
+ wei: wei.toString(),
40005
+ eth: formatEther(wei),
40006
+ funded: wei > 0n,
40007
+ low: wei > 0n && wei < LOW_GAS_THRESHOLD_WEI
40008
+ };
40009
+ }
40010
+ function keystoreAddress(role, safe) {
40011
+ const ks = readJsonFile(resolveKeyPath(role, safe));
40012
+ return ks?.address ? getAddress(`0x${ks.address.replace(/^0x/, "")}`) : null;
40013
+ }
40014
+ var PROBE_TARGET = "0x000000000000000000000000000000000000dEaD";
40015
+ var PROBE_SELECTOR = "0xffffffff";
40016
+ var PROBE_DATA = "0xffffffff";
40017
+ async function probePassThrough(pc, permission, account2) {
40018
+ try {
40019
+ const ok = await pc.readContract({
40020
+ address: permission,
40021
+ abi: IPERMISSION_ABI,
40022
+ functionName: "evaluate",
40023
+ args: [
40024
+ PROBE_DATA,
40025
+ {
40026
+ account: account2,
40027
+ manager: account2,
40028
+ submitter: account2,
40029
+ target: PROBE_TARGET,
40030
+ selector: PROBE_SELECTOR,
40031
+ value: 0n,
40032
+ blockTimestamp: 0n,
40033
+ blockNumber: 0n
40034
+ }
40035
+ ]
40036
+ });
40037
+ return { permission, passesThrough: ok };
40038
+ } catch (err) {
40039
+ return {
40040
+ permission,
40041
+ passesThrough: false,
40042
+ note: `evaluate reverted (${err.message.split("\n")[0]})`
40043
+ };
39867
40044
  }
39868
- return null;
39869
40045
  }
39870
- var RESULT_LONGPOLL_MS = 25e3;
39871
- var SigningServer = class {
39872
- /** This channel owns the server in-process (see SigningChannel). */
39873
- remote = false;
39874
- projectRoot;
39875
- runtimeDir;
39876
- port;
39877
- _url = "";
39878
- pending = /* @__PURE__ */ new Map();
39879
- results = /* @__PURE__ */ new Map();
39880
- resultWaiters = /* @__PURE__ */ new Map();
39881
- clients = /* @__PURE__ */ new Set();
39882
- httpServer = null;
39883
- wss = null;
39884
- _connectedWallet;
39885
- walletListeners = [];
39886
- uiDist;
39887
- /**
39888
- * Whether to publish .sail/runtime/server.json (the daemon-discovery hint).
39889
- * The persistent daemon (`sailor station start`) advertises; ephemeral
39890
- * per-command servers do not, so they never clobber a running daemon's state
39891
- * on a discovery race. The browser UI finds servers by port-probing anyway.
39892
- */
39893
- advertise;
39894
- /** Random secret generated at startup. Required on POST /requests to prevent
39895
- * cross-origin pages from injecting signing requests. */
39896
- requestSecret = "";
39897
- constructor(opts = {}) {
39898
- this.projectRoot = opts.projectRoot ?? process.cwd();
39899
- this.runtimeDir = (0, import_node_path7.join)(this.projectRoot, RUNTIME_SUBDIR);
39900
- this.port = opts.port ?? DEFAULT_SIGNING_PORT;
39901
- this.uiDist = opts.uiDist ?? findUiDist();
39902
- this.advertise = opts.advertise ?? true;
40046
+ async function doctor(options = {}) {
40047
+ const project = new ProjectContext();
40048
+ const chainId = project.chainId;
40049
+ const kernel = project.contracts.kernel;
40050
+ const rpcUrl = getRpcUrl(chainId) ?? getChainById(chainId).rpcUrls.default.http[0];
40051
+ const client = new SailorClient({ chainId, rpcUrl, kernel });
40052
+ const pc = createPublicClient({ chain: getChainById(chainId), transport: http(rpcUrl) });
40053
+ const caps = await client.capabilities();
40054
+ const stored = readJsonFile(sailPath("account.json"));
40055
+ const safe = options.account ? getAddress(options.account) : stored?.safe ? getAddress(stored.safe) : null;
40056
+ let permissions = [];
40057
+ let checks = [];
40058
+ let permsNoCode = [];
40059
+ if (safe) {
40060
+ const mandates = await client.mandate.list(safe);
40061
+ permissions = mandates.map((m) => getAddress(m.permission));
40062
+ if (permissions.length > 0) {
40063
+ const codeChecks = await Promise.all(permissions.map((p) => checkContractExists(pc, p)));
40064
+ permsNoCode = codeChecks.filter((c) => !c.hasCode && !c.error).map((c) => c.address);
40065
+ }
40066
+ if (caps.dispatchModel === "conjunctive" && permissions.length > 0) {
40067
+ checks = await Promise.all(permissions.map((p) => probePassThrough(pc, p, safe)));
40068
+ }
39903
40069
  }
39904
- get url() {
39905
- return this._url;
40070
+ const bricking = checks.filter((c) => c.passesThrough === false);
40071
+ const healthy = safe !== null && bricking.length === 0;
40072
+ const ownerAddr = stored?.owner ? getAddress(stored.owner) : project.getOwner();
40073
+ const managerAddr = stored?.manager ? getAddress(stored.manager) : keystoreAddress("manager", stored?.safe);
40074
+ let chainIdOnChain = null;
40075
+ try {
40076
+ chainIdOnChain = await pc.getChainId();
40077
+ } catch {
39906
40078
  }
39907
- get wsUrl() {
39908
- return this._url.replace("http://", "ws://");
40079
+ const chainIdMatches = chainIdOnChain === null ? null : chainIdOnChain === chainId;
40080
+ let ownerBal = null;
40081
+ let managerBal = null;
40082
+ try {
40083
+ if (ownerAddr) ownerBal = await nativeBalance(pc, ownerAddr);
40084
+ if (managerAddr) managerBal = await nativeBalance(pc, managerAddr);
40085
+ } catch {
39909
40086
  }
39910
- get isRunning() {
39911
- return this.httpServer?.listening ?? false;
40087
+ if (options.json) {
40088
+ console.log(
40089
+ JSON.stringify(
40090
+ {
40091
+ chainId,
40092
+ kernel,
40093
+ dispatchModel: caps.dispatchModel,
40094
+ dispatchTypehash: caps.dispatchTypehash,
40095
+ capabilitySource: caps.source,
40096
+ rpc: { chainIdOnChain, chainIdMatches },
40097
+ wallet: {
40098
+ owner: ownerAddr ? { address: ownerAddr, ...ownerBal ?? {} } : null,
40099
+ manager: managerAddr ? { address: managerAddr, ...managerBal ?? {} } : null
40100
+ },
40101
+ account: safe,
40102
+ saltNonce: stored?.saltNonce ?? null,
40103
+ permissions,
40104
+ permissionsWithoutCode: permsNoCode,
40105
+ conjunctivePassThrough: caps.dispatchModel === "conjunctive" ? checks.map((c) => ({
40106
+ permission: c.permission,
40107
+ passesThrough: c.passesThrough,
40108
+ note: c.note
40109
+ })) : "n/a (selective kernel)",
40110
+ healthy
40111
+ },
40112
+ null,
40113
+ 2
40114
+ )
40115
+ );
40116
+ return;
39912
40117
  }
39913
- async start() {
39914
- this.port = await findAvailablePort(this.port);
39915
- this._url = `http://localhost:${this.port}`;
39916
- this.requestSecret = (0, import_node_crypto2.randomBytes)(16).toString("hex");
39917
- const http2 = (0, import_node_http.createServer)((req, res) => this.handleHttp(req, res));
39918
- this.wss = new import_websocket_server2.default({ server: http2 });
39919
- this.wss.on("connection", (ws, req) => {
39920
- const params = new URL(req.url ?? "/", this._url).searchParams;
39921
- if (params.get("secret") !== this.requestSecret) {
39922
- ws.close(1008, "Unauthorized");
39923
- return;
39924
- }
39925
- this.handleConnection(ws);
39926
- });
39927
- await new Promise((res, rej) => {
39928
- http2.listen(this.port, "127.0.0.1", res);
39929
- http2.once("error", rej);
39930
- });
39931
- this.httpServer = http2;
39932
- if (this.advertise) this.writeRuntimeState();
39933
- process.once("SIGINT", () => this.stop());
39934
- process.once("SIGTERM", () => this.stop());
40118
+ const multiBricking = bricking.length > 0 && permissions.length > 1;
40119
+ if (!safe) {
40120
+ console.log('\u2717 Setup incomplete \u2014 no SMA found. Run "sailor onboard --new-sma" to deploy one.');
40121
+ } else if (permissions.length === 0) {
40122
+ console.log("\u2717 Setup incomplete \u2014 no permissions registered. Your agent cannot dispatch until at least one permission is attached.");
40123
+ } else if (multiBricking) {
40124
+ console.log(`\u2717 Setup issue \u2014 ${bricking.length} of ${permissions.length} permissions block unrelated calls (see below).`);
40125
+ } else {
40126
+ console.log(
40127
+ "\u2713 Everything looks good \u2014 your SMA is deployed, your permission is registered,\n and your agent is authorized to dispatch."
40128
+ );
39935
40129
  }
39936
- stop() {
39937
- for (const [id, entry] of this.pending) {
39938
- clearTimeout(entry.timer);
39939
- this.recordResult(
39940
- { status: "rejected", requestId: id, reason: "Signing server stopped" },
39941
- entry.request
39942
- );
40130
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
40131
+ console.log(`Chain: ${chainId}`);
40132
+ console.log(`Kernel: ${kernel}`);
40133
+ console.log(` dispatch model: ${caps.dispatchModel} (detected via ${caps.source})`);
40134
+ console.log(` DISPATCH_TYPEHASH: ${caps.dispatchTypehash}`);
40135
+ console.log("\nWallet & gas:");
40136
+ if (chainIdMatches === false) {
40137
+ console.log(
40138
+ ` \u2717 RPC serves chain ${chainIdOnChain}, but the project is configured for ${chainId}. Fix RPC_URL in .sail/.env.local before doing anything.`
40139
+ );
40140
+ } else if (chainIdMatches === true) {
40141
+ console.log(` \u2713 RPC serves the configured chain (${chainId}).`);
40142
+ }
40143
+ const showBalance = (label, addr, bal) => {
40144
+ if (!addr) {
40145
+ console.log(` ${label}: not set`);
40146
+ return;
39943
40147
  }
39944
- this.pending.clear();
39945
- for (const ws of this.clients) {
39946
- try {
39947
- ws.close();
39948
- } catch {
39949
- }
40148
+ if (!bal) {
40149
+ console.log(` ${label}: ${addr} (balance unavailable)`);
40150
+ return;
39950
40151
  }
39951
- this.clients.clear();
39952
- this.wss?.close();
39953
- this.httpServer?.close();
39954
- this.httpServer = null;
39955
- if (this.advertise) this.removeRuntimeState();
40152
+ const flag = !bal.funded ? "\u2717 unfunded" : bal.low ? "\u26A0 low" : "\u2713";
40153
+ console.log(` ${label}: ${addr} ${bal.eth} ETH ${flag}`);
40154
+ };
40155
+ showBalance("owner ", ownerAddr, ownerBal);
40156
+ showBalance("manager", managerAddr, managerBal);
40157
+ if (managerBal && !managerBal.funded) {
40158
+ console.log(
40159
+ ' \u2192 The manager (agent) pays gas. Fund it before "sailor run" or dispatches fail.'
40160
+ );
39956
40161
  }
39957
- get connectedWallet() {
39958
- return this._connectedWallet;
40162
+ if (!safe) {
40163
+ console.log('\nAccount: none found. Run "sailor onboard --new-sma", or pass --account <addr>.');
40164
+ console.log("Skipping permission checks.");
40165
+ return;
39959
40166
  }
39960
- /**
39961
- * Resolves as soon as a wallet connects (or immediately if one already is).
39962
- * The CLI calls this before building calldata that needs the owner's address.
39963
- */
39964
- waitForWallet(timeoutMs = 5 * 60 * 1e3) {
39965
- if (this._connectedWallet) return Promise.resolve(this._connectedWallet);
39966
- return new Promise((res, rej) => {
39967
- const timer = setTimeout(
39968
- () => rej(new Error("Timed out waiting for wallet connection in the signing UI")),
39969
- timeoutMs
39970
- );
39971
- this.walletListeners.push((addr) => {
39972
- clearTimeout(timer);
39973
- res(addr);
40167
+ console.log(`Account: ${safe}`);
40168
+ if (stored?.saltNonce != null) {
40169
+ const saltNonce = BigInt(stored.saltNonce);
40170
+ const MAINNET_CHAINS = [1, 8453, 42161, 130];
40171
+ try {
40172
+ const proxyCreationCode = await pc.readContract({
40173
+ address: SAFE_V141.proxyFactory,
40174
+ abi: safeProxyFactoryAbi,
40175
+ functionName: "proxyCreationCode"
39974
40176
  });
39975
- });
39976
- }
39977
- /**
39978
- * Enqueue a signing request and broadcast it to the UI. Returns the full
39979
- * request (with generated id). Used by both the in-process path and the HTTP
39980
- * control plane (POST /requests).
39981
- */
39982
- enqueue(req, timeoutMs = 10 * 60 * 1e3) {
39983
- const id = `req_${Date.now()}_${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
39984
- const request = { ...req, id, createdAt: Date.now() };
39985
- const timer = setTimeout(() => {
39986
- if (this.pending.has(id)) {
39987
- this.pending.delete(id);
39988
- this.recordResult(
39989
- {
39990
- status: "rejected",
39991
- requestId: id,
39992
- reason: `timed out after ${timeoutMs / 1e3}s`
39993
- },
39994
- request
40177
+ const ownerAddr2 = stored.owner ? getAddress(stored.owner) : null;
40178
+ const managerAddr2 = stored.manager ? getAddress(stored.manager) : null;
40179
+ if (ownerAddr2 && managerAddr2) {
40180
+ console.log(
40181
+ `
40182
+ Multi-chain addresses (salt ${saltNonce}, owner ${ownerAddr2}, manager ${managerAddr2}):`
39995
40183
  );
40184
+ const CHAIN_NAMES = { 1: "Ethereum", 8453: "Base", 42161: "Arbitrum", 130: "Unichain" };
40185
+ const deployedChains = /* @__PURE__ */ new Set([
40186
+ stored.chainId,
40187
+ ...stored.deployedChains ?? []
40188
+ ]);
40189
+ const predictions = [];
40190
+ for (const cid of MAINNET_CHAINS) {
40191
+ const dep = sailDeployments[cid];
40192
+ const { predicted } = buildSmaAddressPrediction(
40193
+ dep,
40194
+ ownerAddr2,
40195
+ managerAddr2,
40196
+ saltNonce,
40197
+ proxyCreationCode
40198
+ );
40199
+ predictions.push(predicted.toLowerCase());
40200
+ const isPrimary = cid === stored.chainId && predicted.toLowerCase() === stored.safe.toLowerCase() && (safe == null || safe.toLowerCase() === stored.safe.toLowerCase());
40201
+ const isRecorded = deployedChains.has(cid);
40202
+ const label = isPrimary ? "deployed (this account)" : isRecorded ? `${predicted} \u2713 deployed (recorded)` : predicted;
40203
+ console.log(` ${CHAIN_NAMES[cid].padEnd(12)} (${cid}): ${label}`);
40204
+ }
40205
+ if (new Set(predictions).size === 1) {
40206
+ console.log(" \u2713 Same address on all chains \u2014 cross-chain SMA deployment is live.");
40207
+ } else {
40208
+ console.log(' \u26A0 Addresses differ per chain. Run "sailor account predict" for details.');
40209
+ }
39996
40210
  }
39997
- }, timeoutMs);
39998
- this.pending.set(id, { request, timer });
39999
- this.broadcast({ type: "request", request });
40000
- return request;
40211
+ } catch {
40212
+ }
40213
+ } else if (stored) {
40214
+ console.log(
40215
+ "\nMulti-chain addresses: saltNonce not stored (deployed before salt tracking)."
40216
+ );
40217
+ console.log(" To enable: re-deploy with sailor onboard --new-sma --salt 0");
40001
40218
  }
40002
- /**
40003
- * Resolve once a result for `id` is available (immediately if already
40004
- * resolved). Resolves to `null` if `timeoutMs` elapses first.
40005
- */
40006
- waitForResult(id, timeoutMs) {
40007
- const existing = this.results.get(id);
40008
- if (existing) return Promise.resolve(existing);
40009
- return new Promise((res) => {
40010
- const timer = setTimeout(() => {
40011
- this.resultWaiters.get(id)?.delete(waiter);
40012
- res(null);
40013
- }, timeoutMs);
40014
- const waiter = (r) => {
40015
- clearTimeout(timer);
40016
- res(r);
40017
- };
40018
- if (!this.resultWaiters.has(id)) this.resultWaiters.set(id, /* @__PURE__ */ new Set());
40019
- this.resultWaiters.get(id)?.add(waiter);
40020
- });
40219
+ if (permissions.length === 0) {
40220
+ console.log(
40221
+ "\n\u26A0 No permissions registered \u2014 every dispatch will be denied (NoPermissionsRegistered)."
40222
+ );
40223
+ console.log(' Register at least one with "sailor mandate attach".');
40224
+ return;
40021
40225
  }
40022
- /** Push a signing request to the UI and await the user's response (in-process). */
40023
- async requestSignature(req, timeoutMs = 10 * 60 * 1e3) {
40024
- const request = this.enqueue(req, timeoutMs);
40025
- const result = await this.waitForResult(request.id, timeoutMs + 2e3);
40026
- if (!result) {
40027
- throw new Error(`Signing request "${request.title}" timed out after ${timeoutMs / 1e3}s`);
40226
+ console.log(`
40227
+ Registered permissions (${permissions.length}):`);
40228
+ if (permsNoCode.length > 0) {
40229
+ console.log(
40230
+ `
40231
+ \u26A0 ${permsNoCode.length} registered permission(s) have NO contract code on chain ${chainId} \u2014 dispatches naming them will fail. Verify the address (wrong chain?) or revoke:`
40232
+ );
40233
+ permsNoCode.forEach((p) => console.log(` ${p}`));
40234
+ }
40235
+ if (caps.dispatchModel === "selective") {
40236
+ permissions.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
40237
+ console.log("\nEach dispatch names one permission, so pass-through is not required.");
40238
+ return;
40239
+ }
40240
+ for (let i = 0; i < checks.length; i++) {
40241
+ const c = checks[i];
40242
+ const mark = c.passesThrough ? "\u2713 pass-through" : "\u2717 NOT pass-through";
40243
+ console.log(` ${i + 1}. ${c.permission} ${mark}${c.note ? ` (${c.note})` : ""}`);
40244
+ }
40245
+ if (multiBricking) {
40246
+ console.log(
40247
+ `
40248
+ \u2717 ${bricking.length} permission(s) return false for unrelated calls. On this kernel EVERY registered permission must approve EVERY call, so these BRICK all dispatches (they surface as PermissionDenied). Revoke or replace them with pass-through versions:`
40249
+ );
40250
+ bricking.forEach((c) => console.log(` ${c.permission}`));
40251
+ } else if (permissions.length > 1) {
40252
+ console.log("\n\u2713 All permissions pass through unrelated calls \u2014 dispatch will not be bricked.");
40253
+ }
40254
+ console.log(`
40255
+ Probe is heuristic: an unknown selector (${PROBE_SELECTOR}) to a neutral target (${PROBE_TARGET}).`);
40256
+ }
40257
+
40258
+ // src/commands/init.ts
40259
+ var import_node_fs8 = __toESM(require("node:fs"), 1);
40260
+ var import_node_path7 = __toESM(require("node:path"), 1);
40261
+
40262
+ // src/lib/foundry.ts
40263
+ var import_node_fs7 = require("node:fs");
40264
+ var import_node_path6 = require("node:path");
40265
+ var FOUNDRY_TOML = `[profile.default]
40266
+ src = "mandates"
40267
+ out = "out"
40268
+ libs = ["lib"]
40269
+ remappings = ["@sail/=.sail/contracts/"]
40270
+ solc = "0.8.26"
40271
+ optimizer = true
40272
+ optimizer_runs = 200
40273
+ # Mandates are deployed as standalone contracts and configured via their
40274
+ # constructor, then attached to a Safe with \`sailor mandate attach\`.
40275
+ `;
40276
+ var IPERMISSION_SOL = `// SPDX-License-Identifier: MIT
40277
+ pragma solidity 0.8.26;
40278
+
40279
+ /// @notice Execution context passed to every permission on each dispatch call.
40280
+ /// @dev Read-only snapshot of the transaction environment (staticcall).
40281
+ struct Context {
40282
+ address account; // the Safe whose assets are being moved
40283
+ address manager; // the delegated signer who submitted the dispatch
40284
+ address submitter; // msg.sender of the dispatch (may be a relayer)
40285
+ address target; // the call target
40286
+ bytes4 selector; // leading 4 bytes of calldata
40287
+ uint256 value; // native ETH forwarded (wei)
40288
+ uint256 blockTimestamp; // block.timestamp at dispatch
40289
+ uint256 blockNumber; // block.number at dispatch
40290
+ }
40291
+
40292
+ /// @title IPermission
40293
+ /// @notice Interface every Sail permission (mandate) contract must implement.
40294
+ /// @dev Evaluated via staticcall with a fixed gas cap; a revert or gas
40295
+ /// exhaustion is treated as \`false\`. Must not mutate state.
40296
+ interface IPermission {
40297
+ /// @notice Decide whether a manager-submitted transaction is permitted.
40298
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
40299
+
40300
+ /// @notice Optional stable identifier for off-chain indexing/deduplication.
40301
+ function discriminator() external view returns (bytes32);
40302
+ }
40303
+ `;
40304
+ var EXAMPLE_MANDATE_SOL = `// SPDX-License-Identifier: MIT
40305
+ pragma solidity 0.8.26;
40306
+
40307
+ import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
40308
+
40309
+ /// @title BoundedCallPermission
40310
+ /// @notice General-purpose IPermission primitive. Bounds the universal properties of any call:
40311
+ /// allowed targets, allowed selectors, and max ETH value. Protocol-agnostic.
40312
+ /// For calldata-parameter bounds (amount caps, recipient checks, slippage), write a
40313
+ /// protocol-specific permission \u2014 see examples/permissions/ for the pattern per protocol.
40314
+ /// @dev Deploy one instance per SMA with constructor-configured parameters.
40315
+ contract BoundedCallPermission is IPermission {
40316
+ bytes32 private constant DISCRIMINATOR = keccak256("BoundedCallPermission");
40317
+
40318
+ mapping(address => bool) public isAllowedTarget;
40319
+ mapping(bytes4 => bool) public isAllowedSelector;
40320
+ bool public immutable SELECTOR_FILTERING;
40321
+ uint256 public immutable MAX_VALUE;
40322
+
40323
+ constructor(address[] memory allowedTargets, bytes4[] memory allowedSelectors, uint256 maxValue) {
40324
+ for (uint256 i = 0; i < allowedTargets.length; i++) isAllowedTarget[allowedTargets[i]] = true;
40325
+ SELECTOR_FILTERING = allowedSelectors.length > 0;
40326
+ for (uint256 i = 0; i < allowedSelectors.length; i++) isAllowedSelector[allowedSelectors[i]] = true;
40327
+ MAX_VALUE = maxValue;
40028
40328
  }
40029
- return result;
40030
- }
40031
- recordResult(response, request) {
40032
- const id = response.requestId;
40033
- if (this.results.has(id)) return;
40034
- this.results.set(id, response);
40035
- const waiters2 = this.resultWaiters.get(id);
40036
- if (waiters2) {
40037
- for (const w of waiters2) w(response);
40038
- this.resultWaiters.delete(id);
40329
+
40330
+ function evaluate(bytes calldata, Context calldata ctx) external view returns (bool) {
40331
+ if (!isAllowedTarget[ctx.target]) return false;
40332
+ if (SELECTOR_FILTERING && !isAllowedSelector[ctx.selector]) return false;
40333
+ if (ctx.value > MAX_VALUE) return false;
40334
+ return true;
40039
40335
  }
40040
- this.broadcast({ type: "request-resolved", requestId: id });
40041
- setTimeout(() => this.results.delete(id), 10 * 60 * 1e3).unref?.();
40042
- this.logOwnerActivity(response, request);
40336
+
40337
+ function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
40338
+ }
40339
+ `;
40340
+ var MANDATES_README = `# Mandates
40341
+
40342
+ Solidity permission contracts for this Sailor project live here.
40343
+
40344
+ A permission implements \`@sail/interfaces/IPermission.sol\` \u2014 \`evaluate(txData, ctx)\`
40345
+ returns \`true\` to permit a manager-submitted dispatch, \`false\` to block it.
40346
+
40347
+ ## Authoring + deploying
40348
+
40349
+ 1. Start from \`BoundedCallPermission.sol\` for target/selector/value gating.
40350
+ For calldata-parameter bounds (amount caps, slippage, recipient checks),
40351
+ decode \`txData\` with the target protocol's ABI and add bounds to \`evaluate()\`.
40352
+ Configure all parameters in the **constructor** \u2014 the deploy flow expects a
40353
+ single creation transaction to fully set up the permission.
40354
+ 2. Compile:
40355
+ \`\`\`bash
40356
+ forge build
40357
+ \`\`\`
40358
+ 3. Deploy it (the owner signs the creation tx in the browser signing UI):
40359
+ \`\`\`bash
40360
+ sailor mandate deploy --contract BoundedCallPermission \\
40361
+ --args '[["0xTarget1", "0xTarget2"], [], 0]'
40362
+ \`\`\`
40363
+ Args: (allowedTargets[], allowedSelectors[], maxValue).
40364
+ Pass an empty selector array [] to skip selector filtering.
40365
+ Pass 0 for maxValue to block all ETH transfers.
40366
+ 4. Attach it to a Safe:
40367
+ \`\`\`bash
40368
+ sailor mandate attach --address 0xDeployed --sma 0xSafe
40369
+ \`\`\`
40370
+ (or pass \`--attach --sma 0xSafe\` to \`deploy\` to do both at once.)
40371
+
40372
+ Compiled artifacts are written to \`out/\` and the deployed address is tracked in
40373
+ \`.sail/state/mandates.json\`.
40374
+ `;
40375
+ function scaffoldFoundryWorkspace(root) {
40376
+ const dirs = [(0, import_node_path6.join)(root, "mandates"), (0, import_node_path6.join)(root, ".sail", "contracts", "interfaces")];
40377
+ for (const d of dirs) {
40378
+ if (!(0, import_node_fs7.existsSync)(d)) (0, import_node_fs7.mkdirSync)(d, { recursive: true });
40043
40379
  }
40044
- /**
40045
- * Append the owner's signing decision to the unified activity log. This is
40046
- * the single place every owner action lands — whether the request was
40047
- * approved (a signed tx or an off-chain EIP-712 signature) or rejected — so
40048
- * the dashboard's Recent Activity can show what the owner did, alongside the
40049
- * agent's dispatches. We only log when the originating request is known
40050
- * (its `kind`/`title` give the event meaning); a bare result with no request
40051
- * carries nothing worth showing.
40052
- */
40053
- logOwnerActivity(response, request) {
40054
- if (!request) return;
40055
- const base2 = {
40056
- ts: nowIso(),
40057
- actor: "owner",
40058
- kind: request.kind,
40059
- title: request.title,
40060
- chainId: request.chainId
40061
- };
40062
- let event;
40063
- if (response.status === "signed") {
40064
- event = { ...base2, type: "owner_signed", txHash: response.txHash };
40065
- } else if (response.status === "signature") {
40066
- event = { ...base2, type: "owner_signed", offchain: true };
40380
+ writeIfMissing((0, import_node_path6.join)(root, "foundry.toml"), FOUNDRY_TOML);
40381
+ writeIfMissing(
40382
+ (0, import_node_path6.join)(root, ".sail", "contracts", "interfaces", "IPermission.sol"),
40383
+ IPERMISSION_SOL
40384
+ );
40385
+ writeIfMissing((0, import_node_path6.join)(root, "mandates", "BoundedCallPermission.sol"), EXAMPLE_MANDATE_SOL);
40386
+ writeIfMissing((0, import_node_path6.join)(root, "mandates", "README.md"), MANDATES_README);
40387
+ }
40388
+ function writeIfMissing(path8, content) {
40389
+ if (!(0, import_node_fs7.existsSync)(path8)) (0, import_node_fs7.writeFileSync)(path8, content, "utf8");
40390
+ }
40391
+
40392
+ // src/commands/init.ts
40393
+ var TEMPLATE_COPY_EXCLUDES = /* @__PURE__ */ new Set([
40394
+ "node_modules",
40395
+ "dist",
40396
+ "out",
40397
+ "cache",
40398
+ "broadcast",
40399
+ ".git"
40400
+ ]);
40401
+ function copyDirSync(src, dest) {
40402
+ import_node_fs8.default.mkdirSync(dest, { recursive: true });
40403
+ for (const entry of import_node_fs8.default.readdirSync(src, { withFileTypes: true })) {
40404
+ if (TEMPLATE_COPY_EXCLUDES.has(entry.name)) continue;
40405
+ const srcPath = import_node_path7.default.join(src, entry.name);
40406
+ const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
40407
+ const destPath = import_node_path7.default.join(dest, destName);
40408
+ if (entry.isDirectory()) {
40409
+ copyDirSync(srcPath, destPath);
40067
40410
  } else {
40068
- event = { ...base2, type: "owner_rejected", reason: response.reason };
40069
- }
40070
- try {
40071
- appendActivity(event, (0, import_node_path7.join)(this.projectRoot, ".sail"));
40072
- } catch {
40411
+ import_node_fs8.default.copyFileSync(srcPath, destPath);
40073
40412
  }
40074
40413
  }
40075
- /** Path to `<projectRoot>/.sail/<...segments>`. */
40076
- sailFile(...segments) {
40077
- return (0, import_node_path7.join)(this.projectRoot, ".sail", ...segments);
40414
+ }
40415
+ var SAIL_WORKSPACE_README = `# Sailor Project Workspace
40416
+
40417
+ This folder is the local workspace for one Sailor agent deployment.
40418
+
40419
+ ## Layout
40420
+
40421
+ - \`config.json\` is the project manifest: name, chain, and state location.
40422
+ - \`keys/\` stores encrypted local signing keys. Never commit these files.
40423
+ - \`runtime/\` is for local UI and signing handoff state.
40424
+ - \`state/\` is for persistent agent state, audit logs, and tx history.
40425
+
40426
+ AI coding agents should read the project's \`AGENTS.md\` and this folder's \`config.json\`
40427
+ before changing strategy code or running commands that touch funds.
40428
+ `;
40429
+ function writeIfMissing2(file, content) {
40430
+ if (!import_node_fs8.default.existsSync(file)) import_node_fs8.default.writeFileSync(file, content, "utf-8");
40431
+ }
40432
+ function scaffoldProjectWorkspace(dest, name, options) {
40433
+ const chainId = options.chain ? (() => {
40434
+ const n = Number(options.chain);
40435
+ if (!Number.isInteger(n) || n <= 0) throw new Error(`Invalid chain id: "${options.chain}"`);
40436
+ return n;
40437
+ })() : null;
40438
+ const sailDir2 = import_node_path7.default.join(dest, ".sail");
40439
+ import_node_fs8.default.mkdirSync(import_node_path7.default.join(sailDir2, "keys"), { recursive: true });
40440
+ import_node_fs8.default.mkdirSync(import_node_path7.default.join(sailDir2, "runtime"), { recursive: true });
40441
+ import_node_fs8.default.mkdirSync(import_node_path7.default.join(sailDir2, "state"), { recursive: true });
40442
+ import_node_fs8.default.writeFileSync(
40443
+ import_node_path7.default.join(sailDir2, "config.json"),
40444
+ `${JSON.stringify(
40445
+ {
40446
+ version: 1,
40447
+ name,
40448
+ chainId,
40449
+ // null = chain not yet chosen; Stage 1 will set this
40450
+ stateDir: ".sail/state",
40451
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
40452
+ contracts: {
40453
+ kernel: "",
40454
+ mandateFactory: ""
40455
+ }
40456
+ },
40457
+ null,
40458
+ 2
40459
+ )}
40460
+ `,
40461
+ "utf-8"
40462
+ );
40463
+ writeIfMissing2(import_node_path7.default.join(sailDir2, "README.md"), SAIL_WORKSPACE_README);
40464
+ const chainIdLine = chainId != null ? `CHAIN_ID=${chainId}
40465
+ ` : `# CHAIN_ID=8453 # set after choosing your chain in Stage 1
40466
+ `;
40467
+ import_node_fs8.default.writeFileSync(
40468
+ import_node_path7.default.join(dest, ".env.example"),
40469
+ `# Sailor agent environment
40470
+ RPC_URL=https://your-rpc-endpoint
40471
+ ${chainIdLine}
40472
+ # Optional for non-interactive runs
40473
+ # SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
40474
+ `,
40475
+ "utf-8"
40476
+ );
40477
+ const rpcLine = options.rpcUrl ? `RPC_URL=${options.rpcUrl}` : `# Paste your RPC endpoint here (Alchemy, Infura, or any HTTPS endpoint)
40478
+ # RPC_URL=https://your-rpc-endpoint`;
40479
+ const chainLine = chainId != null ? `
40480
+ CHAIN_ID=${chainId}` : ``;
40481
+ writeIfMissing2(
40482
+ import_node_path7.default.join(sailDir2, ".env.local"),
40483
+ `${rpcLine}${chainLine}
40484
+
40485
+ # Optional for non-interactive runs (CI, GitHub Actions, launchd, systemd)
40486
+ # SAIL_PASSPHRASE=change-me-to-a-strong-passphrase
40487
+ `
40488
+ );
40489
+ }
40490
+ async function initCommand(dir, options = {}) {
40491
+ const inPlace = !dir || dir === ".";
40492
+ const dest = inPlace ? process.cwd() : import_node_path7.default.resolve(process.cwd(), dir);
40493
+ const name = import_node_path7.default.basename(dest);
40494
+ const templatesDir = import_node_path7.default.join(packageRoot(), "templates");
40495
+ const templateName = options.template ?? "default";
40496
+ if (/[/\\.]/.test(templateName) || templateName.includes("..")) {
40497
+ throw new Error(`Invalid template name: "${templateName}"`);
40078
40498
  }
40079
- /** Stream a JSON file back, or a fallback body when it is missing/invalid. */
40080
- sendJsonFile(res, filePath, fallback2) {
40081
- try {
40082
- const raw = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
40083
- JSON.parse(raw);
40084
- res.writeHead(200, { "Content-Type": "application/json" });
40085
- res.end(raw);
40086
- } catch {
40087
- res.writeHead(fallback2.status, { "Content-Type": "application/json" });
40088
- res.end(JSON.stringify(fallback2.body));
40089
- }
40499
+ const templateSrc = import_node_path7.default.join(templatesDir, templateName);
40500
+ const availableTemplates = () => import_node_fs8.default.existsSync(templatesDir) ? import_node_fs8.default.readdirSync(templatesDir).filter((e) => import_node_fs8.default.existsSync(import_node_path7.default.join(templatesDir, e, "package.json"))).join(", ") || "none" : "none";
40501
+ if (!import_node_fs8.default.existsSync(templateSrc) || !import_node_fs8.default.existsSync(import_node_path7.default.join(templateSrc, "package.json"))) {
40502
+ const available = availableTemplates();
40503
+ const hint = available === "none" ? `
40504
+ No templates found under ${templatesDir}.
40505
+ If you're running the in-tree CLI bundle from a monorepo checkout, the scaffolder
40506
+ couldn't locate the repo's templates/ directory. Install the published package, or
40507
+ run from the repo root.` : ` Available: ${available}`;
40508
+ throw new Error(`Template "${templateName}" not found.${hint}`);
40090
40509
  }
40091
- /**
40092
- * Persist a Safe deployed/imported from the dashboard. Mirrors the UI data
40093
- * server's `POST /api/account` (packages/ui/server.js): upsert the SMA into
40094
- * `state/accounts.json` (so the account switcher and the agent see it) BEFORE
40095
- * overwriting `account.json` with the new active SMA — the upsert backfills
40096
- * from the previously-active account.json, so writing it first would drop the
40097
- * prior SMA.
40098
- */
40099
- handleSaveAccount(req, res) {
40100
- this.readBody(req).then((body) => {
40101
- const parsed = body ? JSON.parse(body) : {};
40102
- const { safe, owner: owner2, permissionSigner, manager, chainId, createdAtBlock } = parsed;
40103
- if (!safe || !owner2 || !chainId) {
40104
- res.writeHead(400, { "Content-Type": "application/json" });
40105
- res.end(JSON.stringify({ error: "safe, owner, and chainId are required" }));
40106
- return;
40107
- }
40108
- const record = {
40109
- safe,
40110
- owner: owner2,
40111
- permissionSigner: permissionSigner ?? owner2,
40112
- manager: manager ?? owner2,
40113
- chainId,
40114
- createdAtBlock: createdAtBlock ?? "0"
40115
- };
40116
- const baseSailDir = this.sailFile();
40117
- upsertAccountInList(record, void 0, baseSailDir);
40118
- (0, import_node_fs8.mkdirSync)(baseSailDir, { recursive: true });
40119
- (0, import_node_fs8.writeFileSync)(this.sailFile("account.json"), `${JSON.stringify(record, null, 2)}
40120
- `);
40121
- res.writeHead(200, { "Content-Type": "application/json" });
40122
- res.end(JSON.stringify({ ok: true }));
40123
- }).catch((err) => {
40124
- res.writeHead(500, { "Content-Type": "application/json" });
40125
- res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
40126
- });
40510
+ const cwd = process.cwd();
40511
+ if (!inPlace && !dest.startsWith(cwd + import_node_path7.default.sep) && dest !== cwd) {
40512
+ throw new Error(`Directory must be inside the current working directory`);
40127
40513
  }
40128
- /** All known SMAs, annotating the currently-active one (mirrors the UI server). */
40129
- handleListAccounts(res) {
40130
- res.writeHead(200, { "Content-Type": "application/json" });
40131
- let active = null;
40132
- try {
40133
- active = JSON.parse((0, import_node_fs8.readFileSync)(this.sailFile("account.json"), "utf-8")).safe;
40134
- } catch {
40135
- }
40136
- try {
40137
- const accounts = JSON.parse(
40138
- (0, import_node_fs8.readFileSync)(this.sailFile("state", "accounts.json"), "utf-8")
40139
- );
40140
- res.end(
40141
- JSON.stringify(
40142
- accounts.map((a) => ({
40143
- ...a,
40144
- active: a.safe.toLowerCase() === active?.toLowerCase()
40145
- }))
40146
- )
40514
+ if (!inPlace && import_node_fs8.default.existsSync(dest)) {
40515
+ throw new Error(`Directory already exists: ${dest}`);
40516
+ }
40517
+ if (inPlace && import_node_fs8.default.existsSync(import_node_path7.default.join(dest, ".sail", "config.json"))) {
40518
+ throw new Error(`Already initialized \u2014 .sail/config.json exists`);
40519
+ }
40520
+ copyDirSync(templateSrc, dest);
40521
+ const pkgRoot = packageRoot();
40522
+ const examplesPermSrc = import_node_path7.default.join(pkgRoot, "examples", "permissions");
40523
+ if (import_node_fs8.default.existsSync(examplesPermSrc)) {
40524
+ copyDirSync(examplesPermSrc, import_node_path7.default.join(dest, "examples", "permissions"));
40525
+ }
40526
+ const permModelSrc = import_node_path7.default.join(pkgRoot, "docs", "PERMISSION_MODEL.md");
40527
+ if (import_node_fs8.default.existsSync(permModelSrc)) {
40528
+ import_node_fs8.default.mkdirSync(import_node_path7.default.join(dest, "docs"), { recursive: true });
40529
+ writeIfMissing2(import_node_path7.default.join(dest, "docs", "PERMISSION_MODEL.md"), import_node_fs8.default.readFileSync(permModelSrc, "utf-8"));
40530
+ }
40531
+ const pkgPath = import_node_path7.default.join(dest, "package.json");
40532
+ if (import_node_fs8.default.existsSync(pkgPath)) {
40533
+ const pkg = JSON.parse(import_node_fs8.default.readFileSync(pkgPath, "utf-8"));
40534
+ pkg.name = name;
40535
+ const deps = pkg.dependencies ?? {};
40536
+ if (deps["@sail/sdk"] === "workspace:*") {
40537
+ const sdkPath = import_node_path7.default.join(pkgRoot, "packages", "sdk");
40538
+ deps["@sail/sdk"] = import_node_fs8.default.existsSync(sdkPath) ? `file:${sdkPath}` : (
40539
+ // Fallback: SDK not bundled — user must install it manually.
40540
+ "0.1.0"
40147
40541
  );
40148
- } catch {
40149
- try {
40150
- const a = JSON.parse(
40151
- (0, import_node_fs8.readFileSync)(this.sailFile("account.json"), "utf-8")
40152
- );
40153
- res.end(JSON.stringify([{ ...a, name: "My SMA", active: true, addedAt: null }]));
40154
- } catch {
40155
- res.end("[]");
40156
- }
40157
40542
  }
40543
+ pkg.dependencies = deps;
40544
+ import_node_fs8.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
40545
+ `);
40158
40546
  }
40159
- handleHttp(req, res) {
40160
- const origin = req.headers.origin;
40161
- const url0 = (req.url ?? "/").split("?")[0];
40162
- const isDiscoveryEndpoint = url0 === "/config";
40163
- const allowedOrigin = isDiscoveryEndpoint && origin?.startsWith("http://localhost:") ? origin : origin === this._url ? origin : this._url;
40164
- res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
40165
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
40166
- res.setHeader("Access-Control-Allow-Headers", `Content-Type, ${REQUEST_SECRET_HEADER}`);
40167
- res.setHeader("Vary", "Origin");
40168
- if (req.method === "OPTIONS") {
40169
- res.writeHead(204);
40170
- res.end();
40171
- return;
40547
+ scaffoldProjectWorkspace(dest, name, options);
40548
+ scaffoldFoundryWorkspace(dest);
40549
+ printWelcome(
40550
+ dest,
40551
+ name,
40552
+ inPlace,
40553
+ !!options.rpcUrl,
40554
+ /* freshInit */
40555
+ true
40556
+ );
40557
+ }
40558
+ function chainLabel(chainId) {
40559
+ const labels = {
40560
+ 8453: "Base",
40561
+ 42161: "Arbitrum",
40562
+ 84532: "Base Sepolia",
40563
+ 130: "Unichain"
40564
+ };
40565
+ return labels[chainId] ?? `Chain ${chainId}`;
40566
+ }
40567
+ function detectState(dest) {
40568
+ try {
40569
+ const configRaw = import_node_fs8.default.readFileSync(import_node_path7.default.join(dest, ".sail", "config.json"), "utf-8");
40570
+ const config = JSON.parse(configRaw);
40571
+ const projectName = config.name ?? import_node_path7.default.basename(dest);
40572
+ const accountPath = import_node_path7.default.join(dest, ".sail", "account.json");
40573
+ if (!import_node_fs8.default.existsSync(accountPath)) {
40574
+ return { kind: "B", projectName, chain: chainLabel(config.chainId ?? 0) };
40172
40575
  }
40173
- const url = (req.url ?? "/").split("?")[0];
40174
- if (url === "/config") {
40175
- const isTrustedOrigin = !origin || origin === this._url;
40176
- const wsUrlForClient = isTrustedOrigin ? `${this.wsUrl}?secret=${encodeURIComponent(this.requestSecret)}` : this.wsUrl;
40177
- res.writeHead(200, { "Content-Type": "application/json" });
40178
- res.end(
40179
- JSON.stringify({
40180
- url: this._url,
40181
- wsUrl: wsUrlForClient,
40182
- port: this.port,
40183
- pid: process.pid,
40184
- pendingCount: this.pending.size
40185
- })
40576
+ const accountRaw = import_node_fs8.default.readFileSync(accountPath, "utf-8");
40577
+ const account2 = JSON.parse(accountRaw);
40578
+ const sma = account2.safe ?? "";
40579
+ const chain2 = chainLabel(account2.chainId ?? config.chainId ?? 0);
40580
+ let permissionCount = 0;
40581
+ try {
40582
+ const mandatesRaw = import_node_fs8.default.readFileSync(
40583
+ import_node_path7.default.join(dest, ".sail", "state", "mandates.json"),
40584
+ "utf-8"
40186
40585
  );
40187
- return;
40188
- }
40189
- const secretHeader = req.headers[REQUEST_SECRET_HEADER];
40190
- const isAuthenticated = secretHeader === this.requestSecret;
40191
- if (url === "/pending") {
40192
- if (!isAuthenticated) {
40193
- res.writeHead(403, { "Content-Type": "application/json" });
40194
- res.end(JSON.stringify({ error: "forbidden" }));
40195
- return;
40196
- }
40197
- res.writeHead(200, { "Content-Type": "application/json" });
40198
- res.end(JSON.stringify(Array.from(this.pending.values()).map((e) => e.request)));
40199
- return;
40200
- }
40201
- if (url === "/wallet") {
40202
- if (!isAuthenticated) {
40203
- res.writeHead(403, { "Content-Type": "application/json" });
40204
- res.end(JSON.stringify({ error: "forbidden" }));
40205
- return;
40206
- }
40207
- res.writeHead(200, { "Content-Type": "application/json" });
40208
- res.end(JSON.stringify({ address: this._connectedWallet ?? null }));
40209
- return;
40210
- }
40211
- if (url === "/requests" && req.method === "POST") {
40212
- const supplied = req.headers[REQUEST_SECRET_HEADER];
40213
- if (supplied !== this.requestSecret) {
40214
- res.writeHead(403, { "Content-Type": "application/json" });
40215
- res.end(JSON.stringify({ error: "forbidden" }));
40216
- return;
40217
- }
40218
- this.readBody(req).then((body) => {
40219
- const parsed = JSON.parse(body);
40220
- if (!parsed.kind || !["create-sma", "deploy-mandate", "register-permission", "attach-mandate", "revoke-permissions", "set-delegate", "arbitrary-tx"].includes(parsed.kind)) {
40221
- throw new Error(`Unknown signing request kind: ${String(parsed.kind)}`);
40222
- }
40223
- const request = this.enqueue(parsed);
40224
- res.writeHead(200, { "Content-Type": "application/json" });
40225
- res.end(JSON.stringify({ id: request.id }));
40226
- }).catch((err) => {
40227
- res.writeHead(400, { "Content-Type": "application/json" });
40228
- res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
40229
- });
40230
- return;
40231
- }
40232
- if (url === "/api/account" && req.method === "POST") {
40233
- this.handleSaveAccount(req, res);
40234
- return;
40235
- }
40236
- if (url === "/api/account" && (req.method === "GET" || req.method == null)) {
40237
- this.sendJsonFile(res, (0, import_node_path7.join)(this.projectRoot, ".sail", "account.json"), {
40238
- status: 404,
40239
- body: { error: "account not found" }
40240
- });
40241
- return;
40586
+ const mandates = JSON.parse(mandatesRaw);
40587
+ permissionCount = Array.isArray(mandates.mandates) ? mandates.mandates.length : 0;
40588
+ } catch {
40242
40589
  }
40243
- if (url === "/api/accounts" && (req.method === "GET" || req.method == null)) {
40244
- this.handleListAccounts(res);
40245
- return;
40590
+ if (permissionCount > 0) {
40591
+ return { kind: "D", projectName, chain: chain2, sma, permissionCount };
40246
40592
  }
40247
- const resultMatch = url.match(/^\/requests\/([^/]+)\/result$/);
40248
- if (resultMatch && (req.method === "GET" || req.method == null)) {
40249
- if (!isAuthenticated) {
40250
- res.writeHead(403, { "Content-Type": "application/json" });
40251
- res.end(JSON.stringify({ error: "forbidden" }));
40252
- return;
40253
- }
40254
- const id = decodeURIComponent(resultMatch[1]);
40255
- this.waitForResult(id, RESULT_LONGPOLL_MS).then((result) => {
40256
- if (result) {
40257
- res.writeHead(200, { "Content-Type": "application/json" });
40258
- res.end(JSON.stringify(result));
40259
- } else {
40260
- res.writeHead(204);
40261
- res.end();
40262
- }
40263
- });
40593
+ return { kind: "C", projectName, chain: chain2, sma };
40594
+ } catch {
40595
+ return { kind: "A" };
40596
+ }
40597
+ }
40598
+ function printWelcome(dest, name, inPlace, hasRpc, freshInit = false) {
40599
+ const state = freshInit ? { kind: "A" } : detectState(dest);
40600
+ if (state.kind === "B") {
40601
+ console.log("\nWelcome back.\n");
40602
+ console.log(`Project: ${state.projectName} | Network: ${state.chain}`);
40603
+ console.log("Status: SMA not yet deployed.\n");
40604
+ console.log("Next:");
40605
+ console.log(" sailor ui start");
40606
+ console.log(" Connect your wallet and deploy your SMA in the browser.\n");
40607
+ console.log('Or open this folder in your AI coding assistant and say: "continue"');
40608
+ return;
40609
+ }
40610
+ if (state.kind === "C") {
40611
+ console.log("\nWelcome back.\n");
40612
+ console.log(`Project: ${state.projectName}`);
40613
+ console.log(`SMA: ${state.sma} on ${state.chain}`);
40614
+ console.log(
40615
+ "Permissions: none registered yet \u2014 your agent has no mandate to execute against.\n"
40616
+ );
40617
+ console.log("Next:");
40618
+ console.log(
40619
+ " Write your permission contract in mandates/ (start from BoundedCallPermission.sol)"
40620
+ );
40621
+ console.log(" forge build");
40622
+ console.log(` sailor mandate deploy --contract <Name> --attach --sma ${state.sma}
40623
+ `);
40624
+ console.log('Or open this folder in your AI coding assistant and say: "continue"');
40625
+ return;
40626
+ }
40627
+ if (state.kind === "D") {
40628
+ console.log("\nWelcome back.\n");
40629
+ console.log(`Project: ${state.projectName}`);
40630
+ console.log(`SMA: ${state.sma} on ${state.chain}`);
40631
+ console.log(`Permissions: ${state.permissionCount} registered
40632
+ `);
40633
+ console.log('Open this folder in your AI coding assistant and say: "continue"');
40634
+ return;
40635
+ }
40636
+ if (!inPlace) console.log(`
40637
+ Created ${name}/`);
40638
+ console.log("\nYour Sail agent project is ready. Open your AI coding assistant in this folder and say start.");
40639
+ }
40640
+
40641
+ // src/commands/keys.ts
40642
+ var import_node_fs9 = __toESM(require("node:fs"), 1);
40643
+ var import_node_path8 = __toESM(require("node:path"), 1);
40644
+ async function keysGenerate() {
40645
+ const roleInput = await prompt("Which key? (agent wallet / mandate signer)", "agent wallet");
40646
+ const role = normalizeRole(roleInput);
40647
+ if (!role) {
40648
+ throw new Error(`Unknown key role: "${roleInput}". Choose "agent wallet" or "mandate signer".`);
40649
+ }
40650
+ if (fileExists(keyPath(role))) {
40651
+ const overwrite = await confirm(
40652
+ `A ${roleLabel(role)} key already exists at .sail/keys/${role}.json. Overwrite it?`
40653
+ );
40654
+ if (!overwrite) {
40655
+ console.log("Aborted \u2014 existing key left untouched.");
40264
40656
  return;
40265
40657
  }
40266
- if (this.uiDist) {
40267
- const rawPath = (req.url ?? "/").split("?")[0];
40268
- const filePath = (0, import_node_path7.resolve)((0, import_node_path7.join)(this.uiDist, rawPath === "/" ? "index.html" : rawPath));
40269
- if (!filePath.startsWith((0, import_node_path7.resolve)(this.uiDist))) {
40270
- res.writeHead(403);
40271
- res.end();
40272
- return;
40273
- }
40274
- if ((0, import_node_fs8.existsSync)(filePath)) {
40275
- const mime = MIME[(0, import_node_path7.extname)(filePath)] ?? "application/octet-stream";
40276
- res.writeHead(200, { "Content-Type": mime });
40277
- res.end((0, import_node_fs8.readFileSync)(filePath));
40278
- return;
40279
- }
40280
- const indexHtml = (0, import_node_path7.join)(this.uiDist, "index.html");
40281
- if ((0, import_node_fs8.existsSync)(indexHtml)) {
40282
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
40283
- res.end((0, import_node_fs8.readFileSync)(indexHtml));
40284
- return;
40285
- }
40286
- }
40287
- res.writeHead(404);
40288
- res.end();
40289
40658
  }
40290
- handleConnection(ws) {
40291
- this.clients.add(ws);
40292
- const msg = {
40293
- type: "pending",
40294
- requests: Array.from(this.pending.values()).map((e) => e.request)
40295
- };
40296
- ws.send(JSON.stringify(msg));
40297
- ws.on("message", (data) => {
40298
- try {
40299
- const parsed = JSON.parse(data.toString());
40300
- this.handleClientMessage(ws, parsed);
40301
- } catch {
40659
+ const password = await promptHidden("Set a password to encrypt the key");
40660
+ if (password.length < 8) {
40661
+ throw new Error("Password must be at least 8 characters.");
40662
+ }
40663
+ const confirmation = await promptHidden("Confirm password");
40664
+ if (password !== confirmation) {
40665
+ throw new Error("Passwords do not match.");
40666
+ }
40667
+ const keyring = LocalKeyring.generate();
40668
+ const keystore = await keyring.exportKeystore(password);
40669
+ writeJsonFile(keyPath(role), keystore);
40670
+ const label = role === "manager" ? "Agent wallet" : "Mandate signer";
40671
+ console.log(`
40672
+ ${label} key saved. Address: ${checksum4(keyring.address)}`);
40673
+ console.log(`Encrypted keystore written to .sail/keys/${role}.json`);
40674
+ if (role === "manager") {
40675
+ const save = await confirm(
40676
+ "\nSave passphrase to .sail/.env.local for non-interactive use? (required for CI/GitHub Actions)"
40677
+ );
40678
+ if (save) {
40679
+ const envPath = sailPath(".env.local");
40680
+ let content = "";
40681
+ if (import_node_fs9.default.existsSync(envPath)) {
40682
+ content = import_node_fs9.default.readFileSync(envPath, "utf-8");
40683
+ content = content.replace(/^SAIL_PASSPHRASE=.*\n?/m, "");
40302
40684
  }
40303
- });
40304
- ws.on("close", () => this.clients.delete(ws));
40305
- ws.on("error", () => this.clients.delete(ws));
40306
- }
40307
- handleClientMessage(_ws, msg) {
40308
- if (msg.type === "wallet-connected") {
40309
- if (typeof msg.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(msg.address)) return;
40310
- this._connectedWallet = msg.address;
40311
- for (const listener of this.walletListeners) listener(msg.address);
40312
- this.walletListeners = [];
40313
- return;
40314
- }
40315
- if (msg.type === "wallet-disconnected") {
40316
- this._connectedWallet = void 0;
40317
- return;
40318
- }
40319
- const entry = this.pending.get(msg.requestId);
40320
- if (!entry) return;
40321
- clearTimeout(entry.timer);
40322
- this.pending.delete(msg.requestId);
40323
- const { request } = entry;
40324
- if (msg.type === "signed") {
40325
- this.recordResult(
40326
- { status: "signed", requestId: msg.requestId, txHash: msg.txHash },
40327
- request
40328
- );
40329
- } else if (msg.type === "signature") {
40330
- this.recordResult(
40331
- { status: "signature", requestId: msg.requestId, signature: msg.signature },
40332
- request
40333
- );
40685
+ content = content.trimEnd() + (content.length > 0 ? "\n" : "") + `SAIL_PASSPHRASE=${password}
40686
+ `;
40687
+ import_node_fs9.default.mkdirSync(import_node_path8.default.dirname(envPath), { recursive: true });
40688
+ import_node_fs9.default.writeFileSync(envPath, content, { mode: 384 });
40689
+ console.log("\u2713 SAIL_PASSPHRASE saved to .sail/.env.local (mode 0600)");
40690
+ console.log(" sailor run will now work non-interactively.");
40334
40691
  } else {
40335
- this.recordResult(
40336
- {
40337
- status: "rejected",
40338
- requestId: msg.requestId,
40339
- reason: msg.reason
40340
- },
40341
- request
40342
- );
40692
+ console.log("\nTo run non-interactively, add this to .sail/.env.local:");
40693
+ console.log(` SAIL_PASSPHRASE=<your-passphrase>`);
40343
40694
  }
40344
40695
  }
40345
- readBody(req, maxBytes = 1e6) {
40346
- return new Promise((res, rej) => {
40347
- let size5 = 0;
40348
- const chunks = [];
40349
- req.on("data", (c) => {
40350
- size5 += c.length;
40351
- if (size5 > maxBytes) {
40352
- rej(new Error("Request body too large"));
40353
- req.destroy();
40354
- return;
40355
- }
40356
- chunks.push(c);
40357
- });
40358
- req.on("end", () => res(Buffer.concat(chunks).toString("utf8")));
40359
- req.on("error", rej);
40360
- });
40696
+ }
40697
+ async function keysExportCi() {
40698
+ const account2 = readJsonFile(sailPath("account.json"));
40699
+ const src = resolveKeyPath("manager", account2?.safe);
40700
+ if (!fileExists(src)) {
40701
+ throw new Error(
40702
+ 'No agent wallet keystore found.\nComplete Stage 1 (browser UI) to generate your agent wallet, or run\n"sailor keys generate" and choose "agent wallet" to create one manually.'
40703
+ );
40361
40704
  }
40362
- broadcast(msg) {
40363
- const data = JSON.stringify(msg);
40364
- for (const ws of this.clients) {
40365
- if (ws.readyState === import_websocket2.default.OPEN) ws.send(data);
40705
+ const dest = import_node_path8.default.resolve(process.cwd(), "ci-keystore.json");
40706
+ import_node_fs9.default.copyFileSync(src, dest);
40707
+ console.log(`\u2713 Keystore copied to ci-keystore.json`);
40708
+ console.log(` Source: ${src}`);
40709
+ const gitignorePath = import_node_path8.default.resolve(process.cwd(), ".gitignore");
40710
+ if (import_node_fs9.default.existsSync(gitignorePath)) {
40711
+ const content = import_node_fs9.default.readFileSync(gitignorePath, "utf-8");
40712
+ if (!content.includes("ci-keystore.json")) {
40713
+ import_node_fs9.default.appendFileSync(
40714
+ gitignorePath,
40715
+ "\n# CI keystore \u2014 encrypted agent wallet, safe to commit\n!ci-keystore.json\n"
40716
+ );
40717
+ console.log("\u2713 Added !ci-keystore.json allowlist entry to .gitignore");
40718
+ } else {
40719
+ console.log("\u2713 .gitignore already tracks ci-keystore.json");
40366
40720
  }
40367
40721
  }
40368
- writeRuntimeState() {
40369
- if (!(0, import_node_fs8.existsSync)(this.runtimeDir)) (0, import_node_fs8.mkdirSync)(this.runtimeDir, { recursive: true });
40370
- (0, import_node_fs8.writeFileSync)(
40371
- (0, import_node_path7.join)(this.runtimeDir, SERVER_STATE_FILE),
40372
- JSON.stringify(
40373
- {
40374
- url: this._url,
40375
- wsUrl: this.wsUrl,
40376
- port: this.port,
40377
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
40378
- pid: process.pid,
40379
- requestSecret: this.requestSecret
40380
- },
40381
- null,
40382
- 2
40383
- )
40384
- );
40722
+ console.log("\nNext steps:");
40723
+ console.log(" 1. Add two GitHub Actions secrets (Settings \u2192 Secrets \u2192 Actions):");
40724
+ console.log(" SAIL_PASSPHRASE \u2014 the passphrase that encrypts your agent wallet");
40725
+ console.log(" RPC_URL \u2014 your RPC endpoint");
40726
+ console.log(" 2. Commit and push ci-keystore.json:");
40727
+ console.log(' git add ci-keystore.json && git commit -m "chore: add CI keystore" && git push');
40728
+ console.log("\n The keystore is encrypted \u2014 the raw private key is never exposed.");
40729
+ console.log(" The workflow at .github/workflows/agent-tick.yml unlocks it with SAIL_PASSPHRASE.");
40730
+ }
40731
+ async function keysShow() {
40732
+ const present = ROLES.filter((role) => keyExists(role));
40733
+ if (present.length === 0) {
40734
+ console.log("No keys found in .sail/keys/.");
40735
+ console.log('Run "sailor keys generate" to create one.');
40736
+ return;
40385
40737
  }
40386
- removeRuntimeState() {
40387
- const path8 = (0, import_node_path7.join)(this.runtimeDir, SERVER_STATE_FILE);
40738
+ console.log("Keys in .sail/keys/:\n");
40739
+ for (const role of present) {
40388
40740
  try {
40389
- if ((0, import_node_fs8.existsSync)(path8)) (0, import_node_fs8.unlinkSync)(path8);
40390
- } catch {
40741
+ const keyring = await loadKeyring(role);
40742
+ console.log(` ${roleLabel(role)}: ${checksum4(keyring.address)}`);
40743
+ } catch (err) {
40744
+ console.log(` ${role}: ${err.message}`);
40391
40745
  }
40392
40746
  }
40393
- };
40394
- async function findAvailablePort(startPort) {
40395
- return new Promise((res) => {
40396
- const probe = (0, import_node_net.createServer)();
40397
- probe.listen(startPort, "127.0.0.1", () => {
40398
- const addr = probe.address();
40399
- probe.close(() => res(addr.port));
40400
- });
40401
- probe.on("error", () => res(findAvailablePort(startPort + 1)));
40402
- });
40403
40747
  }
40404
40748
 
40405
- // src/signing/client.ts
40406
- var RUNTIME_SERVER_FILE = (0, import_node_path8.join)(".sail", "runtime", "server.json");
40407
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
40408
- var SigningClient = class {
40409
- constructor(baseUrl, requestSecret = "") {
40410
- this.baseUrl = baseUrl;
40411
- this.requestSecret = requestSecret;
40749
+ // src/commands/mandate-contracts.ts
40750
+ var import_node_child_process = require("node:child_process");
40751
+ var import_node_fs10 = require("node:fs");
40752
+ var import_node_path9 = require("node:path");
40753
+ init_esm2();
40754
+
40755
+ // src/lib/mandates.ts
40756
+ var MandateStore = class {
40757
+ filePath;
40758
+ constructor(filePath = sailPath("state", "mandates.json")) {
40759
+ this.filePath = filePath;
40412
40760
  }
40413
- remote = true;
40414
- get url() {
40415
- return this.baseUrl;
40761
+ read() {
40762
+ const parsed = readJsonFile(this.filePath);
40763
+ return { version: 1, mandates: parsed?.mandates ?? [] };
40416
40764
  }
40417
- async start() {
40418
- if (!await this.ping()) {
40419
- throw new Error(`Signing station not reachable at ${this.baseUrl}`);
40420
- }
40765
+ write(data) {
40766
+ writeJsonFile(this.filePath, data);
40421
40767
  }
40422
- /** No-op: never tear down a daemon the user is running in another process. */
40423
- stop() {
40768
+ list() {
40769
+ return this.read().mandates;
40424
40770
  }
40425
- async ping() {
40426
- try {
40427
- const r = await fetch(`${this.baseUrl}/config`, { signal: AbortSignal.timeout(1500) });
40428
- return r.ok;
40429
- } catch {
40430
- return false;
40431
- }
40771
+ /** Find a tracked mandate by address (case-insensitive) or by exact name. */
40772
+ find(addressOrName) {
40773
+ const needle = addressOrName.toLowerCase();
40774
+ return this.read().mandates.find(
40775
+ (m) => m.address.toLowerCase() === needle || m.name === addressOrName
40776
+ );
40432
40777
  }
40433
- async requestSignature(req, timeoutMs = 10 * 60 * 1e3) {
40434
- const enqueueRes = await fetch(`${this.baseUrl}/requests`, {
40435
- method: "POST",
40436
- headers: { "Content-Type": "application/json", "x-sailor-secret": this.requestSecret },
40437
- body: JSON.stringify(req)
40438
- });
40439
- if (!enqueueRes.ok) {
40440
- throw new Error(`Failed to enqueue signing request (HTTP ${enqueueRes.status})`);
40441
- }
40442
- const { id } = await enqueueRes.json();
40443
- const deadline = Date.now() + timeoutMs;
40444
- while (Date.now() < deadline) {
40445
- const res = await fetch(`${this.baseUrl}/requests/${encodeURIComponent(id)}/result`, {
40446
- headers: { "x-sailor-secret": this.requestSecret }
40447
- });
40448
- if (res.status === 200) return await res.json();
40449
- if (res.status !== 204) {
40450
- throw new Error(`Unexpected result status ${res.status} from signing station`);
40451
- }
40452
- }
40453
- throw new Error(`Signing request "${req.title}" timed out after ${timeoutMs / 1e3}s`);
40778
+ /** Append a newly deployed mandate (replacing any prior record at the same address). */
40779
+ add(mandate2) {
40780
+ const data = this.read();
40781
+ data.mandates = data.mandates.filter(
40782
+ (m) => m.address.toLowerCase() !== mandate2.address.toLowerCase()
40783
+ );
40784
+ data.mandates.push(mandate2);
40785
+ this.write(data);
40454
40786
  }
40455
- async waitForWallet(timeoutMs = 5 * 60 * 1e3) {
40456
- const deadline = Date.now() + timeoutMs;
40457
- while (Date.now() < deadline) {
40458
- try {
40459
- const r = await fetch(`${this.baseUrl}/wallet`, {
40460
- headers: { "x-sailor-secret": this.requestSecret },
40461
- signal: AbortSignal.timeout(2e3)
40462
- });
40463
- if (r.ok) {
40464
- const { address } = await r.json();
40465
- if (address) return address;
40466
- }
40467
- } catch {
40468
- }
40469
- await sleep(1e3);
40470
- }
40471
- throw new Error("Timed out waiting for wallet connection in the signing UI");
40787
+ /** Record that a tracked mandate was attached to an SMA. */
40788
+ recordAttachment(address, attachment) {
40789
+ const data = this.read();
40790
+ const mandate2 = data.mandates.find((m) => m.address.toLowerCase() === address.toLowerCase());
40791
+ if (!mandate2) return;
40792
+ mandate2.attachments ??= [];
40793
+ mandate2.attachments.push({ ...attachment, at: (/* @__PURE__ */ new Date()).toISOString() });
40794
+ this.write(data);
40472
40795
  }
40473
40796
  };
40474
- function readRuntimeServerState(projectRoot) {
40475
- const file = (0, import_node_path8.join)(projectRoot, RUNTIME_SERVER_FILE);
40476
- if (!(0, import_node_fs9.existsSync)(file)) return null;
40477
- try {
40478
- return JSON.parse((0, import_node_fs9.readFileSync)(file, "utf8"));
40479
- } catch {
40480
- return null;
40481
- }
40482
- }
40483
- async function discoverDaemon(projectRoot = process.cwd()) {
40484
- const state = readRuntimeServerState(projectRoot);
40485
- if (!state?.url) return null;
40486
- const client = new SigningClient(state.url, state.requestSecret ?? "");
40487
- return await client.ping() ? client : null;
40488
- }
40489
- async function createSigningChannel(projectRoot = process.cwd()) {
40490
- const daemon = await discoverDaemon(projectRoot);
40491
- if (daemon) return daemon;
40492
- return new SigningServer({ projectRoot, advertise: false });
40493
- }
40494
40797
 
40495
40798
  // src/commands/onboard.ts
40496
40799
  init_esm2();
@@ -41275,6 +41578,12 @@ async function runDeployClone(project, channel, options) {
41275
41578
  `Unsupported clone template "${options.template}". Supported: ${Object.keys(CLONE_TEMPLATES).join(", ")}`
41276
41579
  );
41277
41580
  }
41581
+ const templateMap = project.deployment.standaloneTemplates ?? {};
41582
+ if (Object.keys(templateMap).length === 0) {
41583
+ throw new Error(
41584
+ `No clone templates are available on chain ${project.chainId} yet \u2014 templates are pending redeployment against the new kernel (${project.deployment.kernel}). Deploy your permission directly with \`sailor mandate deploy\` instead.`
41585
+ );
41586
+ }
41278
41587
  const impl = project.deployment.standaloneTemplates?.[options.template];
41279
41588
  if (!impl || !isAddress(impl, { strict: false })) {
41280
41589
  throw new Error(
@@ -41314,7 +41623,7 @@ async function runDeployClone(project, channel, options) {
41314
41623
  [sma, impl, BigInt(Math.floor(Date.now() / 1e3))]
41315
41624
  )
41316
41625
  );
41317
- const clone = predictCloneAddress(impl, project.contracts.permissionFactory, submitter, salt);
41626
+ const clone = predictCloneAddress(impl, project.contracts.mandateFactory, submitter, salt);
41318
41627
  say(() => {
41319
41628
  console.log(`
41320
41629
  ${spec.label} clone (${options.template})`);
@@ -41414,7 +41723,7 @@ Connect the owner wallet (mandate signer) in the browser \u2014 the agent wallet
41414
41723
  args: [sma, impl, salt, initData, deadline, signature]
41415
41724
  });
41416
41725
  const txHash = await walletClient.sendTransaction({
41417
- to: project.contracts.permissionFactory,
41726
+ to: project.contracts.mandateFactory,
41418
41727
  data,
41419
41728
  value: fee,
41420
41729
  account: agentSigner.viemAccount,
@@ -43639,6 +43948,7 @@ account.command("predict").description(
43639
43948
  "--manager <address>",
43640
43949
  "Agent (manager) wallet \u2014 mixed into the kernel salt (defaults to .sail/account.json)"
43641
43950
  ).option("--salt <n>", "CREATE2 salt nonce (default: 0)").option("--chain <id>", "Show prediction for one chain only").option("--json", "Emit machine-readable JSON").action(actionWith(accountPredict));
43951
+ account.command("deploy-chain").description("Deploy the same SMA address on an additional chain using the same owner, manager, and salt").requiredOption("--chain <id>", "Target EVM chain ID (e.g. 8453, 42161, 130, 1)").option("--salt <n>", "CREATE2 salt (defaults to saltNonce stored in .sail/account.json)").option("--json", "Emit machine-readable JSON").action(actionWith(accountDeployChain));
43642
43952
  account.command("rotate-signer").description("Rotate the SMA's delegated signer (agent wallet) and re-approve its mandates").option("--sma <address>", "SMA to rotate (defaults to the active account)").option("--to <address>", "Rotate to an existing agent-wallet address instead of generating one").option("--generate", "Generate a fresh local agent wallet (default when --to is omitted)").option("--skip-reattach", "Do not re-approve the previously-attached mandates").option("--reattach-only", "Skip rotation; only re-approve mandates (resume after funding)").option("--json", "Machine-readable output").action(actionWith(rotateSigner));
43643
43953
  var mandate = program2.command("mandate").description("Manage mandates");
43644
43954
  mandate.command("prepare").description("Prepare a mandate draft for review and signing in the UI (MetaMask)").action(action(mandatePrepare));