@t2000/engine 0.46.9 → 0.46.11
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 +55 -5
- package/dist/index.js +82 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -219,6 +219,15 @@ interface GuardConfig {
|
|
|
219
219
|
costWarning?: boolean;
|
|
220
220
|
retryProtection?: boolean;
|
|
221
221
|
inputValidation?: boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Root-cause guard for "LLM types a recipient address from memory and
|
|
224
|
+
* loses funds to a wrong-but-valid address". When enabled (default),
|
|
225
|
+
* `send_transfer.to` is rejected unless the address can be sourced
|
|
226
|
+
* from a saved contact, the user's own wallet, or the user's recent
|
|
227
|
+
* messages. Set to `false` only if the host has its own equivalent
|
|
228
|
+
* upstream guard (e.g. an off-process verifier).
|
|
229
|
+
*/
|
|
230
|
+
addressSource?: boolean;
|
|
222
231
|
}
|
|
223
232
|
declare const DEFAULT_GUARD_CONFIG: GuardConfig;
|
|
224
233
|
declare class BalanceTracker {
|
|
@@ -249,6 +258,7 @@ declare function createGuardRunnerState(): GuardRunnerState;
|
|
|
249
258
|
declare function runGuards(tool: Tool, call: PendingToolCall, state: GuardRunnerState, config: GuardConfig, conversationContext: {
|
|
250
259
|
fullText: string;
|
|
251
260
|
lastAssistantText: string;
|
|
261
|
+
recentUserText: string;
|
|
252
262
|
},
|
|
253
263
|
/**
|
|
254
264
|
* [v1.4 Item 4] Optional per-guard observation hook. Fired exactly
|
|
@@ -256,7 +266,21 @@ declare function runGuards(tool: Tool, call: PendingToolCall, state: GuardRunner
|
|
|
256
266
|
* up in `events`/`injections`/`block`). Errors thrown by the host
|
|
257
267
|
* are caught so a misbehaving collector can't break tool execution.
|
|
258
268
|
*/
|
|
259
|
-
onGuardFired?: (guard: GuardMetric) => void
|
|
269
|
+
onGuardFired?: (guard: GuardMetric) => void,
|
|
270
|
+
/**
|
|
271
|
+
* Identity context for the address-source safety guard. The guard
|
|
272
|
+
* accepts `send_transfer.to` only when sourced from a saved contact,
|
|
273
|
+
* the user's own wallet, or the user's recent messages — preventing
|
|
274
|
+
* the LLM from typing addresses from memory and shipping funds to a
|
|
275
|
+
* wrong-but-syntactically-valid recipient.
|
|
276
|
+
*/
|
|
277
|
+
identity?: {
|
|
278
|
+
contacts?: ReadonlyArray<{
|
|
279
|
+
name: string;
|
|
280
|
+
address: string;
|
|
281
|
+
}>;
|
|
282
|
+
walletAddress?: string;
|
|
283
|
+
}): GuardCheckResult;
|
|
260
284
|
declare function updateGuardStateAfterToolResult(toolName: string, tool: Tool | undefined, input: unknown, result: unknown, isError: boolean, state: GuardRunnerState): void;
|
|
261
285
|
declare function extractConversationText(messages: Array<{
|
|
262
286
|
role: string;
|
|
@@ -264,6 +288,7 @@ declare function extractConversationText(messages: Array<{
|
|
|
264
288
|
}>): {
|
|
265
289
|
fullText: string;
|
|
266
290
|
lastAssistantText: string;
|
|
291
|
+
recentUserText: string;
|
|
267
292
|
};
|
|
268
293
|
|
|
269
294
|
/**
|
|
@@ -366,8 +391,20 @@ declare const PERMISSION_PRESETS: {
|
|
|
366
391
|
* `config.autonomousDailyLimit`, an otherwise-`auto` tier is downgraded to
|
|
367
392
|
* `confirm`. This is the runtime guard for the daily autonomous spend cap.
|
|
368
393
|
* Tiers above `auto` are returned unchanged.
|
|
394
|
+
*
|
|
395
|
+
* Send-safety rule: when `operation === 'send'` and the destination
|
|
396
|
+
* address is a raw `0x...` (i.e. NOT one of the user's saved contacts),
|
|
397
|
+
* an otherwise-`auto` tier is downgraded to `confirm` regardless of
|
|
398
|
+
* amount. This bounds the "LLM/user typo silently ships funds" failure
|
|
399
|
+
* mode to a single confirmation per recipient — once saved as a contact,
|
|
400
|
+
* subsequent sends to the same address auto-approve under tier as normal.
|
|
369
401
|
*/
|
|
370
|
-
declare function resolvePermissionTier(operation: string, amountUsd: number, config: UserPermissionConfig, sessionSpendUsd?: number
|
|
402
|
+
declare function resolvePermissionTier(operation: string, amountUsd: number, config: UserPermissionConfig, sessionSpendUsd?: number, sendContext?: {
|
|
403
|
+
to?: string;
|
|
404
|
+
contacts?: ReadonlyArray<{
|
|
405
|
+
address: string;
|
|
406
|
+
}>;
|
|
407
|
+
}): 'auto' | 'confirm' | 'explicit';
|
|
371
408
|
declare function toolNameToOperation(toolName: string): PermissionOperation | undefined;
|
|
372
409
|
/**
|
|
373
410
|
* Resolve the USD value of a tool call from its inputs.
|
|
@@ -698,6 +735,18 @@ interface EngineConfig {
|
|
|
698
735
|
priceCache?: Map<string, number>;
|
|
699
736
|
/** Per-user permission config for USD-threshold write tool gating (B.4). */
|
|
700
737
|
permissionConfig?: UserPermissionConfig;
|
|
738
|
+
/**
|
|
739
|
+
* Saved contacts for the current user. Used by `guardAddressSource`
|
|
740
|
+
* (a saved contact's address is considered a trusted source for
|
|
741
|
+
* `send_transfer.to`) and by `permission-rules.resolvePermissionTier`
|
|
742
|
+
* (sends to non-contact addresses always require confirmation,
|
|
743
|
+
* regardless of amount). Hosts SHOULD also surface these in the
|
|
744
|
+
* dynamic system prompt block so the LLM can resolve "send to <name>".
|
|
745
|
+
*/
|
|
746
|
+
contacts?: ReadonlyArray<{
|
|
747
|
+
name: string;
|
|
748
|
+
address: string;
|
|
749
|
+
}>;
|
|
701
750
|
/**
|
|
702
751
|
* [v1.4] Cumulative USD already auto-executed in the current session.
|
|
703
752
|
* Forwarded to `ToolContext` and consulted by `resolvePermissionTier` to
|
|
@@ -880,6 +929,7 @@ declare class QueryEngine {
|
|
|
880
929
|
private readonly contextSummarizer;
|
|
881
930
|
private readonly priceCache;
|
|
882
931
|
private readonly permissionConfig;
|
|
932
|
+
private readonly contacts;
|
|
883
933
|
private readonly sessionSpendUsd;
|
|
884
934
|
private readonly onAutoExecuted;
|
|
885
935
|
private readonly onGuardFired;
|
|
@@ -1810,8 +1860,8 @@ declare const withdrawTool: Tool<{
|
|
|
1810
1860
|
}>;
|
|
1811
1861
|
|
|
1812
1862
|
declare const sendTransferTool: Tool<{
|
|
1813
|
-
amount: number;
|
|
1814
1863
|
to: string;
|
|
1864
|
+
amount: number;
|
|
1815
1865
|
memo?: string | undefined;
|
|
1816
1866
|
}, {
|
|
1817
1867
|
success: boolean;
|
|
@@ -1879,8 +1929,8 @@ declare const mppServicesTool: Tool<{
|
|
|
1879
1929
|
}, Record<string, unknown>>;
|
|
1880
1930
|
|
|
1881
1931
|
declare const swapExecuteTool: Tool<{
|
|
1882
|
-
amount: number;
|
|
1883
1932
|
to: string;
|
|
1933
|
+
amount: number;
|
|
1884
1934
|
from: string;
|
|
1885
1935
|
byAmountIn?: boolean | undefined;
|
|
1886
1936
|
slippage?: number | undefined;
|
|
@@ -1896,8 +1946,8 @@ declare const swapExecuteTool: Tool<{
|
|
|
1896
1946
|
}>;
|
|
1897
1947
|
|
|
1898
1948
|
declare const swapQuoteTool: Tool<{
|
|
1899
|
-
amount: number;
|
|
1900
1949
|
to: string;
|
|
1950
|
+
amount: number;
|
|
1901
1951
|
from: string;
|
|
1902
1952
|
byAmountIn?: boolean | undefined;
|
|
1903
1953
|
}, _t2000_sdk.SwapQuoteResult>;
|
package/dist/index.js
CHANGED
|
@@ -2162,8 +2162,7 @@ var explainTxTool = buildTool({
|
|
|
2162
2162
|
if (balanceChanges?.length) {
|
|
2163
2163
|
for (const bc of balanceChanges) {
|
|
2164
2164
|
const ownerAddr = bc.owner?.AddressOwner ?? bc.owner?.ObjectOwner ?? "unknown";
|
|
2165
|
-
const
|
|
2166
|
-
const symbol = coinParts[coinParts.length - 1] ?? bc.coinType;
|
|
2165
|
+
const symbol = resolveSymbol(bc.coinType);
|
|
2167
2166
|
const amount = Number(bc.amount);
|
|
2168
2167
|
const isNegative = amount < 0;
|
|
2169
2168
|
const decimals = getDecimalsForCoinType(bc.coinType);
|
|
@@ -3610,7 +3609,8 @@ var DEFAULT_GUARD_CONFIG = {
|
|
|
3610
3609
|
artifactPreview: true,
|
|
3611
3610
|
costWarning: true,
|
|
3612
3611
|
retryProtection: true,
|
|
3613
|
-
inputValidation: true
|
|
3612
|
+
inputValidation: true,
|
|
3613
|
+
addressSource: true
|
|
3614
3614
|
};
|
|
3615
3615
|
var BalanceTracker = class {
|
|
3616
3616
|
lastBalanceAt = 0;
|
|
@@ -3667,7 +3667,7 @@ function guardIrreversibility(tool, _call, conversationText) {
|
|
|
3667
3667
|
if (!tool.flags.irreversible) {
|
|
3668
3668
|
return { verdict: "pass", gate: "irreversibility", tier: "safety" };
|
|
3669
3669
|
}
|
|
3670
|
-
const hasPreview = /preview|here.s what|confirm
|
|
3670
|
+
const hasPreview = /preview|here.{0,2}s what|confirm.{0,200}send|looks? good/i.test(conversationText);
|
|
3671
3671
|
if (hasPreview) {
|
|
3672
3672
|
return { verdict: "pass", gate: "irreversibility", tier: "safety" };
|
|
3673
3673
|
}
|
|
@@ -3763,7 +3763,7 @@ function guardSlippage(tool, _call, lastAssistantText) {
|
|
|
3763
3763
|
if (tool.name !== "swap_execute") {
|
|
3764
3764
|
return { verdict: "pass", gate: "slippage_warning", tier: "financial" };
|
|
3765
3765
|
}
|
|
3766
|
-
const hasEstimate =
|
|
3766
|
+
const hasEstimate = /~?\$?\d[\d,]{0,30}(?:\.\d{1,10})?\s*(SUI|USDC|USDT|WETH)/i.test(lastAssistantText) || /approximately|≈|about|expect|receive/i.test(lastAssistantText);
|
|
3767
3767
|
if (hasEstimate) {
|
|
3768
3768
|
return { verdict: "pass", gate: "slippage_warning", tier: "financial" };
|
|
3769
3769
|
}
|
|
@@ -3789,6 +3789,41 @@ function guardCostWarning(tool, _call, conversationText) {
|
|
|
3789
3789
|
message: "This action has a monetary cost. Confirm the user is aware before proceeding."
|
|
3790
3790
|
};
|
|
3791
3791
|
}
|
|
3792
|
+
var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{64}$/;
|
|
3793
|
+
function normalizeAddress(addr) {
|
|
3794
|
+
return addr.trim().toLowerCase();
|
|
3795
|
+
}
|
|
3796
|
+
function guardAddressSource(tool, call, userText, contacts, walletAddress) {
|
|
3797
|
+
if (tool.name !== "send_transfer") {
|
|
3798
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3799
|
+
}
|
|
3800
|
+
const input = call.input;
|
|
3801
|
+
const rawTo = String(input.to ?? "");
|
|
3802
|
+
if (!rawTo) {
|
|
3803
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3804
|
+
}
|
|
3805
|
+
if (!SUI_ADDRESS_REGEX.test(rawTo)) {
|
|
3806
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3807
|
+
}
|
|
3808
|
+
const normalizedTo = normalizeAddress(rawTo);
|
|
3809
|
+
if (walletAddress && normalizeAddress(walletAddress) === normalizedTo) {
|
|
3810
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3811
|
+
}
|
|
3812
|
+
for (const c of contacts) {
|
|
3813
|
+
if (normalizeAddress(c.address) === normalizedTo) {
|
|
3814
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
if (userText.toLowerCase().includes(normalizedTo)) {
|
|
3818
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3819
|
+
}
|
|
3820
|
+
return {
|
|
3821
|
+
verdict: "block",
|
|
3822
|
+
gate: "address_source",
|
|
3823
|
+
tier: "safety",
|
|
3824
|
+
message: `Safety check failed: the recipient address "${rawTo}" was not provided by the user (no saved contact matches, address is not the user's own wallet, and it does not appear verbatim in the user's recent messages). For safety, addresses must be supplied directly by the user \u2014 never reconstructed from memory or partial recall. Ask the user to paste the destination address again exactly.`
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3792
3827
|
function guardArtifactPreview(result) {
|
|
3793
3828
|
if (!result || typeof result !== "object") return null;
|
|
3794
3829
|
const r = result;
|
|
@@ -3816,7 +3851,7 @@ function createGuardRunnerState() {
|
|
|
3816
3851
|
lastHealthFactor: null
|
|
3817
3852
|
};
|
|
3818
3853
|
}
|
|
3819
|
-
function runGuards(tool, call, state, config, conversationContext, onGuardFired) {
|
|
3854
|
+
function runGuards(tool, call, state, config, conversationContext, onGuardFired, identity) {
|
|
3820
3855
|
const results = [];
|
|
3821
3856
|
const now = Date.now();
|
|
3822
3857
|
const fire = (verdict, tier, gate, hadInjection) => {
|
|
@@ -3857,6 +3892,17 @@ function runGuards(tool, call, state, config, conversationContext, onGuardFired)
|
|
|
3857
3892
|
if (config.retryProtection !== false) {
|
|
3858
3893
|
results.push(guardRetryProtection(tool, call, state.retryTracker));
|
|
3859
3894
|
}
|
|
3895
|
+
if (config.addressSource !== false) {
|
|
3896
|
+
results.push(
|
|
3897
|
+
guardAddressSource(
|
|
3898
|
+
tool,
|
|
3899
|
+
call,
|
|
3900
|
+
conversationContext.recentUserText,
|
|
3901
|
+
identity?.contacts ?? [],
|
|
3902
|
+
identity?.walletAddress
|
|
3903
|
+
)
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3860
3906
|
if (config.irreversibility !== false) {
|
|
3861
3907
|
results.push(guardIrreversibility(tool, call, conversationContext.fullText));
|
|
3862
3908
|
}
|
|
@@ -3927,6 +3973,7 @@ function updateGuardStateAfterToolResult(toolName, tool, input, result, isError,
|
|
|
3927
3973
|
}
|
|
3928
3974
|
function extractConversationText(messages) {
|
|
3929
3975
|
const textParts = [];
|
|
3976
|
+
const userParts = [];
|
|
3930
3977
|
let lastAssistantText = "";
|
|
3931
3978
|
for (const msg of messages) {
|
|
3932
3979
|
if (!Array.isArray(msg.content)) continue;
|
|
@@ -3935,13 +3982,19 @@ function extractConversationText(messages) {
|
|
|
3935
3982
|
textParts.push(block.text);
|
|
3936
3983
|
if (msg.role === "assistant") {
|
|
3937
3984
|
lastAssistantText = block.text;
|
|
3985
|
+
} else if (msg.role === "user") {
|
|
3986
|
+
userParts.push(block.text);
|
|
3938
3987
|
}
|
|
3939
3988
|
}
|
|
3940
3989
|
}
|
|
3941
3990
|
}
|
|
3991
|
+
const recentUserParts = userParts.slice(-10);
|
|
3992
|
+
const MAX_REGEX_INPUT = 16 * 1024;
|
|
3993
|
+
const cap = (s) => s.length <= MAX_REGEX_INPUT ? s : s.slice(-MAX_REGEX_INPUT);
|
|
3942
3994
|
return {
|
|
3943
|
-
fullText: textParts.join("\n"),
|
|
3944
|
-
lastAssistantText
|
|
3995
|
+
fullText: cap(textParts.join("\n")),
|
|
3996
|
+
lastAssistantText: cap(lastAssistantText),
|
|
3997
|
+
recentUserText: cap(recentUserParts.join("\n"))
|
|
3945
3998
|
};
|
|
3946
3999
|
}
|
|
3947
4000
|
|
|
@@ -4241,7 +4294,12 @@ var PERMISSION_PRESETS = {
|
|
|
4241
4294
|
]
|
|
4242
4295
|
}
|
|
4243
4296
|
};
|
|
4244
|
-
function
|
|
4297
|
+
function isKnownContactAddress(to, contacts) {
|
|
4298
|
+
if (!to) return false;
|
|
4299
|
+
const normalized = to.trim().toLowerCase();
|
|
4300
|
+
return contacts.some((c) => c.address.trim().toLowerCase() === normalized);
|
|
4301
|
+
}
|
|
4302
|
+
function resolvePermissionTier(operation, amountUsd, config, sessionSpendUsd, sendContext) {
|
|
4245
4303
|
const rule = config.rules.find((r) => r.operation === operation);
|
|
4246
4304
|
const autoBelow = rule?.autoBelow ?? config.globalAutoBelow;
|
|
4247
4305
|
const confirmBetween = rule?.confirmBetween ?? 1e3;
|
|
@@ -4250,7 +4308,10 @@ function resolvePermissionTier(operation, amountUsd, config, sessionSpendUsd) {
|
|
|
4250
4308
|
else if (amountUsd < confirmBetween) tier = "confirm";
|
|
4251
4309
|
else tier = "explicit";
|
|
4252
4310
|
if (tier === "auto" && typeof sessionSpendUsd === "number" && sessionSpendUsd + amountUsd > config.autonomousDailyLimit) {
|
|
4253
|
-
|
|
4311
|
+
tier = "confirm";
|
|
4312
|
+
}
|
|
4313
|
+
if (tier === "auto" && operation === "send" && sendContext?.to && !isKnownContactAddress(sendContext.to, sendContext.contacts ?? [])) {
|
|
4314
|
+
tier = "confirm";
|
|
4254
4315
|
}
|
|
4255
4316
|
return tier;
|
|
4256
4317
|
}
|
|
@@ -4498,6 +4559,9 @@ var QueryEngine = class {
|
|
|
4498
4559
|
contextSummarizer;
|
|
4499
4560
|
priceCache;
|
|
4500
4561
|
permissionConfig;
|
|
4562
|
+
// Saved contacts — consulted by `guardAddressSource` and the permission
|
|
4563
|
+
// tier resolver (sends to non-contact addresses always require confirm).
|
|
4564
|
+
contacts;
|
|
4501
4565
|
// [v1.4] Session-scoped autonomous spend tracking.
|
|
4502
4566
|
sessionSpendUsd;
|
|
4503
4567
|
onAutoExecuted;
|
|
@@ -4546,6 +4610,7 @@ var QueryEngine = class {
|
|
|
4546
4610
|
this.contextSummarizer = config.contextSummarizer;
|
|
4547
4611
|
this.priceCache = config.priceCache;
|
|
4548
4612
|
this.permissionConfig = config.permissionConfig;
|
|
4613
|
+
this.contacts = config.contacts ?? [];
|
|
4549
4614
|
this.sessionSpendUsd = config.sessionSpendUsd;
|
|
4550
4615
|
this.onAutoExecuted = config.onAutoExecuted;
|
|
4551
4616
|
this.onGuardFired = config.onGuardFired;
|
|
@@ -5065,11 +5130,13 @@ ${recipeCtx}`;
|
|
|
5065
5130
|
const operation = toolNameToOperation(call.name);
|
|
5066
5131
|
if (operation) {
|
|
5067
5132
|
const usdValue = resolveUsdValue(call.name, call.input, context.priceCache);
|
|
5133
|
+
const callInput = call.input;
|
|
5068
5134
|
const tier = resolvePermissionTier(
|
|
5069
5135
|
operation,
|
|
5070
5136
|
usdValue,
|
|
5071
5137
|
context.permissionConfig,
|
|
5072
|
-
context.sessionSpendUsd
|
|
5138
|
+
context.sessionSpendUsd,
|
|
5139
|
+
operation === "send" ? { to: typeof callInput.to === "string" ? callInput.to : void 0, contacts: this.contacts } : void 0
|
|
5073
5140
|
);
|
|
5074
5141
|
return tier !== "auto";
|
|
5075
5142
|
}
|
|
@@ -5099,7 +5166,8 @@ ${recipeCtx}`;
|
|
|
5099
5166
|
this.guardState,
|
|
5100
5167
|
this.guardConfig,
|
|
5101
5168
|
convCtx,
|
|
5102
|
-
this.onGuardFired
|
|
5169
|
+
this.onGuardFired,
|
|
5170
|
+
{ contacts: this.contacts, walletAddress: this.walletAddress }
|
|
5103
5171
|
);
|
|
5104
5172
|
this.guardEvents.push(...check.events);
|
|
5105
5173
|
if (check.blocked) {
|
|
@@ -5229,7 +5297,8 @@ ${recipeCtx}`;
|
|
|
5229
5297
|
this.guardState,
|
|
5230
5298
|
this.guardConfig,
|
|
5231
5299
|
convCtx,
|
|
5232
|
-
this.onGuardFired
|
|
5300
|
+
this.onGuardFired,
|
|
5301
|
+
{ contacts: this.contacts, walletAddress: this.walletAddress }
|
|
5233
5302
|
);
|
|
5234
5303
|
this.guardEvents.push(...check.events);
|
|
5235
5304
|
if (check.blocked) {
|