@dev.sail.money/sailor 1.0.0-41 → 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.
- package/AGENTS.md +5 -3
- package/README.md +29 -20
- package/docs/PERMISSION_MODEL.md +1 -1
- package/examples/README.md +24 -0
- package/package.json +1 -1
- package/packages/cli/README.md +0 -1
- package/packages/cli/dist/index.cjs +146 -185
- package/packages/cli/dist/server.cjs +11 -10
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/packages/ui/dist/assets/{add-DbjK_KOY.js → add-BjqRem-K.js} +1 -1
- package/packages/ui/dist/assets/{all-wallets-D-pRl2aR.js → all-wallets-Ce2n1Z8I.js} +1 -1
- package/packages/ui/dist/assets/{app-store-CGPMFKRx.js → app-store-C7E_7mH6.js} +1 -1
- package/packages/ui/dist/assets/{apple-CD0_2QLO.js → apple-BC7kAskQ.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-CxjDQwTG.js → arrow-bottom-Dq_l3FWT.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-circle-BOMbSnUT.js → arrow-bottom-circle-CjACGJGK.js} +1 -1
- package/packages/ui/dist/assets/{arrow-left-BjsweW6y.js → arrow-left-5W_pClNR.js} +1 -1
- package/packages/ui/dist/assets/{arrow-right-C0eZfPlR.js → arrow-right-DXy553gM.js} +1 -1
- package/packages/ui/dist/assets/{arrow-top-B4cVP0zb.js → arrow-top-DD0q04nr.js} +1 -1
- package/packages/ui/dist/assets/{bank-CxRmdPHn.js → bank-CCXCwaWG.js} +1 -1
- package/packages/ui/dist/assets/{basic-BKiQJvSY.js → basic-CAwLXzDD.js} +1 -1
- package/packages/ui/dist/assets/{browser-CT-PmzMC.js → browser-DWmAo_2s.js} +1 -1
- package/packages/ui/dist/assets/{card-Cwr0ZA0B.js → card-C_tjSBK2.js} +1 -1
- package/packages/ui/dist/assets/{ccip-8NLWAk-v.js → ccip-B4SUIV1s.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-C7q-mH5M.js → checkmark-W_dSsbPW.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-bold-Ds0vEbdx.js → checkmark-bold-C9Xi2fBZ.js} +1 -1
- package/packages/ui/dist/assets/{chevron-bottom-DykPQ8Vw.js → chevron-bottom-Dxo8Hw1W.js} +1 -1
- package/packages/ui/dist/assets/{chevron-left-IdV7mUty.js → chevron-left-CD7UuRQl.js} +1 -1
- package/packages/ui/dist/assets/{chevron-right-D7R7ycHG.js → chevron-right-D6kOGOGy.js} +1 -1
- package/packages/ui/dist/assets/{chevron-top-BGSS3FDn.js → chevron-top-ynjtovar.js} +1 -1
- package/packages/ui/dist/assets/{chrome-store-BGnrkOSx.js → chrome-store-CB5wu9g8.js} +1 -1
- package/packages/ui/dist/assets/{clock-DIqM2bTR.js → clock-B3H1XYXj.js} +1 -1
- package/packages/ui/dist/assets/{close-B7lg1PGj.js → close-DVSIOMF5.js} +1 -1
- package/packages/ui/dist/assets/{coinPlaceholder-PvGiH3tt.js → coinPlaceholder-Co_HN9AE.js} +1 -1
- package/packages/ui/dist/assets/{compass-BhHuxeT9.js → compass-CaY11Job.js} +1 -1
- package/packages/ui/dist/assets/{copy-GbypvZa2.js → copy-D7gntTj6.js} +1 -1
- package/packages/ui/dist/assets/{core-DJKvcaym.js → core-CqvnE8sM.js} +3 -3
- package/packages/ui/dist/assets/cursor-rLwK_mXz.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-DqndT7J-.js → cursor-transparent-BrL6QUeS.js} +1 -1
- package/packages/ui/dist/assets/{desktop-DDSy_oKC.js → desktop-BLH7DXoy.js} +1 -1
- package/packages/ui/dist/assets/{disconnect-DVfCfqhj.js → disconnect-BxnvzLHz.js} +1 -1
- package/packages/ui/dist/assets/{discord-CfmEZPK5.js → discord-CfV-36UX.js} +1 -1
- package/packages/ui/dist/assets/{etherscan-CfB9edN9.js → etherscan-CyMQ7xaE.js} +1 -1
- package/packages/ui/dist/assets/{events-cYLbpQpi.js → events-D_3qqJ93.js} +1 -1
- package/packages/ui/dist/assets/{exclamation-triangle-CbV9d5Gt.js → exclamation-triangle-DWjoM6jA.js} +1 -1
- package/packages/ui/dist/assets/{extension-To0EKkgs.js → extension-D3fVJAol.js} +1 -1
- package/packages/ui/dist/assets/{external-link-Cjt_oHSe.js → external-link-D1vHHGXX.js} +1 -1
- package/packages/ui/dist/assets/{facebook-VPqhEho1.js → facebook-Dzk6W-1X.js} +1 -1
- package/packages/ui/dist/assets/{fallback-mNp6Xdgm.js → fallback-BDBC0epM.js} +1 -1
- package/packages/ui/dist/assets/{farcaster-BV_jqIfZ.js → farcaster-BkJt6sOG.js} +1 -1
- package/packages/ui/dist/assets/{filters-BVmnJIrJ.js → filters-Btr-hO6b.js} +1 -1
- package/packages/ui/dist/assets/{github-Dz01qVZ-.js → github-ulrStu89.js} +1 -1
- package/packages/ui/dist/assets/{google-D61au46N.js → google-BTTDuZjf.js} +1 -1
- package/packages/ui/dist/assets/{help-circle-3xbpZnpn.js → help-circle-DcSMbQJh.js} +1 -1
- package/packages/ui/dist/assets/{id-Din3068v.js → id-7WOxEl6j.js} +1 -1
- package/packages/ui/dist/assets/{image-CKfGfDIM.js → image-D2GBTxnU.js} +1 -1
- package/packages/ui/dist/assets/index-BODuSfdj.css +1 -0
- package/packages/ui/dist/assets/{index-CIs_sD9M.js → index-BfABWjw0.js} +1 -1
- package/packages/ui/dist/assets/{index-CxpO9LVP.js → index-BjpGs3bJ.js} +3 -3
- package/packages/ui/dist/assets/{index-CxhKjz7v.js → index-Bvqcol0e.js} +1 -1
- package/packages/ui/dist/assets/{index-DH1S9O_2.js → index-C35kUMRo.js} +1 -1
- package/packages/ui/dist/assets/{index-CBEcDgJN.js → index-DOy_BvMy.js} +58 -58
- package/packages/ui/dist/assets/{index-C4virBcr.js → index-jNjVgIvi.js} +1 -1
- package/packages/ui/dist/assets/{index.es-D8L0a50U.js → index.es-Cz1WraDz.js} +4 -4
- package/packages/ui/dist/assets/{info-DRtyKxfe.js → info-CezLTebu.js} +1 -1
- package/packages/ui/dist/assets/{info-circle-C2kPOuUX.js → info-circle-BHDy0RH1.js} +1 -1
- package/packages/ui/dist/assets/{lightbulb-BN5kFg8c.js → lightbulb-DvUuugiy.js} +1 -1
- package/packages/ui/dist/assets/{mail-_DRdYVZn.js → mail-D6W2cU_-.js} +1 -1
- package/packages/ui/dist/assets/{metamask-sdk-jYYI1ijS.js → metamask-sdk-Cj8b59wb.js} +1 -1
- package/packages/ui/dist/assets/{mobile-CYGolrjP.js → mobile-DD4cG8mI.js} +1 -1
- package/packages/ui/dist/assets/{more-5BeEuz7E.js → more-RsHIPHXv.js} +1 -1
- package/packages/ui/dist/assets/{network-placeholder-Cxnb3rOe.js → network-placeholder-E2lpOWE4.js} +1 -1
- package/packages/ui/dist/assets/{nftPlaceholder-B4rjN_Hx.js → nftPlaceholder-Cm9qGJHf.js} +1 -1
- package/packages/ui/dist/assets/{off-CdpeUxA1.js → off-7ALa1e8N.js} +1 -1
- package/packages/ui/dist/assets/{parseSignature-u9UVA_Bl.js → parseSignature-EU-GmuA0.js} +1 -1
- package/packages/ui/dist/assets/{play-store-BWayr9aI.js → play-store-YmFLoPbj.js} +1 -1
- package/packages/ui/dist/assets/{plus-DfuMhUM_.js → plus-CljVpN-Y.js} +1 -1
- package/packages/ui/dist/assets/{qr-code-Do9ZALD6.js → qr-code-DVGz15q5.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-DooA8q8W.js → recycle-horizontal-Ch_2PFBB.js} +1 -1
- package/packages/ui/dist/assets/{refresh-BZJGp6uc.js → refresh-B1yJjeZR.js} +1 -1
- package/packages/ui/dist/assets/{reown-logo-9mp7ukyl.js → reown-logo-B4gHTrkG.js} +1 -1
- package/packages/ui/dist/assets/{search-CziyZpj-.js → search-C7OPO4bC.js} +1 -1
- package/packages/ui/dist/assets/{secp256k1-DIBahqDI.js → secp256k1-ZHPkrWDd.js} +1 -1
- package/packages/ui/dist/assets/{send-1Muyywuj.js → send-C5Aj89yl.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontal-CEgWi6Pm.js → swapHorizontal-D5l_jKrZ.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalBold-gMkraANQ.js → swapHorizontalBold-TxFZ3Umb.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalMedium-D-Y4qbU1.js → swapHorizontalMedium-M_yuNi0o.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-CAMvP_dz.js → swapHorizontalRoundedBold-G73a-cMg.js} +1 -1
- package/packages/ui/dist/assets/{swapVertical-BJ7q0h0y.js → swapVertical-Gy7GjYS4.js} +1 -1
- package/packages/ui/dist/assets/{telegram-GpnTfdCQ.js → telegram-BnO9isY5.js} +1 -1
- package/packages/ui/dist/assets/{three-dots-BUhvTyOi.js → three-dots-B616bG-O.js} +1 -1
- package/packages/ui/dist/assets/{twitch-BrGPOA5o.js → twitch-DUpzj8Dd.js} +1 -1
- package/packages/ui/dist/assets/{twitterIcon-BjM7MxdG.js → twitterIcon-itK0Mrgj.js} +1 -1
- package/packages/ui/dist/assets/{verify-HbFWmb2f.js → verify-B1Gtl5H9.js} +1 -1
- package/packages/ui/dist/assets/{verify-filled-SlXjIy2_.js → verify-filled-D3noLrZ3.js} +1 -1
- package/packages/ui/dist/assets/{w3m-modal-DbUCvIfl.js → w3m-modal-CiZVJcHR.js} +1 -1
- package/packages/ui/dist/assets/{wallet-CioTciNz.js → wallet-BK1sp3ca.js} +1 -1
- package/packages/ui/dist/assets/{wallet-placeholder-DS4wDXQo.js → wallet-placeholder-CatD0vru.js} +1 -1
- package/packages/ui/dist/assets/{walletconnect-ioE7leyd.js → walletconnect-DmCqpZUB.js} +1 -1
- package/packages/ui/dist/assets/{warning-circle-CfXfQlpb.js → warning-circle-bT7aRO8J.js} +1 -1
- package/packages/ui/dist/assets/{x-DB1YTl_6.js → x-DuFufU-Y.js} +1 -1
- package/packages/ui/dist/index.html +2 -2
- package/scripts/check-docs.mjs +51 -4
- package/scripts/check-init.mjs +12 -0
- package/templates/default/.agents/skills/sail-ci/SKILL.md +66 -0
- package/templates/default/.agents/skills/sail-extend/SKILL.md +74 -0
- package/templates/default/.agents/skills/sail-mandates/SKILL.md +93 -0
- package/templates/default/.agents/skills/sail-mandates/references/approvals.md +42 -0
- package/templates/default/.agents/skills/sail-mandates/references/calls-schema.md +42 -0
- package/templates/default/.agents/skills/sail-mandates/references/constructor-args.md +45 -0
- package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +31 -0
- package/templates/default/.agents/skills/sail-mandates/references/simulate-calls.md +58 -0
- package/templates/default/.agents/skills/sail-onboarding/SKILL.md +73 -0
- package/templates/default/.agents/skills/sail-project-info/SKILL.md +30 -0
- package/templates/default/.agents/skills/sail-servers/SKILL.md +43 -0
- package/templates/default/.agents/skills/sail-transactions/SKILL.md +63 -0
- package/templates/default/AGENTS.md +37 -126
- package/templates/default/test/BoundedCallPermission.t.sol +73 -0
- package/packages/ui/dist/assets/cursor-DwpKqtX4.js +0 -3
- package/packages/ui/dist/assets/index-DS-4Qz48.css +0 -1
- /package/{templates → examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +0 -0
- /package/{templates → examples}/custom-mandate/README.md +0 -0
- /package/{templates → examples}/custom-mandate/foundry.toml +0 -0
- /package/{templates → examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
- /package/{templates → examples}/custom-mandate/mandates/README.md +0 -0
- /package/{templates → examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
- /package/{templates → examples}/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -0
- /package/{templates → examples}/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -0
- /package/{templates → examples}/lifi-permissions/README.md +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{F as l}from"./core-
|
|
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-
|
|
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-
|
|
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,8 +5,8 @@
|
|
|
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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DOy_BvMy.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BODuSfdj.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/scripts/check-docs.mjs
CHANGED
|
@@ -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.
|
|
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 =
|
package/scripts/check-init.mjs
CHANGED
|
@@ -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.
|