@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/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.`,
@@ -434,7 +434,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
434
434
  /**
435
435
  * #3303: on a 426 `client_outdated` refusal, the backend's update hint —
436
436
  * this signer is below the minimum the deployment set, and
437
- * `client_update.upgrade_command` is what updates it.
437
+ * `client_update.upgrade_command` starts the update (#3412: for the signer, the doctor, then its repair line).
438
438
  */
439
439
  client_update;
440
440
  constructor(message, code, refusal, paymentId, flow = "x402") {
@@ -451,7 +451,7 @@ var HavenSignContextError = class extends edge.HavenSigningError {
451
451
  step = signerRefusalStep({
452
452
  nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
453
453
  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`
454
+ 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
455
  });
456
456
  } else if (flow === "direct" && refusal?.httpStatus === 404) {
457
457
  this.fallback = "typed_data_b64";
@@ -476,6 +476,13 @@ var HavenSignContextError = class extends edge.HavenSigningError {
476
476
  nextTool: null,
477
477
  nextToolOmittedReason: "re-run the hosted quote tool you called with the same idempotency_key; which one depends on the flow"
478
478
  });
479
+ } else if (flow === "task") {
480
+ this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
481
+ step = signerRefusalStep({
482
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
483
+ nextTool: null,
484
+ nextToolOmittedReason: `re-check this task budget's status (id ${paymentId}) with the hosted Haven server before retrying`
485
+ });
479
486
  } else {
480
487
  this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
481
488
  step = signerRefusalStep({
@@ -484,6 +491,13 @@ var HavenSignContextError = class extends edge.HavenSigningError {
484
491
  nextArguments: { payment_id: paymentId }
485
492
  });
486
493
  }
494
+ } else if (flow === "task") {
495
+ this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
496
+ step = signerRefusalStep({
497
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
498
+ nextTool: null,
499
+ nextToolOmittedReason: "retry the same haven_sign call with the same task_budget_id"
500
+ });
487
501
  } else {
488
502
  this.fallback = "typed_data_b64";
489
503
  this.next_action = edge.AgentPaymentNextAction.StopAndTellUser;
@@ -652,6 +666,229 @@ async function fetchDirectSignContext(identity, paymentId, fetchImpl = fetch, ti
652
666
  clientUpdate: edge.readClientUpdate(body.client_update)
653
667
  };
654
668
  }
669
+ var SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS = [edge.TASK_SIGN_CONTEXT_VERSION];
670
+ var SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS = [1];
671
+ async function fetchSubBudgetSignContext(identity, subBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
672
+ let response;
673
+ try {
674
+ response = await fetchImpl(
675
+ `${identity.apiUrl}/sub-budgets/${encodeURIComponent(subBudgetId)}/sign-context`,
676
+ {
677
+ headers: signContextHeaders(identity, clientIdentity),
678
+ signal: AbortSignal.timeout(timeoutMs)
679
+ }
680
+ );
681
+ } catch (err) {
682
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
683
+ throw new HavenSignContextError(
684
+ 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)}.`,
685
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
686
+ void 0,
687
+ subBudgetId,
688
+ "task"
689
+ );
690
+ }
691
+ let body;
692
+ try {
693
+ body = await response.json();
694
+ } catch (err) {
695
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
696
+ throw new HavenSignContextError(
697
+ `Haven did not finish sending the sub-budget signing context for ${subBudgetId} within ${timeoutMs} ms. Retry.`,
698
+ "SIGN_CONTEXT_TIMEOUT",
699
+ void 0,
700
+ subBudgetId,
701
+ "task"
702
+ );
703
+ }
704
+ body = {};
705
+ }
706
+ if (!response.ok) {
707
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
708
+ throw new HavenSignContextError(
709
+ `Haven refused the sub-budget signing-context fetch for ${subBudgetId}: ${detail}`,
710
+ "SIGN_CONTEXT_REFUSED",
711
+ refusalOf(response.status, body),
712
+ subBudgetId,
713
+ "task"
714
+ );
715
+ }
716
+ const purpose = body.purpose;
717
+ const typedData = body.typed_data;
718
+ const version = body.sub_budget_sign_context_version;
719
+ const expected = body.expected;
720
+ const malformed = (detail) => {
721
+ throw new HavenSignContextError(
722
+ `The Haven sub-budget sign-context response for ${subBudgetId} is malformed: ${detail}.`,
723
+ "SIGN_CONTEXT_MALFORMED",
724
+ void 0,
725
+ subBudgetId,
726
+ "task"
727
+ );
728
+ };
729
+ if (typeof version !== "number" || !SUPPORTED_SUB_BUDGET_SIGN_CONTEXT_VERSIONS.includes(version)) {
730
+ throw new HavenSignContextError(
731
+ `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.`,
732
+ "SIGN_CONTEXT_MALFORMED",
733
+ void 0,
734
+ subBudgetId,
735
+ "task"
736
+ );
737
+ }
738
+ if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
739
+ if (!expected || typeof expected !== "object") return malformed("missing expected");
740
+ const e = expected;
741
+ if (purpose === "open") {
742
+ 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") {
743
+ return malformed("expected is missing a required field for purpose open");
744
+ }
745
+ return {
746
+ purpose: "open",
747
+ subBudgetId: String(body.sub_budget_id ?? subBudgetId),
748
+ typedData,
749
+ expected: {
750
+ delegateAccount: e.delegate_account,
751
+ chainId: e.chain_id,
752
+ tokenAddress: e.token_address,
753
+ periodAmountAtomic: e.period_amount_atomic,
754
+ periodDurationSeconds: e.period_duration_seconds,
755
+ startDate: e.start_date,
756
+ recipientAddress: e.recipient_address ?? null,
757
+ expiresAt: e.expires_at,
758
+ parentDelegationHash: e.parent_delegation_hash,
759
+ childDelegateAccount: e.child_delegate_account
760
+ }
761
+ };
762
+ }
763
+ if (purpose === "close") {
764
+ const userOpHash = body.user_op_hash;
765
+ if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
766
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
767
+ return malformed("expected is missing a required field for purpose close");
768
+ }
769
+ return {
770
+ purpose: "close",
771
+ subBudgetId: String(body.sub_budget_id ?? subBudgetId),
772
+ typedData,
773
+ userOpHash,
774
+ expected: {
775
+ delegateAccount: e.delegate_account,
776
+ chainId: e.chain_id,
777
+ delegationHash: e.delegation_hash
778
+ }
779
+ };
780
+ }
781
+ return malformed(`unknown purpose '${String(purpose)}'`);
782
+ }
783
+ async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
784
+ let response;
785
+ try {
786
+ response = await fetchImpl(
787
+ `${identity.apiUrl}/task-budgets/${encodeURIComponent(taskBudgetId)}/sign-context`,
788
+ {
789
+ headers: signContextHeaders(identity, clientIdentity),
790
+ signal: AbortSignal.timeout(timeoutMs)
791
+ }
792
+ );
793
+ } catch (err) {
794
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
795
+ throw new HavenSignContextError(
796
+ 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)}.`,
797
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
798
+ void 0,
799
+ taskBudgetId,
800
+ "task"
801
+ );
802
+ }
803
+ let body;
804
+ try {
805
+ body = await response.json();
806
+ } catch (err) {
807
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
808
+ throw new HavenSignContextError(
809
+ `Haven did not finish sending the task-budget signing context for ${taskBudgetId} within ${timeoutMs} ms. Retry.`,
810
+ "SIGN_CONTEXT_TIMEOUT",
811
+ void 0,
812
+ taskBudgetId,
813
+ "task"
814
+ );
815
+ }
816
+ body = {};
817
+ }
818
+ if (!response.ok) {
819
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
820
+ throw new HavenSignContextError(
821
+ `Haven refused the task-budget signing-context fetch for ${taskBudgetId}: ${detail}`,
822
+ "SIGN_CONTEXT_REFUSED",
823
+ refusalOf(response.status, body),
824
+ taskBudgetId,
825
+ "task"
826
+ );
827
+ }
828
+ const purpose = body.purpose;
829
+ const typedData = body.typed_data;
830
+ const version = body.task_sign_context_version;
831
+ const expected = body.expected;
832
+ const malformed = (detail) => {
833
+ throw new HavenSignContextError(
834
+ `The Haven task-budget sign-context response for ${taskBudgetId} is malformed: ${detail}.`,
835
+ "SIGN_CONTEXT_MALFORMED",
836
+ void 0,
837
+ taskBudgetId,
838
+ "task"
839
+ );
840
+ };
841
+ if (typeof version !== "number" || !SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS.includes(version)) {
842
+ throw new HavenSignContextError(
843
+ `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.`,
844
+ "SIGN_CONTEXT_MALFORMED",
845
+ void 0,
846
+ taskBudgetId,
847
+ "task"
848
+ );
849
+ }
850
+ if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
851
+ if (!expected || typeof expected !== "object") return malformed("missing expected");
852
+ const e = expected;
853
+ if (purpose === "open") {
854
+ 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") {
855
+ return malformed("expected is missing a required field for purpose open");
856
+ }
857
+ return {
858
+ purpose: "open",
859
+ taskBudgetId: String(body.task_budget_id ?? taskBudgetId),
860
+ typedData,
861
+ expected: {
862
+ delegateAccount: e.delegate_account,
863
+ chainId: e.chain_id,
864
+ tokenAddress: e.token_address,
865
+ maxAmountAtomic: e.max_amount_atomic,
866
+ recipientAddress: e.recipient_address ?? null,
867
+ expiresAt: e.expires_at,
868
+ parentDelegationHash: e.parent_delegation_hash
869
+ }
870
+ };
871
+ }
872
+ if (purpose === "close") {
873
+ const userOpHash = body.user_op_hash;
874
+ if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
875
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
876
+ return malformed("expected is missing a required field for purpose close");
877
+ }
878
+ return {
879
+ purpose: "close",
880
+ taskBudgetId: String(body.task_budget_id ?? taskBudgetId),
881
+ typedData,
882
+ userOpHash,
883
+ expected: {
884
+ delegateAccount: e.delegate_account,
885
+ chainId: e.chain_id,
886
+ delegationHash: e.delegation_hash
887
+ }
888
+ };
889
+ }
890
+ return malformed(`unknown purpose '${String(purpose)}'`);
891
+ }
655
892
 
656
893
  // src/capabilities.ts
657
894
  var SIGNER_CAPABILITY_KEY = "haven/signer-compatibility";
@@ -659,7 +896,8 @@ function signerCompatibility() {
659
896
  return {
660
897
  x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
661
898
  sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS],
662
- direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS]
899
+ direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS],
900
+ task_sign_context_versions: [...SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS]
663
901
  };
664
902
  }
665
903
  function signerCapabilityAdvertisement() {
@@ -678,12 +916,13 @@ function signerInstructions() {
678
916
  `- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
679
917
  `- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
680
918
  `- 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`,
919
+ `- 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`,
681
920
  "",
682
921
  "Haven quote and prepare results report the expected-context version they will emit",
683
922
  "(signer_compatibility.x402_expected_context_version). If that version is not in the list",
684
923
  "above, this signer is out of date: STOP before signing, and tell the user to update",
685
- `@haven_ai/signer by rerunning \`${edge.connectorRerunCommand()}\`, which reinstalls the pinned`,
686
- "MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
924
+ `@haven_ai/signer: run \`${edge.connectorUpgradeCommand()}\`, then the repair line it prints,`,
925
+ "which reinstalls the pinned MCP runtime. Do not edit the version field to a supported value \u2014 it is part of the",
687
926
  "Haven-signed binding message, so changing it invalidates the signature.",
688
927
  "",
689
928
  "A version-mismatch refusal from haven_sign / haven_sign_x402 / haven_sign_sweep_delegate is",
@@ -691,7 +930,13 @@ function signerInstructions() {
691
930
  "fallback fields alongside the message, so you can branch on it directly. Every signer",
692
931
  "refusal also carries the next-step family: next_tool_name + next_tool_server_role when a",
693
932
  "hosted tool follows (resolve the role against your own server names), else",
694
- "next_tool_omitted_reason saying why not."
933
+ "next_tool_omitted_reason saying why not.",
934
+ "",
935
+ "An undeclared top-level argument is refused, not stripped: haven_sign answers",
936
+ "UNSUPPORTED_ARGUMENT with unknown_arguments, signer_version and fallback (the update",
937
+ "command) instead of dropping the key and answering the generic signing error \u2014 if you see",
938
+ "that refusal, this signer predates the argument form you sent. Update the signer; nothing",
939
+ "was signed, fetched or audited."
695
940
  ].join("\n");
696
941
  }
697
942
  var OWNER_ONLY_MODE = 384;
@@ -910,6 +1155,9 @@ var HavenBareHashRefusedError = class extends edge.HavenSigningError {
910
1155
  };
911
1156
 
912
1157
  // src/tools.ts
1158
+ var SIGNER_NAME = "@haven_ai/signer";
1159
+ var SIGNER_VERSION = "0.7.0-alpha.0";
1160
+ var UNSUPPORTED_ARGUMENT_MARKER = "HavenSignerUnsupportedArgument:";
913
1161
  var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
914
1162
  var HavenUserOpBindingRefusedError = class extends edge.HavenSigningError {
915
1163
  next_action = edge.AgentPaymentNextAction.StopAndTellUser;
@@ -1030,7 +1278,20 @@ var toolSchemas = {
1030
1278
  // Bounded: a realistic redemption payload is ~10KB encoded; 256KB is
1031
1279
  // generous headroom while keeping the offline signer from materializing
1032
1280
  // arbitrarily large caller input.
1033
- typed_data_b64: v3.z.string().min(1).max(262144).optional()
1281
+ typed_data_b64: v3.z.string().min(1).max(262144).optional(),
1282
+ // #3329: sign a task budget's own child delegation (purpose `open`) or
1283
+ // its early-close UserOp (purpose `close`) — the signer fetches the
1284
+ // exact typed data and expectation itself, exactly like payment_id.
1285
+ // Mutually exclusive with payment_id / payload_hash (checked in the
1286
+ // handler, since Zod's object-level refine on a raw shape used by
1287
+ // several call sites is not the shape this schema is composed as).
1288
+ task_budget_id: v3.z.string().min(1).optional(),
1289
+ // #3330: sign a sub-budget's own child delegation or its early-close
1290
+ // UserOp — same fetch-verify-sign channel as task_budget_id, against
1291
+ // `GET /sub-budgets/:id/sign-context`. Mutually exclusive with
1292
+ // payment_id / payload_hash AND with task_budget_id (checked in the
1293
+ // handler).
1294
+ sub_budget_id: v3.z.string().min(1).optional()
1034
1295
  },
1035
1296
  haven_x402_sign_header: {
1036
1297
  // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
@@ -1080,7 +1341,11 @@ var SIGN_DESCRIPTION = [
1080
1341
  "intent against a Haven-signed context \u2014 never an arbitrary typed_data payload, however it is shaped.",
1081
1342
  "Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
1082
1343
  "to mcp__haven-signer__haven_x402_sign_header. A bare payload_hash with no payment_id, typed_data",
1083
- "or x402_expected is REFUSED (BARE_HASH_REFUSED): a hash carries nothing this signer can verify."
1344
+ "or x402_expected is REFUSED (BARE_HASH_REFUSED): a hash carries nothing this signer can verify.",
1345
+ "TASK BUDGETS (#3329): pass task_budget_id ALONE \u2014 mutually exclusive with payment_id and",
1346
+ "payload_hash \u2014 to sign a task budget\u2019s own child delegation (opening it) or its early-close",
1347
+ "UserOp; this signer fetches the exact typed data itself. Result then carries next_tool_name",
1348
+ "(haven_submit) and next_arguments { task_budget_id, signature } instead of x402_binding."
1084
1349
  ].join(" ");
1085
1350
  var X402_SIGN_HEADER_DESCRIPTION = [
1086
1351
  "Build and sign the EIP-3009 merchant payment header for the merchant leg of an x402 payment.",
@@ -1244,9 +1509,172 @@ function createToolHandlers(signer, options = {}) {
1244
1509
  throw err;
1245
1510
  }
1246
1511
  }
1512
+ async function signTaskBudget(taskBudgetId) {
1513
+ const identity = await options.signContext?.loadIdentity() ?? null;
1514
+ if (!identity) {
1515
+ throw new edge.HavenSigningError(
1516
+ `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.`
1517
+ );
1518
+ }
1519
+ const ctx = await fetchTaskBudgetSignContext(
1520
+ identity,
1521
+ taskBudgetId,
1522
+ options.signContext?.fetchImpl,
1523
+ void 0,
1524
+ options.signContext?.clientIdentity
1525
+ );
1526
+ let signature;
1527
+ if (ctx.purpose === "open") {
1528
+ try {
1529
+ edge.assertOwnTaskChild(
1530
+ ctx.typedData,
1531
+ {
1532
+ chainId: ctx.expected.chainId,
1533
+ tokenAddress: ctx.expected.tokenAddress,
1534
+ maxAmountAtomic: ctx.expected.maxAmountAtomic,
1535
+ recipientAddress: ctx.expected.recipientAddress,
1536
+ expiresAt: ctx.expected.expiresAt,
1537
+ parentDelegationHash: ctx.expected.parentDelegationHash
1538
+ },
1539
+ signer.delegateAddress
1540
+ );
1541
+ } catch (err) {
1542
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1543
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1544
+ }
1545
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1546
+ } else {
1547
+ try {
1548
+ edge.assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
1549
+ } catch (err) {
1550
+ if (err instanceof edge.HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
1551
+ throw err;
1552
+ }
1553
+ try {
1554
+ edge.assertOwnTaskBudgetCloseUserOp(
1555
+ ctx.typedData,
1556
+ { delegationHash: ctx.expected.delegationHash },
1557
+ signer.delegateAddress
1558
+ );
1559
+ } catch (err) {
1560
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1561
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1562
+ }
1563
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1564
+ }
1565
+ await auditSigning("haven_sign", viem.hashTypedData(ctx.typedData));
1566
+ return {
1567
+ signature,
1568
+ task_budget_id: ctx.taskBudgetId,
1569
+ purpose: ctx.purpose,
1570
+ next_tool_name: "haven_submit",
1571
+ next_tool_server_role: "hosted",
1572
+ next_arguments: { task_budget_id: ctx.taskBudgetId, signature }
1573
+ };
1574
+ }
1575
+ async function signSubBudget(subBudgetId) {
1576
+ const identity = await options.signContext?.loadIdentity() ?? null;
1577
+ if (!identity) {
1578
+ throw new edge.HavenSigningError(
1579
+ `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.`
1580
+ );
1581
+ }
1582
+ const ctx = await fetchSubBudgetSignContext(
1583
+ identity,
1584
+ subBudgetId,
1585
+ options.signContext?.fetchImpl,
1586
+ void 0,
1587
+ options.signContext?.clientIdentity
1588
+ );
1589
+ let signature;
1590
+ if (ctx.purpose === "open") {
1591
+ try {
1592
+ edge.assertOwnSubBudgetChild(
1593
+ ctx.typedData,
1594
+ {
1595
+ chainId: ctx.expected.chainId,
1596
+ tokenAddress: ctx.expected.tokenAddress,
1597
+ periodAmountAtomic: ctx.expected.periodAmountAtomic,
1598
+ periodDurationSeconds: ctx.expected.periodDurationSeconds,
1599
+ startDate: ctx.expected.startDate,
1600
+ recipientAddress: ctx.expected.recipientAddress,
1601
+ expiresAt: ctx.expected.expiresAt,
1602
+ parentDelegationHash: ctx.expected.parentDelegationHash,
1603
+ delegateAccount: ctx.expected.delegateAccount,
1604
+ childDelegateAccount: ctx.expected.childDelegateAccount
1605
+ },
1606
+ signer.delegateAddress
1607
+ );
1608
+ } catch (err) {
1609
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1610
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1611
+ }
1612
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1613
+ } else {
1614
+ try {
1615
+ edge.assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
1616
+ } catch (err) {
1617
+ if (err instanceof edge.HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
1618
+ throw err;
1619
+ }
1620
+ try {
1621
+ edge.assertOwnSubBudgetCloseUserOp(
1622
+ ctx.typedData,
1623
+ { delegationHash: ctx.expected.delegationHash },
1624
+ signer.delegateAddress
1625
+ );
1626
+ } catch (err) {
1627
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
1628
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
1629
+ }
1630
+ signature = await signer.signDelegationTypedData(ctx.typedData);
1631
+ }
1632
+ await auditSigning("haven_sign", viem.hashTypedData(ctx.typedData));
1633
+ return {
1634
+ signature,
1635
+ sub_budget_id: ctx.subBudgetId,
1636
+ purpose: ctx.purpose,
1637
+ next_tool_name: "haven_submit",
1638
+ next_tool_server_role: "hosted",
1639
+ next_arguments: { sub_budget_id: ctx.subBudgetId, signature }
1640
+ };
1641
+ }
1247
1642
  return {
1248
1643
  haven_sign: async (input) => runTool(async () => {
1249
- const args = parse("haven_sign", coerceX402Expected(input));
1644
+ const args = parseStrictFor("haven_sign", coerceX402Expected(input));
1645
+ if (args.task_budget_id) {
1646
+ if (args.payment_id || args.payload_hash) {
1647
+ throw new v3.z.ZodError([
1648
+ {
1649
+ code: v3.z.ZodIssueCode.custom,
1650
+ path: ["task_budget_id"],
1651
+ message: "task_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
1652
+ }
1653
+ ]);
1654
+ }
1655
+ if (args.sub_budget_id) {
1656
+ throw new v3.z.ZodError([
1657
+ {
1658
+ code: v3.z.ZodIssueCode.custom,
1659
+ path: ["sub_budget_id"],
1660
+ message: "task_budget_id is mutually exclusive with sub_budget_id \u2014 pass exactly one."
1661
+ }
1662
+ ]);
1663
+ }
1664
+ return signTaskBudget(args.task_budget_id);
1665
+ }
1666
+ if (args.sub_budget_id) {
1667
+ if (args.payment_id || args.payload_hash) {
1668
+ throw new v3.z.ZodError([
1669
+ {
1670
+ code: v3.z.ZodIssueCode.custom,
1671
+ path: ["sub_budget_id"],
1672
+ message: "sub_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
1673
+ }
1674
+ ]);
1675
+ }
1676
+ return signSubBudget(args.sub_budget_id);
1677
+ }
1250
1678
  const resolved = await resolveSignContext(args, { allowDirectFallback: true });
1251
1679
  const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
1252
1680
  const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
@@ -1289,7 +1717,7 @@ function createToolHandlers(signer, options = {}) {
1289
1717
  return { signature: result.signature, x402_binding: result.x402Binding, ...clientUpdateField(resolved) };
1290
1718
  }),
1291
1719
  haven_x402_sign_header: async (input) => runTool(async () => {
1292
- const args = parse("haven_x402_sign_header", coercePaymentRequired(input));
1720
+ const args = parseStrictFor("haven_x402_sign_header", coercePaymentRequired(input));
1293
1721
  const result = await signer.buildX402PaymentHeader(
1294
1722
  args.payment_required,
1295
1723
  args.x402_binding
@@ -1301,7 +1729,7 @@ function createToolHandlers(signer, options = {}) {
1301
1729
  return { payment_header: result.paymentHeader, accepted: result.accepted };
1302
1730
  }),
1303
1731
  haven_sign_x402: async (input) => runTool(async () => {
1304
- const args = parse("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
1732
+ const args = parseStrictFor("haven_sign_x402", coerceX402Expected(coercePaymentRequired(input)));
1305
1733
  const resolved = await resolveSignContext(args, { allowDirectFallback: false });
1306
1734
  const fetched = resolved?.kind === "x402" ? resolved.ctx : void 0;
1307
1735
  const payloadHash = fetched?.payloadHash ?? args.payload_hash;
@@ -1337,7 +1765,7 @@ function createToolHandlers(signer, options = {}) {
1337
1765
  };
1338
1766
  }),
1339
1767
  haven_sign_sweep_delegate: async (input) => runTool(async () => {
1340
- const args = parse("haven_sign_sweep_delegate", input);
1768
+ const args = parseStrictFor("haven_sign_sweep_delegate", input);
1341
1769
  const result = await signer.signSweepAuthorization({
1342
1770
  authorization: args.authorization,
1343
1771
  expectedAuth: args.expected_auth,
@@ -1370,8 +1798,18 @@ function createToolHandlers(signer, options = {}) {
1370
1798
  }
1371
1799
  }
1372
1800
  }
1373
- function parse(name, input) {
1374
- return v3.z.object(toolSchemas[name]).parse(input ?? {});
1801
+ function parseStrictFor(name, input) {
1802
+ const result = v3.z.object(toolSchemas[name]).strict().safeParse(input ?? {});
1803
+ if (result.success) return result.data;
1804
+ const topLevelUnknown = result.error.issues.flatMap(
1805
+ (issue) => issue.code === "unrecognized_keys" && issue.path.length === 0 ? issue.keys : []
1806
+ );
1807
+ if (topLevelUnknown.length === 0) throw result.error;
1808
+ const unknown = [...new Set(topLevelUnknown)];
1809
+ const err = new Error(
1810
+ `${UNSUPPORTED_ARGUMENT_MARKER} ${name} does not accept ${unknown.map((k) => `"${k}"`).join(", ")} \u2014 this signer predates that argument form. Nothing was signed or fetched.`
1811
+ );
1812
+ throw err;
1375
1813
  }
1376
1814
  function coerceX402Expected(input) {
1377
1815
  if (!input || typeof input !== "object") return input;
@@ -1410,6 +1848,25 @@ async function runTool(fn) {
1410
1848
  }
1411
1849
  }
1412
1850
  function normalizeError(err) {
1851
+ if (err instanceof Error && err.message.startsWith(UNSUPPORTED_ARGUMENT_MARKER)) {
1852
+ const unknown = [...err.message.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
1853
+ return {
1854
+ success: false,
1855
+ code: "UNSUPPORTED_ARGUMENT",
1856
+ message: err.message.replace(`${UNSUPPORTED_ARGUMENT_MARKER} `, ""),
1857
+ unknown_arguments: unknown,
1858
+ signer_version: SIGNER_VERSION,
1859
+ fallback: edge.connectorUpgradeCommand(),
1860
+ next_action: edge.AgentPaymentNextAction.StopAndTellUser,
1861
+ ...nextStepWireFields(
1862
+ signerRefusalStep({
1863
+ nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
1864
+ nextTool: null,
1865
+ nextToolOmittedReason: `update @haven_ai/signer: run ${edge.connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
1866
+ })
1867
+ )
1868
+ };
1869
+ }
1413
1870
  if (err instanceof v3.z.ZodError) {
1414
1871
  return {
1415
1872
  success: false,
@@ -1431,7 +1888,9 @@ function normalizeError(err) {
1431
1888
  ...nextStepWireFields(signerRefusalStep({
1432
1889
  nextAction: edge.AgentPaymentNextAction.StopAndTellUser,
1433
1890
  nextTool: null,
1434
- nextToolOmittedReason: "update @haven_ai/signer by re-running the connector, then repeat the same call"
1891
+ // #3412: the upgrade command, not "re-run the connector" — a bare re-run
1892
+ // stops at "Missing --setup" on an existing install.
1893
+ nextToolOmittedReason: `update @haven_ai/signer: run ${edge.connectorUpgradeCommand()} and the repair line it prints, then repeat the same call`
1435
1894
  }))
1436
1895
  };
1437
1896
  }
@@ -1517,7 +1976,7 @@ function normalizeError(err) {
1517
1976
 
1518
1977
  // src/consent.ts
1519
1978
  var toolSummaries = {
1520
- 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)",
1979
+ 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)",
1521
1980
  haven_x402_sign_header: "builds and signs the EIP-3009 merchant payment header for an x402 payment",
1522
1981
  haven_sign_x402: "the two above in one call: funding payload + merchant header",
1523
1982
  haven_sign_sweep_delegate: "signs a Haven-prepared gasless sweep of stranded funds back to your own account"
@@ -1646,8 +2105,6 @@ async function writeAckFile(path$1, hash) {
1646
2105
  "utf8"
1647
2106
  );
1648
2107
  }
1649
- var SIGNER_NAME = "@haven_ai/signer";
1650
- var SIGNER_VERSION = "0.5.0-alpha.1";
1651
2108
  async function resolveEdgeSigner(options = {}) {
1652
2109
  const { signer } = await resolveSignerRuntime(options);
1653
2110
  return signer;
@@ -1702,12 +2159,14 @@ function buildSignerMcpServer(signer, options = {}) {
1702
2159
  clientIdentity: edge.havenClientIdentity(SIGNER_NAME, SIGNER_VERSION)
1703
2160
  }
1704
2161
  });
1705
- const registerTool = server.tool.bind(server);
2162
+ const registerTool = server.registerTool.bind(server);
1706
2163
  for (const name of Object.keys(toolSchemas)) {
1707
2164
  registerTool(
1708
2165
  name,
1709
- toolDescriptions[name],
1710
- toolSchemas[name],
2166
+ {
2167
+ description: toolDescriptions[name],
2168
+ inputSchema: zod.z.object(toolSchemas[name]).passthrough()
2169
+ },
1711
2170
  async (args) => toMcpResult(await handlers[name](args))
1712
2171
  );
1713
2172
  }
@@ -1774,6 +2233,7 @@ exports.SIGNER_NAME = SIGNER_NAME;
1774
2233
  exports.SIGNER_VERSION = SIGNER_VERSION;
1775
2234
  exports.SUPPORTED_SWEEP_BINDING_VERSIONS = SUPPORTED_SWEEP_BINDING_VERSIONS;
1776
2235
  exports.SUPPORTED_X402_EXPECTED_VERSIONS = SUPPORTED_X402_EXPECTED_VERSIONS;
2236
+ exports.UNSUPPORTED_ARGUMENT_MARKER = UNSUPPORTED_ARGUMENT_MARKER;
1777
2237
  exports.appendSigningAuditEntry = appendSigningAuditEntry;
1778
2238
  exports.assertSupportedBindingVersion = assertSupportedBindingVersion;
1779
2239
  exports.assertSupportedNodeVersion = assertSupportedNodeVersion;