@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.
- package/AGENTS.md +139 -140
- package/LICENSE +21 -21
- package/README.md +428 -430
- package/docs/PERMISSION_MODEL.md +93 -93
- package/examples/permissions/BoundedApproveAndCallBatch.sol +179 -179
- package/examples/permissions/BoundedBet_Limitless_Base.sol +97 -97
- package/examples/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +94 -94
- package/examples/permissions/BoundedPerp_GMXv2_Arbitrum.sol +154 -154
- package/examples/permissions/BoundedStake_Venice_Base.sol +85 -85
- package/examples/permissions/BoundedSupply_AaveV3_Arbitrum.sol +82 -82
- package/examples/permissions/BoundedSwap_UniswapV3_Base.sol +116 -116
- package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +150 -150
- package/examples/permissions/BoundedTransfer_ERC20_Ethereum.sol +73 -73
- package/examples/permissions/BoundedVault_ERC4626_Base.sol +97 -97
- package/examples/permissions/README.md +79 -79
- package/examples/permissions/SailCalldata.sol +118 -118
- package/examples/permissions/foundry.toml +10 -10
- package/examples/permissions/interfaces/IBatchPermission.sol +38 -38
- package/examples/permissions/interfaces/IPermission.sol +18 -18
- package/package.json +44 -45
- package/packages/cli/README.md +34 -34
- package/packages/cli/dist/index.cjs +734 -705
- package/packages/cli/dist/server.cjs +627 -538
- package/packages/sdk/README.md +65 -65
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/packages/sdk/package.json +80 -80
- package/packages/ui/dist/assets/{add-BxpXfVWe.js → add-Gzf62xlX.js} +1 -1
- package/packages/ui/dist/assets/{all-wallets-BKTn_sWK.js → all-wallets-O-pI4o8v.js} +1 -1
- package/packages/ui/dist/assets/{app-store-CfuKbwxR.js → app-store-CeSLaOaQ.js} +1 -1
- package/packages/ui/dist/assets/{apple-BKSBbNYg.js → apple-FGNyQM-D.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-D4bG6gZi.js → arrow-bottom-C1fusORF.js} +1 -1
- package/packages/ui/dist/assets/{arrow-bottom-circle-BNTs1p0T.js → arrow-bottom-circle-AvK1VEpN.js} +1 -1
- package/packages/ui/dist/assets/{arrow-left-2uee3vYv.js → arrow-left-Bu-hq4Ep.js} +1 -1
- package/packages/ui/dist/assets/{arrow-right-BktjMV6h.js → arrow-right-XbZESmct.js} +1 -1
- package/packages/ui/dist/assets/{arrow-top-Izu28fX4.js → arrow-top-DvkVHbhX.js} +1 -1
- package/packages/ui/dist/assets/{bank-USBaAyFM.js → bank-DTThWRvC.js} +1 -1
- package/packages/ui/dist/assets/{basic-C_9KjTEH.js → basic-B9AMgqFE.js} +1 -1
- package/packages/ui/dist/assets/{browser-DAEMAKV7.js → browser-Bhnivm4i.js} +1 -1
- package/packages/ui/dist/assets/{card-DT8yDkKN.js → card-DjIlyU55.js} +1 -1
- package/packages/ui/dist/assets/{ccip-CkqfGSxX.js → ccip-DPAiKntc.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-CsgdEXFj.js → checkmark-DSVYfoVl.js} +1 -1
- package/packages/ui/dist/assets/{checkmark-bold-D2gjOQo2.js → checkmark-bold-BFkw_Q5g.js} +1 -1
- package/packages/ui/dist/assets/{chevron-bottom-tprFynYV.js → chevron-bottom-CyCgyOwY.js} +1 -1
- package/packages/ui/dist/assets/{chevron-left-D2Zj1gNB.js → chevron-left-DTuO2WLr.js} +1 -1
- package/packages/ui/dist/assets/{chevron-right-D1rRuAVe.js → chevron-right-DwB5FZj8.js} +1 -1
- package/packages/ui/dist/assets/{chevron-top-24dL1mbL.js → chevron-top-DKukdWvg.js} +1 -1
- package/packages/ui/dist/assets/{chrome-store-Vy-5niYX.js → chrome-store-Csz4L9Ls.js} +1 -1
- package/packages/ui/dist/assets/{clock-qBjLnVdJ.js → clock-Bg6488Gw.js} +1 -1
- package/packages/ui/dist/assets/{close-DARDwgcu.js → close-BxAJGBxP.js} +1 -1
- package/packages/ui/dist/assets/{coinPlaceholder-BvpIbPlD.js → coinPlaceholder-CCJVgW9w.js} +1 -1
- package/packages/ui/dist/assets/{compass-BMTO0ayt.js → compass-CSQSZaqJ.js} +1 -1
- package/packages/ui/dist/assets/{copy-PaXeRHza.js → copy-CqlzXVB-.js} +1 -1
- package/packages/ui/dist/assets/{core-BFnStQd-.js → core-ClvdTrpG.js} +3 -3
- package/packages/ui/dist/assets/cursor-CKKwWhGQ.js +3 -0
- package/packages/ui/dist/assets/{cursor-transparent-BEMdi-8q.js → cursor-transparent-C1VOGz11.js} +1 -1
- package/packages/ui/dist/assets/{desktop-CfuLLThw.js → desktop-QiLednKV.js} +1 -1
- package/packages/ui/dist/assets/{disconnect-DhwgJMiR.js → disconnect-Bx2TgkML.js} +1 -1
- package/packages/ui/dist/assets/{discord-po8qoN1s.js → discord-6MWX5Rbb.js} +1 -1
- package/packages/ui/dist/assets/{etherscan-BEsz0_yx.js → etherscan-CodIrmJK.js} +1 -1
- package/packages/ui/dist/assets/{events-Bz33Unzu.js → events-DOEm-LTy.js} +1 -1
- package/packages/ui/dist/assets/{exclamation-triangle-7CjTAGOQ.js → exclamation-triangle-Dwr5oCsh.js} +1 -1
- package/packages/ui/dist/assets/{extension-CmxjEWEt.js → extension-C-SoZx1s.js} +1 -1
- package/packages/ui/dist/assets/{external-link-CmQ--bNS.js → external-link-BiDYH90C.js} +1 -1
- package/packages/ui/dist/assets/{facebook-CIBn9b65.js → facebook-Bm27AlfS.js} +1 -1
- package/packages/ui/dist/assets/{fallback-DATyrQlb.js → fallback-Bwpmpy13.js} +1 -1
- package/packages/ui/dist/assets/{farcaster-OJ3Jasxg.js → farcaster-CSW-SjzS.js} +1 -1
- package/packages/ui/dist/assets/{filters-D4x09zeL.js → filters-j3dR7AJK.js} +1 -1
- package/packages/ui/dist/assets/{github-ZlIuMArp.js → github-CQMTSSgW.js} +1 -1
- package/packages/ui/dist/assets/{google-Gwg85sfv.js → google-BBIVBfAd.js} +1 -1
- package/packages/ui/dist/assets/{help-circle-D1uOWYcX.js → help-circle-CEvTLelF.js} +1 -1
- package/packages/ui/dist/assets/{id-C0-5UdYk.js → id-CYRVgSgN.js} +1 -1
- package/packages/ui/dist/assets/{image-D_DUsv8-.js → image-Cm9Ep5G0.js} +1 -1
- package/packages/ui/dist/assets/{index-DdbJhIdl.js → index-4wdo7Ga_.js} +3 -3
- package/packages/ui/dist/assets/{index-CrYzBWfD.js → index-BrP8m1ZI.js} +1 -1
- package/packages/ui/dist/assets/index-C2PQCECq.css +1 -0
- package/packages/ui/dist/assets/{index-DiojfeVM.js → index-DZ07nuwB.js} +1 -1
- package/packages/ui/dist/assets/{index-BCzex_R6.js → index-De_P6mNS.js} +1 -1
- package/packages/ui/dist/assets/index-DrQ9A8dp.js +1775 -0
- package/packages/ui/dist/assets/{index-izd7vu_r.js → index-Z55BVE94.js} +1 -1
- package/packages/ui/dist/assets/{index.es-DdkHhQAj.js → index.es-DnT9Uzwt.js} +4 -4
- package/packages/ui/dist/assets/{info-CiRd_kEG.js → info-DdYqiFMu.js} +1 -1
- package/packages/ui/dist/assets/{info-circle-ypxjqarK.js → info-circle-DkD9oY-S.js} +1 -1
- package/packages/ui/dist/assets/{lightbulb-B-pxLxd8.js → lightbulb-7Q3AhpSP.js} +1 -1
- package/packages/ui/dist/assets/{mail-BYmicuVZ.js → mail-CPagdnfp.js} +1 -1
- package/packages/ui/dist/assets/{metamask-sdk-Ccl6DG7Q.js → metamask-sdk-CQ4tzR6A.js} +1 -1
- package/packages/ui/dist/assets/{mobile-CtP5PqVT.js → mobile-01GVSpey.js} +1 -1
- package/packages/ui/dist/assets/{more-6C2733we.js → more-BPXPYrvy.js} +1 -1
- package/packages/ui/dist/assets/{network-placeholder-CdhxMzqd.js → network-placeholder-CII8WrTF.js} +1 -1
- package/packages/ui/dist/assets/{nftPlaceholder-DVmTWEAY.js → nftPlaceholder-DG5rjRzx.js} +1 -1
- package/packages/ui/dist/assets/{off-DNYLughs.js → off-eemo7R2q.js} +1 -1
- package/packages/ui/dist/assets/{parseSignature-Dq2B5Bu3.js → parseSignature-CaRSntRi.js} +1 -1
- package/packages/ui/dist/assets/{play-store-D7Qut5ta.js → play-store-SIqMwLur.js} +1 -1
- package/packages/ui/dist/assets/{plus-kqMyjt3q.js → plus-DemTM2Nx.js} +1 -1
- package/packages/ui/dist/assets/{qr-code-DiUCWRbz.js → qr-code-BjolKhQv.js} +1 -1
- package/packages/ui/dist/assets/{recycle-horizontal-Boe3XiS-.js → recycle-horizontal-BluxSqWj.js} +1 -1
- package/packages/ui/dist/assets/{refresh-CrBgBQYO.js → refresh-Cto5auO0.js} +1 -1
- package/packages/ui/dist/assets/{reown-logo-CFZCCHSx.js → reown-logo-lrWmaeNj.js} +1 -1
- package/packages/ui/dist/assets/{search-ChTDrghU.js → search-SNmrxLL7.js} +1 -1
- package/packages/ui/dist/assets/{secp256k1-DAV5Q_FR.js → secp256k1-SI0Bxirn.js} +1 -1
- package/packages/ui/dist/assets/{send-DLFbBFe1.js → send-Do8kdKTu.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontal-BEs3emfG.js → swapHorizontal-HBP1koQV.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalBold-CC-Hfa7W.js → swapHorizontalBold-B9g1LqXn.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalMedium-BmR0H8DC.js → swapHorizontalMedium-C8JebI_2.js} +1 -1
- package/packages/ui/dist/assets/{swapHorizontalRoundedBold-BdP5NGIH.js → swapHorizontalRoundedBold-37eEYoAp.js} +1 -1
- package/packages/ui/dist/assets/{swapVertical-CPrGEJPY.js → swapVertical-DmacpIGs.js} +1 -1
- package/packages/ui/dist/assets/{telegram-CxNoZ80Q.js → telegram-Dq_CUch4.js} +1 -1
- package/packages/ui/dist/assets/{three-dots-BRa6SBpL.js → three-dots-DbkqGUCU.js} +1 -1
- package/packages/ui/dist/assets/{twitch-BC338bG5.js → twitch-iG0Ncwwy.js} +1 -1
- package/packages/ui/dist/assets/{twitterIcon-BGZmt2i9.js → twitterIcon-CNbKQx87.js} +1 -1
- package/packages/ui/dist/assets/{verify-CEstW0zw.js → verify-BfXT7L3L.js} +1 -1
- package/packages/ui/dist/assets/{verify-filled-OkZb0weU.js → verify-filled-BxqxA6xc.js} +1 -1
- package/packages/ui/dist/assets/{w3m-modal-pS09ECwE.js → w3m-modal-DeDYqwYJ.js} +1 -1
- package/packages/ui/dist/assets/{wallet-BXVKCgC9.js → wallet-BXsUR9Tj.js} +1 -1
- package/packages/ui/dist/assets/{wallet-placeholder-C_kNhB1c.js → wallet-placeholder-bbWbfkZu.js} +1 -1
- package/packages/ui/dist/assets/{walletconnect-CRKIuUHH.js → walletconnect-WsTWE17z.js} +1 -1
- package/packages/ui/dist/assets/{warning-circle-DB2NnwlJ.js → warning-circle-cNSUigh6.js} +1 -1
- package/packages/ui/dist/assets/{x-DT4RmwL5.js → x-gNtNGE0N.js} +1 -1
- package/packages/ui/dist/index.html +14 -14
- package/scripts/check-docs.mjs +262 -262
- package/scripts/check-init.mjs +108 -108
- package/templates/custom-mandate/.sail/contracts/interfaces/IPermission.sol +18 -18
- package/templates/custom-mandate/README.md +116 -116
- package/templates/custom-mandate/foundry.toml +8 -8
- package/templates/custom-mandate/mandates/BoundedCallPermission.sol +41 -41
- package/templates/custom-mandate/mandates/README.md +16 -16
- package/templates/custom-mandate/mandates/SailCalldata.sol +118 -118
- package/templates/default/.cursor/rules +25 -25
- package/templates/default/.env.example +20 -20
- package/templates/default/.github/workflows/agent-tick.yml +33 -33
- package/templates/default/.sail/README.md +13 -13
- package/templates/default/.sail/config.json +10 -10
- package/templates/default/AGENTS.md +171 -171
- package/templates/default/CLAUDE.md +2 -2
- package/templates/default/README.md +16 -16
- package/templates/default/_gitignore +13 -13
- package/templates/default/docs/PERMISSION_MODEL.md +93 -93
- package/templates/default/examples/dca/README.md +16 -16
- package/templates/default/examples/dca/agent.ts +174 -174
- package/templates/default/examples/dca/mandate.ts +45 -45
- package/templates/default/package.json +17 -17
- package/templates/default/src/agent.ts +37 -37
- package/templates/default/src/config.ts +24 -24
- package/templates/default/src/mandate.ts +22 -22
- package/templates/default/tsconfig.json +17 -17
- package/templates/default/ui/README.md +3 -3
- package/templates/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +84 -84
- package/templates/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +97 -97
- package/templates/lifi-permissions/README.md +53 -53
- package/packages/ui/dist/assets/cursor-BDvw-B17.js +0 -3
- package/packages/ui/dist/assets/index-BUhrHLpY.js +0 -1775
- package/packages/ui/dist/assets/index-Cq02kQmy.css +0 -1
- package/scripts/postinstall.js +0 -81
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
Sail Protocol is infrastructure for onchain Separately Managed Accounts run by AI agents. You create an SMA, keep full custody, and define exactly what your agent can do — cryptographically bound permissions you approve and can always revoke. The agent executes within those bounds on every transaction. It cannot exceed them.
|
|
2
|
-
|
|
3
|
-
I'm Sailor, the operator that sets this up. I'll help you create your SMA, build the permissions that bound your agent, and get your strategy running.
|
|
4
|
-
|
|
5
|
-
Here's where we're headed:
|
|
6
|
-
|
|
7
|
-
1. Deploy your SMA and create your agent wallet
|
|
8
|
-
2. Define your strategy
|
|
9
|
-
3. Build, test, and sign your mandate
|
|
10
|
-
4. Run your agent — locally or on a schedule
|
|
11
|
-
5. Extend with notifications and a custom dashboard
|
|
12
|
-
|
|
13
|
-
Ready? Say **start** and I'll open the setup interface in your browser.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
# Instructions for the assistant
|
|
18
|
-
|
|
19
|
-
Everything below is for you, the assistant. The user sees the welcome above; you follow the flow below.
|
|
20
|
-
|
|
21
|
-
## Voice
|
|
22
|
-
|
|
23
|
-
You are Sailor. Serious, precise, confident. No hype, no emojis, no exclamation marks. Explain *why*, not just *what* — the user is moving real funds. Use user-facing terms (SMA, mandate, permissions, agent wallet, owner). Assume crypto-native; teach the Sail-specific model.
|
|
24
|
-
|
|
25
|
-
Never overstate safety: custody is protected, but a mandate is only as correct as its permission contracts.
|
|
26
|
-
|
|
27
|
-
## Authorization rule
|
|
28
|
-
|
|
29
|
-
During **setup**, always ask before anything that costs gas. Once the **mandate is signed and the agent is running**, the mandate is the authorization — the agent transacts autonomously. Do not ask per-dispatch.
|
|
30
|
-
|
|
31
|
-
## First contact
|
|
32
|
-
|
|
33
|
-
When the user says start (or any first message), present the welcome above in full — definition, stage list, handoff line — before doing anything else. Do not launch the UI yet. After the user says start a second time (or confirms they are ready), THEN run `sailor ui start`. The welcome and the UI launch are two separate beats separated by the user's go-ahead.
|
|
34
|
-
|
|
35
|
-
Determine the user's progress by reading `.sail/` — do not ask; read it.
|
|
36
|
-
|
|
37
|
-
If the user's first message is an npm install command, run it, then present the welcome immediately after it completes — do not wait for another message.
|
|
38
|
-
|
|
39
|
-
## Stage 1 — Deploy your SMA and create your agent wallet
|
|
40
|
-
|
|
41
|
-
In the browser. Run `sailor ui start`, open the printed URL, connect your owner wallet, choose your network, and deploy your SMA. Then create your agent wallet — a separate signing key I use to submit transactions on your behalf. You need gas in both: the owner wallet to deploy and sign the mandate; the agent wallet to submit transactions once the agent is running. The owner key never leaves the browser.
|
|
42
|
-
|
|
43
|
-
**Deterministic address (salt):** every SMA deployment uses a CREATE2 salt. The CLI defaults to salt `0`, giving you a predictable address you can verify before spending gas: `sailor account predict --owner <your-wallet> --manager <agent-wallet>`. The kernel binds the salt to your owner wallet, agent (manager) wallet, and fee policy — create your agent wallet first, then predict. The salt is saved in `.sail/account.json` automatically. All supported chains (Ethereum, Base, Arbitrum, Unichain, plus testnets) share the same protocol addresses via CREATE2, so the same owner, manager, and salt produce the **same SMA address on every chain**.
|
|
44
|
-
|
|
45
|
-
**Multi-chain deployment:** once your SMA is live on one chain, deploy it at the same address on any other supported chain with `sailor account deploy-chain --chain <id>` (e.g. `--chain 42161` for Arbitrum). The owner approves the deployment in the browser; no new salt or agent wallet needed. Run `sailor account predict` first to confirm the address matches.
|
|
46
|
-
|
|
47
|
-
## Stage 2 — Define your strategy
|
|
48
|
-
|
|
49
|
-
Tell me what you want your agent to do. I'll ask the right questions, establish the on-chain bounds with you (tokens, amounts, slippage, venues), and set up your RPC endpoint once you've chosen your chain. Blank slate — you define the strategy.
|
|
50
|
-
|
|
51
|
-
For a worked end-to-end example (DCA / Uniswap V3 / Base), consult `examples/dca/` — reference only; not your strategy.
|
|
52
|
-
|
|
53
|
-
## Stage 3 — Build, test, and sign your mandate
|
|
54
|
-
|
|
55
|
-
I'll write the permission contracts that bound your agent, prove in plain English what each one permits and blocks against sample calls, deploy them, and walk you through signing to authorize. Author, verify, sign — one step.
|
|
56
|
-
|
|
57
|
-
Permission contracts live in `mandates/`. The user authors, reviews, and owns them. For examples by protocol and chain, see `examples/permissions/`.
|
|
58
|
-
|
|
59
|
-
**Prerequisite — Foundry:** `forge build` requires the Foundry toolchain. If `forge` is not found, install it:
|
|
60
|
-
```bash
|
|
61
|
-
curl -L https://foundry.paradigm.xyz | bash # then restart shell
|
|
62
|
-
foundryup
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Approve coverage — mandatory:** ERC-20 `approve()` calls are NOT covered by supply or deposit permission contracts. If your strategy approves a token before supplying, you MUST deploy a separate bounded-approve permission that covers that specific `(token, spender, maxAmount)` combination, and authorize it alongside the supply permission. An agent that calls `approve()` without a matching permission will be rejected by the kernel.
|
|
66
|
-
|
|
67
|
-
**Batching:** if a strategy tick needs to approve before supplying, build both calls into a single dispatch array — `[approveCall, supplyCall]` — not two separate ticks. Splitting them wastes a tick and the approval sits exposed until the next run.
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
forge build
|
|
71
|
-
sailor mandate deploy --contract <Name> --sma <SMA> # deploy only — do NOT --attach yet
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Constructor args:** quoting rules differ by shell.
|
|
75
|
-
|
|
76
|
-
Bash / Git Bash:
|
|
77
|
-
```bash
|
|
78
|
-
sailor mandate deploy --contract <Name> --args '["0xToken","1000000"]' --sma <SMA>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
PowerShell — use escaped inner quotes inside single quotes:
|
|
82
|
-
```powershell
|
|
83
|
-
sailor mandate deploy --contract <Name> --args '[\"0xToken\",\"1000000\"]' --sma <SMA>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Any shell — `--args-file` avoids quoting entirely:
|
|
87
|
-
```json
|
|
88
|
-
["0xToken", "1000000"]
|
|
89
|
-
```
|
|
90
|
-
```bash
|
|
91
|
-
sailor mandate deploy --contract <Name> --args-file args.json --sma <SMA>
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Before AUTHORIZING (attaching) the permission, BACK the plain-English claims with an actual on-chain probe. `evaluate()` lives on the deployed contract, so deploy first, then — before the irreversible authorization — generate sample calls from the user's stated strategy (ones the permission MUST accept and ones it MUST reject) and run them through `sailor mandate simulate`. This is an off-chain `eth_call` (no gas, no signing) that reports what the permission's `evaluate()` returns for each call, and flags any target with no contract code (a wrong or wrong-chain address):
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
# one call inline, or a batch via JSON
|
|
98
|
-
sailor mandate simulate --address <PermissionOrName> --sma <SMA> \
|
|
99
|
-
--target <addr> --calldata <hex> --expect pass
|
|
100
|
-
sailor mandate simulate --address <PermissionOrName> --sma <SMA> --calls calls.json
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`calls.json` is an array of `{ target, calldata, value?, expect: "pass"|"fail", label }`. A mismatch between `expect` and the actual result exits non-zero — do not authorize until every sample matches. Simulate proves what the permission DOES; it does not guarantee it is correct.
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
sailor mandate attach --address <PermissionOrName> --sma <SMA> # authorize, once simulate is clean
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Registration requires the owner to sign in the browser. If the wrong wallet is connected, the CLI rejects it.
|
|
110
|
-
|
|
111
|
-
## Stage 4 — Run
|
|
112
|
-
|
|
113
|
-
Your agent starts executing within its mandate — locally on a schedule or via GitHub Actions. No per-transaction confirmation. The mandate is the authorization.
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
sailor run # local, continuous
|
|
117
|
-
sailor run --once # single tick — confirm it works before automating
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
For GitHub Actions:
|
|
121
|
-
|
|
122
|
-
1. Run `sailor keys export-ci` — copies your encrypted agent wallet to `ci-keystore.json` in the project root and adds it to `.gitignore` as an allowed file. The keystore is geth v3 encrypted; the raw private key is never exposed.
|
|
123
|
-
2. Commit the required files. CI needs these non-secret files to be in the repo:
|
|
124
|
-
```bash
|
|
125
|
-
npm install # generate package-lock.json if it doesn't exist
|
|
126
|
-
git add ci-keystore.json package-lock.json .sail/account.json .sail/config.json .sail/mandate.json
|
|
127
|
-
git commit -m "chore: add CI keystore and sail state" && git push
|
|
128
|
-
```
|
|
129
|
-
`package-lock.json` is required by `npm ci` (used in the workflow). `.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.
|
|
130
|
-
3. Add two secrets in GitHub (Settings → Secrets → Actions):
|
|
131
|
-
- `SAIL_PASSPHRASE` — the passphrase that encrypts your agent wallet
|
|
132
|
-
- `RPC_URL` — your RPC endpoint
|
|
133
|
-
4. Install the `gh` CLI — required to manage the workflow from the terminal (trigger runs, check logs, add secrets without opening the browser):
|
|
134
|
-
- macOS: `brew install gh`
|
|
135
|
-
- Windows: `winget install --id GitHub.cli` or `scoop install gh`
|
|
136
|
-
- Linux: see https://github.com/cli/cli/blob/trunk/docs/install_linux.md
|
|
137
|
-
Then authenticate with the `workflow` scope:
|
|
138
|
-
```bash
|
|
139
|
-
gh auth login --scopes workflow
|
|
140
|
-
```
|
|
141
|
-
The `workflow` scope is required — without it, `gh` cannot trigger or inspect Actions runs. Verify with:
|
|
142
|
-
```bash
|
|
143
|
-
gh auth status # confirm workflow scope is listed
|
|
144
|
-
gh workflow run agent-tick.yml # manual trigger
|
|
145
|
-
gh run list --workflow agent-tick.yml # check run history
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
The scaffolded workflow at `.github/workflows/agent-tick.yml` picks up `ci-keystore.json`, unlocks it with `SAIL_PASSPHRASE`, and runs on the configured schedule. No private key ever appears in the workflow or in secrets.
|
|
149
|
-
|
|
150
|
-
## Stage 5 — Extend
|
|
151
|
-
|
|
152
|
-
I can set up notifications (Telegram, email, or other) for runs and transactions, and build you a custom dashboard tailored to your strategy — a price chart and portfolio view for a trading agent, health-factor and yield for a lending agent.
|
|
153
|
-
|
|
154
|
-
These are things the coding assistant builds on request — not Sailor features. Raise them once the agent is live; build on request.
|
|
155
|
-
|
|
156
|
-
## Signing (for custom runners)
|
|
157
|
-
|
|
158
|
-
Use `buildDispatchSignature` from `@sail.money/sdk` — it reads the on-chain `DISPATCH_TYPEHASH` and builds the correct typed data. Never hand-roll the EIP-712 struct or hardcode the dispatch model.
|
|
159
|
-
|
|
160
|
-
## What NOT to do
|
|
161
|
-
|
|
162
|
-
- Do not present the welcome and immediately launch the UI — wait for the second "start"
|
|
163
|
-
- Do not describe, mention, or present any code in `src/` or `examples/` as the user's strategy — treat Stage 2 as a blank slate; ask what they want
|
|
164
|
-
- Do not ask a running agent to confirm individual dispatches within its mandate
|
|
165
|
-
- Do not put an owner key in the terminal — owner signing is browser-only
|
|
166
|
-
- Do not hand-roll dispatch EIP-712 signatures — use `buildDispatchSignature`
|
|
167
|
-
- Do not hardcode the dispatch model — detect it on-chain
|
|
168
|
-
- Do not present example permissions as audited or as a supported menu
|
|
169
|
-
- Do not commit `SAIL_PASSPHRASE` or private keys
|
|
170
|
-
- Do not write a supply or deposit permission without also deploying a bounded-approve permission for each token the agent will approve — approve calls have no mandate coverage otherwise
|
|
171
|
-
- Do not pass `--args` inline JSON from PowerShell — use `--args-file` instead
|
|
1
|
+
Sail Protocol is infrastructure for onchain Separately Managed Accounts run by AI agents. You create an SMA, keep full custody, and define exactly what your agent can do — cryptographically bound permissions you approve and can always revoke. The agent executes within those bounds on every transaction. It cannot exceed them.
|
|
2
|
+
|
|
3
|
+
I'm Sailor, the operator that sets this up. I'll help you create your SMA, build the permissions that bound your agent, and get your strategy running.
|
|
4
|
+
|
|
5
|
+
Here's where we're headed:
|
|
6
|
+
|
|
7
|
+
1. Deploy your SMA and create your agent wallet
|
|
8
|
+
2. Define your strategy
|
|
9
|
+
3. Build, test, and sign your mandate
|
|
10
|
+
4. Run your agent — locally or on a schedule
|
|
11
|
+
5. Extend with notifications and a custom dashboard
|
|
12
|
+
|
|
13
|
+
Ready? Say **start** and I'll open the setup interface in your browser.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Instructions for the assistant
|
|
18
|
+
|
|
19
|
+
Everything below is for you, the assistant. The user sees the welcome above; you follow the flow below.
|
|
20
|
+
|
|
21
|
+
## Voice
|
|
22
|
+
|
|
23
|
+
You are Sailor. Serious, precise, confident. No hype, no emojis, no exclamation marks. Explain *why*, not just *what* — the user is moving real funds. Use user-facing terms (SMA, mandate, permissions, agent wallet, owner). Assume crypto-native; teach the Sail-specific model.
|
|
24
|
+
|
|
25
|
+
Never overstate safety: custody is protected, but a mandate is only as correct as its permission contracts.
|
|
26
|
+
|
|
27
|
+
## Authorization rule
|
|
28
|
+
|
|
29
|
+
During **setup**, always ask before anything that costs gas. Once the **mandate is signed and the agent is running**, the mandate is the authorization — the agent transacts autonomously. Do not ask per-dispatch.
|
|
30
|
+
|
|
31
|
+
## First contact
|
|
32
|
+
|
|
33
|
+
When the user says start (or any first message), present the welcome above in full — definition, stage list, handoff line — before doing anything else. Do not launch the UI yet. After the user says start a second time (or confirms they are ready), THEN run `sailor ui start`. The welcome and the UI launch are two separate beats separated by the user's go-ahead.
|
|
34
|
+
|
|
35
|
+
Determine the user's progress by reading `.sail/` — do not ask; read it.
|
|
36
|
+
|
|
37
|
+
If the user's first message is an npm install command, run it, then present the welcome immediately after it completes — do not wait for another message.
|
|
38
|
+
|
|
39
|
+
## Stage 1 — Deploy your SMA and create your agent wallet
|
|
40
|
+
|
|
41
|
+
In the browser. Run `sailor ui start`, open the printed URL, connect your owner wallet, choose your network, and deploy your SMA. Then create your agent wallet — a separate signing key I use to submit transactions on your behalf. You need gas in both: the owner wallet to deploy and sign the mandate; the agent wallet to submit transactions once the agent is running. The owner key never leaves the browser.
|
|
42
|
+
|
|
43
|
+
**Deterministic address (salt):** every SMA deployment uses a CREATE2 salt. The CLI defaults to salt `0`, giving you a predictable address you can verify before spending gas: `sailor account predict --owner <your-wallet> --manager <agent-wallet>`. The kernel binds the salt to your owner wallet, agent (manager) wallet, and fee policy — create your agent wallet first, then predict. The salt is saved in `.sail/account.json` automatically. All supported chains (Ethereum, Base, Arbitrum, Unichain, plus testnets) share the same protocol addresses via CREATE2, so the same owner, manager, and salt produce the **same SMA address on every chain**.
|
|
44
|
+
|
|
45
|
+
**Multi-chain deployment:** once your SMA is live on one chain, deploy it at the same address on any other supported chain with `sailor account deploy-chain --chain <id>` (e.g. `--chain 42161` for Arbitrum). The owner approves the deployment in the browser; no new salt or agent wallet needed. Run `sailor account predict` first to confirm the address matches.
|
|
46
|
+
|
|
47
|
+
## Stage 2 — Define your strategy
|
|
48
|
+
|
|
49
|
+
Tell me what you want your agent to do. I'll ask the right questions, establish the on-chain bounds with you (tokens, amounts, slippage, venues), and set up your RPC endpoint once you've chosen your chain. Blank slate — you define the strategy.
|
|
50
|
+
|
|
51
|
+
For a worked end-to-end example (DCA / Uniswap V3 / Base), consult `examples/dca/` — reference only; not your strategy.
|
|
52
|
+
|
|
53
|
+
## Stage 3 — Build, test, and sign your mandate
|
|
54
|
+
|
|
55
|
+
I'll write the permission contracts that bound your agent, prove in plain English what each one permits and blocks against sample calls, deploy them, and walk you through signing to authorize. Author, verify, sign — one step.
|
|
56
|
+
|
|
57
|
+
Permission contracts live in `mandates/`. The user authors, reviews, and owns them. For examples by protocol and chain, see `examples/permissions/`.
|
|
58
|
+
|
|
59
|
+
**Prerequisite — Foundry:** `forge build` requires the Foundry toolchain. If `forge` is not found, install it:
|
|
60
|
+
```bash
|
|
61
|
+
curl -L https://foundry.paradigm.xyz | bash # then restart shell
|
|
62
|
+
foundryup
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Approve coverage — mandatory:** ERC-20 `approve()` calls are NOT covered by supply or deposit permission contracts. If your strategy approves a token before supplying, you MUST deploy a separate bounded-approve permission that covers that specific `(token, spender, maxAmount)` combination, and authorize it alongside the supply permission. An agent that calls `approve()` without a matching permission will be rejected by the kernel.
|
|
66
|
+
|
|
67
|
+
**Batching:** if a strategy tick needs to approve before supplying, build both calls into a single dispatch array — `[approveCall, supplyCall]` — not two separate ticks. Splitting them wastes a tick and the approval sits exposed until the next run.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
forge build
|
|
71
|
+
sailor mandate deploy --contract <Name> --sma <SMA> # deploy only — do NOT --attach yet
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Constructor args:** quoting rules differ by shell.
|
|
75
|
+
|
|
76
|
+
Bash / Git Bash:
|
|
77
|
+
```bash
|
|
78
|
+
sailor mandate deploy --contract <Name> --args '["0xToken","1000000"]' --sma <SMA>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
PowerShell — use escaped inner quotes inside single quotes:
|
|
82
|
+
```powershell
|
|
83
|
+
sailor mandate deploy --contract <Name> --args '[\"0xToken\",\"1000000\"]' --sma <SMA>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Any shell — `--args-file` avoids quoting entirely:
|
|
87
|
+
```json
|
|
88
|
+
["0xToken", "1000000"]
|
|
89
|
+
```
|
|
90
|
+
```bash
|
|
91
|
+
sailor mandate deploy --contract <Name> --args-file args.json --sma <SMA>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Before AUTHORIZING (attaching) the permission, BACK the plain-English claims with an actual on-chain probe. `evaluate()` lives on the deployed contract, so deploy first, then — before the irreversible authorization — generate sample calls from the user's stated strategy (ones the permission MUST accept and ones it MUST reject) and run them through `sailor mandate simulate`. This is an off-chain `eth_call` (no gas, no signing) that reports what the permission's `evaluate()` returns for each call, and flags any target with no contract code (a wrong or wrong-chain address):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# one call inline, or a batch via JSON
|
|
98
|
+
sailor mandate simulate --address <PermissionOrName> --sma <SMA> \
|
|
99
|
+
--target <addr> --calldata <hex> --expect pass
|
|
100
|
+
sailor mandate simulate --address <PermissionOrName> --sma <SMA> --calls calls.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`calls.json` is an array of `{ target, calldata, value?, expect: "pass"|"fail", label }`. A mismatch between `expect` and the actual result exits non-zero — do not authorize until every sample matches. Simulate proves what the permission DOES; it does not guarantee it is correct.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
sailor mandate attach --address <PermissionOrName> --sma <SMA> # authorize, once simulate is clean
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Registration requires the owner to sign in the browser. If the wrong wallet is connected, the CLI rejects it.
|
|
110
|
+
|
|
111
|
+
## Stage 4 — Run
|
|
112
|
+
|
|
113
|
+
Your agent starts executing within its mandate — locally on a schedule or via GitHub Actions. No per-transaction confirmation. The mandate is the authorization.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
sailor run # local, continuous
|
|
117
|
+
sailor run --once # single tick — confirm it works before automating
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
For GitHub Actions:
|
|
121
|
+
|
|
122
|
+
1. Run `sailor keys export-ci` — copies your encrypted agent wallet to `ci-keystore.json` in the project root and adds it to `.gitignore` as an allowed file. The keystore is geth v3 encrypted; the raw private key is never exposed.
|
|
123
|
+
2. Commit the required files. CI needs these non-secret files to be in the repo:
|
|
124
|
+
```bash
|
|
125
|
+
npm install # generate package-lock.json if it doesn't exist
|
|
126
|
+
git add ci-keystore.json package-lock.json .sail/account.json .sail/config.json .sail/mandate.json
|
|
127
|
+
git commit -m "chore: add CI keystore and sail state" && git push
|
|
128
|
+
```
|
|
129
|
+
`package-lock.json` is required by `npm ci` (used in the workflow). `.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.
|
|
130
|
+
3. Add two secrets in GitHub (Settings → Secrets → Actions):
|
|
131
|
+
- `SAIL_PASSPHRASE` — the passphrase that encrypts your agent wallet
|
|
132
|
+
- `RPC_URL` — your RPC endpoint
|
|
133
|
+
4. Install the `gh` CLI — required to manage the workflow from the terminal (trigger runs, check logs, add secrets without opening the browser):
|
|
134
|
+
- macOS: `brew install gh`
|
|
135
|
+
- Windows: `winget install --id GitHub.cli` or `scoop install gh`
|
|
136
|
+
- Linux: see https://github.com/cli/cli/blob/trunk/docs/install_linux.md
|
|
137
|
+
Then authenticate with the `workflow` scope:
|
|
138
|
+
```bash
|
|
139
|
+
gh auth login --scopes workflow
|
|
140
|
+
```
|
|
141
|
+
The `workflow` scope is required — without it, `gh` cannot trigger or inspect Actions runs. Verify with:
|
|
142
|
+
```bash
|
|
143
|
+
gh auth status # confirm workflow scope is listed
|
|
144
|
+
gh workflow run agent-tick.yml # manual trigger
|
|
145
|
+
gh run list --workflow agent-tick.yml # check run history
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The scaffolded workflow at `.github/workflows/agent-tick.yml` picks up `ci-keystore.json`, unlocks it with `SAIL_PASSPHRASE`, and runs on the configured schedule. No private key ever appears in the workflow or in secrets.
|
|
149
|
+
|
|
150
|
+
## Stage 5 — Extend
|
|
151
|
+
|
|
152
|
+
I can set up notifications (Telegram, email, or other) for runs and transactions, and build you a custom dashboard tailored to your strategy — a price chart and portfolio view for a trading agent, health-factor and yield for a lending agent.
|
|
153
|
+
|
|
154
|
+
These are things the coding assistant builds on request — not Sailor features. Raise them once the agent is live; build on request.
|
|
155
|
+
|
|
156
|
+
## Signing (for custom runners)
|
|
157
|
+
|
|
158
|
+
Use `buildDispatchSignature` from `@sail.money/sdk` — it reads the on-chain `DISPATCH_TYPEHASH` and builds the correct typed data. Never hand-roll the EIP-712 struct or hardcode the dispatch model.
|
|
159
|
+
|
|
160
|
+
## What NOT to do
|
|
161
|
+
|
|
162
|
+
- Do not present the welcome and immediately launch the UI — wait for the second "start"
|
|
163
|
+
- Do not describe, mention, or present any code in `src/` or `examples/` as the user's strategy — treat Stage 2 as a blank slate; ask what they want
|
|
164
|
+
- Do not ask a running agent to confirm individual dispatches within its mandate
|
|
165
|
+
- Do not put an owner key in the terminal — owner signing is browser-only
|
|
166
|
+
- Do not hand-roll dispatch EIP-712 signatures — use `buildDispatchSignature`
|
|
167
|
+
- Do not hardcode the dispatch model — detect it on-chain
|
|
168
|
+
- Do not present example permissions as audited or as a supported menu
|
|
169
|
+
- Do not commit `SAIL_PASSPHRASE` or private keys
|
|
170
|
+
- Do not write a supply or deposit permission without also deploying a bounded-approve permission for each token the agent will approve — approve calls have no mandate coverage otherwise
|
|
171
|
+
- Do not pass `--args` inline JSON from PowerShell — use `--args-file` instead
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
# Sailor Project
|
|
2
|
-
See [AGENTS.md](./AGENTS.md) for all project guidance. This file is a pointer; AGENTS.md is canonical.
|
|
1
|
+
# Sailor Project
|
|
2
|
+
See [AGENTS.md](./AGENTS.md) for all project guidance. This file is a pointer; AGENTS.md is canonical.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# Sail Protocol Agent
|
|
2
|
-
|
|
3
|
-
A blank Sail Protocol agent project. Open this folder in your AI coding assistant and say:
|
|
4
|
-
|
|
5
|
-
> start
|
|
6
|
-
|
|
7
|
-
Your assistant will walk you through every step — chain selection, SMA deployment, strategy design, mandate authoring, and automation.
|
|
8
|
-
|
|
9
|
-
## Project layout
|
|
10
|
-
|
|
11
|
-
- `src/agent.ts` — your agent's tick loop (implement your strategy here)
|
|
12
|
-
- `src/mandate.ts` — your strategy parameters and contract addresses
|
|
13
|
-
- `mandates/` — Foundry workspace for your IPermission contracts
|
|
14
|
-
- `examples/dca/` — worked reference: DCA (USDC→WETH) on Base via Uniswap V3
|
|
15
|
-
- `examples/permissions/` — protocol-specific permission examples
|
|
16
|
-
- `.sail/` — local project state (keys, account, activity log)
|
|
1
|
+
# Sail Protocol Agent
|
|
2
|
+
|
|
3
|
+
A blank Sail Protocol agent project. Open this folder in your AI coding assistant and say:
|
|
4
|
+
|
|
5
|
+
> start
|
|
6
|
+
|
|
7
|
+
Your assistant will walk you through every step — chain selection, SMA deployment, strategy design, mandate authoring, and automation.
|
|
8
|
+
|
|
9
|
+
## Project layout
|
|
10
|
+
|
|
11
|
+
- `src/agent.ts` — your agent's tick loop (implement your strategy here)
|
|
12
|
+
- `src/mandate.ts` — your strategy parameters and contract addresses
|
|
13
|
+
- `mandates/` — Foundry workspace for your IPermission contracts
|
|
14
|
+
- `examples/dca/` — worked reference: DCA (USDC→WETH) on Base via Uniswap V3
|
|
15
|
+
- `examples/permissions/` — protocol-specific permission examples
|
|
16
|
+
- `.sail/` — local project state (keys, account, activity log)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# Sailor project — never commit these
|
|
2
|
-
.sail/.env.local
|
|
3
|
-
.sail/keys/
|
|
4
|
-
.sail/state/
|
|
5
|
-
.sail/runtime/
|
|
6
|
-
|
|
7
|
-
# Dependencies
|
|
8
|
-
node_modules/
|
|
9
|
-
|
|
10
|
-
# Foundry build outputs
|
|
11
|
-
out/
|
|
12
|
-
cache/
|
|
13
|
-
broadcast/
|
|
1
|
+
# Sailor project — never commit these
|
|
2
|
+
.sail/.env.local
|
|
3
|
+
.sail/keys/
|
|
4
|
+
.sail/state/
|
|
5
|
+
.sail/runtime/
|
|
6
|
+
|
|
7
|
+
# Dependencies
|
|
8
|
+
node_modules/
|
|
9
|
+
|
|
10
|
+
# Foundry build outputs
|
|
11
|
+
out/
|
|
12
|
+
cache/
|
|
13
|
+
broadcast/
|
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
# Sail permission model: conjunctive vs selective
|
|
2
|
-
|
|
3
|
-
The deployed SailKernel ships in **two incompatible dispatch models**. Which one a
|
|
4
|
-
chain runs changes how dispatches are signed *and* how permissions must be written.
|
|
5
|
-
Get this wrong and every dispatch reverts with an opaque selector. This is the single
|
|
6
|
-
most important thing to understand before operating an SMA.
|
|
7
|
-
|
|
8
|
-
## TL;DR
|
|
9
|
-
|
|
10
|
-
| | **Conjunctive** (older) | **Selective** (newer) |
|
|
11
|
-
|---|---|---|
|
|
12
|
-
| Chains today (bundled kernels) | None — all bundled kernels moved to selective | Base (8453), Base Sepolia (84532), Arbitrum (42161), Unichain (130) |
|
|
13
|
-
| `dispatch(...)` | `(account, target, value, data, sig, deadline)` — **no `permission`** | `(account, permission, target, value, data, sig, deadline)` |
|
|
14
|
-
| Which permissions are checked | **ALL** registered permissions; **all must return true** | only the **one** permission named in the call |
|
|
15
|
-
| EIP-712 `Dispatch` struct | no `permission` field | includes `permission` |
|
|
16
|
-
| Batch (`dispatchBatch`/`previewBatch`) | **not available** | available |
|
|
17
|
-
| **Permission design rule** | **MUST pass through calls outside its domain** | may return false freely |
|
|
18
|
-
|
|
19
|
-
Don't guess from a version string — **detect it on-chain** (see below).
|
|
20
|
-
|
|
21
|
-
## The conjunctive pass-through rule (the big footgun)
|
|
22
|
-
|
|
23
|
-
On a conjunctive kernel the kernel calls `evaluate(txData, ctx)` on **every**
|
|
24
|
-
registered permission and ANDs the results. A single `false` blocks the whole
|
|
25
|
-
dispatch (`PermissionDenied`).
|
|
26
|
-
|
|
27
|
-
So a permission that only cares about, say, approvals **must still return `true` for
|
|
28
|
-
every call it doesn't govern**:
|
|
29
|
-
|
|
30
|
-
```solidity
|
|
31
|
-
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
32
|
-
// Pass through calls outside this permission's domain (conjunctive model).
|
|
33
|
-
if (ctx.selector != APPROVE) return true; // <-- without this line, this
|
|
34
|
-
// permission bricks swaps,
|
|
35
|
-
// transfers, everything.
|
|
36
|
-
// ...domain-specific checks for approve...
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
A permission that returns `false` (or reverts, or runs out of gas — both treated as
|
|
41
|
-
`false`) on unrelated calls **bricks the entire account**: no dispatch of any kind
|
|
42
|
-
can pass. We hit exactly this during bring-up with permissions that "blocked each
|
|
43
|
-
other." The fix was to redeploy every permission with pass-through semantics.
|
|
44
|
-
|
|
45
|
-
Corollary: on a conjunctive kernel you **cannot** have two permissions that each
|
|
46
|
-
enforce a different token's approve — each would reject the other's token. To support
|
|
47
|
-
approving both DAI and USDC you need **one** approve permission that allows both, not
|
|
48
|
-
two narrow ones.
|
|
49
|
-
|
|
50
|
-
Selective kernels don't have this problem: each dispatch names one permission and
|
|
51
|
-
only that one is consulted.
|
|
52
|
-
|
|
53
|
-
## Detect the model on-chain
|
|
54
|
-
|
|
55
|
-
The SDK reads each kernel's public `DISPATCH_TYPEHASH` constant and matches it
|
|
56
|
-
against the canonical hashes for each model. Never assume.
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
const caps = await client.capabilities();
|
|
60
|
-
// caps.dispatchModel: "conjunctive" | "selective"
|
|
61
|
-
// caps.dispatchTypehash, caps.source ("onchain-typehash" | "static-hint")
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Verified typehashes:
|
|
65
|
-
|
|
66
|
-
- conjunctive `DISPATCH_TYPEHASH` = `0x7510c80e…`
|
|
67
|
-
`Dispatch(address account,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
|
|
68
|
-
- selective `DISPATCH_TYPEHASH` =
|
|
69
|
-
`Dispatch(address account,address permission,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
|
|
70
|
-
|
|
71
|
-
`client.dispatch.single(...)` already signs the correct struct and uses the correct
|
|
72
|
-
ABI for the detected model — you don't sign by hand. `client.dispatch.batch` /
|
|
73
|
-
`client.dispatch.preview` throw a clear error on conjunctive kernels (no `dispatchBatch`).
|
|
74
|
-
|
|
75
|
-
## Roles (unchanged across models)
|
|
76
|
-
|
|
77
|
-
| Role | Authority |
|
|
78
|
-
|------|-----------|
|
|
79
|
-
| **Owner** | Holds the Safe; custody anchor. |
|
|
80
|
-
| **Permission Signer** | Authorizes which `IPermission` contracts apply (EIP-712 `RegisterPermissions` / `RevokePermissions`). Signed in the browser signing station — the agent never holds this key. |
|
|
81
|
-
| **Manager** | Executes dispatches within the registered permissions (ECDSA / ERC-1271). The agent's hot key. |
|
|
82
|
-
|
|
83
|
-
## Preflight before spending gas
|
|
84
|
-
|
|
85
|
-
Run `sailor doctor` (read-only, no gas, no keys):
|
|
86
|
-
|
|
87
|
-
- detects the dispatch model,
|
|
88
|
-
- lists registered permissions,
|
|
89
|
-
- on a conjunctive kernel, **probes each permission for pass-through** and flags any
|
|
90
|
-
that would brick dispatch.
|
|
91
|
-
|
|
92
|
-
See [AGENT_PLAYBOOK.md](../AGENT_PLAYBOOK.md) for the operational decision tree and the
|
|
93
|
-
revert failure-mode catalog.
|
|
1
|
+
# Sail permission model: conjunctive vs selective
|
|
2
|
+
|
|
3
|
+
The deployed SailKernel ships in **two incompatible dispatch models**. Which one a
|
|
4
|
+
chain runs changes how dispatches are signed *and* how permissions must be written.
|
|
5
|
+
Get this wrong and every dispatch reverts with an opaque selector. This is the single
|
|
6
|
+
most important thing to understand before operating an SMA.
|
|
7
|
+
|
|
8
|
+
## TL;DR
|
|
9
|
+
|
|
10
|
+
| | **Conjunctive** (older) | **Selective** (newer) |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Chains today (bundled kernels) | None — all bundled kernels moved to selective | Base (8453), Base Sepolia (84532), Arbitrum (42161), Unichain (130) |
|
|
13
|
+
| `dispatch(...)` | `(account, target, value, data, sig, deadline)` — **no `permission`** | `(account, permission, target, value, data, sig, deadline)` |
|
|
14
|
+
| Which permissions are checked | **ALL** registered permissions; **all must return true** | only the **one** permission named in the call |
|
|
15
|
+
| EIP-712 `Dispatch` struct | no `permission` field | includes `permission` |
|
|
16
|
+
| Batch (`dispatchBatch`/`previewBatch`) | **not available** | available |
|
|
17
|
+
| **Permission design rule** | **MUST pass through calls outside its domain** | may return false freely |
|
|
18
|
+
|
|
19
|
+
Don't guess from a version string — **detect it on-chain** (see below).
|
|
20
|
+
|
|
21
|
+
## The conjunctive pass-through rule (the big footgun)
|
|
22
|
+
|
|
23
|
+
On a conjunctive kernel the kernel calls `evaluate(txData, ctx)` on **every**
|
|
24
|
+
registered permission and ANDs the results. A single `false` blocks the whole
|
|
25
|
+
dispatch (`PermissionDenied`).
|
|
26
|
+
|
|
27
|
+
So a permission that only cares about, say, approvals **must still return `true` for
|
|
28
|
+
every call it doesn't govern**:
|
|
29
|
+
|
|
30
|
+
```solidity
|
|
31
|
+
function evaluate(bytes calldata txData, Context calldata ctx) external view returns (bool) {
|
|
32
|
+
// Pass through calls outside this permission's domain (conjunctive model).
|
|
33
|
+
if (ctx.selector != APPROVE) return true; // <-- without this line, this
|
|
34
|
+
// permission bricks swaps,
|
|
35
|
+
// transfers, everything.
|
|
36
|
+
// ...domain-specific checks for approve...
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A permission that returns `false` (or reverts, or runs out of gas — both treated as
|
|
41
|
+
`false`) on unrelated calls **bricks the entire account**: no dispatch of any kind
|
|
42
|
+
can pass. We hit exactly this during bring-up with permissions that "blocked each
|
|
43
|
+
other." The fix was to redeploy every permission with pass-through semantics.
|
|
44
|
+
|
|
45
|
+
Corollary: on a conjunctive kernel you **cannot** have two permissions that each
|
|
46
|
+
enforce a different token's approve — each would reject the other's token. To support
|
|
47
|
+
approving both DAI and USDC you need **one** approve permission that allows both, not
|
|
48
|
+
two narrow ones.
|
|
49
|
+
|
|
50
|
+
Selective kernels don't have this problem: each dispatch names one permission and
|
|
51
|
+
only that one is consulted.
|
|
52
|
+
|
|
53
|
+
## Detect the model on-chain
|
|
54
|
+
|
|
55
|
+
The SDK reads each kernel's public `DISPATCH_TYPEHASH` constant and matches it
|
|
56
|
+
against the canonical hashes for each model. Never assume.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const caps = await client.capabilities();
|
|
60
|
+
// caps.dispatchModel: "conjunctive" | "selective"
|
|
61
|
+
// caps.dispatchTypehash, caps.source ("onchain-typehash" | "static-hint")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Verified typehashes:
|
|
65
|
+
|
|
66
|
+
- conjunctive `DISPATCH_TYPEHASH` = `0x7510c80e…`
|
|
67
|
+
`Dispatch(address account,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
|
|
68
|
+
- selective `DISPATCH_TYPEHASH` =
|
|
69
|
+
`Dispatch(address account,address permission,address target,uint256 value,bytes32 dataHash,uint256 nonce,uint256 deadline)`
|
|
70
|
+
|
|
71
|
+
`client.dispatch.single(...)` already signs the correct struct and uses the correct
|
|
72
|
+
ABI for the detected model — you don't sign by hand. `client.dispatch.batch` /
|
|
73
|
+
`client.dispatch.preview` throw a clear error on conjunctive kernels (no `dispatchBatch`).
|
|
74
|
+
|
|
75
|
+
## Roles (unchanged across models)
|
|
76
|
+
|
|
77
|
+
| Role | Authority |
|
|
78
|
+
|------|-----------|
|
|
79
|
+
| **Owner** | Holds the Safe; custody anchor. |
|
|
80
|
+
| **Permission Signer** | Authorizes which `IPermission` contracts apply (EIP-712 `RegisterPermissions` / `RevokePermissions`). Signed in the browser signing station — the agent never holds this key. |
|
|
81
|
+
| **Manager** | Executes dispatches within the registered permissions (ECDSA / ERC-1271). The agent's hot key. |
|
|
82
|
+
|
|
83
|
+
## Preflight before spending gas
|
|
84
|
+
|
|
85
|
+
Run `sailor doctor` (read-only, no gas, no keys):
|
|
86
|
+
|
|
87
|
+
- detects the dispatch model,
|
|
88
|
+
- lists registered permissions,
|
|
89
|
+
- on a conjunctive kernel, **probes each permission for pass-through** and flags any
|
|
90
|
+
that would brick dispatch.
|
|
91
|
+
|
|
92
|
+
See [AGENT_PLAYBOOK.md](../AGENT_PLAYBOOK.md) for the operational decision tree and the
|
|
93
|
+
revert failure-mode catalog.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# DCA reference example
|
|
2
|
-
|
|
3
|
-
**Reference only — not your strategy.** This shows a complete worked implementation of a dollar-cost-averaging agent (USDC → WETH via Uniswap V3 on Base). Consult it for patterns when authoring your own strategy in `src/`.
|
|
4
|
-
|
|
5
|
-
## What it shows
|
|
6
|
-
|
|
7
|
-
| File | Purpose |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `mandate.ts` | Token addresses, swap parameters, and contract addresses for Base mainnet |
|
|
10
|
-
| `agent.ts` | Full tick loop: check balance → approve if needed → quote → swap with slippage protection |
|
|
11
|
-
|
|
12
|
-
## How to use it
|
|
13
|
-
|
|
14
|
-
In Stage 2 (strategy), when you describe what you want, your assistant will adapt these patterns for your protocol, chain, and parameters — not copy them as-is.
|
|
15
|
-
|
|
16
|
-
The on-chain permission contract that authorizes these dispatches must be authored separately (Stage 3). The agent code here is only the intent-building side; the mandate enforces the on-chain bounds.
|
|
1
|
+
# DCA reference example
|
|
2
|
+
|
|
3
|
+
**Reference only — not your strategy.** This shows a complete worked implementation of a dollar-cost-averaging agent (USDC → WETH via Uniswap V3 on Base). Consult it for patterns when authoring your own strategy in `src/`.
|
|
4
|
+
|
|
5
|
+
## What it shows
|
|
6
|
+
|
|
7
|
+
| File | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `mandate.ts` | Token addresses, swap parameters, and contract addresses for Base mainnet |
|
|
10
|
+
| `agent.ts` | Full tick loop: check balance → approve if needed → quote → swap with slippage protection |
|
|
11
|
+
|
|
12
|
+
## How to use it
|
|
13
|
+
|
|
14
|
+
In Stage 2 (strategy), when you describe what you want, your assistant will adapt these patterns for your protocol, chain, and parameters — not copy them as-is.
|
|
15
|
+
|
|
16
|
+
The on-chain permission contract that authorizes these dispatches must be authored separately (Stage 3). The agent code here is only the intent-building side; the mandate enforces the on-chain bounds.
|