@haven_ai/signer 0.5.0-alpha.1 → 0.6.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,7 +61,7 @@ It exposes four stdio MCP tools, all sign-only:
61
61
 
62
62
  | Tool | Does | Emits |
63
63
  |---|---|---|
64
- | `haven_sign` | Sign one payment. Preferred form is `{ payment_id }` alone — the signer fetches the exact payload itself. Signs only Haven-prepared payloads (#3272, #3281): a direct-payment `PackedUserOperation` from this signer's own delegate account whose only call redeems a delegation made to that account, or — against a Haven-signed context, which it records and binds — an EIP-3009 funding leg of that same shape paying this signer's own delegate EOA, or an erc7710 settlement child from this signer's own account; other typed data is refused (`TYPED_DATA_NOT_ALLOWED`) | `{ signature }` or `{ signature, x402_binding }` |
64
+ | `haven_sign` | Sign one payment, or (#3329) one task-budget open/close. Preferred form is `{ payment_id }` alone for a payment, or `{ task_budget_id }` alone (mutually exclusive with `payment_id`) for a task-budget open/close — the signer fetches the exact payload itself either way. Signs only Haven-prepared payloads (#3272, #3281, #3329): a direct-payment `PackedUserOperation` from this signer's own delegate account whose only call redeems a delegation made to that account EITHER directly OR through a self-delegated task-budget child under it (the two-link `[task child, budget]` chain), or — against a Haven-signed context, which it records and binds — an EIP-3009 funding leg of that same shape paying this signer's own delegate EOA, or an erc7710 settlement child from this signer's own account; other typed data is refused (`TYPED_DATA_NOT_ALLOWED`) | `{ signature }`, `{ signature, x402_binding }`, or `{ signature, task_budget_id, purpose }` |
65
65
  | `haven_sign_x402` | One-shot x402: funding signature **and** the merchant header in a single local call (`haven_sign` + `haven_x402_sign_header`). `{ payment_id }` alone is the preferred call | `{ signature, x402_binding, payment_header, accepted }` |
66
66
  | `haven_x402_sign_header` | Build + sign the EIP-3009 merchant payment header, only when the fresh merchant `payment_required` matches the recorded `x402_binding` | `{ payment_header, accepted }` |
67
67
  | `haven_sign_sweep_delegate` | Sign a Haven-prepared gasless EIP-3009 sweep that recovers stranded funds from the delegate wallet back to your own account. Never broadcasts | `{ signature }` |
@@ -121,8 +121,10 @@ sender is THIS signer's own delegate account (the counterfactual
121
121
  HybridDeleGator for the delegate key, derived offline — `delegate-account.ts` in `@haven_ai/sdk`);
122
122
  and its `callData` is a single `execute` to the DelegationManager calling
123
123
  `redeemDelegations`, whose arguments are decoded too (`redemption-guard.ts` in `@haven_ai/sdk`):
124
- exactly one delegation (a single grant, never an empty or multi-link chain)
125
- made to this signer's own account by a different account, `SingleDefault` mode, and canonical
124
+ exactly one of two chain shapes (#3329) — a single grant made to this signer's own account by a
125
+ different account, or a two-link `[task child, budget]` chain whose leaf is self-delegated by this
126
+ signer's own account — never an empty chain or any other multi-link shape,
127
+ `SingleDefault` mode, and canonical
126
128
  encoding at every level. The argument check matters: an EMPTY permission
127
129
  context makes the DelegationManager run the execution as the account itself,
128
130
  which would reach `transferOwnership`. A delegate-wallet `TransferWithAuthorization` or `Permit`,
package/dist/cli.cjs CHANGED
@@ -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,6 +446,116 @@ 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 = [1];
450
+ async function fetchTaskBudgetSignContext(identity, taskBudgetId, fetchImpl = fetch, timeoutMs = SIGN_CONTEXT_TIMEOUT_MS, clientIdentity) {
451
+ let response;
452
+ try {
453
+ response = await fetchImpl(
454
+ `${identity.apiUrl}/task-budgets/${encodeURIComponent(taskBudgetId)}/sign-context`,
455
+ {
456
+ headers: signContextHeaders(identity, clientIdentity),
457
+ signal: AbortSignal.timeout(timeoutMs)
458
+ }
459
+ );
460
+ } catch (err) {
461
+ const timedOut = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
462
+ throw new HavenSignContextError(
463
+ 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)}.`,
464
+ timedOut ? "SIGN_CONTEXT_TIMEOUT" : "SIGN_CONTEXT_UNREACHABLE",
465
+ void 0,
466
+ taskBudgetId,
467
+ "task"
468
+ );
469
+ }
470
+ let body;
471
+ try {
472
+ body = await response.json();
473
+ } catch (err) {
474
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
475
+ throw new HavenSignContextError(
476
+ `Haven did not finish sending the task-budget signing context for ${taskBudgetId} within ${timeoutMs} ms. Retry.`,
477
+ "SIGN_CONTEXT_TIMEOUT",
478
+ void 0,
479
+ taskBudgetId,
480
+ "task"
481
+ );
482
+ }
483
+ body = {};
484
+ }
485
+ if (!response.ok) {
486
+ const detail = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
487
+ throw new HavenSignContextError(
488
+ `Haven refused the task-budget signing-context fetch for ${taskBudgetId}: ${detail}`,
489
+ "SIGN_CONTEXT_REFUSED",
490
+ refusalOf(response.status, body),
491
+ taskBudgetId,
492
+ "task"
493
+ );
494
+ }
495
+ const purpose = body.purpose;
496
+ const typedData = body.typed_data;
497
+ const version = body.task_sign_context_version;
498
+ const expected = body.expected;
499
+ const malformed = (detail) => {
500
+ throw new HavenSignContextError(
501
+ `The Haven task-budget sign-context response for ${taskBudgetId} is malformed: ${detail}.`,
502
+ "SIGN_CONTEXT_MALFORMED",
503
+ void 0,
504
+ taskBudgetId,
505
+ "task"
506
+ );
507
+ };
508
+ if (typeof version !== "number" || !SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS.includes(version)) {
509
+ throw new HavenSignContextError(
510
+ `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.`,
511
+ "SIGN_CONTEXT_MALFORMED",
512
+ void 0,
513
+ taskBudgetId,
514
+ "task"
515
+ );
516
+ }
517
+ if (!typedData || typeof typedData !== "object") return malformed("missing typed_data");
518
+ if (!expected || typeof expected !== "object") return malformed("missing expected");
519
+ const e = expected;
520
+ if (purpose === "open") {
521
+ 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") {
522
+ return malformed("expected is missing a required field for purpose open");
523
+ }
524
+ return {
525
+ purpose: "open",
526
+ taskBudgetId: String(body.task_budget_id ?? taskBudgetId),
527
+ typedData,
528
+ expected: {
529
+ delegateAccount: e.delegate_account,
530
+ chainId: e.chain_id,
531
+ tokenAddress: e.token_address,
532
+ maxAmountAtomic: e.max_amount_atomic,
533
+ recipientAddress: e.recipient_address ?? null,
534
+ expiresAt: e.expires_at,
535
+ parentDelegationHash: e.parent_delegation_hash
536
+ }
537
+ };
538
+ }
539
+ if (purpose === "close") {
540
+ const userOpHash = body.user_op_hash;
541
+ if (typeof userOpHash !== "string") return malformed("missing user_op_hash for purpose close");
542
+ if (typeof e.delegate_account !== "string" || typeof e.chain_id !== "number" || typeof e.delegation_hash !== "string") {
543
+ return malformed("expected is missing a required field for purpose close");
544
+ }
545
+ return {
546
+ purpose: "close",
547
+ taskBudgetId: String(body.task_budget_id ?? taskBudgetId),
548
+ typedData,
549
+ userOpHash,
550
+ expected: {
551
+ delegateAccount: e.delegate_account,
552
+ chainId: e.chain_id,
553
+ delegationHash: e.delegation_hash
554
+ }
555
+ };
556
+ }
557
+ return malformed(`unknown purpose '${String(purpose)}'`);
558
+ }
435
559
 
436
560
  // src/tools.ts
437
561
  var USEROP_BINDING_MISMATCH = "USEROP_BINDING_MISMATCH";
@@ -554,7 +678,14 @@ var toolSchemas = {
554
678
  // Bounded: a realistic redemption payload is ~10KB encoded; 256KB is
555
679
  // generous headroom while keeping the offline signer from materializing
556
680
  // arbitrarily large caller input.
557
- typed_data_b64: v3.z.string().min(1).max(262144).optional()
681
+ typed_data_b64: v3.z.string().min(1).max(262144).optional(),
682
+ // #3329: sign a task budget's own child delegation (purpose `open`) or
683
+ // its early-close UserOp (purpose `close`) — the signer fetches the
684
+ // exact typed data and expectation itself, exactly like payment_id.
685
+ // Mutually exclusive with payment_id / payload_hash (checked in the
686
+ // handler, since Zod's object-level refine on a raw shape used by
687
+ // several call sites is not the shape this schema is composed as).
688
+ task_budget_id: v3.z.string().min(1).optional()
558
689
  },
559
690
  haven_x402_sign_header: {
560
691
  // The parsed HTTP 402 PaymentRequired from the merchant. Typed as an object
@@ -604,7 +735,11 @@ var SIGN_DESCRIPTION = [
604
735
  "intent against a Haven-signed context \u2014 never an arbitrary typed_data payload, however it is shaped.",
605
736
  "Next: call mcp__haven__haven_submit with signature, then pass x402_binding",
606
737
  "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."
738
+ "or x402_expected is REFUSED (BARE_HASH_REFUSED): a hash carries nothing this signer can verify.",
739
+ "TASK BUDGETS (#3329): pass task_budget_id ALONE \u2014 mutually exclusive with payment_id and",
740
+ "payload_hash \u2014 to sign a task budget\u2019s own child delegation (opening it) or its early-close",
741
+ "UserOp; this signer fetches the exact typed data itself. Result then carries next_tool_name",
742
+ "(haven_submit) and next_arguments { task_budget_id, signature } instead of x402_binding."
608
743
  ].join(" ");
609
744
  var X402_SIGN_HEADER_DESCRIPTION = [
610
745
  "Build and sign the EIP-3009 merchant payment header for the merchant leg of an x402 payment.",
@@ -768,9 +903,84 @@ function createToolHandlers(signer, options = {}) {
768
903
  throw err;
769
904
  }
770
905
  }
906
+ async function signTaskBudget(taskBudgetId) {
907
+ const identity = await options.signContext?.loadIdentity() ?? null;
908
+ if (!identity) {
909
+ throw new edge.HavenSigningError(
910
+ `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.`
911
+ );
912
+ }
913
+ const ctx = await fetchTaskBudgetSignContext(
914
+ identity,
915
+ taskBudgetId,
916
+ options.signContext?.fetchImpl,
917
+ void 0,
918
+ options.signContext?.clientIdentity
919
+ );
920
+ let signature;
921
+ if (ctx.purpose === "open") {
922
+ try {
923
+ edge.assertOwnTaskChild(
924
+ ctx.typedData,
925
+ {
926
+ chainId: ctx.expected.chainId,
927
+ tokenAddress: ctx.expected.tokenAddress,
928
+ maxAmountAtomic: ctx.expected.maxAmountAtomic,
929
+ recipientAddress: ctx.expected.recipientAddress,
930
+ expiresAt: ctx.expected.expiresAt,
931
+ parentDelegationHash: ctx.expected.parentDelegationHash
932
+ },
933
+ signer.delegateAddress
934
+ );
935
+ } catch (err) {
936
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
937
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
938
+ }
939
+ signature = await signer.signDelegationTypedData(ctx.typedData);
940
+ } else {
941
+ try {
942
+ edge.assertUserOpTypedDataBinding(ctx.typedData, ctx.userOpHash);
943
+ } catch (err) {
944
+ if (err instanceof edge.HavenUserOpBindingError) throw new HavenUserOpBindingRefusedError(err.message);
945
+ throw err;
946
+ }
947
+ try {
948
+ edge.assertOwnTaskBudgetCloseUserOp(
949
+ ctx.typedData,
950
+ { delegationHash: ctx.expected.delegationHash },
951
+ signer.delegateAddress
952
+ );
953
+ } catch (err) {
954
+ if (err instanceof edge.HavenTypedDataRefusedError) throw err;
955
+ throw new edge.HavenTypedDataRefusedError(err instanceof Error ? err.message : String(err));
956
+ }
957
+ signature = await signer.signDelegationTypedData(ctx.typedData);
958
+ }
959
+ await auditSigning("haven_sign", viem.hashTypedData(ctx.typedData));
960
+ return {
961
+ signature,
962
+ task_budget_id: ctx.taskBudgetId,
963
+ purpose: ctx.purpose,
964
+ next_tool_name: "haven_submit",
965
+ next_tool_server_role: "hosted",
966
+ next_arguments: { task_budget_id: ctx.taskBudgetId, signature }
967
+ };
968
+ }
771
969
  return {
772
970
  haven_sign: async (input) => runTool(async () => {
773
971
  const args = parse("haven_sign", coerceX402Expected(input));
972
+ if (args.task_budget_id) {
973
+ if (args.payment_id || args.payload_hash) {
974
+ throw new v3.z.ZodError([
975
+ {
976
+ code: v3.z.ZodIssueCode.custom,
977
+ path: ["task_budget_id"],
978
+ message: "task_budget_id is mutually exclusive with payment_id / payload_hash \u2014 pass exactly one."
979
+ }
980
+ ]);
981
+ }
982
+ return signTaskBudget(args.task_budget_id);
983
+ }
774
984
  const resolved = await resolveSignContext(args, { allowDirectFallback: true });
775
985
  const typedData = resolved?.ctx.typedData ?? resolveTypedData(args);
776
986
  const payloadHash = resolved?.ctx.payloadHash ?? args.payload_hash;
@@ -1041,7 +1251,7 @@ function normalizeError(err) {
1041
1251
 
1042
1252
  // src/consent.ts
1043
1253
  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)",
1254
+ 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
1255
  haven_x402_sign_header: "builds and signs the EIP-3009 merchant payment header for an x402 payment",
1046
1256
  haven_sign_x402: "the two above in one call: funding payload + merchant header",
1047
1257
  haven_sign_sweep_delegate: "signs a Haven-prepared gasless sweep of stranded funds back to your own account"
@@ -1573,7 +1783,8 @@ function signerCompatibility() {
1573
1783
  return {
1574
1784
  x402_expected_context_versions: [...SUPPORTED_X402_EXPECTED_VERSIONS],
1575
1785
  sweep_binding_versions: [...SUPPORTED_SWEEP_BINDING_VERSIONS],
1576
- direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS]
1786
+ direct_sign_context_versions: [...SUPPORTED_DIRECT_SIGN_CONTEXT_VERSIONS],
1787
+ task_sign_context_versions: [...SUPPORTED_TASK_SIGN_CONTEXT_VERSIONS]
1577
1788
  };
1578
1789
  }
1579
1790
  function signerCapabilityAdvertisement() {
@@ -1592,6 +1803,7 @@ function signerInstructions() {
1592
1803
  `- x402 expected-context versions supported: ${compatibility.x402_expected_context_versions.join(", ")}`,
1593
1804
  `- sweep authorization binding versions supported: ${compatibility.sweep_binding_versions.join(", ")}`,
1594
1805
  `- 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`,
1806
+ `- 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
1807
  "",
1596
1808
  "Haven quote and prepare results report the expected-context version they will emit",
1597
1809
  "(signer_compatibility.x402_expected_context_version). If that version is not in the list",
@@ -1709,7 +1921,7 @@ async function warnIfCredentialFilePermissive(path, log = (message) => process.s
1709
1921
 
1710
1922
  // src/server.ts
1711
1923
  var SIGNER_NAME = "@haven_ai/signer";
1712
- var SIGNER_VERSION = "0.5.0-alpha.1";
1924
+ var SIGNER_VERSION = "0.6.0-alpha.0";
1713
1925
  async function resolveSignerRuntime(options = {}) {
1714
1926
  assertSupportedNodeVersion(options.nodeVersion);
1715
1927
  if (options.delegateKey) {