@dev.sail.money/sailor 0.1.0-local → 1.0.0-39

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 (153) hide show
  1. package/AGENTS.md +139 -140
  2. package/LICENSE +21 -21
  3. package/README.md +428 -430
  4. package/docs/PERMISSION_MODEL.md +93 -93
  5. package/examples/permissions/BoundedApproveAndCallBatch.sol +179 -179
  6. package/examples/permissions/BoundedBet_Limitless_Base.sol +97 -97
  7. package/examples/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +94 -94
  8. package/examples/permissions/BoundedPerp_GMXv2_Arbitrum.sol +154 -154
  9. package/examples/permissions/BoundedStake_Venice_Base.sol +85 -85
  10. package/examples/permissions/BoundedSupply_AaveV3_Arbitrum.sol +82 -82
  11. package/examples/permissions/BoundedSwap_UniswapV3_Base.sol +116 -116
  12. package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +150 -150
  13. package/examples/permissions/BoundedTransfer_ERC20_Ethereum.sol +73 -73
  14. package/examples/permissions/BoundedVault_ERC4626_Base.sol +97 -97
  15. package/examples/permissions/README.md +79 -79
  16. package/examples/permissions/SailCalldata.sol +118 -118
  17. package/examples/permissions/foundry.toml +10 -10
  18. package/examples/permissions/interfaces/IBatchPermission.sol +38 -38
  19. package/examples/permissions/interfaces/IPermission.sol +18 -18
  20. package/package.json +44 -45
  21. package/packages/cli/README.md +34 -34
  22. package/packages/cli/dist/index.cjs +734 -705
  23. package/packages/cli/dist/server.cjs +627 -538
  24. package/packages/sdk/README.md +65 -65
  25. package/packages/sdk/dist/intelligence.d.ts +1 -1
  26. package/packages/sdk/dist/intelligence.js +1 -1
  27. package/packages/sdk/package.json +80 -80
  28. package/packages/ui/dist/assets/{add-BxpXfVWe.js → add-Gzf62xlX.js} +1 -1
  29. package/packages/ui/dist/assets/{all-wallets-BKTn_sWK.js → all-wallets-O-pI4o8v.js} +1 -1
  30. package/packages/ui/dist/assets/{app-store-CfuKbwxR.js → app-store-CeSLaOaQ.js} +1 -1
  31. package/packages/ui/dist/assets/{apple-BKSBbNYg.js → apple-FGNyQM-D.js} +1 -1
  32. package/packages/ui/dist/assets/{arrow-bottom-D4bG6gZi.js → arrow-bottom-C1fusORF.js} +1 -1
  33. package/packages/ui/dist/assets/{arrow-bottom-circle-BNTs1p0T.js → arrow-bottom-circle-AvK1VEpN.js} +1 -1
  34. package/packages/ui/dist/assets/{arrow-left-2uee3vYv.js → arrow-left-Bu-hq4Ep.js} +1 -1
  35. package/packages/ui/dist/assets/{arrow-right-BktjMV6h.js → arrow-right-XbZESmct.js} +1 -1
  36. package/packages/ui/dist/assets/{arrow-top-Izu28fX4.js → arrow-top-DvkVHbhX.js} +1 -1
  37. package/packages/ui/dist/assets/{bank-USBaAyFM.js → bank-DTThWRvC.js} +1 -1
  38. package/packages/ui/dist/assets/{basic-C_9KjTEH.js → basic-B9AMgqFE.js} +1 -1
  39. package/packages/ui/dist/assets/{browser-DAEMAKV7.js → browser-Bhnivm4i.js} +1 -1
  40. package/packages/ui/dist/assets/{card-DT8yDkKN.js → card-DjIlyU55.js} +1 -1
  41. package/packages/ui/dist/assets/{ccip-CkqfGSxX.js → ccip-DPAiKntc.js} +1 -1
  42. package/packages/ui/dist/assets/{checkmark-CsgdEXFj.js → checkmark-DSVYfoVl.js} +1 -1
  43. package/packages/ui/dist/assets/{checkmark-bold-D2gjOQo2.js → checkmark-bold-BFkw_Q5g.js} +1 -1
  44. package/packages/ui/dist/assets/{chevron-bottom-tprFynYV.js → chevron-bottom-CyCgyOwY.js} +1 -1
  45. package/packages/ui/dist/assets/{chevron-left-D2Zj1gNB.js → chevron-left-DTuO2WLr.js} +1 -1
  46. package/packages/ui/dist/assets/{chevron-right-D1rRuAVe.js → chevron-right-DwB5FZj8.js} +1 -1
  47. package/packages/ui/dist/assets/{chevron-top-24dL1mbL.js → chevron-top-DKukdWvg.js} +1 -1
  48. package/packages/ui/dist/assets/{chrome-store-Vy-5niYX.js → chrome-store-Csz4L9Ls.js} +1 -1
  49. package/packages/ui/dist/assets/{clock-qBjLnVdJ.js → clock-Bg6488Gw.js} +1 -1
  50. package/packages/ui/dist/assets/{close-DARDwgcu.js → close-BxAJGBxP.js} +1 -1
  51. package/packages/ui/dist/assets/{coinPlaceholder-BvpIbPlD.js → coinPlaceholder-CCJVgW9w.js} +1 -1
  52. package/packages/ui/dist/assets/{compass-BMTO0ayt.js → compass-CSQSZaqJ.js} +1 -1
  53. package/packages/ui/dist/assets/{copy-PaXeRHza.js → copy-CqlzXVB-.js} +1 -1
  54. package/packages/ui/dist/assets/{core-BFnStQd-.js → core-ClvdTrpG.js} +3 -3
  55. package/packages/ui/dist/assets/cursor-CKKwWhGQ.js +3 -0
  56. package/packages/ui/dist/assets/{cursor-transparent-BEMdi-8q.js → cursor-transparent-C1VOGz11.js} +1 -1
  57. package/packages/ui/dist/assets/{desktop-CfuLLThw.js → desktop-QiLednKV.js} +1 -1
  58. package/packages/ui/dist/assets/{disconnect-DhwgJMiR.js → disconnect-Bx2TgkML.js} +1 -1
  59. package/packages/ui/dist/assets/{discord-po8qoN1s.js → discord-6MWX5Rbb.js} +1 -1
  60. package/packages/ui/dist/assets/{etherscan-BEsz0_yx.js → etherscan-CodIrmJK.js} +1 -1
  61. package/packages/ui/dist/assets/{events-Bz33Unzu.js → events-DOEm-LTy.js} +1 -1
  62. package/packages/ui/dist/assets/{exclamation-triangle-7CjTAGOQ.js → exclamation-triangle-Dwr5oCsh.js} +1 -1
  63. package/packages/ui/dist/assets/{extension-CmxjEWEt.js → extension-C-SoZx1s.js} +1 -1
  64. package/packages/ui/dist/assets/{external-link-CmQ--bNS.js → external-link-BiDYH90C.js} +1 -1
  65. package/packages/ui/dist/assets/{facebook-CIBn9b65.js → facebook-Bm27AlfS.js} +1 -1
  66. package/packages/ui/dist/assets/{fallback-DATyrQlb.js → fallback-Bwpmpy13.js} +1 -1
  67. package/packages/ui/dist/assets/{farcaster-OJ3Jasxg.js → farcaster-CSW-SjzS.js} +1 -1
  68. package/packages/ui/dist/assets/{filters-D4x09zeL.js → filters-j3dR7AJK.js} +1 -1
  69. package/packages/ui/dist/assets/{github-ZlIuMArp.js → github-CQMTSSgW.js} +1 -1
  70. package/packages/ui/dist/assets/{google-Gwg85sfv.js → google-BBIVBfAd.js} +1 -1
  71. package/packages/ui/dist/assets/{help-circle-D1uOWYcX.js → help-circle-CEvTLelF.js} +1 -1
  72. package/packages/ui/dist/assets/{id-C0-5UdYk.js → id-CYRVgSgN.js} +1 -1
  73. package/packages/ui/dist/assets/{image-D_DUsv8-.js → image-Cm9Ep5G0.js} +1 -1
  74. package/packages/ui/dist/assets/{index-DdbJhIdl.js → index-4wdo7Ga_.js} +3 -3
  75. package/packages/ui/dist/assets/{index-CrYzBWfD.js → index-BrP8m1ZI.js} +1 -1
  76. package/packages/ui/dist/assets/index-C2PQCECq.css +1 -0
  77. package/packages/ui/dist/assets/{index-DiojfeVM.js → index-DZ07nuwB.js} +1 -1
  78. package/packages/ui/dist/assets/{index-BCzex_R6.js → index-De_P6mNS.js} +1 -1
  79. package/packages/ui/dist/assets/index-DrQ9A8dp.js +1775 -0
  80. package/packages/ui/dist/assets/{index-izd7vu_r.js → index-Z55BVE94.js} +1 -1
  81. package/packages/ui/dist/assets/{index.es-DdkHhQAj.js → index.es-DnT9Uzwt.js} +4 -4
  82. package/packages/ui/dist/assets/{info-CiRd_kEG.js → info-DdYqiFMu.js} +1 -1
  83. package/packages/ui/dist/assets/{info-circle-ypxjqarK.js → info-circle-DkD9oY-S.js} +1 -1
  84. package/packages/ui/dist/assets/{lightbulb-B-pxLxd8.js → lightbulb-7Q3AhpSP.js} +1 -1
  85. package/packages/ui/dist/assets/{mail-BYmicuVZ.js → mail-CPagdnfp.js} +1 -1
  86. package/packages/ui/dist/assets/{metamask-sdk-Ccl6DG7Q.js → metamask-sdk-CQ4tzR6A.js} +1 -1
  87. package/packages/ui/dist/assets/{mobile-CtP5PqVT.js → mobile-01GVSpey.js} +1 -1
  88. package/packages/ui/dist/assets/{more-6C2733we.js → more-BPXPYrvy.js} +1 -1
  89. package/packages/ui/dist/assets/{network-placeholder-CdhxMzqd.js → network-placeholder-CII8WrTF.js} +1 -1
  90. package/packages/ui/dist/assets/{nftPlaceholder-DVmTWEAY.js → nftPlaceholder-DG5rjRzx.js} +1 -1
  91. package/packages/ui/dist/assets/{off-DNYLughs.js → off-eemo7R2q.js} +1 -1
  92. package/packages/ui/dist/assets/{parseSignature-Dq2B5Bu3.js → parseSignature-CaRSntRi.js} +1 -1
  93. package/packages/ui/dist/assets/{play-store-D7Qut5ta.js → play-store-SIqMwLur.js} +1 -1
  94. package/packages/ui/dist/assets/{plus-kqMyjt3q.js → plus-DemTM2Nx.js} +1 -1
  95. package/packages/ui/dist/assets/{qr-code-DiUCWRbz.js → qr-code-BjolKhQv.js} +1 -1
  96. package/packages/ui/dist/assets/{recycle-horizontal-Boe3XiS-.js → recycle-horizontal-BluxSqWj.js} +1 -1
  97. package/packages/ui/dist/assets/{refresh-CrBgBQYO.js → refresh-Cto5auO0.js} +1 -1
  98. package/packages/ui/dist/assets/{reown-logo-CFZCCHSx.js → reown-logo-lrWmaeNj.js} +1 -1
  99. package/packages/ui/dist/assets/{search-ChTDrghU.js → search-SNmrxLL7.js} +1 -1
  100. package/packages/ui/dist/assets/{secp256k1-DAV5Q_FR.js → secp256k1-SI0Bxirn.js} +1 -1
  101. package/packages/ui/dist/assets/{send-DLFbBFe1.js → send-Do8kdKTu.js} +1 -1
  102. package/packages/ui/dist/assets/{swapHorizontal-BEs3emfG.js → swapHorizontal-HBP1koQV.js} +1 -1
  103. package/packages/ui/dist/assets/{swapHorizontalBold-CC-Hfa7W.js → swapHorizontalBold-B9g1LqXn.js} +1 -1
  104. package/packages/ui/dist/assets/{swapHorizontalMedium-BmR0H8DC.js → swapHorizontalMedium-C8JebI_2.js} +1 -1
  105. package/packages/ui/dist/assets/{swapHorizontalRoundedBold-BdP5NGIH.js → swapHorizontalRoundedBold-37eEYoAp.js} +1 -1
  106. package/packages/ui/dist/assets/{swapVertical-CPrGEJPY.js → swapVertical-DmacpIGs.js} +1 -1
  107. package/packages/ui/dist/assets/{telegram-CxNoZ80Q.js → telegram-Dq_CUch4.js} +1 -1
  108. package/packages/ui/dist/assets/{three-dots-BRa6SBpL.js → three-dots-DbkqGUCU.js} +1 -1
  109. package/packages/ui/dist/assets/{twitch-BC338bG5.js → twitch-iG0Ncwwy.js} +1 -1
  110. package/packages/ui/dist/assets/{twitterIcon-BGZmt2i9.js → twitterIcon-CNbKQx87.js} +1 -1
  111. package/packages/ui/dist/assets/{verify-CEstW0zw.js → verify-BfXT7L3L.js} +1 -1
  112. package/packages/ui/dist/assets/{verify-filled-OkZb0weU.js → verify-filled-BxqxA6xc.js} +1 -1
  113. package/packages/ui/dist/assets/{w3m-modal-pS09ECwE.js → w3m-modal-DeDYqwYJ.js} +1 -1
  114. package/packages/ui/dist/assets/{wallet-BXVKCgC9.js → wallet-BXsUR9Tj.js} +1 -1
  115. package/packages/ui/dist/assets/{wallet-placeholder-C_kNhB1c.js → wallet-placeholder-bbWbfkZu.js} +1 -1
  116. package/packages/ui/dist/assets/{walletconnect-CRKIuUHH.js → walletconnect-WsTWE17z.js} +1 -1
  117. package/packages/ui/dist/assets/{warning-circle-DB2NnwlJ.js → warning-circle-cNSUigh6.js} +1 -1
  118. package/packages/ui/dist/assets/{x-DT4RmwL5.js → x-gNtNGE0N.js} +1 -1
  119. package/packages/ui/dist/index.html +14 -14
  120. package/scripts/check-docs.mjs +262 -262
  121. package/scripts/check-init.mjs +108 -108
  122. package/templates/custom-mandate/.sail/contracts/interfaces/IPermission.sol +18 -18
  123. package/templates/custom-mandate/README.md +116 -116
  124. package/templates/custom-mandate/foundry.toml +8 -8
  125. package/templates/custom-mandate/mandates/BoundedCallPermission.sol +41 -41
  126. package/templates/custom-mandate/mandates/README.md +16 -16
  127. package/templates/custom-mandate/mandates/SailCalldata.sol +118 -118
  128. package/templates/default/.cursor/rules +25 -25
  129. package/templates/default/.env.example +20 -20
  130. package/templates/default/.github/workflows/agent-tick.yml +33 -33
  131. package/templates/default/.sail/README.md +13 -13
  132. package/templates/default/.sail/config.json +10 -10
  133. package/templates/default/AGENTS.md +171 -171
  134. package/templates/default/CLAUDE.md +2 -2
  135. package/templates/default/README.md +16 -16
  136. package/templates/default/_gitignore +13 -13
  137. package/templates/default/docs/PERMISSION_MODEL.md +93 -93
  138. package/templates/default/examples/dca/README.md +16 -16
  139. package/templates/default/examples/dca/agent.ts +174 -174
  140. package/templates/default/examples/dca/mandate.ts +45 -45
  141. package/templates/default/package.json +17 -17
  142. package/templates/default/src/agent.ts +37 -37
  143. package/templates/default/src/config.ts +24 -24
  144. package/templates/default/src/mandate.ts +22 -22
  145. package/templates/default/tsconfig.json +17 -17
  146. package/templates/default/ui/README.md +3 -3
  147. package/templates/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +84 -84
  148. package/templates/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +97 -97
  149. package/templates/lifi-permissions/README.md +53 -53
  150. package/packages/ui/dist/assets/cursor-BDvw-B17.js +0 -3
  151. package/packages/ui/dist/assets/index-BUhrHLpY.js +0 -1775
  152. package/packages/ui/dist/assets/index-Cq02kQmy.css +0 -1
  153. package/scripts/postinstall.js +0 -81
@@ -1,108 +1,108 @@
1
- #!/usr/bin/env node
2
- /**
3
- * `sailor init` smoke test.
4
- *
5
- * Scaffolds a fresh project from the in-tree CLI bundle into a temp dir and
6
- * asserts the scaffold succeeded. This exists to catch the class of regression
7
- * the doc-drift gate structurally cannot — e.g. `packageRoot()` resolving to a
8
- * `bin.sailor` package that ships no `templates/`, which made `init` fail from a
9
- * monorepo checkout with "Template ... not found. Available: none".
10
- *
11
- * It runs the REAL built bundle from a monorepo layout, which is exactly the
12
- * in-tree path that broke before. Pure Node + child_process; the only build
13
- * dependency is the CLI bundle (`pnpm --filter sailor build`).
14
- *
15
- * Run: node scripts/check-init.mjs (CI builds the CLI first)
16
- * Exit: 0 = scaffold OK, 1 = failure (prints what was missing).
17
- */
18
-
19
- import { execFileSync } from "node:child_process";
20
- import fs from "node:fs";
21
- import os from "node:os";
22
- import path from "node:path";
23
- import { fileURLToPath } from "node:url";
24
-
25
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
26
- const BUNDLE = path.join(ROOT, "packages/cli/dist/index.cjs");
27
- const PROJECT = "smoke-agent";
28
-
29
- function fail(msg) {
30
- console.error(`✗ init smoke test FAILED: ${msg}`);
31
- process.exit(1);
32
- }
33
-
34
- if (!fs.existsSync(BUNDLE)) {
35
- fail(`CLI bundle not found at ${BUNDLE}.\n Build it first: pnpm --filter sailor build`);
36
- }
37
-
38
- // Scaffold into a temp dir. `init` requires the destination to live inside the
39
- // process cwd, so we run the bundle with cwd set to a fresh temp root.
40
- const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sailor-init-smoke-"));
41
- const dest = path.join(tmpRoot, PROJECT);
42
-
43
- try {
44
- let stdout = "";
45
- try {
46
- stdout = execFileSync(process.execPath, [BUNDLE, "init", PROJECT], {
47
- cwd: tmpRoot,
48
- encoding: "utf-8",
49
- stdio: ["ignore", "pipe", "pipe"],
50
- });
51
- } catch (err) {
52
- const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
53
- fail(`\`sailor init ${PROJECT}\` exited non-zero.\n ${out || err.message}`);
54
- }
55
-
56
- // A successful fresh init prints the single-line handoff to the AI assistant.
57
- if (!/say start/i.test(stdout)) {
58
- fail(`init did not report success.\n stdout: ${stdout.trim()}`);
59
- }
60
-
61
- // Assert the scaffold landed.
62
- const mustExist = [
63
- ".sail/config.json",
64
- "package.json",
65
- "foundry.toml",
66
- "mandates",
67
- "AGENTS.md",
68
- ];
69
- for (const rel of mustExist) {
70
- if (!fs.existsSync(path.join(dest, rel))) fail(`expected scaffolded "${rel}" — not found`);
71
- }
72
-
73
- // config.json is valid JSON named after the project.
74
- const config = JSON.parse(fs.readFileSync(path.join(dest, ".sail/config.json"), "utf-8"));
75
- if (config.name !== PROJECT) fail(`config.json name is "${config.name}", expected "${PROJECT}"`);
76
-
77
- // package.json is valid, renamed, and the workspace protocol was resolved away
78
- // (a leftover "workspace:*" would make the scaffold un-installable for users).
79
- const pkg = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf-8"));
80
- if (pkg.name !== PROJECT) fail(`package.json name is "${pkg.name}", expected "${PROJECT}"`);
81
- if (pkg.dependencies?.["@sail/sdk"] === "workspace:*") {
82
- fail(`package.json still has "@sail/sdk": "workspace:*" — init did not resolve it`);
83
- }
84
-
85
- // Regression guard: an absolute path outside the cwd must be REJECTED, not
86
- // silently nested into `<cwd>/<abs path>`. (Pre-fix, `path.join` swallowed the
87
- // leading slash and scaffolded a bogus nested tree while printing success.)
88
- const outside = path.join(os.tmpdir(), "sailor-init-outside", "agent");
89
- let rejected = false;
90
- try {
91
- execFileSync(process.execPath, [BUNDLE, "init", outside], {
92
- cwd: tmpRoot,
93
- stdio: ["ignore", "pipe", "pipe"],
94
- });
95
- } catch {
96
- rejected = true; // non-zero exit = correctly refused
97
- }
98
- if (!rejected) fail(`an absolute path outside cwd ("${outside}") was accepted — should be rejected`);
99
- if (fs.existsSync(path.join(outside, ".sail/config.json"))) {
100
- fail(`init scaffolded into an out-of-cwd absolute path "${outside}"`);
101
- }
102
-
103
- console.log(`✓ init smoke test passed — scaffolded ${PROJECT}/ from the in-tree bundle`);
104
- console.log("✓ init guard passed — absolute path outside cwd rejected, not silently nested");
105
- } finally {
106
- fs.rmSync(path.join(os.tmpdir(), "sailor-init-outside"), { recursive: true, force: true });
107
- fs.rmSync(tmpRoot, { recursive: true, force: true });
108
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `sailor init` smoke test.
4
+ *
5
+ * Scaffolds a fresh project from the in-tree CLI bundle into a temp dir and
6
+ * asserts the scaffold succeeded. This exists to catch the class of regression
7
+ * the doc-drift gate structurally cannot — e.g. `packageRoot()` resolving to a
8
+ * `bin.sailor` package that ships no `templates/`, which made `init` fail from a
9
+ * monorepo checkout with "Template ... not found. Available: none".
10
+ *
11
+ * It runs the REAL built bundle from a monorepo layout, which is exactly the
12
+ * in-tree path that broke before. Pure Node + child_process; the only build
13
+ * dependency is the CLI bundle (`pnpm --filter sailor build`).
14
+ *
15
+ * Run: node scripts/check-init.mjs (CI builds the CLI first)
16
+ * Exit: 0 = scaffold OK, 1 = failure (prints what was missing).
17
+ */
18
+
19
+ import { execFileSync } from "node:child_process";
20
+ import fs from "node:fs";
21
+ import os from "node:os";
22
+ import path from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
26
+ const BUNDLE = path.join(ROOT, "packages/cli/dist/index.cjs");
27
+ const PROJECT = "smoke-agent";
28
+
29
+ function fail(msg) {
30
+ console.error(`✗ init smoke test FAILED: ${msg}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ if (!fs.existsSync(BUNDLE)) {
35
+ fail(`CLI bundle not found at ${BUNDLE}.\n Build it first: pnpm --filter sailor build`);
36
+ }
37
+
38
+ // Scaffold into a temp dir. `init` requires the destination to live inside the
39
+ // process cwd, so we run the bundle with cwd set to a fresh temp root.
40
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sailor-init-smoke-"));
41
+ const dest = path.join(tmpRoot, PROJECT);
42
+
43
+ try {
44
+ let stdout = "";
45
+ try {
46
+ stdout = execFileSync(process.execPath, [BUNDLE, "init", PROJECT], {
47
+ cwd: tmpRoot,
48
+ encoding: "utf-8",
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ });
51
+ } catch (err) {
52
+ const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
53
+ fail(`\`sailor init ${PROJECT}\` exited non-zero.\n ${out || err.message}`);
54
+ }
55
+
56
+ // A successful fresh init prints the AGENTS.md onboarding banner.
57
+ if (!/AGENTS\.md/i.test(stdout)) {
58
+ fail(`init did not report success.\n stdout: ${stdout.trim()}`);
59
+ }
60
+
61
+ // Assert the scaffold landed.
62
+ const mustExist = [
63
+ ".sail/config.json",
64
+ "package.json",
65
+ "foundry.toml",
66
+ "mandates",
67
+ "AGENTS.md",
68
+ ];
69
+ for (const rel of mustExist) {
70
+ if (!fs.existsSync(path.join(dest, rel))) fail(`expected scaffolded "${rel}" — not found`);
71
+ }
72
+
73
+ // config.json is valid JSON named after the project.
74
+ const config = JSON.parse(fs.readFileSync(path.join(dest, ".sail/config.json"), "utf-8"));
75
+ if (config.name !== PROJECT) fail(`config.json name is "${config.name}", expected "${PROJECT}"`);
76
+
77
+ // package.json is valid, renamed, and the workspace protocol was resolved away
78
+ // (a leftover "workspace:*" would make the scaffold un-installable for users).
79
+ const pkg = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf-8"));
80
+ if (pkg.name !== PROJECT) fail(`package.json name is "${pkg.name}", expected "${PROJECT}"`);
81
+ if (pkg.dependencies?.["@sail/sdk"] === "workspace:*") {
82
+ fail(`package.json still has "@sail/sdk": "workspace:*" — init did not resolve it`);
83
+ }
84
+
85
+ // Regression guard: an absolute path outside the cwd must be REJECTED, not
86
+ // silently nested into `<cwd>/<abs path>`. (Pre-fix, `path.join` swallowed the
87
+ // leading slash and scaffolded a bogus nested tree while printing success.)
88
+ const outside = path.join(os.tmpdir(), "sailor-init-outside", "agent");
89
+ let rejected = false;
90
+ try {
91
+ execFileSync(process.execPath, [BUNDLE, "init", outside], {
92
+ cwd: tmpRoot,
93
+ stdio: ["ignore", "pipe", "pipe"],
94
+ });
95
+ } catch {
96
+ rejected = true; // non-zero exit = correctly refused
97
+ }
98
+ if (!rejected) fail(`an absolute path outside cwd ("${outside}") was accepted — should be rejected`);
99
+ if (fs.existsSync(path.join(outside, ".sail/config.json"))) {
100
+ fail(`init scaffolded into an out-of-cwd absolute path "${outside}"`);
101
+ }
102
+
103
+ console.log(`✓ init smoke test passed — scaffolded ${PROJECT}/ from the in-tree bundle`);
104
+ console.log("✓ init guard passed — absolute path outside cwd rejected, not silently nested");
105
+ } finally {
106
+ fs.rmSync(path.join(os.tmpdir(), "sailor-init-outside"), { recursive: true, force: true });
107
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
108
+ }
@@ -1,18 +1,18 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
3
-
4
- struct Context {
5
- address account;
6
- address manager;
7
- address submitter;
8
- address target;
9
- bytes4 selector;
10
- uint256 value;
11
- uint256 blockTimestamp;
12
- uint256 blockNumber;
13
- }
14
-
15
- interface IPermission {
16
- function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
17
- function discriminator() external view returns (bytes32);
18
- }
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ struct Context {
5
+ address account;
6
+ address manager;
7
+ address submitter;
8
+ address target;
9
+ bytes4 selector;
10
+ uint256 value;
11
+ uint256 blockTimestamp;
12
+ uint256 blockNumber;
13
+ }
14
+
15
+ interface IPermission {
16
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
17
+ function discriminator() external view returns (bytes32);
18
+ }
@@ -1,116 +1,116 @@
1
- # Write Your Own Permission — Sail Protocol
2
-
3
- Sail Protocol accepts ANY contract implementing `IPermission`. There is no fixed set of
4
- permission types. `BoundedCallPermission` here is a general primitive; `examples/permissions/`
5
- shows protocol-specific patterns. Every financial bound your mandate enforces should live in
6
- Solidity — the kernel checks `evaluate()` on every dispatch. The agent's TypeScript can be changed
7
- without your signature; the permission contract cannot. You own what you deploy.
8
-
9
- ---
10
-
11
- Sailor does not ship a blessed library of financial permission contracts. You author, review, and
12
- deploy your own `IPermission` contract, and Sailor makes deploying and registering it easy.
13
-
14
- ## What a permission contract is
15
-
16
- A permission contract is an on-chain policy that the SailKernel consults before it lets your agent
17
- (the manager) execute any transaction from your SMA. On every dispatch the kernel calls
18
- `evaluate(txData, ctx)` on each registered permission via `staticcall` — return `true` to allow the
19
- call, `false` to block it. The contract holds your rules (allowed targets, size caps, token
20
- allowlists, time windows, …) so the agent can only ever act inside the bounds you deployed.
21
-
22
- ## The IPermission interface
23
-
24
- ```solidity
25
- interface IPermission {
26
- function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
27
- function discriminator() external view returns (bytes32);
28
- }
29
-
30
- struct Context {
31
- address account; // the SMA (Safe) the dispatch executes from
32
- address manager; // the delegated signer authorized to dispatch
33
- address submitter; // the address that submitted the dispatch transaction
34
- address target; // the contract the call is directed at
35
- bytes4 selector; // the 4-byte function selector being called
36
- uint256 value; // msg.value (native asset) sent with the call
37
- uint256 blockTimestamp; // block.timestamp at evaluation
38
- uint256 blockNumber; // block.number at evaluation
39
- }
40
- ```
41
-
42
- - `evaluate` — your policy. Return `true` to permit the call, `false` to block it. Runs under a
43
- 100k-gas `staticcall`; a revert or gas overage is treated as `false`.
44
- - `discriminator` — a stable `bytes32` name for your permission (e.g. `keccak256("MyMandate")`).
45
-
46
- Keep all policy parameters constructor-configured so each deployment is a complete, reviewable
47
- policy before it is attached to the SMA.
48
-
49
- ## Workflow
50
-
51
- ```bash
52
- # 1. Write your contract in mandates/ (start from BoundedCallPermission.sol)
53
- # 2. Compile
54
- forge build
55
-
56
- # 3. Deploy and attach in one step
57
- sailor mandate deploy --contract <Name> --attach --sma <SMA>
58
- ```
59
-
60
- Or deploy first and attach later (two-step):
61
-
62
- ```bash
63
- sailor mandate deploy --contract <Name> # prints the deployed address
64
- sailor mandate attach --address <deployedAddress> --sma <SMA>
65
- ```
66
-
67
- Both attach paths open the browser signing station so the owner authorizes the registration
68
- (EIP-712 `RegisterPermission`); the agent submits the on-chain transaction.
69
-
70
- ## Prerequisites
71
-
72
- - [Foundry](https://book.getfoundry.sh/getting-started/installation)
73
- - An existing Sailor agent (created with `sailor init`)
74
-
75
- ## Responsibility
76
-
77
- > **You are responsible for the correctness of your permission logic. Sailor registers whatever
78
- > contract address you provide. A bug can block all agent activity or authorize transactions you did
79
- > not intend. Review carefully before attaching.**
80
-
81
- ## Extracting calldata parameters safely
82
-
83
- When you need to bound a specific call argument (amount cap, recipient check, slippage floor),
84
- use `SailCalldata` instead of manual `abi.decode`. The two common bugs it prevents:
85
-
86
- 1. **Forgetting the length check** — decoding before checking `txData.length` can revert or
87
- silently return wrong values. `SailCalldata.hasParams(txData, N)` is the one-line guard.
88
- 2. **Wrong slot index** — off-by-one decodes the wrong parameter. Named helpers make the
89
- intent explicit: `asAddress(txData, 0)`, `asUint256(txData, 1)`, `asAddress(txData, 2)`.
90
-
91
- ```solidity
92
- import {SailCalldata} from "./SailCalldata.sol";
93
-
94
- function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
95
- if (ctx.target != POOL) return false;
96
- if (ctx.selector != SEL_SUPPLY) return false;
97
- // supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)
98
- if (!SailCalldata.hasParams(txData, 4)) return false;
99
- address asset = SailCalldata.asAddress(txData, 0);
100
- uint256 amount = SailCalldata.asUint256(txData, 1);
101
- address onBehalfOf = SailCalldata.asAddress(txData, 2);
102
- // ...
103
- }
104
- ```
105
-
106
- Available helpers: `asAddress`, `asUint256`, `asInt256`, `asBytes32`, `asBool`,
107
- `asUint128`, `asUint64`, `asUint32`, `asUint24`, `asUint16`, `asBytes4`.
108
- Only covers static (fixed-size) types. For `bytes`, `string`, or dynamic arrays,
109
- use `abi.decode(txData[4:], ...)` after the `hasParams` guard.
110
-
111
- ## Structure
112
-
113
- - `foundry.toml` — Foundry config with `@sail/` remapping to `.sail/contracts/`
114
- - `.sail/contracts/interfaces/IPermission.sol` — interface copy (matches SailProtocol)
115
- - `mandates/BoundedCallPermission.sol` — general primitive: allowlisted targets, optional selector filter, max ETH value
116
- - `mandates/SailCalldata.sol` — safe calldata parameter extraction helpers
1
+ # Write Your Own Permission — Sail Protocol
2
+
3
+ Sail Protocol accepts ANY contract implementing `IPermission`. There is no fixed set of
4
+ permission types. `BoundedCallPermission` here is a general primitive; `examples/permissions/`
5
+ shows protocol-specific patterns. Every financial bound your mandate enforces should live in
6
+ Solidity — the kernel checks `evaluate()` on every dispatch. The agent's TypeScript can be changed
7
+ without your signature; the permission contract cannot. You own what you deploy.
8
+
9
+ ---
10
+
11
+ Sailor does not ship a blessed library of financial permission contracts. You author, review, and
12
+ deploy your own `IPermission` contract, and Sailor makes deploying and registering it easy.
13
+
14
+ ## What a permission contract is
15
+
16
+ A permission contract is an on-chain policy that the SailKernel consults before it lets your agent
17
+ (the manager) execute any transaction from your SMA. On every dispatch the kernel calls
18
+ `evaluate(txData, ctx)` on each registered permission via `staticcall` — return `true` to allow the
19
+ call, `false` to block it. The contract holds your rules (allowed targets, size caps, token
20
+ allowlists, time windows, …) so the agent can only ever act inside the bounds you deployed.
21
+
22
+ ## The IPermission interface
23
+
24
+ ```solidity
25
+ interface IPermission {
26
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool);
27
+ function discriminator() external view returns (bytes32);
28
+ }
29
+
30
+ struct Context {
31
+ address account; // the SMA (Safe) the dispatch executes from
32
+ address manager; // the delegated signer authorized to dispatch
33
+ address submitter; // the address that submitted the dispatch transaction
34
+ address target; // the contract the call is directed at
35
+ bytes4 selector; // the 4-byte function selector being called
36
+ uint256 value; // msg.value (native asset) sent with the call
37
+ uint256 blockTimestamp; // block.timestamp at evaluation
38
+ uint256 blockNumber; // block.number at evaluation
39
+ }
40
+ ```
41
+
42
+ - `evaluate` — your policy. Return `true` to permit the call, `false` to block it. Runs under a
43
+ 100k-gas `staticcall`; a revert or gas overage is treated as `false`.
44
+ - `discriminator` — a stable `bytes32` name for your permission (e.g. `keccak256("MyMandate")`).
45
+
46
+ Keep all policy parameters constructor-configured so each deployment is a complete, reviewable
47
+ policy before it is attached to the SMA.
48
+
49
+ ## Workflow
50
+
51
+ ```bash
52
+ # 1. Write your contract in mandates/ (start from BoundedCallPermission.sol)
53
+ # 2. Compile
54
+ forge build
55
+
56
+ # 3. Deploy and attach in one step
57
+ sailor mandate deploy --contract <Name> --attach --sma <SMA>
58
+ ```
59
+
60
+ Or deploy first and attach later (two-step):
61
+
62
+ ```bash
63
+ sailor mandate deploy --contract <Name> # prints the deployed address
64
+ sailor mandate attach --address <deployedAddress> --sma <SMA>
65
+ ```
66
+
67
+ Both attach paths open the browser signing station so the owner authorizes the registration
68
+ (EIP-712 `RegisterPermission`); the agent submits the on-chain transaction.
69
+
70
+ ## Prerequisites
71
+
72
+ - [Foundry](https://book.getfoundry.sh/getting-started/installation)
73
+ - An existing Sailor agent (created with `sailor init`)
74
+
75
+ ## Responsibility
76
+
77
+ > **You are responsible for the correctness of your permission logic. Sailor registers whatever
78
+ > contract address you provide. A bug can block all agent activity or authorize transactions you did
79
+ > not intend. Review carefully before attaching.**
80
+
81
+ ## Extracting calldata parameters safely
82
+
83
+ When you need to bound a specific call argument (amount cap, recipient check, slippage floor),
84
+ use `SailCalldata` instead of manual `abi.decode`. The two common bugs it prevents:
85
+
86
+ 1. **Forgetting the length check** — decoding before checking `txData.length` can revert or
87
+ silently return wrong values. `SailCalldata.hasParams(txData, N)` is the one-line guard.
88
+ 2. **Wrong slot index** — off-by-one decodes the wrong parameter. Named helpers make the
89
+ intent explicit: `asAddress(txData, 0)`, `asUint256(txData, 1)`, `asAddress(txData, 2)`.
90
+
91
+ ```solidity
92
+ import {SailCalldata} from "./SailCalldata.sol";
93
+
94
+ function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
95
+ if (ctx.target != POOL) return false;
96
+ if (ctx.selector != SEL_SUPPLY) return false;
97
+ // supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)
98
+ if (!SailCalldata.hasParams(txData, 4)) return false;
99
+ address asset = SailCalldata.asAddress(txData, 0);
100
+ uint256 amount = SailCalldata.asUint256(txData, 1);
101
+ address onBehalfOf = SailCalldata.asAddress(txData, 2);
102
+ // ...
103
+ }
104
+ ```
105
+
106
+ Available helpers: `asAddress`, `asUint256`, `asInt256`, `asBytes32`, `asBool`,
107
+ `asUint128`, `asUint64`, `asUint32`, `asUint24`, `asUint16`, `asBytes4`.
108
+ Only covers static (fixed-size) types. For `bytes`, `string`, or dynamic arrays,
109
+ use `abi.decode(txData[4:], ...)` after the `hasParams` guard.
110
+
111
+ ## Structure
112
+
113
+ - `foundry.toml` — Foundry config with `@sail/` remapping to `.sail/contracts/`
114
+ - `.sail/contracts/interfaces/IPermission.sol` — interface copy (matches SailProtocol)
115
+ - `mandates/BoundedCallPermission.sol` — general primitive: allowlisted targets, optional selector filter, max ETH value
116
+ - `mandates/SailCalldata.sol` — safe calldata parameter extraction helpers
@@ -1,8 +1,8 @@
1
- [profile.default]
2
- src = "mandates"
3
- out = "out"
4
- libs = ["lib"]
5
- remappings = ["@sail/=.sail/contracts/"]
6
- solc = "0.8.26"
7
- optimizer = true
8
- optimizer_runs = 200
1
+ [profile.default]
2
+ src = "mandates"
3
+ out = "out"
4
+ libs = ["lib"]
5
+ remappings = ["@sail/=.sail/contracts/"]
6
+ solc = "0.8.26"
7
+ optimizer = true
8
+ optimizer_runs = 200
@@ -1,41 +1,41 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
3
-
4
- import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
5
- // SailCalldata: safe helpers for extracting calldata parameters inside evaluate().
6
- // Use SailCalldata.hasParams(txData, N) + SailCalldata.asAddress/asUint256/... instead of
7
- // manual abi.decode when you need to bound specific call arguments (amounts, recipients, etc.).
8
- // See SailCalldata.sol for the full API and examples/permissions/ for protocol examples.
9
- import {SailCalldata} from "./SailCalldata.sol";
10
-
11
- /// @title BoundedCallPermission
12
- /// @notice General-purpose IPermission primitive. Bounds the universal properties of any call:
13
- /// allowed targets, allowed selectors, and max ETH value. Protocol-agnostic.
14
- /// For calldata-parameter bounds (amount caps, recipient checks, slippage), use
15
- /// SailCalldata (imported above) and write a protocol-specific permission —
16
- /// see examples/permissions/ for the pattern per protocol.
17
- /// @dev Deploy one instance per SMA with constructor-configured parameters.
18
- contract BoundedCallPermission is IPermission {
19
- bytes32 private constant DISCRIMINATOR = keccak256("BoundedCallPermission");
20
-
21
- mapping(address => bool) public isAllowedTarget;
22
- mapping(bytes4 => bool) public isAllowedSelector;
23
- bool public immutable SELECTOR_FILTERING;
24
- uint256 public immutable MAX_VALUE;
25
-
26
- constructor(address[] memory allowedTargets, bytes4[] memory allowedSelectors, uint256 maxValue) {
27
- for (uint256 i = 0; i < allowedTargets.length; i++) isAllowedTarget[allowedTargets[i]] = true;
28
- SELECTOR_FILTERING = allowedSelectors.length > 0;
29
- for (uint256 i = 0; i < allowedSelectors.length; i++) isAllowedSelector[allowedSelectors[i]] = true;
30
- MAX_VALUE = maxValue;
31
- }
32
-
33
- function evaluate(bytes calldata, Context calldata ctx) external view returns (bool) {
34
- if (!isAllowedTarget[ctx.target]) return false;
35
- if (SELECTOR_FILTERING && !isAllowedSelector[ctx.selector]) return false;
36
- if (ctx.value > MAX_VALUE) return false;
37
- return true;
38
- }
39
-
40
- function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
41
- }
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import {IPermission, Context} from "@sail/interfaces/IPermission.sol";
5
+ // SailCalldata: safe helpers for extracting calldata parameters inside evaluate().
6
+ // Use SailCalldata.hasParams(txData, N) + SailCalldata.asAddress/asUint256/... instead of
7
+ // manual abi.decode when you need to bound specific call arguments (amounts, recipients, etc.).
8
+ // See SailCalldata.sol for the full API and examples/permissions/ for protocol examples.
9
+ import {SailCalldata} from "./SailCalldata.sol";
10
+
11
+ /// @title BoundedCallPermission
12
+ /// @notice General-purpose IPermission primitive. Bounds the universal properties of any call:
13
+ /// allowed targets, allowed selectors, and max ETH value. Protocol-agnostic.
14
+ /// For calldata-parameter bounds (amount caps, recipient checks, slippage), use
15
+ /// SailCalldata (imported above) and write a protocol-specific permission —
16
+ /// see examples/permissions/ for the pattern per protocol.
17
+ /// @dev Deploy one instance per SMA with constructor-configured parameters.
18
+ contract BoundedCallPermission is IPermission {
19
+ bytes32 private constant DISCRIMINATOR = keccak256("BoundedCallPermission");
20
+
21
+ mapping(address => bool) public isAllowedTarget;
22
+ mapping(bytes4 => bool) public isAllowedSelector;
23
+ bool public immutable SELECTOR_FILTERING;
24
+ uint256 public immutable MAX_VALUE;
25
+
26
+ constructor(address[] memory allowedTargets, bytes4[] memory allowedSelectors, uint256 maxValue) {
27
+ for (uint256 i = 0; i < allowedTargets.length; i++) isAllowedTarget[allowedTargets[i]] = true;
28
+ SELECTOR_FILTERING = allowedSelectors.length > 0;
29
+ for (uint256 i = 0; i < allowedSelectors.length; i++) isAllowedSelector[allowedSelectors[i]] = true;
30
+ MAX_VALUE = maxValue;
31
+ }
32
+
33
+ function evaluate(bytes calldata, Context calldata ctx) external view returns (bool) {
34
+ if (!isAllowedTarget[ctx.target]) return false;
35
+ if (SELECTOR_FILTERING && !isAllowedSelector[ctx.selector]) return false;
36
+ if (ctx.value > MAX_VALUE) return false;
37
+ return true;
38
+ }
39
+
40
+ function discriminator() external pure returns (bytes32) { return DISCRIMINATOR; }
41
+ }
@@ -1,16 +1,16 @@
1
- # Mandates
2
-
3
- Solidity permission contracts live here. Each contract implements `@sail/interfaces/IPermission.sol`.
4
- The SailKernel calls `evaluate(txData, ctx)` before any manager dispatch. Return `true` to permit,
5
- `false` to block.
6
-
7
- ## Workflow
8
-
9
- ```bash
10
- forge build
11
- sailor mandate prepare
12
- sailor ui
13
- ```
14
-
15
- Keep all policy parameters constructor-configured so each deployment has a complete, reviewable
16
- policy before it is attached to the SMA.
1
+ # Mandates
2
+
3
+ Solidity permission contracts live here. Each contract implements `@sail/interfaces/IPermission.sol`.
4
+ The SailKernel calls `evaluate(txData, ctx)` before any manager dispatch. Return `true` to permit,
5
+ `false` to block.
6
+
7
+ ## Workflow
8
+
9
+ ```bash
10
+ forge build
11
+ sailor mandate prepare
12
+ sailor ui
13
+ ```
14
+
15
+ Keep all policy parameters constructor-configured so each deployment has a complete, reviewable
16
+ policy before it is attached to the SMA.