agentic-wallet-mcp 0.14.0 → 0.14.2

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.
@@ -2982,7 +2982,7 @@ var require_compile = __commonJS({
2982
2982
  const schOrFunc = root.refs[ref];
2983
2983
  if (schOrFunc)
2984
2984
  return schOrFunc;
2985
- let _sch = resolve.call(this, root, ref);
2985
+ let _sch = resolve2.call(this, root, ref);
2986
2986
  if (_sch === void 0) {
2987
2987
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2988
2988
  const { schemaId } = this.opts;
@@ -3009,7 +3009,7 @@ var require_compile = __commonJS({
3009
3009
  function sameSchemaEnv(s1, s2) {
3010
3010
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3011
3011
  }
3012
- function resolve(root, ref) {
3012
+ function resolve2(root, ref) {
3013
3013
  let sch;
3014
3014
  while (typeof (sch = this.refs[ref]) == "string")
3015
3015
  ref = sch;
@@ -3640,7 +3640,7 @@ var require_fast_uri = __commonJS({
3640
3640
  }
3641
3641
  return uri;
3642
3642
  }
3643
- function resolve(baseURI, relativeURI, options) {
3643
+ function resolve2(baseURI, relativeURI, options) {
3644
3644
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3645
3645
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3646
3646
  schemelessOptions.skipEscape = true;
@@ -3898,7 +3898,7 @@ var require_fast_uri = __commonJS({
3898
3898
  var fastUri = {
3899
3899
  SCHEMES,
3900
3900
  normalize: normalize3,
3901
- resolve,
3901
+ resolve: resolve2,
3902
3902
  resolveComponent,
3903
3903
  equal,
3904
3904
  serialize,
@@ -7030,7 +7030,7 @@ var require_aspromise = __commonJS({
7030
7030
  var params = new Array(arguments.length - 1), offset = 0, index = 2, pending = true;
7031
7031
  while (index < arguments.length)
7032
7032
  params[offset++] = arguments[index++];
7033
- return new Promise(function executor(resolve, reject) {
7033
+ return new Promise(function executor(resolve2, reject) {
7034
7034
  params[offset] = function callback(err) {
7035
7035
  if (pending) {
7036
7036
  pending = false;
@@ -7040,7 +7040,7 @@ var require_aspromise = __commonJS({
7040
7040
  var params2 = new Array(arguments.length - 1), offset2 = 0;
7041
7041
  while (offset2 < params2.length)
7042
7042
  params2[offset2++] = arguments[offset2];
7043
- resolve.apply(null, params2);
7043
+ resolve2.apply(null, params2);
7044
7044
  }
7045
7045
  }
7046
7046
  };
@@ -8568,7 +8568,7 @@ var require_path = __commonJS({
8568
8568
  return prefix + parts.join("/");
8569
8569
  }
8570
8570
  );
8571
- path.resolve = function resolve(originPath, includePath, alreadyNormalized) {
8571
+ path.resolve = function resolve2(originPath, includePath, alreadyNormalized) {
8572
8572
  if (!alreadyNormalized)
8573
8573
  includePath = normalize3(includePath);
8574
8574
  if (isAbsolute(includePath))
@@ -8848,7 +8848,7 @@ var require_field = __commonJS({
8848
8848
  keepComments ? this.comment : void 0
8849
8849
  ]);
8850
8850
  };
8851
- Field.prototype.resolve = function resolve() {
8851
+ Field.prototype.resolve = function resolve2() {
8852
8852
  if (this.resolved)
8853
8853
  return this;
8854
8854
  if ((this.typeDefault = types.defaults[this.type]) === void 0) {
@@ -9252,7 +9252,7 @@ var require_mapfield = __commonJS({
9252
9252
  keepComments ? this.comment : void 0
9253
9253
  ]);
9254
9254
  };
9255
- MapField.prototype.resolve = function resolve() {
9255
+ MapField.prototype.resolve = function resolve2() {
9256
9256
  if (this.resolved)
9257
9257
  return this;
9258
9258
  if (types.mapKey[this.keyType] === void 0)
@@ -9329,7 +9329,7 @@ var require_method = __commonJS({
9329
9329
  this.parsedOptions
9330
9330
  ]);
9331
9331
  };
9332
- Method.prototype.resolve = function resolve() {
9332
+ Method.prototype.resolve = function resolve2() {
9333
9333
  if (this.resolved)
9334
9334
  return this;
9335
9335
  this.resolvedRequestType = this.parent.lookupType(this.requestType);
@@ -10610,7 +10610,7 @@ var require_object = __commonJS({
10610
10610
  this.parent = null;
10611
10611
  this.resolved = false;
10612
10612
  };
10613
- ReflectionObject.prototype.resolve = function resolve() {
10613
+ ReflectionObject.prototype.resolve = function resolve2() {
10614
10614
  if (this.resolved)
10615
10615
  return this;
10616
10616
  if (this.root instanceof Root)
@@ -20049,7 +20049,7 @@ var require_sha3 = __commonJS({
20049
20049
  function _interopRequireDefault(obj) {
20050
20050
  return obj && obj.__esModule ? obj : { "default": obj };
20051
20051
  }
20052
- var createHash10 = function createHash11(_ref) {
20052
+ var createHash12 = function createHash13(_ref) {
20053
20053
  var allowedSizes = _ref.allowedSizes, defaultSize = _ref.defaultSize, padding = _ref.padding;
20054
20054
  return function Hash() {
20055
20055
  var _this = this;
@@ -20088,11 +20088,11 @@ var require_sha3 = __commonJS({
20088
20088
  return this;
20089
20089
  };
20090
20090
  };
20091
- var Keccak = createHash10({ allowedSizes: [224, 256, 384, 512], defaultSize: 512, padding: 1 });
20091
+ var Keccak = createHash12({ allowedSizes: [224, 256, 384, 512], defaultSize: 512, padding: 1 });
20092
20092
  exports2.Keccak = Keccak;
20093
- var SHA3 = createHash10({ allowedSizes: [224, 256, 384, 512], defaultSize: 512, padding: 6 });
20093
+ var SHA3 = createHash12({ allowedSizes: [224, 256, 384, 512], defaultSize: 512, padding: 6 });
20094
20094
  exports2.SHA3 = SHA3;
20095
- var SHAKE = createHash10({ allowedSizes: [128, 256], defaultSize: 256, padding: 31 });
20095
+ var SHAKE = createHash12({ allowedSizes: [128, 256], defaultSize: 256, padding: 31 });
20096
20096
  exports2.SHAKE = SHAKE;
20097
20097
  var SHA3Hash = Keccak;
20098
20098
  exports2.SHA3Hash = SHA3Hash;
@@ -31382,16 +31382,16 @@ __export(index_exports, {
31382
31382
  describeAssetAmount: () => describeAssetAmount
31383
31383
  });
31384
31384
  module.exports = __toCommonJS(index_exports);
31385
- var import_node_crypto10 = require("node:crypto");
31385
+ var import_node_crypto13 = require("node:crypto");
31386
31386
  var import_node_fs = require("node:fs");
31387
- var import_promises7 = require("node:fs/promises");
31387
+ var import_promises10 = require("node:fs/promises");
31388
31388
  var import_node_os2 = require("node:os");
31389
- var import_node_path8 = require("node:path");
31389
+ var import_node_path10 = require("node:path");
31390
31390
 
31391
31391
  // package.json
31392
31392
  var package_default = {
31393
31393
  name: "agentic-wallet-mcp",
31394
- version: "0.14.0",
31394
+ version: "0.14.2",
31395
31395
  description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
31396
31396
  keywords: [
31397
31397
  "mcp",
@@ -38773,7 +38773,7 @@ var Protocol = class {
38773
38773
  return;
38774
38774
  }
38775
38775
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
38776
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
38776
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
38777
38777
  options?.signal?.throwIfAborted();
38778
38778
  }
38779
38779
  } catch (error2) {
@@ -38790,7 +38790,7 @@ var Protocol = class {
38790
38790
  */
38791
38791
  request(request, resultSchema, options) {
38792
38792
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
38793
- return new Promise((resolve, reject) => {
38793
+ return new Promise((resolve2, reject) => {
38794
38794
  const earlyReject = (error2) => {
38795
38795
  reject(error2);
38796
38796
  };
@@ -38868,7 +38868,7 @@ var Protocol = class {
38868
38868
  if (!parseResult.success) {
38869
38869
  reject(parseResult.error);
38870
38870
  } else {
38871
- resolve(parseResult.data);
38871
+ resolve2(parseResult.data);
38872
38872
  }
38873
38873
  } catch (error2) {
38874
38874
  reject(error2);
@@ -39129,12 +39129,12 @@ var Protocol = class {
39129
39129
  }
39130
39130
  } catch {
39131
39131
  }
39132
- return new Promise((resolve, reject) => {
39132
+ return new Promise((resolve2, reject) => {
39133
39133
  if (signal.aborted) {
39134
39134
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
39135
39135
  return;
39136
39136
  }
39137
- const timeoutId = setTimeout(resolve, interval);
39137
+ const timeoutId = setTimeout(resolve2, interval);
39138
39138
  signal.addEventListener("abort", () => {
39139
39139
  clearTimeout(timeoutId);
39140
39140
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -40004,12 +40004,12 @@ var StdioServerTransport = class {
40004
40004
  this.onclose?.();
40005
40005
  }
40006
40006
  send(message) {
40007
- return new Promise((resolve) => {
40007
+ return new Promise((resolve2) => {
40008
40008
  const json = serializeMessage(message);
40009
40009
  if (this._stdout.write(json)) {
40010
- resolve();
40010
+ resolve2();
40011
40011
  } else {
40012
- this._stdout.once("drain", resolve);
40012
+ this._stdout.once("drain", resolve2);
40013
40013
  }
40014
40014
  });
40015
40015
  }
@@ -40860,10 +40860,10 @@ async function fetchTemplateFields(templateId, registryAddress, nodeBaseUrl, que
40860
40860
  var WalletBeError = class extends Error {
40861
40861
  errorCode;
40862
40862
  cause;
40863
- constructor(message, errorCode, cause) {
40863
+ constructor(message, errorCode2, cause) {
40864
40864
  super(message);
40865
40865
  this.name = "WalletBeError";
40866
- this.errorCode = errorCode;
40866
+ this.errorCode = errorCode2;
40867
40867
  this.cause = cause;
40868
40868
  }
40869
40869
  };
@@ -41305,6 +41305,15 @@ async function resolveIssuerProofKeys(vc, resolver) {
41305
41305
  return { bbsPublicKey, ed25519PublicKey };
41306
41306
  }
41307
41307
 
41308
+ // src/orchestrator/birthcert-choice.ts
41309
+ function aiBirthcertChoice(kind, opts = {}) {
41310
+ const note = `AI Birthcert comes in two kinds. BASIC: self-declared by the agent, with no check of the owner's identity; it is quick, shows only the agent's username, and does not mean the owner was verified. VERIFIED: the owner's identity is confirmed through MyDigital ID; the fee is paid up front, the owner completes a check in the MyID app, and the agent name must be unique (the name is only checked after payment, so a name that turns out to be taken is refused late, and whether the fee was spent cannot be confirmed). Each kind has its own price, read from the check for that kind: never assume Basic is free. This result is for the ${kind === "basic" ? "BASIC" : "VERIFIED"} one. If the user has not said which kind they want (they did not say "verified" or "basic", and did not mention MyDigital ID or owner verification), do not start either: run this check for both kinds (it is free) to see what the wallet already holds, tell the user the difference in plain words, offer only the kind or kinds they do not already hold, and ask which they prefer. If they want both, issue them one after the other, Basic first, and finish and show the first before starting the second; each has its own price and needs its own yes, so "both" is not a yes to either payment. Holding one kind never blocks the other, and replacing one that is held is its own explicit choice.`;
41311
+ return opts.unavailable ? note + " The Verified kind is not available on this wallet or network: say so, and offer the Basic one only." : note;
41312
+ }
41313
+
41314
+ // src/mcp-tools.ts
41315
+ var import_promises4 = require("node:fs/promises");
41316
+
41308
41317
  // src/orchestrator/prove.ts
41309
41318
  async function proveIdentity(wallet, proofRequestHeader, holderDid) {
41310
41319
  const pr = await wallet.respondToChallenge(proofRequestHeader, holderDid);
@@ -41615,7 +41624,7 @@ var RECOVERY_MAX_POLLS = 5;
41615
41624
  var RECOVERY_POLL_DELAY_MS = 2e3;
41616
41625
  async function pollSettlementOutcome(deps, paymentId) {
41617
41626
  if (!deps.mbi.getStatus) return void 0;
41618
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
41627
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
41619
41628
  let last;
41620
41629
  for (let polls = 1; polls <= RECOVERY_MAX_POLLS; polls++) {
41621
41630
  let status;
@@ -42236,7 +42245,7 @@ var TEMPLATE_ALIASES = [
42236
42245
  deriveAttributes: { id: "agentUsername" },
42237
42246
  validateAttributes: { dob: validateDob, countryOfOrigin: validateCountryOfOrigin },
42238
42247
  passDesignId: {
42239
- testnet: "did:zid:992e1e18985ba36a09ba0fbfeb601ddeb449f5e4e882ed69d0d620085f399818",
42248
+ testnet: "did:zid:bb7202482ef6053be6d46ad8e2190b0ba1e3b2024d24ce7f37bc4c3747e2378f",
42240
42249
  mainnet: "did:zid:915955cb71c6fd2a256d04344f57381084903cb6a85a1c9ddb71c746f08932ab"
42241
42250
  }
42242
42251
  }
@@ -42301,6 +42310,8 @@ async function credentialPreflight(deps, input) {
42301
42310
  const blockers = [];
42302
42311
  const notChecked = [];
42303
42312
  const isVerified = input.credential === VERIFIED_AI_BIRTHCERT;
42313
+ const kind = isVerified ? "verified" : deps.isBasicAiBirthcert?.(input.credential) ? "basic" : void 0;
42314
+ const choice = kind ? { choice: aiBirthcertChoice(kind) } : {};
42304
42315
  let alreadyHeld;
42305
42316
  if (deps.findHeld) {
42306
42317
  try {
@@ -42318,7 +42329,8 @@ async function credentialPreflight(deps, input) {
42318
42329
  blockers: [
42319
42330
  `This wallet already holds a valid ${found.label}${until}${found.vcId ? `, ${found.vcId}` : ""}. Tell the user, and do not start another: a new one would REPLACE it (the wallet keeps one per template) and costs the fee. Only if the user explicitly says they want a replacement, run this check again with replacing: true.`
42320
42331
  ],
42321
- notChecked: []
42332
+ notChecked: [],
42333
+ ...choice
42322
42334
  };
42323
42335
  }
42324
42336
  } else {
@@ -42344,7 +42356,8 @@ async function credentialPreflight(deps, input) {
42344
42356
  ...alreadyHeld ? { alreadyHeld } : {},
42345
42357
  ...schema ? { schema } : {},
42346
42358
  blockers: [`Could not price this credential: ${why}`],
42347
- notChecked: [...notChecked, ...uncheckable(isVerified)]
42359
+ notChecked: [...notChecked, ...uncheckable(isVerified)],
42360
+ ...choice
42348
42361
  };
42349
42362
  }
42350
42363
  const asset = String(quote.asset ?? "");
@@ -42360,12 +42373,21 @@ async function credentialPreflight(deps, input) {
42360
42373
  } else if (!isFree && BigInt(feeBalance.balance) < BigInt(requiredRaw)) {
42361
42374
  blockers.push(`Not enough ${feeBalance.token}: the fee is ${renderAmount(requiredRaw, feeBalance)}, the balance is ${feeBalance.display}.`);
42362
42375
  }
42376
+ const gasNotes = [];
42363
42377
  if (selfPaysGas && asset !== NATIVE) {
42364
42378
  const gas = balances[1];
42365
42379
  if (gas && "error" in gas) {
42366
- blockers.push(`Could not read the ZTX balance (${gas.error}) \u2014 ZTX pays network gas and is separate from the fee.`);
42380
+ const msg = `Could not read the ZTX balance (${gas.error}) \u2014 ZTX pays network gas and is separate from the fee.`;
42381
+ if (isFree) gasNotes.push(`${msg} Issuance is free right now, so none is needed unless it becomes chargeable.`);
42382
+ else blockers.push(msg);
42367
42383
  } else if (gas && BigInt(gas.balance) === 0n) {
42368
- blockers.push(`No ZTX for network gas. ZTX is separate from the ${feeBalance.token ?? asset} fee and is needed for every transaction.`);
42384
+ if (isFree) {
42385
+ gasNotes.push(
42386
+ "This wallet holds no ZTX. Issuance is free right now, so no payment is made and no gas is needed; if it becomes chargeable and this wallet pays its own gas, it will need ZTX."
42387
+ );
42388
+ } else {
42389
+ blockers.push(`No ZTX for network gas. ZTX is separate from the ${feeBalance.token ?? asset} fee and is needed for every transaction.`);
42390
+ }
42369
42391
  }
42370
42392
  }
42371
42393
  const cap = deps.policyGoverns && await deps.policyGoverns(asset) ? describePolicyGovernedCap(asset) : describePaymentCap(asset, requiredRaw, deps.caps);
@@ -42386,7 +42408,8 @@ async function credentialPreflight(deps, input) {
42386
42408
  ...alreadyHeld ? { alreadyHeld } : {},
42387
42409
  ...schema ? { schema } : {},
42388
42410
  blockers,
42389
- notChecked: [...notChecked, ...uncheckable(isVerified, isFree)]
42411
+ notChecked: [...notChecked, ...gasNotes, ...uncheckable(isVerified, isFree)],
42412
+ ...choice
42390
42413
  };
42391
42414
  }
42392
42415
  function renderAmount(raw, balance) {
@@ -42416,7 +42439,7 @@ function uncheckable(isVerified, isFree = false) {
42416
42439
  }
42417
42440
  if (isFree) {
42418
42441
  items.unshift(
42419
- "Whether issuance is still free at apply time \u2014 `paymentRequired` is a service-wide MBI setting and can change between this quote and issuance."
42442
+ "Whether issuance is still free at apply time \u2014 whether the service charges is a service-wide setting and can change between this quote and issuance."
42420
42443
  );
42421
42444
  }
42422
42445
  return items;
@@ -43962,7 +43985,7 @@ async function attachAttributeMeanings(result, readVocabulary) {
43962
43985
  }
43963
43986
 
43964
43987
  // src/orchestrator/write-policy.ts
43965
- var import_node_crypto6 = require("node:crypto");
43988
+ var import_node_crypto7 = require("node:crypto");
43966
43989
 
43967
43990
  // src/clients/policy-write-receipt-store.ts
43968
43991
  var import_node_crypto5 = require("node:crypto");
@@ -44052,6 +44075,12 @@ function parseGasPayer(value) {
44052
44075
  if (value === "self" || value === "sponsored") return { ok: true, gasPayer: value };
44053
44076
  return { ok: false };
44054
44077
  }
44078
+ function parseMaxGasRaw(value) {
44079
+ if (value === void 0 || value === null || value === "") return { ok: true };
44080
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return { ok: true, maxGasRaw: String(value) };
44081
+ if (typeof value === "string" && /^[1-9]\d{0,17}$/.test(value)) return { ok: true, maxGasRaw: value };
44082
+ return { ok: false };
44083
+ }
44055
44084
  var UINT = /^\d+$/;
44056
44085
  var RESERVE_DIVISOR = 5n;
44057
44086
  var MAX_REASON = 200;
@@ -44124,6 +44153,7 @@ async function assessSelfPay(deps, call) {
44124
44153
  }
44125
44154
 
44126
44155
  // src/orchestrator/policy-self-pay.ts
44156
+ var import_node_crypto6 = require("node:crypto");
44127
44157
  var SCALAR_OF = {
44128
44158
  STRING: "STRING",
44129
44159
  NUMBER: "NUMBER",
@@ -44132,6 +44162,7 @@ var SCALAR_OF = {
44132
44162
  NUMBER_LIST: "NUMBER",
44133
44163
  ADDRESS_LIST: "ADDRESS"
44134
44164
  };
44165
+ var SELF_PAY_CEIL_LEDGERS = 60;
44135
44166
  var MAX_ATTRIBUTES = 64;
44136
44167
  var MAX_LIST_ENTRIES = 500;
44137
44168
  function parseList2(text) {
@@ -44225,6 +44256,9 @@ function createSelfPay(chain) {
44225
44256
  },
44226
44257
  queryNativeBalance: chain.queryNativeBalance,
44227
44258
  ...chain.getTransaction ? { lookupTransaction: chain.getTransaction } : {},
44259
+ pendingSend: (policyKey) => chain.markers.get(chain.ownerAddress, policyKey),
44260
+ clearSend: (policyKey, txHash) => chain.markers.remove(chain.ownerAddress, policyKey, txHash),
44261
+ currentLedger: chain.currentLedger,
44228
44262
  async execute(call, limit) {
44229
44263
  const built = buildPolicyCall(call);
44230
44264
  if (!built.ok) return { kind: "rejected", reason: built.reason };
@@ -44250,9 +44284,17 @@ function createSelfPay(chain) {
44250
44284
  reason: `the node now estimates a fee of ${fee.feeLimit}, above the ${limit.maxFeeRaw} this wallet checked its balance against`
44251
44285
  };
44252
44286
  }
44287
+ let ceil;
44288
+ try {
44289
+ const ledger = await chain.currentLedger();
44290
+ if (typeof ledger !== "string" || !/^\d{1,18}$/.test(ledger)) throw new Error("the ledger number is not available");
44291
+ ceil = (BigInt(ledger) + BigInt(SELF_PAY_CEIL_LEDGERS)).toString();
44292
+ } catch (e) {
44293
+ return { kind: "rejected", reason: `the ledger number could not be read, so the transaction could not be given an expiry: ${boundedReason(e)}` };
44294
+ }
44253
44295
  let blob;
44254
44296
  try {
44255
- blob = chain.buildBlob({ sourceAddress: chain.ownerAddress, nonce, gasPrice: fee.gasPrice, feeLimit: ceiling.toString(), operations: [operation] }).blob;
44297
+ blob = chain.buildBlob({ sourceAddress: chain.ownerAddress, nonce, gasPrice: fee.gasPrice, feeLimit: ceiling.toString(), ceilLedgerSeq: ceil, operations: [operation] }).blob;
44256
44298
  } catch (e) {
44257
44299
  return { kind: "rejected", reason: `the transaction could not be built: ${boundedReason(e)}` };
44258
44300
  }
@@ -44264,12 +44306,37 @@ function createSelfPay(chain) {
44264
44306
  if (refusal) return { kind: "refused", refusal };
44265
44307
  return { kind: "sign_failed", reason: boundedReason(e) };
44266
44308
  }
44309
+ const txHash = (0, import_node_crypto6.createHash)("sha256").update(Buffer.from(blob, "hex")).digest("hex");
44310
+ const marker = {
44311
+ policyKey: call.policyKey,
44312
+ ownerAddress: chain.ownerAddress,
44313
+ operation: call.operation,
44314
+ nonce,
44315
+ txHash,
44316
+ ceilLedgerSeq: ceil,
44317
+ submittedAt: (chain.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
44318
+ };
44319
+ try {
44320
+ await chain.markers.set(marker);
44321
+ } catch (e) {
44322
+ try {
44323
+ await chain.markers.remove(chain.ownerAddress, call.policyKey, txHash);
44324
+ } catch {
44325
+ }
44326
+ return { kind: "rejected", reason: `the send could not be recorded, so nothing was sent: ${boundedReason(e)}` };
44327
+ }
44267
44328
  try {
44268
44329
  const { hash } = await chain.submit({ blob, signBlob: signed.signBlob, publicKey: signed.publicKey });
44269
- return { kind: "submitted", txHash: hash, nonce, feeLimit: ceiling.toString() };
44330
+ return { kind: "submitted", txHash: hash, nonce, feeLimit: ceiling.toString(), ceilLedgerSeq: ceil };
44270
44331
  } catch (e) {
44271
- if (isSubmitRejection(e)) return { kind: "rejected", reason: `the node rejected the transaction: ${boundedReason(e)}` };
44272
- return { kind: "unknown", reason: boundedReason(e), nonce };
44332
+ if (isSubmitRejection(e)) {
44333
+ try {
44334
+ await chain.markers.remove(chain.ownerAddress, call.policyKey, txHash);
44335
+ } catch {
44336
+ }
44337
+ return { kind: "rejected", reason: `the node rejected the transaction: ${boundedReason(e)}` };
44338
+ }
44339
+ return { kind: "unknown", reason: boundedReason(e), nonce, txHash, ceilLedgerSeq: ceil };
44273
44340
  }
44274
44341
  }
44275
44342
  };
@@ -44530,6 +44597,8 @@ function isDryRun(value) {
44530
44597
  async function writePolicy(deps, input) {
44531
44598
  const gas = parseGasPayer(input?.gasPayer);
44532
44599
  if (!gas.ok) return refusedGasPayer(input?.policyKey);
44600
+ const maxGas = parseMaxGasRaw(input?.maxGasRaw);
44601
+ if (!maxGas.ok) return refusedMaxGas(input?.policyKey);
44533
44602
  if (!deps.client && gas.gasPayer !== "self" && !(gas.gasPayer === void 0 && deps.selfPay)) {
44534
44603
  return unavailable(
44535
44604
  `No policy write service is configured for ${deps.network ?? "this network"}, so nothing was attempted and nothing was paid.`
@@ -44561,7 +44630,7 @@ async function writePolicy(deps, input) {
44561
44630
  ...input.validToBlock !== void 0 ? { validToBlock: input.validToBlock } : {},
44562
44631
  // A sha256 hex string: the service's column is 128 characters, and the old `${owner}-${policyKey}-${ISO}` overflowed it
44563
44632
  // for a policy key longer than about 65. Still unique per owner, key and moment.
44564
- requestKey: input.requestKey ?? (0, import_node_crypto6.createHash)("sha256").update(`${deps.ownerAddress}-${input.policyKey}-${(deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString()}`).digest("hex")
44633
+ requestKey: input.requestKey ?? (0, import_node_crypto7.createHash)("sha256").update(`${deps.ownerAddress}-${input.policyKey}-${(deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString()}`).digest("hex")
44565
44634
  };
44566
44635
  const supplied = input.templateContractAddress;
44567
44636
  const omitted = supplied === void 0 || supplied === null || typeof supplied === "string" && supplied.trim() === "";
@@ -44616,7 +44685,8 @@ async function writePolicy(deps, input) {
44616
44685
  pollBudgetMs: input.pollBudgetMs ?? DEFAULT_BUDGET_MS,
44617
44686
  tool: "write_policy",
44618
44687
  verb: "write",
44619
- ...selfUnavailable ? { unavailable: selfUnavailable } : {}
44688
+ ...selfUnavailable ? { unavailable: selfUnavailable } : {},
44689
+ ...maxGas.maxGasRaw ? { maxGasRaw: maxGas.maxGasRaw } : {}
44620
44690
  });
44621
44691
  }
44622
44692
  if (!deps.client) {
@@ -44681,7 +44751,7 @@ async function writePolicy(deps, input) {
44681
44751
  ...gas.gasPayer ? { gasPayer: gas.gasPayer } : {}
44682
44752
  });
44683
44753
  }
44684
- async function recoverInFlight(deps, client, a) {
44754
+ async function recoverInFlightLocked(deps, client, a) {
44685
44755
  const { pre } = a;
44686
44756
  const known = await deps.receipts.get(pre.blobId);
44687
44757
  const receipt = {
@@ -44712,6 +44782,8 @@ async function recoverInFlight(deps, client, a) {
44712
44782
  message: `${a.dryRun ? "DRY RUN" : "NOT CONFIRMED"} \u2014 nothing was collected. A write under "${a.policyKey}" has ALREADY BEEN PAID FOR and is waiting to be collected; the service said: "${clip2(pre.detail)}". Pass the receipt below to check_policy_write to finish it. Do not pay again.`
44713
44783
  };
44714
44784
  }
44785
+ const heldCollect = await holdForSelfPaySend(deps, a.policyKey, COLLECT_HELD);
44786
+ if (heldCollect) return { ...heldCollect, paid: true, paymentReceipt: pre.blobId, policyKey: a.policyKey, interpretation: a.interpretation };
44715
44787
  const stored = known ? operationOf(known) : void 0;
44716
44788
  if (stored !== void 0 && pre.operation !== void 0 && pre.operation !== stored) {
44717
44789
  return {
@@ -44771,6 +44843,13 @@ async function describeQuote(deps, accept) {
44771
44843
  }
44772
44844
  return { asset, amount, extra, amountHuman, affordability };
44773
44845
  }
44846
+ function refusedMaxGas(policyKey) {
44847
+ return {
44848
+ state: "refused",
44849
+ ...typeof policyKey === "string" ? { policyKey } : {},
44850
+ message: "maxGasRaw must be a positive whole number of raw units (micro-ZTX): the requiredRaw of the self option the user was shown, or be left out. NOTHING WAS PAID."
44851
+ };
44852
+ }
44774
44853
  function refusedGasPayer(policyKey) {
44775
44854
  return {
44776
44855
  state: "refused",
@@ -44799,7 +44878,7 @@ function selfPayNotAvailable(a) {
44799
44878
  function selfPayWords(self2) {
44800
44879
  const gas = self2.gas;
44801
44880
  if (self2.status === "enough") {
44802
- return `Costs no fee, only about ${gas.estimatedHuman} of network gas (the wallet asks to hold ${gas.requiredHuman}); it holds ${self2.balance?.human}, enough.`;
44881
+ return `Costs no fee, only about ${gas.estimatedHuman} of network gas (the wallet asks to hold ${gas.requiredHuman}, the estimate plus a fifth, and that is the figure to confirm); it holds ${self2.balance?.human}, enough.`;
44803
44882
  }
44804
44883
  return `Costs no fee, only network gas in ZTX${gas.estimatedHuman ? ` (about ${gas.estimatedHuman})` : ""}. ${self2.status === "short" ? "The wallet could NOT pay it right now" : "Whether the wallet can pay it is not known"}: ${clip2(self2.problems.join(" "), 400)}`;
44805
44884
  }
@@ -44844,7 +44923,7 @@ async function offerGasChoice(deps, a, accept) {
44844
44923
  } : {},
44845
44924
  ...self2 ? { self: { fee: "none", ...self2 } } : {}
44846
44925
  },
44847
- message: `CHOICE NEEDED \u2014 nothing was paid and nothing was written. ` + (both ? `There are two ways to pay to ${a.verb} this policy; ask the user which they want. ` : `Only ${quote ? "sponsored" : "self"} is available to ${a.verb} this policy right now; tell the user that and ask whether to go ahead that way. `) + `${sponsoredText} ${selfText} Say plainly what the wallet can pay right now. After the user has seen what the policy means (interpretation) and chosen, call ${a.tool} again with gasPayer ${both ? '"sponsored" or "self"' : only} and confirm: true. Never choose for the user and never pass confirm on your own judgement.`
44926
+ message: `CHOICE NEEDED \u2014 nothing was paid and nothing was written. ` + (both ? `There are two ways to pay to ${a.verb} this policy; ask the user which they want. ` : `Only ${quote ? "sponsored" : "self"} is available to ${a.verb} this policy right now; tell the user that and ask whether to go ahead that way. `) + `${sponsoredText} ${selfText} Say plainly what the wallet can pay right now. After the user has seen what the policy means (interpretation) and chosen, call ${a.tool} again with gasPayer ${both ? '"sponsored" or "self"' : only} and confirm: true${self2 && self2.gas.requiredRaw ? `; for "self" also pass maxGasRaw "${self2.gas.requiredRaw}", the gas figure shown above, the most the user agrees to` : ""}. Never choose for the user and never pass confirm on your own judgement.`
44848
44927
  };
44849
44928
  }
44850
44929
  function chainMatchesCall(record2, call) {
@@ -44906,7 +44985,144 @@ async function confirmSelfWrite(deps, call, budgetMs, txHash) {
44906
44985
  waited += wait;
44907
44986
  }
44908
44987
  }
44988
+ var selfPayLocks = /* @__PURE__ */ new Map();
44989
+ var SELF_PAY_LOCK_WAIT_MS = 15e3;
44990
+ async function withSelfPayLock(key, waitMs, fn) {
44991
+ const before = (selfPayLocks.get(key) ?? Promise.resolve()).catch(() => void 0);
44992
+ let release;
44993
+ const mine = new Promise((resolve2) => {
44994
+ release = resolve2;
44995
+ });
44996
+ const tail = Promise.all([before, mine]);
44997
+ selfPayLocks.set(key, tail);
44998
+ let timer;
44999
+ const acquired = await Promise.race([
45000
+ before.then(() => true),
45001
+ new Promise((resolve2) => {
45002
+ timer = setTimeout(() => resolve2(false), waitMs);
45003
+ })
45004
+ ]);
45005
+ clearTimeout(timer);
45006
+ const done = () => {
45007
+ release();
45008
+ void tail.then(() => {
45009
+ if (selfPayLocks.get(key) === tail) selfPayLocks.delete(key);
45010
+ });
45011
+ };
45012
+ if (!acquired) {
45013
+ done();
45014
+ return { kind: "busy" };
45015
+ }
45016
+ try {
45017
+ return { kind: "ran", value: await fn() };
45018
+ } finally {
45019
+ done();
45020
+ }
45021
+ }
45022
+ var lockWaitMs = (deps) => deps.lockWaitMs ?? SELF_PAY_LOCK_WAIT_MS;
45023
+ var busyMessage = (policyKey, nothing) => `Another write, collect or removal for "${policyKey}" is in progress in this wallet, so ${nothing}. Ask again shortly.`;
45024
+ async function holdForSelfPaySend(deps, policyKey, nothing) {
45025
+ const inFlight = await resolveInFlight(deps, policyKey);
45026
+ if (inFlight.kind === "none") return void 0;
45027
+ if (inFlight.kind === "unreadable") {
45028
+ return {
45029
+ state: "unavailable",
45030
+ message: `Whether an earlier self-paid write for "${policyKey}" is still in flight could not be checked (${inFlight.detail ?? "the record of it could not be read"}), so ${nothing}. A transaction that is still in flight could overwrite this.`
45031
+ };
45032
+ }
45033
+ const m = inFlight.marker;
45034
+ return {
45035
+ state: "refused",
45036
+ txHash: m.txHash,
45037
+ message: `A self-paid ${m.operation === "UPDATE" ? "update" : "write"} for "${policyKey}" was sent at ${m.submittedAt} (transaction ${m.txHash}, nonce ${m.nonce}) and its outcome is not known yet. It can still be included until ledger ${m.ceilLedgerSeq}${inFlight.ledger ? ` (the ledger is now ${inFlight.ledger})` : ""}, and it would replace the whole policy, so ${nothing}. Read the policy with get_my_policy to see whether it landed, or ask again after ledger ${m.ceilLedgerSeq}, when it can no longer land.`
45038
+ };
45039
+ }
45040
+ async function resolveInFlight(deps, policyKey, mayClear = true) {
45041
+ const sp = deps.selfPay;
45042
+ if (!sp?.pendingSend) return { kind: "none" };
45043
+ let marker;
45044
+ try {
45045
+ marker = await bounded(deps, () => sp.pendingSend(policyKey));
45046
+ } catch (e) {
45047
+ return { kind: "unreadable", detail: clip2(e instanceof Error ? e.message : String(e), 1200) };
45048
+ }
45049
+ if (!marker) return { kind: "none" };
45050
+ const settled = marker;
45051
+ const clear = async () => {
45052
+ if (!mayClear) return;
45053
+ try {
45054
+ await sp.clearSend?.(policyKey, settled.txHash);
45055
+ } catch {
45056
+ }
45057
+ };
45058
+ if (sp.lookupTransaction) {
45059
+ try {
45060
+ const lookup = sp.lookupTransaction;
45061
+ const hash = marker.txHash;
45062
+ const tx = await bounded(deps, () => lookup(hash));
45063
+ if (tx.status === "success" || tx.status === "failed") {
45064
+ await clear();
45065
+ return { kind: "none" };
45066
+ }
45067
+ } catch {
45068
+ }
45069
+ }
45070
+ let ledger;
45071
+ if (sp.currentLedger) {
45072
+ try {
45073
+ const read2 = sp.currentLedger;
45074
+ ledger = await bounded(deps, () => read2());
45075
+ } catch {
45076
+ ledger = void 0;
45077
+ }
45078
+ }
45079
+ if (typeof ledger === "string" && /^\d{1,18}$/.test(ledger) && BigInt(ledger) > BigInt(marker.ceilLedgerSeq)) {
45080
+ await clear();
45081
+ return { kind: "none" };
45082
+ }
45083
+ return { kind: "blocked", marker, ...typeof ledger === "string" && /^\d{1,18}$/.test(ledger) ? { ledger } : {} };
45084
+ }
45085
+ var selfPayLockKey = (deps, policyKey) => `${deps.ownerAddress}
45086
+ ${policyKey}`;
44909
45087
  async function runSelfPay(deps, a) {
45088
+ if (!a.canWrite) return runSelfPayLocked(deps, a);
45089
+ const r = await withSelfPayLock(selfPayLockKey(deps, a.call.policyKey), lockWaitMs(deps), () => runSelfPayLocked(deps, a));
45090
+ if (r.kind === "ran") return r.value;
45091
+ return { state: "refused", paid: false, policyKey: a.call.policyKey, interpretation: a.interpretation, message: busyMessage(a.call.policyKey, "NOTHING WAS SENT") };
45092
+ }
45093
+ async function recoverInFlight(deps, client, a) {
45094
+ if (!a.canWrite) return recoverInFlightLocked(deps, client, a);
45095
+ const r = await withSelfPayLock(selfPayLockKey(deps, a.policyKey), lockWaitMs(deps), () => recoverInFlightLocked(deps, client, a));
45096
+ if (r.kind === "ran") return r.value;
45097
+ return {
45098
+ state: "refused",
45099
+ paid: true,
45100
+ paymentReceipt: a.pre.blobId,
45101
+ policyKey: a.policyKey,
45102
+ interpretation: a.interpretation,
45103
+ message: busyMessage(a.policyKey, COLLECT_HELD)
45104
+ };
45105
+ }
45106
+ async function runPaidFlow(deps, client, a) {
45107
+ const choosing = a.gasPayer === void 0 && deps.selfPay !== void 0;
45108
+ if (!a.canWrite || choosing) return runPaidFlowLocked(deps, client, a);
45109
+ const r = await withSelfPayLock(selfPayLockKey(deps, a.policyKey), lockWaitMs(deps), () => runPaidFlowLocked(deps, client, a));
45110
+ if (r.kind === "ran") return r.value;
45111
+ return { state: "refused", paid: false, policyKey: a.policyKey, interpretation: a.interpretation, message: busyMessage(a.policyKey, "NOTHING WAS PAID") };
45112
+ }
45113
+ var COLLECT_HELD = "NOTHING WAS COLLECTED (the earlier payment is not lost: do not pay again, ask again later)";
45114
+ async function collectUnlessSelfPayHeld(deps, receipt, budgetMs) {
45115
+ const r = await withSelfPayLock(selfPayLockKey(deps, receipt.policyKey), lockWaitMs(deps), async () => {
45116
+ const applied = await checkChainFirst(deps, receipt);
45117
+ if (applied) return applied;
45118
+ const held = await holdForSelfPaySend(deps, receipt.policyKey, COLLECT_HELD);
45119
+ if (held) return { ...held, paid: true, paymentReceipt: receipt.blobId, policyKey: receipt.policyKey };
45120
+ return pollCollect(deps, deps.client, receipt, budgetMs);
45121
+ });
45122
+ if (r.kind === "ran") return r.value;
45123
+ return { state: "refused", paid: true, paymentReceipt: receipt.blobId, policyKey: receipt.policyKey, message: busyMessage(receipt.policyKey, COLLECT_HELD) };
45124
+ }
45125
+ async function runSelfPayLocked(deps, a) {
44910
45126
  const { call, interpretation } = a;
44911
45127
  const policyKey = call.policyKey;
44912
45128
  const done = a.verb === "write" ? "written" : "updated";
@@ -44934,6 +45150,22 @@ async function runSelfPay(deps, a) {
44934
45150
  message: `A paid write for "${policyKey}" is still waiting to be finished (receipt ${waiting.blobId}), and a self-paid write now could be overwritten by its collect, or overwrite it. Finish it first with check_policy_write, then ask again. NOTHING WAS SENT.`
44935
45151
  };
44936
45152
  }
45153
+ const inFlight = await resolveInFlight(deps, policyKey, a.canWrite);
45154
+ if (inFlight.kind === "unreadable") {
45155
+ return unavailable(
45156
+ 'Whether an earlier self-paid write for "' + policyKey + '" is still in flight could not be checked (' + (inFlight.detail ?? "the record of it could not be read") + "), so NOTHING WAS SENT."
45157
+ );
45158
+ }
45159
+ if (inFlight.kind === "blocked") {
45160
+ const m = inFlight.marker;
45161
+ return {
45162
+ state: "unknown",
45163
+ policyKey,
45164
+ txHash: m.txHash,
45165
+ interpretation,
45166
+ message: `A self-paid ${m.operation === "UPDATE" ? "update" : "write"} for "${policyKey}" was sent at ${m.submittedAt} (transaction ${m.txHash}, nonce ${m.nonce}) and its outcome is not known yet. It can still be included until ledger ${m.ceilLedgerSeq}${inFlight.ledger ? ` (the ledger is now ${inFlight.ledger})` : ""}, so NOTHING WAS SENT this time: a second transaction could land on top of it. Read the policy with get_my_policy to see whether it landed, or ask again after ledger ${m.ceilLedgerSeq}, when it can no longer land.`
45167
+ };
45168
+ }
44937
45169
  let current;
44938
45170
  try {
44939
45171
  current = await readBounded(deps, policyKey);
@@ -44975,7 +45207,7 @@ async function runSelfPay(deps, a) {
44975
45207
  policyKey,
44976
45208
  interpretation,
44977
45209
  gasOptions: { self: self2 },
44978
- message: `QUOTE ONLY \u2014 nothing was sent and nothing was written. Paying the gas yourself: ${selfPayWords(assessment)} ` + (!a.dryRun && !a.confirmed ? `A policy is a spending limit, so a person must agree to it first: show the user what it means (interpretation) and the cost, and only after they say yes call ${a.tool} again with gasPayer "self" and confirm: true. Never pass confirm on your own judgement.` : assessment.status === "enough" ? `Ask again without dryRun to ${a.verb} it.` : "Fix that, then ask again without dryRun.")
45210
+ message: `QUOTE ONLY \u2014 nothing was sent and nothing was written. Paying the gas yourself: ${selfPayWords(assessment)} ` + (!a.dryRun && !a.confirmed ? `A policy is a spending limit, so a person must agree to it first: show the user what it means (interpretation) and the cost, and only after they say yes call ${a.tool} again with gasPayer "self"${assessment.gas.requiredRaw ? `, maxGasRaw "${assessment.gas.requiredRaw}" (the gas figure above)` : ""} and confirm: true. Never pass confirm on your own judgement.` : assessment.status === "enough" ? `Ask again without dryRun to ${a.verb} it, with gasPayer "self" and maxGasRaw "${assessment.gas.requiredRaw}".` : "Fix that, then ask again without dryRun.")
44979
45211
  };
44980
45212
  }
44981
45213
  if (assessment.status !== "enough") {
@@ -44988,7 +45220,28 @@ async function runSelfPay(deps, a) {
44988
45220
  message: `NOTHING WAS SENT: the wallet cannot confirm it can pay the gas. ${clip2(assessment.problems.join(" "), 500)} ` + (assessment.status === "short" ? 'Top up ZTX or use gasPayer "sponsored".' : 'Fix the cause above and ask again, or use gasPayer "sponsored".')
44989
45221
  };
44990
45222
  }
44991
- const sent = await deps.selfPay.execute(call, { maxFeeRaw: assessment.gas.requiredRaw });
45223
+ const requiredRaw = assessment.gas.requiredRaw;
45224
+ if (a.maxGasRaw === void 0) {
45225
+ return {
45226
+ state: "refused",
45227
+ paid: false,
45228
+ policyKey,
45229
+ interpretation,
45230
+ gasOptions: { self: self2 },
45231
+ message: `NOTHING WAS SENT: a self-paid write needs maxGasRaw, the most gas the user agreed to, so it can never cost more than they were shown. The gas the wallet asks to hold for this write is now ${assessment.gas.requiredHuman} (${requiredRaw} raw: the estimate plus a fifth). Show the user that figure and, if they agree, call ${a.tool} again with gasPayer "self", maxGasRaw "${requiredRaw}" and confirm: true.`
45232
+ };
45233
+ }
45234
+ if (BigInt(requiredRaw) > BigInt(a.maxGasRaw)) {
45235
+ return {
45236
+ state: "refused",
45237
+ paid: false,
45238
+ policyKey,
45239
+ interpretation,
45240
+ gasOptions: { self: self2 },
45241
+ message: `NOTHING WAS SENT: the gas the wallet asks to hold for this write is now ${assessment.gas.requiredHuman} (${requiredRaw} raw: the estimate plus a fifth), more than the ${a.maxGasRaw} raw the user agreed to. Show the user the new figure and, if they agree, call ${a.tool} again with gasPayer "self", maxGasRaw "${requiredRaw}" and confirm: true.`
45242
+ };
45243
+ }
45244
+ const sent = await deps.selfPay.execute(call, { maxFeeRaw: requiredRaw });
44992
45245
  switch (sent.kind) {
44993
45246
  case "rejected":
44994
45247
  return { state: "refused", paid: false, policyKey, interpretation, message: `NOTHING WAS SENT and no gas was spent: ${sent.reason}. Fix the cause and ask again.` };
@@ -45018,11 +45271,18 @@ async function runSelfPay(deps, a) {
45018
45271
  return {
45019
45272
  state: "unknown",
45020
45273
  policyKey,
45274
+ txHash: sent.txHash,
45021
45275
  interpretation,
45022
- message: `The transaction was sent but the node's answer never arrived (${sent.reason}), so it MAY be on chain and the gas MAY be spent. Do NOT send it again. Read the policy with get_my_policy to see whether it took effect (sent with nonce ${sent.nonce}).`
45276
+ message: `The transaction was sent but the node's answer never arrived (${sent.reason}), so it MAY be on chain and the gas MAY be spent. Do NOT send it again. Read the policy with get_my_policy to see whether it took effect (transaction ${sent.txHash}, nonce ${sent.nonce}). It can be included only until ledger ${sent.ceilLedgerSeq}; this wallet remembers it and will not send another for this key until it has landed or can no longer land.`
45023
45277
  };
45024
45278
  case "submitted": {
45025
45279
  const landed = await confirmSelfWrite(deps, call, a.pollBudgetMs, sent.txHash);
45280
+ if (landed.kind !== "not_yet") {
45281
+ try {
45282
+ await deps.selfPay.clearSend?.(policyKey, sent.txHash);
45283
+ } catch {
45284
+ }
45285
+ }
45026
45286
  if (landed.kind === "failed") {
45027
45287
  return {
45028
45288
  state: "write_failed",
@@ -45054,7 +45314,7 @@ async function runSelfPay(deps, a) {
45054
45314
  }
45055
45315
  }
45056
45316
  }
45057
- async function runPaidFlow(deps, client, a) {
45317
+ async function runPaidFlowLocked(deps, client, a) {
45058
45318
  const { policyKey, interpretation, dryRun, confirmed, canWrite } = a;
45059
45319
  const accepts = a.challenge.accepts ?? [];
45060
45320
  const offersChoice = a.gasPayer === void 0 && deps.selfPay !== void 0;
@@ -45088,6 +45348,8 @@ async function runPaidFlow(deps, client, a) {
45088
45348
  message: `QUOTE ONLY \u2014 nothing was paid and nothing was written. The service asked for ${amountHuman ?? `${amount} of ${asset}`} to ${a.verb} this policy. That is what it asked for just now: it can change, and it is not a promise the payment will be allowed, because ` + (worded ? `balances can change and the payment cap is enforced again when paying. ${worded} ` : `the wallet's payment cap is applied only when paying. `) + (!dryRun && !confirmed ? `A policy is a spending limit, so a person must agree to it first: show the user what it means (interpretation) and the price, and only after they say yes call ${a.tool} again with confirm: true. Never pass confirm on your own judgement.` + (affordability?.verdict === "not_affordable" ? " Fix the affordability problem above first." : "") : affordability?.verdict === "not_affordable" ? "Fix that, then ask again without dryRun to deploy." : "Ask again without dryRun to deploy.")
45089
45349
  };
45090
45350
  }
45351
+ const held = await holdForSelfPaySend(deps, policyKey, "NOTHING WAS PAID");
45352
+ if (held) return { ...held, paid: false, policyKey, interpretation };
45091
45353
  let paymentHeader;
45092
45354
  try {
45093
45355
  paymentHeader = await deps.pay(accept);
@@ -45161,14 +45423,14 @@ async function checkPolicyWrite(deps, input = {}) {
45161
45423
  }
45162
45424
  if (pending.length > 1) {
45163
45425
  const others = pending.slice(1).map((r) => `${r.policyKey} (${r.blobId})`).join(", ");
45164
- const result = await checkChainFirst(deps, receipt) ?? await pollCollect(deps, deps.client, receipt, input.pollBudgetMs ?? DEFAULT_BUDGET_MS);
45426
+ const result = await collectUnlessSelfPayHeld(deps, receipt, input.pollBudgetMs ?? DEFAULT_BUDGET_MS);
45165
45427
  return {
45166
45428
  ...result,
45167
45429
  message: `${result.message} NOTE: ${pending.length} paid-for writes are on file and this checked the most recent one only. The others are: ${others}. Check each by passing its receipt.`
45168
45430
  };
45169
45431
  }
45170
45432
  }
45171
- return await checkChainFirst(deps, receipt) ?? pollCollect(deps, deps.client, receipt, input.pollBudgetMs ?? DEFAULT_BUDGET_MS);
45433
+ return collectUnlessSelfPayHeld(deps, receipt, input.pollBudgetMs ?? DEFAULT_BUDGET_MS);
45172
45434
  }
45173
45435
  async function checkChainFirst(deps, receipt) {
45174
45436
  const shows = await chainShows(deps, receipt);
@@ -45189,6 +45451,8 @@ function blockString(value) {
45189
45451
  async function updatePolicy(deps, input) {
45190
45452
  const gas = parseGasPayer(input?.gasPayer);
45191
45453
  if (!gas.ok) return refusedGasPayer(input?.policyKey);
45454
+ const maxGas = parseMaxGasRaw(input?.maxGasRaw);
45455
+ if (!maxGas.ok) return refusedMaxGas(input?.policyKey);
45192
45456
  const client = deps.client;
45193
45457
  if (!client && gas.gasPayer !== "self" && !(gas.gasPayer === void 0 && deps.selfPay)) {
45194
45458
  return unavailable(
@@ -45338,7 +45602,8 @@ async function updatePolicy(deps, input) {
45338
45602
  pollBudgetMs: input.pollBudgetMs ?? DEFAULT_BUDGET_MS,
45339
45603
  tool: "update_policy",
45340
45604
  verb: "update",
45341
- ...blocker ? { unavailable: blocker } : {}
45605
+ ...blocker ? { unavailable: blocker } : {},
45606
+ ...maxGas.maxGasRaw ? { maxGasRaw: maxGas.maxGasRaw } : {}
45342
45607
  });
45343
45608
  }
45344
45609
  const selfUnavailable = blocker ?? (!selfCall ? NO_WINDOW_REASON : void 0);
@@ -45422,6 +45687,12 @@ var REMOVE_CONSEQUENCE = "Removing a policy removes every limit it set, and Wall
45422
45687
  var NEVER_TWICE = "Calling remove_policy again is expected to be safe: the service is expected not to submit a second removal for a policy that is already being removed.";
45423
45688
  var IN_PROGRESS_MESSAGE = (detail) => `A write for this policy has ALREADY BEEN PAID FOR and is still being completed by the service (${detail}). Do not pay again, and nothing was paid by this call. If this wallet holds its receipt, check_policy_write finishes it; otherwise wait and read the policy back later with get_my_policy to see whether it took effect.`;
45424
45689
  async function removePolicy(deps, input) {
45690
+ if (!(input?.confirm === true && typeof input.policyKey === "string" && input.policyKey !== "")) return removePolicyLocked(deps, input);
45691
+ const r = await withSelfPayLock(selfPayLockKey(deps, input.policyKey), lockWaitMs(deps), () => removePolicyLocked(deps, input));
45692
+ if (r.kind === "ran") return r.value;
45693
+ return { state: "refused", policyKey: input.policyKey, message: busyMessage(input.policyKey, "NOTHING WAS REMOVED") };
45694
+ }
45695
+ async function removePolicyLocked(deps, input) {
45425
45696
  const client = deps.client;
45426
45697
  if (!client) {
45427
45698
  return {
@@ -45457,6 +45728,8 @@ async function removePolicy(deps, input) {
45457
45728
  message: `NOTHING WAS REMOVED. ${REMOVE_CONSEQUENCE} Show the user what the policy "${input.policyKey}" currently limits${attributes ? " (currentAttributes)" : ""} and what removing it means, and only if they clearly agree call remove_policy again with confirm: true. Never pass confirm on your own judgement.`
45458
45729
  };
45459
45730
  }
45731
+ const heldRemove = await holdForSelfPaySend(deps, input.policyKey, "NOTHING WAS REMOVED");
45732
+ if (heldRemove) return { state: heldRemove.state, policyKey: input.policyKey, message: heldRemove.message };
45460
45733
  const result = await client.remove(deps.ownerAddress, input.policyKey, deps.hsmPassword);
45461
45734
  switch (result.kind) {
45462
45735
  case "not_found":
@@ -45513,11 +45786,11 @@ async function removePolicy(deps, input) {
45513
45786
  const started = (deps.now?.() ?? /* @__PURE__ */ new Date()).getTime();
45514
45787
  let polls = 0;
45515
45788
  let waited = 0;
45516
- let unreadable = false;
45789
+ let unreadable2 = false;
45517
45790
  for (; ; ) {
45518
45791
  const read2 = await deps.readPolicy(input.policyKey);
45519
45792
  if ("error" in read2) {
45520
- unreadable = true;
45793
+ unreadable2 = true;
45521
45794
  } else if (read2.found === false) {
45522
45795
  return {
45523
45796
  state: "removed",
@@ -45526,7 +45799,7 @@ async function removePolicy(deps, input) {
45526
45799
  message: `The policy "${input.policyKey}" is removed: the chain no longer holds it. Its limits no longer apply.`
45527
45800
  };
45528
45801
  } else {
45529
- unreadable = false;
45802
+ unreadable2 = false;
45530
45803
  }
45531
45804
  const elapsed = Math.max((deps.now?.() ?? /* @__PURE__ */ new Date()).getTime() - started, waited);
45532
45805
  if (++polls >= MAX_POLLS || elapsed + wait >= budget) break;
@@ -45534,7 +45807,7 @@ async function removePolicy(deps, input) {
45534
45807
  waited += wait;
45535
45808
  }
45536
45809
  return submittedMessage(
45537
- unreadable ? "the chain could not be read to confirm it." : "the chain still shows the policy, so the block has not confirmed the removal yet."
45810
+ unreadable2 ? "the chain could not be read to confirm it." : "the chain still shows the policy, so the block has not confirmed the removal yet."
45538
45811
  );
45539
45812
  }
45540
45813
 
@@ -45759,6 +46032,17 @@ async function transferToken(deps, opts) {
45759
46032
  var DEFAULT_EXPIRY_MINUTES = 5;
45760
46033
  var MAX_EXPIRY_MINUTES = 60;
45761
46034
  var MAX_REVEAL_PATHS = 50;
46035
+ function qrGuidance(qrCodePath, hasQrImage) {
46036
+ const fallback = "If you cannot attach an image in this chat, give the user the link and say plainly that you could not send the QR code.";
46037
+ const never2 = "Never send any other image as the QR code, and never call another image a QR code: the credential's pass design is not one.";
46038
+ if (qrCodePath !== void 0) {
46039
+ return `Send the user the QR code by attaching the file at ${qrCodePath} (the same image is also returned with this result, as its first image; any other image is not the QR code). ${never2} ${fallback} Also give them the link.`;
46040
+ }
46041
+ if (hasQrImage) {
46042
+ return `There is no QR image file this time, so give the user the link. The QR code is the first image returned with this result; any other image is not the QR code. ${never2} If your channel cannot show images, say plainly that you could not send the QR code.`;
46043
+ }
46044
+ return `No QR code could be made this time, so give the user the link and say plainly that you could not send a QR code. Any image returned with this result is not the QR code. ${never2}`;
46045
+ }
45762
46046
  function buildVerificationLink(template, referenceId) {
45763
46047
  const problem = checkVerifyLinkTemplate(template);
45764
46048
  if (problem) throw new Error(problem);
@@ -45852,8 +46136,20 @@ async function createVerificationLink(input, deps) {
45852
46136
  const expiresAt = new Date(startedAt + expiryMinutes * 6e4).toISOString();
45853
46137
  let qrCodePngBase64;
45854
46138
  let qrError;
46139
+ let qrCodePath;
46140
+ let qrFileError;
45855
46141
  try {
45856
- qrCodePngBase64 = (await deps.renderQr(link)).toString("base64");
46142
+ const png = await deps.renderQr(link);
46143
+ if (png.length === 0) throw new Error("the QR renderer returned nothing");
46144
+ qrCodePngBase64 = png.toString("base64");
46145
+ if (deps.saveQr) {
46146
+ try {
46147
+ qrCodePath = await deps.saveQr(png, referenceId, startedAt + expiryMinutes * 6e4);
46148
+ } catch (e) {
46149
+ const code = e?.code;
46150
+ qrFileError = typeof code === "string" && /^[A-Z0-9_]{1,32}$/.test(code) ? `could not write the QR file (${code})` : "could not write the QR file";
46151
+ }
46152
+ }
45857
46153
  } catch (e) {
45858
46154
  qrError = messageOf(e);
45859
46155
  }
@@ -45866,7 +46162,9 @@ async function createVerificationLink(input, deps) {
45866
46162
  revealed: revealed.revealed,
45867
46163
  ...qrCodePngBase64 !== void 0 ? { qrCodePngBase64 } : {},
45868
46164
  ...qrError !== void 0 ? { qrError } : {},
45869
- message: `Show the user the QR code and the link, and tell them to open it in the MyID app (they need MyID installed first). It stops working at about ${expiresAt}, ${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"} after it was created \u2014 if it lapses, create a new one. This ${revealed.revealed === "all" ? "reveals the whole credential" : `reveals: ${revealed.revealed.map((p) => JSON.stringify(p)).join(", ")}`}. Show it only to the user: whoever holds the link can open what it reveals until then.`
46165
+ ...qrCodePath !== void 0 ? { qrCodePath } : {},
46166
+ ...qrFileError !== void 0 ? { qrFileError } : {},
46167
+ message: `${qrGuidance(qrCodePath, qrCodePngBase64 !== void 0)} Tell them to open the link in the MyID app (they need MyID installed first). It stops working at about ${expiresAt}, ${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"} after it was created \u2014 if it lapses, create a new one. This ${revealed.revealed === "all" ? "reveals the whole credential" : `reveals: ${revealed.revealed.map((p) => JSON.stringify(p)).join(", ")}`}. Show it only to the user: whoever holds the link can open what it reveals until then.`
45870
46168
  };
45871
46169
  }
45872
46170
 
@@ -45941,6 +46239,7 @@ function createTools(deps) {
45941
46239
  let vc = input.vc;
45942
46240
  let credentialUsed = "the credential you supplied";
45943
46241
  let kind;
46242
+ let savedPassImages;
45944
46243
  const wantsDefault = (input.revealAttribute === void 0 || input.revealAttribute.length === 0) && input.revealAll !== true;
45945
46244
  const verifiedId = deps.config.aiBirthcertVerifiedTemplateId;
45946
46245
  const basicId = resolveTemplateAlias("ai birthcert", deps.config.network);
@@ -45956,11 +46255,13 @@ function createTools(deps) {
45956
46255
  };
45957
46256
  }
45958
46257
  vc = chosen.vc;
46258
+ savedPassImages = chosen.vcPassImagePaths;
45959
46259
  credentialUsed = verified ? "Verified AI Birthcert" : "Basic AI Birthcert";
45960
46260
  kind = verified ? "verified" : "basic";
45961
46261
  } else if (wantsDefault && typeof vc === "object" && vc !== null && !Array.isArray(vc)) {
45962
46262
  const id = vc.id;
45963
46263
  const match = typeof id === "string" ? (await loadValidCachedCredentials(deps.cache)).find((e) => e.vc?.id === id) : void 0;
46264
+ savedPassImages = match?.vcPassImagePaths;
45964
46265
  if (match && verifiedId && match.templateId === verifiedId) kind = "verified";
45965
46266
  else if (match && basicId && match.templateId === basicId) kind = "basic";
45966
46267
  }
@@ -45996,12 +46297,16 @@ function createTools(deps) {
45996
46297
  expiryMinutes: input.expiryMinutes
45997
46298
  });
45998
46299
  if (!result.created) return result;
46300
+ const passImages = savedPassImages ? await existingFiles(savedPassImages) : [];
45999
46301
  const basicNote = kind === "basic" ? " This is a Basic AI Birthcert: it shows the agent's registered username only and does not mean the owner was verified, so do not describe the agent as owner-verified." : "";
46302
+ const passNote = passImages.length > 0 ? " The credential's pass design image is also returned (vcPassImagePaths): if the user wants to see it, send it as its own picture, captioned as the credential's pass design. It is not the QR code." : "";
46303
+ const extra = basicNote + passNote;
46000
46304
  return {
46001
46305
  ...result,
46002
46306
  credentialUsed,
46003
46307
  ...revealedByDefault ? { revealedByDefault: true } : {},
46004
- ...basicNote ? { message: result.message + basicNote } : {}
46308
+ ...passImages.length > 0 ? { vcPassImagePaths: passImages } : {},
46309
+ ...extra ? { message: result.message + extra } : {}
46005
46310
  };
46006
46311
  },
46007
46312
  pay_and_fetch(input) {
@@ -46090,7 +46395,14 @@ function createTools(deps) {
46090
46395
  */
46091
46396
  async credential_preflight(input) {
46092
46397
  if (input.credential === VERIFIED_AI_BIRTHCERT && !deps.verifyAiBirthcert) {
46093
- return { credential: input.credential, ready: false, balances: [], blockers: [AI_BIRTHCERT_NOT_CONFIGURED_ERROR], notChecked: [] };
46398
+ return {
46399
+ credential: input.credential,
46400
+ ready: false,
46401
+ balances: [],
46402
+ blockers: [AI_BIRTHCERT_NOT_CONFIGURED_ERROR],
46403
+ notChecked: [],
46404
+ choice: aiBirthcertChoice("verified", { unavailable: true })
46405
+ };
46094
46406
  }
46095
46407
  if (!deps.queryTokenBalance) {
46096
46408
  return {
@@ -46101,8 +46413,11 @@ function createTools(deps) {
46101
46413
  notChecked: []
46102
46414
  };
46103
46415
  }
46416
+ const basicId = resolveTemplateAlias("ai birthcert", deps.config.network);
46417
+ const isBasicAiBirthcert = (credential) => basicId !== void 0 && (credential === basicId || resolveTemplateAlias(credential, deps.config.network) === basicId);
46104
46418
  return credentialPreflight(
46105
46419
  {
46420
+ isBasicAiBirthcert,
46106
46421
  quoteVerified: (q) => deps.verifyAiBirthcert.request(q),
46107
46422
  quoteTemplate: async (templateId) => {
46108
46423
  const resolved = resolveTemplateAlias(templateId, deps.config.network) ?? templateId;
@@ -46365,6 +46680,17 @@ async function priceTemplate(deps, tools, templateId) {
46365
46680
  ...schema ? { schema } : {}
46366
46681
  };
46367
46682
  }
46683
+ async function existingFiles(paths) {
46684
+ const found = [];
46685
+ for (const path of paths) {
46686
+ try {
46687
+ await (0, import_promises4.access)(path);
46688
+ found.push(path);
46689
+ } catch {
46690
+ }
46691
+ }
46692
+ return found;
46693
+ }
46368
46694
 
46369
46695
  // src/held-credential.ts
46370
46696
  function createHeldCredentialFinder(deps) {
@@ -47097,6 +47423,57 @@ async function renderQrPng(text) {
47097
47423
  ]);
47098
47424
  }
47099
47425
 
47426
+ // src/qr-file-store.ts
47427
+ var import_node_crypto8 = require("node:crypto");
47428
+ var import_promises5 = require("node:fs/promises");
47429
+ var import_node_path5 = require("node:path");
47430
+ var PREFIX = "verification-qr-";
47431
+ var OWN_FILE = /^verification-qr-[a-z0-9]{1,8}-(\d+)-[a-f0-9]{6}\.png$/;
47432
+ function tagFor(referenceId) {
47433
+ const tag = referenceId.toLowerCase().replace(/^v2-/, "").replace(/[^a-z0-9]/g, "").slice(0, 8);
47434
+ return tag === "" ? "qr" : tag;
47435
+ }
47436
+ function createFsQrFileStore(dir, opts = {}) {
47437
+ const now = opts.now ?? Date.now;
47438
+ const randomSuffix = opts.randomSuffix ?? (() => (0, import_node_crypto8.randomBytes)(3).toString("hex"));
47439
+ const root = (0, import_node_path5.resolve)(dir);
47440
+ async function removeExpired() {
47441
+ let names;
47442
+ try {
47443
+ names = await (0, import_promises5.readdir)(root);
47444
+ } catch {
47445
+ return;
47446
+ }
47447
+ const at = now();
47448
+ for (const name of names) {
47449
+ const m = OWN_FILE.exec(name);
47450
+ if (!m) continue;
47451
+ if (Number(m[1]) > at) continue;
47452
+ try {
47453
+ await (0, import_promises5.unlink)((0, import_node_path5.join)(root, name));
47454
+ } catch {
47455
+ }
47456
+ }
47457
+ }
47458
+ return {
47459
+ async save(png, referenceId, expiresAtMs) {
47460
+ await (0, import_promises5.mkdir)(root, { recursive: true, mode: 448 });
47461
+ await removeExpired();
47462
+ for (let attempt = 0; ; attempt++) {
47463
+ const name = `${PREFIX}${tagFor(referenceId)}-${Math.floor(expiresAtMs)}-${randomSuffix()}.png`;
47464
+ const path = (0, import_node_path5.join)(root, name);
47465
+ try {
47466
+ await (0, import_promises5.writeFile)(path, png, { mode: 384, flag: "wx" });
47467
+ return path;
47468
+ } catch (e) {
47469
+ if (e.code !== "EEXIST" || attempt >= 4) throw e;
47470
+ }
47471
+ }
47472
+ },
47473
+ sweep: removeExpired
47474
+ };
47475
+ }
47476
+
47100
47477
  // src/policy-cap-bypass.ts
47101
47478
  var NATIVE2 = "ZTX";
47102
47479
  var RAW_AMOUNT2 = /^\d{1,77}$/;
@@ -47163,8 +47540,8 @@ function createPolicyGovernsCheck(options) {
47163
47540
  const pending = inFlight;
47164
47541
  pending.catch(() => void 0);
47165
47542
  let timer;
47166
- const timedOut = new Promise((resolve) => {
47167
- timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
47543
+ const timedOut = new Promise((resolve2) => {
47544
+ timer = setTimeout(() => resolve2(TIMED_OUT), timeoutMs);
47168
47545
  });
47169
47546
  try {
47170
47547
  const result = await Promise.race([pending, timedOut]);
@@ -47262,12 +47639,12 @@ function configPathFrom(argv) {
47262
47639
  if (!path || path.startsWith("--")) throw new Error("agentic-wallet-mcp: --config requires a file path");
47263
47640
  return path;
47264
47641
  }
47265
- function loadConfigFileEnv(argv, readFile7) {
47642
+ function loadConfigFileEnv(argv, readFile8) {
47266
47643
  const path = configPathFrom(argv);
47267
47644
  if (path === null) return {};
47268
47645
  let raw;
47269
47646
  try {
47270
- raw = readFile7(path);
47647
+ raw = readFile8(path);
47271
47648
  } catch (e) {
47272
47649
  throw new Error(`agentic-wallet-mcp: cannot read config file ${path}: ${e.message}`);
47273
47650
  }
@@ -47299,9 +47676,9 @@ function loadConfigFileEnv(argv, readFile7) {
47299
47676
  }
47300
47677
 
47301
47678
  // src/hsm-password.ts
47302
- var import_node_crypto7 = require("node:crypto");
47679
+ var import_node_crypto9 = require("node:crypto");
47303
47680
  function generateHsmPassword() {
47304
- return (0, import_node_crypto7.randomBytes)(24).toString("base64url");
47681
+ return (0, import_node_crypto9.randomBytes)(24).toString("base64url");
47305
47682
  }
47306
47683
 
47307
47684
  // src/export-credentials.ts
@@ -47338,8 +47715,8 @@ permanently inaccessible.
47338
47715
  }
47339
47716
 
47340
47717
  // src/clients/account-store.ts
47341
- var import_promises4 = require("node:fs/promises");
47342
- var import_node_path5 = require("node:path");
47718
+ var import_promises6 = require("node:fs/promises");
47719
+ var import_node_path6 = require("node:path");
47343
47720
  function isStoredAccountShape(value) {
47344
47721
  return typeof value === "object" && value !== null && typeof value.zetrixAddress === "string" && typeof value.holderDid === "string" && typeof value.hsmPassword === "string";
47345
47722
  }
@@ -47347,7 +47724,7 @@ function createFsAccountStore(filePath) {
47347
47724
  return {
47348
47725
  async get() {
47349
47726
  try {
47350
- const raw = await (0, import_promises4.readFile)(filePath, "utf8");
47727
+ const raw = await (0, import_promises6.readFile)(filePath, "utf8");
47351
47728
  const parsed = JSON.parse(raw);
47352
47729
  return isStoredAccountShape(parsed) ? parsed : null;
47353
47730
  } catch {
@@ -47355,8 +47732,8 @@ function createFsAccountStore(filePath) {
47355
47732
  }
47356
47733
  },
47357
47734
  async set(account) {
47358
- await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(filePath), { recursive: true, mode: 448 });
47359
- await (0, import_promises4.writeFile)(filePath, JSON.stringify(account, null, 2), { encoding: "utf8", mode: 384 });
47735
+ await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(filePath), { recursive: true, mode: 448 });
47736
+ await (0, import_promises6.writeFile)(filePath, JSON.stringify(account, null, 2), { encoding: "utf8", mode: 384 });
47360
47737
  }
47361
47738
  };
47362
47739
  }
@@ -47374,13 +47751,13 @@ function splitQrCode(result) {
47374
47751
  if (typeof qrCodePngBase64 !== "string" || qrCodePngBase64 === "") return { text: result };
47375
47752
  return { text: rest, qrCodePngBase64 };
47376
47753
  }
47377
- async function buildToolContent(result, readFile7) {
47754
+ async function buildToolContent(result, readFile8) {
47378
47755
  const { text, qrCodePngBase64 } = splitQrCode(result);
47379
47756
  const blocks = [{ type: "text", text: JSON.stringify(text) }];
47380
47757
  if (qrCodePngBase64 !== void 0) blocks.push({ type: "image", data: qrCodePngBase64, mimeType: "image/png" });
47381
47758
  for (const path of extractVcPassImagePaths(result)) {
47382
47759
  try {
47383
- const data = await readFile7(path);
47760
+ const data = await readFile8(path);
47384
47761
  blocks.push({ type: "image", data: data.toString("base64"), mimeType: "image/png" });
47385
47762
  } catch {
47386
47763
  }
@@ -47529,8 +47906,8 @@ var SsivcClient = class {
47529
47906
  };
47530
47907
 
47531
47908
  // src/clients/ssivc-session-store.ts
47532
- var import_promises5 = require("node:fs/promises");
47533
- var import_node_path6 = require("node:path");
47909
+ var import_promises7 = require("node:fs/promises");
47910
+ var import_node_path7 = require("node:path");
47534
47911
  var isAbsentOrString = (v) => v === void 0 || typeof v === "string";
47535
47912
  function isStoredSessionShape(value) {
47536
47913
  if (typeof value !== "object" || value === null) return false;
@@ -47541,7 +47918,7 @@ function createFsSsivcSessionStore(filePath) {
47541
47918
  return {
47542
47919
  async get() {
47543
47920
  try {
47544
- const raw = await (0, import_promises5.readFile)(filePath, "utf8");
47921
+ const raw = await (0, import_promises7.readFile)(filePath, "utf8");
47545
47922
  const parsed = JSON.parse(raw);
47546
47923
  return isStoredSessionShape(parsed) ? parsed : null;
47547
47924
  } catch (err) {
@@ -47551,23 +47928,23 @@ function createFsSsivcSessionStore(filePath) {
47551
47928
  }
47552
47929
  },
47553
47930
  async set(session) {
47554
- await (0, import_promises5.mkdir)((0, import_node_path6.dirname)(filePath), { recursive: true, mode: 448 });
47931
+ await (0, import_promises7.mkdir)((0, import_node_path7.dirname)(filePath), { recursive: true, mode: 448 });
47555
47932
  const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
47556
- await (0, import_promises5.writeFile)(tmpPath, JSON.stringify(session, null, 2), { encoding: "utf8", mode: 384 });
47557
- await (0, import_promises5.rename)(tmpPath, filePath);
47933
+ await (0, import_promises7.writeFile)(tmpPath, JSON.stringify(session, null, 2), { encoding: "utf8", mode: 384 });
47934
+ await (0, import_promises7.rename)(tmpPath, filePath);
47558
47935
  },
47559
47936
  async clear() {
47560
- await (0, import_promises5.rm)(filePath, { force: true });
47937
+ await (0, import_promises7.rm)(filePath, { force: true });
47561
47938
  }
47562
47939
  };
47563
47940
  }
47564
47941
 
47565
47942
  // src/clients/ssivc-download-quarantine-store.ts
47566
- var import_node_crypto8 = require("node:crypto");
47567
- var import_promises6 = require("node:fs/promises");
47568
- var import_node_path7 = require("node:path");
47943
+ var import_node_crypto10 = require("node:crypto");
47944
+ var import_promises8 = require("node:fs/promises");
47945
+ var import_node_path8 = require("node:path");
47569
47946
  function quarantineFileName(vcId) {
47570
- return `${(0, import_node_crypto8.createHash)("sha256").update(vcId).digest("hex")}.json`;
47947
+ return `${(0, import_node_crypto10.createHash)("sha256").update(vcId).digest("hex")}.json`;
47571
47948
  }
47572
47949
  function isQuarantinedDownloadShape(value) {
47573
47950
  if (typeof value !== "object" || value === null) return false;
@@ -47575,7 +47952,7 @@ function isQuarantinedDownloadShape(value) {
47575
47952
  return typeof v.vcId === "string" && "entries" in v && typeof v.downloadedAt === "string";
47576
47953
  }
47577
47954
  function createFsDownloadQuarantineStore(baseDir) {
47578
- const pathFor = (vcId) => (0, import_node_path7.join)(baseDir, quarantineFileName(vcId));
47955
+ const pathFor = (vcId) => (0, import_node_path8.join)(baseDir, quarantineFileName(vcId));
47579
47956
  const locks = /* @__PURE__ */ new Map();
47580
47957
  return {
47581
47958
  filePathFor: pathFor,
@@ -47590,7 +47967,7 @@ function createFsDownloadQuarantineStore(baseDir) {
47590
47967
  },
47591
47968
  async get(vcId) {
47592
47969
  try {
47593
- const raw = await (0, import_promises6.readFile)(pathFor(vcId), "utf8");
47970
+ const raw = await (0, import_promises8.readFile)(pathFor(vcId), "utf8");
47594
47971
  const parsed = JSON.parse(raw);
47595
47972
  return isQuarantinedDownloadShape(parsed) ? parsed : null;
47596
47973
  } catch {
@@ -47598,17 +47975,17 @@ function createFsDownloadQuarantineStore(baseDir) {
47598
47975
  }
47599
47976
  },
47600
47977
  async set(entry) {
47601
- await (0, import_promises6.mkdir)(baseDir, { recursive: true, mode: 448 });
47978
+ await (0, import_promises8.mkdir)(baseDir, { recursive: true, mode: 448 });
47602
47979
  const filePath = pathFor(entry.vcId);
47603
47980
  const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
47604
- await (0, import_promises6.writeFile)(tmpPath, JSON.stringify(entry, null, 2), { encoding: "utf8", mode: 384 });
47605
- await (0, import_promises6.rename)(tmpPath, filePath);
47981
+ await (0, import_promises8.writeFile)(tmpPath, JSON.stringify(entry, null, 2), { encoding: "utf8", mode: 384 });
47982
+ await (0, import_promises8.rename)(tmpPath, filePath);
47606
47983
  }
47607
47984
  };
47608
47985
  }
47609
47986
 
47610
47987
  // src/orchestrator/verify-ai-birthcert.ts
47611
- var import_node_crypto9 = require("node:crypto");
47988
+ var import_node_crypto11 = require("node:crypto");
47612
47989
 
47613
47990
  // src/canonical-json.ts
47614
47991
  function canonicalizeJson(value) {
@@ -47734,7 +48111,7 @@ function retryDelayMs(retryAfterSeconds) {
47734
48111
  if (!Number.isFinite(retryAfterSeconds) || retryAfterSeconds <= 0) return FALLBACK_RETRY_DELAY_MS;
47735
48112
  return Math.min(retryAfterSeconds * 1e3, MAX_RETRY_DELAY_MS);
47736
48113
  }
47737
- var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
48114
+ var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
47738
48115
  var SettlementStillQueuedError = class extends Error {
47739
48116
  constructor(message, paymentReceipt) {
47740
48117
  super(message);
@@ -48001,7 +48378,7 @@ async function buildSessionBody(deps, agentName, input) {
48001
48378
  ownerReference: deps.holderDid
48002
48379
  };
48003
48380
  Object.assign(fields, optionalRequestFields(input));
48004
- const digestHex = (0, import_node_crypto9.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
48381
+ const digestHex = (0, import_node_crypto11.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
48005
48382
  const { signBlob: signedData } = await deps.signHexBlob(digestHex);
48006
48383
  return { ...fields, signedData };
48007
48384
  }
@@ -48945,6 +49322,100 @@ function clip3(text, max = 200) {
48945
49322
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
48946
49323
  }
48947
49324
 
49325
+ // src/clients/self-pay-marker-store.ts
49326
+ var import_node_crypto12 = require("node:crypto");
49327
+ var import_promises9 = require("node:fs/promises");
49328
+ var import_node_path9 = require("node:path");
49329
+ function markerFileName(ownerAddress, policyKey) {
49330
+ return `${(0, import_node_crypto12.createHash)("sha256").update(`${ownerAddress}
49331
+ ${policyKey}`).digest("hex")}.json`;
49332
+ }
49333
+ var DIGITS = /^\d{1,18}$/;
49334
+ var HEX64 = /^[0-9a-fA-F]{64}$/;
49335
+ var ISO_8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
49336
+ function isRealDate(iso) {
49337
+ const t = Date.parse(iso);
49338
+ return !Number.isNaN(t) && new Date(t).toISOString().slice(0, 10) === iso.slice(0, 10);
49339
+ }
49340
+ function isMarkerShape(value) {
49341
+ if (typeof value !== "object" || value === null) return false;
49342
+ const v = value;
49343
+ return typeof v.policyKey === "string" && v.policyKey !== "" && typeof v.ownerAddress === "string" && v.ownerAddress !== "" && (v.operation === "CREATE" || v.operation === "UPDATE") && typeof v.nonce === "string" && DIGITS.test(v.nonce) && typeof v.txHash === "string" && HEX64.test(v.txHash) && typeof v.ceilLedgerSeq === "string" && DIGITS.test(v.ceilLedgerSeq) && typeof v.submittedAt === "string" && ISO_8601.test(v.submittedAt) && isRealDate(v.submittedAt);
49344
+ }
49345
+ var DIRECTORY_FSYNC_UNSUPPORTED = /* @__PURE__ */ new Set(["EISDIR", "EPERM", "EACCES", "EINVAL", "ENOTSUP", "ENOSYS", "EBADF"]);
49346
+ var errorCode = (e) => typeof e === "object" && e !== null ? e.code : void 0;
49347
+ function unreadable(file, why) {
49348
+ return new Error(
49349
+ `the note of an earlier self-paid send (${file}) cannot be trusted: ${why}. A send can still land for 60 ledgers: delete that file only once at least 15 minutes have passed since it was last changed and the chain is producing ledgers, or once get_my_policy shows the write`
49350
+ );
49351
+ }
49352
+ function createFsSelfPayMarkerStore(baseDir) {
49353
+ const pathFor = (owner, key) => (0, import_node_path9.join)(baseDir, markerFileName(owner, key));
49354
+ async function read2(ownerAddress, policyKey) {
49355
+ const file = pathFor(ownerAddress, policyKey);
49356
+ let raw;
49357
+ try {
49358
+ raw = await (0, import_promises9.readFile)(file, "utf8");
49359
+ } catch (e) {
49360
+ if (errorCode(e) === "ENOENT") return null;
49361
+ throw unreadable(file, `it could not be read (${errorCode(e) ?? "unknown error"})`);
49362
+ }
49363
+ let parsed;
49364
+ try {
49365
+ parsed = JSON.parse(raw);
49366
+ } catch {
49367
+ throw unreadable(file, raw === "" ? "it is empty" : "it is not valid JSON");
49368
+ }
49369
+ if (!isMarkerShape(parsed) || parsed.ownerAddress !== ownerAddress || parsed.policyKey !== policyKey) {
49370
+ throw unreadable(file, "it is not a valid note for this owner and key");
49371
+ }
49372
+ return parsed;
49373
+ }
49374
+ return {
49375
+ get: read2,
49376
+ async set(marker) {
49377
+ if (!isMarkerShape(marker)) throw new Error("refusing to save a malformed self-pay marker");
49378
+ await (0, import_promises9.mkdir)(baseDir, { recursive: true, mode: 448 });
49379
+ const filePath = pathFor(marker.ownerAddress, marker.policyKey);
49380
+ const tmpPath = `${filePath}.tmp-${(0, import_node_crypto12.randomUUID)()}`;
49381
+ try {
49382
+ const handle = await (0, import_promises9.open)(tmpPath, "w", 384);
49383
+ try {
49384
+ await handle.writeFile(JSON.stringify(marker, null, 2), "utf8");
49385
+ await handle.sync();
49386
+ } finally {
49387
+ await handle.close();
49388
+ }
49389
+ await (0, import_promises9.rename)(tmpPath, filePath);
49390
+ } catch (e) {
49391
+ await (0, import_promises9.unlink)(tmpPath).catch(() => void 0);
49392
+ throw e;
49393
+ }
49394
+ try {
49395
+ const dir = await (0, import_promises9.open)(baseDir, "r");
49396
+ try {
49397
+ await dir.sync();
49398
+ } finally {
49399
+ await dir.close();
49400
+ }
49401
+ } catch (e) {
49402
+ if (!DIRECTORY_FSYNC_UNSUPPORTED.has(errorCode(e) ?? "")) throw e;
49403
+ }
49404
+ },
49405
+ async remove(ownerAddress, policyKey, expectedTxHash) {
49406
+ if (expectedTxHash !== void 0) {
49407
+ const current = await read2(ownerAddress, policyKey);
49408
+ if (current === null || current.txHash !== expectedTxHash) return;
49409
+ }
49410
+ try {
49411
+ await (0, import_promises9.unlink)(pathFor(ownerAddress, policyKey));
49412
+ } catch (e) {
49413
+ if (errorCode(e) !== "ENOENT") throw e;
49414
+ }
49415
+ }
49416
+ };
49417
+ }
49418
+
48948
49419
  // src/orchestrator/policy-affordability.ts
48949
49420
  var NATIVE3 = "ZTX";
48950
49421
  var MAX_DETAIL = 160;
@@ -49039,6 +49510,7 @@ async function checkAffordability(deps, accept) {
49039
49510
 
49040
49511
  // src/index.ts
49041
49512
  var packageVersion = package_default.version;
49513
+ var POLICY_HTTP_TIMEOUT_MS = 6e4;
49042
49514
  function buildToolList() {
49043
49515
  return [
49044
49516
  {
@@ -49055,12 +49527,12 @@ function buildToolList() {
49055
49527
  },
49056
49528
  {
49057
49529
  name: "prove_identity",
49058
- description: "Answer an x401 PROOF-REQUEST and return the PROOF-RESPONSE header to replay to the resource server.",
49530
+ description: "Answer an x401 PROOF-REQUEST and return the PROOF-RESPONSE header to replay to the resource server. If the wallet holds several credentials, pass vc: use the one the request or the verifier's challenge names, and if it names none, prefer the Verified AI Birthcert over the Basic one and say which one you presented.",
49059
49531
  inputSchema: {
49060
49532
  type: "object",
49061
49533
  properties: {
49062
49534
  proofRequest: { type: "string", description: "The PROOF-REQUEST header value from the 401 challenge." },
49063
- vc: { type: "object", description: "The VerifiableCredential to present. Omit to use the wallet's single locally-cached credential, if there is exactly one \u2014 the call fails with a clear error if none or several are cached." },
49535
+ vc: { type: "object", description: "The VerifiableCredential to present. Omit to use the wallet's single locally-cached credential, if there is exactly one \u2014 the call fails with a clear error if none or several are cached, so with several, pass it." },
49064
49536
  revealAttribute: { type: "array", items: { type: "string" }, description: "Dotted disclosure paths to reveal. Omit to reveal exactly the claims the challenge (DCQL) requests; a challenge naming no claims reveals all." },
49065
49537
  issuerKeys: {
49066
49538
  type: "object",
@@ -49076,7 +49548,7 @@ function buildToolList() {
49076
49548
  },
49077
49549
  {
49078
49550
  name: "create_verification_qr",
49079
- description: "Create a link and a QR code that open this wallet's credential in the MyID app, so a human can see the identity behind the agent. Use it when the user asks whether the agent is verified, or asks to verify the agent in MyID. With no vc, it presents the Verified AI Birthcert if it holds one, otherwise the Basic AI Birthcert, and if it holds neither it returns created: false and says to create one first. By default it reveals only a standard minimal set (for the Verified AI Birthcert: agentName, evidenceProvider and ownerVerified; for the Basic AI Birthcert: agentUsername); pass revealAttribute to reveal other specific attributes the user asks for, and revealAll: true only when the user explicitly asks to reveal everything. A Basic AI Birthcert does not mean the owner was verified, so say so if that is what was presented. The wallet presents the credential to MBI and returns the link, the QR code image and the time the link stops working (5 minutes by default); the link holds only a reference id, and that reference id grants access to the revealed attributes until it expires. Show the link and QR code only to the human user, never pass them to another service, and tell the user to open the link in MyID (they need the app installed). If it returns created: false, relay the reason and do not invent a link.",
49551
+ description: "Create a link and a QR code that open this wallet's credential in the MyID app, so a human can see the identity behind the agent. Use it when the user asks whether the agent is verified, or asks to verify the agent in MyID. With no vc, it presents the Verified AI Birthcert if it holds one, otherwise the Basic AI Birthcert, and if it holds neither it returns created: false and says to create one first. By default it reveals only a standard minimal set (for the Verified AI Birthcert: agentName, evidenceProvider and ownerVerified; for the Basic AI Birthcert: agentUsername); pass revealAttribute to reveal other specific attributes the user asks for, and revealAll: true only when the user explicitly asks to reveal everything. A Basic AI Birthcert does not mean the owner was verified, so say so if that is what was presented. The wallet presents the credential to MBI and returns the link, the QR code (as an image and as a PNG file at qrCodePath) and the time the link stops working (5 minutes by default); the link holds only a reference id, and that reference id grants access to the revealed attributes until it expires. Show the link and QR code only to the human user, never pass them to another service, and tell the user to open the link in MyID (they need the app installed). In a chat channel such as WhatsApp, send the QR code by attaching the file at qrCodePath; never send any other image as the QR code, because the credential's pass design is not one, and if you cannot attach an image, give the link and say plainly that you could not send the QR code. When the credential has a saved pass design, the result also carries vcPassImagePaths: if the user wants to see the credential itself, send that image too, as its own picture captioned as the credential's pass design and never as the QR code. If it returns created: false, relay the reason and do not invent a link.",
49080
49552
  inputSchema: {
49081
49553
  type: "object",
49082
49554
  properties: {
@@ -49181,7 +49653,7 @@ function buildToolList() {
49181
49653
  },
49182
49654
  {
49183
49655
  name: "write_policy",
49184
- description: 'Deploy a spending policy on chain. THIS PAYS A REAL FEE. Run policy_preflight first and show the user its `interpretation`, because a policy that is valid can still mean something other than what they asked for, and this tool cannot take that back once it is written. It only pays and writes with confirm: true, which the user must have given after seeing the interpretation and the price: without it you get the price back (state "quoted", needsConfirmation: true) and nothing is paid. Never pass confirm on your own judgement. Amounts (perTransactionMax, cumulativeMax, velocityCap) are RAW base units: 1 of a 6-decimal token is 0.000001 of it. Send the SAME attributes and the SAME amountUnit you preflighted with, and never substitute convertedAmounts for the values \u2014 that converts them twice. "whole" gives whole-token amounts the wallet converts before anything is paid; "base" confirms a raw value under one whole token is intended; without it such a value is refused. For any amount attribute you may give valueHuman (whole tokens, "100" for 100 JMYR) instead of value: the wallet converts it with the token\'s own decimals, shows both forms, and never sends valueHuman on. Never put convertedAmounts in valueHuman: those are raw base units and would be scaled again. maxTransactionCount is a count of transactions, not an amount, so give it as value and never scale it. The flow is three steps and the middle one is where care is needed: a free pre-check, a payment that writes NOTHING, and a collect that finishes the write once the payment settles. Between the payment and the settlement A PAYMENT HAS BEEN MADE and no policy exists \u2014 that window is normal, not a failure. On the sponsored path, if `state` is "settling" or "submitted", A PAYMENT HAS BEEN MADE: never call this tool again for the same policy, never tell the user it failed, and pass `paymentReceipt` to check_policy_write instead. "submitted" means the transaction is on chain but the block has not confirmed it \u2014 do NOT report the policy as created, even though a txHash is present. "written" is the only state that means the policy exists. "already_exists" and "refused" both mean nothing was paid: they come from the FREE pre-check, or, for "refused" with `policyDenied`, from Wallet BE refusing to sign. When `policyDenied` is true a spending policy refused the fee before it was signed: a decision, so do not retry, and no policy was written. When `policyCheckUnavailable` is true (state "unavailable") the spending policy check could not be completed, nothing was paid and no policy was written; trying again shortly is right. "payment_refused" is different: a payment was presented and the service rejected it, and that says nothing either way about whether the fee was taken \u2014 never tell the user they were not charged, and never tell them they were. "receipt_void" is the one state where paying again is correct \u2014 the settlement failed and the receipt bought nothing; `payFresh` is set to say so. On the sponsored path, "write_failed" and "unknown" both mean money moved and paying again would NOT help: quote `paymentReceipt` to support rather than retrying. To find a templateId, call get_policy_template_schema with no arguments \u2014 it lists the templates with their ids. Never invent one. The template contract address is never the user\'s to supply, so leave it out rather than asking. Who pays the gas is the USER\'s choice: if this returns "needs_choice", show the user both options (sponsored: a JMYR fee; self: ZTX for network gas, no fee), let them pick, and call again with gasPayer and confirm: true. Never choose for them. With gasPayer "self" there is no paymentReceipt and check_policy_write does not apply. A self-paid "submitted" means the network gas is spent: do NOT send it again, check get_my_policy, and quote the txHash to support. A self-paid "unknown" means it may be on chain: do NOT send it again, check get_my_policy, and quote the nonce. A self-paid "write_failed" means the transaction was included and rejected: the gas is spent and nothing was written. Self-paid gas is spent even when the write fails on chain. To learn the price BEFORE paying, pass dryRun: it runs every free check and returns the service\'s quote, paying nothing and writing no policy. "quoted" means that \u2014 no payment was made. It also reports `affordability`: whether this wallet currently holds the quoted amount of the fee asset (plus some ZTX for gas when the payer needs it) and whether the payment cap would refuse it \u2014 each shortfall named separately, and "unknown" when a balance could not be read, which is not a yes. Tell the user what it says before they decide to pay. A quote is what the service asked for just now; it can change, and it is not a promise the payment will be allowed \u2014 balances move and the cap is enforced again when paying. Never ask the user for their HSM password \u2014 this tool does not take one.',
49656
+ description: 'Deploy a spending policy on chain. THIS PAYS A REAL FEE. Run policy_preflight first and show the user its `interpretation`, because a policy that is valid can still mean something other than what they asked for, and this tool cannot take that back once it is written. It only pays and writes with confirm: true, which the user must have given after seeing the interpretation and the price: without it you get the price back (state "quoted", needsConfirmation: true) and nothing is paid. Never pass confirm on your own judgement. Amounts (perTransactionMax, cumulativeMax, velocityCap) are RAW base units: 1 of a 6-decimal token is 0.000001 of it. Send the SAME attributes and the SAME amountUnit you preflighted with, and never substitute convertedAmounts for the values \u2014 that converts them twice. "whole" gives whole-token amounts the wallet converts before anything is paid; "base" confirms a raw value under one whole token is intended; without it such a value is refused. For any amount attribute you may give valueHuman (whole tokens, "100" for 100 JMYR) instead of value: the wallet converts it with the token\'s own decimals, shows both forms, and never sends valueHuman on. Never put convertedAmounts in valueHuman: those are raw base units and would be scaled again. maxTransactionCount is a count of transactions, not an amount, so give it as value and never scale it. The flow is three steps and the middle one is where care is needed: a free pre-check, a payment that writes NOTHING, and a collect that finishes the write once the payment settles. Between the payment and the settlement A PAYMENT HAS BEEN MADE and no policy exists \u2014 that window is normal, not a failure. On the sponsored path, if `state` is "settling" or "submitted", A PAYMENT HAS BEEN MADE: never call this tool again for the same policy, never tell the user it failed, and pass `paymentReceipt` to check_policy_write instead. "submitted" means the transaction is on chain but the block has not confirmed it \u2014 do NOT report the policy as created, even though a txHash is present. "written" is the only state that means the policy exists. "already_exists" and "refused" both mean nothing was paid: they come from the FREE pre-check, or, for "refused" with `policyDenied`, from Wallet BE refusing to sign. When `policyDenied` is true a spending policy refused the fee before it was signed: a decision, so do not retry, and no policy was written. When `policyCheckUnavailable` is true (state "unavailable") the spending policy check could not be completed, nothing was paid and no policy was written; trying again shortly is right. "payment_refused" is different: a payment was presented and the service rejected it, and that says nothing either way about whether the fee was taken \u2014 never tell the user they were not charged, and never tell them they were. "receipt_void" is the one state where paying again is correct \u2014 the settlement failed and the receipt bought nothing; `payFresh` is set to say so. On the sponsored path, "write_failed" and "unknown" both mean money moved and paying again would NOT help: quote `paymentReceipt` to support rather than retrying. To find a templateId, call get_policy_template_schema with no arguments \u2014 it lists the templates with their ids. Never invent one. The template contract address is never the user\'s to supply, so leave it out rather than asking. Who pays the gas is the USER\'s choice: if this returns "needs_choice", show the user both options (sponsored: a JMYR fee; self: ZTX for network gas, no fee), let them pick, and call again with gasPayer and confirm: true. Never choose for them. With gasPayer "self" there is no paymentReceipt and check_policy_write does not apply. A self-paid "submitted" means the network gas is spent: do NOT send it again, check get_my_policy, and quote the txHash to support. A self-paid "unknown" means it may be on chain: do NOT send it again, check get_my_policy, and quote the nonce. The wallet remembers it and refuses another send, a sponsored write, a collect and a removal for that key until it has landed or can no longer land, and says when. A self-paid "write_failed" means the transaction was included and rejected: the gas is spent and nothing was written. To send a self-paid write pass maxGasRaw: the requiredRaw of the self option you showed the user. It is the most gas they agreed to, and the wallet refuses, free, if the gas now costs more, so show the user any new figure and ask again. Self-paid gas is spent even when the write fails on chain. To learn the price BEFORE paying, pass dryRun: it runs every free check and returns the service\'s quote, paying nothing and writing no policy. "quoted" means that \u2014 no payment was made. It also reports `affordability`: whether this wallet currently holds the quoted amount of the fee asset (plus some ZTX for gas when the payer needs it) and whether the payment cap would refuse it \u2014 each shortfall named separately, and "unknown" when a balance could not be read, which is not a yes. Tell the user what it says before they decide to pay. A quote is what the service asked for just now; it can change, and it is not a promise the payment will be allowed \u2014 balances move and the cap is enforced again when paying. Never ask the user for their HSM password \u2014 this tool does not take one.',
49185
49657
  inputSchema: {
49186
49658
  type: "object",
49187
49659
  properties: {
@@ -49241,6 +49713,10 @@ function buildToolList() {
49241
49713
  type: "string",
49242
49714
  enum: ["sponsored", "self"],
49243
49715
  description: 'Who pays the gas to write. "sponsored": the fee is paid in JMYR and the network gas is covered, so no ZTX is needed. "self": no fee, only the network gas in ZTX. Leave it out unless the user has chosen: when the wallet can pay the gas itself, a call without it returns state "needs_choice" with both options and what each would cost, and the user decides. Never choose for the user.'
49716
+ },
49717
+ maxGasRaw: {
49718
+ type: "string",
49719
+ description: 'Self-pay only. The most gas, in raw units (micro-ZTX), the user agreed to pay to write: the "requiredRaw" of the self option (gasOptions.self.gas.requiredRaw) that you showed them. A self-paid send needs it and is refused, free, without it or when the gas now costs more than this, so it can never cost more than the user was shown.'
49244
49720
  }
49245
49721
  },
49246
49722
  required: ["policyKey", "attributes", "templateId"]
@@ -49248,7 +49724,7 @@ function buildToolList() {
49248
49724
  },
49249
49725
  {
49250
49726
  name: "update_policy",
49251
- description: 'Replace a deployed spending policy\'s attributes and validity window. THIS PAYS A REAL FEE. Read the policy first with get_my_policy and pass its `forUpdate.expectedUpdatedAtBlock` as expectedUpdatedAtBlock: if the policy has changed since, the update is refused before anything is paid. attributes is the FULL replacement set, not a patch \u2014 anything you leave out is dropped \u2014 so start from the policy\'s current attributes and change only what the user asked for. Do not pass a templateId: the policy keeps the template it already references. The validity window is kept as it is unless you pass validFromBlock or validToBlock. Run policy_preflight first and show the user its `interpretation`, because a policy that is valid can still mean something other than what they asked for, and an update cannot be taken back once it is written. It only pays and writes with confirm: true, which the user must have given after seeing the interpretation and the price: without it you get the price back (state "quoted", needsConfirmation: true) and nothing is paid. Never pass confirm on your own judgement. Amounts (perTransactionMax, cumulativeMax, velocityCap) are RAW base units, exactly as in write_policy: send the SAME attributes and the SAME amountUnit you preflighted with, and never substitute convertedAmounts for the values. For any amount attribute you may give valueHuman (whole tokens, "100" for 100 JMYR) instead of value, as in write_policy; maxTransactionCount is a count, not an amount, so give it as value and never scale it. The flow is the same three steps as write_policy: a free pre-check, a payment that changes NOTHING, and a collect that applies the update once the payment settles. Between the payment and the settlement A PAYMENT HAS BEEN MADE and the policy is still unchanged \u2014 that window is normal, not a failure. On the sponsored path, if `state` is "settling" or "submitted", A PAYMENT HAS BEEN MADE: never call this tool again for the same policy, never tell the user it failed, and pass `paymentReceipt` to check_policy_write instead. "submitted" means the transaction is on chain but the block has not confirmed it \u2014 do NOT report the policy as updated, even though a txHash is present. "written" is the only state that means the policy was updated. "not_found", "modified", "template_unavailable" and "refused" all come from the free checks, before any payment, and say so. For "modified", read the policy again with get_my_policy, show the user what it holds now, and update from that; never retry with the old expectedUpdatedAtBlock. When `policyDenied` is true a spending policy refused the fee before it was signed: a decision, so do not retry. "payment_refused" is different: a payment was presented and the service rejected it, and that says nothing either way about whether the fee was taken \u2014 never tell the user they were not charged, and never tell them they were. "receipt_void" is the one state where paying again is correct \u2014 the settlement failed and the receipt bought nothing; `payFresh` is set to say so. On the sponsored path, "write_failed" means the update was NOT applied and the policy is unchanged, but a payment may have been taken: paying again would not help, so quote `paymentReceipt` to support rather than retrying. On the sponsored path "unknown" also means money moved without a verdict: do the same. Who pays the gas is the USER\'s choice: if this returns "needs_choice", show the user both options (sponsored: a JMYR fee; self: ZTX for network gas, no fee), let them pick, and call again with gasPayer and confirm: true. Never choose for them. With gasPayer "self" there is no paymentReceipt and check_policy_write does not apply. A self-paid "submitted" means the network gas is spent: do NOT send it again, check get_my_policy, and quote the txHash to support. A self-paid "unknown" means it may be on chain: do NOT send it again, check get_my_policy, and quote the nonce. A self-paid "write_failed" means the transaction was included and rejected: the gas is spent and nothing was written. Self-paid gas is spent even when the write fails on chain. To learn the price BEFORE paying, pass dryRun: it runs every free check and returns the service\'s quote, paying nothing and changing no policy. Tell the user what it says before they decide to pay. To lift a policy\'s limits entirely, that is remove_policy, not an update with no attributes. Never ask the user for their HSM password \u2014 this tool does not take one.',
49727
+ description: 'Replace a deployed spending policy\'s attributes and validity window. THIS PAYS A REAL FEE. Read the policy first with get_my_policy and pass its `forUpdate.expectedUpdatedAtBlock` as expectedUpdatedAtBlock: if the policy has changed since, the update is refused before anything is paid. attributes is the FULL replacement set, not a patch \u2014 anything you leave out is dropped \u2014 so start from the policy\'s current attributes and change only what the user asked for. Do not pass a templateId: the policy keeps the template it already references. The validity window is kept as it is unless you pass validFromBlock or validToBlock. Run policy_preflight first and show the user its `interpretation`, because a policy that is valid can still mean something other than what they asked for, and an update cannot be taken back once it is written. It only pays and writes with confirm: true, which the user must have given after seeing the interpretation and the price: without it you get the price back (state "quoted", needsConfirmation: true) and nothing is paid. Never pass confirm on your own judgement. Amounts (perTransactionMax, cumulativeMax, velocityCap) are RAW base units, exactly as in write_policy: send the SAME attributes and the SAME amountUnit you preflighted with, and never substitute convertedAmounts for the values. For any amount attribute you may give valueHuman (whole tokens, "100" for 100 JMYR) instead of value, as in write_policy; maxTransactionCount is a count, not an amount, so give it as value and never scale it. The flow is the same three steps as write_policy: a free pre-check, a payment that changes NOTHING, and a collect that applies the update once the payment settles. Between the payment and the settlement A PAYMENT HAS BEEN MADE and the policy is still unchanged \u2014 that window is normal, not a failure. On the sponsored path, if `state` is "settling" or "submitted", A PAYMENT HAS BEEN MADE: never call this tool again for the same policy, never tell the user it failed, and pass `paymentReceipt` to check_policy_write instead. "submitted" means the transaction is on chain but the block has not confirmed it \u2014 do NOT report the policy as updated, even though a txHash is present. "written" is the only state that means the policy was updated. "not_found", "modified", "template_unavailable" and "refused" all come from the free checks, before any payment, and say so. For "modified", read the policy again with get_my_policy, show the user what it holds now, and update from that; never retry with the old expectedUpdatedAtBlock. When `policyDenied` is true a spending policy refused the fee before it was signed: a decision, so do not retry. "payment_refused" is different: a payment was presented and the service rejected it, and that says nothing either way about whether the fee was taken \u2014 never tell the user they were not charged, and never tell them they were. "receipt_void" is the one state where paying again is correct \u2014 the settlement failed and the receipt bought nothing; `payFresh` is set to say so. On the sponsored path, "write_failed" means the update was NOT applied and the policy is unchanged, but a payment may have been taken: paying again would not help, so quote `paymentReceipt` to support rather than retrying. On the sponsored path "unknown" also means money moved without a verdict: do the same. Who pays the gas is the USER\'s choice: if this returns "needs_choice", show the user both options (sponsored: a JMYR fee; self: ZTX for network gas, no fee), let them pick, and call again with gasPayer and confirm: true. Never choose for them. With gasPayer "self" there is no paymentReceipt and check_policy_write does not apply. A self-paid "submitted" means the network gas is spent: do NOT send it again, check get_my_policy, and quote the txHash to support. A self-paid "unknown" means it may be on chain: do NOT send it again, check get_my_policy, and quote the nonce. The wallet remembers it and refuses another send, a sponsored write, a collect and a removal for that key until it has landed or can no longer land, and says when. A self-paid "write_failed" means the transaction was included and rejected: the gas is spent and nothing was written. To send a self-paid write pass maxGasRaw: the requiredRaw of the self option you showed the user. It is the most gas they agreed to, and the wallet refuses, free, if the gas now costs more, so show the user any new figure and ask again. Self-paid gas is spent even when the write fails on chain. To learn the price BEFORE paying, pass dryRun: it runs every free check and returns the service\'s quote, paying nothing and changing no policy. Tell the user what it says before they decide to pay. To lift a policy\'s limits entirely, that is remove_policy, not an update with no attributes. Never ask the user for their HSM password \u2014 this tool does not take one.',
49252
49728
  inputSchema: {
49253
49729
  type: "object",
49254
49730
  properties: {
@@ -49297,6 +49773,10 @@ function buildToolList() {
49297
49773
  enum: ["sponsored", "self"],
49298
49774
  description: 'Who pays the gas to update. "sponsored": the fee is paid in JMYR and the network gas is covered, so no ZTX is needed. "self": no fee, only the network gas in ZTX. Leave it out unless the user has chosen: when the wallet can pay the gas itself, a call without it returns state "needs_choice" with both options and what each would cost, and the user decides. Never choose for the user.'
49299
49775
  },
49776
+ maxGasRaw: {
49777
+ type: "string",
49778
+ description: 'Self-pay only. The most gas, in raw units (micro-ZTX), the user agreed to pay to update: the "requiredRaw" of the self option (gasOptions.self.gas.requiredRaw) that you showed them. A self-paid send needs it and is refused, free, without it or when the gas now costs more than this, so it can never cost more than the user was shown.'
49779
+ },
49300
49780
  pollBudgetMs: {
49301
49781
  type: "number",
49302
49782
  description: "How long to wait for the settlement before handing back the receipt. The default is about a minute; settlement usually takes around twenty seconds."
@@ -49307,7 +49787,7 @@ function buildToolList() {
49307
49787
  },
49308
49788
  {
49309
49789
  name: "remove_policy",
49310
- description: `Remove a deployed spending policy. REMOVING A POLICY REMOVES EVERY LIMIT IT SET: once it is gone, Wallet BE signs spends of that asset without any limit. Removing is free, but it needs the user's clear agreement. Without confirm: true nothing is sent and you get state "needs_confirmation" with what the policy currently limits: show the user that and what removing it means, and only if they clearly agree call again with confirm: true. Never pass confirm on your own judgement. "removed" is the only state that means the policy is gone: it is reported only when the chain no longer holds it. "submitted" means the removal is on its way but not confirmed \u2014 do NOT tell the user it is removed; check with get_my_policy (found: false means it is gone). Calling remove_policy again is free, and the service is expected not to submit a second removal. "not_found" means there was nothing to remove, or it is already gone \u2014 confirm with get_my_policy before saying which. "in_progress" means a paid write for this policy is still being completed: if this wallet holds its receipt, check_policy_write finishes it, otherwise wait and ask again later. To change a policy rather than remove it, use update_policy. Never ask the user for their HSM password \u2014 this tool does not take one.`,
49790
+ description: `Remove a deployed spending policy. REMOVING A POLICY REMOVES EVERY LIMIT IT SET: once it is gone, Wallet BE signs spends of that asset without any limit. Removing is free, but it needs the user's clear agreement. Without confirm: true nothing is sent and you get state "needs_confirmation" with what the policy currently limits: show the user that and what removing it means, and only if they clearly agree call again with confirm: true. Never pass confirm on your own judgement. "removed" is the only state that means the policy is gone: it is reported only when the chain no longer holds it. "submitted" means the removal is on its way but not confirmed \u2014 do NOT tell the user it is removed; check with get_my_policy (found: false means it is gone). Calling remove_policy again is free, and the service is expected not to submit a second removal. "not_found" means there was nothing to remove, or it is already gone \u2014 confirm with get_my_policy before saying which. "in_progress" means a paid write for this policy is still being completed: if this wallet holds its receipt, check_policy_write finishes it, otherwise wait and ask again later. "refused" naming a self-paid transaction means an earlier self-paid update may still land and would bring the old policy back: NOTHING WAS REMOVED; wait until the ledger it names, or read the policy with get_my_policy, then ask again. "refused" saying another write is in progress in this wallet, or "unavailable" saying a record could not be read, also means NOTHING WAS REMOVED: ask again shortly. To change a policy rather than remove it, use update_policy. Never ask the user for their HSM password \u2014 this tool does not take one.`,
49311
49791
  inputSchema: {
49312
49792
  type: "object",
49313
49793
  properties: {
@@ -49329,7 +49809,7 @@ function buildToolList() {
49329
49809
  },
49330
49810
  {
49331
49811
  name: "check_policy_write",
49332
- description: 'Finish a policy write that has already been PAID FOR. FREE \u2014 this tool never pays, on any path, which is why it is the right answer to "did my policy get created?" and calling write_policy again is not. Call it with the `paymentReceipt` from a "settling" or "submitted" result, or with no arguments to resume the most recent pending write. The states mean the same as in write_policy: "written" is the only one where the policy exists, "settling" and "submitted" mean keep waiting, and "unknown" means the outcome could not be determined \u2014 none of which is a reason to pay again. A paid update_policy is finished the same way: its receipt is collected on the update route, and "written" then means the policy was updated. If this wallet holds no record of the receipt, that does NOT mean the write failed: the service completes a paid write on its own, and asking to write the same policyKey again will report the truth for free before any payment.',
49812
+ description: 'Finish a policy write that has already been PAID FOR. FREE \u2014 this tool never pays, on any path, which is why it is the right answer to "did my policy get created?" and calling write_policy again is not. Call it with the `paymentReceipt` from a "settling" or "submitted" result, or with no arguments to resume the most recent pending write. The states mean the same as in write_policy: "written" is the only one where the policy exists, "settling" and "submitted" mean keep waiting, and "unknown" means the outcome could not be determined \u2014 none of which is a reason to pay again. A paid update_policy is finished the same way: its receipt is collected on the update route, and "written" then means the policy was updated. "refused" or "unavailable" saying NOTHING WAS COLLECTED means a self-paid write for this policy may still land, or another write for it is in progress in this wallet: the payment is not lost, so do not pay again; ask again shortly. If this wallet holds no record of the receipt, that does NOT mean the write failed: the service completes a paid write on its own, and asking to write the same policyKey again will report the truth for free before any payment.',
49333
49813
  inputSchema: {
49334
49814
  type: "object",
49335
49815
  properties: {
@@ -49418,7 +49898,7 @@ function buildToolList() {
49418
49898
  },
49419
49899
  {
49420
49900
  name: "subscribe_and_issue",
49421
- description: "Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a spending policy refuses the fee, the result has `policyDenied: true` (a decision: do not retry) or `policyCheckUnavailable: true` (transient: trying again shortly is right); either way nothing was paid and no credential was issued. If a still-valid credential for this templateId is already cached locally, it is returned directly with no payment (fromCache: true). Setting forceReissue:true alone does NOT pay again \u2014 it only asks to be shown that existing credential first: you get back { issued: false, vcId, vc, fromCache: true, reason } instead, and nothing is paid. Show that existing credential to the user and ask whether they actually want to replace it; only call this tool again, with BOTH forceReissue:true and confirmReplaceExistingVc set to exactly the vcId you were shown, if they explicitly say yes. Never pass confirmReplaceExistingVc on your own judgement. When nothing valid is cached, forceReissue has no effect either way \u2014 that case has always paid and issued fresh. When a call DOES render a full credential to the user (a fresh issuance, a plain cache hit, or a confirmed replace), show every claim under vc.credentialSubject as a table \u2014 not a cherry-picked subset \u2014 labelling each key by title-casing it (the exact claim set varies by template; use schema.required/optional to know what to expect), plus vcId and validUntil if present. If vcPassImagePaths is also present, the credential's own pass-design image(s) came back attached to this result \u2014 tell the user their credential's official pass design is shown below and display the image(s); do not silently drop them from your summary just because they are not text. Payment is asset-agnostic \u2014 MBI's 402 challenge may quote the native ZETRIX token or a ZTP20 token (e.g. JMYR); pass dryRun:true first to see the quoted asset/amount for free before committing to pay. What a call actually cost is reported precisely: paidAsset/amountPaid are set ONLY when this call paid, a cache hit reports the earlier charge under originalPayment instead (never as amountPaid, so summing spend cannot double-count), and any failure after the payment has settled on chain reports paymentAttempted: { asset, amount, paymentId }. Two such failures exist and mean different things: MBI 4006 is a definitive facilitator rejection, while 4012 (HTTP 502) means the outcome is INDETERMINATE \u2014 the payment may well have landed. On 4012 the wallet automatically polls MBI's recovery endpoint and reports recovery: { status, txHash?, vcId?, polls }, where status is ISSUED (the credential exists after all \u2014 fetch it by vcId, since recovery returns no VC body), FAILED, or REQUIRED/SETTLED (still unresolved) / UNKNOWN (recovery itself unreachable). NEVER retry a payment after either failure: the funds may already be gone, and a retry charges the full amount again \u2014 look the paymentId up instead. Every response also includes { schema: { required, optional } } whenever the template schema could be read from chain \u2014 the template's full declared attribute list, so you see the complete field list, not just what went wrong. For the AI Birthcert specifically: this issues the BASIC one \u2014 self-declared by the agent, agent-paid via x402, owner identity NOT identity-verified. If the user asked for a \"verified\" AI birthcert (owner identity confirmed via MyDigital ID), use request_ai_birthcert_verification instead \u2014 this tool cannot produce that credential.",
49901
+ description: "Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a spending policy refuses the fee, the result has `policyDenied: true` (a decision: do not retry) or `policyCheckUnavailable: true` (transient: trying again shortly is right); either way nothing was paid and no credential was issued. If a still-valid credential for this templateId is already cached locally, it is returned directly with no payment (fromCache: true). Setting forceReissue:true alone does NOT pay again \u2014 it only asks to be shown that existing credential first: you get back { issued: false, vcId, vc, fromCache: true, reason } instead, and nothing is paid. Show that existing credential to the user and ask whether they actually want to replace it; only call this tool again, with BOTH forceReissue:true and confirmReplaceExistingVc set to exactly the vcId you were shown, if they explicitly say yes. Never pass confirmReplaceExistingVc on your own judgement. When nothing valid is cached, forceReissue has no effect either way \u2014 that case has always paid and issued fresh. When a call DOES render a full credential to the user (a fresh issuance, a plain cache hit, or a confirmed replace), show every claim under vc.credentialSubject as a table \u2014 not a cherry-picked subset \u2014 labelling each key by title-casing it (the exact claim set varies by template; use schema.required/optional to know what to expect), plus vcId and validUntil if present. If vcPassImagePaths is also present, the credential's own pass-design image(s) came back attached to this result \u2014 tell the user their credential's official pass design is shown below and display the image(s); do not silently drop them from your summary just because they are not text. Payment is asset-agnostic \u2014 MBI's 402 challenge may quote the native ZETRIX token or a ZTP20 token (e.g. JMYR); pass dryRun:true first to see the quoted asset/amount for free before committing to pay. What a call actually cost is reported precisely: paidAsset/amountPaid are set ONLY when this call paid, a cache hit reports the earlier charge under originalPayment instead (never as amountPaid, so summing spend cannot double-count), and any failure after the payment has settled on chain reports paymentAttempted: { asset, amount, paymentId }. Two such failures exist and mean different things: MBI 4006 is a definitive facilitator rejection, while 4012 (HTTP 502) means the outcome is INDETERMINATE \u2014 the payment may well have landed. On 4012 the wallet automatically polls MBI's recovery endpoint and reports recovery: { status, txHash?, vcId?, polls }, where status is ISSUED (the credential exists after all \u2014 fetch it by vcId, since recovery returns no VC body), FAILED, or REQUIRED/SETTLED (still unresolved) / UNKNOWN (recovery itself unreachable). NEVER retry a payment after either failure: the funds may already be gone, and a retry charges the full amount again \u2014 look the paymentId up instead. Every response also includes { schema: { required, optional } } whenever the template schema could be read from chain \u2014 the template's full declared attribute list, so you see the complete field list, not just what went wrong. For the AI Birthcert specifically: this issues the BASIC one \u2014 self-declared by the agent, agent-paid via x402, owner identity NOT identity-verified. If the user asked for a \"verified\" AI birthcert (owner identity confirmed via MyDigital ID), use request_ai_birthcert_verification instead \u2014 this tool cannot produce that credential. If the user did not say which kind, ask first: see the `choice` note in credential_preflight.",
49422
49902
  inputSchema: {
49423
49903
  type: "object",
49424
49904
  properties: {
@@ -49428,12 +49908,12 @@ function buildToolList() {
49428
49908
  },
49429
49909
  attributes: {
49430
49910
  type: "object",
49431
- description: `Claim values for the credential (schema varies by template \u2014 check what the issuer requires before guessing). ALWAYS ask the human owner directly for any name/identifier-type value (e.g. an agentUsername or similar field) \u2014 NEVER invent, guess, or silently reuse one of your own choosing; if the template requires one, get it from the user first. "agentDid" is the one exception: it does not need to be supplied, since it is auto-filled with this wallet's own holder DID (the credential's self-referential subject) unless you explicitly override it.`
49911
+ description: `Claim values for the credential (schema varies by template \u2014 check what the issuer requires before guessing). ALWAYS ask the human owner directly for any name/identifier-type value (e.g. an agentUsername or similar field) \u2014 NEVER invent, guess, or silently reuse one of your own choosing; if the template requires one, get it from the user first. When the user answers with a name \u2014 even a short phrase, even one that repeats words from your question \u2014 that IS the name: use it as given and do not ask for it again, unless it was refused (for example already in use). Tidying it (spaces or capitals, say) is optional and never needed; if you do want to suggest a tidier form, ask and WAIT for their answer, and use the tidied form only after a yes. Never go ahead on a name, or on a payment, that the user has not agreed to. Any example you give is only an example of the format, not a suggestion to use. "agentDid" is the one exception: it does not need to be supplied, since it is auto-filled with this wallet's own holder DID (the credential's self-referential subject) unless you explicitly override it.`
49432
49912
  },
49433
49913
  expirationDate: { type: "string" },
49434
49914
  dryRun: {
49435
49915
  type: "boolean",
49436
- description: "Price the credential without paying, signing, or issuing anything. Returns { quote: { asset, maxAmountRequired, payTo, gasModel, paymentRequired? }, schema: { required, optional } }. Priced via MBI's /quote endpoint, which cannot issue \u2014 so this is safe even on a template that issues for free (such a template mints synchronously on the real path, which is why pricing never goes through it). Never writes the VC cache, so it cannot displace a credential you already hold. `quote.paymentRequired` is the authoritative free-vs-paid answer: false means issuance is currently free and `maxAmountRequired` will not be charged. When the field is ABSENT the MBI predates it and the amount is unconfirmed \u2014 report it as a possible charge, never as certainly free. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
49916
+ description: "Price the credential without paying, signing, or issuing anything. Returns { quote: { asset, maxAmountRequired, payTo, gasModel, paymentRequired? }, schema: { required, optional } }. Priced via MBI's /quote endpoint, which cannot issue \u2014 so this is safe even on a template that issues for free (such a template mints synchronously on the real path, which is why pricing never goes through it). Never writes the VC cache, so it cannot displace a credential you already hold. `quote.paymentRequired` is the authoritative free-vs-paid answer: false means issuance is currently free and `maxAmountRequired` will not be charged. When the field is ABSENT the MBI predates it and the amount is unconfirmed \u2014 report it as a possible charge, never as certainly free. Tell the user in plain words that it is free or chargeable; never quote JSON field names such as paymentRequired to them. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
49437
49917
  },
49438
49918
  forceReissue: {
49439
49919
  type: "boolean",
@@ -49449,13 +49929,13 @@ function buildToolList() {
49449
49929
  },
49450
49930
  {
49451
49931
  name: "request_ai_birthcert_verification",
49452
- description: 'Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). ALWAYS run credential_preflight for "verified_ai_birthcert" immediately before calling this, even if you checked earlier in the conversation \u2014 preflight is free, this tool spends real funds, and a balance the user topped up a minute ago is not the balance you read before that. If a spending policy refuses the session fee, the result is { error } with `policyDenied: true` (a decision: do not retry) or `policyCheckUnavailable: true` (transient: trying again shortly is right); either way nothing was paid and no session was created. Returns { sessionId, verificationUrl, expiresAt, expiresIn, expiresInSeconds, message } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Tell them how long the link is good for by quoting `expiresIn` exactly as given: the wallet works it out from its own clock, so never calculate the time remaining yourself from `expiresAt` \u2014 your clock and timezone may differ from the server\'s. `message` here contains a "Tell the user:" sentence about following up \u2014 relay it rather than composing your own reassurance. Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. Nothing keeps running after this call returns \u2014 there is no background polling, webhook, or timer, so you can only check status when the user sends you a new message. NEVER tell the user you have "set up an automation" or that you will "alert them" when it completes \u2014 you cannot act on your own between messages, and promising that leaves them waiting for a notification that will never come. Instead, tell them how long the link is good for and ask them to message you back once done (or after a few minutes) so you can check_ai_birthcert_verification again. ALWAYS ask the human owner directly what agentName they want to use for a NEW session \u2014 NEVER invent, guess, or silently choose one yourself. (Calling again with the SAME name to resume an already-pending session, as described below, is not "inventing" one \u2014 that is reusing the exact name the owner already gave.) IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the same credential-issuance payment cap as subscribe_and_issue, which by default allows exactly 1 JMYR per call. Set MAX_PAYMENT_AMOUNT to override it; a spending policy that governs the asset replaces the default cap. It can return { error: "..." } instead of a session. Read `message` before deciding what to tell the user or whether retrying is safe \u2014 it does NOT always mean nothing was paid. For insufficient funds or a payment-cap block specifically, nothing is created and nothing was paid, so once the underlying problem is fixed (e.g. the user tops up), calling this tool again is the right next step. Several OTHER { error } shapes mean the opposite: a payment may already have been sent on a prior or even this call (blob_already_settled, a receipt that could not be saved locally, RECEIPT VOID, OUTCOME UNKNOWN) \u2014 for any of those, calling this tool again pays the fee AGAIN, so do NOT retry without explicit agreement from the user, exactly as `message` itself will say. It can also return { settlementPending: true, paymentReceipt, message } instead \u2014 a payment WAS SENT and no session exists yet. That is NOT a failure and NOT an error, but do NOT pay again and do NOT call this tool again to retry it \u2014 call check_ai_birthcert_verification to follow it through instead. It covers three different situations, told apart by the `issuerRejected` and `paymentInvalid` fields. With neither set, do NOT assume the payment is known to have succeeded \u2014 this shape covers TWO different states that look identical from these fields alone: usually the settlement is genuinely still clearing (the message says "still being processed"), but sometimes the outcome could not be determined at all yet (the message says "has not been confirmed yet" and does not say "still being processed") \u2014 an indeterminate state, not a confirmed one, even though neither flag is set. Either way the receipt is saved and you must not pay again, so report it as "payment sent, still settling" without promising the user it definitely succeeded. When the message contains a "Tell the user:" sentence, relay that sentence rather than composing your own reassurance, and if asked what the service said, quote only what the message says it answered. With issuerRejected: true the credential service was reached and refused the request \u2014 the settlement is NOT what failed, but do NOT describe this as the payment having succeeded either: `message` quotes its reason, and you must relay that reason rather than describing it as still settling or as a success. With paymentInvalid: true the service has specifically ruled the payment or receipt invalid \u2014 do NOT say "the settlement is not what failed" for this one, since this verdict IS about the payment; relay what it said instead, and do NOT describe it as a success either. Neither this case nor issuerRejected tells you whether the fee was taken, in either direction \u2014 never tell the user they were not charged, and just as much, never tell them they WERE charged either. That no-claim rule covers issuerRejected and paymentInvalid specifically. The plain no-flag case above is different again, and NOT simply "known to have succeeded" either \u2014 see the note on it above: it is two states, only one of which is confirmed, so read `message` there too rather than assuming success from the shape alone. If the user did NOT ask for a "verified" credential specifically, they most likely want the self-declared, non-verified Basic AI Birthcert instead \u2014 use subscribe_and_issue for that. BEFORE this pays or starts a session, it checks whether this holder already has a Verified AI Birthcert \u2014 found in the local cache, or (if the cache has not seen it yet) resolved from a previously-issued session. If one exists and is STILL VALID, nothing is paid and no session is started \u2014 you get back { existingVerifiedVc: { vcId, validUntil }, message } instead. Show that existing credential to the user and ask whether they actually want to replace it; only call this tool again, with confirmReplaceExistingVc set to exactly existingVerifiedVc.vcId, if they explicitly say yes. Never pass confirmReplaceExistingVc on your own judgement. If the existing VC has already EXPIRED, this proceeds automatically \u2014 no confirmation needed \u2014 and the result carries replacedExpiredVc: { vcId, validUntil } naming the one it replaced. That field can appear on a settled session, on a payment still settling, or on an EARLIER call\'s still-pending session simply being returned unchanged \u2014 so treat it only as "the holder\'s old VC had expired", never as proof that THIS call itself just spent money; read the rest of the result (an error, a settled session, or settlementPending) to know what this call actually did. It can also return { error } specifically because an already-issued VC exists but could not be confirmed as valid or expired (a resolution problem, not a payment problem) \u2014 nothing is paid on that path either; relay `error` as given, since it names the concrete next step (retry check_ai_birthcert_verification, or as a last resort clear_stuck_payment_receipt with the user\'s explicit agreement).',
49932
+ description: 'Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). ALWAYS run credential_preflight for "verified_ai_birthcert" immediately before calling this, even if you checked earlier in the conversation \u2014 preflight is free, this tool spends real funds, and a balance the user topped up a minute ago is not the balance you read before that. If a spending policy refuses the session fee, the result is { error } with `policyDenied: true` (a decision: do not retry) or `policyCheckUnavailable: true` (transient: trying again shortly is right); either way nothing was paid and no session was created. Returns { sessionId, verificationUrl, expiresAt, expiresIn, expiresInSeconds, message } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Tell them how long the link is good for by quoting `expiresIn` exactly as given: the wallet works it out from its own clock, so never calculate the time remaining yourself from `expiresAt` \u2014 your clock and timezone may differ from the server\'s. `message` here contains a "Tell the user:" sentence about following up \u2014 relay it rather than composing your own reassurance. Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. Nothing keeps running after this call returns \u2014 there is no background polling, webhook, or timer, so you can only check status when the user sends you a new message. NEVER tell the user you have "set up an automation" or that you will "alert them" when it completes \u2014 you cannot act on your own between messages, and promising that leaves them waiting for a notification that will never come. Instead, tell them how long the link is good for and ask them to message you back once done (or after a few minutes) so you can check_ai_birthcert_verification again. ALWAYS ask the human owner directly what agentName they want to use for a NEW session \u2014 NEVER invent, guess, or silently choose one yourself. (Calling again with the SAME name to resume an already-pending session, as described below, is not "inventing" one \u2014 that is reusing the exact name the owner already gave.) IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the same credential-issuance payment cap as subscribe_and_issue, which by default allows exactly 1 JMYR per call. Set MAX_PAYMENT_AMOUNT to override it; a spending policy that governs the asset replaces the default cap. It can return { error: "..." } instead of a session. Read `message` before deciding what to tell the user or whether retrying is safe \u2014 it does NOT always mean nothing was paid. For insufficient funds or a payment-cap block specifically, nothing is created and nothing was paid, so once the underlying problem is fixed (e.g. the user tops up), calling this tool again is the right next step. Several OTHER { error } shapes mean the opposite: a payment may already have been sent on a prior or even this call (blob_already_settled, a receipt that could not be saved locally, RECEIPT VOID, OUTCOME UNKNOWN) \u2014 for any of those, calling this tool again pays the fee AGAIN, so do NOT retry without explicit agreement from the user, exactly as `message` itself will say. It can also return { settlementPending: true, paymentReceipt, message } instead \u2014 a payment WAS SENT and no session exists yet. That is NOT a failure and NOT an error, but do NOT pay again and do NOT call this tool again to retry it \u2014 call check_ai_birthcert_verification to follow it through instead. It covers three different situations, told apart by the `issuerRejected` and `paymentInvalid` fields. With neither set, do NOT assume the payment is known to have succeeded \u2014 this shape covers TWO different states that look identical from these fields alone: usually the settlement is genuinely still clearing (the message says "still being processed"), but sometimes the outcome could not be determined at all yet (the message says "has not been confirmed yet" and does not say "still being processed") \u2014 an indeterminate state, not a confirmed one, even though neither flag is set. Either way the receipt is saved and you must not pay again, so report it as "payment sent, still settling" without promising the user it definitely succeeded. When the message contains a "Tell the user:" sentence, relay that sentence rather than composing your own reassurance, and if asked what the service said, quote only what the message says it answered. With issuerRejected: true the credential service was reached and refused the request \u2014 the settlement is NOT what failed, but do NOT describe this as the payment having succeeded either: `message` quotes its reason, and you must relay that reason rather than describing it as still settling or as a success. With paymentInvalid: true the service has specifically ruled the payment or receipt invalid \u2014 do NOT say "the settlement is not what failed" for this one, since this verdict IS about the payment; relay what it said instead, and do NOT describe it as a success either. Neither this case nor issuerRejected tells you whether the fee was taken, in either direction \u2014 never tell the user they were not charged, and just as much, never tell them they WERE charged either. That no-claim rule covers issuerRejected and paymentInvalid specifically. The plain no-flag case above is different again, and NOT simply "known to have succeeded" either \u2014 see the note on it above: it is two states, only one of which is confirmed, so read `message` there too rather than assuming success from the shape alone. If the request does not say which kind of AI Birthcert the user wants, do not start this: run credential_preflight for both kinds and ask which they prefer (see its `choice` note), and use this tool only for the Verified kind; if they want the self-declared, non-verified Basic one, use subscribe_and_issue for that. BEFORE this pays or starts a session, it checks whether this holder already has a Verified AI Birthcert \u2014 found in the local cache, or (if the cache has not seen it yet) resolved from a previously-issued session. If one exists and is STILL VALID, nothing is paid and no session is started \u2014 you get back { existingVerifiedVc: { vcId, validUntil }, message } instead. Show that existing credential to the user and ask whether they actually want to replace it; only call this tool again, with confirmReplaceExistingVc set to exactly existingVerifiedVc.vcId, if they explicitly say yes. Never pass confirmReplaceExistingVc on your own judgement. If the existing VC has already EXPIRED, this proceeds automatically \u2014 no confirmation needed \u2014 and the result carries replacedExpiredVc: { vcId, validUntil } naming the one it replaced. That field can appear on a settled session, on a payment still settling, or on an EARLIER call\'s still-pending session simply being returned unchanged \u2014 so treat it only as "the holder\'s old VC had expired", never as proof that THIS call itself just spent money; read the rest of the result (an error, a settled session, or settlementPending) to know what this call actually did. It can also return { error } specifically because an already-issued VC exists but could not be confirmed as valid or expired (a resolution problem, not a payment problem) \u2014 nothing is paid on that path either; relay `error` as given, since it names the concrete next step (retry check_ai_birthcert_verification, or as a last resort clear_stuck_payment_receipt with the user\'s explicit agreement).',
49453
49933
  inputSchema: {
49454
49934
  type: "object",
49455
49935
  properties: {
49456
49936
  agentName: {
49457
49937
  type: "string",
49458
- description: "A unique, human-readable name for this agent. Must not already be in use for a Verified AI Birthcert, or issuance will fail. ALWAYS ask the human owner directly which name to use for a new session \u2014 NEVER invent, guess, or silently choose one yourself."
49938
+ description: "A unique, human-readable name for this agent. Must not already be in use for a Verified AI Birthcert, or issuance will fail. ALWAYS ask the human owner directly which name to use for a new session \u2014 NEVER invent, guess, or silently choose one yourself. When the user answers with a name \u2014 even a short phrase, even one that repeats words from your question \u2014 that IS the name: use it as given and do not ask for it again, unless it was refused (for example already in use). Tidying it (spaces or capitals, say) is optional and never needed; if you do want to suggest a tidier form, ask and WAIT for their answer, and use the tidied form only after a yes. Never go ahead on a name, or on a payment, that the user has not agreed to. Any example you give is only an example of the format, not a suggestion to use."
49459
49939
  },
49460
49940
  agentPurpose: { type: "string", description: 'Optional \u2014 what this agent does, e.g. "Negotiate and settle supplier invoices".' },
49461
49941
  evidenceAssuranceLevel: { type: "string", description: 'Optional \u2014 assurance level of the identity evidence, e.g. "high".' },
@@ -49484,13 +49964,13 @@ function buildToolList() {
49484
49964
  },
49485
49965
  {
49486
49966
  name: "credential_preflight",
49487
- description: "FREE readiness check \u2014 call this FIRST, before collecting ANY application detail from the user, whenever they ask for a credential. Spends nothing, signs no transaction, creates no session. Returns { ready, fee, balances, cap, schema?, alreadyHeld?, blockers, notChecked }: the live fee and which side pays gas, the balances that matter, whether the spending limit permits it, and \u2014 for a template credential \u2014 the attributes it requires. `blockers` lists EVERY reason it is not ready at once (a low balance and a too-low spending limit are different problems and both appear together), so one round of fixes is enough rather than discovering them one failed payment at a time. ALWAYS relay `notChecked` too: a clean result is not a guarantee. In particular it does NOT check whether an agent name is free \u2014 myid decides that at issuance, after payment. Report `fee.display` and each balance's `display` to the user, never the raw base-unit numbers. `fee.paymentRequired: false` means issuance is currently FREE \u2014 `fee.display` is then only what it WOULD cost if payment were switched back on, so do not ask the user to fund it and do not present that amount as a charge. Gas is separate and can still block a free credential. When the field is ABSENT the cost is unknown (an older MBI), and it is treated as chargeable \u2014 never report absent as free. If the result has `alreadyHeld`, this wallet already holds a valid copy of that credential: tell the user so, name it, and stop \u2014 do not ask for an agent name or any other detail, and do not begin an issuance. A new one would replace it. Only when the user has explicitly said they want a replacement, run this check with replacing: true.",
49967
+ description: "FREE readiness check \u2014 call this FIRST, before collecting ANY application detail from the user, whenever they ask for a credential. Spends nothing, signs no transaction, creates no session. Returns { ready, fee, balances, cap, schema?, alreadyHeld?, blockers, notChecked }: the live fee and which side pays gas, the balances that matter, whether the spending limit permits it, and \u2014 for a template credential \u2014 the attributes it requires. `blockers` lists EVERY reason it is not ready at once (a low balance and a too-low spending limit are different problems and both appear together), so one round of fixes is enough rather than discovering them one failed payment at a time. ALWAYS relay `notChecked` too: a clean result is not a guarantee. In particular it does NOT check whether an agent name is free \u2014 myid decides that at issuance, after payment. Report `fee.display` and each balance's `display` to the user, never the raw base-unit numbers. `fee.paymentRequired: false` means issuance is currently FREE \u2014 `fee.display` is then only what it WOULD cost if payment were switched back on, so do not ask the user to fund it and do not present that amount as a charge. Tell the user in plain words that it is free or chargeable; never quote JSON field names such as paymentRequired to them. A free credential sends no transaction, so no gas is spent and a missing ZTX balance is not a blocker on it: never ask the user to fund ZTX for a free credential, and if notChecked mentions ZTX, pass it on as a note only. When the field is ABSENT the cost is unknown (an older MBI), and it is treated as chargeable \u2014 never report absent as free. If the result has `alreadyHeld`, this wallet already holds a valid copy of that credential: tell the user so, name it, and stop \u2014 do not ask for an agent name or any other detail, and do not begin an issuance. A new one would replace it. That stop applies to that credential only: the `choice` note still applies to the other kind. Only when the user has explicitly said they want a replacement, run this check with replacing: true. When a user asks for an AI Birthcert and the request does not say which kind they want (Basic, which is self-declared, or Verified, through MyDigital ID), do not pick one: follow the `choice` note in the result, which is to run this check for both kinds, tell the user the difference in plain words, and ask which they prefer.",
49488
49968
  inputSchema: {
49489
49969
  type: "object",
49490
49970
  properties: {
49491
49971
  credential: {
49492
49972
  type: "string",
49493
- description: 'Which credential to price: "verified_ai_birthcert" for the MyDigital-ID-verified AI Birthcert, or a template id (did:zid:...) / known template name (e.g. "AI Birthcert") for a template-issued one.'
49973
+ description: 'Which credential to price: "verified_ai_birthcert" for the VERIFIED (MyDigital-ID-verified) AI Birthcert, or a template id (did:zid:...) / known template name for a template-issued one \u2014 "AI Birthcert" is the BASIC, self-declared one. For the Verified one use exactly "verified_ai_birthcert": any name containing "birthcert" is priced as the Basic one.'
49494
49974
  },
49495
49975
  agentName: {
49496
49976
  type: "string",
@@ -49566,10 +50046,10 @@ function buildPolicyWriteDeps(input) {
49566
50046
  const templateContract = input.policyTemplateAddress;
49567
50047
  return {
49568
50048
  policyWriteDeps: {
49569
- ...input.policyWriteUrl ? { client: new PolicyWriteClient(input.policyWriteUrl, (url, init) => fetch(url, init)) } : {},
50049
+ ...input.policyWriteUrl ? { client: new PolicyWriteClient(input.policyWriteUrl, (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(POLICY_HTTP_TIMEOUT_MS) })) } : {},
49570
50050
  // A self-paid write has no service to catch what the attribute vocabulary would have, so it needs a vocabulary to check against.
49571
50051
  hasVocabularySource: Boolean(input.readVocabulary),
49572
- receipts: createFsPolicyWriteReceiptStore((0, import_node_path8.join)(input.stateDir, "policy-write-receipts")),
50052
+ receipts: createFsPolicyWriteReceiptStore((0, import_node_path10.join)(input.stateDir, "policy-write-receipts")),
49573
50053
  // The one narrowing left, and it is here rather than at the call site so the call site
49574
50054
  // cannot pass something else entirely. The orchestrator describes an accept as an opaque
49575
50055
  // record because it never reads one; the payer wants the SDK's type.
@@ -49590,7 +50070,10 @@ function buildPolicyWriteDeps(input) {
49590
50070
  selfPay: createSelfPay({
49591
50071
  registryAddress: input.policyRegistryAddress,
49592
50072
  ownerAddress: input.ownerAddress,
49593
- ...input.selfPayChain
50073
+ ...input.selfPayChain,
50074
+ // One note per (owner, policy key), written before a send and kept until its outcome is settled.
50075
+ // Scoped by network: the same key gives the same address on testnet and mainnet, and a testnet note must not block a mainnet write.
50076
+ markers: createFsSelfPayMarkerStore((0, import_node_path10.join)(input.stateDir, "self-pay-markers", input.network.replace(/[^A-Za-z0-9_-]/g, "-")))
49594
50077
  })
49595
50078
  } : {},
49596
50079
  checkAffordability: (accept) => checkAffordability({ queryBalance: input.queryBalance, caps: input.caps, policyGoverns: input.policyGoverns }, accept),
@@ -49712,8 +50195,8 @@ function asPayRequest(accept) {
49712
50195
  }
49713
50196
  async function main() {
49714
50197
  const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
49715
- const stateDir = (process.env.ZETRIX_WALLET_STATE_DIR ?? fileEnv.ZETRIX_WALLET_STATE_DIR ?? (0, import_node_path8.join)((0, import_node_os2.homedir)(), ".agentic-wallet-mcp")).replace(/\/+$/, "");
49716
- const accountStore = createFsAccountStore((0, import_node_path8.join)(stateDir, "account.json"));
50198
+ const stateDir = (process.env.ZETRIX_WALLET_STATE_DIR ?? fileEnv.ZETRIX_WALLET_STATE_DIR ?? (0, import_node_path10.join)((0, import_node_os2.homedir)(), ".agentic-wallet-mcp")).replace(/\/+$/, "");
50199
+ const accountStore = createFsAccountStore((0, import_node_path10.join)(stateDir, "account.json"));
49717
50200
  if (process.argv[2] === "export-credentials") {
49718
50201
  const { exitCode } = await exportCredentials({
49719
50202
  getAccount: () => accountStore.get(),
@@ -49733,7 +50216,7 @@ async function main() {
49733
50216
  const config2 = loadConfig(env);
49734
50217
  const hsmPassword = config2.hsmPassword;
49735
50218
  const be = new WalletBeClient(config2.walletBeUrl);
49736
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
50219
+ const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
49737
50220
  const { zetrixAddress, holderDid, publicKeyHex, created, didMismatch, activated } = await resolveHolder(
49738
50221
  {
49739
50222
  createAccount: (password) => be.createAccount(password),
@@ -49786,8 +50269,8 @@ async function main() {
49786
50269
  const nodeBaseUrl = `https://${config2.nodeHost}${config2.nodePort ? `:${config2.nodePort}` : ""}`;
49787
50270
  const nodeMetaQuery = (url) => fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
49788
50271
  const resolveTemplateFields = (templateId) => fetchTemplateFields(templateId, config2.templateRegistryAddress, nodeBaseUrl, nodeMetaQuery);
49789
- const cacheScope = (0, import_node_crypto10.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
49790
- const vcCache = createFsVcCache((0, import_node_path8.join)(config2.stateDir, "vc-cache", cacheScope));
50272
+ const cacheScope = (0, import_node_crypto13.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
50273
+ const vcCache = createFsVcCache((0, import_node_path10.join)(config2.stateDir, "vc-cache", cacheScope));
49791
50274
  const mbi = new MbiClient(config2.mbiBaseUrl);
49792
50275
  const messageSigner = (message) => be.signMessage(message, zetrixAddress, hsmPassword);
49793
50276
  const formatAssetAmount = (asset, raw) => describeAssetAmount(asset, raw, contractQuery);
@@ -49839,10 +50322,12 @@ async function main() {
49839
50322
  }
49840
50323
  };
49841
50324
  const { pay, payForCredential, payForPolicyWrite, preflightCaps, policyWriteCaps } = buildPayers(config2, makePay);
49842
- const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path8.join)(config2.stateDir, "ssivc-download-quarantine"));
49843
- const passImagesDir = (0, import_node_path8.join)(config2.stateDir, "vc-pass-images");
50325
+ const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path10.join)(config2.stateDir, "ssivc-download-quarantine"));
50326
+ const passImagesDir = (0, import_node_path10.join)(config2.stateDir, "vc-pass-images");
50327
+ const verificationQrFiles = createFsQrFileStore((0, import_node_path10.join)(config2.stateDir, "verification-qr"));
50328
+ void verificationQrFiles.sweep().catch(() => void 0);
49844
50329
  const verifyAiBirthcert = config2.ssivcBaseUrl ? (() => {
49845
- const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path8.join)(config2.stateDir, "ssivc-session.json"));
50330
+ const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path10.join)(config2.stateDir, "ssivc-session.json"));
49846
50331
  const ssivc = new SsivcClient(config2.ssivcBaseUrl);
49847
50332
  const verifyAiBirthcertDeps = {
49848
50333
  ssivc,
@@ -49979,6 +50464,16 @@ async function main() {
49979
50464
  // The same submit wrapper transfer_token uses, so a node refusal is told apart from an answer that never came.
49980
50465
  submit: transferDeps.submit,
49981
50466
  queryNativeBalance: () => queryTokenBalance2("ZTX"),
50467
+ // The current ledger, so a self-paid transaction can carry an expiry. `undefined` when it cannot be read.
50468
+ currentLedger: async () => {
50469
+ try {
50470
+ const r = await sdk.block.getNumber();
50471
+ const n = r.errorCode === 0 ? r.result?.header?.blockNumber : void 0;
50472
+ return n === void 0 ? void 0 : String(n);
50473
+ } catch {
50474
+ return void 0;
50475
+ }
50476
+ },
49982
50477
  // What became of a submitted transaction, so one that was included and then rejected by the contract (the gas is still spent)
49983
50478
  // is reported as that.
49984
50479
  getTransaction: buildTransactionLookup((hash) => sdk.transaction.getInfo(hash))
@@ -50006,7 +50501,9 @@ async function main() {
50006
50501
  signHexBlob: walletBeSignerFn,
50007
50502
  signMessage: messageSigner,
50008
50503
  linkTemplate: config2.myidVerifyLinkTemplate,
50009
- renderQr: renderQrPng
50504
+ renderQr: renderQrPng,
50505
+ // A chat channel (WhatsApp) sends files, not MCP image blocks: without a file the agent has no QR to attach.
50506
+ saveQr: (png, referenceId, expiresAtMs) => verificationQrFiles.save(png, referenceId, expiresAtMs)
50010
50507
  }),
50011
50508
  verifyAiBirthcert,
50012
50509
  // So credential_preflight can say "you already have one" before the agent asks the user for anything.
@@ -50025,7 +50522,7 @@ async function main() {
50025
50522
  if (!fn) throw new Error(`Unknown tool: ${name}`);
50026
50523
  try {
50027
50524
  const result = await fn(args ?? {});
50028
- return { content: await buildToolContent(result, (p) => (0, import_promises7.readFile)(p)) };
50525
+ return { content: await buildToolContent(result, (p) => (0, import_promises10.readFile)(p)) };
50029
50526
  } catch (err) {
50030
50527
  const chain = [];
50031
50528
  let e = err;