@dev.sail.money/sailor 0.0.2-15 → 0.0.2-17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/examples/permissions/BoundedSwap_UniswapV3_Base.sol +4 -3
  2. package/examples/permissions/BoundedSwap_UniswapV4_Unichain.sol +4 -0
  3. package/package.json +1 -1
  4. package/packages/cli/dist/index.cjs +401 -44
  5. package/packages/sdk/dist/intelligence.d.ts +1 -1
  6. package/packages/sdk/dist/intelligence.js +1 -1
  7. package/packages/sdk/dist/templates/ammLiquidity.d.ts +24 -11
  8. package/packages/sdk/dist/templates/ammLiquidity.d.ts.map +1 -1
  9. package/packages/sdk/dist/templates/ammLiquidity.js +39 -31
  10. package/packages/sdk/dist/templates/ammLiquidity.js.map +1 -1
  11. package/packages/sdk/dist/templates/approveAndCallBatch.d.ts +24 -10
  12. package/packages/sdk/dist/templates/approveAndCallBatch.d.ts.map +1 -1
  13. package/packages/sdk/dist/templates/approveAndCallBatch.js +36 -23
  14. package/packages/sdk/dist/templates/approveAndCallBatch.js.map +1 -1
  15. package/packages/sdk/dist/templates/boundedBorrow.d.ts +19 -9
  16. package/packages/sdk/dist/templates/boundedBorrow.d.ts.map +1 -1
  17. package/packages/sdk/dist/templates/boundedBorrow.js +28 -19
  18. package/packages/sdk/dist/templates/boundedBorrow.js.map +1 -1
  19. package/packages/sdk/dist/templates/boundedSwap.d.ts +19 -9
  20. package/packages/sdk/dist/templates/boundedSwap.d.ts.map +1 -1
  21. package/packages/sdk/dist/templates/boundedSwap.js +30 -20
  22. package/packages/sdk/dist/templates/boundedSwap.js.map +1 -1
  23. package/packages/sdk/dist/templates/defiBundle.d.ts +35 -9
  24. package/packages/sdk/dist/templates/defiBundle.d.ts.map +1 -1
  25. package/packages/sdk/dist/templates/defiBundle.js +84 -22
  26. package/packages/sdk/dist/templates/defiBundle.js.map +1 -1
  27. package/packages/sdk/dist/templates/pendle.d.ts +23 -8
  28. package/packages/sdk/dist/templates/pendle.d.ts.map +1 -1
  29. package/packages/sdk/dist/templates/pendle.js +34 -14
  30. package/packages/sdk/dist/templates/pendle.js.map +1 -1
  31. package/packages/sdk/dist/templates/transferTarget.d.ts +11 -3
  32. package/packages/sdk/dist/templates/transferTarget.d.ts.map +1 -1
  33. package/packages/sdk/dist/templates/transferTarget.js +14 -7
  34. package/packages/sdk/dist/templates/transferTarget.js.map +1 -1
  35. package/packages/sdk/package.json +1 -0
  36. package/templates/default/.github/workflows/agent-tick.yml +8 -7
  37. package/templates/default/AGENTS.md +12 -1
  38. package/templates/default/src/config.ts +1 -11
  39. package/templates/default/tsconfig.json +7 -1
@@ -39455,6 +39455,40 @@ ${label} key saved. Address: ${checksum4(keyring.address)}`);
39455
39455
  }
39456
39456
  }
39457
39457
  }
39458
+ async function keysExportCi() {
39459
+ const account2 = readJsonFile(sailPath("account.json"));
39460
+ const src = resolveKeyPath("manager", account2?.safe);
39461
+ if (!fileExists(src)) {
39462
+ throw new Error(
39463
+ 'No agent wallet keystore found.\nComplete Stage 1 (browser UI) to generate your agent wallet, or run\n"sailor keys generate" and choose "agent wallet" to create one manually.'
39464
+ );
39465
+ }
39466
+ const dest = import_node_path6.default.resolve(process.cwd(), "ci-keystore.json");
39467
+ import_node_fs7.default.copyFileSync(src, dest);
39468
+ console.log(`\u2713 Keystore copied to ci-keystore.json`);
39469
+ console.log(` Source: ${src}`);
39470
+ const gitignorePath = import_node_path6.default.resolve(process.cwd(), ".gitignore");
39471
+ if (import_node_fs7.default.existsSync(gitignorePath)) {
39472
+ const content = import_node_fs7.default.readFileSync(gitignorePath, "utf-8");
39473
+ if (!content.includes("ci-keystore.json")) {
39474
+ import_node_fs7.default.appendFileSync(
39475
+ gitignorePath,
39476
+ "\n# CI keystore \u2014 encrypted agent wallet, safe to commit\n!ci-keystore.json\n"
39477
+ );
39478
+ console.log("\u2713 Added !ci-keystore.json allowlist entry to .gitignore");
39479
+ } else {
39480
+ console.log("\u2713 .gitignore already tracks ci-keystore.json");
39481
+ }
39482
+ }
39483
+ console.log("\nNext steps:");
39484
+ console.log(" 1. Add two GitHub Actions secrets (Settings \u2192 Secrets \u2192 Actions):");
39485
+ console.log(" SAIL_PASSPHRASE \u2014 the passphrase that encrypts your agent wallet");
39486
+ console.log(" RPC_URL \u2014 your RPC endpoint");
39487
+ console.log(" 2. Commit and push ci-keystore.json:");
39488
+ console.log(' git add ci-keystore.json && git commit -m "chore: add CI keystore" && git push');
39489
+ console.log("\n The keystore is encrypted \u2014 the raw private key is never exposed.");
39490
+ console.log(" The workflow at .github/workflows/agent-tick.yml unlocks it with SAIL_PASSPHRASE.");
39491
+ }
39458
39492
  async function keysShow() {
39459
39493
  const present = ROLES.filter((role) => keyExists(role));
39460
39494
  if (present.length === 0) {
@@ -40403,7 +40437,7 @@ async function resolveSmaChoice(options, json) {
40403
40437
  if (!isAddress(options.sma, { strict: false })) {
40404
40438
  throw new Error(`Invalid --sma address: ${options.sma}`);
40405
40439
  }
40406
- return { kind: "address", address: options.sma };
40440
+ return { kind: "address", address: getAddress(options.sma) };
40407
40441
  }
40408
40442
  if (options.newSma) return { kind: "new" };
40409
40443
  if (json) {
@@ -40415,7 +40449,7 @@ async function resolveSmaChoice(options, json) {
40415
40449
  );
40416
40450
  if (choice.toLowerCase() === "y" || choice.toLowerCase() === "yes") return { kind: "new" };
40417
40451
  if (!isAddress(choice, { strict: false })) throw new Error(`Invalid SMA address: ${choice}`);
40418
- return { kind: "address", address: choice };
40452
+ return { kind: "address", address: getAddress(choice) };
40419
40453
  }
40420
40454
  async function resolveTemplate(project, options, json) {
40421
40455
  const templates = (() => {
@@ -40432,7 +40466,7 @@ async function resolveTemplate(project, options, json) {
40432
40466
  );
40433
40467
  if (match) return { address: match.address, label: match.label };
40434
40468
  if (isAddress(options.template, { strict: false })) {
40435
- return { address: options.template, label: options.template };
40469
+ return { address: getAddress(options.template), label: options.template };
40436
40470
  }
40437
40471
  throw new Error(
40438
40472
  `Unknown mandate template "${options.template}". Run "sailor mandate templates".`
@@ -40779,7 +40813,7 @@ async function runDeploy(project, channel, options) {
40779
40813
  if (!json) fn();
40780
40814
  };
40781
40815
  if (options.attach && !options.sma) throw new Error("--attach requires --sma <address>");
40782
- if (options.sma && !isAddress(options.sma)) {
40816
+ if (options.sma && !isAddress(options.sma, { strict: false })) {
40783
40817
  throw new Error(`Invalid --sma address: ${options.sma}`);
40784
40818
  }
40785
40819
  if (project.chainId !== 8453) {
@@ -40855,16 +40889,17 @@ async function runDeploy(project, channel, options) {
40855
40889
  });
40856
40890
  let attachTxHash;
40857
40891
  if (options.attach && options.sma) {
40892
+ const sma = getAddress(options.sma);
40858
40893
  attachTxHash = await attachToSma(
40859
40894
  project,
40860
40895
  channel,
40861
40896
  publicClient,
40862
- options.sma,
40897
+ sma,
40863
40898
  deployed,
40864
40899
  record.name,
40865
40900
  json
40866
40901
  );
40867
- store.recordAttachment(deployed, { sma: options.sma, txHash: attachTxHash });
40902
+ store.recordAttachment(deployed, { sma, txHash: attachTxHash });
40868
40903
  } else {
40869
40904
  say(
40870
40905
  () => console.log(
@@ -40877,7 +40912,283 @@ Register it later with: sailor mandate attach --address ${deployed} --sma <SMA>`
40877
40912
  }, {
40878
40913
  status: "ok",
40879
40914
  mandate: { name: record.name, address: deployed, txHash: response.txHash, chainId },
40880
- attached: options.attach ? { sma: options.sma, txHash: attachTxHash } : null
40915
+ attached: options.attach ? { sma: getAddress(options.sma), txHash: attachTxHash } : null
40916
+ });
40917
+ }
40918
+ var CLONE_INIT_PREFIX = "0x3d602d80600a3d3981f3363d3d373d3d3d363d73";
40919
+ var CLONE_INIT_SUFFIX = "0x5af43d82803e903d91602b57fd5bf3";
40920
+ var PERMISSION_FACTORY_ABI = [
40921
+ {
40922
+ type: "function",
40923
+ name: "deployAndAttach",
40924
+ stateMutability: "payable",
40925
+ inputs: [
40926
+ { name: "account", type: "address" },
40927
+ { name: "impl", type: "address" },
40928
+ { name: "salt", type: "bytes32" },
40929
+ { name: "initData", type: "bytes" },
40930
+ { name: "kernelDeadline", type: "uint256" },
40931
+ { name: "kernelSig", type: "bytes" }
40932
+ ],
40933
+ outputs: [{ name: "clone", type: "address" }]
40934
+ }
40935
+ ];
40936
+ var CLONE_TEMPLATES = {
40937
+ boundedApprove: {
40938
+ label: "Bounded Approve",
40939
+ buildInitData: (p) => encodeFunctionData({
40940
+ abi: [
40941
+ {
40942
+ type: "function",
40943
+ name: "initialize",
40944
+ stateMutability: "nonpayable",
40945
+ inputs: [
40946
+ { name: "allowedTokens", type: "address[]" },
40947
+ { name: "allowedSpenders", type: "address[]" },
40948
+ { name: "_maxAmountPerTx", type: "uint256" },
40949
+ { name: "_permissionSigner", type: "address" }
40950
+ ],
40951
+ outputs: []
40952
+ }
40953
+ ],
40954
+ functionName: "initialize",
40955
+ args: [p.tokens, p.spenders, p.max, p.permissionSigner]
40956
+ }),
40957
+ describe: (p) => [
40958
+ { label: "Allowed tokens", value: p.tokens.join(", ") },
40959
+ { label: "Allowed spenders", value: p.spenders.join(", ") },
40960
+ {
40961
+ label: "Max approval / tx",
40962
+ value: p.max === maxUint256 ? "unlimited (uint256 max)" : p.max.toString()
40963
+ }
40964
+ ]
40965
+ }
40966
+ };
40967
+ function predictCloneAddress(impl, factory, submitter, salt) {
40968
+ const namespacedSalt = keccak256(
40969
+ encodeAbiParameters([{ type: "address" }, { type: "bytes32" }], [submitter, salt])
40970
+ );
40971
+ const initCode = concatHex([CLONE_INIT_PREFIX, impl, CLONE_INIT_SUFFIX]);
40972
+ return getCreate2Address({
40973
+ from: factory,
40974
+ salt: namespacedSalt,
40975
+ bytecodeHash: keccak256(initCode)
40976
+ });
40977
+ }
40978
+ function parseAddressList(csv, flag) {
40979
+ if (!csv) throw new Error(`${flag} is required (comma-separated address list)`);
40980
+ const list = csv.split(",").map((s) => s.trim()).filter(Boolean);
40981
+ if (list.length === 0) throw new Error(`${flag} is empty`);
40982
+ for (const a of list) {
40983
+ if (!isAddress(a, { strict: false })) throw new Error(`${flag} contains an invalid address: ${a}`);
40984
+ }
40985
+ return list.map((a) => getAddress(a));
40986
+ }
40987
+ async function mandateDeployClone(options) {
40988
+ const project = requireProject();
40989
+ const channel = await createSigningChannel(process.cwd());
40990
+ try {
40991
+ await channel.start();
40992
+ await runDeployClone(project, channel, options);
40993
+ } catch (err) {
40994
+ fail(err, options.json);
40995
+ } finally {
40996
+ channel.stop();
40997
+ }
40998
+ }
40999
+ async function runDeployClone(project, channel, options) {
41000
+ const json = !!options.json;
41001
+ const say = (fn) => {
41002
+ if (!json) fn();
41003
+ };
41004
+ if (!isAddress(options.sma, { strict: false })) throw new Error(`Invalid --sma address: ${options.sma}`);
41005
+ const sma = getAddress(options.sma);
41006
+ const spec = CLONE_TEMPLATES[options.template];
41007
+ if (!spec) {
41008
+ throw new Error(
41009
+ `Unsupported clone template "${options.template}". Supported: ${Object.keys(CLONE_TEMPLATES).join(", ")}`
41010
+ );
41011
+ }
41012
+ const impl = project.deployment.standaloneTemplates?.[options.template];
41013
+ if (!impl || !isAddress(impl, { strict: false })) {
41014
+ throw new Error(
41015
+ `No "${options.template}" standalone template is bundled for chain ${project.chainId}.`
41016
+ );
41017
+ }
41018
+ const chain2 = getChainById(project.chainId);
41019
+ const publicClient = publicClientFor(project);
41020
+ const agentSigner = await loadManagerSigner2();
41021
+ const registered = await publicClient.readContract({
41022
+ address: project.contracts.kernel,
41023
+ abi: SailKernelAbi,
41024
+ functionName: "registered",
41025
+ args: [sma]
41026
+ });
41027
+ if (!registered) {
41028
+ throw new Error(`SMA ${sma} is not registered with SailKernel; cannot register a permission.`);
41029
+ }
41030
+ const kernelConfig = await publicClient.readContract({
41031
+ address: project.contracts.kernel,
41032
+ abi: SailKernelAbi,
41033
+ functionName: "configs",
41034
+ args: [sma]
41035
+ });
41036
+ const permissionSigner = kernelConfig[0];
41037
+ const initParams = {
41038
+ permissionSigner,
41039
+ tokens: parseAddressList(options.tokens, "--tokens"),
41040
+ spenders: parseAddressList(options.spenders, "--spenders"),
41041
+ max: options.max ? BigInt(options.max) : maxUint256
41042
+ };
41043
+ const initData = spec.buildInitData(initParams);
41044
+ const submitter = agentSigner.address;
41045
+ const salt = keccak256(
41046
+ encodeAbiParameters(
41047
+ [{ type: "address" }, { type: "address" }, { type: "uint256" }],
41048
+ [sma, impl, BigInt(Math.floor(Date.now() / 1e3))]
41049
+ )
41050
+ );
41051
+ const clone = predictCloneAddress(impl, project.contracts.permissionFactory, submitter, salt);
41052
+ say(() => {
41053
+ console.log(`
41054
+ ${spec.label} clone (${options.template})`);
41055
+ console.log(` logic impl: ${impl}`);
41056
+ console.log(` predicted clone: ${clone}`);
41057
+ console.log(` SMA: ${sma}`);
41058
+ for (const d of spec.describe(initParams)) console.log(` ${d.label}: ${d.value}`);
41059
+ console.log(`
41060
+ \u2192 Signing station:
41061
+ Open ${channel.url} and connect your Owner wallet
41062
+ `);
41063
+ });
41064
+ const nonce = await publicClient.readContract({
41065
+ address: project.contracts.kernel,
41066
+ abi: SailKernelAbi,
41067
+ functionName: "signerNonces",
41068
+ args: [sma]
41069
+ });
41070
+ let registerPermissionHasDeadline = false;
41071
+ try {
41072
+ const caps = await detectKernelCapabilities(publicClient, project.contracts.kernel, {
41073
+ chainId: project.chainId
41074
+ });
41075
+ registerPermissionHasDeadline = caps.registerPermissionHasDeadline;
41076
+ } catch {
41077
+ }
41078
+ const deadline = registerPermissionHasDeadline ? BigInt(Math.floor(Date.now() / 1e3) + 300) : void 0;
41079
+ const typedData = buildRegisterPermissionTypedData({
41080
+ chainId: project.chainId,
41081
+ kernel: project.contracts.kernel,
41082
+ account: sma,
41083
+ permission: clone,
41084
+ nonce,
41085
+ hasDeadline: registerPermissionHasDeadline,
41086
+ deadline
41087
+ });
41088
+ const label = options.label ?? `${spec.label} (${options.template})`;
41089
+ say(
41090
+ () => console.log(
41091
+ `Pushing signing request \u2014 the mandate signer (${permissionSigner}) must sign in the browser.`
41092
+ )
41093
+ );
41094
+ const response = await channel.requestSignature({
41095
+ type: "typed-data",
41096
+ kind: "register-permission",
41097
+ title: `Authorize "${label}"`,
41098
+ description: `Sign to authorize a new ${spec.label} permission on your SMA. The agent deploys and registers it in one transaction.`,
41099
+ chainId: project.chainId,
41100
+ details: [
41101
+ { label: "SMA", value: sma },
41102
+ { label: "Permission (predicted)", value: clone },
41103
+ { label: "Template", value: options.template },
41104
+ { label: "Mandate signer", value: permissionSigner },
41105
+ ...spec.describe(initParams)
41106
+ ],
41107
+ typedData
41108
+ });
41109
+ if (response.status === "rejected") {
41110
+ throw new Error(`User rejected authorization: ${response.reason ?? "no reason given"}`);
41111
+ }
41112
+ if (response.status !== "signature") {
41113
+ throw new Error(`Expected EIP-712 signature response, got: ${response.status}`);
41114
+ }
41115
+ const signature = response.signature;
41116
+ try {
41117
+ const recoveredSigner = await recoverTypedDataAddress({
41118
+ domain: sailKernelDomain({ chainId: project.chainId, kernel: project.contracts.kernel }),
41119
+ types: registerPermissionHasDeadline ? REGISTER_PERMISSION_TYPES : REGISTER_PERMISSION_TYPES_NO_DEADLINE,
41120
+ primaryType: "RegisterPermission",
41121
+ message: registerPermissionHasDeadline ? { account: sma, permission: clone, nonce, deadline } : { account: sma, permission: clone, nonce },
41122
+ signature
41123
+ });
41124
+ if (recoveredSigner.toLowerCase() !== permissionSigner.toLowerCase()) {
41125
+ throw new Error(
41126
+ `Security: RegisterPermission was signed by ${recoveredSigner} but the on-chain mandate signer is ${permissionSigner}.
41127
+ Connect the owner wallet (mandate signer) in the browser \u2014 the agent wallet must never sign permission registrations.`
41128
+ );
41129
+ }
41130
+ } catch (err) {
41131
+ if (err.message.startsWith("Security:")) throw err;
41132
+ }
41133
+ const fee = await estimatePermissionFee(publicClient, project.contracts.governance, clone);
41134
+ if (!registerPermissionHasDeadline || deadline === void 0) {
41135
+ throw new Error(
41136
+ "deploy-clone requires a selective kernel (RegisterPermission with deadline). This chain's kernel does not match."
41137
+ );
41138
+ }
41139
+ say(() => console.log(`Submitting deployAndAttach (agent pays gas; fee ${fee} wei)\u2026`));
41140
+ const walletClient = createWalletClient({
41141
+ account: agentSigner.viemAccount,
41142
+ chain: chain2,
41143
+ transport: http(getRpcUrl(project.chainId))
41144
+ });
41145
+ const data = encodeFunctionData({
41146
+ abi: PERMISSION_FACTORY_ABI,
41147
+ functionName: "deployAndAttach",
41148
+ args: [sma, impl, salt, initData, deadline, signature]
41149
+ });
41150
+ const txHash = await walletClient.sendTransaction({
41151
+ to: project.contracts.permissionFactory,
41152
+ data,
41153
+ value: fee,
41154
+ account: agentSigner.viemAccount,
41155
+ chain: chain2
41156
+ });
41157
+ say(() => console.log("Waiting for confirmation\u2026"));
41158
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
41159
+ if (receipt.status !== "success") {
41160
+ throw new Error(`deployAndAttach reverted (tx ${txHash})`);
41161
+ }
41162
+ const attached = await pollForPermission(publicClient, project.contracts.kernel, sma, clone);
41163
+ if (!attached) {
41164
+ throw new Error(
41165
+ `Tx ${txHash} mined, but clone ${clone} is not in getPermissions(${sma}). Verify on-chain.`
41166
+ );
41167
+ }
41168
+ say(() => console.log("\u2713", `Deployed + registered ${spec.label} at ${clone}`));
41169
+ const store = new MandateStore();
41170
+ store.add({
41171
+ name: label,
41172
+ address: clone,
41173
+ txHash,
41174
+ chainId: project.chainId,
41175
+ deployedAt: (/* @__PURE__ */ new Date()).toISOString()
41176
+ });
41177
+ store.recordAttachment(clone, { sma, txHash });
41178
+ appendActivity({
41179
+ ts: nowIso(),
41180
+ actor: "agent",
41181
+ type: "permission_registered",
41182
+ permission: clone,
41183
+ name: label,
41184
+ sma,
41185
+ txHash,
41186
+ chainId: project.chainId
41187
+ });
41188
+ emit(json, () => {
41189
+ }, {
41190
+ status: "ok",
41191
+ clone: { template: options.template, address: clone, impl, txHash, sma, chainId: project.chainId }
40881
41192
  });
40882
41193
  }
40883
41194
  async function mandateAttach(options) {
@@ -40894,15 +41205,17 @@ async function mandateAttach(options) {
40894
41205
  }
40895
41206
  async function runAttach(project, channel, options) {
40896
41207
  const json = !!options.json;
40897
- if (!isAddress(options.sma)) throw new Error(`Invalid --sma address: ${options.sma}`);
41208
+ if (!isAddress(options.sma, { strict: false })) throw new Error(`Invalid --sma address: ${options.sma}`);
41209
+ const sma = getAddress(options.sma);
40898
41210
  const store = new MandateStore();
40899
41211
  const tracked = store.find(options.address);
40900
- const mandateAddress = tracked?.address ?? options.address;
40901
- if (!isAddress(mandateAddress)) {
41212
+ const rawAddress = tracked?.address ?? options.address;
41213
+ if (!isAddress(rawAddress, { strict: false })) {
40902
41214
  throw new Error(
40903
41215
  `--address must be a deployed mandate address or a tracked name: ${options.address}`
40904
41216
  );
40905
41217
  }
41218
+ const mandateAddress = getAddress(rawAddress);
40906
41219
  const label = options.label ?? tracked?.name ?? "mandate";
40907
41220
  const publicClient = publicClientFor(project);
40908
41221
  if (!json) {
@@ -40917,16 +41230,16 @@ async function runAttach(project, channel, options) {
40917
41230
  project,
40918
41231
  channel,
40919
41232
  publicClient,
40920
- options.sma,
41233
+ sma,
40921
41234
  mandateAddress,
40922
41235
  label,
40923
41236
  json
40924
41237
  );
40925
- if (tracked) store.recordAttachment(mandateAddress, { sma: options.sma, txHash });
41238
+ if (tracked) store.recordAttachment(mandateAddress, { sma, txHash });
40926
41239
  emit(json, () => {
40927
41240
  }, {
40928
41241
  status: "ok",
40929
- attached: { sma: options.sma, mandate: mandateAddress, txHash }
41242
+ attached: { sma, mandate: mandateAddress, txHash }
40930
41243
  });
40931
41244
  }
40932
41245
  async function mandateRevoke(options) {
@@ -40946,11 +41259,11 @@ async function runRevoke(project, channel, options) {
40946
41259
  const say = (fn) => {
40947
41260
  if (!json) fn();
40948
41261
  };
40949
- if (!isAddress(options.sma)) throw new Error(`Invalid --sma address: ${options.sma}`);
41262
+ if (!isAddress(options.sma, { strict: false })) throw new Error(`Invalid --sma address: ${options.sma}`);
40950
41263
  if (!options.all && !options.address) {
40951
41264
  throw new Error("Provide --address <permission> (or a tracked name), or --all");
40952
41265
  }
40953
- const sma = options.sma;
41266
+ const sma = getAddress(options.sma);
40954
41267
  const kernel = project.contracts.kernel;
40955
41268
  const publicClient = publicClientFor(project);
40956
41269
  const onchain = await publicClient.readContract({
@@ -40966,10 +41279,11 @@ async function runRevoke(project, channel, options) {
40966
41279
  targets = onchain;
40967
41280
  } else {
40968
41281
  const tracked = store.find(options.address);
40969
- const wanted = tracked?.address ?? options.address;
40970
- if (!isAddress(wanted)) {
41282
+ const rawWanted = tracked?.address ?? options.address;
41283
+ if (!isAddress(rawWanted, { strict: false })) {
40971
41284
  throw new Error(`--address must be a permission address or a tracked name: ${options.address}`);
40972
41285
  }
41286
+ const wanted = getAddress(rawWanted);
40973
41287
  const match = onchain.find((p) => p.toLowerCase() === wanted.toLowerCase());
40974
41288
  if (!match) {
40975
41289
  throw new Error(`${wanted} is not in the SMA's current permission set; nothing to revoke.`);
@@ -41291,9 +41605,29 @@ function runForgeBuild() {
41291
41605
  }
41292
41606
 
41293
41607
  // src/commands/mandate.ts
41294
- function trackedPermissionsFor(account2) {
41608
+ init_esm2();
41609
+ async function fetchOnChainPermissions(account2) {
41610
+ try {
41611
+ const project = new ProjectContext();
41612
+ const rpcUrl = getRpcUrl(project.chainId) ?? getChainById(project.chainId).rpcUrls.default.http[0];
41613
+ const pc = createPublicClient({
41614
+ chain: getChainById(project.chainId),
41615
+ transport: http(rpcUrl)
41616
+ });
41617
+ const onChain = await pc.readContract({
41618
+ address: project.contracts.kernel,
41619
+ abi: SailKernelAbi,
41620
+ functionName: "getPermissions",
41621
+ args: [account2.safe]
41622
+ });
41623
+ return new Set(onChain.map((a) => a.toLowerCase()));
41624
+ } catch {
41625
+ return null;
41626
+ }
41627
+ }
41628
+ async function trackedPermissionsFor(account2) {
41295
41629
  const store = new MandateStore();
41296
- return store.list().filter((m) => m.chainId === account2.chainId).map((m) => {
41630
+ const local = store.list().filter((m) => m.chainId === account2.chainId).map((m) => {
41297
41631
  const attachment = m.attachments?.find(
41298
41632
  (a) => a.sma.toLowerCase() === account2.safe.toLowerCase()
41299
41633
  );
@@ -41304,6 +41638,15 @@ function trackedPermissionsFor(account2) {
41304
41638
  attachedAt: attachment?.at
41305
41639
  };
41306
41640
  });
41641
+ const onChain = await fetchOnChainPermissions(account2);
41642
+ if (onChain !== null) {
41643
+ for (const p of local) {
41644
+ if (p.registeredOnSma && !onChain.has(p.address.toLowerCase())) {
41645
+ p.revokedOnChain = true;
41646
+ }
41647
+ }
41648
+ }
41649
+ return local;
41307
41650
  }
41308
41651
  function printNoPermissionsGuidance() {
41309
41652
  console.log(
@@ -41315,7 +41658,7 @@ async function mandatePrepare() {
41315
41658
  if (!account2) {
41316
41659
  throw new Error('No account found at .sail/account.json.\nRun "sailor account create" first.');
41317
41660
  }
41318
- const permissions = trackedPermissionsFor(account2);
41661
+ const permissions = await trackedPermissionsFor(account2);
41319
41662
  if (permissions.length === 0) {
41320
41663
  printNoPermissionsGuidance();
41321
41664
  return;
@@ -41324,7 +41667,7 @@ async function mandatePrepare() {
41324
41667
  ${permissions.length} permission(s) tracked for SMA ${account2.safe}:
41325
41668
  `);
41326
41669
  for (const p of permissions) {
41327
- const status2 = p.registeredOnSma ? `registered on this SMA${p.attachedAt ? ` (${p.attachedAt})` : ""}` : "not yet registered on this SMA";
41670
+ const status2 = p.revokedOnChain ? "revoked on-chain (local record is stale)" : p.registeredOnSma ? `registered on this SMA${p.attachedAt ? ` (${p.attachedAt})` : ""}` : "not yet registered on this SMA";
41328
41671
  console.log(`\u2022 ${p.label}`);
41329
41672
  console.log(` ${p.address}`);
41330
41673
  console.log(` ${status2}`);
@@ -41332,7 +41675,8 @@ ${permissions.length} permission(s) tracked for SMA ${account2.safe}:
41332
41675
  const draft = {
41333
41676
  account: account2.safe,
41334
41677
  chainId: account2.chainId,
41335
- permissions: permissions.map((p) => ({ address: p.address, label: p.label })),
41678
+ // Exclude permissions revoked on-chain the draft reflects the live set.
41679
+ permissions: permissions.filter((p) => !p.revokedOnChain).map((p) => ({ address: p.address, label: p.label })),
41336
41680
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
41337
41681
  };
41338
41682
  writeJsonFile(sailPath("mandate-draft.json"), draft);
@@ -41343,7 +41687,7 @@ async function mandateSign(opts = {}) {
41343
41687
  if (!account2) {
41344
41688
  throw new Error('No account found at .sail/account.json.\nRun "sailor account create" first.');
41345
41689
  }
41346
- const permissions = trackedPermissionsFor(account2);
41690
+ const permissions = await trackedPermissionsFor(account2);
41347
41691
  if (permissions.length === 0) {
41348
41692
  printNoPermissionsGuidance();
41349
41693
  return;
@@ -41353,22 +41697,25 @@ Permissions tracked for SMA ${account2.safe}:
41353
41697
  `);
41354
41698
  for (const p of permissions) {
41355
41699
  console.log(`\u2022 ${p.label} (${p.address})`);
41356
- console.log(` ${p.registeredOnSma ? "registered on-chain" : "NOT yet registered on this SMA"}`);
41700
+ console.log(
41701
+ ` ${p.revokedOnChain ? "revoked on-chain (local record is stale)" : p.registeredOnSma ? "registered on-chain" : "NOT yet registered on this SMA"}`
41702
+ );
41357
41703
  }
41358
41704
  console.log(
41359
41705
  "\nNote: `sailor mandate sign` reviews and confirms the permissions attached to your SMA.\nOn-chain registration happens via `sailor mandate attach` (or `sailor mandate deploy --attach`)."
41360
41706
  );
41707
+ const activePermissions = permissions.filter((p) => !p.revokedOnChain);
41361
41708
  const proceed = opts.yes || await confirm(
41362
- `Confirm these ${permissions.length} permission(s) are authorized for your SMA?`
41709
+ `Confirm these ${activePermissions.length} permission(s) are authorized for your SMA?`
41363
41710
  );
41364
41711
  if (!proceed) {
41365
41712
  console.log("No permissions confirmed.");
41366
41713
  return;
41367
41714
  }
41368
- const unregistered = permissions.filter((p) => !p.registeredOnSma);
41715
+ const unregistered = activePermissions.filter((p) => !p.registeredOnSma);
41369
41716
  if (unregistered.length === 0) {
41370
41717
  console.log(`
41371
- \u2713 Confirmed ${permissions.length} permission(s) for ${account2.safe}.`);
41718
+ \u2713 Confirmed ${activePermissions.length} permission(s) for ${account2.safe}.`);
41372
41719
  } else {
41373
41720
  console.log(
41374
41721
  `
@@ -41384,7 +41731,8 @@ ${unregistered.length} permission(s) are not yet registered on this SMA. Initiat
41384
41731
  signedAt: (/* @__PURE__ */ new Date()).toISOString(),
41385
41732
  signature: "",
41386
41733
  registeredOnChain: true,
41387
- permissions: permissions.map((p) => ({ template: p.label, params: {} }))
41734
+ // Only include permissions that are currently active on-chain.
41735
+ permissions: activePermissions.map((p) => ({ template: p.label, params: {} }))
41388
41736
  };
41389
41737
  writeJsonFile(sailPath("mandate.json"), storedMandate);
41390
41738
  console.log(`
@@ -41723,14 +42071,15 @@ async function resolveNewManager(options, oldManager, json, say) {
41723
42071
  if (!isAddress(options.to, { strict: false })) {
41724
42072
  throw new Error(`Invalid --to address: ${options.to}`);
41725
42073
  }
42074
+ const to = getAddress(options.to);
41726
42075
  say(
41727
42076
  () => console.log(
41728
42077
  `
41729
- Rotating to existing address ${options.to}. The local agent keystore is left unchanged \u2014
42078
+ Rotating to existing address ${to}. The local agent keystore is left unchanged \u2014
41730
42079
  ensure the agent that signs dispatches holds this key.`
41731
42080
  )
41732
42081
  );
41733
- return options.to;
42082
+ return to;
41734
42083
  }
41735
42084
  if (json) {
41736
42085
  throw new Error("Pass --to <address> in --json mode (key generation is interactive).");
@@ -42114,7 +42463,8 @@ Configure the chain in @sail/chains or set KERNEL_ADDRESS in .sail/.env.local.`
42114
42463
  } catch (err) {
42115
42464
  console.error(`could not read registered permissions: ${err.message}`);
42116
42465
  }
42117
- let tickDispatched = 0;
42466
+ let tickExecuted = 0;
42467
+ let tickReverted = 0;
42118
42468
  let tickSkipped = 0;
42119
42469
  for (const rawDispatch of dispatches) {
42120
42470
  const dispatch = rawDispatch;
@@ -42193,9 +42543,15 @@ Configure the chain in @sail/chains or set KERNEL_ADDRESS in .sail/.env.local.`
42193
42543
  } catch {
42194
42544
  }
42195
42545
  }
42196
- appendActivity({ ts: nowIso(), actor: "agent", type: "dispatch_executed", permission, target, txHash: result.txHash });
42197
- console.log(`executed: ${result.txHash}`);
42198
- tickDispatched++;
42546
+ if (!result.success) {
42547
+ appendActivity({ ts: nowIso(), actor: "agent", type: "dispatch_reverted", permission, target, txHash: result.txHash, gasUsed: String(result.gasUsed) });
42548
+ console.error(`reverted: ${result.txHash} (gas used: ${result.gasUsed})`);
42549
+ tickReverted++;
42550
+ } else {
42551
+ appendActivity({ ts: nowIso(), actor: "agent", type: "dispatch_executed", permission, target, txHash: result.txHash });
42552
+ console.log(`executed: ${result.txHash}`);
42553
+ tickExecuted++;
42554
+ }
42199
42555
  } catch (err) {
42200
42556
  const reason = err.message;
42201
42557
  console.error(`dispatch error: ${reason}`);
@@ -42204,13 +42560,10 @@ Configure the chain in @sail/chains or set KERNEL_ADDRESS in .sail/.env.local.`
42204
42560
  }
42205
42561
  }
42206
42562
  if (dispatches.length > 0) {
42207
- if (tickSkipped > 0) {
42208
- console.log(
42209
- `tick complete: ${tickDispatched} dispatched, ${tickSkipped} skipped (no matching permission or denied)`
42210
- );
42211
- } else {
42212
- console.log(`tick complete: ${tickDispatched} dispatched`);
42213
- }
42563
+ const parts = [`${tickExecuted} executed`];
42564
+ if (tickReverted > 0) parts.push(`${tickReverted} reverted`);
42565
+ if (tickSkipped > 0) parts.push(`${tickSkipped} skipped`);
42566
+ console.log(`tick complete: ${parts.join(", ")}`);
42214
42567
  }
42215
42568
  appendActivity({ ts: nowIso(), actor: "agent", type: "tick_end" });
42216
42569
  }
@@ -42265,8 +42618,8 @@ async function scan(options) {
42265
42618
  process.exit(1);
42266
42619
  }
42267
42620
  const project = new ProjectContext();
42268
- const owner2 = options.owner ?? project.getOwner() ?? void 0;
42269
- if (!owner2 || !isAddress(owner2, { strict: false })) {
42621
+ const rawOwner = options.owner ?? project.getOwner() ?? void 0;
42622
+ if (!rawOwner || !isAddress(rawOwner, { strict: false })) {
42270
42623
  emit(
42271
42624
  options.json,
42272
42625
  () => console.log(
@@ -42276,6 +42629,7 @@ async function scan(options) {
42276
42629
  );
42277
42630
  process.exit(1);
42278
42631
  }
42632
+ const owner2 = getAddress(rawOwner);
42279
42633
  const chainId = project.chainId;
42280
42634
  const kernel = project.contracts.kernel;
42281
42635
  const publicClient = createPublicClient({
@@ -42699,6 +43053,9 @@ ui.action(action(uiCommand));
42699
43053
  var keys = program2.command("keys").description("Manage local signing keys");
42700
43054
  keys.command("generate").description("Generate and encrypt an agent wallet or mandate signer key").action(action(keysGenerate));
42701
43055
  keys.command("show").description("Show the address of each stored key").action(action(keysShow));
43056
+ keys.command("export-ci").description(
43057
+ "Copy the encrypted agent wallet keystore to ci-keystore.json for committing to CI"
43058
+ ).action(action(keysExportCi));
42702
43059
  var account = program2.command("account").description("Manage the Sail SMA");
42703
43060
  account.command("create").description("Create a new Sail SMA on-chain").action(action(accountCreate));
42704
43061
  account.command("rotate-signer").description("Rotate the SMA's delegated signer (agent wallet) and re-approve its mandates").option("--sma <address>", "SMA to rotate (defaults to the active account)").option("--to <address>", "Rotate to an existing agent-wallet address instead of generating one").option("--generate", "Generate a fresh local agent wallet (default when --to is omitted)").option("--skip-reattach", "Do not re-approve the previously-attached mandates").option("--reattach-only", "Skip rotation; only re-approve mandates (resume after funding)").option("--json", "Machine-readable output").action(actionWith(rotateSigner));
@@ -42707,6 +43064,7 @@ mandate.command("prepare").description("Prepare a mandate draft for review and s
42707
43064
  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));
42708
43065
  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 a JSON array, e.g. '[["0x.."],"1000"]'`).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));
42709
43066
  mandate.command("attach").description("Register an already-deployed permission on an SMA (EIP-712 RegisterPermission)").requiredOption("--address <mandateOrName>", "Permission address, or a name tracked locally").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));
43067
+ mandate.command("deploy-clone").description("Deploy + register a standalone clone permission (e.g. boundedApprove) 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));
42710
43068
  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));
42711
43069
  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));
42712
43070
  mandate.command("list").description("List permission contracts deployed from this project").action(action(async () => mandateContractsList()));
@@ -42744,7 +43102,6 @@ function stub(name, description) {
42744
43102
  console.log(`sailor ${name}: not implemented yet`);
42745
43103
  });
42746
43104
  }
42747
- stub("setup", "Walk through the Sailor setup guide");
42748
43105
  stub("dispatch preview", "Preview a dispatch without submitting");
42749
43106
  program2.parse(process.argv);
42750
43107
  /*! Bundled license information: