@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 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
@@ -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 coinParts = bc.coinType.split("::");
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.*send|looks? good/i.test(conversationText);
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 = /~?\$?[\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);
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 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) {
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
- return "confirm";
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) {