shll-skills 5.1.0 → 5.2.0

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
@@ -270,6 +270,19 @@ Step 2: Call SHLL execute_calldata(token_id, target, data, value) → PolicyGuar
270
270
 
271
271
  For multi-step transactions (e.g. approve + swap), use `execute_calldata_batch` to execute atomically.
272
272
 
273
+ ⚠️ **CRITICAL SECURITY: Verify Recipient Address**
274
+
275
+ Before executing calldata from an external source, you **MUST verify** that any `recipient`, `to`, or `receiver` address embedded in the calldata matches the agent's vault address. Use the `portfolio` tool to get the vault address first.
276
+
277
+ **Why:** A compromised or malicious API could return valid-looking swap calldata but with the recipient set to an attacker's address. PolicyGuard validates the target contract and spending limits, but does NOT parse internal calldata fields like `recipient`.
278
+
279
+ ```
280
+ Step 0: portfolio(token_id) → get vault address
281
+ Step 1: Get calldata from OKX/Bitget/1inch
282
+ Step 2: Verify that 'recipient' in calldata == vault address
283
+ Step 3: execute_calldata(token_id, target, data, value)
284
+ ```
285
+
273
286
  ---
274
287
 
275
288
  ## LINKS
package/dist/mcp.js CHANGED
@@ -641,9 +641,51 @@ function createClients() {
641
641
  });
642
642
  return { account, publicClient, policyClient, config };
643
643
  }
644
+ var AGENT_NFA_EXPIRY_ABI = [
645
+ { name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
646
+ { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] }
647
+ ];
648
+ async function checkAgentExpiry(tokenId) {
649
+ const config = getConfig();
650
+ const pc = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(config.rpc) });
651
+ const [operatorExpires, userExpires] = await Promise.all([
652
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
653
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] })
654
+ ]);
655
+ const now = BigInt(Math.floor(Date.now() / 1e3));
656
+ if (now > operatorExpires) {
657
+ return {
658
+ expired: true,
659
+ content: [{
660
+ type: "text",
661
+ text: JSON.stringify({
662
+ status: "error",
663
+ message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
664
+ expiredAt: new Date(Number(operatorExpires) * 1e3).toISOString(),
665
+ action: "renew"
666
+ })
667
+ }]
668
+ };
669
+ }
670
+ if (now > userExpires) {
671
+ return {
672
+ expired: true,
673
+ content: [{
674
+ type: "text",
675
+ text: JSON.stringify({
676
+ status: "error",
677
+ message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
678
+ expiredAt: new Date(Number(userExpires) * 1e3).toISOString(),
679
+ action: "renew"
680
+ })
681
+ }]
682
+ };
683
+ }
684
+ return { expired: false };
685
+ }
644
686
  var server = new import_mcp.McpServer({
645
687
  name: "shll-defi",
646
- version: "5.0.0"
688
+ version: "5.2.0"
647
689
  });
648
690
  server.tool(
649
691
  "portfolio",
@@ -741,6 +783,8 @@ server.tool(
741
783
  async ({ token_id, from, to, amount, dex, slippage }) => {
742
784
  const { publicClient, policyClient } = createClients();
743
785
  const tokenId = BigInt(token_id);
786
+ const expiryCheck = await checkAgentExpiry(tokenId);
787
+ if (expiryCheck.expired) return { content: expiryCheck.content };
744
788
  const vault = await policyClient.getVault(tokenId);
745
789
  const fromToken = resolveToken(from);
746
790
  const toToken = resolveToken(to);
@@ -834,6 +878,8 @@ server.tool(
834
878
  async ({ token_id, token, amount }) => {
835
879
  const { publicClient, policyClient } = createClients();
836
880
  const tokenId = BigInt(token_id);
881
+ const expiryCheck = await checkAgentExpiry(tokenId);
882
+ if (expiryCheck.expired) return { content: expiryCheck.content };
837
883
  const symbol = token.toUpperCase();
838
884
  const vTokenAddr = VENUS_VTOKENS[symbol];
839
885
  if (!vTokenAddr) return { content: [{ type: "text", text: JSON.stringify({ error: `Unsupported: ${symbol}. Use: ${Object.keys(VENUS_VTOKENS).join(", ")}` }) }] };
@@ -870,6 +916,8 @@ server.tool(
870
916
  async ({ token_id, token, amount }) => {
871
917
  const { policyClient } = createClients();
872
918
  const tokenId = BigInt(token_id);
919
+ const expiryCheck = await checkAgentExpiry(tokenId);
920
+ if (expiryCheck.expired) return { content: expiryCheck.content };
873
921
  const symbol = token.toUpperCase();
874
922
  const vTokenAddr = VENUS_VTOKENS[symbol];
875
923
  if (!vTokenAddr) return { content: [{ type: "text", text: JSON.stringify({ error: `Unsupported: ${symbol}` }) }] };
@@ -926,6 +974,8 @@ server.tool(
926
974
  async ({ token_id, token, amount, to }) => {
927
975
  const { policyClient } = createClients();
928
976
  const tokenId = BigInt(token_id);
977
+ const expiryCheck = await checkAgentExpiry(tokenId);
978
+ if (expiryCheck.expired) return { content: expiryCheck.content };
929
979
  const tokenInfo = resolveToken(token);
930
980
  const amt = parseAmount(amount, tokenInfo.decimals);
931
981
  const recipient = to;
@@ -1009,6 +1059,8 @@ server.tool(
1009
1059
  async ({ token_id, amount }) => {
1010
1060
  const { policyClient } = createClients();
1011
1061
  const tokenId = BigInt(token_id);
1062
+ const expiryCheck = await checkAgentExpiry(tokenId);
1063
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1012
1064
  const amt = (0, import_viem2.parseEther)(amount);
1013
1065
  const data = (0, import_viem2.encodeFunctionData)({ abi: WBNB_ABI, functionName: "deposit" });
1014
1066
  const action = { target: WBNB, value: amt, data };
@@ -1028,6 +1080,8 @@ server.tool(
1028
1080
  async ({ token_id, amount }) => {
1029
1081
  const { policyClient } = createClients();
1030
1082
  const tokenId = BigInt(token_id);
1083
+ const expiryCheck = await checkAgentExpiry(tokenId);
1084
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1031
1085
  const amt = (0, import_viem2.parseEther)(amount);
1032
1086
  const data = (0, import_viem2.encodeFunctionData)({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
1033
1087
  const action = { target: WBNB, value: 0n, data };
@@ -1224,6 +1278,8 @@ server.tool(
1224
1278
  }
1225
1279
  const { account, publicClient, policyClient, config } = createClients();
1226
1280
  const tokenId = BigInt(token_id);
1281
+ const expiryCheck = await checkAgentExpiry(tokenId);
1282
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1227
1283
  const walletClient = (0, import_viem2.createWalletClient)({ account, chain: import_chains2.bsc, transport: (0, import_viem2.http)(config.rpc) });
1228
1284
  const policies = await policyClient.getPolicies(tokenId);
1229
1285
  const results = [];
@@ -1316,16 +1372,18 @@ server.tool(
1316
1372
  );
1317
1373
  server.tool(
1318
1374
  "execute_calldata",
1319
- "Execute raw calldata through PolicyGuard safety layer. Use this to execute transactions from other DeFi skills (OKX DEX API, Bitget, 1inch, etc.) with SHLL on-chain policy enforcement. All actions are validated against spending limits, cooldowns, and whitelists before execution.",
1375
+ "Execute raw calldata through PolicyGuard safety layer. Use this to execute transactions from other DeFi skills (OKX DEX API, Bitget, 1inch, etc.) with SHLL on-chain policy enforcement. IMPORTANT: Before calling, verify that any 'recipient' or 'to' address embedded in the calldata matches the agent's vault address (use the 'portfolio' tool to check). This prevents funds from being routed to an unintended address.",
1320
1376
  {
1321
1377
  token_id: import_zod.z.string().describe("Agent NFA Token ID"),
1322
- target: import_zod.z.string().describe("Target contract address (0x...)"),
1323
- data: import_zod.z.string().describe("Transaction calldata hex string"),
1378
+ target: import_zod.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Must be a valid Ethereum address").describe("Target contract address (0x...)"),
1379
+ data: import_zod.z.string().regex(/^0x[0-9a-fA-F]*$/, "Must be a valid hex string starting with 0x").describe("Transaction calldata hex string"),
1324
1380
  value: import_zod.z.string().default("0").describe("Native BNB value in wei (default: 0)")
1325
1381
  },
1326
1382
  async ({ token_id, target, data, value }) => {
1327
1383
  const { policyClient } = createClients();
1328
1384
  const tokenId = BigInt(token_id);
1385
+ const expiryCheck = await checkAgentExpiry(tokenId);
1386
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1329
1387
  const action = {
1330
1388
  target,
1331
1389
  value: BigInt(value),
@@ -1363,14 +1421,16 @@ server.tool(
1363
1421
  {
1364
1422
  token_id: import_zod.z.string().describe("Agent NFA Token ID"),
1365
1423
  actions: import_zod.z.array(import_zod.z.object({
1366
- target: import_zod.z.string().describe("Target contract address"),
1367
- data: import_zod.z.string().describe("Calldata hex"),
1424
+ target: import_zod.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Must be a valid Ethereum address").describe("Target contract address"),
1425
+ data: import_zod.z.string().regex(/^0x[0-9a-fA-F]*$/, "Must be valid hex").describe("Calldata hex"),
1368
1426
  value: import_zod.z.string().default("0").describe("BNB value in wei")
1369
1427
  })).describe("Array of actions to execute atomically")
1370
1428
  },
1371
1429
  async ({ token_id, actions: rawActions }) => {
1372
1430
  const { policyClient } = createClients();
1373
1431
  const tokenId = BigInt(token_id);
1432
+ const expiryCheck = await checkAgentExpiry(tokenId);
1433
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1374
1434
  const actions = rawActions.map((a) => ({
1375
1435
  target: a.target,
1376
1436
  value: BigInt(a.value || "0"),
package/dist/mcp.mjs CHANGED
@@ -152,9 +152,51 @@ function createClients() {
152
152
  });
153
153
  return { account, publicClient, policyClient, config };
154
154
  }
155
+ var AGENT_NFA_EXPIRY_ABI = [
156
+ { name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
157
+ { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] }
158
+ ];
159
+ async function checkAgentExpiry(tokenId) {
160
+ const config = getConfig();
161
+ const pc = createPublicClient({ chain: bsc, transport: http(config.rpc) });
162
+ const [operatorExpires, userExpires] = await Promise.all([
163
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
164
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] })
165
+ ]);
166
+ const now = BigInt(Math.floor(Date.now() / 1e3));
167
+ if (now > operatorExpires) {
168
+ return {
169
+ expired: true,
170
+ content: [{
171
+ type: "text",
172
+ text: JSON.stringify({
173
+ status: "error",
174
+ message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
175
+ expiredAt: new Date(Number(operatorExpires) * 1e3).toISOString(),
176
+ action: "renew"
177
+ })
178
+ }]
179
+ };
180
+ }
181
+ if (now > userExpires) {
182
+ return {
183
+ expired: true,
184
+ content: [{
185
+ type: "text",
186
+ text: JSON.stringify({
187
+ status: "error",
188
+ message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
189
+ expiredAt: new Date(Number(userExpires) * 1e3).toISOString(),
190
+ action: "renew"
191
+ })
192
+ }]
193
+ };
194
+ }
195
+ return { expired: false };
196
+ }
155
197
  var server = new McpServer({
156
198
  name: "shll-defi",
157
- version: "5.0.0"
199
+ version: "5.2.0"
158
200
  });
159
201
  server.tool(
160
202
  "portfolio",
@@ -252,6 +294,8 @@ server.tool(
252
294
  async ({ token_id, from, to, amount, dex, slippage }) => {
253
295
  const { publicClient, policyClient } = createClients();
254
296
  const tokenId = BigInt(token_id);
297
+ const expiryCheck = await checkAgentExpiry(tokenId);
298
+ if (expiryCheck.expired) return { content: expiryCheck.content };
255
299
  const vault = await policyClient.getVault(tokenId);
256
300
  const fromToken = resolveToken(from);
257
301
  const toToken = resolveToken(to);
@@ -345,6 +389,8 @@ server.tool(
345
389
  async ({ token_id, token, amount }) => {
346
390
  const { publicClient, policyClient } = createClients();
347
391
  const tokenId = BigInt(token_id);
392
+ const expiryCheck = await checkAgentExpiry(tokenId);
393
+ if (expiryCheck.expired) return { content: expiryCheck.content };
348
394
  const symbol = token.toUpperCase();
349
395
  const vTokenAddr = VENUS_VTOKENS[symbol];
350
396
  if (!vTokenAddr) return { content: [{ type: "text", text: JSON.stringify({ error: `Unsupported: ${symbol}. Use: ${Object.keys(VENUS_VTOKENS).join(", ")}` }) }] };
@@ -381,6 +427,8 @@ server.tool(
381
427
  async ({ token_id, token, amount }) => {
382
428
  const { policyClient } = createClients();
383
429
  const tokenId = BigInt(token_id);
430
+ const expiryCheck = await checkAgentExpiry(tokenId);
431
+ if (expiryCheck.expired) return { content: expiryCheck.content };
384
432
  const symbol = token.toUpperCase();
385
433
  const vTokenAddr = VENUS_VTOKENS[symbol];
386
434
  if (!vTokenAddr) return { content: [{ type: "text", text: JSON.stringify({ error: `Unsupported: ${symbol}` }) }] };
@@ -437,6 +485,8 @@ server.tool(
437
485
  async ({ token_id, token, amount, to }) => {
438
486
  const { policyClient } = createClients();
439
487
  const tokenId = BigInt(token_id);
488
+ const expiryCheck = await checkAgentExpiry(tokenId);
489
+ if (expiryCheck.expired) return { content: expiryCheck.content };
440
490
  const tokenInfo = resolveToken(token);
441
491
  const amt = parseAmount(amount, tokenInfo.decimals);
442
492
  const recipient = to;
@@ -520,6 +570,8 @@ server.tool(
520
570
  async ({ token_id, amount }) => {
521
571
  const { policyClient } = createClients();
522
572
  const tokenId = BigInt(token_id);
573
+ const expiryCheck = await checkAgentExpiry(tokenId);
574
+ if (expiryCheck.expired) return { content: expiryCheck.content };
523
575
  const amt = parseEther(amount);
524
576
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "deposit" });
525
577
  const action = { target: WBNB, value: amt, data };
@@ -539,6 +591,8 @@ server.tool(
539
591
  async ({ token_id, amount }) => {
540
592
  const { policyClient } = createClients();
541
593
  const tokenId = BigInt(token_id);
594
+ const expiryCheck = await checkAgentExpiry(tokenId);
595
+ if (expiryCheck.expired) return { content: expiryCheck.content };
542
596
  const amt = parseEther(amount);
543
597
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
544
598
  const action = { target: WBNB, value: 0n, data };
@@ -735,6 +789,8 @@ server.tool(
735
789
  }
736
790
  const { account, publicClient, policyClient, config } = createClients();
737
791
  const tokenId = BigInt(token_id);
792
+ const expiryCheck = await checkAgentExpiry(tokenId);
793
+ if (expiryCheck.expired) return { content: expiryCheck.content };
738
794
  const walletClient = createWalletClient({ account, chain: bsc, transport: http(config.rpc) });
739
795
  const policies = await policyClient.getPolicies(tokenId);
740
796
  const results = [];
@@ -827,16 +883,18 @@ server.tool(
827
883
  );
828
884
  server.tool(
829
885
  "execute_calldata",
830
- "Execute raw calldata through PolicyGuard safety layer. Use this to execute transactions from other DeFi skills (OKX DEX API, Bitget, 1inch, etc.) with SHLL on-chain policy enforcement. All actions are validated against spending limits, cooldowns, and whitelists before execution.",
886
+ "Execute raw calldata through PolicyGuard safety layer. Use this to execute transactions from other DeFi skills (OKX DEX API, Bitget, 1inch, etc.) with SHLL on-chain policy enforcement. IMPORTANT: Before calling, verify that any 'recipient' or 'to' address embedded in the calldata matches the agent's vault address (use the 'portfolio' tool to check). This prevents funds from being routed to an unintended address.",
831
887
  {
832
888
  token_id: z.string().describe("Agent NFA Token ID"),
833
- target: z.string().describe("Target contract address (0x...)"),
834
- data: z.string().describe("Transaction calldata hex string"),
889
+ target: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Must be a valid Ethereum address").describe("Target contract address (0x...)"),
890
+ data: z.string().regex(/^0x[0-9a-fA-F]*$/, "Must be a valid hex string starting with 0x").describe("Transaction calldata hex string"),
835
891
  value: z.string().default("0").describe("Native BNB value in wei (default: 0)")
836
892
  },
837
893
  async ({ token_id, target, data, value }) => {
838
894
  const { policyClient } = createClients();
839
895
  const tokenId = BigInt(token_id);
896
+ const expiryCheck = await checkAgentExpiry(tokenId);
897
+ if (expiryCheck.expired) return { content: expiryCheck.content };
840
898
  const action = {
841
899
  target,
842
900
  value: BigInt(value),
@@ -874,14 +932,16 @@ server.tool(
874
932
  {
875
933
  token_id: z.string().describe("Agent NFA Token ID"),
876
934
  actions: z.array(z.object({
877
- target: z.string().describe("Target contract address"),
878
- data: z.string().describe("Calldata hex"),
935
+ target: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Must be a valid Ethereum address").describe("Target contract address"),
936
+ data: z.string().regex(/^0x[0-9a-fA-F]*$/, "Must be valid hex").describe("Calldata hex"),
879
937
  value: z.string().default("0").describe("BNB value in wei")
880
938
  })).describe("Array of actions to execute atomically")
881
939
  },
882
940
  async ({ token_id, actions: rawActions }) => {
883
941
  const { policyClient } = createClients();
884
942
  const tokenId = BigInt(token_id);
943
+ const expiryCheck = await checkAgentExpiry(tokenId);
944
+ if (expiryCheck.expired) return { content: expiryCheck.content };
885
945
  const actions = rawActions.map((a) => ({
886
946
  target: a.target,
887
947
  value: BigInt(a.value || "0"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shll-skills",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "SHLL DeFi Agent — CLI + MCP Server for BSC",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/mcp.ts CHANGED
@@ -191,13 +191,56 @@ function createClients() {
191
191
  return { account, publicClient, policyClient, config };
192
192
  }
193
193
 
194
+ // Expiry pre-check: prevents write operations on expired agents with clear error
195
+ const AGENT_NFA_EXPIRY_ABI = [
196
+ { name: "operatorExpiresOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
197
+ { name: "userExpires", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
198
+ ] as const;
199
+
200
+ async function checkAgentExpiry(tokenId: bigint) {
201
+ const config = getConfig();
202
+ const pc = createPublicClient({ chain: bsc, transport: http(config.rpc) });
203
+ const [operatorExpires, userExpires] = await Promise.all([
204
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
205
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] }) as Promise<bigint>,
206
+ ]);
207
+ const now = BigInt(Math.floor(Date.now() / 1000));
208
+ if (now > operatorExpires) {
209
+ return {
210
+ expired: true,
211
+ content: [{
212
+ type: "text" as const, text: JSON.stringify({
213
+ status: "error",
214
+ message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1000).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
215
+ expiredAt: new Date(Number(operatorExpires) * 1000).toISOString(),
216
+ action: "renew",
217
+ })
218
+ }],
219
+ };
220
+ }
221
+ if (now > userExpires) {
222
+ return {
223
+ expired: true,
224
+ content: [{
225
+ type: "text" as const, text: JSON.stringify({
226
+ status: "error",
227
+ message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1000).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
228
+ expiredAt: new Date(Number(userExpires) * 1000).toISOString(),
229
+ action: "renew",
230
+ })
231
+ }],
232
+ };
233
+ }
234
+ return { expired: false };
235
+ }
236
+
194
237
  // ═══════════════════════════════════════════════════════
195
238
  // MCP Server
196
239
  // ═══════════════════════════════════════════════════════
197
240
 
198
241
  const server = new McpServer({
199
242
  name: "shll-defi",
200
- version: "5.0.0",
243
+ version: "5.2.0",
201
244
  });
202
245
 
203
246
  // ── Tool: portfolio ─────────────────────────────────────
@@ -306,6 +349,7 @@ server.tool(
306
349
  async ({ token_id, from, to, amount, dex, slippage }) => {
307
350
  const { publicClient, policyClient } = createClients();
308
351
  const tokenId = BigInt(token_id);
352
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
309
353
  const vault = await policyClient.getVault(tokenId);
310
354
 
311
355
  const fromToken = resolveToken(from);
@@ -410,6 +454,7 @@ server.tool(
410
454
  async ({ token_id, token, amount }) => {
411
455
  const { publicClient, policyClient } = createClients();
412
456
  const tokenId = BigInt(token_id);
457
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
413
458
  const symbol = token.toUpperCase();
414
459
  const vTokenAddr = VENUS_VTOKENS[symbol];
415
460
  if (!vTokenAddr) return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Unsupported: ${symbol}. Use: ${Object.keys(VENUS_VTOKENS).join(", ")}` }) }] };
@@ -455,6 +500,7 @@ server.tool(
455
500
  async ({ token_id, token, amount }) => {
456
501
  const { policyClient } = createClients();
457
502
  const tokenId = BigInt(token_id);
503
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
458
504
  const symbol = token.toUpperCase();
459
505
  const vTokenAddr = VENUS_VTOKENS[symbol];
460
506
  if (!vTokenAddr) return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Unsupported: ${symbol}` }) }] };
@@ -519,6 +565,7 @@ server.tool(
519
565
  async ({ token_id, token, amount, to }) => {
520
566
  const { policyClient } = createClients();
521
567
  const tokenId = BigInt(token_id);
568
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
522
569
  const tokenInfo = resolveToken(token);
523
570
  const amt = parseAmount(amount, tokenInfo.decimals);
524
571
  const recipient = to as Address;
@@ -616,6 +663,7 @@ server.tool(
616
663
  async ({ token_id, amount }) => {
617
664
  const { policyClient } = createClients();
618
665
  const tokenId = BigInt(token_id);
666
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
619
667
  const amt = parseEther(amount);
620
668
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "deposit" });
621
669
  const action: Action = { target: WBNB as Address, value: amt, data };
@@ -639,6 +687,7 @@ server.tool(
639
687
  async ({ token_id, amount }) => {
640
688
  const { policyClient } = createClients();
641
689
  const tokenId = BigInt(token_id);
690
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
642
691
  const amt = parseEther(amount);
643
692
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
644
693
  const action: Action = { target: WBNB as Address, value: 0n, data };
@@ -862,6 +911,7 @@ server.tool(
862
911
 
863
912
  const { account, publicClient, policyClient, config } = createClients();
864
913
  const tokenId = BigInt(token_id);
914
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
865
915
  const walletClient = createWalletClient({ account, chain: bsc, transport: http(config.rpc) });
866
916
  const policies = await policyClient.getPolicies(tokenId);
867
917
  const results: string[] = [];
@@ -973,16 +1023,17 @@ server.tool(
973
1023
  // (OKX DEX API, Bitget, 1inch, etc.) and routes through PolicyGuard
974
1024
  server.tool(
975
1025
  "execute_calldata",
976
- "Execute raw calldata through PolicyGuard safety layer. Use this to execute transactions from other DeFi skills (OKX DEX API, Bitget, 1inch, etc.) with SHLL on-chain policy enforcement. All actions are validated against spending limits, cooldowns, and whitelists before execution.",
1026
+ "Execute raw calldata through PolicyGuard safety layer. Use this to execute transactions from other DeFi skills (OKX DEX API, Bitget, 1inch, etc.) with SHLL on-chain policy enforcement. IMPORTANT: Before calling, verify that any 'recipient' or 'to' address embedded in the calldata matches the agent's vault address (use the 'portfolio' tool to check). This prevents funds from being routed to an unintended address.",
977
1027
  {
978
1028
  token_id: z.string().describe("Agent NFA Token ID"),
979
- target: z.string().describe("Target contract address (0x...)"),
980
- data: z.string().describe("Transaction calldata hex string"),
1029
+ target: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Must be a valid Ethereum address").describe("Target contract address (0x...)"),
1030
+ data: z.string().regex(/^0x[0-9a-fA-F]*$/, "Must be a valid hex string starting with 0x").describe("Transaction calldata hex string"),
981
1031
  value: z.string().default("0").describe("Native BNB value in wei (default: 0)"),
982
1032
  },
983
1033
  async ({ token_id, target, data, value }) => {
984
1034
  const { policyClient } = createClients();
985
1035
  const tokenId = BigInt(token_id);
1036
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
986
1037
  const action: Action = {
987
1038
  target: target as Address,
988
1039
  value: BigInt(value),
@@ -1023,14 +1074,15 @@ server.tool(
1023
1074
  {
1024
1075
  token_id: z.string().describe("Agent NFA Token ID"),
1025
1076
  actions: z.array(z.object({
1026
- target: z.string().describe("Target contract address"),
1027
- data: z.string().describe("Calldata hex"),
1077
+ target: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Must be a valid Ethereum address").describe("Target contract address"),
1078
+ data: z.string().regex(/^0x[0-9a-fA-F]*$/, "Must be valid hex").describe("Calldata hex"),
1028
1079
  value: z.string().default("0").describe("BNB value in wei"),
1029
1080
  })).describe("Array of actions to execute atomically"),
1030
1081
  },
1031
1082
  async ({ token_id, actions: rawActions }) => {
1032
1083
  const { policyClient } = createClients();
1033
1084
  const tokenId = BigInt(token_id);
1085
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
1034
1086
  const actions: Action[] = rawActions.map(a => ({
1035
1087
  target: a.target as Address,
1036
1088
  value: BigInt(a.value || "0"),