@dev.sail.money/sailor 1.0.0-42 → 1.1.0-43

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 (127) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +29 -20
  3. package/docs/PERMISSION_MODEL.md +1 -1
  4. package/examples/README.md +24 -0
  5. package/package.json +1 -1
  6. package/packages/cli/README.md +0 -1
  7. package/packages/cli/dist/index.cjs +146 -185
  8. package/packages/cli/dist/server.cjs +2 -1
  9. package/packages/sdk/dist/intelligence.d.ts +1 -1
  10. package/packages/sdk/dist/intelligence.js +1 -1
  11. package/packages/ui/dist/assets/{add-i2P8A2gs.js → add-BjqRem-K.js} +1 -1
  12. package/packages/ui/dist/assets/{all-wallets-BVgI7zL8.js → all-wallets-Ce2n1Z8I.js} +1 -1
  13. package/packages/ui/dist/assets/{app-store-BewBVZmc.js → app-store-C7E_7mH6.js} +1 -1
  14. package/packages/ui/dist/assets/{apple-C7MUnEQz.js → apple-BC7kAskQ.js} +1 -1
  15. package/packages/ui/dist/assets/{arrow-bottom-DfToMb64.js → arrow-bottom-Dq_l3FWT.js} +1 -1
  16. package/packages/ui/dist/assets/{arrow-bottom-circle-DJb-rBBb.js → arrow-bottom-circle-CjACGJGK.js} +1 -1
  17. package/packages/ui/dist/assets/{arrow-left-DsWmN2OG.js → arrow-left-5W_pClNR.js} +1 -1
  18. package/packages/ui/dist/assets/{arrow-right-DJUX6dvg.js → arrow-right-DXy553gM.js} +1 -1
  19. package/packages/ui/dist/assets/{arrow-top-bK9-SH4h.js → arrow-top-DD0q04nr.js} +1 -1
  20. package/packages/ui/dist/assets/{bank-LlHF24Ta.js → bank-CCXCwaWG.js} +1 -1
  21. package/packages/ui/dist/assets/{basic-BgERj65k.js → basic-CAwLXzDD.js} +1 -1
  22. package/packages/ui/dist/assets/{browser-B9v0kcdd.js → browser-DWmAo_2s.js} +1 -1
  23. package/packages/ui/dist/assets/{card-QhDXGe3I.js → card-C_tjSBK2.js} +1 -1
  24. package/packages/ui/dist/assets/{ccip-CP4Hg6QU.js → ccip-B4SUIV1s.js} +1 -1
  25. package/packages/ui/dist/assets/{checkmark-ScbLFePj.js → checkmark-W_dSsbPW.js} +1 -1
  26. package/packages/ui/dist/assets/{checkmark-bold-SxqQsHPF.js → checkmark-bold-C9Xi2fBZ.js} +1 -1
  27. package/packages/ui/dist/assets/{chevron-bottom-HOzCXggY.js → chevron-bottom-Dxo8Hw1W.js} +1 -1
  28. package/packages/ui/dist/assets/{chevron-left-DmtXEk14.js → chevron-left-CD7UuRQl.js} +1 -1
  29. package/packages/ui/dist/assets/{chevron-right-DJCWUGen.js → chevron-right-D6kOGOGy.js} +1 -1
  30. package/packages/ui/dist/assets/{chevron-top-Bj9XyjOS.js → chevron-top-ynjtovar.js} +1 -1
  31. package/packages/ui/dist/assets/{chrome-store-C0Rq-0OK.js → chrome-store-CB5wu9g8.js} +1 -1
  32. package/packages/ui/dist/assets/{clock-DJiHF6By.js → clock-B3H1XYXj.js} +1 -1
  33. package/packages/ui/dist/assets/{close-B58KpBOm.js → close-DVSIOMF5.js} +1 -1
  34. package/packages/ui/dist/assets/{coinPlaceholder-QrD1nktf.js → coinPlaceholder-Co_HN9AE.js} +1 -1
  35. package/packages/ui/dist/assets/{compass-DkdmCS4M.js → compass-CaY11Job.js} +1 -1
  36. package/packages/ui/dist/assets/{copy-BKkOeIGO.js → copy-D7gntTj6.js} +1 -1
  37. package/packages/ui/dist/assets/{core-BJ5Wn_0H.js → core-CqvnE8sM.js} +3 -3
  38. package/packages/ui/dist/assets/cursor-rLwK_mXz.js +3 -0
  39. package/packages/ui/dist/assets/{cursor-transparent-CwUDRud2.js → cursor-transparent-BrL6QUeS.js} +1 -1
  40. package/packages/ui/dist/assets/{desktop-CKk_cTjE.js → desktop-BLH7DXoy.js} +1 -1
  41. package/packages/ui/dist/assets/{disconnect-BR4tm4ry.js → disconnect-BxnvzLHz.js} +1 -1
  42. package/packages/ui/dist/assets/{discord-CimCnw-R.js → discord-CfV-36UX.js} +1 -1
  43. package/packages/ui/dist/assets/{etherscan-DKSSzuOS.js → etherscan-CyMQ7xaE.js} +1 -1
  44. package/packages/ui/dist/assets/{events-HAbmebGY.js → events-D_3qqJ93.js} +1 -1
  45. package/packages/ui/dist/assets/{exclamation-triangle-CnI_W0DF.js → exclamation-triangle-DWjoM6jA.js} +1 -1
  46. package/packages/ui/dist/assets/{extension-BSpH53zE.js → extension-D3fVJAol.js} +1 -1
  47. package/packages/ui/dist/assets/{external-link-D2QNdCHI.js → external-link-D1vHHGXX.js} +1 -1
  48. package/packages/ui/dist/assets/{facebook-CgSGAlNU.js → facebook-Dzk6W-1X.js} +1 -1
  49. package/packages/ui/dist/assets/{fallback-CUotDuSc.js → fallback-BDBC0epM.js} +1 -1
  50. package/packages/ui/dist/assets/{farcaster-SPALE2oQ.js → farcaster-BkJt6sOG.js} +1 -1
  51. package/packages/ui/dist/assets/{filters-DjDEAYKk.js → filters-Btr-hO6b.js} +1 -1
  52. package/packages/ui/dist/assets/{github-CP9cMjmv.js → github-ulrStu89.js} +1 -1
  53. package/packages/ui/dist/assets/{google-DHxowSYF.js → google-BTTDuZjf.js} +1 -1
  54. package/packages/ui/dist/assets/{help-circle-Y1PsVJnm.js → help-circle-DcSMbQJh.js} +1 -1
  55. package/packages/ui/dist/assets/{id-Du_4Z0EW.js → id-7WOxEl6j.js} +1 -1
  56. package/packages/ui/dist/assets/{image-tsFuvzhF.js → image-D2GBTxnU.js} +1 -1
  57. package/packages/ui/dist/assets/{index-7RnuTfdS.js → index-BfABWjw0.js} +1 -1
  58. package/packages/ui/dist/assets/{index-tPkIpVzb.js → index-BjpGs3bJ.js} +3 -3
  59. package/packages/ui/dist/assets/{index-0OXGjc-u.js → index-Bvqcol0e.js} +1 -1
  60. package/packages/ui/dist/assets/{index-CNBJUjiM.js → index-C35kUMRo.js} +1 -1
  61. package/packages/ui/dist/assets/{index-Bj9jEvxf.js → index-DOy_BvMy.js} +27 -27
  62. package/packages/ui/dist/assets/{index-DExn8x4G.js → index-jNjVgIvi.js} +1 -1
  63. package/packages/ui/dist/assets/{index.es-DIo7ubqt.js → index.es-Cz1WraDz.js} +4 -4
  64. package/packages/ui/dist/assets/{info-DwysfTrK.js → info-CezLTebu.js} +1 -1
  65. package/packages/ui/dist/assets/{info-circle-BgqfZd9m.js → info-circle-BHDy0RH1.js} +1 -1
  66. package/packages/ui/dist/assets/{lightbulb-Cy1dZk-v.js → lightbulb-DvUuugiy.js} +1 -1
  67. package/packages/ui/dist/assets/{mail-q8uF2fJv.js → mail-D6W2cU_-.js} +1 -1
  68. package/packages/ui/dist/assets/{metamask-sdk-CC2GhoTH.js → metamask-sdk-Cj8b59wb.js} +1 -1
  69. package/packages/ui/dist/assets/{mobile-BM3dHloL.js → mobile-DD4cG8mI.js} +1 -1
  70. package/packages/ui/dist/assets/{more-DtxChT3n.js → more-RsHIPHXv.js} +1 -1
  71. package/packages/ui/dist/assets/{network-placeholder-CkS7N2-s.js → network-placeholder-E2lpOWE4.js} +1 -1
  72. package/packages/ui/dist/assets/{nftPlaceholder-BP2BELBx.js → nftPlaceholder-Cm9qGJHf.js} +1 -1
  73. package/packages/ui/dist/assets/{off-CGlNgOvT.js → off-7ALa1e8N.js} +1 -1
  74. package/packages/ui/dist/assets/{parseSignature-B-9InYad.js → parseSignature-EU-GmuA0.js} +1 -1
  75. package/packages/ui/dist/assets/{play-store-j4WI9tEY.js → play-store-YmFLoPbj.js} +1 -1
  76. package/packages/ui/dist/assets/{plus-DoBLB2r6.js → plus-CljVpN-Y.js} +1 -1
  77. package/packages/ui/dist/assets/{qr-code-CxMPa6dI.js → qr-code-DVGz15q5.js} +1 -1
  78. package/packages/ui/dist/assets/{recycle-horizontal-tKM6wEFt.js → recycle-horizontal-Ch_2PFBB.js} +1 -1
  79. package/packages/ui/dist/assets/{refresh-D3gJv-wD.js → refresh-B1yJjeZR.js} +1 -1
  80. package/packages/ui/dist/assets/{reown-logo-CB8aduTF.js → reown-logo-B4gHTrkG.js} +1 -1
  81. package/packages/ui/dist/assets/{search-CEFrSvpQ.js → search-C7OPO4bC.js} +1 -1
  82. package/packages/ui/dist/assets/{secp256k1-BiXu1g8S.js → secp256k1-ZHPkrWDd.js} +1 -1
  83. package/packages/ui/dist/assets/{send-DpKQgtRb.js → send-C5Aj89yl.js} +1 -1
  84. package/packages/ui/dist/assets/{swapHorizontal-a9Ybf18t.js → swapHorizontal-D5l_jKrZ.js} +1 -1
  85. package/packages/ui/dist/assets/{swapHorizontalBold-DP452gU2.js → swapHorizontalBold-TxFZ3Umb.js} +1 -1
  86. package/packages/ui/dist/assets/{swapHorizontalMedium-eG99M64A.js → swapHorizontalMedium-M_yuNi0o.js} +1 -1
  87. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-BAc_to7T.js → swapHorizontalRoundedBold-G73a-cMg.js} +1 -1
  88. package/packages/ui/dist/assets/{swapVertical-BU1HNAb2.js → swapVertical-Gy7GjYS4.js} +1 -1
  89. package/packages/ui/dist/assets/{telegram-DFyhBidh.js → telegram-BnO9isY5.js} +1 -1
  90. package/packages/ui/dist/assets/{three-dots-BdnfIeUB.js → three-dots-B616bG-O.js} +1 -1
  91. package/packages/ui/dist/assets/{twitch-DdurBvf3.js → twitch-DUpzj8Dd.js} +1 -1
  92. package/packages/ui/dist/assets/{twitterIcon-JS1Kh388.js → twitterIcon-itK0Mrgj.js} +1 -1
  93. package/packages/ui/dist/assets/{verify-C-ZvR7T4.js → verify-B1Gtl5H9.js} +1 -1
  94. package/packages/ui/dist/assets/{verify-filled-Dzgdgepq.js → verify-filled-D3noLrZ3.js} +1 -1
  95. package/packages/ui/dist/assets/{w3m-modal-B_CKEhcT.js → w3m-modal-CiZVJcHR.js} +1 -1
  96. package/packages/ui/dist/assets/{wallet-C4JYrLNC.js → wallet-BK1sp3ca.js} +1 -1
  97. package/packages/ui/dist/assets/{wallet-placeholder-DURyjfcE.js → wallet-placeholder-CatD0vru.js} +1 -1
  98. package/packages/ui/dist/assets/{walletconnect-C5I2F5B-.js → walletconnect-DmCqpZUB.js} +1 -1
  99. package/packages/ui/dist/assets/{warning-circle-Blhki0Aq.js → warning-circle-bT7aRO8J.js} +1 -1
  100. package/packages/ui/dist/assets/{x-NdGVznpk.js → x-DuFufU-Y.js} +1 -1
  101. package/packages/ui/dist/index.html +1 -1
  102. package/scripts/check-docs.mjs +51 -4
  103. package/scripts/check-init.mjs +12 -0
  104. package/templates/default/.agents/skills/sail-ci/SKILL.md +66 -0
  105. package/templates/default/.agents/skills/sail-extend/SKILL.md +74 -0
  106. package/templates/default/.agents/skills/sail-mandates/SKILL.md +93 -0
  107. package/templates/default/.agents/skills/sail-mandates/references/approvals.md +42 -0
  108. package/templates/default/.agents/skills/sail-mandates/references/calls-schema.md +42 -0
  109. package/templates/default/.agents/skills/sail-mandates/references/constructor-args.md +45 -0
  110. package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +31 -0
  111. package/templates/default/.agents/skills/sail-mandates/references/simulate-calls.md +58 -0
  112. package/templates/default/.agents/skills/sail-onboarding/SKILL.md +73 -0
  113. package/templates/default/.agents/skills/sail-project-info/SKILL.md +30 -0
  114. package/templates/default/.agents/skills/sail-servers/SKILL.md +43 -0
  115. package/templates/default/.agents/skills/sail-transactions/SKILL.md +63 -0
  116. package/templates/default/AGENTS.md +37 -126
  117. package/templates/default/test/BoundedCallPermission.t.sol +73 -0
  118. package/packages/ui/dist/assets/cursor-Dk5BeeUC.js +0 -3
  119. /package/{templates → examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +0 -0
  120. /package/{templates → examples}/custom-mandate/README.md +0 -0
  121. /package/{templates → examples}/custom-mandate/foundry.toml +0 -0
  122. /package/{templates → examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
  123. /package/{templates → examples}/custom-mandate/mandates/README.md +0 -0
  124. /package/{templates → examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
  125. /package/{templates → examples}/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -0
  126. /package/{templates → examples}/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -0
  127. /package/{templates → examples}/lifi-permissions/README.md +0 -0
@@ -1,4 +1,4 @@
1
- import{F as l}from"./core-BJ5Wn_0H.js";import"./index-Bj9jEvxf.js";import"./events-HAbmebGY.js";import"./index.es-DIo7ubqt.js";import"./fallback-CUotDuSc.js";const o=l`<svg fill="none" viewBox="0 0 96 67">
1
+ import{F as l}from"./core-CqvnE8sM.js";import"./index-DOy_BvMy.js";import"./events-D_3qqJ93.js";import"./index.es-Cz1WraDz.js";import"./fallback-BDBC0epM.js";const o=l`<svg fill="none" viewBox="0 0 96 67">
2
2
  <path
3
3
  fill="currentColor"
4
4
  d="M25.32 18.8a32.56 32.56 0 0 1 45.36 0l1.5 1.47c.63.62.63 1.61 0 2.22l-5.15 5.05c-.31.3-.82.3-1.14 0l-2.07-2.03a22.71 22.71 0 0 0-31.64 0l-2.22 2.18c-.31.3-.82.3-1.14 0l-5.15-5.05a1.55 1.55 0 0 1 0-2.22l1.65-1.62Zm56.02 10.44 4.59 4.5c.63.6.63 1.6 0 2.21l-20.7 20.26c-.62.61-1.63.61-2.26 0L48.28 41.83a.4.4 0 0 0-.56 0L33.03 56.21c-.63.61-1.64.61-2.27 0L10.07 35.95a1.55 1.55 0 0 1 0-2.22l4.59-4.5a1.63 1.63 0 0 1 2.27 0L31.6 43.63a.4.4 0 0 0 .57 0l14.69-14.38a1.63 1.63 0 0 1 2.26 0l14.69 14.38a.4.4 0 0 0 .57 0l14.68-14.38a1.63 1.63 0 0 1 2.27 0Z"
@@ -1,4 +1,4 @@
1
- import{F as r}from"./core-BJ5Wn_0H.js";import"./index-Bj9jEvxf.js";import"./events-HAbmebGY.js";import"./index.es-DIo7ubqt.js";import"./fallback-CUotDuSc.js";const a=r`<svg fill="none" viewBox="0 0 20 20">
1
+ import{F as r}from"./core-CqvnE8sM.js";import"./index-DOy_BvMy.js";import"./events-D_3qqJ93.js";import"./index.es-Cz1WraDz.js";import"./fallback-BDBC0epM.js";const a=r`<svg fill="none" viewBox="0 0 20 20">
2
2
  <path
3
3
  fill="currentColor"
4
4
  d="M11 6.67a1 1 0 1 0-2 0v2.66a1 1 0 0 0 2 0V6.67ZM10 14.5a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"
@@ -1,4 +1,4 @@
1
- import{F as l}from"./core-BJ5Wn_0H.js";import"./index-Bj9jEvxf.js";import"./events-HAbmebGY.js";import"./index.es-DIo7ubqt.js";import"./fallback-CUotDuSc.js";const a=l`<svg fill="none" viewBox="0 0 41 40">
1
+ import{F as l}from"./core-CqvnE8sM.js";import"./index-DOy_BvMy.js";import"./events-D_3qqJ93.js";import"./index.es-Cz1WraDz.js";import"./fallback-BDBC0epM.js";const a=l`<svg fill="none" viewBox="0 0 41 40">
2
2
  <g clip-path="url(#a)">
3
3
  <path fill="#000" d="M.8 0h40v40H.8z" />
4
4
  <path
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#040b16" />
7
7
  <title>Sail - Unlocking Personalized Money</title>
8
- <script type="module" crossorigin src="/assets/index-Bj9jEvxf.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DOy_BvMy.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-BODuSfdj.css">
10
10
  </head>
11
11
  <body>
@@ -19,7 +19,7 @@
19
19
  * Run: `node scripts/check-docs.mjs` (or `pnpm docs:check`). Exit 1 on any miss.
20
20
  */
21
21
 
22
- import { readFileSync, readdirSync, statSync } from "node:fs";
22
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
23
23
  import { dirname, join, relative } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
25
 
@@ -159,8 +159,6 @@ const DOC_GLOBS = [
159
159
  "templates",
160
160
  "packages/cli/README.md",
161
161
  "packages/sdk/README.md",
162
- "packages/chains/README.md",
163
- "packages/create-app/README.md",
164
162
  ];
165
163
 
166
164
  function collectDocs() {
@@ -193,7 +191,54 @@ function codeRegions(md) {
193
191
  return regions;
194
192
  }
195
193
 
196
- // ── 4. Validate ──────────────────────────────────────────────────────────────
194
+ // ── 4. Skills consistency ─────────────────────────────────────────────────────
195
+ //
196
+ // The scaffolded template ships agent skills under .agents/skills/. AGENTS.md is
197
+ // the routing layer: it must point at every skill that exists, and every skill it
198
+ // points at must exist with valid frontmatter — otherwise an agent either never
199
+ // discovers a workflow or follows a dangling pointer.
200
+
201
+ function checkSkills(errors) {
202
+ const skillsRoot = join(ROOT, "templates/default/.agents/skills");
203
+ const agentsPath = join(ROOT, "templates/default/AGENTS.md");
204
+ if (!existsSync(skillsRoot)) {
205
+ errors.push("templates/default/.agents/skills: directory missing");
206
+ return;
207
+ }
208
+ const agentsMd = readFileSync(agentsPath, "utf-8");
209
+ const dirs = readdirSync(skillsRoot).filter((d) =>
210
+ statSync(join(skillsRoot, d)).isDirectory(),
211
+ );
212
+ if (dirs.length === 0) errors.push("templates/default/.agents/skills: no skills found");
213
+
214
+ for (const d of dirs) {
215
+ const skillFile = join(skillsRoot, d, "SKILL.md");
216
+ if (!existsSync(skillFile)) {
217
+ errors.push(`templates/default/.agents/skills/${d}: missing SKILL.md`);
218
+ continue;
219
+ }
220
+ const fm = readFileSync(skillFile, "utf-8").match(/^---\n([\s\S]*?)\n---/);
221
+ if (!fm) {
222
+ errors.push(`${rel(skillFile)}: missing YAML frontmatter`);
223
+ continue;
224
+ }
225
+ const name = fm[1].match(/^name:\s*(\S+)\s*$/m)?.[1];
226
+ const description = fm[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
227
+ if (name !== d) errors.push(`${rel(skillFile)}: frontmatter name "${name}" ≠ directory "${d}"`);
228
+ if (!description) errors.push(`${rel(skillFile)}: frontmatter description missing or empty`);
229
+ if (!agentsMd.includes(`.agents/skills/${d}/SKILL.md`)) {
230
+ errors.push(`templates/default/AGENTS.md: routing table does not reference .agents/skills/${d}/SKILL.md`);
231
+ }
232
+ }
233
+
234
+ for (const m of agentsMd.matchAll(/\.agents\/skills\/([\w-]+)\/SKILL\.md/g)) {
235
+ if (!dirs.includes(m[1])) {
236
+ errors.push(`templates/default/AGENTS.md: references .agents/skills/${m[1]}/SKILL.md which does not exist`);
237
+ }
238
+ }
239
+ }
240
+
241
+ // ── 5. Validate ──────────────────────────────────────────────────────────────
197
242
 
198
243
  function main() {
199
244
  const cli = parseCliSurface();
@@ -242,6 +287,8 @@ function main() {
242
287
  }
243
288
  }
244
289
 
290
+ checkSkills(errors);
291
+
245
292
  // ── Report ───────────────────────────────────────────────────────────────
246
293
  const cliCount = cli.leaves.size + [...cli.groups.values()].reduce((n, s) => n + s.size, 0);
247
294
  const sdkCount =
@@ -65,6 +65,18 @@ try {
65
65
  "foundry.toml",
66
66
  "mandates",
67
67
  "AGENTS.md",
68
+ ".sail/contracts/interfaces/IPermission.sol",
69
+ ".sail/contracts/interfaces/IBatchPermission.sol",
70
+ "test/BoundedCallPermission.t.sol",
71
+ "examples/custom-mandate/README.md",
72
+ ".agents/skills/sail-onboarding/SKILL.md",
73
+ ".agents/skills/sail-project-info/SKILL.md",
74
+ ".agents/skills/sail-servers/SKILL.md",
75
+ ".agents/skills/sail-transactions/SKILL.md",
76
+ ".agents/skills/sail-mandates/SKILL.md",
77
+ ".agents/skills/sail-mandates/references/approvals.md",
78
+ ".agents/skills/sail-ci/SKILL.md",
79
+ ".agents/skills/sail-extend/SKILL.md",
68
80
  ];
69
81
  for (const rel of mustExist) {
70
82
  if (!fs.existsSync(path.join(dest, rel))) fail(`expected scaffolded "${rel}" — not found`);
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: sail-ci
3
+ description: Automate the agent on a schedule with GitHub Actions — exporting the encrypted keystore, committing the right files, configuring secrets, and driving the workflow with the gh CLI. Use when the user wants the agent to run on a schedule, in CI, or unattended after sailor run --once has been confirmed working.
4
+ ---
5
+
6
+ # Sail CI — GitHub Actions automation
7
+
8
+ The scaffolded workflow at `.github/workflows/agent-tick.yml` runs `npx sailor run --once` on a cron schedule (default: every Monday 09:00 UTC — edit the `cron` line to the user's cadence; `workflow_dispatch` allows manual runs). It uses `npm ci`, copies `ci-keystore.json` to `.sail/keys/manager.json`, and unlocks it with `SAIL_PASSPHRASE`. `CHAIN_ID` comes from the repository variable `CHAIN_ID` (default `8453`). No private key ever appears in the workflow or in secrets.
9
+
10
+ Confirm `sailor run --once` works locally before automating.
11
+
12
+ ## 1. Export the keystore
13
+
14
+ ```bash
15
+ sailor keys export-ci
16
+ ```
17
+
18
+ Copies the encrypted agent wallet to `ci-keystore.json` in the project root and adds a `!ci-keystore.json` allowlist entry to `.gitignore`. The keystore is geth v3 encrypted (scrypt + aes-128-ctr); the raw private key is never exposed — safe to commit.
19
+
20
+ ## 2. Commit the required files
21
+
22
+ CI needs these non-secret files in the repo:
23
+
24
+ ```bash
25
+ npm install # generate package-lock.json if it doesn't exist
26
+ git add ci-keystore.json package-lock.json .sail/account.json .sail/config.json .sail/mandate.json
27
+ git commit -m "chore: add CI keystore and sail state" && git push
28
+ ```
29
+
30
+ `package-lock.json` is required by `npm ci`. `.sail/account.json`, `.sail/config.json`, and `.sail/mandate.json` contain only public addresses and flags — no secrets. The `.gitignore` already has `!` exceptions for all of these.
31
+
32
+ ## 3. Add the two repository secrets
33
+
34
+ GitHub → Settings → Secrets and variables → Actions:
35
+
36
+ - `SAIL_PASSPHRASE` — the passphrase that encrypts the agent wallet
37
+ - `RPC_URL` — the RPC endpoint
38
+
39
+ (If the chain is not Base, also set the repository **variable** `CHAIN_ID` to the right chain id.)
40
+
41
+ ## 4. Install and authenticate the gh CLI
42
+
43
+ Required to manage the workflow from the terminal (trigger runs, check logs, add secrets without the browser):
44
+
45
+ - macOS: `brew install gh`
46
+ - Windows: `winget install --id GitHub.cli` or `scoop install gh`
47
+ - Linux: https://github.com/cli/cli/blob/trunk/docs/install_linux.md
48
+
49
+ Authenticate with the `workflow` scope — without it, `gh` cannot trigger or inspect Actions runs:
50
+
51
+ ```bash
52
+ gh auth login --scopes workflow
53
+ gh auth status # confirm workflow scope is listed
54
+ ```
55
+
56
+ ## 5. Drive it
57
+
58
+ ```bash
59
+ gh secret set SAIL_PASSPHRASE # prompts for the value
60
+ gh secret set RPC_URL
61
+ gh workflow run agent-tick.yml # manual trigger
62
+ gh run list --workflow agent-tick.yml # run history
63
+ gh run view --log # logs of the latest run
64
+ ```
65
+
66
+ A failing run's logs show the same stderr the local runner produces (`reverted: <txHash>`, `skipped: no registered permission…`) — debug with the sail-transactions skill.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: sail-extend
3
+ description: Recipes for extending a live agent with notifications (Telegram, email) and a strategy-specific dashboard. Use when the agent is running and the user asks for run/transaction alerts, monitoring, or a custom view of their strategy.
4
+ ---
5
+
6
+ # Sail extend — notifications and custom dashboards
7
+
8
+ These are user-land code the assistant writes into this project on request — not Sailor features. Build them only once the agent is live.
9
+
10
+ ## Notifications
11
+
12
+ Two hook points, pick per the user's setup:
13
+
14
+ 1. **Inside the agent loop** (`src/agent.ts`) — fires on every tick, works locally and in CI. Send from within `tick()` after a meaningful event, or read `.sail/activity.jsonl` and alert on `dispatch_reverted` / `dispatch_denied` / `error` entries.
15
+ 2. **As a GitHub Actions step** (`.github/workflows/agent-tick.yml`) — fires once per scheduled run; simplest for run-level success/failure alerts.
16
+
17
+ ### Telegram (Bot API)
18
+
19
+ One-time setup: create a bot with @BotFather (get `TELEGRAM_BOT_TOKEN`), have the user message the bot once, read their chat id from `https://api.telegram.org/bot<token>/getUpdates`. Store both as secrets/env — never hardcode.
20
+
21
+ The exact curl shape:
22
+
23
+ ```bash
24
+ curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
25
+ -d chat_id="${TELEGRAM_CHAT_ID}" \
26
+ --data-urlencode text="sailor: tick complete — 2 executed, 0 reverted"
27
+ ```
28
+
29
+ As a CI step appended to `agent-tick.yml` (add `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` as repo secrets):
30
+
31
+ ```yaml
32
+ - name: Notify Telegram
33
+ if: always()
34
+ env:
35
+ TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
36
+ CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
37
+ run: |
38
+ curl -s "https://api.telegram.org/bot${TOKEN}/sendMessage" \
39
+ -d chat_id="${CHAT}" \
40
+ --data-urlencode text="agent-tick: ${{ job.status }} — $(date -u +%FT%TZ)"
41
+ ```
42
+
43
+ From `src/agent.ts`, the same call via `fetch()` inside `tick()` — gate it so a notification failure never throws into the tick (wrap in try/catch; a lost alert must not stop a dispatch).
44
+
45
+ ### Email (CI step)
46
+
47
+ Simplest reliable path is a provider action in the workflow, e.g. `dawidd6/action-send-mail` with SMTP credentials in secrets:
48
+
49
+ ```yaml
50
+ - name: Email on failure
51
+ if: failure()
52
+ uses: dawidd6/action-send-mail@v3
53
+ with:
54
+ server_address: smtp.example.com
55
+ server_port: 465
56
+ username: ${{ secrets.MAIL_USERNAME }}
57
+ password: ${{ secrets.MAIL_PASSWORD }}
58
+ subject: "Sail agent tick failed"
59
+ to: user@example.com
60
+ from: sailor-agent
61
+ body: "Run ${{ github.run_id }} failed. Check gh run view --log."
62
+ ```
63
+
64
+ For local runs, email is rarely worth a daemon — prefer Telegram, or pipe `.sail/activity.jsonl` into whatever the user already monitors.
65
+
66
+ ## Custom dashboard
67
+
68
+ The stock dashboard (`sailor ui start`) shows account state, mandate health, balances, and activity. A strategy-specific view (price chart + portfolio for a trading agent; health factor + yield for a lending agent) is a small app the assistant builds reading:
69
+
70
+ - `.sail/activity.jsonl` — every dispatch with txHash, gas, denial reasons, owner signing events; append-only JSON lines, trivially tailable.
71
+ - `.sail/account.json` / `.sail/state/mandates.json` — the SMA address and the permission set to display.
72
+ - On-chain state via the SDK: `import { SailorClient } from '@sail.money/sailor/sdk'` for kernel/mandate reads, or any viem `PublicClient` for balances and protocol positions.
73
+
74
+ Keep it local and read-only: a small server (or static page polling a tiny endpoint) that reads `.sail/` and the chain. Do not put keys, passphrases, or write operations in a dashboard. Pick a port outside 3333–3999 (reserved by per-project Sailor UIs) and 3141 (signing station).
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: sail-mandates
3
+ description: The full permission-contract lifecycle — designing bounds with the user, authoring Solidity permissions, Foundry testing, deploying, simulating, and authorizing on the SMA, plus revoke/update/list and clone templates. Use when anything touches a permission contract or the mandate: writing or changing what the agent is allowed to do, deploying or attaching permissions, or verifying them before authorization.
4
+ ---
5
+
6
+ # Sail mandates
7
+
8
+ The lifecycle is an ordered set of gates. **The order is the correctness model** — skipping a gate or reordering them is how funds get lost. Never authorize (attach) anything that has not passed every earlier gate.
9
+
10
+ ## Gate 1 — Pin the strategy bounds with the user
11
+
12
+ Establish, explicitly: tokens, amounts, venues, slippage, recipients. Philosophy: **every meaningful financial bound is enforced on-chain in Solidity**; only frequency/cadence lives in agent TypeScript. If a bound matters and it is not in a permission contract, it is not a bound.
13
+
14
+ ## Gate 2 — Enumerate approvals and pick the execution model
15
+
16
+ List every ERC-20 `approve()` the strategy implies — protocol permissions never cover `approve()`, so each needs explicit coverage. How it is covered depends on the model you choose (read [references/approvals.md](references/approvals.md) before writing any contract):
17
+
18
+ - **Per-call (default).** Approve and act are separate single-call dispatches, each gated by its own `IPermission`. Approve a sufficient allowance once and skip it when the on-chain allowance already covers the next action (the `examples/dca/` pattern). This is what the scaffolded `IPermission` supports out of the box.
19
+ - **Atomic batch (advanced).** Approve + action run as one `dispatchBatch`. A batch consults exactly ONE batch-aware `IBatchPermission` (`evaluateBatch`) that validates the whole sequence — NOT two narrow `IPermission`s. Use this only when atomicity matters; see `references/approvals.md`. Do not mix the models.
20
+
21
+ ## Gate 3 — Author the permission contracts
22
+
23
+ Permission contracts live in `mandates/`. The user authors, reviews, and owns them. Start from the worked examples — see [references/examples-index.md](references/examples-index.md) for what each `examples/permissions/*.sol` teaches — adapt them, never present them as audited or as a closed menu.
24
+
25
+ - Implement `IPermission.evaluate(bytes txData, Context ctx) → bool` (single-call) or `IBatchPermission.evaluateBatch(Call[] calls, BatchContext ctx) → bool` (batch). Interfaces are vendored under `.sail/contracts/`.
26
+ - Use the `SailCalldata` library for bounded calldata decoding — slot-indexed reads after the 4-byte selector prevent silent truncation bugs.
27
+ - Bind recipients/beneficiaries to `ctx.account` wherever the protocol exposes them — funds must route to the SMA.
28
+ - **Selector correctness is life-or-death.** Verify every selector against the venue's authoritative deployed ABI — `cast sig "fn(types…)"` against the verified source — never from memory. A wrong selector fails closed (every legitimate call rejected) or worse, gates nothing. Real precedents: Venice staking is `stake(address,uint256)` = `0xadc9772e`, not `stake(uint256)` = `0xa694fc3a`; GMX v2's `createOrder` struct has changed across router versions — recompute the selector against the exact router the agent calls.
29
+
30
+ Prerequisite — Foundry. If `forge` is not found:
31
+
32
+ ```bash
33
+ curl -L https://foundry.paradigm.xyz | bash # then restart shell
34
+ foundryup
35
+ ```
36
+
37
+ ## Gate 4 — Write and run Foundry tests BEFORE any deployment
38
+
39
+ `test/BoundedCallPermission.t.sol` is the scaffolded example — copy it for each permission you author. Write tests that call `evaluate()` (and `evaluateBatch()` for batch permissions) directly with calldata derived from the user's stated strategy:
40
+
41
+ - **Accept cases**: every call the strategy must make.
42
+ - **Reject cases**: out-of-bounds amounts, wrong tokens, wrong recipients, wrong selectors, unbound venues.
43
+
44
+ ```bash
45
+ forge build
46
+ forge test
47
+ ```
48
+
49
+ This gate comes before deployment because it is the only gate that exercises your boundary logic with full control of inputs, at zero cost. Do not deploy a permission whose tests do not pass.
50
+
51
+ ## Gate 5 — Deploy (deploy only — never --attach yet)
52
+
53
+ ```bash
54
+ sailor mandate deploy --contract <Name> --sma <SMA> --json # BLOCKS — owner signs the contract-creation tx in the browser
55
+ ```
56
+
57
+ The owner pays gas; the deployed address is read from the receipt and tracked in `.sail/state/mandates.json`. Add `--build` to run `forge build` first.
58
+
59
+ Constructor args: `--args '["0xToken","1000000"]'` (JSON array, inline, bash) or `--args-file args.json` (any shell — required on PowerShell). Full per-shell quoting rules: [references/constructor-args.md](references/constructor-args.md). Values are coerced to the constructor's ABI types (uint→bigint, etc.) and the array length is validated.
60
+
61
+ ## Gate 6 — Simulate against must-pass AND must-fail samples
62
+
63
+ `evaluate()` lives on the deployed contract, so simulate after deploy and before the irreversible authorization. Generate sample calls from the user's stated strategy — ones the permission MUST accept and ones it MUST reject:
64
+
65
+ ```bash
66
+ sailor mandate simulate --address <PermissionOrName> --sma <SMA> --calls calls.json --json
67
+ ```
68
+
69
+ This is an off-chain `eth_call` — no gas, no signing. It reports what `evaluate()` returns per call, flags any target with no contract code on this chain (wrong or wrong-chain address), and checks whether each 4-byte selector actually routes on the target's bytecode. A mismatch between `expect` and the actual result exits non-zero. **Zero mismatches required before proceeding.** Simulate proves what the permission DOES; it does not guarantee it is correct.
70
+
71
+ `calls.json` schema: [references/calls-schema.md](references/calls-schema.md). How to design pass/fail cases: [references/simulate-calls.md](references/simulate-calls.md).
72
+
73
+ **Batch permissions:** simulate probes single-call `evaluate()` only — it does not exercise `evaluateBatch()`. Verify batch permissions by calling `evaluateBatch(calls, ctx)` directly via `cast call` with pass and fail batches before attaching.
74
+
75
+ ## Gate 7 — Attach (authorize)
76
+
77
+ ```bash
78
+ sailor mandate attach --address <PermissionOrName> --sma <SMA> --json # BLOCKS — owner signs RegisterPermission EIP-712 in the browser
79
+ ```
80
+
81
+ Only now is the permission live. The owner (mandate signer) signs in the browser; the agent submits the registration and pays gas plus any registration fee. **Fund the agent wallet before attaching**, or this step fails with `gas required exceeds allowance`. The CLI verifies the signature came from the on-chain mandate signer — a wrong connected wallet is rejected. After confirmation it polls `getPermissions()` until the permission appears. Per-call model: attach every permission the strategy needs (bounded-approve alongside the protocol permission) in one signing session.
82
+
83
+ ## Maintenance
84
+
85
+ - `sailor mandate revoke --address <P> --sma <SMA> --json` (or `--all`) — owner signs `RevokePermissions` in the browser (BLOCKS); agent submits. Revocations are recorded to the activity log; `state/mandates.json` keeps the historical record.
86
+ - `sailor mandate update --address <P> --name/--source-path/--artifact-path` — fix tracked metadata.
87
+ - `sailor mandate list` — everything deployed from this project, with attachments.
88
+ - `sailor mandate sign` — reviews the permission set and reconciles against live on-chain `getPermissions()` before writing `mandate.json`; permissions revoked on-chain are excluded even if still in local state. `--yes` for non-interactive use.
89
+ - `sailor account rotate-signer` — rotates the agent wallet and re-approves attached mandates (BLOCKS on browser); `--reattach-only` resumes after funding, `--list` shows known agent wallets.
90
+
91
+ ## Clone templates (deploy-clone)
92
+
93
+ `sailor mandate deploy-clone --template boundedApprove --sma <SMA> --tokens <csv> --spenders <csv> --max <wei> --json` deploys + registers an EIP-1167 clone of a published implementation in one transaction (owner signs `RegisterPermission` for the predicted clone address — BLOCKS; agent submits `deployAndAttach`). The only template key is `boundedApprove`. Implementations come from the SDK deployment registry (`standaloneTemplates`) — currently **empty on all six chains** pending redeployment against the new kernel, so deploy-clone errors with a clear message and you should write and deploy a bounded-approve permission with `sailor mandate deploy` instead. Check availability with `sailor mandate templates --json`.
@@ -0,0 +1,42 @@
1
+ # ERC-20 approve coverage
2
+
3
+ ## The rule
4
+
5
+ Protocol permissions (supply, swap, deposit, stake, …) do NOT cover ERC-20 `approve()`. The kernel evaluates the approve as its own call: an agent that calls `approve()` without authorizing coverage for it is rejected, and the tick fails.
6
+
7
+ There are two ways to cover and execute an approve + action. Pick ONE; they are not mixable.
8
+
9
+ ## Model A — per-call (default, simplest)
10
+
11
+ Approve and action are **separate single-call dispatches**, each gated by its own `IPermission`. This is what the scaffolded `IPermission` interface and the `examples/dca/` agent use.
12
+
13
+ 1. Deploy a bounded-approve `IPermission` covering the `(token, spender, max amount)` triple — a clone where available:
14
+ ```bash
15
+ sailor mandate deploy-clone --template boundedApprove --sma <SMA> \
16
+ --tokens <token,...> --spenders <spender,...> --max <amount>
17
+ ```
18
+ or a custom `IPermission` that bounds the approve's spender and amount.
19
+ 2. Deploy the protocol `IPermission` (swap/supply/…). Attach both in one signing session.
20
+ 3. At runtime, manage the allowance instead of approving every tick: read the on-chain allowance, emit an approve dispatch ONLY when it is insufficient, approving a large amount so subsequent ticks skip it. The DCA example does exactly this — it returns the approve as its own tick's dispatch, then swaps on later ticks once the allowance is set.
21
+
22
+ This needs no batch support and works on every kernel.
23
+
24
+ ## Model B — atomic batch (advanced)
25
+
26
+ Approve + action run as one `dispatchBatch` — atomic, single transaction. A batch dispatch consults **exactly one batch-aware `IBatchPermission`**, never a pair of `IPermission`s:
27
+
28
+ - The permission implements `@sail/interfaces/IBatchPermission.sol` — `evaluateBatch(Call[] calls, BatchContext ctx)` validates the WHOLE sequence (ordering, the approve's spender/amount, the action, and mandatory allowance cleanup) and `isBatchPermission()` returns true. `examples/permissions/BoundedApproveAndCallBatch.sol` is the model.
29
+ - A normal `IPermission` placed in a batch is rejected by the kernel with `PermissionNotBatchAware`. You cannot assemble a batch from two narrow per-call permissions — that was the trap.
30
+ - Batch is a **selective-kernel** feature (`dispatchBatch` / `previewBatch`); conjunctive kernels have neither. Confirm the model with `sailor doctor`; details in `docs/PERMISSION_MODEL.md`.
31
+ - At runtime, return one `Dispatch` whose `calls` array is `[approveCall, actionCall]`; the runner detects `calls.length > 1` and routes through `dispatch.batch`.
32
+
33
+ Use Model B only when atomicity genuinely matters (e.g. the approve must not be observable between calls). Otherwise prefer Model A — less contract to author and test.
34
+
35
+ ## Verifying each model
36
+
37
+ - **Model A:** `sailor mandate simulate` probes each `IPermission`'s single-call `evaluate()`. See `simulate-calls.md`.
38
+ - **Model B:** `simulate` does NOT cover batch permissions. Validate the exact `[approve, action]` sequence through the kernel's `previewBatch` view (no gas, no signing) — `sailor run --once` exercises this path against a registered batch permission, or call the permission's `evaluateBatch` view directly with `cast call`.
39
+
40
+ ## Kernel-model corollary (conjunctive only)
41
+
42
+ On a conjunctive kernel ALL registered permissions must return true on EVERY call, so two narrow approve permissions (one per token) brick each other. Use ONE approve permission allowing all needed tokens. On selective kernels — all currently bundled chains — each dispatch names exactly one permission, so this does not apply.
@@ -0,0 +1,42 @@
1
+ # calls.json — batch schema for `sailor mandate simulate`
2
+
3
+ A non-empty JSON array. Each entry is one sample call probed against the permission's `evaluate()`:
4
+
5
+ ```json
6
+ [
7
+ {
8
+ "target": "0xVenueContract",
9
+ "calldata": "0x04e45aaf…",
10
+ "value": "0",
11
+ "expect": "pass",
12
+ "label": "swap 100 USDC → WETH within bounds"
13
+ },
14
+ {
15
+ "target": "0xVenueContract",
16
+ "calldata": "0x04e45aaf…",
17
+ "expect": "fail",
18
+ "label": "swap exceeding MAX_AMOUNT_IN — must be rejected"
19
+ }
20
+ ]
21
+ ```
22
+
23
+ | Field | Required | Meaning |
24
+ |---|---|---|
25
+ | `target` | yes | Call target address (the venue contract) |
26
+ | `calldata` | yes | 0x-prefixed hex calldata (`data` also accepted as the key) |
27
+ | `value` | no | ETH value in wei, integer or numeric string; default `0` |
28
+ | `expect` | no | `"pass"` or `"fail"`. Any mismatch with the actual result makes the command exit non-zero |
29
+ | `label` | no | Human-readable description shown per result; defaults to `call N` |
30
+
31
+ ## What simulate reports per call
32
+
33
+ - `result`: `pass` (evaluate returned true) or `fail`, with `reverted`/`revertReason` when evaluate() reverted rather than returning false.
34
+ - `targetHasCode`: whether the target has contract code on this chain — `false` means a wrong or wrong-chain address; that call would fail on-chain regardless of the permission.
35
+ - `selectorRoutes`: whether the calldata's 4-byte selector is found in the target's bytecode (`null` for proxies, where the check is indeterminate). `false` strongly suggests a wrong selector.
36
+ - `match`: per-call expectation verdict; `mismatches` summarizes. JSON output (`--json`) carries all of the above plus `submitterIsStandIn` (no local manager key — a stand-in submitter was used) and `blockContextStale` (block fetch failed; time/block-gated permissions may show false negatives).
37
+
38
+ ## Rules of use
39
+
40
+ - Derive samples from the user's stated strategy: every call the agent must make → `expect: "pass"`; boundary violations (too-large amount, wrong token, wrong recipient, wrong venue) → `expect: "fail"`.
41
+ - Do not authorize until every sample matches — zero mismatches.
42
+ - Batch permissions: simulate exercises single-call `evaluate()` only. Probe `evaluateBatch(calls, ctx)` directly with `cast call` for batch verification.
@@ -0,0 +1,45 @@
1
+ # Constructor args — per-shell quoting
2
+
3
+ `sailor mandate deploy` takes constructor arguments as a JSON array. The CLI validates the array length against the constructor's ABI and coerces each value to its ABI type: `uint*`/`int*` → bigint (pass numbers as strings to avoid precision loss), `bool` → boolean, arrays recursively.
4
+
5
+ ## Bash / Git Bash / zsh
6
+
7
+ Single quotes around the whole array; inner double quotes survive:
8
+
9
+ ```bash
10
+ sailor mandate deploy --contract <Name> --args '["0xToken","1000000"]' --sma <SMA>
11
+ ```
12
+
13
+ Nested arrays work the same way:
14
+
15
+ ```bash
16
+ sailor mandate deploy --contract <Name> --args '["0xSigner",["0xTargetA","0xTargetB"]]' --sma <SMA>
17
+ ```
18
+
19
+ ## PowerShell
20
+
21
+ Inline JSON is unreliable in PowerShell — quote stripping mangles the array even with escaped inner quotes. **Do not pass `--args` inline from PowerShell.** Use `--args-file`:
22
+
23
+ ```powershell
24
+ sailor mandate deploy --contract <Name> --args-file args.json --sma <SMA>
25
+ ```
26
+
27
+ (If you must inline, the escaped form is `--args '[\"0xToken\",\"1000000\"]'` — but prefer the file.)
28
+
29
+ ## Any shell — `--args-file` (recommended whenever quoting bites)
30
+
31
+ Write the array to a file, no quoting rules at all:
32
+
33
+ ```json
34
+ ["0xToken", "1000000"]
35
+ ```
36
+
37
+ ```bash
38
+ sailor mandate deploy --contract <Name> --args-file args.json --sma <SMA>
39
+ ```
40
+
41
+ ## Errors you will see
42
+
43
+ - `Constructor takes no arguments but --args were provided` — drop `--args`.
44
+ - `Constructor expects N argument(s)` — pass exactly N elements, in declaration order.
45
+ - `--args must be a JSON array of N element(s)` — the JSON parsed but the length is wrong, or it is not an array (a shell ate your quotes).
@@ -0,0 +1,31 @@
1
+ # examples/permissions/ — index
2
+
3
+ Worked reference permissions, one bounding pattern each. They are **not audited, not maintained by Sail core, and not a menu** — adapt them to the user's strategy. Each contract's header documents what is ENFORCED ON-CHAIN vs AGENT-ENFORCED / NOT BOUNDED — read that split before borrowing anything.
4
+
5
+ Permissions only bound on-chain actions: venues with off-chain order matching (Polymarket, Hyperliquid) cannot be bounded this way; fully on-chain venues (Uniswap, Aave, GMX, Limitless) can.
6
+
7
+ | File | Protocol / chain | Teaches | Key verified selectors |
8
+ |---|---|---|---|
9
+ | `BoundedSwap_UniswapV3_Base.sol` | Uniswap V3 SwapRouter02 · Base | Selector allowlist + amount cap + tokenOut allowlist + min-out slippage floor (MIN_BPS) | `0x04e45aaf` exactInputSingle (SwapRouter02), `0x095ea7b3` approve |
10
+ | `BoundedSwap_UniswapV4_Unichain.sol` | Uniswap V4 Universal Router · Unichain | Decoding command/action bytes: single V4_SWAP command, single SWAP_EXACT_IN_SINGLE action, currency + amount + slippage bounds | `0x3593564c` execute(bytes,bytes[],uint256), `0x24856bc3` execute(bytes,bytes[]) |
11
+ | `BoundedBorrow_AaveV3_Arbitrum.sol` | Aave V3 Pool · Arbitrum | Asset allowlist + borrow cap + `onBehalfOf == ctx.account` binding + rate-mode allowlist (use [2]; stable deprecated in V3.1) | `0xa415bcad` borrow(address,uint256,uint256,uint16,address) |
12
+ | `BoundedSupply_AaveV3_Arbitrum.sol` | Aave V3 Pool · Arbitrum | SailCalldata-based decoding; supply cap + `onBehalfOf == ctx.account`; supply does NOT gate withdrawal, and the prior approve needs its own permission | `0x617ba037` supply(address,uint256,address,uint16) |
13
+ | `BoundedVault_ERC4626_Base.sol` | ERC-4626 standard · any EVM | Vault allowlist; deposit capped + receiver bound; withdraw/redeem receiver+owner bound but amount-unbounded (SMA can always fully exit) | `0x6e553f65` deposit, `0xb460af94` withdraw, `0xba087652` redeem |
14
+ | `BoundedStake_Venice_Base.sol` | Venice (VVV) staking · Base | Recipient binding on stake; claim() structurally routes rewards to caller (the SMA) | `0xadc9772e` stake(address,uint256), `0x4e71d92d` claim() |
15
+ | `BoundedTransfer_ERC20_Ethereum.sol` | ERC-20 · any EVM | Token allowlist + recipient allowlist + per-transfer cap; caps are per-token base units (USDC 6 decimals, WETH 18) — one permission per token if amounts differ | `0xa9059cbb` transfer(address,uint256) |
16
+ | `BoundedPerp_GMXv2_Arbitrum.sol` | GMX v2 ExchangeRouter · Arbitrum | Market allowlist + collateral cap + size cap (USD is 1e30-scaled) + long/short flags on a struct-typed call | `0x212234c3` createOrder(CreateOrderParams) — MUST re-verify, see below |
17
+ | `BoundedBet_Limitless_Base.sol` | Limitless CTF exchange · Base | Condition allowlist + stake cap + outcome allowlist on a prediction market | buy(bytes32,uint256,uint256) — **ABI UNVERIFIED**, see below |
18
+ | `BoundedApproveAndCallBatch.sol` | Sail batch dispatch · any selective kernel | `IBatchPermission`: atomic approve → consume → reset-to-0 in exactly 3 calls; single-call evaluate() deliberately returns false | `0x095ea7b3` approve (calls 0 and 2) |
19
+
20
+ ## Lessons the examples encode
21
+
22
+ - **Venice — the wrong-selector bug.** The live contract's function is `stake(address,uint256)` = `0xadc9772e`. The intuitive single-arg `stake(uint256)` = `0xa694fc3a` does not exist on the target. Gating the wrong selector fails closed: every legitimate stake silently rejected. Always confirm the selector against the deployed contract.
23
+ - **GMX — versioned ABIs drift.** GMX has multiple ExchangeRouter deployments and the `CreateOrderParams` struct has gained fields over time. Before deploying: pick the exact router the agent calls, read its verified ABI, recompute with `cast sig "createOrder(<exact tuple>)"`, and update the selector and struct if they differ. The committed `0x212234c3` is verified against gmx-io/gmx-synthetics at time of writing — re-verify anyway.
24
+ - **Limitless — unverified ABI as a worked warning.** The exchange address and `buy` signature are inferred from CTF patterns, not verified against the deployed contract. The contract's own header lists the verification steps; do them before any deploy.
25
+ - **Batch permissions and simulate.** `BoundedApproveAndCallBatch` bounds the token/spender/amount and the consuming target+selector, optionally amount-matching the consume to the approve — but it does NOT bound where the consuming call routes funds. Pair it with a recipient-binding single-call permission, or choose consuming selectors that structurally pay msg.sender. `sailor mandate simulate` cannot exercise `evaluateBatch()` — verify with `cast call` pass/fail batches.
26
+
27
+ ## Interfaces (vendored in `examples/permissions/interfaces/`)
28
+
29
+ - `IPermission.evaluate(bytes txData, Context ctx) → bool`; Context: `account, manager, submitter, target, selector, value, blockTimestamp, blockNumber`.
30
+ - `IBatchPermission.evaluateBatch(Call[] calls, BatchContext ctx) → bool` + `isBatchPermission() → true`; Call: `(target, value, data)`; BatchContext: `account, manager, submitter, permission, batchHash, blockTimestamp, blockNumber`.
31
+ - `SailCalldata.sol`: bounded slot-indexed calldata readers (`hasParams`, `asAddress`, `asUint256`, …) — use these instead of `abi.decode` to avoid wrong-offset reads.
@@ -0,0 +1,58 @@
1
+ # Designing simulate cases (calls.json)
2
+
3
+ ## Schema
4
+
5
+ `calls.json` is an array of sample calls. Full field reference: [calls-schema.md](calls-schema.md).
6
+
7
+ ```json
8
+ [
9
+ {
10
+ "label": "approve USDC to router at cap",
11
+ "target": "0xA0b8...USDC",
12
+ "calldata": "0x095ea7b3...",
13
+ "expect": "pass"
14
+ },
15
+ {
16
+ "label": "approve over the cap",
17
+ "target": "0xA0b8...USDC",
18
+ "calldata": "0x095ea7b3...",
19
+ "expect": "fail"
20
+ }
21
+ ]
22
+ ```
23
+
24
+ Fields: `target`, `calldata`, optional `value`, `expect` (`"pass"` or `"fail"`), `label`.
25
+
26
+ ## Deriving cases from the strategy
27
+
28
+ **Must-pass:** one entry per distinct call the agent will make — the approve at its cap, the swap/supply with exact in-bounds parameters, and so on.
29
+
30
+ **Must-fail:** mutate each bound one at a time:
31
+
32
+ - wrong token, spender, or recipient
33
+ - amount over the cap
34
+ - slippage / minimum-out below the floor
35
+ - wrong target contract; wrong selector
36
+ - nonzero ETH `value` when not allowed
37
+
38
+ A permission simulated with no must-fail cases is untested — passing everything is exactly how a broken (too-permissive) permission behaves.
39
+
40
+ ## Running
41
+
42
+ ```bash
43
+ # batch
44
+ sailor mandate simulate --address <PermissionOrName> --sma <SMA> --calls calls.json
45
+
46
+ # one call inline
47
+ sailor mandate simulate --address <PermissionOrName> --sma <SMA> \
48
+ --target <addr> --calldata <hex> --expect pass
49
+ ```
50
+
51
+ - Off-chain `eth_call` — no gas, no signing, uses the same evaluation context as the runner.
52
+ - Flags any target with no contract code (wrong or wrong-chain address).
53
+ - Any `expect` mismatch → non-zero exit → do NOT attach. `--json` for automation.
54
+
55
+ ## Limits
56
+
57
+ - Simulates the single-call `evaluate()` only — batch permissions need a direct `cast call` to their batch view; see `approvals.md`.
58
+ - Proves behavior, not intent: the contract still needs review and forge tests.