@haven_ai/mcp 0.5.0-alpha.1 → 0.7.0-alpha.0

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/README.md CHANGED
@@ -117,22 +117,20 @@ Environment variable form:
117
117
  - `haven_discover_tools`
118
118
  - `haven_submit_catalog_entry`
119
119
  - `haven_list_receipts`
120
+ - `haven_open_task_budget` (#3329)
121
+ - `haven_close_task_budget` (#3329)
122
+ - `haven_submit` (#3329 — relays a local signer signature by `task_budget_id`; a `payment_id` is refused on this runtime, which signs and submits payments inline)
120
123
 
121
- ### `idempotencyKey` is deprecated — send `idempotency_key`
124
+ ### `idempotencyKey` is removed — send `idempotency_key`
122
125
 
123
126
  The tools that take an idempotency key (`haven_send`, `haven_pay_mcp_tool`,
124
- `haven_quote_x402`, `haven_pay_x402_quote`, `haven_pay_x402`) now accept
125
- **`idempotency_key`**, the spelling the hosted Haven MCP surface and every other
126
- Haven wire contract use. `idempotencyKey` still works and returns a deprecation
127
- warning; it will be removed in a future release.
127
+ `haven_quote_x402`, `haven_pay_x402_quote`, `haven_pay_x402`) take
128
+ **`idempotency_key`**, the spelling the hosted Haven MCP surface uses.
128
129
 
129
- Send **one** of them. Sending both with different values is refused
130
- (`AMBIGUOUS_IDEMPOTENCY_KEY`) with nothing contacted or spent — the key decides
131
- whether a retry is the same payment or a second one, so Haven will not guess
132
- which scope you meant. Sending both with the same value is fine.
133
-
134
- The warning arrives in an optional `warnings` array on the success result. It is
135
- additive: a caller that ignores it sees the response it always saw.
130
+ The legacy `idempotencyKey` spelling is **refused**, not silently accepted or
131
+ dropped: any call that sets it — alone or alongside `idempotency_key` — fails
132
+ with `IDEMPOTENCY_KEY_RENAMED` before anything is contacted or spent. Send
133
+ `idempotency_key` instead.
136
134
 
137
135
  ## First-launch consent
138
136
 
@@ -156,7 +154,7 @@ Tools this server will expose to your agent runtime:
156
154
 
157
155
  On-chain budget (the real spend gate — enforced by the agent's
158
156
  signed delegation, not by Haven):
159
- • up to 50.000000 USDC per 1440 min
157
+ • up to 25 USDC per 1440 min
160
158
 
161
159
  Anything above the on-chain budget is declined before any money
162
160
  moves — it is not queued, and no one is asked to review it. If the
package/dist/cli.cjs CHANGED
@@ -238,6 +238,17 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
238
238
  );
239
239
  }
240
240
  }
241
+ var TASK_BUDGET_MIN_SIGNER_VERSION = "0.6.0-alpha.0";
242
+ var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
243
+ function taskSignerCompatibilityNotice() {
244
+ return {
245
+ task_sign_context_version: sdk.TASK_SIGN_CONTEXT_VERSION,
246
+ min_signer_version: TASK_BUDGET_MIN_SIGNER_VERSION,
247
+ signer_capability: SIGNER_CAPABILITY_KEY,
248
+ check: `Call next_tool with next_arguments EXACTLY as given \u2014 the signer fetches the exact bytes by task_budget_id. If haven_sign answers SIGNING_ERROR with a message starting "Pass payment_id (preferred for delegation-rail x402", the signer predates task budgets and signed nothing: first close this budget with haven_close_task_budget { task_budget_id } to release it, then update the connector by running \`${sdk.connectorUpgradeCommand()}\` and the repair line it prints, then open the budget again.`,
249
+ fallback: `haven_sign answered SIGNING_ERROR with a message starting "Pass payment_id (preferred for delegation-rail x402" \u2014 the signer predates task budgets and signed nothing: call haven_close_task_budget { task_budget_id } to release the pending budget, then update the connector by running \`${sdk.connectorUpgradeCommand()}\` and the repair line it prints, then open the budget again. There is no relay fallback for this signing context.`
250
+ };
251
+ }
241
252
  var headersSchema = v3.z.record(v3.z.string(), v3.z.string()).optional();
242
253
  var toolSchemas = {
243
254
  haven_send: {
@@ -245,16 +256,24 @@ var toolSchemas = {
245
256
  recipient: v3.z.string().min(1),
246
257
  amount: v3.z.string().min(1),
247
258
  idempotency_key: v3.z.string().optional(),
248
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
249
- idempotencyKey: v3.z.string().optional()
259
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
260
+ idempotencyKey: v3.z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key"),
261
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
262
+ task_budget_id: v3.z.string().min(1).optional(),
263
+ /**
264
+ * #3330: spend against an open sub-budget this agent HOLDS (it is the
265
+ * sub-agent B) instead of the agent's own budget. Mutually exclusive
266
+ * with task_budget_id — the backend refuses a body naming both.
267
+ */
268
+ sub_budget_id: v3.z.string().min(1).optional()
250
269
  },
251
270
  haven_pay_mcp_tool: {
252
271
  merchant_url: v3.z.string().url(),
253
272
  tool_name: v3.z.string().min(1),
254
273
  arguments: v3.z.record(v3.z.string(), v3.z.unknown()).optional(),
255
274
  idempotency_key: v3.z.string().optional(),
256
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
257
- idempotencyKey: v3.z.string().optional()
275
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
276
+ idempotencyKey: v3.z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key")
258
277
  },
259
278
  haven_quote_x402: {
260
279
  url: v3.z.string().url(),
@@ -262,14 +281,18 @@ var toolSchemas = {
262
281
  headers: headersSchema,
263
282
  body: v3.z.string().optional(),
264
283
  idempotency_key: v3.z.string().optional(),
265
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
266
- idempotencyKey: v3.z.string().optional()
284
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
285
+ idempotencyKey: v3.z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key")
267
286
  },
268
287
  haven_pay_x402_quote: {
269
288
  quote: v3.z.unknown(),
270
289
  idempotency_key: v3.z.string().optional(),
271
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
272
- idempotencyKey: v3.z.string().optional()
290
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
291
+ idempotencyKey: v3.z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key"),
292
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
293
+ task_budget_id: v3.z.string().min(1).optional(),
294
+ /** #3330: spend against an open sub-budget this agent holds (it is sub-agent B). Mutually exclusive with task_budget_id. */
295
+ sub_budget_id: v3.z.string().min(1).optional()
273
296
  },
274
297
  haven_pay_x402: {
275
298
  url: v3.z.string().url(),
@@ -277,8 +300,12 @@ var toolSchemas = {
277
300
  headers: headersSchema,
278
301
  body: v3.z.string().optional(),
279
302
  idempotency_key: v3.z.string().optional(),
280
- /** Legacy spelling, accepted during the #2366 window. Warns; do not use. */
281
- idempotencyKey: v3.z.string().optional()
303
+ /** REMOVED (#3411): declared only so the resolver can refuse it by name, never accepted. */
304
+ idempotencyKey: v3.z.string().optional().describe("REMOVED (#3411): refused \u2014 send idempotency_key"),
305
+ /** #3329: spend against an open task budget instead of the agent's period budget. */
306
+ task_budget_id: v3.z.string().min(1).optional(),
307
+ /** #3330: spend against an open sub-budget this agent holds (it is sub-agent B). Mutually exclusive with task_budget_id. */
308
+ sub_budget_id: v3.z.string().min(1).optional()
282
309
  },
283
310
  haven_resume_x402_payment: {
284
311
  payment_id: v3.z.string().optional(),
@@ -306,13 +333,65 @@ var toolSchemas = {
306
333
  haven_list_receipts: {
307
334
  limit: v3.z.number().int().min(1).max(100).optional(),
308
335
  /** #3128: the previous page's next_cursor (a receipt id). */
309
- cursor: v3.z.string().min(1).optional()
336
+ cursor: v3.z.string().min(1).optional(),
337
+ /** #3423: drop each row's payload echoes; the default shape is unchanged. */
338
+ compact: v3.z.boolean().optional().describe("true drops challengePayload, selectedPayment and protocolReceiptPayload from each row; omit for the full row.")
310
339
  },
311
340
  haven_verify_receipt: {
312
341
  receipt: v3.z.unknown()
342
+ },
343
+ // #3329: a budget for one task that ends by itself — a short-lived child of
344
+ // the agent's own budget, capped and time-boxed independently of the period
345
+ // reset.
346
+ haven_open_task_budget: {
347
+ max_amount_human: v3.z.string().min(1),
348
+ ttl_minutes: v3.z.number().int().min(1).max(1440),
349
+ recipient: v3.z.string().optional(),
350
+ label: v3.z.string().max(120).optional(),
351
+ token: v3.z.string().optional()
352
+ },
353
+ haven_close_task_budget: {
354
+ task_budget_id: v3.z.string().min(1)
355
+ },
356
+ // #3329: relays a signature from the local signer — either the open/close
357
+ // signature for a task budget, or (schema parity with the hosted surface)
358
+ // a direct-payment signature by payment_id. Exactly one of task_budget_id /
359
+ // payment_id, never both or neither.
360
+ haven_submit: {
361
+ task_budget_id: v3.z.string().min(1).optional(),
362
+ payment_id: v3.z.string().min(1).optional(),
363
+ signature: v3.z.string().regex(/^0x[0-9a-fA-F]+$/, "signature must be a 0x-prefixed hex string")
313
364
  }
314
365
  // #3101: keys survive on the type (see the hosted server's contracts.ts).
315
366
  };
367
+ var OPEN_TASK_BUDGET_DESCRIPTION = [
368
+ "Open a budget for one task that ends by itself: a spending cap, good for at most ttl_minutes,",
369
+ "reserved out of the agent's own budget and separate from its period reset.",
370
+ 'Pass max_amount_human (whole tokens, e.g. "5" for 5 USDC), ttl_minutes (1-1440), and optionally',
371
+ "recipient (pins every payment against this task budget to one address), label, and token",
372
+ '(defaults to USDC). Returns { task_budget, next_action: "sign", next_tool, next_arguments } \u2014',
373
+ "call next_tool with next_arguments EXACTLY as given to get a signature, then relay it with",
374
+ "haven_submit. Spending anything above the reserved amount, past the deadline, or to a",
375
+ "different recipient than the one pinned here is declined on the spot \u2014 nothing is queued.",
376
+ "A recipient pin is checked against where each payment first goes: haven_pay_x402 and",
377
+ "haven_pay_x402_quote first move funds into the agent's own wallet, so a budget pinned to a",
378
+ "merchant is declined there \u2014 pin only budgets meant for haven_send."
379
+ ].join(" ");
380
+ var CLOSE_TASK_BUDGET_DESCRIPTION = [
381
+ "End a task budget early, before its deadline, releasing whatever of its cap was unspent back",
382
+ "to the agent's own budget. Pass task_budget_id. If the budget was never signed (still pending),",
383
+ 'this ends it immediately with { task_budget, status: "closed" } and nothing was ever reserved',
384
+ 'on-chain. Otherwise it returns a signature request: { task_budget, next_action: "sign",',
385
+ "next_tool, next_arguments } \u2014 call next_tool with next_arguments EXACTLY as given, then relay",
386
+ "the signature with haven_submit. A task budget past its own deadline closes immediately, the",
387
+ "same as the pending case."
388
+ ].join(" ");
389
+ var SUBMIT_DESCRIPTION = [
390
+ "Relay a signature from the local signer. Pass exactly one of task_budget_id (from",
391
+ "haven_open_task_budget or haven_close_task_budget) or payment_id \u2014 never both, never neither \u2014",
392
+ "plus signature. For a task budget this opens or closes it on-chain and returns { task_budget,",
393
+ 'status }; a close in progress may return { task_budget, status: "closed", close_tx_hash }.'
394
+ ].join(" ");
316
395
  var toolDescriptions = {
317
396
  haven_send: sdk.composeDescription(sdk.toolDescriptions.send),
318
397
  haven_pay_mcp_tool: sdk.composeDescription(sdk.toolDescriptions.payMcpTool),
@@ -328,7 +407,10 @@ var toolDescriptions = {
328
407
  haven_discover_tools: sdk.composeDescription(sdk.toolDescriptions.discoverTools),
329
408
  haven_submit_catalog_entry: sdk.composeDescription(sdk.toolDescriptions.submitCatalogEntry),
330
409
  haven_list_receipts: sdk.composeDescription(sdk.toolDescriptions.listReceipts),
331
- haven_verify_receipt: sdk.composeDescription(sdk.toolDescriptions.verifyReceipt)
410
+ haven_verify_receipt: sdk.composeDescription(sdk.toolDescriptions.verifyReceipt),
411
+ haven_open_task_budget: OPEN_TASK_BUDGET_DESCRIPTION,
412
+ haven_close_task_budget: CLOSE_TASK_BUDGET_DESCRIPTION,
413
+ haven_submit: SUBMIT_DESCRIPTION
332
414
  };
333
415
  function createToolHandlers(haven) {
334
416
  return {
@@ -342,9 +424,15 @@ function createToolHandlers(haven) {
342
424
  token: args.asset,
343
425
  amount: args.amount,
344
426
  to: args.recipient,
345
- // #1207: was accepted by the schema but silently dropped — now
346
- // carried to the backend's replay contract.
347
- idempotencyKey: args.idempotencyKey
427
+ // #1207/#3411: was accepted by the schema but silently dropped — now
428
+ // carried to the backend's replay contract via the resolved carrier.
429
+ idempotencyKey: args.resolvedIdempotencyKey,
430
+ // #3329: spend against an open task budget instead of the
431
+ // agent's period budget, when the caller names one.
432
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {},
433
+ // #3330: spend through a sub-budget this agent holds (it is
434
+ // sub-agent B). Exactly one of the two ids, never both.
435
+ ...typeof args.sub_budget_id === "string" ? { subBudgetId: args.sub_budget_id } : {}
348
436
  });
349
437
  return {
350
438
  payment_id: result.paymentId,
@@ -382,7 +470,7 @@ function createToolHandlers(haven) {
382
470
  body: JSON.stringify(envelope)
383
471
  };
384
472
  let merchantUrl = args.merchant_url;
385
- const idempotencyKey = args.idempotencyKey;
473
+ const idempotencyKey = args.resolvedIdempotencyKey;
386
474
  const attempt = () => haven.fetch(merchantUrl, init, { idempotencyKey });
387
475
  let response = await attempt();
388
476
  if (!response.ok) {
@@ -416,7 +504,7 @@ function createToolHandlers(haven) {
416
504
  if ("success" in pf) return pf;
417
505
  const { args, warnings } = pf;
418
506
  try {
419
- const data = await haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey });
507
+ const data = await haven.quoteX402(args.url, requestInit(args), { idempotencyKey: args.resolvedIdempotencyKey });
420
508
  return warnings.length > 0 ? { success: true, data, warnings } : { success: true, data };
421
509
  } catch (err) {
422
510
  return normalizeError(err);
@@ -442,7 +530,11 @@ function createToolHandlers(haven) {
442
530
  );
443
531
  }
444
532
  return runTool(async () => {
445
- const response = await haven.payX402Quote(args.quote, { idempotencyKey: args.idempotencyKey });
533
+ const response = await haven.payX402Quote(args.quote, {
534
+ idempotencyKey: args.resolvedIdempotencyKey,
535
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {},
536
+ ...typeof args.sub_budget_id === "string" ? { subBudgetId: args.sub_budget_id } : {}
537
+ });
446
538
  return responsePayload(response);
447
539
  }, warnings);
448
540
  },
@@ -451,7 +543,11 @@ function createToolHandlers(haven) {
451
543
  if ("success" in pf) return pf;
452
544
  const { args, warnings } = pf;
453
545
  return runTool(async () => {
454
- const response = await haven.fetch(args.url, requestInit(args), { idempotencyKey: args.idempotencyKey });
546
+ const response = await haven.fetch(args.url, requestInit(args), {
547
+ idempotencyKey: args.resolvedIdempotencyKey,
548
+ ...typeof args.task_budget_id === "string" ? { taskBudgetId: args.task_budget_id } : {},
549
+ ...typeof args.sub_budget_id === "string" ? { subBudgetId: args.sub_budget_id } : {}
550
+ });
455
551
  return responsePayload(response);
456
552
  }, warnings);
457
553
  },
@@ -547,11 +643,82 @@ function createToolHandlers(haven) {
547
643
  },
548
644
  haven_list_receipts: async (input) => {
549
645
  const args = objectInput("haven_list_receipts", input);
550
- return runTool(async () => haven.listReceiptsPage({ limit: args.limit, cursor: args.cursor }));
646
+ return runTool(
647
+ async () => haven.listReceiptsPage({ limit: args.limit, cursor: args.cursor, compact: args.compact === true })
648
+ );
551
649
  },
552
650
  haven_verify_receipt: async (input) => {
553
651
  const args = objectInput("haven_verify_receipt", input);
554
652
  return runTool(async () => sdk.verifyPaymentReceipt(args.receipt));
653
+ },
654
+ haven_open_task_budget: async (input) => {
655
+ const args = objectInput("haven_open_task_budget", input);
656
+ return runTool(async () => {
657
+ const token = await resolveTaskBudgetToken(haven, typeof args.token === "string" ? args.token : void 0);
658
+ const maxAmountAtomic = humanToAtomicOrNull(args.max_amount_human, token.decimals);
659
+ if (maxAmountAtomic === null) {
660
+ throw new sdk.HavenApiError(
661
+ `max_amount_human ("${args.max_amount_human}") is not a valid decimal amount for ${token.symbol} (${token.decimals} decimal places). Nothing was reserved.`,
662
+ 400
663
+ );
664
+ }
665
+ const result = await haven.openTaskBudget({
666
+ tokenAddress: token.address,
667
+ maxAmountAtomic,
668
+ ttlSeconds: Number(args.ttl_minutes) * 60,
669
+ recipientAddress: typeof args.recipient === "string" ? args.recipient : void 0,
670
+ label: typeof args.label === "string" ? args.label : void 0
671
+ });
672
+ return {
673
+ task_budget: result.taskBudget,
674
+ next_action: "sign",
675
+ next_tool: "mcp__haven-signer__haven_sign",
676
+ next_arguments: { task_budget_id: result.taskBudget.id },
677
+ signer_compatibility: taskSignerCompatibilityNotice()
678
+ };
679
+ });
680
+ },
681
+ haven_close_task_budget: async (input) => {
682
+ const args = objectInput("haven_close_task_budget", input);
683
+ return runTool(async () => {
684
+ const result = await haven.closeTaskBudget(args.task_budget_id);
685
+ if (result.status === "closed") {
686
+ return { task_budget: result.taskBudget, status: "closed" };
687
+ }
688
+ return {
689
+ task_budget: result.taskBudget,
690
+ next_action: "sign",
691
+ next_tool: "mcp__haven-signer__haven_sign",
692
+ next_arguments: { task_budget_id: args.task_budget_id },
693
+ signer_compatibility: taskSignerCompatibilityNotice()
694
+ };
695
+ });
696
+ },
697
+ haven_submit: async (input) => {
698
+ const args = objectInput("haven_submit", input);
699
+ const hasTaskBudget = typeof args.task_budget_id === "string" && args.task_budget_id.length > 0;
700
+ const hasPayment = typeof args.payment_id === "string" && args.payment_id.length > 0;
701
+ if (hasTaskBudget === hasPayment) {
702
+ return {
703
+ success: false,
704
+ code: "INVALID_INPUT",
705
+ message: "haven_submit takes exactly one of task_budget_id or payment_id, never both or neither. Nothing was relayed."
706
+ };
707
+ }
708
+ return runTool(async () => {
709
+ if (hasTaskBudget) {
710
+ const result = await haven.submitTaskBudget(args.task_budget_id, args.signature);
711
+ return {
712
+ task_budget: result.taskBudget,
713
+ status: result.status,
714
+ ...result.closeTxHash !== void 0 ? { close_tx_hash: result.closeTxHash } : {}
715
+ };
716
+ }
717
+ throw new sdk.HavenApiError(
718
+ "haven_submit with payment_id is not supported on the local MCP surface: haven_send signs and submits a direct payment in one call, so there is no separate relay step. Use haven_submit with task_budget_id to relay a task-budget open/close signature.",
719
+ 400
720
+ );
721
+ });
555
722
  }
556
723
  };
557
724
  async function resumeState(args, rail) {
@@ -580,28 +747,20 @@ function preflight(name, input) {
580
747
  }
581
748
  const idem = resolveIdempotencyKey(args);
582
749
  if ("success" in idem) return idem;
583
- if (idem.key !== void 0) args = { ...args, idempotencyKey: idem.key };
750
+ if (idem.key !== void 0) args = { ...args, resolvedIdempotencyKey: idem.key };
584
751
  return { args, warnings: idem.warnings };
585
752
  }
586
753
  function resolveIdempotencyKey(args) {
587
754
  const modern = typeof args.idempotency_key === "string" ? args.idempotency_key : void 0;
588
755
  const legacy = typeof args.idempotencyKey === "string" ? args.idempotencyKey : void 0;
589
- if (modern !== void 0 && legacy !== void 0 && modern !== legacy) {
756
+ if (legacy !== void 0) {
590
757
  return {
591
758
  success: false,
592
- code: "AMBIGUOUS_IDEMPOTENCY_KEY",
593
- message: "Both idempotency_key and idempotencyKey were sent with different values. Nothing was contacted or spent. Send exactly one \u2014 idempotency_key is the current spelling; idempotencyKey is deprecated and will be removed."
759
+ code: "IDEMPOTENCY_KEY_RENAMED",
760
+ message: "idempotencyKey has been renamed to idempotency_key and is no longer accepted. Send idempotency_key instead. Nothing was contacted or spent."
594
761
  };
595
762
  }
596
763
  if (modern !== void 0) return { key: modern, warnings: [] };
597
- if (legacy !== void 0) {
598
- return {
599
- key: legacy,
600
- warnings: [
601
- "idempotencyKey is deprecated and will be removed in a future release of @haven_ai/mcp. Send idempotency_key instead \u2014 it is the spelling the hosted Haven MCP surface and every other Haven wire contract use."
602
- ]
603
- };
604
- }
605
764
  return { warnings: [] };
606
765
  }
607
766
  function buildMcpToolsCallEnvelope(toolName, args) {
@@ -643,6 +802,28 @@ async function responsePayload(response) {
643
802
  body: parseMaybeJson(text)
644
803
  };
645
804
  }
805
+ async function resolveTaskBudgetToken(haven, symbolOrAddress) {
806
+ const wanted = (symbolOrAddress ?? "USDC").toLowerCase();
807
+ const { allowances } = await haven.getAllowances();
808
+ const isAddress = /^0x[0-9a-fA-F]{40}$/.test(wanted);
809
+ const match = isAddress ? allowances.find((a) => a.tokenAddress.toLowerCase() === wanted) : allowances.find((a) => (a.tokenSymbol ?? "").toLowerCase() === wanted);
810
+ if (!match) {
811
+ const known = allowances.map((a) => `${a.tokenSymbol} (${a.tokenAddress})`);
812
+ throw new sdk.HavenApiError(
813
+ `token "${symbolOrAddress ?? "USDC"}" is not the symbol or address of any allowance this agent holds${known.length > 0 ? ` (it holds: ${known.join(", ")})` : " (it holds none)"}. Nothing was reserved.`,
814
+ 400
815
+ );
816
+ }
817
+ const resolved = sdk.resolveTokenFromAddress(match.tokenAddress);
818
+ return { address: match.tokenAddress, symbol: match.tokenSymbol, decimals: resolved?.decimals ?? 6 };
819
+ }
820
+ function humanToAtomicOrNull(human, decimals) {
821
+ if (!/^[0-9]+(\.[0-9]+)?$/.test(human)) return null;
822
+ const [whole, frac = ""] = human.split(".");
823
+ if (frac.length > decimals) return null;
824
+ const atomic = BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(frac.padEnd(decimals, "0") || "0");
825
+ return atomic.toString();
826
+ }
646
827
  function parseMaybeJson(text) {
647
828
  if (!text) return null;
648
829
  try {
@@ -810,7 +991,7 @@ function renderConsentBlock(input, hash) {
810
991
  lines.push("signed delegation, not by Haven):");
811
992
  for (const a of input.allowanceSummary) {
812
993
  const reset = a.resetMinutes ? ` per ${a.resetMinutes} min` : " (no reset)";
813
- lines.push(` \u2022 up to ${a.amount} ${a.token}${reset}`);
994
+ lines.push(` \u2022 up to ${describeBudgetAmount(a, input.chainId)}${reset}`);
814
995
  }
815
996
  }
816
997
  lines.push("");
@@ -907,7 +1088,14 @@ async function consentInputFromClient(haven, seed, toolNames) {
907
1088
  }
908
1089
  const liveAllowanceSummary = list.map((a) => ({
909
1090
  token: a.tokenSymbol ?? "UNKNOWN",
910
- amount: a.onchain?.amount ?? a.configuredAmount ?? "0",
1091
+ // #3410: `onchain.amount` is the atomic budget (`budget_atomic` on the
1092
+ // backend). The old `a.configuredAmount` fallback here was unreachable
1093
+ // — `onchain` is required in the SDK types and dereferenced
1094
+ // unconditionally, so a response without it throws into the seed path —
1095
+ // and it was the one HUMAN-decimal value in this chain; a human number
1096
+ // must never reach an atomic field. '0' keeps the (defensive) shape for
1097
+ // a malformed onchain object without inventing a unit.
1098
+ amount: a.onchain?.amount ?? "0",
911
1099
  resetMinutes: typeof a.onchain?.resetTimeMin === "number" ? a.onchain.resetTimeMin : typeof a.resetPeriodMin === "number" ? a.resetPeriodMin : null
912
1100
  }));
913
1101
  allowanceSummary = liveAllowanceSummary;
@@ -930,6 +1118,11 @@ function isAllowanceSummary(value) {
930
1118
  function derivePrefix(apiKey) {
931
1119
  return apiKey.slice(0, 12);
932
1120
  }
1121
+ function describeBudgetAmount(a, chainId) {
1122
+ const token = chainId === void 0 ? null : sdk.resolveTokenBySymbol(chainId, a.token);
1123
+ if (!token) return `${a.amount} ${a.token} (atomic units)`;
1124
+ return `${sdk.formatTokenAmount(a.amount, token.decimals)} ${a.token}`;
1125
+ }
933
1126
  function registeredToolNames() {
934
1127
  return Object.keys(toolSchemas);
935
1128
  }
@@ -954,7 +1147,7 @@ async function resolveHavenClient(options = {}) {
954
1147
  return { client, credentials };
955
1148
  }
956
1149
  var MCP_NAME = "@haven_ai/mcp";
957
- var MCP_VERSION = "0.5.0-alpha.1";
1150
+ var MCP_VERSION = "0.7.0-alpha.0";
958
1151
  var MCP_INSTRUCTIONS = [
959
1152
  "Haven local MCP server: signs in-process with the delegate key it holds on",
960
1153
  "this machine \u2014 the key never leaves this process. Call haven_get_agent",