shll-skills 5.3.4 → 5.4.1

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/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: shll-run
3
3
  description: Execute DeFi transactions on BSC via SHLL AgentNFA. The AI handles all commands — users only need to chat.
4
- version: 5.3.4
4
+ version: 5.4.1
5
5
  author: SHLL Team
6
6
  website: https://shll.run
7
7
  twitter: https://twitter.com/shllrun
package/dist/index.js CHANGED
@@ -797,6 +797,57 @@ function createClient(options) {
797
797
  chainId: 56
798
798
  });
799
799
  }
800
+ var AGENT_NFA_ACCESS_ABI = [
801
+ { name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
802
+ { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
803
+ { name: "operatorOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
804
+ { name: "userOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
805
+ { name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] }
806
+ ];
807
+ async function checkAccess(opts, tokenId) {
808
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
809
+ const nfa = opts.nfaAddress || DEFAULT_NFA;
810
+ const pk = toHex(process.env.RUNNER_PRIVATE_KEY || "");
811
+ const account = (0, import_accounts2.privateKeyToAccount)(pk);
812
+ const pc = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(rpcUrl) });
813
+ const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
814
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
815
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userExpires", args: [tokenId] }),
816
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorOf", args: [tokenId] }),
817
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userOf", args: [tokenId] }),
818
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "ownerOf", args: [tokenId] })
819
+ ]);
820
+ const now = BigInt(Math.floor(Date.now() / 1e3));
821
+ if (now > userExpires) {
822
+ output({ status: "error", message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me` });
823
+ process.exit(1);
824
+ }
825
+ if (now > operatorExpires) {
826
+ output({ status: "error", message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1e3).toISOString()}). Please re-authorize via setup_guide.` });
827
+ process.exit(1);
828
+ }
829
+ const runnerAddr = account.address.toLowerCase();
830
+ const isOperator = operator.toLowerCase() === runnerAddr;
831
+ const isRenter = renter.toLowerCase() === runnerAddr;
832
+ const isOwner = owner.toLowerCase() === runnerAddr;
833
+ if (!isOperator && !isRenter && !isOwner) {
834
+ output({
835
+ status: "error",
836
+ message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}.`,
837
+ yourWallet: account.address,
838
+ onChainOperator: operator,
839
+ onChainRenter: renter,
840
+ onChainOwner: owner,
841
+ howToFix: [
842
+ `1. Use 'setup_guide' command to generate an OperatorPermit for this wallet`,
843
+ `2. Renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry>) on AgentNFA`,
844
+ `3. Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety to set operator`,
845
+ `4. Use the correct RUNNER_PRIVATE_KEY for operator ${operator}`
846
+ ]
847
+ });
848
+ process.exit(1);
849
+ }
850
+ }
800
851
  var program = new import_commander.Command();
801
852
  program.name("shll-onchain-runner").description("Execute DeFi actions securely via SHLL AgentNFA");
802
853
  var swapCmd = new import_commander.Command("swap").description("Swap tokens on PancakeSwap (auto-routes V2/V3)").requiredOption("-f, --from <token>", "Input token (symbol or 0x address, e.g. USDC, BNB)").requiredOption("-t, --to <token>", "Output token (symbol or 0x address)").requiredOption("-a, --amount <number>", "Amount to swap (human-readable, e.g. 0.5)").option("-s, --slippage <percent>", "Slippage tolerance in percent (default: 5)", "5").option("--dex <mode>", "DEX routing: auto, v2, v3 (default: auto)", "auto").option("--fee <tier>", "V3 fee tier in bps (default: 2500 = 0.25%)", "2500").option("--router <address>", "DEX router address (override)");
@@ -805,6 +856,7 @@ swapCmd.action(async (opts) => {
805
856
  try {
806
857
  const client = createClient(opts);
807
858
  const tokenId = BigInt(opts.tokenId);
859
+ await checkAccess(opts, tokenId);
808
860
  const rpcUrl = opts.rpc || DEFAULT_RPC;
809
861
  const fromToken = resolveToken(opts.from);
810
862
  const toToken = resolveToken(opts.to);
@@ -1361,6 +1413,7 @@ wrapCmd.action(async (opts) => {
1361
1413
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
1362
1414
  process.exit(1);
1363
1415
  }
1416
+ await checkAccess(opts, BigInt(opts.tokenId));
1364
1417
  const client = new PolicyClient({
1365
1418
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
1366
1419
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -1396,6 +1449,7 @@ unwrapCmd.action(async (opts) => {
1396
1449
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
1397
1450
  process.exit(1);
1398
1451
  }
1452
+ await checkAccess(opts, BigInt(opts.tokenId));
1399
1453
  const client = new PolicyClient({
1400
1454
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
1401
1455
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -1432,6 +1486,7 @@ transferCmd.action(async (opts) => {
1432
1486
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
1433
1487
  process.exit(1);
1434
1488
  }
1489
+ await checkAccess(opts, BigInt(opts.tokenId));
1435
1490
  const client = new PolicyClient({
1436
1491
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
1437
1492
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -2003,6 +2058,7 @@ lendCmd.action(async (opts) => {
2003
2058
  try {
2004
2059
  const client = createClient(opts);
2005
2060
  const tokenId = BigInt(opts.tokenId);
2061
+ await checkAccess(opts, tokenId);
2006
2062
  const rpcUrl = opts.rpc || DEFAULT_RPC;
2007
2063
  const publicClient = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(rpcUrl) });
2008
2064
  const symbol = opts.token.toUpperCase();
@@ -2070,6 +2126,7 @@ redeemCmd.action(async (opts) => {
2070
2126
  try {
2071
2127
  const client = createClient(opts);
2072
2128
  const tokenId = BigInt(opts.tokenId);
2129
+ await checkAccess(opts, tokenId);
2073
2130
  const symbol = opts.token.toUpperCase();
2074
2131
  const vTokenAddr = VENUS_VTOKENS[symbol];
2075
2132
  if (!vTokenAddr) {
package/dist/index.mjs CHANGED
@@ -309,6 +309,57 @@ function createClient(options) {
309
309
  chainId: 56
310
310
  });
311
311
  }
312
+ var AGENT_NFA_ACCESS_ABI = [
313
+ { name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
314
+ { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
315
+ { name: "operatorOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
316
+ { name: "userOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
317
+ { name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] }
318
+ ];
319
+ async function checkAccess(opts, tokenId) {
320
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
321
+ const nfa = opts.nfaAddress || DEFAULT_NFA;
322
+ const pk = toHex(process.env.RUNNER_PRIVATE_KEY || "");
323
+ const account = privateKeyToAccount(pk);
324
+ const pc = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
325
+ const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
326
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
327
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userExpires", args: [tokenId] }),
328
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorOf", args: [tokenId] }),
329
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userOf", args: [tokenId] }),
330
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "ownerOf", args: [tokenId] })
331
+ ]);
332
+ const now = BigInt(Math.floor(Date.now() / 1e3));
333
+ if (now > userExpires) {
334
+ output({ status: "error", message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me` });
335
+ process.exit(1);
336
+ }
337
+ if (now > operatorExpires) {
338
+ output({ status: "error", message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1e3).toISOString()}). Please re-authorize via setup_guide.` });
339
+ process.exit(1);
340
+ }
341
+ const runnerAddr = account.address.toLowerCase();
342
+ const isOperator = operator.toLowerCase() === runnerAddr;
343
+ const isRenter = renter.toLowerCase() === runnerAddr;
344
+ const isOwner = owner.toLowerCase() === runnerAddr;
345
+ if (!isOperator && !isRenter && !isOwner) {
346
+ output({
347
+ status: "error",
348
+ message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}.`,
349
+ yourWallet: account.address,
350
+ onChainOperator: operator,
351
+ onChainRenter: renter,
352
+ onChainOwner: owner,
353
+ howToFix: [
354
+ `1. Use 'setup_guide' command to generate an OperatorPermit for this wallet`,
355
+ `2. Renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry>) on AgentNFA`,
356
+ `3. Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety to set operator`,
357
+ `4. Use the correct RUNNER_PRIVATE_KEY for operator ${operator}`
358
+ ]
359
+ });
360
+ process.exit(1);
361
+ }
362
+ }
312
363
  var program = new Command();
313
364
  program.name("shll-onchain-runner").description("Execute DeFi actions securely via SHLL AgentNFA");
314
365
  var swapCmd = new Command("swap").description("Swap tokens on PancakeSwap (auto-routes V2/V3)").requiredOption("-f, --from <token>", "Input token (symbol or 0x address, e.g. USDC, BNB)").requiredOption("-t, --to <token>", "Output token (symbol or 0x address)").requiredOption("-a, --amount <number>", "Amount to swap (human-readable, e.g. 0.5)").option("-s, --slippage <percent>", "Slippage tolerance in percent (default: 5)", "5").option("--dex <mode>", "DEX routing: auto, v2, v3 (default: auto)", "auto").option("--fee <tier>", "V3 fee tier in bps (default: 2500 = 0.25%)", "2500").option("--router <address>", "DEX router address (override)");
@@ -317,6 +368,7 @@ swapCmd.action(async (opts) => {
317
368
  try {
318
369
  const client = createClient(opts);
319
370
  const tokenId = BigInt(opts.tokenId);
371
+ await checkAccess(opts, tokenId);
320
372
  const rpcUrl = opts.rpc || DEFAULT_RPC;
321
373
  const fromToken = resolveToken(opts.from);
322
374
  const toToken = resolveToken(opts.to);
@@ -873,6 +925,7 @@ wrapCmd.action(async (opts) => {
873
925
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
874
926
  process.exit(1);
875
927
  }
928
+ await checkAccess(opts, BigInt(opts.tokenId));
876
929
  const client = new PolicyClient({
877
930
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
878
931
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -908,6 +961,7 @@ unwrapCmd.action(async (opts) => {
908
961
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
909
962
  process.exit(1);
910
963
  }
964
+ await checkAccess(opts, BigInt(opts.tokenId));
911
965
  const client = new PolicyClient({
912
966
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
913
967
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -944,6 +998,7 @@ transferCmd.action(async (opts) => {
944
998
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
945
999
  process.exit(1);
946
1000
  }
1001
+ await checkAccess(opts, BigInt(opts.tokenId));
947
1002
  const client = new PolicyClient({
948
1003
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
949
1004
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -1515,6 +1570,7 @@ lendCmd.action(async (opts) => {
1515
1570
  try {
1516
1571
  const client = createClient(opts);
1517
1572
  const tokenId = BigInt(opts.tokenId);
1573
+ await checkAccess(opts, tokenId);
1518
1574
  const rpcUrl = opts.rpc || DEFAULT_RPC;
1519
1575
  const publicClient = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
1520
1576
  const symbol = opts.token.toUpperCase();
@@ -1582,6 +1638,7 @@ redeemCmd.action(async (opts) => {
1582
1638
  try {
1583
1639
  const client = createClient(opts);
1584
1640
  const tokenId = BigInt(opts.tokenId);
1641
+ await checkAccess(opts, tokenId);
1585
1642
  const symbol = opts.token.toUpperCase();
1586
1643
  const vTokenAddr = VENUS_VTOKENS[symbol];
1587
1644
  if (!vTokenAddr) {
package/dist/mcp.js CHANGED
@@ -646,16 +646,23 @@ function createClients() {
646
646
  });
647
647
  return { account, publicClient, policyClient, config };
648
648
  }
649
- var AGENT_NFA_EXPIRY_ABI = [
649
+ var AGENT_NFA_CHECK_ABI = [
650
650
  { name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
651
- { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] }
651
+ { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
652
+ { name: "operatorOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
653
+ { name: "userOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
654
+ { name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] }
652
655
  ];
653
656
  async function checkAgentExpiry(tokenId) {
654
657
  const config = getConfig();
658
+ const account = (0, import_accounts2.privateKeyToAccount)(config.privateKey);
655
659
  const pc = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(config.rpc) });
656
- const [operatorExpires, userExpires] = await Promise.all([
657
- pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
658
- pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] })
660
+ const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
661
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
662
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "userExpires", args: [tokenId] }),
663
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorOf", args: [tokenId] }),
664
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "userOf", args: [tokenId] }),
665
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "ownerOf", args: [tokenId] })
659
666
  ]);
660
667
  const now = BigInt(Math.floor(Date.now() / 1e3));
661
668
  if (now > operatorExpires) {
@@ -686,6 +693,33 @@ async function checkAgentExpiry(tokenId) {
686
693
  }]
687
694
  };
688
695
  }
696
+ const runnerAddr = account.address.toLowerCase();
697
+ const isOperator = operator.toLowerCase() === runnerAddr;
698
+ const isRenter = renter.toLowerCase() === runnerAddr;
699
+ const isOwner = owner.toLowerCase() === runnerAddr;
700
+ if (!isOperator && !isRenter && !isOwner) {
701
+ return {
702
+ expired: true,
703
+ // reuse expired flag to block execution
704
+ content: [{
705
+ type: "text",
706
+ text: JSON.stringify({
707
+ status: "error",
708
+ message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}. Your wallet must be the operator, renter, or owner to execute transactions.`,
709
+ yourWallet: account.address,
710
+ onChainOperator: operator,
711
+ onChainRenter: renter,
712
+ onChainOwner: owner,
713
+ howToFix: [
714
+ `Option 1: Use the 'setup_guide' tool \u2014 it generates an EIP-712 OperatorPermit that lets the renter (${renter}) authorize your current wallet (${account.address}) as operator. The renter signs the permit in their browser wallet, then the runner submits it on-chain.`,
715
+ `Option 2: The renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry_timestamp>) on AgentNFA contract at ${config.nfa} to directly authorize this wallet.`,
716
+ `Option 3: Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety and set ${account.address} as the operator.`,
717
+ `Option 4: If you have access to the correct operator wallet (${operator}), set RUNNER_PRIVATE_KEY to that wallet's private key instead.`
718
+ ]
719
+ })
720
+ }]
721
+ };
722
+ }
689
723
  return { expired: false };
690
724
  }
691
725
  function policyRejectionHelp(reason, tokenId) {
package/dist/mcp.mjs CHANGED
@@ -157,16 +157,23 @@ function createClients() {
157
157
  });
158
158
  return { account, publicClient, policyClient, config };
159
159
  }
160
- var AGENT_NFA_EXPIRY_ABI = [
160
+ var AGENT_NFA_CHECK_ABI = [
161
161
  { name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
162
- { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] }
162
+ { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
163
+ { name: "operatorOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
164
+ { name: "userOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
165
+ { name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] }
163
166
  ];
164
167
  async function checkAgentExpiry(tokenId) {
165
168
  const config = getConfig();
169
+ const account = privateKeyToAccount(config.privateKey);
166
170
  const pc = createPublicClient({ chain: bsc, transport: http(config.rpc) });
167
- const [operatorExpires, userExpires] = await Promise.all([
168
- pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
169
- pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] })
171
+ const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
172
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
173
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "userExpires", args: [tokenId] }),
174
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorOf", args: [tokenId] }),
175
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "userOf", args: [tokenId] }),
176
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "ownerOf", args: [tokenId] })
170
177
  ]);
171
178
  const now = BigInt(Math.floor(Date.now() / 1e3));
172
179
  if (now > operatorExpires) {
@@ -197,6 +204,33 @@ async function checkAgentExpiry(tokenId) {
197
204
  }]
198
205
  };
199
206
  }
207
+ const runnerAddr = account.address.toLowerCase();
208
+ const isOperator = operator.toLowerCase() === runnerAddr;
209
+ const isRenter = renter.toLowerCase() === runnerAddr;
210
+ const isOwner = owner.toLowerCase() === runnerAddr;
211
+ if (!isOperator && !isRenter && !isOwner) {
212
+ return {
213
+ expired: true,
214
+ // reuse expired flag to block execution
215
+ content: [{
216
+ type: "text",
217
+ text: JSON.stringify({
218
+ status: "error",
219
+ message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}. Your wallet must be the operator, renter, or owner to execute transactions.`,
220
+ yourWallet: account.address,
221
+ onChainOperator: operator,
222
+ onChainRenter: renter,
223
+ onChainOwner: owner,
224
+ howToFix: [
225
+ `Option 1: Use the 'setup_guide' tool \u2014 it generates an EIP-712 OperatorPermit that lets the renter (${renter}) authorize your current wallet (${account.address}) as operator. The renter signs the permit in their browser wallet, then the runner submits it on-chain.`,
226
+ `Option 2: The renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry_timestamp>) on AgentNFA contract at ${config.nfa} to directly authorize this wallet.`,
227
+ `Option 3: Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety and set ${account.address} as the operator.`,
228
+ `Option 4: If you have access to the correct operator wallet (${operator}), set RUNNER_PRIVATE_KEY to that wallet's private key instead.`
229
+ ]
230
+ })
231
+ }]
232
+ };
233
+ }
200
234
  return { expired: false };
201
235
  }
202
236
  function policyRejectionHelp(reason, tokenId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shll-skills",
3
- "version": "5.3.4",
3
+ "version": "5.4.1",
4
4
  "description": "SHLL DeFi Agent — CLI + MCP Server for BSC",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -24,4 +24,4 @@
24
24
  "tsup": "^8.0.2",
25
25
  "typescript": "^5.4.5"
26
26
  }
27
- }
27
+ }
package/src/index.ts CHANGED
@@ -325,8 +325,61 @@ function createClient(options: Record<string, string>): PolicyClient {
325
325
  chainId: 56,
326
326
  });
327
327
  }
328
-
329
328
  // ── Program ─────────────────────────────────────────────
329
+
330
+ // Pre-check: prevents write operations on expired/unauthorized agents with clear error
331
+ const AGENT_NFA_ACCESS_ABI = [
332
+ { name: "operatorExpiresOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
333
+ { name: "userExpires", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
334
+ { name: "operatorOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
335
+ { name: "userOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
336
+ { name: "ownerOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
337
+ ] as const;
338
+
339
+ async function checkAccess(opts: Record<string, string>, tokenId: bigint) {
340
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
341
+ const nfa = (opts.nfaAddress || DEFAULT_NFA) as Address;
342
+ const pk = toHex(process.env.RUNNER_PRIVATE_KEY || "");
343
+ const account = privateKeyToAccount(pk);
344
+ const pc = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
345
+ const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
346
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
347
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userExpires", args: [tokenId] }) as Promise<bigint>,
348
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorOf", args: [tokenId] }) as Promise<Address>,
349
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userOf", args: [tokenId] }) as Promise<Address>,
350
+ pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "ownerOf", args: [tokenId] }) as Promise<Address>,
351
+ ]);
352
+ const now = BigInt(Math.floor(Date.now() / 1000));
353
+ if (now > userExpires) {
354
+ output({ status: "error", message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1000).toISOString()}). Please renew at https://shll.run/me` });
355
+ process.exit(1);
356
+ }
357
+ if (now > operatorExpires) {
358
+ output({ status: "error", message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1000).toISOString()}). Please re-authorize via setup_guide.` });
359
+ process.exit(1);
360
+ }
361
+ const runnerAddr = account.address.toLowerCase();
362
+ const isOperator = operator.toLowerCase() === runnerAddr;
363
+ const isRenter = renter.toLowerCase() === runnerAddr;
364
+ const isOwner = owner.toLowerCase() === runnerAddr;
365
+ if (!isOperator && !isRenter && !isOwner) {
366
+ output({
367
+ status: "error",
368
+ message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}.`,
369
+ yourWallet: account.address,
370
+ onChainOperator: operator,
371
+ onChainRenter: renter,
372
+ onChainOwner: owner,
373
+ howToFix: [
374
+ `1. Use 'setup_guide' command to generate an OperatorPermit for this wallet`,
375
+ `2. Renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry>) on AgentNFA`,
376
+ `3. Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety to set operator`,
377
+ `4. Use the correct RUNNER_PRIVATE_KEY for operator ${operator}`,
378
+ ],
379
+ });
380
+ process.exit(1);
381
+ }
382
+ }
330
383
  const program = new Command();
331
384
  program.name("shll-onchain-runner").description("Execute DeFi actions securely via SHLL AgentNFA");
332
385
 
@@ -346,6 +399,7 @@ swapCmd.action(async (opts) => {
346
399
  try {
347
400
  const client = createClient(opts);
348
401
  const tokenId = BigInt(opts.tokenId);
402
+ await checkAccess(opts, tokenId);
349
403
  const rpcUrl = opts.rpc || DEFAULT_RPC;
350
404
 
351
405
  const fromToken = resolveToken(opts.from);
@@ -1067,6 +1121,7 @@ wrapCmd.action(async (opts) => {
1067
1121
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
1068
1122
  process.exit(1);
1069
1123
  }
1124
+ await checkAccess(opts, BigInt(opts.tokenId));
1070
1125
  const client = new PolicyClient({
1071
1126
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
1072
1127
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -1117,6 +1172,7 @@ unwrapCmd.action(async (opts) => {
1117
1172
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
1118
1173
  process.exit(1);
1119
1174
  }
1175
+ await checkAccess(opts, BigInt(opts.tokenId));
1120
1176
  const client = new PolicyClient({
1121
1177
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
1122
1178
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -1170,6 +1226,7 @@ transferCmd.action(async (opts) => {
1170
1226
  output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
1171
1227
  process.exit(1);
1172
1228
  }
1229
+ await checkAccess(opts, BigInt(opts.tokenId));
1173
1230
  const client = new PolicyClient({
1174
1231
  operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
1175
1232
  rpcUrl: opts.rpc || DEFAULT_RPC,
@@ -1909,6 +1966,7 @@ lendCmd.action(async (opts) => {
1909
1966
  try {
1910
1967
  const client = createClient(opts);
1911
1968
  const tokenId = BigInt(opts.tokenId);
1969
+ await checkAccess(opts, tokenId);
1912
1970
  const rpcUrl = opts.rpc || DEFAULT_RPC;
1913
1971
  const publicClient = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
1914
1972
 
@@ -1997,6 +2055,7 @@ redeemCmd.action(async (opts) => {
1997
2055
  try {
1998
2056
  const client = createClient(opts);
1999
2057
  const tokenId = BigInt(opts.tokenId);
2058
+ await checkAccess(opts, tokenId);
2000
2059
 
2001
2060
  const symbol = opts.token.toUpperCase();
2002
2061
  const vTokenAddr = VENUS_VTOKENS[symbol];
package/src/mcp.ts CHANGED
@@ -196,18 +196,25 @@ function createClients() {
196
196
  return { account, publicClient, policyClient, config };
197
197
  }
198
198
 
199
- // Expiry pre-check: prevents write operations on expired agents with clear error
200
- const AGENT_NFA_EXPIRY_ABI = [
199
+ // Pre-check: prevents write operations on expired/unauthorized agents with clear errors
200
+ const AGENT_NFA_CHECK_ABI = [
201
201
  { name: "operatorExpiresOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
202
202
  { name: "userExpires", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
203
+ { name: "operatorOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
204
+ { name: "userOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
205
+ { name: "ownerOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
203
206
  ] as const;
204
207
 
205
208
  async function checkAgentExpiry(tokenId: bigint) {
206
209
  const config = getConfig();
210
+ const account = privateKeyToAccount(config.privateKey as Hex);
207
211
  const pc = createPublicClient({ chain: bsc, transport: http(config.rpc) });
208
- const [operatorExpires, userExpires] = await Promise.all([
209
- pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
210
- pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] }) as Promise<bigint>,
212
+ const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
213
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
214
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "userExpires", args: [tokenId] }) as Promise<bigint>,
215
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorOf", args: [tokenId] }) as Promise<Address>,
216
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "userOf", args: [tokenId] }) as Promise<Address>,
217
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "ownerOf", args: [tokenId] }) as Promise<Address>,
211
218
  ]);
212
219
  const now = BigInt(Math.floor(Date.now() / 1000));
213
220
  if (now > operatorExpires) {
@@ -236,6 +243,32 @@ async function checkAgentExpiry(tokenId: bigint) {
236
243
  }],
237
244
  };
238
245
  }
246
+ // Operator identity check: verify RUNNER_PRIVATE_KEY wallet can execute
247
+ const runnerAddr = account.address.toLowerCase();
248
+ const isOperator = operator.toLowerCase() === runnerAddr;
249
+ const isRenter = renter.toLowerCase() === runnerAddr;
250
+ const isOwner = owner.toLowerCase() === runnerAddr;
251
+ if (!isOperator && !isRenter && !isOwner) {
252
+ return {
253
+ expired: true, // reuse expired flag to block execution
254
+ content: [{
255
+ type: "text" as const, text: JSON.stringify({
256
+ status: "error",
257
+ message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}. Your wallet must be the operator, renter, or owner to execute transactions.`,
258
+ yourWallet: account.address,
259
+ onChainOperator: operator,
260
+ onChainRenter: renter,
261
+ onChainOwner: owner,
262
+ howToFix: [
263
+ `Option 1: Use the 'setup_guide' tool — it generates an EIP-712 OperatorPermit that lets the renter (${renter}) authorize your current wallet (${account.address}) as operator. The renter signs the permit in their browser wallet, then the runner submits it on-chain.`,
264
+ `Option 2: The renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry_timestamp>) on AgentNFA contract at ${config.nfa} to directly authorize this wallet.`,
265
+ `Option 3: Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety and set ${account.address} as the operator.`,
266
+ `Option 4: If you have access to the correct operator wallet (${operator}), set RUNNER_PRIVATE_KEY to that wallet's private key instead.`,
267
+ ],
268
+ })
269
+ }],
270
+ };
271
+ }
239
272
  return { expired: false };
240
273
  }
241
274