@t2000/engine 0.46.10 → 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 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): GuardCheckResult;
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): 'auto' | 'confirm' | 'explicit';
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
@@ -3609,7 +3609,8 @@ var DEFAULT_GUARD_CONFIG = {
3609
3609
  artifactPreview: true,
3610
3610
  costWarning: true,
3611
3611
  retryProtection: true,
3612
- inputValidation: true
3612
+ inputValidation: true,
3613
+ addressSource: true
3613
3614
  };
3614
3615
  var BalanceTracker = class {
3615
3616
  lastBalanceAt = 0;
@@ -3666,7 +3667,7 @@ function guardIrreversibility(tool, _call, conversationText) {
3666
3667
  if (!tool.flags.irreversible) {
3667
3668
  return { verdict: "pass", gate: "irreversibility", tier: "safety" };
3668
3669
  }
3669
- const hasPreview = /preview|here.s what|confirm.*send|looks? good/i.test(conversationText);
3670
+ const hasPreview = /preview|here.{0,2}s what|confirm.{0,200}send|looks? good/i.test(conversationText);
3670
3671
  if (hasPreview) {
3671
3672
  return { verdict: "pass", gate: "irreversibility", tier: "safety" };
3672
3673
  }
@@ -3762,7 +3763,7 @@ function guardSlippage(tool, _call, lastAssistantText) {
3762
3763
  if (tool.name !== "swap_execute") {
3763
3764
  return { verdict: "pass", gate: "slippage_warning", tier: "financial" };
3764
3765
  }
3765
- const hasEstimate = /~?\$?[\d,]+\.?\d*\s*(SUI|USDC|USDT|WETH)/i.test(lastAssistantText) || /approximately|≈|about|expect|receive/i.test(lastAssistantText);
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);
3766
3767
  if (hasEstimate) {
3767
3768
  return { verdict: "pass", gate: "slippage_warning", tier: "financial" };
3768
3769
  }
@@ -3788,6 +3789,41 @@ function guardCostWarning(tool, _call, conversationText) {
3788
3789
  message: "This action has a monetary cost. Confirm the user is aware before proceeding."
3789
3790
  };
3790
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
+ }
3791
3827
  function guardArtifactPreview(result) {
3792
3828
  if (!result || typeof result !== "object") return null;
3793
3829
  const r = result;
@@ -3815,7 +3851,7 @@ function createGuardRunnerState() {
3815
3851
  lastHealthFactor: null
3816
3852
  };
3817
3853
  }
3818
- function runGuards(tool, call, state, config, conversationContext, onGuardFired) {
3854
+ function runGuards(tool, call, state, config, conversationContext, onGuardFired, identity) {
3819
3855
  const results = [];
3820
3856
  const now = Date.now();
3821
3857
  const fire = (verdict, tier, gate, hadInjection) => {
@@ -3856,6 +3892,17 @@ function runGuards(tool, call, state, config, conversationContext, onGuardFired)
3856
3892
  if (config.retryProtection !== false) {
3857
3893
  results.push(guardRetryProtection(tool, call, state.retryTracker));
3858
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
+ }
3859
3906
  if (config.irreversibility !== false) {
3860
3907
  results.push(guardIrreversibility(tool, call, conversationContext.fullText));
3861
3908
  }
@@ -3926,6 +3973,7 @@ function updateGuardStateAfterToolResult(toolName, tool, input, result, isError,
3926
3973
  }
3927
3974
  function extractConversationText(messages) {
3928
3975
  const textParts = [];
3976
+ const userParts = [];
3929
3977
  let lastAssistantText = "";
3930
3978
  for (const msg of messages) {
3931
3979
  if (!Array.isArray(msg.content)) continue;
@@ -3934,13 +3982,19 @@ function extractConversationText(messages) {
3934
3982
  textParts.push(block.text);
3935
3983
  if (msg.role === "assistant") {
3936
3984
  lastAssistantText = block.text;
3985
+ } else if (msg.role === "user") {
3986
+ userParts.push(block.text);
3937
3987
  }
3938
3988
  }
3939
3989
  }
3940
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);
3941
3994
  return {
3942
- fullText: textParts.join("\n"),
3943
- lastAssistantText
3995
+ fullText: cap(textParts.join("\n")),
3996
+ lastAssistantText: cap(lastAssistantText),
3997
+ recentUserText: cap(recentUserParts.join("\n"))
3944
3998
  };
3945
3999
  }
3946
4000
 
@@ -4240,7 +4294,12 @@ var PERMISSION_PRESETS = {
4240
4294
  ]
4241
4295
  }
4242
4296
  };
4243
- function resolvePermissionTier(operation, amountUsd, config, sessionSpendUsd) {
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) {
4244
4303
  const rule = config.rules.find((r) => r.operation === operation);
4245
4304
  const autoBelow = rule?.autoBelow ?? config.globalAutoBelow;
4246
4305
  const confirmBetween = rule?.confirmBetween ?? 1e3;
@@ -4249,7 +4308,10 @@ function resolvePermissionTier(operation, amountUsd, config, sessionSpendUsd) {
4249
4308
  else if (amountUsd < confirmBetween) tier = "confirm";
4250
4309
  else tier = "explicit";
4251
4310
  if (tier === "auto" && typeof sessionSpendUsd === "number" && sessionSpendUsd + amountUsd > config.autonomousDailyLimit) {
4252
- return "confirm";
4311
+ tier = "confirm";
4312
+ }
4313
+ if (tier === "auto" && operation === "send" && sendContext?.to && !isKnownContactAddress(sendContext.to, sendContext.contacts ?? [])) {
4314
+ tier = "confirm";
4253
4315
  }
4254
4316
  return tier;
4255
4317
  }
@@ -4497,6 +4559,9 @@ var QueryEngine = class {
4497
4559
  contextSummarizer;
4498
4560
  priceCache;
4499
4561
  permissionConfig;
4562
+ // Saved contacts — consulted by `guardAddressSource` and the permission
4563
+ // tier resolver (sends to non-contact addresses always require confirm).
4564
+ contacts;
4500
4565
  // [v1.4] Session-scoped autonomous spend tracking.
4501
4566
  sessionSpendUsd;
4502
4567
  onAutoExecuted;
@@ -4545,6 +4610,7 @@ var QueryEngine = class {
4545
4610
  this.contextSummarizer = config.contextSummarizer;
4546
4611
  this.priceCache = config.priceCache;
4547
4612
  this.permissionConfig = config.permissionConfig;
4613
+ this.contacts = config.contacts ?? [];
4548
4614
  this.sessionSpendUsd = config.sessionSpendUsd;
4549
4615
  this.onAutoExecuted = config.onAutoExecuted;
4550
4616
  this.onGuardFired = config.onGuardFired;
@@ -5064,11 +5130,13 @@ ${recipeCtx}`;
5064
5130
  const operation = toolNameToOperation(call.name);
5065
5131
  if (operation) {
5066
5132
  const usdValue = resolveUsdValue(call.name, call.input, context.priceCache);
5133
+ const callInput = call.input;
5067
5134
  const tier = resolvePermissionTier(
5068
5135
  operation,
5069
5136
  usdValue,
5070
5137
  context.permissionConfig,
5071
- context.sessionSpendUsd
5138
+ context.sessionSpendUsd,
5139
+ operation === "send" ? { to: typeof callInput.to === "string" ? callInput.to : void 0, contacts: this.contacts } : void 0
5072
5140
  );
5073
5141
  return tier !== "auto";
5074
5142
  }
@@ -5098,7 +5166,8 @@ ${recipeCtx}`;
5098
5166
  this.guardState,
5099
5167
  this.guardConfig,
5100
5168
  convCtx,
5101
- this.onGuardFired
5169
+ this.onGuardFired,
5170
+ { contacts: this.contacts, walletAddress: this.walletAddress }
5102
5171
  );
5103
5172
  this.guardEvents.push(...check.events);
5104
5173
  if (check.blocked) {
@@ -5228,7 +5297,8 @@ ${recipeCtx}`;
5228
5297
  this.guardState,
5229
5298
  this.guardConfig,
5230
5299
  convCtx,
5231
- this.onGuardFired
5300
+ this.onGuardFired,
5301
+ { contacts: this.contacts, walletAddress: this.walletAddress }
5232
5302
  );
5233
5303
  this.guardEvents.push(...check.events);
5234
5304
  if (check.blocked) {