@haven_ai/signer 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/dist/cli.cjs CHANGED
@@ -214,7 +214,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
214
214
  /**
215
215
  * #3303: on a 426 `client_outdated` refusal, the backend's update hint —
216
216
  * this signer is below the minimum the deployment set, and
217
- * `client_update.upgrade_command` is what updates it.
217
+ * `client_update.upgrade_command` starts the update (#3412: for the signer, the doctor, then its repair line).
218
218
  */
219
219
  client_update;
220
220
  constructor(message, code, refusal, paymentId, flow = "x402") {
@@ -231,7 +231,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
231
231
  step = signerRefusalStep({
232
232
  nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
233
233
  nextTool: null,
234
- nextToolOmittedReason: `this signer is below the minimum version Haven accepts \u2014 tell the user to run ${refusal.clientUpdate?.upgrade_command ?? "the connector again"}, restart the agent runtime, then retry the same haven_sign / haven_sign_x402 call`
234
+ nextToolOmittedReason: `this signer is below the minimum version Haven accepts \u2014 tell the user to run ${refusal.clientUpdate?.upgrade_command ?? edge.connectorUpgradeCommand()} and the repair line it prints, restart the agent runtime, then retry the same haven_sign / haven_sign_x402 call`
235
235
  });
236
236
  } else if (flow === "direct" && refusal?.httpStatus === 404) {
237
237
  this.fallback = "typed_data_b64";
@@ -256,6 +256,13 @@ var HavenSignContextError = class extends edge.HavenSigningError {
256
256
  nextTool: null,
257
257
  nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow"
258
258
  });
259
+ } else if (flow === "task") {
260
+ this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
261
+ step = signerRefusalStep({
262
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
263
+ nextTool: null,
264
+ nextToolOmittedReason: `re-check this task budget's status (id ${paymentId}) with the hosted Haven server before retrying`
265
+ });
259
266
  } else {
260
267
  this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
261
268
  step = signerRefusalStep({
@@ -264,6 +271,13 @@ var HavenSignContextError = class extends edge.HavenSigningError {
264
271
  nextArguments: { payment_id: paymentId }
265
272
  });
266
273
  }
274
+ } else if (flow === "task") {
275
+ this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
276
+ step = signerRefusalStep({
277
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
278
+ nextTool: null,
279
+ nextToolOmittedReason: "retry the same haven_sign call with the same task_budget_id"
280
+ });
267
281
  } else {
268
282
  this.fallback = "typed_data_b64";
269
283
  this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
@@ -432,8 +446,234 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
432
446
  clientUpdate: edge.readClientUpdate(body.client_update)
433
447
  };
434
448
  }
449
+ var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [edge.TASK_SIGN_CONTEXT_VERSION];
450
+ var SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS = [1];
451
+ async function fetchSubBudgetSignContext(identity, subBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
452
+ let response;
453
+ try {
454
+ response = await fetchImpl(
455
+ `${identity.apiUrl}/sub-budgets/${encodeURIComponent(subBudgetId)}/sign-context`,
456
+ {
457
+ headers: signContextHeaders(identity, clientIdentity),
458
+ signal: AbortSignal.timeout(timeoutMs)
459
+ }
460
+ );
461
+ } catch (err) {
462
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
463
+ throw new HavenSignContextError(
464
+ timedOut ? `Haven did not answer the sub-budget signing-context fetch for ${subBudgetId} within ${timeoutMs} ms. Retry.` : `Could not reach Haven to fetch the sub-budget signing context for ${subBudgetId}: ${err instanceof Error ? err.message : String(err)}.`,
465
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
466
+ void 0,
467
+ subBudgetId,
468
+ "task"
469
+ );
470
+ }
471
+ let body;
472
+ try {
473
+ body = await response.json();
474
+ } catch (err) {
475
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
476
+ throw new HavenSignContextError(
477
+ `Haven did not finish sending the sub-budget signing context for ${subBudgetId} within ${timeoutMs} ms. Retry.`,
478
+ "SIGN_CONTEXT_TIMEOUT",
479
+ void 0,
480
+ subBudgetId,
481
+ "task"
482
+ );
483
+ }
484
+ body = {};
485
+ }
486
+ if (!response.ok) {
487
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
488
+ throw new HavenSignContextError(
489
+ `Haven refused the sub-budget signing-context fetch for ${subBudgetId}: ${detail}`,
490
+ "SIGN_CONTEXT_REFUSED",
491
+ refusalOf(response.status, body),
492
+ subBudgetId,
493
+ "task"
494
+ );
495
+ }
496
+ const purpose = body.purpose;
497
+ const typedData = body.typed_data;
498
+ const version = body.sub_budget_sign_context_version;
499
+ const expected = body.expected;
500
+ const malformed = (detail) => {
501
+ throw new HavenSignContextError(
502
+ `The Haven sub-budget sign-context response for ${subBudgetId} is malformed: ${detail}.`,
503
+ "SIGN_CONTEXT_MALFORMED",
504
+ void 0,
505
+ subBudgetId,
506
+ "task"
507
+ );
508
+ };
509
+ if (typeof version !== "number" || !SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS.includes(version)) {
510
+ throw new HavenSignContextError(
511
+ `The Haven sub-budget sign-context response is version ${String(version)}, which this signer does not support (supported: ${SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.`,
512
+ "SIGN_CONTEXT_MALFORMED",
513
+ void 0,
514
+ subBudgetId,
515
+ "task"
516
+ );
517
+ }
518
+ if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
519
+ if (!expected || typeof expected !== "object") return malformed("missing expected");
520
+ const e = expected;
521
+ if (purpose === "open") {
522
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.token_address !== "string" || typeof e.period_amount_atomic !== "string" || typeof e.period_duration_seconds !== "number" || typeof e.start_date !== "number" || e.recipient_address !== null && typeof e.recipient_address !== "string" || typeof e.expires_at !== "number" || typeof e.parent_delegation_hash !== "string" || typeof e.child_delegate_account !== "string") {
523
+ return malformed("expected is missing a required field for purpose open");
524
+ }
525
+ return {
526
+ purpose: "open",
527
+ subBudgetId: String(body.sub_budget_id ?? subBudgetId),
528
+ typedData,
529
+ expected: {
530
+ delegateAccount: e.delegate_account,
531
+ chainId: e.chain_id,
532
+ tokenAddress: e.token_address,
533
+ periodAmountAtomic: e.period_amount_atomic,
534
+ periodDurationSeconds: e.period_duration_seconds,
535
+ startDate: e.start_date,
536
+ recipientAddress: e.recipient_address ?? null,
537
+ expiresAt: e.expires_at,
538
+ parentDelegationHash: e.parent_delegation_hash,
539
+ childDelegateAccount: e.child_delegate_account
540
+ }
541
+ };
542
+ }
543
+ if (purpose === "close") {
544
+ const userOpHash = body.user_op_hash;
545
+ if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
546
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
547
+ return malformed("expected is missing a required field for purpose close");
548
+ }
549
+ return {
550
+ purpose: "close",
551
+ subBudgetId: String(body.sub_budget_id ?? subBudgetId),
552
+ typedData,
553
+ userOpHash,
554
+ expected: {
555
+ delegateAccount: e.delegate_account,
556
+ chainId: e.chain_id,
557
+ delegationHash: e.delegation_hash
558
+ }
559
+ };
560
+ }
561
+ return malformed(`unknown purpose '${String(purpose)}'`);
562
+ }
563
+ async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
564
+ let response;
565
+ try {
566
+ response = await fetchImpl(
567
+ `${identity.apiUrl}/task-budgets/${encodeURIComponent(taskBudgetId)}/sign-context`,
568
+ {
569
+ headers: signContextHeaders(identity, clientIdentity),
570
+ signal: AbortSignal.timeout(timeoutMs)
571
+ }
572
+ );
573
+ } catch (err) {
574
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
575
+ throw new HavenSignContextError(
576
+ timedOut ? `Haven did not answer the task-budget signing-context fetch for ${taskBudgetId} within ${timeoutMs} ms. Retry.` : `Could not reach Haven to fetch the task-budget signing context for ${taskBudgetId}: ${err instanceof Error ? err.message : String(err)}.`,
577
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
578
+ void 0,
579
+ taskBudgetId,
580
+ "task"
581
+ );
582
+ }
583
+ let body;
584
+ try {
585
+ body = await response.json();
586
+ } catch (err) {
587
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
588
+ throw new HavenSignContextError(
589
+ `Haven did not finish sending the task-budget signing context for ${taskBudgetId} within ${timeoutMs} ms. Retry.`,
590
+ "SIGN_CONTEXT_TIMEOUT",
591
+ void 0,
592
+ taskBudgetId,
593
+ "task"
594
+ );
595
+ }
596
+ body = {};
597
+ }
598
+ if (!response.ok) {
599
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
600
+ throw new HavenSignContextError(
601
+ `Haven refused the task-budget signing-context fetch for ${taskBudgetId}: ${detail}`,
602
+ "SIGN_CONTEXT_REFUSED",
603
+ refusalOf(response.status, body),
604
+ taskBudgetId,
605
+ "task"
606
+ );
607
+ }
608
+ const purpose = body.purpose;
609
+ const typedData = body.typed_data;
610
+ const version = body.task_sign_context_version;
611
+ const expected = body.expected;
612
+ const malformed = (detail) => {
613
+ throw new HavenSignContextError(
614
+ `The Haven task-budget sign-context response for ${taskBudgetId} is malformed: ${detail}.`,
615
+ "SIGN_CONTEXT_MALFORMED",
616
+ void 0,
617
+ taskBudgetId,
618
+ "task"
619
+ );
620
+ };
621
+ if (typeof version !== "number" || !SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS.includes(version)) {
622
+ throw new HavenSignContextError(
623
+ `The Haven task-budget sign-context response is version ${String(version)}, which this signer does not support (supported: ${SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.`,
624
+ "SIGN_CONTEXT_MALFORMED",
625
+ void 0,
626
+ taskBudgetId,
627
+ "task"
628
+ );
629
+ }
630
+ if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
631
+ if (!expected || typeof expected !== "object") return malformed("missing expected");
632
+ const e = expected;
633
+ if (purpose === "open") {
634
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.token_address !== "string" || typeof e.max_amount_atomic !== "string" || e.recipient_address !== null && typeof e.recipient_address !== "string" || typeof e.expires_at !== "number" || typeof e.parent_delegation_hash !== "string") {
635
+ return malformed("expected is missing a required field for purpose open");
636
+ }
637
+ return {
638
+ purpose: "open",
639
+ taskBudgetId: String(body.task_budget_id ?? taskBudgetId),
640
+ typedData,
641
+ expected: {
642
+ delegateAccount: e.delegate_account,
643
+ chainId: e.chain_id,
644
+ tokenAddress: e.token_address,
645
+ maxAmountAtomic: e.max_amount_atomic,
646
+ recipientAddress: e.recipient_address ?? null,
647
+ expiresAt: e.expires_at,
648
+ parentDelegationHash: e.parent_delegation_hash
649
+ }
650
+ };
651
+ }
652
+ if (purpose === "close") {
653
+ const userOpHash = body.user_op_hash;
654
+ if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
655
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
656
+ return malformed("expected is missing a required field for purpose close");
657
+ }
658
+ return {
659
+ purpose: "close",
660
+ taskBudgetId: String(body.task_budget_id ?? taskBudgetId),
661
+ typedData,
662
+ userOpHash,
663
+ expected: {
664
+ delegateAccount: e.delegate_account,
665
+ chainId: e.chain_id,
666
+ delegationHash: e.delegation_hash
667
+ }
668
+ };
669
+ }
670
+ return malformed(`unknown purpose '${String(purpose)}'`);
671
+ }
435
672
 
436
673
  // src/tools.ts
674
+ var SIGNER_NAME = "@haven_ai/signer";
675
+ var SIGNER_VERSION = "0.7.0-alpha.0";
676
+ var UNSUPPORTED_ARGUMENT_MARKER = "HavenSignerUnsupportedArgument:";
437
677
  var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
438
678
  var HavenUserOpBindingRefusedError = class extends edge.HavenSigningError {
439
679
  next_action = edge.AgentPaymentNextAction.StopAndTellUser;
@@ -554,7 +794,20 @@ var toolSchemas = {
554
794
  // Bounded: a realistic redemption payload is ~10KB encoded; 256KB is
555
795
  // generous headroom while keeping the offline signer from materializing
556
796
  // arbitrarily large caller input.
557
- typed_data_b64: v3.z.string().min(1).max(262144).optional()
797
+ typed_data_b64: v3.z.string().min(1).max(262144).optional(),
798
+ // #3329: sign a task budget's own child delegation (purpose `open`) or
799
+ // its early-close UserOp (purpose `close`) — the signer fetches the
800
+ // exact typed data and expectation itself, exactly like payment_id.
801
+ // Mutually exclusive with payment_id / payload_hash (checked in the
802
+ // handler, since Zod's object-level refine on a raw shape used by
803
+ // several call sites is not the shape this schema is composed as).
804
+ task_budget_id: v3.z.string().min(1).optional(),
805
+ // #3330: sign a sub-budget's own child delegation or its early-close
806
+ // UserOp — same fetch-verify-sign channel as task_budget_id, against
807
+ // `GET /sub-budgets/:id/sign-context`. Mutually exclusive with
808
+ // payment_id / payload_hash AND with task_budget_id (checked in the
809
+ // handler).
810
+ sub_budget_id: v3.z.string().min(1).optional()
558
811
  },
559
812
  haven_x402_sign_header: {
560
813
  // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
@@ -604,7 +857,11 @@ var SIGN_DESCRIPTION = [
604
857
  "intent against a Haven-signed context \u2014 never an arbitrary typed_data payload, however it is shaped.",
605
858
  "Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
606
859
  "to mcp__haven-signer__haven_x402_sign_header. A bare payload_hash with no payment_id, typed_data",
607
- "or x402_expected is REFUSED (BARE_HASH_REFUSED): a hash carries nothing this signer can verify."
860
+ "or x402_expected is REFUSED (BARE_HASH_REFUSED): a hash carries nothing this signer can verify.",
861
+ "TASK BUDGETS (#3329): pass task_budget_id ALONE \u2014 mutually exclusive with payment_id and",
862
+ "payload_hash \u2014 to sign a task budget\u2019s own child delegation (opening it) or its early-close",
863
+ "UserOp; this signer fetches the exact typed data itself. Result then carries next_tool_name",
864
+ "(haven_submit) and next_arguments { task_budget_id, signature } instead of x402_binding."
608
865
  ].join(" ");
609
866
  var X402_SIGN_HEADER_DESCRIPTION = [
610
867
  "Build and sign the EIP-3009 merchant payment header for the merchant leg of an x402 payment.",
@@ -768,9 +1025,172 @@ function createToolHandlers(signer, options = {}) {
768
1025
  throw err;
769
1026
  }
770
1027
  }
1028
+ async function signTaskBudget(taskBudgetId) {
1029
+ const identity = await options.signContext?.loadIdentity() ?? null;
1030
+ if (!identity) {
1031
+ throw new edge.HavenSigningError(
1032
+ `task_budget_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${edge.connectorRerunCommand()}\` to restore it.`
1033
+ );
1034
+ }
1035
+ const ctx = await fetchTaskBudgetSignContext(
1036
+ identity,
1037
+ taskBudgetId,
1038
+ options.signContext?.fetchImpl,
1039
+ void 0,
1040
+ options.signContext?.clientIdentity
1041
+ );
1042
+ let signature;
1043
+ if (ctx.purpose === "open") {
1044
+ try {
1045
+ edge.assertOwnTaskChild(
1046
+ ctx.typedData,
1047
+ {
1048
+ chainId: ctx.expected.chainId,
1049
+ tokenAddress: ctx.expected.tokenAddress,
1050
+ maxAmountAtomic: ctx.expected.maxAmountAtomic,
1051
+ recipientAddress: ctx.expected.recipientAddress,
1052
+ expiresAt: ctx.expected.expiresAt,
1053
+ parentDelegationHash: ctx.expected.parentDelegationHash
1054
+ },
1055
+ signer.delegateAddress
1056
+ );
1057
+ } catch (err) {
1058
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1059
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1060
+ }
1061
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1062
+ } else {
1063
+ try {
1064
+ edge.assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
1065
+ } catch (err) {
1066
+ if (err instanceof edge.HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
1067
+ throw err;
1068
+ }
1069
+ try {
1070
+ edge.assertOwnTaskBudgetCloseUserOp(
1071
+ ctx.typedData,
1072
+ { delegationHash: ctx.expected.delegationHash },
1073
+ signer.delegateAddress
1074
+ );
1075
+ } catch (err) {
1076
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1077
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1078
+ }
1079
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1080
+ }
1081
+ await auditSigning("haven_sign", viem.hashTypedData(ctx.typedData));
1082
+ return {
1083
+ signature,
1084
+ task_budget_id: ctx.taskBudgetId,
1085
+ purpose: ctx.purpose,
1086
+ next_tool_name: "haven_submit",
1087
+ next_tool_server_role: "hosted",
1088
+ next_arguments: { task_budget_id: ctx.taskBudgetId, signature }
1089
+ };
1090
+ }
1091
+ async function signSubBudget(subBudgetId) {
1092
+ const identity = await options.signContext?.loadIdentity() ?? null;
1093
+ if (!identity) {
1094
+ throw new edge.HavenSigningError(
1095
+ `sub_budget_id signing needs the agent identity (identity.json next to the signer credentials), which this signer could not load. Re-run \`${edge.connectorRerunCommand()}\` to restore it.`
1096
+ );
1097
+ }
1098
+ const ctx = await fetchSubBudgetSignContext(
1099
+ identity,
1100
+ subBudgetId,
1101
+ options.signContext?.fetchImpl,
1102
+ void 0,
1103
+ options.signContext?.clientIdentity
1104
+ );
1105
+ let signature;
1106
+ if (ctx.purpose === "open") {
1107
+ try {
1108
+ edge.assertOwnSubBudgetChild(
1109
+ ctx.typedData,
1110
+ {
1111
+ chainId: ctx.expected.chainId,
1112
+ tokenAddress: ctx.expected.tokenAddress,
1113
+ periodAmountAtomic: ctx.expected.periodAmountAtomic,
1114
+ periodDurationSeconds: ctx.expected.periodDurationSeconds,
1115
+ startDate: ctx.expected.startDate,
1116
+ recipientAddress: ctx.expected.recipientAddress,
1117
+ expiresAt: ctx.expected.expiresAt,
1118
+ parentDelegationHash: ctx.expected.parentDelegationHash,
1119
+ delegateAccount: ctx.expected.delegateAccount,
1120
+ childDelegateAccount: ctx.expected.childDelegateAccount
1121
+ },
1122
+ signer.delegateAddress
1123
+ );
1124
+ } catch (err) {
1125
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1126
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1127
+ }
1128
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1129
+ } else {
1130
+ try {
1131
+ edge.assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
1132
+ } catch (err) {
1133
+ if (err instanceof edge.HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
1134
+ throw err;
1135
+ }
1136
+ try {
1137
+ edge.assertOwnSubBudgetCloseUserOp(
1138
+ ctx.typedData,
1139
+ { delegationHash: ctx.expected.delegationHash },
1140
+ signer.delegateAddress
1141
+ );
1142
+ } catch (err) {
1143
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1144
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1145
+ }
1146
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1147
+ }
1148
+ await auditSigning("haven_sign", viem.hashTypedData(ctx.typedData));
1149
+ return {
1150
+ signature,
1151
+ sub_budget_id: ctx.subBudgetId,
1152
+ purpose: ctx.purpose,
1153
+ next_tool_name: "haven_submit",
1154
+ next_tool_server_role: "hosted",
1155
+ next_arguments: { sub_budget_id: ctx.subBudgetId, signature }
1156
+ };
1157
+ }
771
1158
  return {
772
1159
  haven_sign: async (input) => runTool(async () => {
773
- const args = parse("haven_sign", coerceX402Expected(input));
1160
+ const args = parseStrictFor("haven_sign", coerceX402Expected(input));
1161
+ if (args.task_budget_id) {
1162
+ if (args.payment_id || args.payload_hash) {
1163
+ throw new v3.z.ZodError([
1164
+ {
1165
+ code: v3.z.ZodIssueCode.custom,
1166
+ path: ["task_budget_id"],
1167
+ message: "task_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
1168
+ }
1169
+ ]);
1170
+ }
1171
+ if (args.sub_budget_id) {
1172
+ throw new v3.z.ZodError([
1173
+ {
1174
+ code: v3.z.ZodIssueCode.custom,
1175
+ path: ["sub_budget_id"],
1176
+ message: "task_budget_id is mutually exclusive with sub_budget_id \u2014 pass exactly one."
1177
+ }
1178
+ ]);
1179
+ }
1180
+ return signTaskBudget(args.task_budget_id);
1181
+ }
1182
+ if (args.sub_budget_id) {
1183
+ if (args.payment_id || args.payload_hash) {
1184
+ throw new v3.z.ZodError([
1185
+ {
1186
+ code: v3.z.ZodIssueCode.custom,
1187
+ path: ["sub_budget_id"],
1188
+ message: "sub_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
1189
+ }
1190
+ ]);
1191
+ }
1192
+ return signSubBudget(args.sub_budget_id);
1193
+ }
774
1194
  const resolved = await resolveSignContext(args, { allowDirectFallback: true });
775
1195
  const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
776
1196
  const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
@@ -813,7 +1233,7 @@ function createToolHandlers(signer, options = {}) {
813
1233
  return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
814
1234
  }),
815
1235
  haven_x402_sign_header: async (input) => runTool(async () => {
816
- const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
1236
+ const args = parseStrictFor("haven_x402_sign_header", coercePaymentRequired(input));
817
1237
  const result = await signer.buildX402PaymentHeader(
818
1238
  args.payment_required,
819
1239
  args.x402_binding
@@ -825,7 +1245,7 @@ function createToolHandlers(signer, options = {}) {
825
1245
  return { payment_header: result.paymentHeader, accepted: result.accepted };
826
1246
  }),
827
1247
  haven_sign_x402: async (input) => runTool(async () => {
828
- const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
1248
+ const args = parseStrictFor("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
829
1249
  const resolved = await resolveSignContext(args, { allowDirectFallback: false });
830
1250
  const fetched = resolved?.kind === "x402" ? resolved.ctx : void 0;
831
1251
  const payloadHash = fetched?.payloadHash ?? args.payload_hash;
@@ -861,7 +1281,7 @@ function createToolHandlers(signer, options = {}) {
861
1281
  };
862
1282
  }),
863
1283
  haven_sign_sweep_delegate: async (input) => runTool(async () => {
864
- const args = parse("haven_sign_sweep_delegate", input);
1284
+ const args = parseStrictFor("haven_sign_sweep_delegate", input);
865
1285
  const result = await signer.signSweepAuthorization({
866
1286
  authorization: args.authorization,
867
1287
  expectedAuth: args.expected_auth,
@@ -894,8 +1314,18 @@ function createToolHandlers(signer, options = {}) {
894
1314
  }
895
1315
  }
896
1316
  }
897
- function parse(name, input) {
898
- return v3.z.object(toolSchemas[name]).parse(input ?? {});
1317
+ function parseStrictFor(name, input) {
1318
+ const result = v3.z.object(toolSchemas[name]).strict().safeParse(input ?? {});
1319
+ if (result.success) return result.data;
1320
+ const topLevelUnknown = result.error.issues.flatMap(
1321
+ (issue) => issue.code === "unrecognized_keys" && issue.path.length === 0 ? issue.keys : []
1322
+ );
1323
+ if (topLevelUnknown.length === 0) throw result.error;
1324
+ const unknown = [...new Set(topLevelUnknown)];
1325
+ const err = new Error(
1326
+ `${UNSUPPORTED_ARGUMENT_MARKER} ${name} does not accept ${unknown.map((k) => `"${k}"`).join(", ")} \u2014 this signer predates that argument form. Nothing was signed or fetched.`
1327
+ );
1328
+ throw err;
899
1329
  }
900
1330
  function coerceX402Expected(input) {
901
1331
  if (!input || typeof input !== "object") return input;
@@ -934,6 +1364,25 @@ async function runTool(fn) {
934
1364
  }
935
1365
  }
936
1366
  function normalizeError(err) {
1367
+ if (err instanceof Error && err.message.startsWith(UNSUPPORTED_ARGUMENT_MARKER)) {
1368
+ const unknown = [...err.message.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
1369
+ return {
1370
+ success: false,
1371
+ code: "UNSUPPORTED_ARGUMENT",
1372
+ message: err.message.replace(`${UNSUPPORTED_ARGUMENT_MARKER} `, ""),
1373
+ unknown_arguments: unknown,
1374
+ signer_version: SIGNER_VERSION,
1375
+ fallback: edge.connectorUpgradeCommand(),
1376
+ next_action: edge.AgentPaymentNextAction.StopAndTellUser,
1377
+ ...nextStepWireFields(
1378
+ signerRefusalStep({
1379
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
1380
+ nextTool: null,
1381
+ nextToolOmittedReason: `update @haven_ai/signer: run ${edge.connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
1382
+ })
1383
+ )
1384
+ };
1385
+ }
937
1386
  if (err instanceof v3.z.ZodError) {
938
1387
  return {
939
1388
  success: false,
@@ -955,7 +1404,9 @@ function normalizeError(err) {
955
1404
  ...nextStepWireFields(signerRefusalStep({
956
1405
  nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
957
1406
  nextTool: null,
958
- nextToolOmittedReason: "update @haven_ai/signer by re-running the connector, then repeat the same call"
1407
+ // #3412: the upgrade command, not "re-run the connector" — a bare re-run
1408
+ // stops at "Missing --setup" on an existing install.
1409
+ nextToolOmittedReason: `update @haven_ai/signer: run ${edge.connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
959
1410
  }))
960
1411
  };
961
1412
  }
@@ -1041,7 +1492,7 @@ function normalizeError(err) {
1041
1492
 
1042
1493
  // src/consent.ts
1043
1494
  var toolSummaries = {
1044
- haven_sign: "signs one Haven-prepared payment (a direct-payment UserOp from your own account whose only call redeems a delegation made to it, or an x402 funding intent against a Haven-signed context)",
1495
+ haven_sign: "signs a Haven-prepared payment, task-budget open, or task-budget close (a UserOp from your own account whose only call redeems a delegation made to it, directly or through a self-delegated task-budget child, or an x402 funding intent against a Haven-signed context)",
1045
1496
  haven_x402_sign_header: "builds and signs the EIP-3009 merchant payment header for an x402 payment",
1046
1497
  haven_sign_x402: "the two above in one call: funding payload + merchant header",
1047
1498
  haven_sign_sweep_delegate: "signs a Haven-prepared gasless sweep of stranded funds back to your own account"
@@ -1472,7 +1923,7 @@ function assertSupportedBindingVersion(received, supported, context) {
1472
1923
  const highest = Math.max(...supported);
1473
1924
  const outOfDate = received > highest;
1474
1925
  const code = context === "x402 expected context" ? edge.SignerRefusalCode.UnsupportedExpectedContextVersion : edge.SignerRefusalCode.UnsupportedSweepBindingVersion;
1475
- const ceiling = outOfDate ? `This signer is out of date: it supports ${context} versions up to ${highest}, and Haven sent version ${received}. Update @haven_ai/signer \u2014 rerun the Haven connector (\`${edge.connectorRerunCommand()}\`), which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
1926
+ const ceiling = outOfDate ? `This signer is out of date: it supports ${context} versions up to ${highest}, and Haven sent version ${received}. Update @haven_ai/signer \u2014 run \`${edge.connectorUpgradeCommand()}\`, then the repair line it prints, which reinstalls the pinned MCP runtime.` : `Unsupported ${context} version ${received}: this signer supports ${supported.join(", ")}.`;
1476
1927
  const fallback = outOfDate ? edge.SIGNER_UPDATE_FALLBACK : `This ${context} version (${received}) is older than what this signer enforces (${supported.join(", ")}) \u2014 updating @haven_ai/signer will not restore it. Nothing was signed or spent; stop and tell the user rather than retrying.`;
1477
1928
  throw new edge.HavenUnsupportedSignerVersionError(
1478
1929
  `${ceiling} Nothing was signed. Do not rewrite the version field to a supported value: it is part of the Haven-signed binding message, so changing it invalidates the signature and would misrepresent what Haven declared.`,
@@ -1573,7 +2024,8 @@ function signerCompatibility() {
1573
2024
  return {
1574
2025
  x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
1575
2026
  sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS],
1576
- direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS]
2027
+ direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS],
2028
+ task_sign_context_versions: [...SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS]
1577
2029
  };
1578
2030
  }
1579
2031
  function signerCapabilityAdvertisement() {
@@ -1592,12 +2044,13 @@ function signerInstructions() {
1592
2044
  `- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
1593
2045
  `- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
1594
2046
  `- direct-payment (haven_send / haven_pay) sign-context versions supported: ${compatibility.direct_sign_context_versions.join(", ")} \u2014 pass payment_id alone to haven_sign; this signer fetches the exact bytes`,
2047
+ `- task-budget sign-context versions supported: ${compatibility.task_sign_context_versions.join(", ")} \u2014 pass task_budget_id alone to haven_sign; this signer fetches the exact bytes`,
1595
2048
  "",
1596
2049
  "Haven quote and prepare results report the expected-context version they will emit",
1597
2050
  "(signer_compatibility.x402_expected_context_version). If that version is not in the list",
1598
2051
  "above, this signer is out of date: STOP before signing, and tell the user to update",
1599
- `@haven_ai/signer by rerunning \`${edge.connectorRerunCommand()}\`, which reinstalls the pinned`,
1600
- "MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
2052
+ `@haven_ai/signer: run \`${edge.connectorUpgradeCommand()}\`, then the repair line it prints,`,
2053
+ "which reinstalls the pinned MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
1601
2054
  "Haven-signed binding message, so changing it invalidates the signature.",
1602
2055
  "",
1603
2056
  "A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
@@ -1605,7 +2058,13 @@ function signerInstructions() {
1605
2058
  "fallback fields alongside the message, so you can branch on it directly. Every signer",
1606
2059
  "refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
1607
2060
  "hosted tool follows (resolve the role against your own server names), else",
1608
- "next_tool_omitted_reason saying why not."
2061
+ "next_tool_omitted_reason saying why not.",
2062
+ "",
2063
+ "An undeclared top-level argument is refused, not stripped: haven_sign answers",
2064
+ "UNSUPPORTED_ARGUMENT with unknown_arguments, signer_version and fallback (the update",
2065
+ "command) instead of dropping the key and answering the generic signing error \u2014 if you see",
2066
+ "that refusal, this signer predates the argument form you sent. Update the signer; nothing",
2067
+ "was signed, fetched or audited."
1609
2068
  ].join("\n");
1610
2069
  }
1611
2070
  async function loadSignerCredentials(path = process.env.HAVEN_CREDENTIALS) {
@@ -1708,8 +2167,6 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1708
2167
  }
1709
2168
 
1710
2169
  // src/server.ts
1711
- var SIGNER_NAME = "@haven_ai/signer";
1712
- var SIGNER_VERSION = "0.5.0-alpha.1";
1713
2170
  async function resolveSignerRuntime(options = {}) {
1714
2171
  assertSupportedNodeVersion(options.nodeVersion);
1715
2172
  if (options.delegateKey) {
@@ -1760,12 +2217,14 @@ function buildSignerMcpServer(signer, options = {}) {
1760
2217
  clientIdentity: edge.havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
1761
2218
  }
1762
2219
  });
1763
- const registerTool = server.tool.bind(server);
2220
+ const registerTool = server.registerTool.bind(server);
1764
2221
  for (const name of Object.keys(toolSchemas)) {
1765
2222
  registerTool(
1766
2223
  name,
1767
- toolDescriptions[name],
1768
- toolSchemas[name],
2224
+ {
2225
+ description: toolDescriptions[name],
2226
+ inputSchema: zod.z.object(toolSchemas[name]).passthrough()
2227
+ },
1769
2228
  async (args) => toMcpResult(await handlers[name](args))
1770
2229
  );
1771
2230
  }