@dev.sail.money/sailor 1.2.0-83 → 1.2.1-197
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/README.md +1 -1
- package/examples/custom-mandate/README.md +8 -1
- package/examples/permissions/README.md +3 -0
- package/package.json +2 -1
- package/packages/cli/dist/index.cjs +205 -3
- package/packages/sdk/dist/intelligence.d.ts +1 -1
- package/packages/sdk/dist/intelligence.js +1 -1
- package/scripts/clean.mjs +17 -0
- package/templates/default/.agents/skills/sail-extend/SKILL.md +2 -2
- package/templates/default/.agents/skills/sail-mandates/SKILL.md +17 -3
- package/templates/default/.agents/skills/sail-onboarding/SKILL.md +8 -1
- package/templates/default/.agents/skills/sail-servers/SKILL.md +16 -0
- package/templates/default/.agents/skills/sail-transactions/SKILL.md +1 -1
- package/templates/default/AGENTS.md +18 -1
- package/templates/default/examples/dca/mandate.ts +9 -2
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ sailor onboard --new-sma # deploy SMA and optionally attach a mandate
|
|
|
143
143
|
sailor mandate simulate # probe a permission off-chain (no gas) before registering
|
|
144
144
|
sailor mandate sign # sign the mandate — reconciles against live on-chain state
|
|
145
145
|
sailor mandate deploy # deploy a Foundry-compiled permission contract
|
|
146
|
-
sailor mandate attach # register
|
|
146
|
+
sailor mandate attach # register a deployed permission on an SMA (or a comma-separated list, in one signature)
|
|
147
147
|
|
|
148
148
|
# Agent operation
|
|
149
149
|
sailor run --once # single tick — confirm it works before automating
|
|
@@ -64,7 +64,14 @@ sailor mandate deploy --contract <Name> # prints the deployed address
|
|
|
64
64
|
sailor mandate attach --address <deployedAddress> --sma <SMA>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
To attach several permissions, deploy each one first, then register them all in a single
|
|
68
|
+
signature by passing a comma-separated list:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
sailor mandate attach --address <addr1>,<addr2>,<addr3> --sma <SMA>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
These attach commands open the browser signing station so the owner authorizes the registration
|
|
68
75
|
(EIP-712 `RegisterPermission`); the agent submits the on-chain transaction.
|
|
69
76
|
|
|
70
77
|
## Prerequisites
|
|
@@ -74,6 +74,9 @@ To deploy within a Sailor project (copy the .sol file to `mandates/` first):
|
|
|
74
74
|
sailor mandate deploy --contract <Name> --args '[...]' --attach --sma <SMA>
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
For several permissions, deploy each one (without `--attach`), then register them together in
|
|
78
|
+
one signature: `sailor mandate attach --address <addr1>,<addr2> --sma <SMA>`.
|
|
79
|
+
|
|
77
80
|
## Verify before you authorize
|
|
78
81
|
|
|
79
82
|
Prove a permission accepts the calls you want and rejects the ones you don't — before paying
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev.sail.money/sailor",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1-197",
|
|
4
4
|
"description": "Operator toolkit for Sail Protocol",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sailor": "packages/cli/dist/index.cjs"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"AGENTS.md"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
+
"clean": "node scripts/clean.mjs",
|
|
27
28
|
"build": "pnpm --filter @sail/sdk build && pnpm --filter sailor build && pnpm --filter sailor-ui build",
|
|
28
29
|
"test": "pnpm --filter sailor-ui test",
|
|
29
30
|
"test:ui": "pnpm --filter sailor-ui test:ui",
|
|
@@ -38596,7 +38596,7 @@ var import_websocket2 = __toESM(require_websocket2(), 1);
|
|
|
38596
38596
|
var import_websocket_server2 = __toESM(require_websocket_server2(), 1);
|
|
38597
38597
|
|
|
38598
38598
|
// src/signing/server.ts
|
|
38599
|
-
var DEFAULT_SIGNING_PORT = 3141;
|
|
38599
|
+
var DEFAULT_SIGNING_PORT = Number(process.env.SAILOR_STATION_PORT ?? 3141);
|
|
38600
38600
|
var RUNTIME_SUBDIR = (0, import_node_path4.join)(".sail", "runtime");
|
|
38601
38601
|
var SERVER_STATE_FILE = "server.json";
|
|
38602
38602
|
var REQUEST_SECRET_HEADER = "x-sailor-secret";
|
|
@@ -40770,6 +40770,8 @@ function scaffoldProjectWorkspace(dest, name, options) {
|
|
|
40770
40770
|
import_node_fs9.default.mkdirSync(import_node_path8.default.join(sailDir2, "keys"), { recursive: true });
|
|
40771
40771
|
import_node_fs9.default.mkdirSync(import_node_path8.default.join(sailDir2, "runtime"), { recursive: true });
|
|
40772
40772
|
import_node_fs9.default.mkdirSync(import_node_path8.default.join(sailDir2, "state"), { recursive: true });
|
|
40773
|
+
const installMode = process.env.SAILOR_INSTALL_MODE === "docker" ? "docker" : "local";
|
|
40774
|
+
const containerName = process.env.SAILOR_CONTAINER_NAME ?? "agent";
|
|
40773
40775
|
import_node_fs9.default.writeFileSync(
|
|
40774
40776
|
import_node_path8.default.join(sailDir2, "config.json"),
|
|
40775
40777
|
`${JSON.stringify(
|
|
@@ -40780,6 +40782,8 @@ function scaffoldProjectWorkspace(dest, name, options) {
|
|
|
40780
40782
|
// null = chain not yet chosen; Stage 1 will set this
|
|
40781
40783
|
stateDir: ".sail/state",
|
|
40782
40784
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
40785
|
+
installMode,
|
|
40786
|
+
...installMode === "docker" ? { containerName } : {},
|
|
40783
40787
|
contracts: {
|
|
40784
40788
|
kernel: "",
|
|
40785
40789
|
mandateFactory: ""
|
|
@@ -40871,6 +40875,14 @@ Pass --force to scaffold into it anyway (existing files with the same name are o
|
|
|
40871
40875
|
"This project is already initialized.\nRun `sailor update` to re-sync template files, or `sailor init --force` to re-initialize (overwrites scaffold files; your .sail/keys/ and .sail/state/ are left in place)."
|
|
40872
40876
|
);
|
|
40873
40877
|
}
|
|
40878
|
+
const existingConfigPath = import_node_path8.default.join(dest, ".sail", "config.json");
|
|
40879
|
+
const previousConfig = import_node_fs9.default.existsSync(existingConfigPath) ? (() => {
|
|
40880
|
+
try {
|
|
40881
|
+
return JSON.parse(import_node_fs9.default.readFileSync(existingConfigPath, "utf-8"));
|
|
40882
|
+
} catch {
|
|
40883
|
+
return null;
|
|
40884
|
+
}
|
|
40885
|
+
})() : null;
|
|
40874
40886
|
copyDirSync(templateSrc, dest);
|
|
40875
40887
|
const pkgRoot = packageRoot();
|
|
40876
40888
|
const examplesPermSrc = import_node_path8.default.join(pkgRoot, "examples", "permissions");
|
|
@@ -40910,6 +40922,18 @@ Pass --force to scaffold into it anyway (existing files with the same name are o
|
|
|
40910
40922
|
}
|
|
40911
40923
|
scaffoldProjectWorkspace(dest, name, options);
|
|
40912
40924
|
scaffoldFoundryWorkspace(dest);
|
|
40925
|
+
const newMode = process.env.SAILOR_INSTALL_MODE === "docker" ? "docker" : "local";
|
|
40926
|
+
if (previousConfig?.installMode === "docker" && newMode === "local") {
|
|
40927
|
+
const prev = previousConfig.containerName ?? "agent";
|
|
40928
|
+
console.log(`
|
|
40929
|
+
Switched to local install. If the Docker container is still running:`);
|
|
40930
|
+
console.log(` docker stop ${prev}`);
|
|
40931
|
+
console.log(`You can restart it anytime with the standard docker run command.`);
|
|
40932
|
+
} else if (previousConfig?.installMode === "local" && newMode === "docker") {
|
|
40933
|
+
const containerName = process.env.SAILOR_CONTAINER_NAME ?? "agent";
|
|
40934
|
+
console.log(`
|
|
40935
|
+
Switched to Docker install (container: ${containerName}).`);
|
|
40936
|
+
}
|
|
40913
40937
|
printWelcome(
|
|
40914
40938
|
dest,
|
|
40915
40939
|
name,
|
|
@@ -41020,6 +41044,12 @@ Created ${name}/`);
|
|
|
41020
41044
|
"\u2551 If you skip this step, setup WILL break and you will have to \u2551",
|
|
41021
41045
|
"\u2551 restart. There are no shortcuts. \u2551",
|
|
41022
41046
|
"\u2551 \u2551",
|
|
41047
|
+
"\u2551 IF SAILOR IS RUNNING IN DOCKER: \u2551",
|
|
41048
|
+
"\u2551 \u2022 Read project files from your local filesystem \u2014 they are \u2551",
|
|
41049
|
+
"\u2551 shared via volume mount, do NOT use docker exec to read them. \u2551",
|
|
41050
|
+
"\u2551 \u2022 Prefix every sailor command with: \u2551",
|
|
41051
|
+
"\u2551 docker exec <containerName> sailor <command> \u2551",
|
|
41052
|
+
"\u2551 \u2551",
|
|
41023
41053
|
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
41024
41054
|
""
|
|
41025
41055
|
].join("\n"));
|
|
@@ -41073,6 +41103,39 @@ async function updateCommand() {
|
|
|
41073
41103
|
}
|
|
41074
41104
|
const added = [];
|
|
41075
41105
|
copyDirSyncIfMissing(templateSrc, dest, added);
|
|
41106
|
+
const configPath = import_node_path9.default.join(dest, ".sail", "config.json");
|
|
41107
|
+
try {
|
|
41108
|
+
const config = JSON.parse(import_node_fs10.default.readFileSync(configPath, "utf-8"));
|
|
41109
|
+
const newMode = process.env.SAILOR_INSTALL_MODE === "docker" ? "docker" : "local";
|
|
41110
|
+
const containerName = process.env.SAILOR_CONTAINER_NAME ?? "agent";
|
|
41111
|
+
if (config.installMode !== newMode) {
|
|
41112
|
+
const previousMode = config.installMode;
|
|
41113
|
+
const previousContainer = config.containerName;
|
|
41114
|
+
config.installMode = newMode;
|
|
41115
|
+
if (newMode === "docker") {
|
|
41116
|
+
config.containerName = containerName;
|
|
41117
|
+
} else {
|
|
41118
|
+
delete config.containerName;
|
|
41119
|
+
}
|
|
41120
|
+
import_node_fs10.default.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
41121
|
+
`, "utf-8");
|
|
41122
|
+
if (previousMode === "docker" && newMode === "local") {
|
|
41123
|
+
const prev = previousContainer ?? "agent";
|
|
41124
|
+
console.log(`
|
|
41125
|
+
Switched to local install. If the Docker container is still running:`);
|
|
41126
|
+
console.log(` docker stop ${prev}`);
|
|
41127
|
+
console.log(`You can restart it anytime with the standard docker run command.`);
|
|
41128
|
+
} else if (newMode === "docker") {
|
|
41129
|
+
console.log(`
|
|
41130
|
+
Switched to Docker install (container: ${containerName}).`);
|
|
41131
|
+
}
|
|
41132
|
+
} else if (newMode === "docker" && config.containerName !== containerName) {
|
|
41133
|
+
config.containerName = containerName;
|
|
41134
|
+
import_node_fs10.default.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
41135
|
+
`, "utf-8");
|
|
41136
|
+
}
|
|
41137
|
+
} catch {
|
|
41138
|
+
}
|
|
41076
41139
|
if (removed.length === 0 && updated.length === 0 && added.length === 0) {
|
|
41077
41140
|
console.log("Nothing to update.");
|
|
41078
41141
|
return;
|
|
@@ -42343,6 +42406,10 @@ async function runAttach(project, channel, options) {
|
|
|
42343
42406
|
if (!isAddress(options.sma, { strict: false })) throw new Error(`Invalid --sma address: ${options.sma}`);
|
|
42344
42407
|
const sma = getAddress(options.sma);
|
|
42345
42408
|
const store = new MandateStore();
|
|
42409
|
+
if (options.address.includes(",")) {
|
|
42410
|
+
await runAttachBatch(project, channel, options, sma, store);
|
|
42411
|
+
return;
|
|
42412
|
+
}
|
|
42346
42413
|
const tracked = store.find(options.address);
|
|
42347
42414
|
const rawAddress = tracked?.address ?? options.address;
|
|
42348
42415
|
if (!isAddress(rawAddress, { strict: false })) {
|
|
@@ -42370,6 +42437,21 @@ async function runAttach(project, channel, options) {
|
|
|
42370
42437
|
attached: { sma, mandate: mandateAddress, txHash }
|
|
42371
42438
|
});
|
|
42372
42439
|
}
|
|
42440
|
+
async function runAttachBatch(project, channel, options, sma, store) {
|
|
42441
|
+
const json = !!options.json;
|
|
42442
|
+
const permissions = [...new Set(parseAddressList(options.address, "--address"))];
|
|
42443
|
+
const publicClient = publicClientFor(project);
|
|
42444
|
+
announceSigningUrl(json);
|
|
42445
|
+
const txHash = await attachBatchToSma(project, channel, publicClient, sma, permissions, json);
|
|
42446
|
+
for (const permission of permissions) {
|
|
42447
|
+
if (store.find(permission)) store.recordAttachment(permission, { sma, txHash });
|
|
42448
|
+
}
|
|
42449
|
+
emit(json, () => {
|
|
42450
|
+
}, {
|
|
42451
|
+
status: "ok",
|
|
42452
|
+
attached: permissions.map((mandate2) => ({ sma, mandate: mandate2, txHash }))
|
|
42453
|
+
});
|
|
42454
|
+
}
|
|
42373
42455
|
async function mandateRevoke(options) {
|
|
42374
42456
|
const project = requireProject();
|
|
42375
42457
|
const channel = await createSigningChannel(process.cwd());
|
|
@@ -42559,6 +42641,123 @@ async function attachToSma(project, channel, publicClient, sma, mandate2, label,
|
|
|
42559
42641
|
});
|
|
42560
42642
|
return txHash;
|
|
42561
42643
|
}
|
|
42644
|
+
async function attachBatchToSma(project, channel, publicClient, sma, permissions, json = false) {
|
|
42645
|
+
const say = (fn) => {
|
|
42646
|
+
if (!json) fn();
|
|
42647
|
+
};
|
|
42648
|
+
const agentSigner = await loadManagerSigner2();
|
|
42649
|
+
const registered = await publicClient.readContract({
|
|
42650
|
+
address: project.contracts.kernel,
|
|
42651
|
+
abi: SailKernelAbi,
|
|
42652
|
+
functionName: "registered",
|
|
42653
|
+
args: [sma]
|
|
42654
|
+
});
|
|
42655
|
+
if (!registered) {
|
|
42656
|
+
throw new Error(`SMA ${sma} is not registered with SailKernel; cannot register permissions.`);
|
|
42657
|
+
}
|
|
42658
|
+
const kernelConfig = await publicClient.readContract({
|
|
42659
|
+
address: project.contracts.kernel,
|
|
42660
|
+
abi: SailKernelAbi,
|
|
42661
|
+
functionName: "configs",
|
|
42662
|
+
args: [sma]
|
|
42663
|
+
});
|
|
42664
|
+
const permissionSigner = kernelConfig[0];
|
|
42665
|
+
const caps = await detectKernelCapabilities(publicClient, project.contracts.kernel, {
|
|
42666
|
+
chainId: project.chainId
|
|
42667
|
+
});
|
|
42668
|
+
if (caps.dispatchModel !== "selective") {
|
|
42669
|
+
throw new Error(
|
|
42670
|
+
`Batch attach requires a selective kernel, but ${project.contracts.kernel} reports dispatchModel="${caps.dispatchModel}". Attach permissions one at a time instead (sailor mandate attach --address <one> --sma ${sma}).`
|
|
42671
|
+
);
|
|
42672
|
+
}
|
|
42673
|
+
const nonce = await publicClient.readContract({
|
|
42674
|
+
address: project.contracts.kernel,
|
|
42675
|
+
abi: SailKernelAbi,
|
|
42676
|
+
functionName: "signerNonces",
|
|
42677
|
+
args: [sma]
|
|
42678
|
+
});
|
|
42679
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + 600);
|
|
42680
|
+
const typedData = buildRegisterPermissionsBatchTypedData({
|
|
42681
|
+
chainId: project.chainId,
|
|
42682
|
+
kernel: project.contracts.kernel,
|
|
42683
|
+
account: sma,
|
|
42684
|
+
permissions,
|
|
42685
|
+
nonce,
|
|
42686
|
+
deadline
|
|
42687
|
+
});
|
|
42688
|
+
say(
|
|
42689
|
+
() => console.log(
|
|
42690
|
+
`
|
|
42691
|
+
Attaching ${permissions.length} permissions in one signature \u2014 the mandate signer (${permissionSigner}) signs in the browser\u2026`
|
|
42692
|
+
)
|
|
42693
|
+
);
|
|
42694
|
+
const response = await channel.requestSignature({
|
|
42695
|
+
type: "typed-data",
|
|
42696
|
+
kind: "register-permission",
|
|
42697
|
+
title: `Authorize ${permissions.length} permissions`,
|
|
42698
|
+
description: `Sign once to authorize ${permissions.length} permissions on your SMA. The agent submits the registration transaction and pays gas plus the registration fee.`,
|
|
42699
|
+
chainId: project.chainId,
|
|
42700
|
+
details: [
|
|
42701
|
+
{ label: "SMA", value: sma },
|
|
42702
|
+
{ label: "Permissions", value: String(permissions.length) },
|
|
42703
|
+
{ label: "Mandate signer", value: permissionSigner }
|
|
42704
|
+
],
|
|
42705
|
+
typedData
|
|
42706
|
+
});
|
|
42707
|
+
if (response.status === "rejected") {
|
|
42708
|
+
throw new Error(`User rejected mandate authorization: ${response.reason ?? "no reason given"}`);
|
|
42709
|
+
}
|
|
42710
|
+
if (response.status !== "signature") {
|
|
42711
|
+
throw new Error(`Expected an EIP-712 signature response, got: ${response.status}`);
|
|
42712
|
+
}
|
|
42713
|
+
let fee = 0n;
|
|
42714
|
+
for (const permission of permissions) {
|
|
42715
|
+
fee += await estimatePermissionFee(publicClient, project.contracts.governance, permission);
|
|
42716
|
+
}
|
|
42717
|
+
const chain2 = getChainById(project.chainId);
|
|
42718
|
+
const walletClient = createWalletClient({
|
|
42719
|
+
account: agentSigner.viemAccount,
|
|
42720
|
+
chain: chain2,
|
|
42721
|
+
transport: http(getRpcUrl(project.chainId))
|
|
42722
|
+
});
|
|
42723
|
+
const registerData = encodeFunctionData({
|
|
42724
|
+
abi: SailKernelAbi,
|
|
42725
|
+
functionName: "registerPermissions",
|
|
42726
|
+
args: [sma, permissions, deadline, response.signature]
|
|
42727
|
+
});
|
|
42728
|
+
say(() => console.log(`Submitting batch registration (agent pays gas; fee ${fee} wei)\u2026`));
|
|
42729
|
+
const txHash = await walletClient.sendTransaction({
|
|
42730
|
+
to: project.contracts.kernel,
|
|
42731
|
+
data: registerData,
|
|
42732
|
+
value: fee,
|
|
42733
|
+
account: agentSigner.viemAccount,
|
|
42734
|
+
chain: chain2
|
|
42735
|
+
});
|
|
42736
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
42737
|
+
if (receipt.status !== "success") {
|
|
42738
|
+
throw new Error(`registerPermissions reverted (tx ${txHash})`);
|
|
42739
|
+
}
|
|
42740
|
+
for (const permission of permissions) {
|
|
42741
|
+
const present = await pollForPermission(publicClient, project.contracts.kernel, sma, permission);
|
|
42742
|
+
appendActivity({
|
|
42743
|
+
ts: nowIso(),
|
|
42744
|
+
actor: "agent",
|
|
42745
|
+
type: "permission_registered",
|
|
42746
|
+
permission,
|
|
42747
|
+
sma,
|
|
42748
|
+
txHash,
|
|
42749
|
+
chainId: project.chainId
|
|
42750
|
+
});
|
|
42751
|
+
say(() => {
|
|
42752
|
+
if (!present) {
|
|
42753
|
+
console.log(`\u26A0 ${permission} not yet visible in the permission set \u2014 verify on-chain.`);
|
|
42754
|
+
} else {
|
|
42755
|
+
console.log("\u2713", `${permission} present in getPermissions(${sma})`);
|
|
42756
|
+
}
|
|
42757
|
+
});
|
|
42758
|
+
}
|
|
42759
|
+
return txHash;
|
|
42760
|
+
}
|
|
42562
42761
|
async function pollForPermission(publicClient, kernel, account2, permission, attempts = 6) {
|
|
42563
42762
|
const needle = permission.toLowerCase();
|
|
42564
42763
|
for (let i = 0; i < attempts; i++) {
|
|
@@ -45089,7 +45288,7 @@ async function uiCommand() {
|
|
|
45089
45288
|
const serverBundle = import_node_path16.default.resolve(distDir, "server.cjs");
|
|
45090
45289
|
const projectRoot = process.cwd();
|
|
45091
45290
|
const sailDir2 = import_node_path16.default.join(projectRoot, ".sail");
|
|
45092
|
-
const port = await findFreePort(projectPort(projectRoot));
|
|
45291
|
+
const port = await findFreePort(process.env.PORT ? Number(process.env.PORT) : projectPort(projectRoot));
|
|
45093
45292
|
if (!import_node_fs20.default.existsSync(serverBundle)) {
|
|
45094
45293
|
throw new Error(`Server bundle not found at ${serverBundle}. Re-run the sailor build.`);
|
|
45095
45294
|
}
|
|
@@ -45238,7 +45437,10 @@ var mandate = program2.command("mandate").description("Manage mandates");
|
|
|
45238
45437
|
mandate.command("prepare").description("Prepare a mandate draft for review and signing in the UI (MetaMask)").action(action(mandatePrepare));
|
|
45239
45438
|
mandate.command("sign").description("Review and confirm the permissions authorized for your SMA").option("--yes", "Skip the confirmation prompt (for non-interactive / CI use)").action(actionWith(mandateSign));
|
|
45240
45439
|
mandate.command("deploy").description("Deploy a Foundry-compiled permission contract via the browser signing UI").option("--artifact <path>", "Path to the Foundry artifact JSON (out/<Name>.sol/<Name>.json)").option("--contract <name>", "Contract name; resolves to <out>/<name>.sol/<name>.json").option("--out <dir>", "Foundry output directory", "out").option("--name <label>", "Label to track this permission under (defaults to contract name)").option("--args <json>", `Constructor args as JSON array. Bash: '["0x..","1"]'. PowerShell: '[\\"0x..\\",\\"1\\"]'. Use --args-file to avoid quoting.`).option("--args-file <path>", "Path to a JSON file containing constructor args array (recommended on PowerShell)").option("--build", "Run `forge build` before deploying").option("--attach", "After deploy, register the permission on --sma").option("--sma <address>", "SMA to register on (required with --attach)").option("--json", "Emit machine-readable JSON").action(actionWith(mandateDeploy));
|
|
45241
|
-
mandate.command("attach").description("Register
|
|
45440
|
+
mandate.command("attach").description("Register one or more already-deployed permissions on an SMA (EIP-712 RegisterPermission; a comma-separated list registers all in one signature)").requiredOption(
|
|
45441
|
+
"--address <mandateOrName>",
|
|
45442
|
+
"Permission address or locally-tracked name; or a comma-separated list of addresses to register together in one signature"
|
|
45443
|
+
).requiredOption("--sma <address>", "SMA to register the permission on").option("--label <label>", "Human-readable label shown in the signing UI").option("--json", "Emit machine-readable JSON").action(actionWith(mandateAttach));
|
|
45242
45444
|
mandate.command("deploy-clone").description("[currently unavailable \u2014 no clone templates deployed on any chain; use `mandate deploy`] Deploy + register a standalone clone permission via the signing UI").requiredOption("--template <key>", "Standalone clone template key (e.g. boundedApprove)").requiredOption("--sma <address>", "SMA to deploy the clone for and register it on").option("--tokens <csv>", "Comma-separated allowed token addresses").option("--spenders <csv>", "Comma-separated allowed spender addresses").option("--max <amount>", "Max amount per tx in base units (default: uint256 max)").option("--label <label>", "Human-readable label to track this permission under").option("--json", "Emit machine-readable JSON").action(actionWith(mandateDeployClone));
|
|
45243
45445
|
mandate.command("revoke").description("Revoke permission(s) from an SMA (EIP-712 RevokePermissions, owner-authorized)").option("--address <permissionOrName>", "Permission address, or a name tracked locally").requiredOption("--sma <address>", "Safe (SMA) to revoke the permission(s) from").option("--all", "Revoke every permission currently registered on the SMA").option("--json", "Output JSON").action(actionWith(mandateRevoke));
|
|
45244
45446
|
mandate.command("templates").description("Show how to author your own permission contract (and any community-deployed addresses)").option("--json", "Emit machine-readable JSON").action(actionWith(mandateTemplates));
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Do not edit manually — run `pnpm build` to regenerate.
|
|
6
6
|
*
|
|
7
7
|
* Spec version : 1.2.0
|
|
8
|
-
* Generated at : 2026-06-
|
|
8
|
+
* Generated at : 2026-06-20T11:04:54.885Z
|
|
9
9
|
*/
|
|
10
10
|
export declare const SAIL_INTELLIGENCE_BASE_URL = "https://api.sail.money";
|
|
11
11
|
export declare const SAIL_INTELLIGENCE_DOCS_URL = "https://api.sail.money/docs";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Do not edit manually — run `pnpm build` to regenerate.
|
|
6
6
|
*
|
|
7
7
|
* Spec version : 1.2.0
|
|
8
|
-
* Generated at : 2026-06-
|
|
8
|
+
* Generated at : 2026-06-20T11:04:54.885Z
|
|
9
9
|
*/
|
|
10
10
|
export const SAIL_INTELLIGENCE_BASE_URL = "https://api.sail.money";
|
|
11
11
|
export const SAIL_INTELLIGENCE_DOCS_URL = "https://api.sail.money/docs";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const targets = [
|
|
4
|
+
"node_modules",
|
|
5
|
+
"packages/sdk/node_modules",
|
|
6
|
+
"packages/sdk/dist",
|
|
7
|
+
"packages/sdk/tsconfig.tsbuildinfo",
|
|
8
|
+
"packages/cli/node_modules",
|
|
9
|
+
"packages/cli/dist",
|
|
10
|
+
"packages/ui/node_modules",
|
|
11
|
+
"packages/ui/dist",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
for (const p of targets) {
|
|
15
|
+
rmSync(p, { recursive: true, force: true });
|
|
16
|
+
console.log(` removed ${p}`);
|
|
17
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sail-extend
|
|
3
|
-
description: Recipes for extending a live agent with notifications (Telegram, email) and a strategy-specific dashboard. Use
|
|
3
|
+
description: Recipes for extending a live agent with notifications (Telegram, email) and a strategy-specific dashboard. Use once the agent is live to offer and build run/transaction alerts, monitoring, or a custom view of the strategy — stage 5 of onboarding. Offer this proactively when the agent goes live, not only when the operator asks; the operator may opt out.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Sail extend — notifications and custom dashboards
|
|
7
7
|
|
|
8
|
-
These are user-land code the assistant writes into this project
|
|
8
|
+
These are user-land code the assistant writes into this project — not Sailor features. Once the agent is live (stage 5 of onboarding), proactively offer the operator run/transaction notifications and a strategy-specific dashboard — one line on each — and build whatever they accept. Build only once the agent is live; the operator may decline, but the offer is not optional.
|
|
9
9
|
|
|
10
10
|
## Notifications
|
|
11
11
|
|
|
@@ -9,7 +9,16 @@ The lifecycle is an ordered set of gates. **The order is the correctness model**
|
|
|
9
9
|
|
|
10
10
|
## Gate 1 — Pin the strategy bounds with the user
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Every constraint a strategy needs is one of two kinds, and you must tell the operator which is which so they sign knowing what is enforced where:
|
|
13
|
+
|
|
14
|
+
- **Safety bounds** — protect against loss or theft: amount caps, recipient allowlists, venue/router allowlists, slippage/min-out floors, LTV ceilings, and the like. These are enforced **on-chain in a permission contract, default-ON**. Dropping one requires an explicit, stated justification — never a silent omission. **If a bound matters and it is not in a permission contract, it is not a bound.**
|
|
15
|
+
- **Strategy parameters** — express *how* the strategy runs, not a theft/loss surface: cadence/frequency, schedule, rebalance timing. These live in **agent logic** by nature — permissions are stateless, and these are not safety surfaces (the safety bounds hold regardless of timing). If the operator states one (e.g. a cadence), it is a required agent-side guard that must be **wired and confirmed before go-live** — not optional, never silently dropped — but do not try to push it on-chain.
|
|
16
|
+
|
|
17
|
+
**Enumerate** from the operator's stated strategy *and* from what the protocol can express for the venues involved — do not work from a fixed checklist. Explain what each constraint protects against, classify it as a safety bound (on-chain) or a strategy parameter (agent-side), and say so to the operator.
|
|
18
|
+
|
|
19
|
+
**Precedence.** Operator intent and the strategy's stated bounds outrank any example. If the operator asks for a constraint an example omits, include it — never let an example's shape narrow the mandate below what the operator requested.
|
|
20
|
+
|
|
21
|
+
Examples are illustrations, not the supported set. Sail supports any token, venue, protocol, pool, or contract expressible as a permission — never treat an example's specific addresses as the only ones available. When the operator names something not in your examples, resolve it from authoritative sources (official docs, canonical lists, block explorers) and verify on-chain before binding a mandate to it. Caps are denominated in base units — a token decimals mismatch (USDC is 6, most ERC-20s are 18) silently mis-sizes every bound; confirm decimals on-chain before sizing caps.
|
|
13
22
|
|
|
14
23
|
## Gate 2 — Enumerate approvals and pick the execution model
|
|
15
24
|
|
|
@@ -56,6 +65,8 @@ sailor mandate deploy --contract <Name> --sma <SMA> --json # BLOCKS — owner
|
|
|
56
65
|
|
|
57
66
|
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
67
|
|
|
68
|
+
When a strategy needs several permissions, **deploy all of them first** (don't `--attach` yet). Each deploy is its own owner-signed contract-creation transaction — those cannot be combined — but attaching them is a single signature (Gate 7), so deploy the full set, then attach it in one step.
|
|
69
|
+
|
|
59
70
|
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
71
|
|
|
61
72
|
## Gate 6 — Simulate against must-pass AND must-fail samples
|
|
@@ -75,10 +86,13 @@ This is an off-chain `eth_call` — no gas, no signing. It reports what `evaluat
|
|
|
75
86
|
## Gate 7 — Attach (authorize)
|
|
76
87
|
|
|
77
88
|
```bash
|
|
78
|
-
sailor mandate attach --address <PermissionOrName> --sma <SMA> --json
|
|
89
|
+
sailor mandate attach --address <PermissionOrName> --sma <SMA> --json # one permission, one signature
|
|
90
|
+
sailor mandate attach --address <addr1>,<addr2>,<addr3> --sma <SMA> --json # many permissions, ONE signature
|
|
79
91
|
```
|
|
80
92
|
|
|
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
|
|
93
|
+
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 permissions appear.
|
|
94
|
+
|
|
95
|
+
When a strategy needs several permissions (e.g. a bounded-approve alongside the protocol permission), attach them all at once by passing a comma-separated list of addresses — the registration approvals collapse to a **single** browser signature via the kernel's `registerPermissions`. The earlier per-contract deploy approvals (Gate 5) are separate and unavoidable. A single permission attaches exactly as before with `--address <one>`.
|
|
82
96
|
|
|
83
97
|
## Maintenance
|
|
84
98
|
|
|
@@ -7,6 +7,13 @@ description: Walks the agent through setting up a new Sailor project or resuming
|
|
|
7
7
|
|
|
8
8
|
## Running the CLI
|
|
9
9
|
|
|
10
|
+
**Determine the installation mode first** — read `.sail/config.json → installMode` before running any command:
|
|
11
|
+
|
|
12
|
+
- `"local"` (or field absent) — `sailor` is on the PATH. Run commands directly: `sailor <command>`
|
|
13
|
+
- `"docker"` — sailor runs in a container. Read `containerName` from the same config. Prefix every command:
|
|
14
|
+
`docker exec <containerName> sailor <command>`
|
|
15
|
+
Project files are on your **local filesystem** (mounted at `/workspace` inside the container) — read and write them normally from local paths. Do NOT use `docker exec` to read files; the volume mount makes them directly accessible.
|
|
16
|
+
|
|
10
17
|
The published package is **`@sail.money/sailor`** — always use the scoped name with the registry. The bare name `sailor` is a different, unrelated npm package; never `npx sailor@<version>` or `npm i sailor`. Install it (`npm i -g @sail.money/sailor`, or as a project dep), after which the `sailor` bin works bare (`sailor <command>`) and `npx sailor <command>` resolves the installed bin. Every `sailor …` command in these skills assumes it is installed. Confirm the toolchain up front and pin a recent version — `npx @sail.money/sailor@latest --version` — because an old cached `npx` build can be missing newer commands (e.g. `mandate simulate`); if a documented command reports "unknown command", you are on a stale version, not hitting a missing feature.
|
|
11
18
|
|
|
12
19
|
After upgrading the CLI, run `sailor update` from the project root to pull in updated skills, `AGENTS.md`, `Dockerfile`, and other tooling files. User files (`src/`, `mandates/`, `.sail/`, `package.json`) are never touched.
|
|
@@ -20,7 +27,7 @@ Stage machine keyed off `.sail/`. Read the state, enter at the right stage, neve
|
|
|
20
27
|
| `config.json` has `chainId: null` | Stage 0 — chain not chosen; ask which chain, write it to `config.json` |
|
|
21
28
|
| No `account.json` | Stage 1 — SMA not deployed |
|
|
22
29
|
| `account.json` exists, `state/mandates.json` empty or absent | SMA live, no permissions — hand off to **sail-mandates** |
|
|
23
|
-
| `account.json` + tracked mandates |
|
|
30
|
+
| `account.json` + tracked mandates | SMA live with permissions — hand off to **sail-transactions** to run. Once the agent is live, the flow is not done: **offer Extend (stage 5 — notifications + a custom dashboard) by default**, unless the operator opts out, then hand off to **sail-extend**. The end state is "stage 5 offered," not "running" |
|
|
24
31
|
|
|
25
32
|
Supported chains: Ethereum (1), Base (8453), Arbitrum (42161), Unichain (130), Base Sepolia (84532), Eth Sepolia (11155111). `sailor chains --json` lists them with kernel addresses.
|
|
26
33
|
|
|
@@ -36,6 +36,22 @@ sailor station stop --json # SIGTERM, verified against the recorded URL first
|
|
|
36
36
|
|
|
37
37
|
Signing-flow commands (`mandate deploy/attach/deploy-clone/revoke`, `onboard`, `account deploy-chain`, `account rotate-signer`, `owner connect`) push requests to a running station daemon if one exists, otherwise they spin up an ephemeral in-process signing server for the duration of the command. Starting a persistent station first means the owner connects their wallet once and approves a whole sequence of requests in the same browser tab — do this before any multi-step signing flow.
|
|
38
38
|
|
|
39
|
+
## Docker installation
|
|
40
|
+
|
|
41
|
+
If `.sail/config.json → installMode` is `"docker"`, prefix every command with `docker exec <containerName>` (read `containerName` from the same config):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
docker exec agent sailor ui start
|
|
45
|
+
docker exec agent sailor ui status
|
|
46
|
+
docker exec agent sailor ui stop
|
|
47
|
+
|
|
48
|
+
docker exec agent sailor station start --json
|
|
49
|
+
docker exec agent sailor station status --json
|
|
50
|
+
docker exec agent sailor station stop --json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The UI always binds to port **3334** in Docker (the image sets `ENV PORT=3334`). Project files at `/workspace` are your local directory — read and write them directly from local paths; only `sailor` commands need the `docker exec` prefix.
|
|
54
|
+
|
|
39
55
|
## Troubleshooting
|
|
40
56
|
|
|
41
57
|
- Command stuck "waiting"? It is blocked on a browser signature — check `GET /config` `pendingCount`, and tell the user to open the station URL and approve. Signing requests time out after 10 minutes.
|
|
@@ -51,7 +51,7 @@ These open a signing channel, push a request, and wait (default timeout 10 minut
|
|
|
51
51
|
| `sailor account deploy-chain --chain <id>` | `create-sma` transaction on the target chain |
|
|
52
52
|
| `sailor account rotate-signer` | Delegate rotation + mandate re-approvals |
|
|
53
53
|
| `sailor mandate deploy` | `deploy-mandate` contract-creation transaction (owner pays gas) |
|
|
54
|
-
| `sailor mandate attach` | `RegisterPermission` EIP-712 (off-chain signature; agent submits and pays gas) |
|
|
54
|
+
| `sailor mandate attach` | `RegisterPermission` EIP-712 — one permission; a comma-separated `--address` list signs `RegisterPermissions` once for all (off-chain signature; agent submits and pays gas) |
|
|
55
55
|
| `sailor mandate deploy-clone` | `RegisterPermission` EIP-712 for the predicted clone address |
|
|
56
56
|
| `sailor mandate revoke` | `RevokePermissions` EIP-712 (agent submits and pays gas) |
|
|
57
57
|
| `sailor owner connect` | Nothing — blocks up to 300s waiting for a wallet to connect |
|
|
@@ -24,6 +24,10 @@ Ready? Say **start** and I'll open the setup interface in your browser.
|
|
|
24
24
|
|
|
25
25
|
Everything below is for you, the assistant. The user sees the welcome above; you follow the flow below.
|
|
26
26
|
|
|
27
|
+
## Your job in mandate design
|
|
28
|
+
|
|
29
|
+
When designing a mandate, your job is to help the operator express the **tightest, most complete mandate that captures their strategy's intent** — not the smallest one that compiles. Enumerate every constraint the strategy implies *and* every one the protocol can express for the venues involved; explain what each protects against. Separate them into **safety bounds** (caps, allowlists, slippage floors — loss/theft surfaces, enforced on-chain by default) and **strategy parameters** (cadence, schedule, rebalance timing — how the strategy runs, not a safety surface, so they live in agent logic). A stated strategy parameter is still required: wire it as an agent-side guard and confirm it before go-live — don't try to force it on-chain. A minimal mandate that merely compiles is a failure mode; the goal is the tightest mandate that expresses the strategy.
|
|
30
|
+
|
|
27
31
|
## Voice
|
|
28
32
|
|
|
29
33
|
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.
|
|
@@ -40,6 +44,18 @@ When the user says start (or any first message), present the welcome above in fu
|
|
|
40
44
|
|
|
41
45
|
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.
|
|
42
46
|
|
|
47
|
+
## Stage flow — track to completion
|
|
48
|
+
|
|
49
|
+
The five stages above are a checklist you drive to completion, not a list you mention once. Track which stages are done (read `.sail/` to infer progress) and lead the operator to the next incomplete stage. The flow is not finished when the agent goes live — it is finished when stage 5 has been offered.
|
|
50
|
+
|
|
51
|
+
- [ ] 1. SMA + agent wallet deployed
|
|
52
|
+
- [ ] 2. Strategy defined
|
|
53
|
+
- [ ] 3. Mandate built, tested, signed
|
|
54
|
+
- [ ] 4. Agent running (locally or scheduled)
|
|
55
|
+
- [ ] 5. Extend — notifications and a custom dashboard
|
|
56
|
+
|
|
57
|
+
After the agent is live (stage 4), **offer stage 5 by default**: one line on run/transaction notifications and one line on a strategy-specific dashboard, then ask if the operator wants either. Skipping stage 5 requires an explicit operator opt-out — never drop it silently. Hand off to **sail-extend** to build whatever they accept.
|
|
58
|
+
|
|
43
59
|
## Project state — read `.sail/`, never ask
|
|
44
60
|
|
|
45
61
|
Determine the user's progress by reading `.sail/` — do not ask; read it.
|
|
@@ -61,7 +77,7 @@ Detailed procedures live in skills. If your tooling does not auto-discover skill
|
|
|
61
77
|
|
|
62
78
|
| Skill | Load when | Path |
|
|
63
79
|
|---|---|---|
|
|
64
|
-
| sail-onboarding | New project setup, or resuming a partially set-up project | `.agents/skills/sail-onboarding/SKILL.md` |
|
|
80
|
+
| sail-onboarding | New project setup, or resuming a partially set-up project, documentation of sailor commands | `.agents/skills/sail-onboarding/SKILL.md` |
|
|
65
81
|
| sail-project-info | Any question about project, account, mandate, chain, or environment state | `.agents/skills/sail-project-info/SKILL.md` |
|
|
66
82
|
| sail-servers | Starting, stopping, or health-checking the dashboard or signing station | `.agents/skills/sail-servers/SKILL.md` |
|
|
67
83
|
| sail-transactions | Building dispatches or any EVM transaction for the agent | `.agents/skills/sail-transactions/SKILL.md` |
|
|
@@ -82,3 +98,4 @@ Detailed procedures live in skills. If your tooling does not auto-discover skill
|
|
|
82
98
|
- ERC-20 `approve()` calls are NOT covered by supply, swap, or deposit permissions — every approve the strategy makes needs explicit coverage. Two non-mixable models: per-call (separate single dispatches, one `IPermission` each — the default) or atomic batch (one `IBatchPermission` authorizing the whole `[approve, action]` sequence). A normal `IPermission` cannot authorize a batch. Details: `.agents/skills/sail-mandates/references/approvals.md`
|
|
83
99
|
- Never authorize (attach) a permission before `forge test` and `sailor mandate simulate` both pass against samples derived from the user's strategy
|
|
84
100
|
- Do not pass `--args` inline JSON from PowerShell — use `--args-file` instead
|
|
101
|
+
- Operator intent and the strategy's stated bounds outrank any example. If the operator asks for a bound an example omits, include it. Never let an example's shape narrow a mandate below what the operator requested
|
|
@@ -9,13 +9,20 @@ import type { Address } from "@sail.money/sailor/sdk";
|
|
|
9
9
|
* Tokens in the DCA basket.
|
|
10
10
|
* ALLOWED_TOKENS[0] = USDC (input — what the agent spends)
|
|
11
11
|
* ALLOWED_TOKENS[1] = WETH (output — what the agent accumulates)
|
|
12
|
+
*
|
|
13
|
+
* PLACEHOLDERS — replace with the operator's tokens for their chain. These are not
|
|
14
|
+
* "the supported tokens"; any valid ERC-20 on a supported chain works. Resolve and
|
|
15
|
+
* verify a token's address on-chain (symbol/decimals) before using it — see Gate 1 of
|
|
16
|
+
* the sail-mandates skill.
|
|
12
17
|
*/
|
|
13
18
|
export const ALLOWED_TOKENS: Address[] = [
|
|
14
|
-
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base (6 decimals)
|
|
15
|
-
"0x4200000000000000000000000000000000000006", // WETH on Base (18 decimals)
|
|
19
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base (6 decimals) — placeholder
|
|
20
|
+
"0x4200000000000000000000000000000000000006", // WETH on Base (18 decimals) — placeholder
|
|
16
21
|
];
|
|
17
22
|
|
|
18
23
|
// ── Swap parameters ───────────────────────────────────────────────────────────
|
|
24
|
+
// PLACEHOLDER VALUES — these encode this reference strategy, not the operator's.
|
|
25
|
+
// Replace amounts, slippage, and fee tier with the operator's stated bounds.
|
|
19
26
|
|
|
20
27
|
/** Amount of USDC to spend per swap (in USDC base units, 6 decimals). Default: 5 USDC. */
|
|
21
28
|
export const SWAP_AMOUNT_USDC = 5_000_000n; // 5 USDC
|