@haven_ai/signer 0.6.0-alpha.0 → 0.8.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/index.cjs CHANGED
@@ -255,7 +255,7 @@ function assertSupportedBindingVersion(received, supported, context) {
255
255
  const highest = Math.max(...supported);
256
256
  const outOfDate = received > highest;
257
257
  const code = context === "x402 expected context" ? edge.SignerRefusalCode.UnsupportedExpectedContextVersion : edge.SignerRefusalCode.UnsupportedSweepBindingVersion;
258
- 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(", ")}.`;
258
+ 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(", ")}.`;
259
259
  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.`;
260
260
  throw new edge.HavenUnsupportedSignerVersionError(
261
261
  `${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.`,
@@ -350,7 +350,14 @@ function sameAddress(a, b) {
350
350
  return a.toLowerCase() === b.toLowerCase();
351
351
  }
352
352
  var SIGNER_HOSTED_HANDOFF_SHAPES = {
353
- haven_get_payment_status: { payment_id: zod.z.string().min(1) }
353
+ haven_get_payment_status: { payment_id: zod.z.string().min(1) },
354
+ // #3506: `haven_sign` hands a signed budget row to the hosted `haven_submit`
355
+ // (`signTaskBudget` / `signSubBudget` in tools.ts emit exactly these
356
+ // `next_arguments`). One hosted tool, two argument shapes, so each shape is
357
+ // its own entry keyed `<hosted tool>#<variant>`; the parity test reads the
358
+ // part before `#` as the hosted tool it checks the entry against.
359
+ "haven_submit#task_budget": { task_budget_id: zod.z.string().min(1), signature: zod.z.string().regex(/^0x[0-9a-fA-F]+$/) },
360
+ "haven_submit#sub_budget": { sub_budget_id: zod.z.string().min(1), signature: zod.z.string().regex(/^0x[0-9a-fA-F]+$/) }
354
361
  };
355
362
  function target(shape) {
356
363
  const schema = zod.z.object(shape).strict();
@@ -381,7 +388,8 @@ function nextStepWireFields(step) {
381
388
  }
382
389
 
383
390
  // src/sign-context.ts
384
- var DIRECT_RELAY_FALLBACK = "call haven_sign with payload_hash and typed_data_b64 from the haven_send / haven_pay result, passed through unchanged";
391
+ var DIRECT_RELAY_FALLBACK = "re-run whichever of haven_send / haven_pay you called, with the SAME idempotency_key (echoed on its result) plus include_signing_payload: true, then call haven_sign with payload_hash and typed_data_b64 from THAT re-run's result, passed through unchanged";
392
+ var DIRECT_404_FALLBACK = "either the payment_id is not from this agent\u2019s own POST /payments call, or this Haven backend predates #3271 (which also predates #3495, so its result already carries payload_hash and typed_data_b64 unconditionally): pass those fields from the ORIGINAL haven_send / haven_pay result, unchanged, if it already carries them \u2014 or, on a newer server, re-run haven_send / haven_pay with the SAME idempotency_key plus include_signing_payload: true and pass them from THAT result instead";
385
393
  var HavenSignContextError = class extends edge.HavenSigningError {
386
394
  /**
387
395
  * #3001 / #3010 review: the recovery path, when signing OTHER bytes is
@@ -434,7 +442,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
434
442
  /**
435
443
  * #3303: on a 426 `client_outdated` refusal, the backend's update hint —
436
444
  * this signer is below the minimum the deployment set, and
437
- * `client_update.upgrade_command` is what updates it.
445
+ * `client_update.upgrade_command` starts the update (#3412: for the signer, the doctor, then its repair line).
438
446
  */
439
447
  client_update;
440
448
  constructor(message, code, refusal, paymentId, flow = "x402") {
@@ -451,7 +459,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
451
459
  step = signerRefusalStep({
452
460
  nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
453
461
  nextTool: null,
454
- 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`
462
+ 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`
455
463
  });
456
464
  } else if (flow === "direct" && refusal?.httpStatus === 404) {
457
465
  this.fallback = "typed_data_b64";
@@ -459,7 +467,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
459
467
  step = signerRefusalStep({
460
468
  nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
461
469
  nextTool: null,
462
- nextToolOmittedReason: DIRECT_RELAY_FALLBACK
470
+ nextToolOmittedReason: DIRECT_404_FALLBACK
463
471
  });
464
472
  } else if (flow === "direct" && refusal?.errorCode === "expired") {
465
473
  this.next_action = edge.AgentPaymentNextAction.PaymentWindowExpired;
@@ -614,7 +622,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
614
622
  } catch (err) {
615
623
  const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
616
624
  throw new HavenSignContextError(
617
- timedOut ? `Haven did not answer the direct-payment signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the payment result instead.` : `Could not reach Haven to fetch the direct-payment signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or pass typed_data_b64 from the payment result instead.`,
625
+ timedOut ? `Haven did not answer the direct-payment signing-context fetch for ${paymentId} within ${timeoutMs} ms. Retry, or re-run haven_send / haven_pay with the same idempotency_key plus include_signing_payload: true and pass typed_data_b64 from that result instead.` : `Could not reach Haven to fetch the direct-payment signing context for ${paymentId}: ${err instanceof Error ? err.message : String(err)}. Retry, or re-run haven_send / haven_pay with the same idempotency_key plus include_signing_payload: true and pass typed_data_b64 from that result instead.`,
618
626
  timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
619
627
  void 0,
620
628
  paymentId,
@@ -627,7 +635,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
627
635
  } catch (err) {
628
636
  if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
629
637
  throw new HavenSignContextError(
630
- `Haven did not finish sending the direct-payment signing context for ${paymentId} within ${timeoutMs} ms. Retry, or pass typed_data_b64 from the payment result instead.`,
638
+ `Haven did not finish sending the direct-payment signing context for ${paymentId} within ${timeoutMs} ms. Retry, or re-run haven_send / haven_pay with the same idempotency_key plus include_signing_payload: true and pass typed_data_b64 from that result instead.`,
631
639
  "SIGN_CONTEXT_TIMEOUT",
632
640
  void 0,
633
641
  paymentId,
@@ -639,7 +647,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
639
647
  if (!response.ok) {
640
648
  const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
641
649
  throw new HavenSignContextError(
642
- `Haven refused the direct-payment signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 either the payment_id is not from this agent\u2019s own POST /payments call, or this Haven backend predates #3271. Pass payload_hash and typed_data_b64 from the haven_send / haven_pay result, unchanged, instead." : body.error_code === "expired" ? " The payment window has expired; call haven_send / haven_pay again with the same idempotency_key." : ""),
650
+ `Haven refused the direct-payment signing-context fetch for ${paymentId}: ${detail}` + (response.status === 404 ? " \u2014 either the payment_id is not from this agent\u2019s own POST /payments call, or this Haven backend predates #3271 (which also predates #3495, so its haven_send / haven_pay result already carries payload_hash and typed_data_b64 unconditionally). Pass payload_hash and typed_data_b64, unchanged, from the original result \u2014 or, on a newer server, from a same-key re-run with include_signing_payload: true." : body.error_code === "expired" ? " The payment window has expired; call haven_send / haven_pay again with the same idempotency_key." : ""),
643
651
  "SIGN_CONTEXT_REFUSED",
644
652
  refusalOf(response.status, body),
645
653
  paymentId,
@@ -650,7 +658,7 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
650
658
  const version = body.direct_sign_context_version;
651
659
  if (!signData || typeof signData.hash !== "string" || !signData.typed_data || typeof signData.typed_data !== "object" || signData.signature_scheme !== "eip712_userop" || typeof version !== "number" || !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version)) {
652
660
  throw new HavenSignContextError(
653
- typeof version === "number" && !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version) ? `The Haven direct-payment sign-context response is version ${version}, which this signer does not support (supported: ${SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.` : "The Haven direct-payment sign-context response is missing sign_data.typed_data, or its signature_scheme is not 'eip712_userop' \u2014 the backend may predate #3271. Pass typed_data_b64 from the payment result instead.",
661
+ typeof version === "number" && !SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.includes(version) ? `The Haven direct-payment sign-context response is version ${version}, which this signer does not support (supported: ${SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS.join(", ")}). Update @haven_ai/signer.` : "The Haven direct-payment sign-context response is missing sign_data.typed_data, or its signature_scheme is not 'eip712_userop' \u2014 the backend may predate #3271. Re-run haven_send / haven_pay with the same idempotency_key plus include_signing_payload: true and pass typed_data_b64 from that result instead.",
654
662
  "SIGN_CONTEXT_MALFORMED",
655
663
  void 0,
656
664
  paymentId,
@@ -666,7 +674,120 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
666
674
  clientUpdate: edge.readClientUpdate(body.client_update)
667
675
  };
668
676
  }
669
- var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [1];
677
+ var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [edge.TASK_SIGN_CONTEXT_VERSION];
678
+ var SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS = [1];
679
+ async function fetchSubBudgetSignContext(identity, subBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
680
+ let response;
681
+ try {
682
+ response = await fetchImpl(
683
+ `${identity.apiUrl}/sub-budgets/${encodeURIComponent(subBudgetId)}/sign-context`,
684
+ {
685
+ headers: signContextHeaders(identity, clientIdentity),
686
+ signal: AbortSignal.timeout(timeoutMs)
687
+ }
688
+ );
689
+ } catch (err) {
690
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
691
+ throw new HavenSignContextError(
692
+ 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)}.`,
693
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
694
+ void 0,
695
+ subBudgetId,
696
+ "task"
697
+ );
698
+ }
699
+ let body;
700
+ try {
701
+ body = await response.json();
702
+ } catch (err) {
703
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
704
+ throw new HavenSignContextError(
705
+ `Haven did not finish sending the sub-budget signing context for ${subBudgetId} within ${timeoutMs} ms. Retry.`,
706
+ "SIGN_CONTEXT_TIMEOUT",
707
+ void 0,
708
+ subBudgetId,
709
+ "task"
710
+ );
711
+ }
712
+ body = {};
713
+ }
714
+ if (!response.ok) {
715
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
716
+ throw new HavenSignContextError(
717
+ `Haven refused the sub-budget signing-context fetch for ${subBudgetId}: ${detail}`,
718
+ "SIGN_CONTEXT_REFUSED",
719
+ refusalOf(response.status, body),
720
+ subBudgetId,
721
+ "task"
722
+ );
723
+ }
724
+ const purpose = body.purpose;
725
+ const typedData = body.typed_data;
726
+ const version = body.sub_budget_sign_context_version;
727
+ const expected = body.expected;
728
+ const malformed = (detail) => {
729
+ throw new HavenSignContextError(
730
+ `The Haven sub-budget sign-context response for ${subBudgetId} is malformed: ${detail}.`,
731
+ "SIGN_CONTEXT_MALFORMED",
732
+ void 0,
733
+ subBudgetId,
734
+ "task"
735
+ );
736
+ };
737
+ if (typeof version !== "number" || !SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS.includes(version)) {
738
+ throw new HavenSignContextError(
739
+ `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.`,
740
+ "SIGN_CONTEXT_MALFORMED",
741
+ void 0,
742
+ subBudgetId,
743
+ "task"
744
+ );
745
+ }
746
+ if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
747
+ if (!expected || typeof expected !== "object") return malformed("missing expected");
748
+ const e = expected;
749
+ if (purpose === "open") {
750
+ 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") {
751
+ return malformed("expected is missing a required field for purpose open");
752
+ }
753
+ return {
754
+ purpose: "open",
755
+ subBudgetId: String(body.sub_budget_id ?? subBudgetId),
756
+ typedData,
757
+ expected: {
758
+ delegateAccount: e.delegate_account,
759
+ chainId: e.chain_id,
760
+ tokenAddress: e.token_address,
761
+ periodAmountAtomic: e.period_amount_atomic,
762
+ periodDurationSeconds: e.period_duration_seconds,
763
+ startDate: e.start_date,
764
+ recipientAddress: e.recipient_address ?? null,
765
+ expiresAt: e.expires_at,
766
+ parentDelegationHash: e.parent_delegation_hash,
767
+ childDelegateAccount: e.child_delegate_account
768
+ }
769
+ };
770
+ }
771
+ if (purpose === "close") {
772
+ const userOpHash = body.user_op_hash;
773
+ if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
774
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
775
+ return malformed("expected is missing a required field for purpose close");
776
+ }
777
+ return {
778
+ purpose: "close",
779
+ subBudgetId: String(body.sub_budget_id ?? subBudgetId),
780
+ typedData,
781
+ userOpHash,
782
+ expected: {
783
+ delegateAccount: e.delegate_account,
784
+ chainId: e.chain_id,
785
+ delegationHash: e.delegation_hash
786
+ }
787
+ };
788
+ }
789
+ return malformed(`unknown purpose '${String(purpose)}'`);
790
+ }
670
791
  async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
671
792
  let response;
672
793
  try {
@@ -784,7 +905,8 @@ function signerCompatibility() {
784
905
  x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
785
906
  sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS],
786
907
  direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS],
787
- task_sign_context_versions: [...SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS]
908
+ task_sign_context_versions: [...SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS],
909
+ sub_budget_sign_context_versions: [...SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS]
788
910
  };
789
911
  }
790
912
  function signerCapabilityAdvertisement() {
@@ -804,12 +926,13 @@ function signerInstructions() {
804
926
  `- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
805
927
  `- 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`,
806
928
  `- 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`,
929
+ `- sub-budget sign-context versions supported: ${compatibility.sub_budget_sign_context_versions.join(", ")} \u2014 pass sub_budget_id alone to haven_sign; this signer fetches the exact bytes (a sub-budget your owner issued narrows your own budget for another agent of your account)`,
807
930
  "",
808
931
  "Haven quote and prepare results report the expected-context version they will emit",
809
932
  "(signer_compatibility.x402_expected_context_version). If that version is not in the list",
810
933
  "above, this signer is out of date: STOP before signing, and tell the user to update",
811
- `@haven_ai/signer by rerunning \`${edge.connectorRerunCommand()}\`, which reinstalls the pinned`,
812
- "MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
934
+ `@haven_ai/signer: run \`${edge.connectorUpgradeCommand()}\`, then the repair line it prints,`,
935
+ "which reinstalls the pinned MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
813
936
  "Haven-signed binding message, so changing it invalidates the signature.",
814
937
  "",
815
938
  "A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
@@ -817,7 +940,13 @@ function signerInstructions() {
817
940
  "fallback fields alongside the message, so you can branch on it directly. Every signer",
818
941
  "refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
819
942
  "hosted tool follows (resolve the role against your own server names), else",
820
- "next_tool_omitted_reason saying why not."
943
+ "next_tool_omitted_reason saying why not.",
944
+ "",
945
+ "An undeclared top-level argument is refused, not stripped: haven_sign answers",
946
+ "UNSUPPORTED_ARGUMENT with unknown_arguments, signer_version and fallback (the update",
947
+ "command) instead of dropping the key and answering the generic signing error \u2014 if you see",
948
+ "that refusal, this signer predates the argument form you sent. Update the signer; nothing",
949
+ "was signed, fetched or audited."
821
950
  ].join("\n");
822
951
  }
823
952
  var OWNER_ONLY_MODE = 384;
@@ -1036,6 +1165,9 @@ var HavenBareHashRefusedError = class extends edge.HavenSigningError {
1036
1165
  };
1037
1166
 
1038
1167
  // src/tools.ts
1168
+ var SIGNER_NAME = "@haven_ai/signer";
1169
+ var SIGNER_VERSION = "0.8.0-alpha.0";
1170
+ var UNSUPPORTED_ARGUMENT_MARKER = "HavenSignerUnsupportedArgument:";
1039
1171
  var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
1040
1172
  var HavenUserOpBindingRefusedError = class extends edge.HavenSigningError {
1041
1173
  next_action = edge.AgentPaymentNextAction.StopAndTellUser;
@@ -1048,7 +1180,7 @@ var HavenUserOpBindingRefusedError = class extends edge.HavenSigningError {
1048
1180
  this.step = signerRefusalStep({
1049
1181
  nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
1050
1182
  nextTool: null,
1051
- nextToolOmittedReason: "call haven_sign again with payment_id alone (preferred), or with typed_data copied unchanged from the payment result"
1183
+ nextToolOmittedReason: "call haven_sign again with payment_id alone (preferred), or with typed_data copied unchanged from the payment result (or, on a compact direct-payment result, from a same-key re-run with include_signing_payload: true)"
1052
1184
  });
1053
1185
  }
1054
1186
  };
@@ -1163,7 +1295,13 @@ var toolSchemas = {
1163
1295
  // Mutually exclusive with payment_id / payload_hash (checked in the
1164
1296
  // handler, since Zod's object-level refine on a raw shape used by
1165
1297
  // several call sites is not the shape this schema is composed as).
1166
- task_budget_id: v3.z.string().min(1).optional()
1298
+ task_budget_id: v3.z.string().min(1).optional(),
1299
+ // #3330: sign a sub-budget's own child delegation or its early-close
1300
+ // UserOp — same fetch-verify-sign channel as task_budget_id, against
1301
+ // `GET /sub-budgets/:id/sign-context`. Mutually exclusive with
1302
+ // payment_id / payload_hash AND with task_budget_id (checked in the
1303
+ // handler).
1304
+ sub_budget_id: v3.z.string().min(1).optional()
1167
1305
  },
1168
1306
  haven_x402_sign_header: {
1169
1307
  // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
@@ -1444,9 +1582,76 @@ function createToolHandlers(signer, options = {}) {
1444
1582
  next_arguments: { task_budget_id: ctx.taskBudgetId, signature }
1445
1583
  };
1446
1584
  }
1585
+ async function signSubBudget(subBudgetId) {
1586
+ const identity = await options.signContext?.loadIdentity() ?? null;
1587
+ if (!identity) {
1588
+ throw new edge.HavenSigningError(
1589
+ `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.`
1590
+ );
1591
+ }
1592
+ const ctx = await fetchSubBudgetSignContext(
1593
+ identity,
1594
+ subBudgetId,
1595
+ options.signContext?.fetchImpl,
1596
+ void 0,
1597
+ options.signContext?.clientIdentity
1598
+ );
1599
+ let signature;
1600
+ if (ctx.purpose === "open") {
1601
+ try {
1602
+ edge.assertOwnSubBudgetChild(
1603
+ ctx.typedData,
1604
+ {
1605
+ chainId: ctx.expected.chainId,
1606
+ tokenAddress: ctx.expected.tokenAddress,
1607
+ periodAmountAtomic: ctx.expected.periodAmountAtomic,
1608
+ periodDurationSeconds: ctx.expected.periodDurationSeconds,
1609
+ startDate: ctx.expected.startDate,
1610
+ recipientAddress: ctx.expected.recipientAddress,
1611
+ expiresAt: ctx.expected.expiresAt,
1612
+ parentDelegationHash: ctx.expected.parentDelegationHash,
1613
+ delegateAccount: ctx.expected.delegateAccount,
1614
+ childDelegateAccount: ctx.expected.childDelegateAccount
1615
+ },
1616
+ signer.delegateAddress
1617
+ );
1618
+ } catch (err) {
1619
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1620
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1621
+ }
1622
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1623
+ } else {
1624
+ try {
1625
+ edge.assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
1626
+ } catch (err) {
1627
+ if (err instanceof edge.HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
1628
+ throw err;
1629
+ }
1630
+ try {
1631
+ edge.assertOwnSubBudgetCloseUserOp(
1632
+ ctx.typedData,
1633
+ { delegationHash: ctx.expected.delegationHash },
1634
+ signer.delegateAddress
1635
+ );
1636
+ } catch (err) {
1637
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1638
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1639
+ }
1640
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1641
+ }
1642
+ await auditSigning("haven_sign", viem.hashTypedData(ctx.typedData));
1643
+ return {
1644
+ signature,
1645
+ sub_budget_id: ctx.subBudgetId,
1646
+ purpose: ctx.purpose,
1647
+ next_tool_name: "haven_submit",
1648
+ next_tool_server_role: "hosted",
1649
+ next_arguments: { sub_budget_id: ctx.subBudgetId, signature }
1650
+ };
1651
+ }
1447
1652
  return {
1448
1653
  haven_sign: async (input) => runTool(async () => {
1449
- const args = parse("haven_sign", coerceX402Expected(input));
1654
+ const args = parseStrictFor("haven_sign", coerceX402Expected(input));
1450
1655
  if (args.task_budget_id) {
1451
1656
  if (args.payment_id || args.payload_hash) {
1452
1657
  throw new v3.z.ZodError([
@@ -1457,8 +1662,29 @@ function createToolHandlers(signer, options = {}) {
1457
1662
  }
1458
1663
  ]);
1459
1664
  }
1665
+ if (args.sub_budget_id) {
1666
+ throw new v3.z.ZodError([
1667
+ {
1668
+ code: v3.z.ZodIssueCode.custom,
1669
+ path: ["sub_budget_id"],
1670
+ message: "task_budget_id is mutually exclusive with sub_budget_id \u2014 pass exactly one."
1671
+ }
1672
+ ]);
1673
+ }
1460
1674
  return signTaskBudget(args.task_budget_id);
1461
1675
  }
1676
+ if (args.sub_budget_id) {
1677
+ if (args.payment_id || args.payload_hash) {
1678
+ throw new v3.z.ZodError([
1679
+ {
1680
+ code: v3.z.ZodIssueCode.custom,
1681
+ path: ["sub_budget_id"],
1682
+ message: "sub_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
1683
+ }
1684
+ ]);
1685
+ }
1686
+ return signSubBudget(args.sub_budget_id);
1687
+ }
1462
1688
  const resolved = await resolveSignContext(args, { allowDirectFallback: true });
1463
1689
  const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
1464
1690
  const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
@@ -1501,7 +1727,7 @@ function createToolHandlers(signer, options = {}) {
1501
1727
  return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
1502
1728
  }),
1503
1729
  haven_x402_sign_header: async (input) => runTool(async () => {
1504
- const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
1730
+ const args = parseStrictFor("haven_x402_sign_header", coercePaymentRequired(input));
1505
1731
  const result = await signer.buildX402PaymentHeader(
1506
1732
  args.payment_required,
1507
1733
  args.x402_binding
@@ -1513,7 +1739,7 @@ function createToolHandlers(signer, options = {}) {
1513
1739
  return { payment_header: result.paymentHeader, accepted: result.accepted };
1514
1740
  }),
1515
1741
  haven_sign_x402: async (input) => runTool(async () => {
1516
- const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
1742
+ const args = parseStrictFor("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
1517
1743
  const resolved = await resolveSignContext(args, { allowDirectFallback: false });
1518
1744
  const fetched = resolved?.kind === "x402" ? resolved.ctx : void 0;
1519
1745
  const payloadHash = fetched?.payloadHash ?? args.payload_hash;
@@ -1549,7 +1775,7 @@ function createToolHandlers(signer, options = {}) {
1549
1775
  };
1550
1776
  }),
1551
1777
  haven_sign_sweep_delegate: async (input) => runTool(async () => {
1552
- const args = parse("haven_sign_sweep_delegate", input);
1778
+ const args = parseStrictFor("haven_sign_sweep_delegate", input);
1553
1779
  const result = await signer.signSweepAuthorization({
1554
1780
  authorization: args.authorization,
1555
1781
  expectedAuth: args.expected_auth,
@@ -1582,8 +1808,18 @@ function createToolHandlers(signer, options = {}) {
1582
1808
  }
1583
1809
  }
1584
1810
  }
1585
- function parse(name, input) {
1586
- return v3.z.object(toolSchemas[name]).parse(input ?? {});
1811
+ function parseStrictFor(name, input) {
1812
+ const result = v3.z.object(toolSchemas[name]).strict().safeParse(input ?? {});
1813
+ if (result.success) return result.data;
1814
+ const topLevelUnknown = result.error.issues.flatMap(
1815
+ (issue) => issue.code === "unrecognized_keys" && issue.path.length === 0 ? issue.keys : []
1816
+ );
1817
+ if (topLevelUnknown.length === 0) throw result.error;
1818
+ const unknown = [...new Set(topLevelUnknown)];
1819
+ const err = new Error(
1820
+ `${UNSUPPORTED_ARGUMENT_MARKER} ${name} does not accept ${unknown.map((k) => `"${k}"`).join(", ")} \u2014 this signer predates that argument form. Nothing was signed or fetched.`
1821
+ );
1822
+ throw err;
1587
1823
  }
1588
1824
  function coerceX402Expected(input) {
1589
1825
  if (!input || typeof input !== "object") return input;
@@ -1622,6 +1858,25 @@ async function runTool(fn) {
1622
1858
  }
1623
1859
  }
1624
1860
  function normalizeError(err) {
1861
+ if (err instanceof Error && err.message.startsWith(UNSUPPORTED_ARGUMENT_MARKER)) {
1862
+ const unknown = [...err.message.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
1863
+ return {
1864
+ success: false,
1865
+ code: "UNSUPPORTED_ARGUMENT",
1866
+ message: err.message.replace(`${UNSUPPORTED_ARGUMENT_MARKER} `, ""),
1867
+ unknown_arguments: unknown,
1868
+ signer_version: SIGNER_VERSION,
1869
+ fallback: edge.connectorUpgradeCommand(),
1870
+ next_action: edge.AgentPaymentNextAction.StopAndTellUser,
1871
+ ...nextStepWireFields(
1872
+ signerRefusalStep({
1873
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
1874
+ nextTool: null,
1875
+ nextToolOmittedReason: `update @haven_ai/signer: run ${edge.connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
1876
+ })
1877
+ )
1878
+ };
1879
+ }
1625
1880
  if (err instanceof v3.z.ZodError) {
1626
1881
  return {
1627
1882
  success: false,
@@ -1643,7 +1898,9 @@ function normalizeError(err) {
1643
1898
  ...nextStepWireFields(signerRefusalStep({
1644
1899
  nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
1645
1900
  nextTool: null,
1646
- nextToolOmittedReason: "update @haven_ai/signer by re-running the connector, then repeat the same call"
1901
+ // #3412: the upgrade command, not "re-run the connector" — a bare re-run
1902
+ // stops at "Missing --setup" on an existing install.
1903
+ nextToolOmittedReason: `update @haven_ai/signer: run ${edge.connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
1647
1904
  }))
1648
1905
  };
1649
1906
  }
@@ -1729,7 +1986,7 @@ function normalizeError(err) {
1729
1986
 
1730
1987
  // src/consent.ts
1731
1988
  var toolSummaries = {
1732
- 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)",
1989
+ haven_sign: "signs a Haven-prepared payment, task-budget or sub-budget open or close (a UserOp from your own account redeeming a delegation made to it, directly or via a task-budget child; a sub-budget child narrowing your budget for another agent of yours; or an x402 funding intent against a Haven-signed context)",
1733
1990
  haven_x402_sign_header: "builds and signs the EIP-3009 merchant payment header for an x402 payment",
1734
1991
  haven_sign_x402: "the two above in one call: funding payload + merchant header",
1735
1992
  haven_sign_sweep_delegate: "signs a Haven-prepared gasless sweep of stranded funds back to your own account"
@@ -1858,8 +2115,6 @@ async function writeAckFile(path$1, hash) {
1858
2115
  "utf8"
1859
2116
  );
1860
2117
  }
1861
- var SIGNER_NAME = "@haven_ai/signer";
1862
- var SIGNER_VERSION = "0.6.0-alpha.0";
1863
2118
  async function resolveEdgeSigner(options = {}) {
1864
2119
  const { signer } = await resolveSignerRuntime(options);
1865
2120
  return signer;
@@ -1914,12 +2169,14 @@ function buildSignerMcpServer(signer, options = {}) {
1914
2169
  clientIdentity: edge.havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
1915
2170
  }
1916
2171
  });
1917
- const registerTool = server.tool.bind(server);
2172
+ const registerTool = server.registerTool.bind(server);
1918
2173
  for (const name of Object.keys(toolSchemas)) {
1919
2174
  registerTool(
1920
2175
  name,
1921
- toolDescriptions[name],
1922
- toolSchemas[name],
2176
+ {
2177
+ description: toolDescriptions[name],
2178
+ inputSchema: zod.z.object(toolSchemas[name]).passthrough()
2179
+ },
1923
2180
  async (args) => toMcpResult(await handlers[name](args))
1924
2181
  );
1925
2182
  }
@@ -1986,6 +2243,7 @@ exports.SIGNER_NAME = SIGNER_NAME;
1986
2243
  exports.SIGNER_VERSION = SIGNER_VERSION;
1987
2244
  exports.SUPPORTED_SWEEP_BINDING_VERSIONS = SUPPORTED_SWEEP_BINDING_VERSIONS;
1988
2245
  exports.SUPPORTED_X402_EXPECTED_VERSIONS = SUPPORTED_X402_EXPECTED_VERSIONS;
2246
+ exports.UNSUPPORTED_ARGUMENT_MARKER = UNSUPPORTED_ARGUMENT_MARKER;
1989
2247
  exports.appendSigningAuditEntry = appendSigningAuditEntry;
1990
2248
  exports.assertSupportedBindingVersion = assertSupportedBindingVersion;
1991
2249
  exports.assertSupportedNodeVersion = assertSupportedNodeVersion;