@t2000/engine 0.46.10 → 0.46.12
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 +65 -5
- package/dist/index.js +138 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -219,6 +219,23 @@ 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;
|
|
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;
|
|
222
239
|
}
|
|
223
240
|
declare const DEFAULT_GUARD_CONFIG: GuardConfig;
|
|
224
241
|
declare class BalanceTracker {
|
|
@@ -249,6 +266,7 @@ declare function createGuardRunnerState(): GuardRunnerState;
|
|
|
249
266
|
declare function runGuards(tool: Tool, call: PendingToolCall, state: GuardRunnerState, config: GuardConfig, conversationContext: {
|
|
250
267
|
fullText: string;
|
|
251
268
|
lastAssistantText: string;
|
|
269
|
+
recentUserText: string;
|
|
252
270
|
},
|
|
253
271
|
/**
|
|
254
272
|
* [v1.4 Item 4] Optional per-guard observation hook. Fired exactly
|
|
@@ -256,7 +274,21 @@ declare function runGuards(tool: Tool, call: PendingToolCall, state: GuardRunner
|
|
|
256
274
|
* up in `events`/`injections`/`block`). Errors thrown by the host
|
|
257
275
|
* are caught so a misbehaving collector can't break tool execution.
|
|
258
276
|
*/
|
|
259
|
-
onGuardFired?: (guard: GuardMetric) => void
|
|
277
|
+
onGuardFired?: (guard: GuardMetric) => void,
|
|
278
|
+
/**
|
|
279
|
+
* Identity context for the address-source safety guard. The guard
|
|
280
|
+
* accepts `send_transfer.to` only when sourced from a saved contact,
|
|
281
|
+
* the user's own wallet, or the user's recent messages — preventing
|
|
282
|
+
* the LLM from typing addresses from memory and shipping funds to a
|
|
283
|
+
* wrong-but-syntactically-valid recipient.
|
|
284
|
+
*/
|
|
285
|
+
identity?: {
|
|
286
|
+
contacts?: ReadonlyArray<{
|
|
287
|
+
name: string;
|
|
288
|
+
address: string;
|
|
289
|
+
}>;
|
|
290
|
+
walletAddress?: string;
|
|
291
|
+
}): GuardCheckResult;
|
|
260
292
|
declare function updateGuardStateAfterToolResult(toolName: string, tool: Tool | undefined, input: unknown, result: unknown, isError: boolean, state: GuardRunnerState): void;
|
|
261
293
|
declare function extractConversationText(messages: Array<{
|
|
262
294
|
role: string;
|
|
@@ -264,6 +296,7 @@ declare function extractConversationText(messages: Array<{
|
|
|
264
296
|
}>): {
|
|
265
297
|
fullText: string;
|
|
266
298
|
lastAssistantText: string;
|
|
299
|
+
recentUserText: string;
|
|
267
300
|
};
|
|
268
301
|
|
|
269
302
|
/**
|
|
@@ -366,8 +399,20 @@ declare const PERMISSION_PRESETS: {
|
|
|
366
399
|
* `config.autonomousDailyLimit`, an otherwise-`auto` tier is downgraded to
|
|
367
400
|
* `confirm`. This is the runtime guard for the daily autonomous spend cap.
|
|
368
401
|
* Tiers above `auto` are returned unchanged.
|
|
402
|
+
*
|
|
403
|
+
* Send-safety rule: when `operation === 'send'` and the destination
|
|
404
|
+
* address is a raw `0x...` (i.e. NOT one of the user's saved contacts),
|
|
405
|
+
* an otherwise-`auto` tier is downgraded to `confirm` regardless of
|
|
406
|
+
* amount. This bounds the "LLM/user typo silently ships funds" failure
|
|
407
|
+
* mode to a single confirmation per recipient — once saved as a contact,
|
|
408
|
+
* subsequent sends to the same address auto-approve under tier as normal.
|
|
369
409
|
*/
|
|
370
|
-
declare function resolvePermissionTier(operation: string, amountUsd: number, config: UserPermissionConfig, sessionSpendUsd?: number
|
|
410
|
+
declare function resolvePermissionTier(operation: string, amountUsd: number, config: UserPermissionConfig, sessionSpendUsd?: number, sendContext?: {
|
|
411
|
+
to?: string;
|
|
412
|
+
contacts?: ReadonlyArray<{
|
|
413
|
+
address: string;
|
|
414
|
+
}>;
|
|
415
|
+
}): 'auto' | 'confirm' | 'explicit';
|
|
371
416
|
declare function toolNameToOperation(toolName: string): PermissionOperation | undefined;
|
|
372
417
|
/**
|
|
373
418
|
* Resolve the USD value of a tool call from its inputs.
|
|
@@ -698,6 +743,18 @@ interface EngineConfig {
|
|
|
698
743
|
priceCache?: Map<string, number>;
|
|
699
744
|
/** Per-user permission config for USD-threshold write tool gating (B.4). */
|
|
700
745
|
permissionConfig?: UserPermissionConfig;
|
|
746
|
+
/**
|
|
747
|
+
* Saved contacts for the current user. Used by `guardAddressSource`
|
|
748
|
+
* (a saved contact's address is considered a trusted source for
|
|
749
|
+
* `send_transfer.to`) and by `permission-rules.resolvePermissionTier`
|
|
750
|
+
* (sends to non-contact addresses always require confirmation,
|
|
751
|
+
* regardless of amount). Hosts SHOULD also surface these in the
|
|
752
|
+
* dynamic system prompt block so the LLM can resolve "send to <name>".
|
|
753
|
+
*/
|
|
754
|
+
contacts?: ReadonlyArray<{
|
|
755
|
+
name: string;
|
|
756
|
+
address: string;
|
|
757
|
+
}>;
|
|
701
758
|
/**
|
|
702
759
|
* [v1.4] Cumulative USD already auto-executed in the current session.
|
|
703
760
|
* Forwarded to `ToolContext` and consulted by `resolvePermissionTier` to
|
|
@@ -880,6 +937,7 @@ declare class QueryEngine {
|
|
|
880
937
|
private readonly contextSummarizer;
|
|
881
938
|
private readonly priceCache;
|
|
882
939
|
private readonly permissionConfig;
|
|
940
|
+
private readonly contacts;
|
|
883
941
|
private readonly sessionSpendUsd;
|
|
884
942
|
private readonly onAutoExecuted;
|
|
885
943
|
private readonly onGuardFired;
|
|
@@ -1810,13 +1868,15 @@ declare const withdrawTool: Tool<{
|
|
|
1810
1868
|
}>;
|
|
1811
1869
|
|
|
1812
1870
|
declare const sendTransferTool: Tool<{
|
|
1813
|
-
amount: number;
|
|
1814
1871
|
to: string;
|
|
1872
|
+
amount: number;
|
|
1873
|
+
asset?: string | undefined;
|
|
1815
1874
|
memo?: string | undefined;
|
|
1816
1875
|
}, {
|
|
1817
1876
|
success: boolean;
|
|
1818
1877
|
tx: string;
|
|
1819
1878
|
amount: number;
|
|
1879
|
+
asset: "USDC" | "USDT" | "SUI" | "USDe" | "USDsui" | "WAL" | "ETH" | "NAVX" | "GOLD";
|
|
1820
1880
|
to: string;
|
|
1821
1881
|
contactName: string | undefined;
|
|
1822
1882
|
gasCost: number;
|
|
@@ -1879,8 +1939,8 @@ declare const mppServicesTool: Tool<{
|
|
|
1879
1939
|
}, Record<string, unknown>>;
|
|
1880
1940
|
|
|
1881
1941
|
declare const swapExecuteTool: Tool<{
|
|
1882
|
-
amount: number;
|
|
1883
1942
|
to: string;
|
|
1943
|
+
amount: number;
|
|
1884
1944
|
from: string;
|
|
1885
1945
|
byAmountIn?: boolean | undefined;
|
|
1886
1946
|
slippage?: number | undefined;
|
|
@@ -1896,8 +1956,8 @@ declare const swapExecuteTool: Tool<{
|
|
|
1896
1956
|
}>;
|
|
1897
1957
|
|
|
1898
1958
|
declare const swapQuoteTool: Tool<{
|
|
1899
|
-
amount: number;
|
|
1900
1959
|
to: string;
|
|
1960
|
+
amount: number;
|
|
1901
1961
|
from: string;
|
|
1902
1962
|
byAmountIn?: boolean | undefined;
|
|
1903
1963
|
}, _t2000_sdk.SwapQuoteResult>;
|
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
|
});
|
|
@@ -3609,7 +3626,9 @@ var DEFAULT_GUARD_CONFIG = {
|
|
|
3609
3626
|
artifactPreview: true,
|
|
3610
3627
|
costWarning: true,
|
|
3611
3628
|
retryProtection: true,
|
|
3612
|
-
inputValidation: true
|
|
3629
|
+
inputValidation: true,
|
|
3630
|
+
addressSource: true,
|
|
3631
|
+
assetIntent: true
|
|
3613
3632
|
};
|
|
3614
3633
|
var BalanceTracker = class {
|
|
3615
3634
|
lastBalanceAt = 0;
|
|
@@ -3666,7 +3685,7 @@ function guardIrreversibility(tool, _call, conversationText) {
|
|
|
3666
3685
|
if (!tool.flags.irreversible) {
|
|
3667
3686
|
return { verdict: "pass", gate: "irreversibility", tier: "safety" };
|
|
3668
3687
|
}
|
|
3669
|
-
const hasPreview = /preview|here.s what|confirm
|
|
3688
|
+
const hasPreview = /preview|here.{0,2}s what|confirm.{0,200}send|looks? good/i.test(conversationText);
|
|
3670
3689
|
if (hasPreview) {
|
|
3671
3690
|
return { verdict: "pass", gate: "irreversibility", tier: "safety" };
|
|
3672
3691
|
}
|
|
@@ -3762,7 +3781,7 @@ function guardSlippage(tool, _call, lastAssistantText) {
|
|
|
3762
3781
|
if (tool.name !== "swap_execute") {
|
|
3763
3782
|
return { verdict: "pass", gate: "slippage_warning", tier: "financial" };
|
|
3764
3783
|
}
|
|
3765
|
-
const hasEstimate =
|
|
3784
|
+
const hasEstimate = /~?\$?\d[\d,]{0,30}(?:\.\d{1,10})?\s*(SUI|USDC|USDT|WETH)/i.test(lastAssistantText) || /approximately|≈|about|expect|receive/i.test(lastAssistantText);
|
|
3766
3785
|
if (hasEstimate) {
|
|
3767
3786
|
return { verdict: "pass", gate: "slippage_warning", tier: "financial" };
|
|
3768
3787
|
}
|
|
@@ -3788,6 +3807,72 @@ function guardCostWarning(tool, _call, conversationText) {
|
|
|
3788
3807
|
message: "This action has a monetary cost. Confirm the user is aware before proceeding."
|
|
3789
3808
|
};
|
|
3790
3809
|
}
|
|
3810
|
+
var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{64}$/;
|
|
3811
|
+
function normalizeAddress(addr) {
|
|
3812
|
+
return addr.trim().toLowerCase();
|
|
3813
|
+
}
|
|
3814
|
+
var NON_USDC_TOKEN_WORDS = [
|
|
3815
|
+
// Patterns are anchored with \b on both sides. Case-insensitive.
|
|
3816
|
+
{ symbol: "SUI", pattern: /\bSUI\b/i },
|
|
3817
|
+
{ symbol: "USDT", pattern: /\bUSDT\b/i },
|
|
3818
|
+
{ symbol: "USDe", pattern: /\bUSDe\b/i },
|
|
3819
|
+
{ symbol: "USDsui", pattern: /\bUSDsui\b/i },
|
|
3820
|
+
{ symbol: "WAL", pattern: /\bWAL\b/i },
|
|
3821
|
+
{ symbol: "ETH", pattern: /\bETH\b/i },
|
|
3822
|
+
{ symbol: "NAVX", pattern: /\bNAVX\b/i },
|
|
3823
|
+
{ symbol: "GOLD", pattern: /\bGOLD\b/i }
|
|
3824
|
+
];
|
|
3825
|
+
function guardAssetIntent(tool, call, userText) {
|
|
3826
|
+
if (tool.name !== "send_transfer") {
|
|
3827
|
+
return { verdict: "pass", gate: "asset_intent", tier: "safety" };
|
|
3828
|
+
}
|
|
3829
|
+
const input = call.input;
|
|
3830
|
+
const assetWasSet = !(input.asset === void 0 || input.asset === null || input.asset === "");
|
|
3831
|
+
if (assetWasSet) {
|
|
3832
|
+
return { verdict: "pass", gate: "asset_intent", tier: "safety" };
|
|
3833
|
+
}
|
|
3834
|
+
const mentioned = NON_USDC_TOKEN_WORDS.find((t) => t.pattern.test(userText));
|
|
3835
|
+
if (!mentioned) {
|
|
3836
|
+
return { verdict: "pass", gate: "asset_intent", tier: "safety" };
|
|
3837
|
+
}
|
|
3838
|
+
return {
|
|
3839
|
+
verdict: "block",
|
|
3840
|
+
gate: "asset_intent",
|
|
3841
|
+
tier: "safety",
|
|
3842
|
+
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.`
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
function guardAddressSource(tool, call, userText, contacts, walletAddress) {
|
|
3846
|
+
if (tool.name !== "send_transfer") {
|
|
3847
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3848
|
+
}
|
|
3849
|
+
const input = call.input;
|
|
3850
|
+
const rawTo = String(input.to ?? "");
|
|
3851
|
+
if (!rawTo) {
|
|
3852
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3853
|
+
}
|
|
3854
|
+
if (!SUI_ADDRESS_REGEX.test(rawTo)) {
|
|
3855
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3856
|
+
}
|
|
3857
|
+
const normalizedTo = normalizeAddress(rawTo);
|
|
3858
|
+
if (walletAddress && normalizeAddress(walletAddress) === normalizedTo) {
|
|
3859
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3860
|
+
}
|
|
3861
|
+
for (const c of contacts) {
|
|
3862
|
+
if (normalizeAddress(c.address) === normalizedTo) {
|
|
3863
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
if (userText.toLowerCase().includes(normalizedTo)) {
|
|
3867
|
+
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3868
|
+
}
|
|
3869
|
+
return {
|
|
3870
|
+
verdict: "block",
|
|
3871
|
+
gate: "address_source",
|
|
3872
|
+
tier: "safety",
|
|
3873
|
+
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.`
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3791
3876
|
function guardArtifactPreview(result) {
|
|
3792
3877
|
if (!result || typeof result !== "object") return null;
|
|
3793
3878
|
const r = result;
|
|
@@ -3815,7 +3900,7 @@ function createGuardRunnerState() {
|
|
|
3815
3900
|
lastHealthFactor: null
|
|
3816
3901
|
};
|
|
3817
3902
|
}
|
|
3818
|
-
function runGuards(tool, call, state, config, conversationContext, onGuardFired) {
|
|
3903
|
+
function runGuards(tool, call, state, config, conversationContext, onGuardFired, identity) {
|
|
3819
3904
|
const results = [];
|
|
3820
3905
|
const now = Date.now();
|
|
3821
3906
|
const fire = (verdict, tier, gate, hadInjection) => {
|
|
@@ -3856,6 +3941,20 @@ function runGuards(tool, call, state, config, conversationContext, onGuardFired)
|
|
|
3856
3941
|
if (config.retryProtection !== false) {
|
|
3857
3942
|
results.push(guardRetryProtection(tool, call, state.retryTracker));
|
|
3858
3943
|
}
|
|
3944
|
+
if (config.addressSource !== false) {
|
|
3945
|
+
results.push(
|
|
3946
|
+
guardAddressSource(
|
|
3947
|
+
tool,
|
|
3948
|
+
call,
|
|
3949
|
+
conversationContext.recentUserText,
|
|
3950
|
+
identity?.contacts ?? [],
|
|
3951
|
+
identity?.walletAddress
|
|
3952
|
+
)
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3955
|
+
if (config.assetIntent !== false) {
|
|
3956
|
+
results.push(guardAssetIntent(tool, call, conversationContext.recentUserText));
|
|
3957
|
+
}
|
|
3859
3958
|
if (config.irreversibility !== false) {
|
|
3860
3959
|
results.push(guardIrreversibility(tool, call, conversationContext.fullText));
|
|
3861
3960
|
}
|
|
@@ -3926,6 +4025,7 @@ function updateGuardStateAfterToolResult(toolName, tool, input, result, isError,
|
|
|
3926
4025
|
}
|
|
3927
4026
|
function extractConversationText(messages) {
|
|
3928
4027
|
const textParts = [];
|
|
4028
|
+
const userParts = [];
|
|
3929
4029
|
let lastAssistantText = "";
|
|
3930
4030
|
for (const msg of messages) {
|
|
3931
4031
|
if (!Array.isArray(msg.content)) continue;
|
|
@@ -3934,13 +4034,19 @@ function extractConversationText(messages) {
|
|
|
3934
4034
|
textParts.push(block.text);
|
|
3935
4035
|
if (msg.role === "assistant") {
|
|
3936
4036
|
lastAssistantText = block.text;
|
|
4037
|
+
} else if (msg.role === "user") {
|
|
4038
|
+
userParts.push(block.text);
|
|
3937
4039
|
}
|
|
3938
4040
|
}
|
|
3939
4041
|
}
|
|
3940
4042
|
}
|
|
4043
|
+
const recentUserParts = userParts.slice(-10);
|
|
4044
|
+
const MAX_REGEX_INPUT = 16 * 1024;
|
|
4045
|
+
const cap = (s) => s.length <= MAX_REGEX_INPUT ? s : s.slice(-MAX_REGEX_INPUT);
|
|
3941
4046
|
return {
|
|
3942
|
-
fullText: textParts.join("\n"),
|
|
3943
|
-
lastAssistantText
|
|
4047
|
+
fullText: cap(textParts.join("\n")),
|
|
4048
|
+
lastAssistantText: cap(lastAssistantText),
|
|
4049
|
+
recentUserText: cap(recentUserParts.join("\n"))
|
|
3944
4050
|
};
|
|
3945
4051
|
}
|
|
3946
4052
|
|
|
@@ -4240,7 +4346,12 @@ var PERMISSION_PRESETS = {
|
|
|
4240
4346
|
]
|
|
4241
4347
|
}
|
|
4242
4348
|
};
|
|
4243
|
-
function
|
|
4349
|
+
function isKnownContactAddress(to, contacts) {
|
|
4350
|
+
if (!to) return false;
|
|
4351
|
+
const normalized = to.trim().toLowerCase();
|
|
4352
|
+
return contacts.some((c) => c.address.trim().toLowerCase() === normalized);
|
|
4353
|
+
}
|
|
4354
|
+
function resolvePermissionTier(operation, amountUsd, config, sessionSpendUsd, sendContext) {
|
|
4244
4355
|
const rule = config.rules.find((r) => r.operation === operation);
|
|
4245
4356
|
const autoBelow = rule?.autoBelow ?? config.globalAutoBelow;
|
|
4246
4357
|
const confirmBetween = rule?.confirmBetween ?? 1e3;
|
|
@@ -4249,7 +4360,10 @@ function resolvePermissionTier(operation, amountUsd, config, sessionSpendUsd) {
|
|
|
4249
4360
|
else if (amountUsd < confirmBetween) tier = "confirm";
|
|
4250
4361
|
else tier = "explicit";
|
|
4251
4362
|
if (tier === "auto" && typeof sessionSpendUsd === "number" && sessionSpendUsd + amountUsd > config.autonomousDailyLimit) {
|
|
4252
|
-
|
|
4363
|
+
tier = "confirm";
|
|
4364
|
+
}
|
|
4365
|
+
if (tier === "auto" && operation === "send" && sendContext?.to && !isKnownContactAddress(sendContext.to, sendContext.contacts ?? [])) {
|
|
4366
|
+
tier = "confirm";
|
|
4253
4367
|
}
|
|
4254
4368
|
return tier;
|
|
4255
4369
|
}
|
|
@@ -4497,6 +4611,9 @@ var QueryEngine = class {
|
|
|
4497
4611
|
contextSummarizer;
|
|
4498
4612
|
priceCache;
|
|
4499
4613
|
permissionConfig;
|
|
4614
|
+
// Saved contacts — consulted by `guardAddressSource` and the permission
|
|
4615
|
+
// tier resolver (sends to non-contact addresses always require confirm).
|
|
4616
|
+
contacts;
|
|
4500
4617
|
// [v1.4] Session-scoped autonomous spend tracking.
|
|
4501
4618
|
sessionSpendUsd;
|
|
4502
4619
|
onAutoExecuted;
|
|
@@ -4545,6 +4662,7 @@ var QueryEngine = class {
|
|
|
4545
4662
|
this.contextSummarizer = config.contextSummarizer;
|
|
4546
4663
|
this.priceCache = config.priceCache;
|
|
4547
4664
|
this.permissionConfig = config.permissionConfig;
|
|
4665
|
+
this.contacts = config.contacts ?? [];
|
|
4548
4666
|
this.sessionSpendUsd = config.sessionSpendUsd;
|
|
4549
4667
|
this.onAutoExecuted = config.onAutoExecuted;
|
|
4550
4668
|
this.onGuardFired = config.onGuardFired;
|
|
@@ -5064,11 +5182,13 @@ ${recipeCtx}`;
|
|
|
5064
5182
|
const operation = toolNameToOperation(call.name);
|
|
5065
5183
|
if (operation) {
|
|
5066
5184
|
const usdValue = resolveUsdValue(call.name, call.input, context.priceCache);
|
|
5185
|
+
const callInput = call.input;
|
|
5067
5186
|
const tier = resolvePermissionTier(
|
|
5068
5187
|
operation,
|
|
5069
5188
|
usdValue,
|
|
5070
5189
|
context.permissionConfig,
|
|
5071
|
-
context.sessionSpendUsd
|
|
5190
|
+
context.sessionSpendUsd,
|
|
5191
|
+
operation === "send" ? { to: typeof callInput.to === "string" ? callInput.to : void 0, contacts: this.contacts } : void 0
|
|
5072
5192
|
);
|
|
5073
5193
|
return tier !== "auto";
|
|
5074
5194
|
}
|
|
@@ -5098,7 +5218,8 @@ ${recipeCtx}`;
|
|
|
5098
5218
|
this.guardState,
|
|
5099
5219
|
this.guardConfig,
|
|
5100
5220
|
convCtx,
|
|
5101
|
-
this.onGuardFired
|
|
5221
|
+
this.onGuardFired,
|
|
5222
|
+
{ contacts: this.contacts, walletAddress: this.walletAddress }
|
|
5102
5223
|
);
|
|
5103
5224
|
this.guardEvents.push(...check.events);
|
|
5104
5225
|
if (check.blocked) {
|
|
@@ -5228,7 +5349,8 @@ ${recipeCtx}`;
|
|
|
5228
5349
|
this.guardState,
|
|
5229
5350
|
this.guardConfig,
|
|
5230
5351
|
convCtx,
|
|
5231
|
-
this.onGuardFired
|
|
5352
|
+
this.onGuardFired,
|
|
5353
|
+
{ contacts: this.contacts, walletAddress: this.walletAddress }
|
|
5232
5354
|
);
|
|
5233
5355
|
this.guardEvents.push(...check.events);
|
|
5234
5356
|
if (check.blocked) {
|