@t2000/engine 0.46.11 → 0.46.13
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/dist/index.d.ts +49 -0
- package/dist/index.js +132 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -228,6 +228,25 @@ interface GuardConfig {
|
|
|
228
228
|
* upstream guard (e.g. an off-process verifier).
|
|
229
229
|
*/
|
|
230
230
|
addressSource?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Companion to `addressSource`: blocks send_transfer that defaults to
|
|
233
|
+
* USDC when the user's recent messages clearly named a non-USDC token
|
|
234
|
+
* (SUI, USDT, WAL, etc.). Without this, the LLM would call
|
|
235
|
+
* `send_transfer({ amount, to })` for a "send my SUI" request and the
|
|
236
|
+
* tool would silently ship USDC. Default on.
|
|
237
|
+
*/
|
|
238
|
+
assetIntent?: boolean;
|
|
239
|
+
/**
|
|
240
|
+
* Root-cause fix for "LLM hallucinates a stale training-data price
|
|
241
|
+
* (e.g. '$3.50/SUI') and shows the user a wildly wrong estimate before
|
|
242
|
+
* the swap card renders". When enabled (default), `swap_execute` is
|
|
243
|
+
* blocked unless a matching `swap_quote(from, to, amount)` ran in the
|
|
244
|
+
* recent past (60s window, ±1% amount tolerance). The block forces the
|
|
245
|
+
* LLM to fetch a real on-chain quote and cite its actual numbers, not
|
|
246
|
+
* a guess. Set to `false` only if the host has its own pre-execution
|
|
247
|
+
* quote requirement.
|
|
248
|
+
*/
|
|
249
|
+
swapPreview?: boolean;
|
|
231
250
|
}
|
|
232
251
|
declare const DEFAULT_GUARD_CONFIG: GuardConfig;
|
|
233
252
|
declare class BalanceTracker {
|
|
@@ -247,11 +266,39 @@ declare class RetryTracker {
|
|
|
247
266
|
previousResult?: unknown;
|
|
248
267
|
};
|
|
249
268
|
}
|
|
269
|
+
declare class SwapQuoteTracker {
|
|
270
|
+
/** Quotes recorded in the recent window. Trimmed lazily on every check. */
|
|
271
|
+
private quotes;
|
|
272
|
+
/** Match window: 60s is generous enough for slow LLM turns but tight enough
|
|
273
|
+
* to invalidate stale quotes from earlier in the session. */
|
|
274
|
+
private readonly windowMs;
|
|
275
|
+
/** Amount tolerance: ±1% (covers gas-padding, integer-rounding, and the
|
|
276
|
+
* rare case where the LLM rounds the input differently between quote and
|
|
277
|
+
* execute). Prices barely move in 60s so 1% is forgiving but meaningful. */
|
|
278
|
+
private readonly amountTolerance;
|
|
279
|
+
/**
|
|
280
|
+
* Normalize a token identifier so symbol vs. coinType vs. case don't
|
|
281
|
+
* cause spurious mismatches. Lowercase + trim is sufficient because the
|
|
282
|
+
* SDK's resolver itself is case-insensitive on symbols.
|
|
283
|
+
*/
|
|
284
|
+
private normalize;
|
|
285
|
+
record(input: {
|
|
286
|
+
from: string;
|
|
287
|
+
to: string;
|
|
288
|
+
amount: number;
|
|
289
|
+
}): void;
|
|
290
|
+
hasMatchingQuote(input: {
|
|
291
|
+
from: string;
|
|
292
|
+
to: string;
|
|
293
|
+
amount: number;
|
|
294
|
+
}): boolean;
|
|
295
|
+
}
|
|
250
296
|
declare function guardArtifactPreview(result: unknown): GuardInjection | null;
|
|
251
297
|
declare function guardStaleData(toolFlags: ToolFlags): GuardInjection | null;
|
|
252
298
|
interface GuardRunnerState {
|
|
253
299
|
balanceTracker: BalanceTracker;
|
|
254
300
|
retryTracker: RetryTracker;
|
|
301
|
+
swapQuoteTracker: SwapQuoteTracker;
|
|
255
302
|
lastHealthFactor: number | null;
|
|
256
303
|
}
|
|
257
304
|
declare function createGuardRunnerState(): GuardRunnerState;
|
|
@@ -1862,11 +1909,13 @@ declare const withdrawTool: Tool<{
|
|
|
1862
1909
|
declare const sendTransferTool: Tool<{
|
|
1863
1910
|
to: string;
|
|
1864
1911
|
amount: number;
|
|
1912
|
+
asset?: string | undefined;
|
|
1865
1913
|
memo?: string | undefined;
|
|
1866
1914
|
}, {
|
|
1867
1915
|
success: boolean;
|
|
1868
1916
|
tx: string;
|
|
1869
1917
|
amount: number;
|
|
1918
|
+
asset: "USDC" | "USDT" | "SUI" | "USDe" | "USDsui" | "WAL" | "ETH" | "NAVX" | "GOLD";
|
|
1870
1919
|
to: string;
|
|
1871
1920
|
contactName: string | undefined;
|
|
1872
1921
|
gasCost: number;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { resolveSymbol, getDecimalsForCoinType, assertAllowedAsset, getSwapQuote, extractTransferDetails, classifyTransaction } from '@t2000/sdk';
|
|
2
|
+
import { ALL_NAVI_ASSETS, resolveSymbol, getDecimalsForCoinType, assertAllowedAsset, SUPPORTED_ASSETS, getSwapQuote, extractTransferDetails, classifyTransaction } from '@t2000/sdk';
|
|
3
3
|
import { readdirSync, readFileSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
@@ -1447,12 +1447,14 @@ var withdrawTool = buildTool({
|
|
|
1447
1447
|
};
|
|
1448
1448
|
}
|
|
1449
1449
|
});
|
|
1450
|
+
var ASSET_LIST = ALL_NAVI_ASSETS.map((a) => String(a)).join(", ");
|
|
1450
1451
|
var sendTransferTool = buildTool({
|
|
1451
1452
|
name: "send_transfer",
|
|
1452
|
-
description:
|
|
1453
|
+
description: `Send ANY supported token (${ASSET_LIST}) to another Sui address or contact name. Validates the address, checks balance, and executes the on-chain transfer. MUST set the \`asset\` field to the token symbol you want to send (case-insensitive). If \`asset\` is omitted, USDC is assumed \u2014 only do this when the user explicitly asks for USDC. When the user asks to send a token by name (SUI, USDT, etc.) or to send the proceeds of a just-completed swap, you MUST pass \`asset\` matching that token. Returns tx hash, gas cost, and updated balance.`,
|
|
1453
1454
|
inputSchema: z.object({
|
|
1454
1455
|
to: z.string().min(1),
|
|
1455
1456
|
amount: z.number().positive(),
|
|
1457
|
+
asset: z.string().optional(),
|
|
1456
1458
|
memo: z.string().optional()
|
|
1457
1459
|
}),
|
|
1458
1460
|
jsonSchema: {
|
|
@@ -1464,7 +1466,11 @@ var sendTransferTool = buildTool({
|
|
|
1464
1466
|
},
|
|
1465
1467
|
amount: {
|
|
1466
1468
|
type: "number",
|
|
1467
|
-
description: "Amount in USD
|
|
1469
|
+
description: "Amount of the asset to send (denominated in the asset\u2019s own units, NOT USD). For USDC this is the USDC count; for SUI this is the SUI count."
|
|
1470
|
+
},
|
|
1471
|
+
asset: {
|
|
1472
|
+
type: "string",
|
|
1473
|
+
description: `Token symbol to send. One of: ${ASSET_LIST}. Defaults to USDC if omitted. REQUIRED whenever the user names a non-USDC token or you are forwarding the proceeds of a swap.`
|
|
1468
1474
|
},
|
|
1469
1475
|
memo: {
|
|
1470
1476
|
type: "string",
|
|
@@ -1487,16 +1493,27 @@ var sendTransferTool = buildTool({
|
|
|
1487
1493
|
if (input.amount <= 0) {
|
|
1488
1494
|
return { valid: false, error: "Amount must be positive." };
|
|
1489
1495
|
}
|
|
1496
|
+
if (input.asset !== void 0) {
|
|
1497
|
+
const normalized = String(input.asset).toUpperCase();
|
|
1498
|
+
if (!(normalized in SUPPORTED_ASSETS)) {
|
|
1499
|
+
return {
|
|
1500
|
+
valid: false,
|
|
1501
|
+
error: `Unsupported asset "${input.asset}". send_transfer accepts: ${ASSET_LIST}.`
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1490
1505
|
return { valid: true };
|
|
1491
1506
|
},
|
|
1492
1507
|
async call(input, context) {
|
|
1493
1508
|
const agent = requireAgent(context);
|
|
1494
|
-
const
|
|
1509
|
+
const asset = input.asset ? String(input.asset).toUpperCase() : "USDC";
|
|
1510
|
+
const result = await agent.send({ to: input.to, amount: input.amount, asset });
|
|
1495
1511
|
return {
|
|
1496
1512
|
data: {
|
|
1497
1513
|
success: result.success,
|
|
1498
1514
|
tx: result.tx,
|
|
1499
1515
|
amount: result.amount,
|
|
1516
|
+
asset,
|
|
1500
1517
|
to: result.to,
|
|
1501
1518
|
contactName: result.contactName,
|
|
1502
1519
|
gasCost: result.gasCost,
|
|
@@ -1504,7 +1521,7 @@ var sendTransferTool = buildTool({
|
|
|
1504
1521
|
balance: result.balance,
|
|
1505
1522
|
memo: input.memo ?? null
|
|
1506
1523
|
},
|
|
1507
|
-
displayText: `Sent
|
|
1524
|
+
displayText: `Sent ${result.amount} ${asset} to ${result.contactName ?? `${result.to.slice(0, 10)}\u2026`} (tx: ${result.tx.slice(0, 8)}\u2026)`
|
|
1508
1525
|
};
|
|
1509
1526
|
}
|
|
1510
1527
|
});
|
|
@@ -3610,7 +3627,9 @@ var DEFAULT_GUARD_CONFIG = {
|
|
|
3610
3627
|
costWarning: true,
|
|
3611
3628
|
retryProtection: true,
|
|
3612
3629
|
inputValidation: true,
|
|
3613
|
-
addressSource: true
|
|
3630
|
+
addressSource: true,
|
|
3631
|
+
assetIntent: true,
|
|
3632
|
+
swapPreview: true
|
|
3614
3633
|
};
|
|
3615
3634
|
var BalanceTracker = class {
|
|
3616
3635
|
lastBalanceAt = 0;
|
|
@@ -3651,6 +3670,45 @@ var RetryTracker = class {
|
|
|
3651
3670
|
return { blocked: true, previousResult: prev.result };
|
|
3652
3671
|
}
|
|
3653
3672
|
};
|
|
3673
|
+
var SwapQuoteTracker = class {
|
|
3674
|
+
/** Quotes recorded in the recent window. Trimmed lazily on every check. */
|
|
3675
|
+
quotes = [];
|
|
3676
|
+
/** Match window: 60s is generous enough for slow LLM turns but tight enough
|
|
3677
|
+
* to invalidate stale quotes from earlier in the session. */
|
|
3678
|
+
windowMs = 6e4;
|
|
3679
|
+
/** Amount tolerance: ±1% (covers gas-padding, integer-rounding, and the
|
|
3680
|
+
* rare case where the LLM rounds the input differently between quote and
|
|
3681
|
+
* execute). Prices barely move in 60s so 1% is forgiving but meaningful. */
|
|
3682
|
+
amountTolerance = 0.01;
|
|
3683
|
+
/**
|
|
3684
|
+
* Normalize a token identifier so symbol vs. coinType vs. case don't
|
|
3685
|
+
* cause spurious mismatches. Lowercase + trim is sufficient because the
|
|
3686
|
+
* SDK's resolver itself is case-insensitive on symbols.
|
|
3687
|
+
*/
|
|
3688
|
+
normalize(token) {
|
|
3689
|
+
return token.trim().toLowerCase();
|
|
3690
|
+
}
|
|
3691
|
+
record(input) {
|
|
3692
|
+
const now = Date.now();
|
|
3693
|
+
this.quotes.push({
|
|
3694
|
+
from: this.normalize(input.from),
|
|
3695
|
+
to: this.normalize(input.to),
|
|
3696
|
+
amount: input.amount,
|
|
3697
|
+
ts: now
|
|
3698
|
+
});
|
|
3699
|
+
const cutoff = now - this.windowMs;
|
|
3700
|
+
this.quotes = this.quotes.filter((q) => q.ts > cutoff);
|
|
3701
|
+
}
|
|
3702
|
+
hasMatchingQuote(input) {
|
|
3703
|
+
const cutoff = Date.now() - this.windowMs;
|
|
3704
|
+
const fromN = this.normalize(input.from);
|
|
3705
|
+
const toN = this.normalize(input.to);
|
|
3706
|
+
const target = input.amount;
|
|
3707
|
+
return this.quotes.some(
|
|
3708
|
+
(q) => q.ts > cutoff && q.from === fromN && q.to === toN && target > 0 && Math.abs(q.amount - target) / target <= this.amountTolerance
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
};
|
|
3654
3712
|
function guardRetryProtection(tool, call, retryTracker) {
|
|
3655
3713
|
const check = retryTracker.isBlocked(tool.name, call.input);
|
|
3656
3714
|
if (check.blocked) {
|
|
@@ -3793,6 +3851,58 @@ var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{64}$/;
|
|
|
3793
3851
|
function normalizeAddress(addr) {
|
|
3794
3852
|
return addr.trim().toLowerCase();
|
|
3795
3853
|
}
|
|
3854
|
+
var NON_USDC_TOKEN_WORDS = [
|
|
3855
|
+
// Patterns are anchored with \b on both sides. Case-insensitive.
|
|
3856
|
+
{ symbol: "SUI", pattern: /\bSUI\b/i },
|
|
3857
|
+
{ symbol: "USDT", pattern: /\bUSDT\b/i },
|
|
3858
|
+
{ symbol: "USDe", pattern: /\bUSDe\b/i },
|
|
3859
|
+
{ symbol: "USDsui", pattern: /\bUSDsui\b/i },
|
|
3860
|
+
{ symbol: "WAL", pattern: /\bWAL\b/i },
|
|
3861
|
+
{ symbol: "ETH", pattern: /\bETH\b/i },
|
|
3862
|
+
{ symbol: "NAVX", pattern: /\bNAVX\b/i },
|
|
3863
|
+
{ symbol: "GOLD", pattern: /\bGOLD\b/i }
|
|
3864
|
+
];
|
|
3865
|
+
function guardAssetIntent(tool, call, userText) {
|
|
3866
|
+
if (tool.name !== "send_transfer") {
|
|
3867
|
+
return { verdict: "pass", gate: "asset_intent", tier: "safety" };
|
|
3868
|
+
}
|
|
3869
|
+
const input = call.input;
|
|
3870
|
+
const assetWasSet = !(input.asset === void 0 || input.asset === null || input.asset === "");
|
|
3871
|
+
if (assetWasSet) {
|
|
3872
|
+
return { verdict: "pass", gate: "asset_intent", tier: "safety" };
|
|
3873
|
+
}
|
|
3874
|
+
const mentioned = NON_USDC_TOKEN_WORDS.find((t) => t.pattern.test(userText));
|
|
3875
|
+
if (!mentioned) {
|
|
3876
|
+
return { verdict: "pass", gate: "asset_intent", tier: "safety" };
|
|
3877
|
+
}
|
|
3878
|
+
return {
|
|
3879
|
+
verdict: "block",
|
|
3880
|
+
gate: "asset_intent",
|
|
3881
|
+
tier: "safety",
|
|
3882
|
+
message: `Asset mismatch: the user's recent messages mention "${mentioned.symbol}" but send_transfer was called without an \`asset\` field (defaults to USDC). If the user asked you to send ${mentioned.symbol}, re-issue send_transfer with \`asset: "${mentioned.symbol}"\`. If the user really meant USDC, set \`asset: "USDC"\` explicitly to confirm intent. Never default to USDC when the user named a different token.`
|
|
3883
|
+
};
|
|
3884
|
+
}
|
|
3885
|
+
function guardSwapPreview(tool, call, swapQuoteTracker) {
|
|
3886
|
+
if (tool.name !== "swap_execute") {
|
|
3887
|
+
return { verdict: "pass", gate: "swap_preview", tier: "safety" };
|
|
3888
|
+
}
|
|
3889
|
+
const input = call.input;
|
|
3890
|
+
const from = typeof input.from === "string" ? input.from : "";
|
|
3891
|
+
const to = typeof input.to === "string" ? input.to : "";
|
|
3892
|
+
const amount = Number(input.amount ?? 0);
|
|
3893
|
+
if (!from || !to || !(amount > 0)) {
|
|
3894
|
+
return { verdict: "pass", gate: "swap_preview", tier: "safety" };
|
|
3895
|
+
}
|
|
3896
|
+
if (swapQuoteTracker.hasMatchingQuote({ from, to, amount })) {
|
|
3897
|
+
return { verdict: "pass", gate: "swap_preview", tier: "safety" };
|
|
3898
|
+
}
|
|
3899
|
+
return {
|
|
3900
|
+
verdict: "block",
|
|
3901
|
+
gate: "swap_preview",
|
|
3902
|
+
tier: "safety",
|
|
3903
|
+
message: `swap_execute requires a recent matching swap_quote so the user sees an accurate preview. Call swap_quote({ from: "${from}", to: "${to}", amount: ${amount} }) first, then re-issue swap_execute with the same params. swap_quote is read-only and returns the real on-chain output, route, and price impact \u2014 never estimate from memory.`
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3796
3906
|
function guardAddressSource(tool, call, userText, contacts, walletAddress) {
|
|
3797
3907
|
if (tool.name !== "send_transfer") {
|
|
3798
3908
|
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
@@ -3848,6 +3958,7 @@ function createGuardRunnerState() {
|
|
|
3848
3958
|
return {
|
|
3849
3959
|
balanceTracker: new BalanceTracker(),
|
|
3850
3960
|
retryTracker: new RetryTracker(),
|
|
3961
|
+
swapQuoteTracker: new SwapQuoteTracker(),
|
|
3851
3962
|
lastHealthFactor: null
|
|
3852
3963
|
};
|
|
3853
3964
|
}
|
|
@@ -3903,6 +4014,12 @@ function runGuards(tool, call, state, config, conversationContext, onGuardFired,
|
|
|
3903
4014
|
)
|
|
3904
4015
|
);
|
|
3905
4016
|
}
|
|
4017
|
+
if (config.assetIntent !== false) {
|
|
4018
|
+
results.push(guardAssetIntent(tool, call, conversationContext.recentUserText));
|
|
4019
|
+
}
|
|
4020
|
+
if (config.swapPreview !== false) {
|
|
4021
|
+
results.push(guardSwapPreview(tool, call, state.swapQuoteTracker));
|
|
4022
|
+
}
|
|
3906
4023
|
if (config.irreversibility !== false) {
|
|
3907
4024
|
results.push(guardIrreversibility(tool, call, conversationContext.fullText));
|
|
3908
4025
|
}
|
|
@@ -3969,6 +4086,15 @@ function updateGuardStateAfterToolResult(toolName, tool, input, result, isError,
|
|
|
3969
4086
|
state.lastHealthFactor = hf;
|
|
3970
4087
|
}
|
|
3971
4088
|
}
|
|
4089
|
+
if (toolName === "swap_quote" && input && typeof input === "object") {
|
|
4090
|
+
const i = input;
|
|
4091
|
+
const from = typeof i.from === "string" ? i.from : "";
|
|
4092
|
+
const to = typeof i.to === "string" ? i.to : "";
|
|
4093
|
+
const amount = Number(i.amount ?? 0);
|
|
4094
|
+
if (from && to && amount > 0) {
|
|
4095
|
+
state.swapQuoteTracker.record({ from, to, amount });
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
3972
4098
|
state.retryTracker.record(toolName, input, result);
|
|
3973
4099
|
}
|
|
3974
4100
|
function extractConversationText(messages) {
|